├── src
├── api
│ ├── home.js
│ ├── base.js
│ ├── index.js
│ └── fetchAPI.js
├── routes
│ ├── index.jsx
│ └── Home
│ │ ├── models
│ │ ├── index.js
│ │ └── PostModel.js
│ │ ├── components
│ │ ├── Assets
│ │ │ ├── mobx.png
│ │ │ ├── react.png
│ │ │ ├── webpack.png
│ │ │ ├── reactrouter.png
│ │ │ ├── GitHub-Mark-32px.png
│ │ │ ├── GitHub-Mark-Light-32px.png
│ │ │ └── react_logo.svg
│ │ ├── ArticlePage
│ │ │ ├── style.less
│ │ │ └── index.jsx
│ │ ├── Protected
│ │ │ └── index.jsx
│ │ ├── PostsPage
│ │ │ ├── index.jsx
│ │ │ └── style.less
│ │ └── WelcomePage
│ │ │ ├── index.jsx
│ │ │ └── style.less
│ │ ├── service
│ │ └── index.js
│ │ ├── index.jsx
│ │ ├── container
│ │ ├── index.jsx
│ │ ├── Posts.jsx
│ │ └── Article.jsx
│ │ └── modules
│ │ └── HomeStore.js
├── static
│ ├── img
│ │ ├── 404.png
│ │ └── cloud_404.png
│ └── css
│ │ ├── common.less
│ │ └── normalize.less
├── constants
│ └── env.js
├── utils
│ └── index.js
├── components
│ ├── TopNav
│ │ ├── style.less
│ │ ├── ActiveLink.jsx
│ │ └── index.jsx
│ ├── TopBar
│ │ ├── style.less
│ │ ├── Button.jsx
│ │ └── index.jsx
│ ├── FootBar
│ │ └── index.jsx
│ ├── LazyLoader
│ │ └── index.jsx
│ └── NotFound
│ │ ├── index.jsx
│ │ └── style.less
├── store
│ ├── index.js
│ └── RouterStore.js
├── index.html
├── coreLayout
│ └── index.jsx
└── index.jsx
├── .gitignore
├── public
├── favicon.ico
└── lib
│ ├── min
│ └── manifest.json
│ └── debug
│ └── manifest.json
├── .prettierrc
├── postcss.config.js
├── .babelrc
├── webpack.dll.lib.js
├── .eslintrc
├── package.json
├── webpack.config.production.js
├── webpack.config.js
└── README.md
/src/api/home.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/routes/index.jsx:
--------------------------------------------------------------------------------
1 | import baseRoute from './Home';
2 |
3 | export default [...baseRoute];
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 | .env
4 | .DS_Store
5 | npm-debug.log
6 | .idea/
7 | output/
8 |
--------------------------------------------------------------------------------
/src/routes/Home/models/index.js:
--------------------------------------------------------------------------------
1 | import PostModel from './PostModel';
2 |
3 | export { PostModel };
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clojurie/react-mobx-react-router4-boilerplate/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/api/base.js:
--------------------------------------------------------------------------------
1 | export default {
2 | getPostList: {
3 | method: 'GET',
4 | url: '/posts/'
5 | }
6 | };
7 |
--------------------------------------------------------------------------------
/src/static/img/404.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clojurie/react-mobx-react-router4-boilerplate/HEAD/src/static/img/404.png
--------------------------------------------------------------------------------
/src/static/img/cloud_404.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clojurie/react-mobx-react-router4-boilerplate/HEAD/src/static/img/cloud_404.png
--------------------------------------------------------------------------------
/src/routes/Home/components/Assets/mobx.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clojurie/react-mobx-react-router4-boilerplate/HEAD/src/routes/Home/components/Assets/mobx.png
--------------------------------------------------------------------------------
/src/routes/Home/components/Assets/react.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clojurie/react-mobx-react-router4-boilerplate/HEAD/src/routes/Home/components/Assets/react.png
--------------------------------------------------------------------------------
/src/routes/Home/components/Assets/webpack.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clojurie/react-mobx-react-router4-boilerplate/HEAD/src/routes/Home/components/Assets/webpack.png
--------------------------------------------------------------------------------
/src/constants/env.js:
--------------------------------------------------------------------------------
1 | const isProduction = process.env.NODE_ENV === 'production';
2 | const isDevelopment = !isProduction;
3 |
4 | export { isProduction, isDevelopment };
5 |
--------------------------------------------------------------------------------
/src/routes/Home/components/Assets/reactrouter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clojurie/react-mobx-react-router4-boilerplate/HEAD/src/routes/Home/components/Assets/reactrouter.png
--------------------------------------------------------------------------------
/src/routes/Home/components/Assets/GitHub-Mark-32px.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clojurie/react-mobx-react-router4-boilerplate/HEAD/src/routes/Home/components/Assets/GitHub-Mark-32px.png
--------------------------------------------------------------------------------
/src/routes/Home/components/Assets/GitHub-Mark-Light-32px.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clojurie/react-mobx-react-router4-boilerplate/HEAD/src/routes/Home/components/Assets/GitHub-Mark-Light-32px.png
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | const utils = {
2 | sleep: (timeoutMS) => {
3 | return new Promise((resolve) => {
4 | setTimeout(resolve, timeoutMS);
5 | });
6 | }
7 | };
8 |
9 | export default utils;
10 |
--------------------------------------------------------------------------------
/src/api/index.js:
--------------------------------------------------------------------------------
1 | import { assign } from 'lodash';
2 | import baseAPIs from './base';
3 |
4 | // 后续随 API 增加,可以考虑根据功能拆分成单独文件维护
5 | // 拆分 API 在此处 assign 进来,如
6 | // assign({}, baseAPIs, { moreAPIs })
7 |
8 | export default assign({}, baseAPIs, {});
9 |
--------------------------------------------------------------------------------
/src/routes/Home/service/index.js:
--------------------------------------------------------------------------------
1 | import api from 'api';
2 | import fetchAPI from 'api/fetchAPI';
3 |
4 | const Posts = {
5 | getPostList: (params) => fetchAPI.get(api.getPostList.url, { params }),
6 |
7 | getPostItem: ({ id }) => fetchAPI.get(`${api.getPostList.url}/${id}`)
8 | };
9 |
10 | export { Posts };
11 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "tabWidth": 2,
4 | "arrowParens": "always",
5 | "useTabs": false,
6 | "singleQuote": true,
7 | "jsxSingleQuote": true,
8 | "jsxBracketSameLine": false,
9 | "semi": true,
10 | "trailingComma": "none",
11 | "bracketSpacing": true,
12 | "parser": "babylon"
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/TopNav/style.less:
--------------------------------------------------------------------------------
1 | .topNav {
2 | display: flex;
3 | flex: 1;
4 |
5 | a {
6 | margin-right: 24px;
7 | color: rgba(#000, .5);
8 | font-weight: 500;
9 |
10 | &:hover {
11 | color: rgba(#000, 1);
12 | }
13 | }
14 |
15 | a.active {
16 | color: rgba(#000, 1);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import HomeStore from 'routes/Home/modules/HomeStore';
2 | import RouterStore from './RouterStore';
3 |
4 | export default class RootStore {
5 | static create() {
6 | return new RootStore();
7 | }
8 |
9 | constructor() {
10 | this.routerStore = new RouterStore();
11 | this.homeStore = new HomeStore();
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/TopBar/style.less:
--------------------------------------------------------------------------------
1 | .topbar {
2 | display: flex;
3 | flex: 0 1 auto;
4 | align-items: center;
5 | padding: 0 24px;
6 | width: 100%;
7 | height: 60px;
8 | background: #F6E27F;
9 |
10 | .button {
11 | color: rgba(#000, .5);
12 | font-weight: 600;
13 |
14 | &:hover {
15 | color: rgba(#000, 1);
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/TopNav/ActiveLink.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, Link } from 'react-router-dom';
3 |
4 | const ActiveLink = ({ to, activeOnlyWhenExact, ...rest }) => (
5 |
6 | {({ match }) => }
7 |
8 | );
9 |
10 | export default ActiveLink;
11 |
--------------------------------------------------------------------------------
/src/components/TopBar/Button.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import propTypes from 'prop-types';
3 | import style from './style.less';
4 |
5 | const Button = ({ onClick, title }) => {
6 | return (
7 |
8 | {title}
9 |
10 | );
11 | };
12 |
13 | Button.propTypes = {
14 | title: propTypes.string,
15 | onClick: propTypes.func
16 | };
17 |
18 | export default Button;
19 |
--------------------------------------------------------------------------------
/src/store/RouterStore.js:
--------------------------------------------------------------------------------
1 | import { createBrowserHistory } from 'history';
2 | import { RouterStore as BaseRouterStore, syncHistoryWithStore } from 'mobx-react-router';
3 |
4 | const history = createBrowserHistory();
5 |
6 | class RouterStore extends BaseRouterStore {
7 | constructor() {
8 | super();
9 | if (history) {
10 | this.history = syncHistoryWithStore(history, this);
11 | }
12 | }
13 | }
14 |
15 | export default RouterStore;
16 |
--------------------------------------------------------------------------------
/src/routes/Home/models/PostModel.js:
--------------------------------------------------------------------------------
1 | import { observable } from 'mobx';
2 |
3 | export default class PostModel {
4 | @observable id;
5 | @observable userId;
6 | @observable title;
7 | @observable body;
8 |
9 | constructor({ id, userId, title, body }) {
10 | this.id = id;
11 | this.userId = userId;
12 | this.title = title;
13 | this.body = body;
14 | }
15 |
16 | static fromJS(data) {
17 | return new PostModel(data);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/api/fetchAPI.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | // https://www.jianshu.com/p/7a9fbcbb1114
3 |
4 | const fetchAPI = axios.create({
5 | // baseURL: isDevelopment ? 'https://www.easy-mock.com/mock/593611b991470c0ac101d474' : '/',
6 | baseURL: __DEV__ ? '/api' : 'http://jsonplaceholder.typicode.com',
7 | timeout: 10000, // 设置超时时间
8 | headers: {
9 | Accept: 'application/json',
10 | 'X-Requested-With': 'XMLHttpRequest'
11 | }
12 | });
13 |
14 | export default fetchAPI;
15 |
--------------------------------------------------------------------------------
/src/routes/Home/index.jsx:
--------------------------------------------------------------------------------
1 | import LazyLoader from 'components/LazyLoader';
2 | import HomePage from './container';
3 |
4 | export default [
5 | {
6 | path: '/',
7 | exact: true,
8 | component: HomePage
9 | },
10 | {
11 | path: '/posts/',
12 | exact: true,
13 | component: LazyLoader(() => import('./container/Posts'))
14 | },
15 | {
16 | path: '/posts/:id?',
17 | exact: true,
18 | component: LazyLoader(() => import('./container/Article'))
19 | }
20 | ];
21 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | require('postcss-preset-env')({
4 | autoprefixer: false
5 | }),
6 |
7 | require('css-declaration-sorter')({
8 | order: 'concentric-css'
9 | }),
10 |
11 | require('cssnano')({
12 | preset: 'advanced',
13 | autoprefixer: false,
14 | 'postcss-zindex': false
15 | }),
16 |
17 | require('autoprefixer')({
18 | browsers: ['>1%', 'last 4 versions', 'Firefox ESR', 'not ie < 9'],
19 | remove: true
20 | }),
21 |
22 | require('css-mqpacker')()
23 | ]
24 | };
25 |
--------------------------------------------------------------------------------
/src/routes/Home/components/ArticlePage/style.less:
--------------------------------------------------------------------------------
1 | .post {
2 | min-width: 960px;
3 | max-width: 960px;
4 | padding: 48px 24px;
5 | margin: 0 auto;
6 |
7 | article {
8 | margin-top: 72px;
9 | }
10 |
11 | h1 {
12 | color: #000;
13 | margin-bottom: 24px;
14 | font-weight: 700;
15 | &::first-letter {
16 | text-transform: uppercase;
17 | }
18 | }
19 |
20 | p {
21 | &::first-letter {
22 | text-transform: uppercase;
23 | }
24 | }
25 | @media screen and (max-width: 960px) {
26 | min-width: 100%;
27 | max-width: 100%;
28 | padding: 48px 24px;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Boilerplate
6 |
7 |
19 |
20 |
21 |
22 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/routes/Home/container/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import propTypes from 'prop-types';
3 | import { observer } from 'mobx-react';
4 | import TopBar from 'components/TopBar';
5 | import FootBar from 'components/FootBar';
6 | import WelcomePage from '../components/WelcomePage';
7 |
8 | @observer
9 | class HomeContainer extends Component {
10 | constructor(props) {
11 | super(props);
12 | }
13 |
14 | render() {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 | );
22 | }
23 | }
24 |
25 | HomeContainer.propTypes = {
26 | homeStore: propTypes.object
27 | };
28 |
29 | export default HomeContainer;
30 |
--------------------------------------------------------------------------------
/src/components/FootBar/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import propTypes from 'prop-types';
3 |
4 | class FootBar extends Component {
5 | constructor(props) {
6 | super(props);
7 | }
8 |
9 | render() {
10 | return (
11 |
20 | );
21 | }
22 | }
23 |
24 | FootBar.propTypes = {
25 | location: propTypes.object,
26 | routerStore: propTypes.object,
27 | homeStore: propTypes.object
28 | };
29 |
30 | export default FootBar;
31 |
--------------------------------------------------------------------------------
/src/coreLayout/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Switch, Route } from 'react-router-dom';
3 | import NotFound from 'components/NotFound';
4 | import routes from 'routes';
5 |
6 | import '../static/css/normalize.less';
7 | import '../static/css/common.less';
8 |
9 | export default class CoreLayout extends Component {
10 | componentDidMount() {
11 | document.querySelector('#root').style.display = 'block';
12 | }
13 |
14 | render() {
15 | return (
16 |
17 | {routes.map((route, i) => {
18 | /* eslint-disable react/no-array-index-key */
19 | return ;
20 | })}
21 |
22 |
23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/LazyLoader/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const asyncComponent = (loadComponent) =>
4 | class AsyncComponent extends React.Component {
5 | state = {
6 | Component: null
7 | };
8 |
9 | componentDidMount() {
10 | if (this.hasLoadedComponent()) {
11 | return;
12 | }
13 |
14 | loadComponent()
15 | .then((module) => module.default)
16 | .then((Component) => {
17 | this.setState({ Component });
18 | })
19 | .catch((err) => {
20 | console.error('Cannot load component in ');
21 | throw err;
22 | });
23 | }
24 |
25 | hasLoadedComponent() {
26 | const { Component } = this.state;
27 | return Component !== null;
28 | }
29 |
30 | render() {
31 | const { Component } = this.state;
32 | return Component ? : null;
33 | }
34 | };
35 |
36 | export default asyncComponent;
37 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env",
5 | {
6 | "loose": true,
7 | "modules": false,
8 | "useBuiltIns": "usage",
9 | "targets": {
10 | "browsers": [
11 | "> 1%",
12 | "last 2 versions",
13 | "Android >= 4"
14 | ]
15 | }
16 | }
17 | ],
18 | "@babel/preset-react"
19 | ],
20 | "plugins": [
21 | [
22 | "@babel/plugin-proposal-decorators",
23 | {
24 | "legacy": true
25 | }
26 | ],
27 | "@babel/plugin-syntax-jsx",
28 | "@babel/plugin-syntax-dynamic-import",
29 | [
30 | "@babel/plugin-proposal-class-properties",
31 | {
32 | "loose": true
33 | }
34 | ],
35 | [
36 | "@babel/plugin-transform-runtime",
37 | {
38 | "corejs": 2,
39 | "helpers": true,
40 | "regenerator": true,
41 | "useESModules": false
42 | }
43 | ],
44 | [
45 | "lodash"
46 | ]
47 | ]
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/TopNav/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import propTypes from 'prop-types';
3 | import { inject, observer } from 'mobx-react';
4 | import ActiveLink from './ActiveLink';
5 |
6 | import style from './style.less';
7 |
8 | @inject('homeStore')
9 | @observer
10 | class Index extends Component {
11 | constructor(props) {
12 | super(props);
13 | }
14 |
15 | authenticate(e) {
16 | e.preventDefault();
17 | const { homeStore } = this.props;
18 | homeStore.authenticate();
19 | }
20 |
21 | render() {
22 | const { homeStore } = this.props;
23 | const { authenticated } = homeStore;
24 | return (
25 |
31 | );
32 | }
33 | }
34 |
35 | Index.propTypes = {
36 | location: propTypes.object,
37 | routerStore: propTypes.object,
38 | homeStore: propTypes.object
39 | };
40 |
41 | export default Index;
42 |
--------------------------------------------------------------------------------
/src/routes/Home/components/ArticlePage/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import propTypes from 'prop-types';
3 | import { observer } from 'mobx-react';
4 | import { Link } from 'react-router-dom';
5 | import Protected from '../Protected';
6 |
7 | import style from './style.less';
8 |
9 | @Protected
10 | @observer
11 | class PostsPage extends Component {
12 | constructor(props) {
13 | super(props);
14 | }
15 |
16 | render() {
17 | const { item, loading } = this.props;
18 | return (
19 |
20 |
← Back to Posts
21 | {loading ? (
22 |
23 | {item.title}
24 | {item.body}
25 |
26 | ) : (
27 |
28 | Loading...
29 |
30 | )}
31 |
32 | );
33 | }
34 | }
35 |
36 | PostsPage.propTypes = {
37 | item: propTypes.object.isRequired,
38 | loading: propTypes.bool.isRequired
39 | };
40 |
41 | export default PostsPage;
42 |
--------------------------------------------------------------------------------
/src/routes/Home/components/Protected/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { inject, observer } from 'mobx-react';
3 | import { Redirect } from 'react-router-dom';
4 |
5 | export default function Protected(Children) {
6 | @inject('homeStore')
7 | @observer
8 | class AuthenticatedComponent extends Component {
9 | constructor(props) {
10 | super(props);
11 | const { homeStore } = this.props;
12 | this.homeStore = homeStore;
13 | }
14 |
15 | render() {
16 | const { authenticated, authenticating } = this.homeStore;
17 | const { location } = this.props;
18 | return (
19 |
20 | {authenticated ? (
21 |
22 | ) : !authenticating && !authenticated ? (
23 |
29 | ) : null}
30 |
31 | );
32 | }
33 | }
34 |
35 | return AuthenticatedComponent;
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/TopBar/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import propTypes from 'prop-types';
3 | import { inject, observer } from 'mobx-react';
4 | import TopNav from '../TopNav';
5 | import Button from './Button';
6 |
7 | import style from './style.less';
8 |
9 | @inject('routerStore')
10 | @inject('homeStore')
11 | @observer
12 | class TopBar extends Component {
13 | constructor(props) {
14 | super(props);
15 | }
16 |
17 | authenticate = (e) => {
18 | e.preventDefault();
19 | const { homeStore } = this.props;
20 | homeStore.authenticate();
21 | };
22 |
23 | render() {
24 | const {
25 | location,
26 | homeStore: { authenticated }
27 | } = this.props;
28 | return (
29 |
30 |
31 |
32 |
33 | );
34 | }
35 | }
36 |
37 | TopBar.propTypes = {
38 | location: propTypes.object,
39 | routerStore: propTypes.object,
40 | homeStore: propTypes.object
41 | };
42 |
43 | export default TopBar;
44 |
--------------------------------------------------------------------------------
/src/routes/Home/container/Posts.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import propTypes from 'prop-types';
3 | import { inject, observer } from 'mobx-react';
4 | import TopBar from 'components/TopBar';
5 | import FootBar from 'components/FootBar';
6 | import PostsPage from '../components/PostsPage';
7 |
8 | @inject('routerStore')
9 | @inject('homeStore')
10 | @observer
11 | class HomeContainer extends Component {
12 | constructor(props) {
13 | super(props);
14 | this.homeStore = props.homeStore;
15 | }
16 |
17 | componentDidMount() {
18 | this.homeStore.getPostList();
19 | }
20 |
21 | componentWillUnmount() {
22 | this.homeStore.clearItems();
23 | }
24 |
25 | render() {
26 | const {
27 | match: { url }
28 | } = this.props;
29 |
30 | const { items, loading } = this.homeStore;
31 | return (
32 |
33 |
34 |
35 |
36 |
37 | );
38 | }
39 | }
40 |
41 | HomeContainer.propTypes = {
42 | homeStore: propTypes.object,
43 | match: propTypes.object
44 | };
45 |
46 | export default HomeContainer;
47 |
--------------------------------------------------------------------------------
/src/routes/Home/container/Article.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import propTypes from 'prop-types';
3 | import { inject, observer } from 'mobx-react';
4 | import TopBar from 'components/TopBar';
5 | import FootBar from 'components/FootBar';
6 | import ArticlePage from '../components/ArticlePage';
7 |
8 | @inject('routerStore')
9 | @inject('homeStore')
10 | @observer
11 | class HomeContainer extends Component {
12 | constructor(props) {
13 | super(props);
14 | const { homeStore } = this.props;
15 | this.homeStore = homeStore;
16 | }
17 |
18 | componentDidMount() {
19 | const {
20 | match: {
21 | params: { id = null }
22 | }
23 | } = this.props;
24 | this.homeStore.getPostItem({ id });
25 | }
26 |
27 | componentWillUnmount() {
28 | this.homeStore.clearItems();
29 | }
30 |
31 | render() {
32 | const { item, loadingItem } = this.homeStore;
33 | return (
34 |
35 |
36 |
37 |
38 |
39 | );
40 | }
41 | }
42 |
43 | HomeContainer.propTypes = {
44 | homeStore: propTypes.object,
45 | match: propTypes.object
46 | };
47 |
48 | export default HomeContainer;
49 |
--------------------------------------------------------------------------------
/src/components/NotFound/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import img404 from '../../static/img/404.png';
3 | import cloud404 from '../../static/img/cloud_404.png';
4 |
5 | import style from './style.less';
6 |
7 | const goBack = (e) => {
8 | e.preventDefault();
9 | return history.back();
10 | };
11 |
12 | export const NotFound = () => (
13 |
14 |
15 |
21 |
22 |
当前页面无法访问!
23 |
404 Not Found
24 |
请检查您输入的网址是否正确,请点击以下按钮返回主页或者发送错误报告
25 |
26 | 返回首页
27 |
28 |
29 |
30 |
31 | );
32 |
33 | export default NotFound;
34 |
--------------------------------------------------------------------------------
/src/routes/Home/components/PostsPage/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import propTypes from 'prop-types';
4 | import { observer } from 'mobx-react';
5 | import Protected from '../Protected';
6 |
7 | import style from './style.less';
8 |
9 | @Protected
10 | @observer
11 | class PostsPage extends Component {
12 | constructor(props) {
13 | super(props);
14 | }
15 |
16 | render() {
17 | const { items, url, loading } = this.props;
18 | return (
19 |
20 |
Posts
21 |
Posts are fetched from jsonplaceholder.typicode.com
22 |
23 |
37 |
38 | );
39 | }
40 | }
41 |
42 | PostsPage.propTypes = {
43 | match: propTypes.object,
44 | loading: propTypes.bool,
45 | items: propTypes.array.isRequired,
46 | url: propTypes.string.isRequired
47 | };
48 |
49 | export default PostsPage;
50 |
--------------------------------------------------------------------------------
/src/routes/Home/components/PostsPage/style.less:
--------------------------------------------------------------------------------
1 | .posts {
2 | min-width: 960px;
3 | max-width: 960px;
4 | padding: 48px 24px;
5 | margin: 0 auto;
6 | h1 {
7 | color: #000;
8 | font-weight: 700;
9 | margin-bottom: 9px;
10 | }
11 | .subheader {
12 | font-size: 1.414em;
13 | font-weight: 300;
14 | }
15 | hr {
16 | border: none;
17 | height: 1px;
18 | color:red;
19 | background-color: #ddd;
20 | margin-top: 48px;
21 | margin-bottom: 48px;
22 | width: 100%;
23 | }
24 | ul {
25 | display: flex;
26 | flex-flow: row wrap;
27 | li {
28 | display: flex;
29 | flex-direction: column;
30 | justify-content: space-between;
31 | flex: 0 1 auto;
32 | margin-right: 36px;
33 | width: calc(50% - 36px);
34 | margin-bottom: 36px;
35 | a {
36 | display: block;
37 | font-weight: 700;
38 | margin-bottom: 12px;
39 | h1 {
40 | font-size: 1.414em;
41 | line-height: 1.2;
42 | &::first-letter {
43 | text-transform: uppercase;
44 | }
45 | }
46 | }
47 | p {
48 | font-weight: 400;
49 | &::first-letter {
50 | text-transform: uppercase;
51 | }
52 | }
53 | }
54 | }
55 | @media screen and (max-width: 960px) {
56 | min-width: 100%;
57 | max-width: 100%;
58 | padding: 48px 24px;
59 | ul li {
60 | flex: 0 1 auto;
61 | width: 100%;
62 | margin-right: 0;
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/webpack.dll.lib.js:
--------------------------------------------------------------------------------
1 | /* webpack --config webpack.dll.config.js --progress */
2 |
3 | const path = require('path');
4 | const webpack = require('webpack');
5 |
6 | const isDebug = process.env.NODE_ENV === 'development';
7 | const publicPath = './public/lib';
8 |
9 | const outputPath = isDebug ? path.join(__dirname, `${publicPath}/debug`) : path.join(__dirname, `${publicPath}/min`);
10 |
11 | const lib = [
12 | 'react',
13 | 'react-dom',
14 | 'react-router',
15 | 'react-router-dom',
16 | 'history',
17 | 'mobx',
18 | 'mobx-react',
19 | 'mobx-react-router',
20 | 'axios',
21 | 'classnames'
22 | ];
23 |
24 | const plugin = [
25 | new webpack.DllPlugin({
26 | /**
27 | * path
28 | * 定义 manifest 文件生成的位置
29 | * [name]的部分由entry的名字替换
30 | */
31 | path: path.join(outputPath, 'manifest.json'),
32 | /**
33 | * name
34 | * dll bundle 输出到那个全局变量上
35 | * 和 output.library 一样即可。
36 | */
37 | name: '[name]',
38 | context: __dirname
39 | })
40 | ];
41 |
42 | module.exports = {
43 | devtool: '#source-map',
44 | entry: {
45 | lib: lib
46 | },
47 | module: {
48 | rules: [
49 | {
50 | test: /\.(js|jsx)$/,
51 | exclude: /node_modules/,
52 | loader: 'babel-loader',
53 | query: {
54 | compact: true,
55 | cacheDirectory: true
56 | }
57 | }
58 | ]
59 | },
60 | mode: isDebug ? 'development' : 'production',
61 | output: {
62 | path: outputPath,
63 | filename: isDebug ? '[name].js' : '[name].[hash:9].js',
64 | /**
65 | * output.library
66 | * 将会定义为 window.${output.library}
67 | * 在这次的例子中,将会定义为`window.vendor_library`
68 | */
69 | library: '[name]'
70 | },
71 | plugins: plugin
72 | };
73 |
--------------------------------------------------------------------------------
/src/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { configure } from 'mobx';
4 | import RedBox from 'redbox-react';
5 | import { Provider } from 'mobx-react';
6 | import { Router } from 'react-router-dom';
7 | import { AppContainer } from 'react-hot-loader';
8 | import store from './store';
9 | import App from './coreLayout';
10 |
11 | // ========================================================
12 | // Store Instantiation
13 | // ========================================================
14 |
15 | const stores = store.create();
16 | const {
17 | routerStore: { history }
18 | } = stores;
19 |
20 | global._____APP_STATE_____ = stores;
21 |
22 | // changing observed observable values only in actions
23 | configure({ enforceActions: 'always' });
24 |
25 | // ========================================================
26 | // Render Setup
27 | // ========================================================
28 | const MOUNT_NODE = document.getElementById('root');
29 | let render = () => {
30 | ReactDOM.render(
31 |
32 |
33 |
34 |
35 |
36 |
37 | ,
38 | MOUNT_NODE
39 | );
40 | };
41 |
42 | // This code is excluded from production bundle
43 | if (__DEV__) {
44 | if (module.hot) {
45 | // Development render functions
46 | const renderApp = render;
47 | const renderError = (error) => {
48 | ReactDOM.render(, MOUNT_NODE);
49 | };
50 |
51 | // Wrap render in try/catch
52 | render = () => {
53 | try {
54 | renderApp();
55 | } catch (error) {
56 | console.error(error);
57 | renderError(error);
58 | }
59 | };
60 |
61 | // hot reload
62 | if (module.hot) {
63 | module.hot.accept('./coreLayout', () => {
64 | try {
65 | renderApp();
66 | } catch (e) {
67 | render(, MOUNT_NODE);
68 | }
69 | });
70 | }
71 | }
72 | }
73 |
74 | // ========================================================
75 | // Go!
76 | // ========================================================
77 | render();
78 |
--------------------------------------------------------------------------------
/src/routes/Home/components/Assets/react_logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "airbnb",
4 | "eslint:recommended",
5 | "plugin:react/recommended",
6 | "prettier/react"
7 | ],
8 | "env": {
9 | "browser": true,
10 | "node": true,
11 | "jasmine": true,
12 | "jest": true,
13 | "es6": true
14 | },
15 | "parser": "babel-eslint",
16 | "parserOptions": {
17 | "ecmaVersion": 6,
18 | "sourceType": "module",
19 | "ecmaFeatures": {
20 | "jsx": true,
21 | "experimentalObjectRestSpread": true
22 | }
23 | },
24 | "plugins": [
25 | "react",
26 | "jsx-a11y",
27 | "import",
28 | "prettier",
29 | "promise"
30 | ],
31 | "globals": {
32 | "__VERSION__": true,
33 | "__DEV__": false,
34 | "__TEST__": false,
35 | "__PROD__": false,
36 | "__COVERAGE__": false,
37 | "React": true,
38 | "TestUtils": true
39 | },
40 | "rules": {
41 | "semi": [
42 | "error",
43 | "always"
44 | ],
45 | "no-octal-escape": 0,
46 | "no-console": 0,
47 | "import/no-unresolved": 0,
48 | "import/prefer-default-export": 0,
49 | "import/no-extraneous-dependencies": 0,
50 | "no-mixed-operators": 0,
51 | "prefer-promise-reject-errors": 0,
52 | "key-spacing": 0,
53 | "quotes": ["error", "single"],
54 | "jsx-quotes": [
55 | 2,
56 | "prefer-single"
57 | ],
58 | "max-len": [
59 | 2,
60 | 160,
61 | 2
62 | ],
63 | "object-curly-spacing": [
64 | 2,
65 | "always"
66 | ],
67 | "prettier/prettier": [
68 | "error",
69 | {
70 | "jsxSingleQuote": true
71 | }
72 | ],
73 | "no-useless-constructor": 0,
74 | "jsx-a11y/no-noninteractive-element-interactions": 0,
75 | "react/no-unused-prop-types": 0,
76 | "react/jsx-one-expression-per-line": 0,
77 | "jsx-a11y/click-events-have-key-events": 0,
78 | "jsx-a11y/no-static-element-interactions": 0,
79 | "jsx-a11y/anchor-is-valid": 0,
80 | "react/require-default-props": 0,
81 | "react/forbid-prop-types": 0,
82 | "react/no-danger": 0,
83 | "react/no-array-index-key": 0,
84 | "react/prefer-stateless-function": 0
85 | },
86 | "settings": {
87 | "react": {
88 | "version": "16"
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/routes/Home/modules/HomeStore.js:
--------------------------------------------------------------------------------
1 | import { observable, action, computed } from 'mobx';
2 | import { isEmpty } from 'lodash';
3 | import { PostModel } from '../models';
4 | import { Posts } from '../service';
5 |
6 | class HomeStore {
7 | @observable authenticated;
8 | @observable authenticating;
9 | @observable items;
10 | @observable item;
11 | @observable testval;
12 |
13 | constructor() {
14 | this.authenticated = false;
15 | this.authenticating = false;
16 | this.items = [];
17 | this.item = {};
18 | }
19 |
20 | getPostList(params = {}) {
21 | Posts.getPostList(params)
22 | .then(
23 | action('success', ({ status, data }) => {
24 | if (status === 200) {
25 | this.items = data.map((item) => PostModel.fromJS(item));
26 | } else {
27 | Promise.reject(status);
28 | }
29 | })
30 | )
31 | .catch(
32 | action('error', (error) => {
33 | console.log(error);
34 | })
35 | );
36 | }
37 |
38 | getPostItem(params = {}) {
39 | Posts.getPostItem(params)
40 | .then(
41 | action('success', ({ status, data }) => {
42 | if (status === 200) {
43 | this.item = PostModel.fromJS(data);
44 | } else {
45 | Promise.reject(status);
46 | }
47 | })
48 | )
49 | .catch(
50 | action('error', (error) => {
51 | console.log(error);
52 | })
53 | );
54 | }
55 |
56 | @action clearItems() {
57 | this.items = [];
58 | this.item = {};
59 | }
60 |
61 | @action authenticate() {
62 | return new Promise((resolve) => {
63 | setTimeout(
64 | action('success', () => {
65 | this.authenticated = !this.authenticated;
66 | this.authenticating = false;
67 | resolve(this.authenticated);
68 | }),
69 | 200
70 | );
71 | });
72 | }
73 |
74 | @computed get loading() {
75 | return this.items.length > 0;
76 | }
77 |
78 | @computed get loadingItem() {
79 | return !isEmpty(this.item);
80 | }
81 |
82 | @computed get getItem() {
83 | return this.items.filter((todo) => todo.id === 1);
84 | }
85 | }
86 |
87 | export default HomeStore;
88 |
--------------------------------------------------------------------------------
/src/static/css/common.less:
--------------------------------------------------------------------------------
1 | :global {
2 | html, body, div, span, applet, object, iframe,
3 | h1, h2, h3, h4, h5, h6, p, blockquote, pre,
4 | a, abbr, acronym, address, big, cite, code,
5 | del, dfn, em, img, ins, kbd, q, s, samp,
6 | small, strike, strong, sub, sup, tt, var,
7 | b, u, i, center,
8 | dl, dt, dd, ol, ul, li,
9 | fieldset, form, label, legend,
10 | table, caption, tbody, tfoot, thead, tr, th, td,
11 | article, aside, canvas, details, embed,
12 | figure, figcaption, footer, header, hgroup,
13 | menu, nav, output, ruby, section, summary,
14 | time, mark, audio, video {
15 | margin: 0;
16 | padding: 0;
17 | border: 0;
18 | font-size: 100%;
19 | font: inherit;
20 | vertical-align: baseline;
21 | }
22 |
23 | /* HTML5 display-role reset for older browsers */
24 |
25 | article, aside, details, figcaption, figure,
26 | footer, header, hgroup, menu, nav, section {
27 | display: block;
28 | }
29 |
30 | body {
31 | line-height: 1;
32 | }
33 |
34 | ol, ul {
35 | list-style: none;
36 | }
37 |
38 | blockquote, q {
39 | quotes: none;
40 | }
41 |
42 | blockquote:before, blockquote:after,
43 | q:before, q:after {
44 | content: '';
45 | content: none;
46 | }
47 |
48 | table {
49 | border-collapse: collapse;
50 | border-spacing: 0;
51 | }
52 |
53 | *, *:before, *:after {
54 | box-sizing: inherit;
55 | }
56 |
57 | html {
58 | font-family: 'Roboto', Helvetica, sans-serif;
59 | font-size: 100%;
60 | font-style: normal;
61 | font-weight: normal;
62 | text-rendering: optimizeLegibility;
63 | -webkit-font-smoothing: antialiased;
64 | line-height: 1.5;
65 | box-sizing: border-box;
66 | }
67 |
68 | body {
69 | color: #666;
70 | display: flex;
71 | line-height: 1.5;
72 | flex-direction: column;
73 | flex: 1;
74 | padding: 0;
75 | margin: 0;
76 | overflow-y: scroll;
77 | }
78 |
79 | .wrapper {
80 | min-height: 100vh;
81 | }
82 |
83 | #root, .wrapper, .page, .authComponent {
84 | display: flex;
85 | flex-direction: column;
86 | flex: 1;
87 | }
88 |
89 | footer {
90 | display: flex;
91 | flex: 0 1 auto;
92 | align-items: center;
93 | justify-content: center;
94 | width: 100%;
95 | height: 60px;
96 | padding: 18px 0;
97 | border-top: 1px solid #eee;
98 | font-size: 13px;
99 | color: #666;
100 |
101 | a {
102 | margin-left: .5ch;
103 |
104 | &:first-child {
105 | margin-right: .5ch;
106 | }
107 | }
108 | }
109 |
110 | h1 {
111 | margin-top: 0;
112 | font-size: 3.998em;
113 | }
114 |
115 | h1, h2 {
116 | font-size: 2.827em;
117 | }
118 |
119 | h3 {
120 | font-size: 1.999em;
121 | }
122 |
123 | h4 {
124 | font-size: 1.414em;
125 | }
126 |
127 | small, .font_small {
128 | font-size: 0.707em;
129 | }
130 |
131 | a {
132 | text-decoration: none;
133 | cursor: pointer;
134 | color: rgba(#000, .9);
135 |
136 | &:hover {
137 | color: rgba(#000, 1);
138 | }
139 |
140 | &.active {
141 | color: rgba(#000, 1);
142 | }
143 | }
144 |
145 | #preloader {
146 | display: none;
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/src/static/css/normalize.less:
--------------------------------------------------------------------------------
1 | /*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */
2 |
3 | /**
4 | * 1. Change the default font family in all browsers (opinionated).
5 | * 2. Correct the line height in all browsers.
6 | * 3. Prevent adjustments of font size after orientation changes in
7 | * IE on Windows Phone and in iOS.
8 | */
9 |
10 | /* Document
11 | ========================================================================== */
12 |
13 | :global {
14 |
15 | html,
16 | body {
17 | margin: 0;
18 | padding: 0;
19 | -webkit-tap-highlight-color: transparent;
20 | font-size: 14px;
21 | font-family: 'STHeiti', 'Microsoft YaHei', 'Helvetica', 'Arial', sans-serif;
22 | }
23 |
24 | *,
25 | *:before,
26 | *:after {
27 | box-sizing: inherit;
28 | -webkit-overflow-scrolling: touch;
29 | }
30 |
31 | .text-left {
32 | text-align: left;
33 | }
34 |
35 | .text-right {
36 | text-align: right;
37 | }
38 |
39 | .fr {
40 | float: right;
41 | }
42 |
43 | .fl {
44 | float: left;
45 | }
46 |
47 | // reset
48 | div, span, applet, object, iframe,
49 | h1, h2, h3, h4, h5, h6, p, blockquote, pre,
50 | a, abbr, acronym, address, big, cite, code,
51 | del, dfn, em, img, ins, kbd, q, s, samp,
52 | small, strike, strong, sub, sup, tt, var,
53 | b, u, i, center,
54 | dl, dt, dd, ol, ul, li,
55 | fieldset, form, label, legend,
56 | table, caption, tbody, tfoot, thead, tr, th, td,
57 | article, aside, canvas, details, embed,
58 | figure, figcaption, footer, header, hgroup,
59 | menu, nav, output, ruby, section, summary,
60 | time, mark, audio, video {
61 | margin: 0;
62 | padding: 0;
63 | border: 0;
64 | font-size: 100%;
65 | font: inherit;
66 | vertical-align: baseline;
67 | font-family: PingFangSC-Regular, sans-serif;
68 | }
69 |
70 | article, aside, details, figcaption, figure,
71 | footer, header, hgroup, menu, nav, section {
72 | display: block;
73 | }
74 |
75 | body {
76 | line-height: 1.5;
77 | -webkit-font-smoothing: antialiased;
78 | }
79 |
80 | ol, ul {
81 | list-style: none;
82 | }
83 |
84 | blockquote, q {
85 | quotes: none;
86 | }
87 |
88 | blockquote:before, blockquote:after,
89 | q:before, q:after {
90 | content: '';
91 | content: none;
92 | }
93 |
94 | table {
95 | border-collapse: collapse;
96 | border-spacing: 0;
97 | }
98 |
99 | b, strong {
100 | font-weight: bolder;
101 | }
102 |
103 | [hidden] {
104 | display: none;
105 | }
106 |
107 | .no-select {
108 | -ms-user-select: none;
109 | -moz-user-select: none;
110 | -webkit-user-select: none;
111 | user-select: none;
112 | }
113 |
114 | .no-appear {
115 | -webkit-appearance: none;
116 | -moz-appearance: none;
117 | -ms-appearance: none;
118 | -o-appearance: none;
119 | appearance: none;
120 | }
121 |
122 | .no-wrap {
123 | overflow: hidden;
124 | white-space: nowrap;
125 | text-overflow: ellipsis;
126 | }
127 |
128 | .clearfix:before, .clearfix:after {
129 | content: " ";
130 | display: table;
131 | }
132 |
133 | .clearfix:after {
134 | clear: both;
135 | }
136 |
137 | /* for ie 6/7 */
138 |
139 | .clearfix {
140 | zoom: 1;
141 | }
142 |
143 | *, *:after, *:before {
144 | -webkit-box-sizing: border-box;
145 | -moz-box-sizing: border-box;
146 | box-sizing: border-box;
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/src/routes/Home/components/WelcomePage/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import style from './style.less';
4 |
5 | export default class WelcomePage extends Component {
6 | constructor(props) {
7 | super(props);
8 | }
9 |
10 | render() {
11 | return (
12 |
13 |
31 |
32 |
33 |
Included libraries
34 |
35 |
36 |
37 |
38 |
39 |
40 | React 16
41 |
42 |
UI Library
43 |
44 | React makes it painless to create
45 | interactive UIs.
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | MobX 5
54 |
55 |
Reactive State Management
56 |
MobX is a battle tested library that makes state management simple and scalable.
57 |
58 |
59 |
60 |
61 |
62 |
63 | React Router 4
64 |
65 |
Routing Library
66 |
67 | React Router is a declarative way to render, at any location, any UI that you and your team can think
68 | up.
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | Webpack 4
77 |
78 |
Module Bundler
79 |
Webpack takes modules with dependencies and generates static assets representing those modules.
80 |
81 |
82 |
83 |
Extras
84 |
85 |
86 | - ✓ React-hot-loader
87 | - ✓ Mobx-react-router
88 | - ✓ CSS Modules
89 | - ✓ Pushstate Server Preview
90 |
91 |
92 | - ✓ Prettier & ESlint
93 | - ✓ Babel-preset-env
94 | - ✓ Code Splitting
95 | - ✓ Webpack Dll Plugin
96 |
97 |
98 |
99 |
100 | );
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "MartinsReactBoilerplate",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "webpack.config.js",
6 | "scripts": {
7 | "lint": "eslint ./src --ext .jsx,.js",
8 | "lint:fix": "npm run lint -- --fix",
9 | "format": "prettier-eslint --write './src/**/*.{ts,js,json,jsx}'",
10 | "start": "cross-env NODE_ENV=development webpack-dev-server --inline --progress --config webpack.config.js",
11 | "build": "rimraf ./output && cross-env NODE_ENV=production webpack --config webpack.config.production.js",
12 | "preview": "pushstate-server ./output 3003",
13 | "dll": "NODE_ENV=production webpack --config webpack.dll.lib.js --progress",
14 | "dll:dev": "NODE_ENV=development webpack --config webpack.dll.lib.js --progress"
15 | },
16 | "keywords": [
17 | "React",
18 | "React-Router",
19 | "MobX",
20 | "Webpack",
21 | "ES6",
22 | "ES7"
23 | ],
24 | "author": "Martin Haagensli",
25 | "license": "ISC",
26 | "devDependencies": {
27 | "@babel/core": "^7.1.2",
28 | "@babel/plugin-proposal-class-properties": "^7.1.0",
29 | "@babel/plugin-proposal-decorators": "^7.1.2",
30 | "@babel/plugin-syntax-dynamic-import": "^7.0.0",
31 | "@babel/plugin-syntax-jsx": "^7.0.0",
32 | "@babel/plugin-transform-runtime": "^7.1.0",
33 | "@babel/polyfill": "^7.0.0",
34 | "@babel/preset-env": "^7.1.0",
35 | "@babel/preset-react": "^7.0.0",
36 | "@babel/runtime": "^7.1.2",
37 | "@babel/runtime-corejs2": "^7.1.2",
38 | "add-asset-html-webpack-plugin": "^3.1.2",
39 | "autoprefixer": "^9.4.2",
40 | "babel-eslint": "^9.0.0",
41 | "babel-loader": "^8.0.4",
42 | "babel-plugin-add-module-exports": "^0.3.3",
43 | "babel-plugin-import": "^1.6.6",
44 | "babel-plugin-lodash": "^3.3.4",
45 | "babel-plugin-react-css-modules": "^3.4.2",
46 | "cross-env": "^5.1.4",
47 | "css-loader": "^0.28.11",
48 | "eslint": "^5.10.0",
49 | "eslint-config-airbnb": "^17.0.0",
50 | "eslint-config-prettier": "^3.3.0",
51 | "eslint-loader": "^2.1.1",
52 | "eslint-module-utils": "^2.0.0",
53 | "eslint-plugin-import": "^2.2.0",
54 | "eslint-plugin-jasmine": "^2.2.0",
55 | "eslint-plugin-jsx-a11y": "^6.1.0",
56 | "eslint-plugin-prettier": "^3.0.0",
57 | "eslint-plugin-promise": "^3.8.0",
58 | "eslint-plugin-react": "^7.11.1",
59 | "file-loader": "^1.1.11",
60 | "html-webpack-plugin": "^3.2.0",
61 | "image-webpack-loader": "^4.2.0",
62 | "less": "^3.8.1",
63 | "less-loader": "^4.1.0",
64 | "lint-staged": "^8.1.0",
65 | "lodash-webpack-plugin": "^0.11.5",
66 | "mini-css-extract-plugin": "^0.4.5",
67 | "optimize-css-assets-webpack-plugin": "^4.0.0",
68 | "postcss-loader": "^2.1.3",
69 | "prerender-spa-plugin": "^3.4.0",
70 | "prettier": "^1.15.3",
71 | "prettier-eslint": "^8.8.2",
72 | "prettier-eslint-cli": "^4.7.1",
73 | "pushstate-server": "^3.0.1",
74 | "react-hot-loader": "^4.6.0",
75 | "redbox-react": "^1.6.0",
76 | "rimraf": "^2.5.4",
77 | "style-loader": "^0.20.3",
78 | "uglifyjs-webpack-plugin": "^2.0.1",
79 | "url-loader": "^1.0.1",
80 | "webpack": "^4.20.2",
81 | "webpack-bundle-analyzer": "^3.0.3",
82 | "webpack-cli": "^3.1.2",
83 | "webpack-dev-server": "^3.1.10",
84 | "webpack-merge": "^4.1.5"
85 | },
86 | "dependencies": {
87 | "axios": "^0.18.0",
88 | "classnames": "^2.2.6",
89 | "css-declaration-sorter": "^2.0.0",
90 | "css-mqpacker": "^6.0.1",
91 | "faker": "^4.1.0",
92 | "history": "^4.7.2",
93 | "husky": "^1.2.0",
94 | "immer": "^1.9.0",
95 | "lazy-route": "^1.0.7",
96 | "lodash": "^4.17.11",
97 | "mobx": "^5.7.0",
98 | "mobx-react": "^5.4.3",
99 | "mobx-react-router": "^4.0.5",
100 | "moment": "^2.15.1",
101 | "package": "^1.0.1",
102 | "postcss-preset-env": "^4.1.0",
103 | "prop-types": "^15.5.10",
104 | "puppeteer": "^1.11.0",
105 | "querystring": "^0.2.0",
106 | "react": "^16.8.2",
107 | "react-dom": "^16.8.2",
108 | "react-router": "^4.3.1",
109 | "react-router-dom": "^4.3.1"
110 | },
111 | "lint-staged": {
112 | "src/**/*.{js,jsx}": [
113 | "npm run format",
114 | "npm run lint:fix",
115 | "git add"
116 | ]
117 | },
118 | "husky": {
119 | "hooks": {
120 | "pre-commit": "lint-staged",
121 | "pre-push": ""
122 | }
123 | },
124 | "repository": {
125 | "url": "git@github.com:mhaagens/react-mobx-react-router4-boilerplate.git",
126 | "type": "git"
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/components/NotFound/style.less:
--------------------------------------------------------------------------------
1 | .notFound {
2 | background: #fff;
3 | marginTop: -20px;
4 |
5 | .wscnHttp404 {
6 | position: relative;
7 | width: 1200px;
8 | margin: 0 auto;
9 | padding: 0 100px;
10 | overflow: hidden;
11 |
12 | .pic404 {
13 | position: relative;
14 | float: left;
15 | width: 600px;
16 | padding: 150px 0;
17 | overflow: hidden;
18 |
19 | &__parent {
20 | width: 100%;
21 | }
22 |
23 | &__child {
24 | position: absolute;
25 |
26 | &.left {
27 | width: 80px;
28 | top: 17px;
29 | left: 220px;
30 | opacity: 0;
31 | animation-name: cloudLeft;
32 | animation-duration: 2s;
33 | animation-timing-function: linear;
34 | animation-fill-mode: forwards;
35 | animation-delay: 1s;
36 | }
37 |
38 | &.mid {
39 | width: 46px;
40 | top: 10px;
41 | left: 420px;
42 | opacity: 0;
43 | animation-name: cloudMid;
44 | animation-duration: 2s;
45 | animation-timing-function: linear;
46 | animation-fill-mode: forwards;
47 | animation-delay: 1.2s;
48 | }
49 |
50 | &.right {
51 | width: 62px;
52 | top: 100px;
53 | left: 500px;
54 | opacity: 0;
55 | animation-name: cloudRight;
56 | animation-duration: 2s;
57 | animation-timing-function: linear;
58 | animation-fill-mode: forwards;
59 | animation-delay: 1s;
60 | }
61 |
62 | @keyframes cloudLeft {
63 | 0% {
64 | top: 17px;
65 | left: 220px;
66 | opacity: 0;
67 | }
68 | 20% {
69 | top: 33px;
70 | left: 188px;
71 | opacity: 1;
72 | }
73 | 80% {
74 | top: 81px;
75 | left: 92px;
76 | opacity: 1;
77 | }
78 | 100% {
79 | top: 97px;
80 | left: 60px;
81 | opacity: 0;
82 | }
83 | }
84 | @keyframes cloudMid {
85 | 0% {
86 | top: 10px;
87 | left: 420px;
88 | opacity: 0;
89 | }
90 | 20% {
91 | top: 40px;
92 | left: 360px;
93 | opacity: 1;
94 | }
95 | 70% {
96 | top: 130px;
97 | left: 180px;
98 | opacity: 1;
99 | }
100 | 100% {
101 | top: 160px;
102 | left: 120px;
103 | opacity: 0;
104 | }
105 | }
106 | @keyframes cloudRight {
107 | 0% {
108 | top: 100px;
109 | left: 500px;
110 | opacity: 0;
111 | }
112 | 20% {
113 | top: 120px;
114 | left: 460px;
115 | opacity: 1;
116 | }
117 | 80% {
118 | top: 180px;
119 | left: 340px;
120 | opacity: 1;
121 | }
122 | 100% {
123 | top: 200px;
124 | left: 300px;
125 | opacity: 0;
126 | }
127 | }
128 | }
129 | }
130 |
131 | .bullshit {
132 | position: relative;
133 | float: left;
134 | width: 300px;
135 | padding: 150px 0;
136 | overflow: hidden;
137 |
138 | &__oops {
139 | font-size: 32px;
140 | font-weight: bold;
141 | line-height: 40px;
142 | color: #1482f0;
143 | opacity: 0;
144 | margin-bottom: 20px;
145 | animation-name: slideUp;
146 | animation-duration: 0.5s;
147 | animation-fill-mode: forwards;
148 | }
149 |
150 | &__headline {
151 | font-size: 20px;
152 | line-height: 24px;
153 | color: #1482f0;
154 | opacity: 0;
155 | margin-bottom: 10px;
156 | animation-name: slideUp;
157 | animation-duration: 0.5s;
158 | animation-delay: 0.1s;
159 | animation-fill-mode: forwards;
160 | }
161 |
162 | &__info {
163 | font-size: 13px;
164 | line-height: 21px;
165 | color: grey;
166 | opacity: 0;
167 | margin-bottom: 30px;
168 | animation-name: slideUp;
169 | animation-duration: 0.5s;
170 | animation-delay: 0.2s;
171 | animation-fill-mode: forwards;
172 | }
173 |
174 | &__return_home {
175 | display: block;
176 | float: left;
177 | width: 110px;
178 | height: 36px;
179 | background: #1482f0;
180 | border-radius: 100px;
181 | text-align: center;
182 | color: #ffffff;
183 | opacity: 0;
184 | font-size: 14px;
185 | line-height: 36px;
186 | cursor: pointer;
187 | animation-name: slideUp;
188 | animation-duration: 0.5s;
189 | animation-delay: 0.3s;
190 | animation-fill-mode: forwards;
191 | }
192 |
193 | @keyframes slideUp {
194 | 0% {
195 | transform: translateY(60px);
196 | opacity: 0;
197 | }
198 | 100% {
199 | transform: translateY(0);
200 | opacity: 1;
201 | }
202 | }
203 | }
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/src/routes/Home/components/WelcomePage/style.less:
--------------------------------------------------------------------------------
1 | .home {
2 | .hero-unit {
3 | display: flex;
4 | flex: 0 1 auto;
5 | flex-flow: row wrap;
6 | align-items: center;
7 | justify-content: center;
8 | width: 100%;
9 | }
10 | .react-logo {
11 | display: block;
12 | flex: none;
13 | overflow: hidden;
14 | clear: both;
15 | width: 100%;
16 | height: 96px;
17 | margin-bottom: 24px;
18 | background: url(../Assets/react_logo.svg);
19 | background-repeat: no-repeat;
20 | background-size: 96px 96px;
21 | background-position: center;
22 | }
23 | header {
24 | position: relative;
25 | display: flex;
26 | flex-direction: column;
27 | align-items: center;
28 | justify-content: center;
29 | min-height: calc(480px - 60px);
30 | background: #F6E27F;
31 | .hero-unit {
32 | margin-bottom: 24px;
33 | }
34 | .react-logo {
35 | }
36 | h1 {
37 | font-weight: 700;
38 | color: #000;
39 | }
40 | h4 {
41 | color: rgba(#000, .5);
42 | font-weight: 400;
43 | }
44 | @media screen and (max-width: 960px) {
45 | padding: 24px;
46 | text-align: center;
47 | h1 {
48 | font-size: 6vmax;
49 | }
50 | }
51 | }
52 | main {
53 | display: flex;
54 | flex-flow: row wrap;
55 | min-width: 960px;
56 | max-width: 960px;
57 | margin: 0 auto;
58 | padding: 96px 0;
59 | .section-header {
60 | flex: 0 1 auto;
61 | width: 100%;
62 | padding-bottom: 72px;
63 | text-align: center;
64 | color: #000;
65 | h3 {
66 | font-weight: 700;
67 | }
68 | &.extras {
69 | margin-top: 48px;
70 | h4 {
71 | font-weight: 700;
72 | color: #000;
73 | }
74 | ul {
75 | display: flex;
76 | flex: 1;
77 | flex-flow: row nowrap;
78 | width: 100%;
79 | max-width: 960px;
80 | margin: 36px auto 0 auto;
81 | li {
82 | display: flex;
83 | flex: 1;
84 | align-content: center;
85 | justify-content: center;
86 | }
87 | @media screen and (max-width: 960px) {
88 | flex-flow: column;
89 | li {
90 | flex: 0 1 auto;
91 | width: 100%;
92 | margin-bottom: 18px;
93 | }
94 | }
95 | }
96 | }
97 | hr {
98 | border: none;
99 | height: 1px;
100 | color: #ddd;
101 | background-color: #ddd;
102 | margin: 24px auto 0px;
103 | max-width: 96px;
104 | }
105 | }
106 | .boilerplate-item {
107 | display: flex;
108 | flex: 0 1 auto;
109 | flex-flow: row nowrap;
110 | width: calc(50% - 48px);
111 | margin-right: 48px;
112 | margin-bottom: 48px;
113 | .boilerplate-logo {
114 | display: flex;
115 | width: 80px;
116 | height: 80px;
117 | margin-right: 24px;
118 | background: url(../Assets/react.png);
119 | background-repeat: no-repeat;
120 | background-size: contain;
121 | &.mobx {
122 | background: url(../Assets/mobx.png);
123 | background-repeat: no-repeat;
124 | background-size: contain;
125 | }
126 | &.reactrouter {
127 | background: url(../Assets/reactrouter.png);
128 | background-repeat: no-repeat;
129 | background-size: contain;
130 | }
131 | &.webpack {
132 | background: url(../Assets/webpack.png);
133 | background-repeat: no-repeat;
134 | background-size: contain;
135 | }
136 | }
137 | .boilerplate-item-content {
138 | flex: 1;
139 | }
140 | h4 {
141 | font-weight: 500;
142 | color: #000;
143 | }
144 | small {
145 | text-transform: uppercase;
146 | font-weight: 500;
147 | letter-spacing: 1px;
148 | color: #999;
149 | }
150 | p {
151 | margin-top: 12px;
152 | }
153 | }
154 | @media screen and (max-width: 960px) {
155 | min-width: 100%;
156 | max-width: 100%;
157 | padding: 48px 24px;
158 | .boilerplate-item {
159 | flex: 0 1 auto;
160 | width: 100%;
161 | margin-right: 0;
162 | }
163 | }
164 | }
165 | }
166 |
167 | .github-buttons {
168 | display: flex;
169 | flex: 0 1 auto;
170 | width: 100%;
171 | margin: 36px 0;
172 | justify-content: center;
173 | align-items: center;
174 | }
175 |
176 | .github-buttons a {
177 | display: flex;
178 | align-items: center;
179 | padding: 18px 24px;
180 | border-radius: 2px;
181 | border: 1px solid #000;
182 | color: #000;
183 | font-weight: 600;
184 | font-size: 18px;
185 | &:before {
186 | content: '';
187 | width: 24px;
188 | height: 24px;
189 | margin-right: 12px;
190 | background: url(../Assets/GitHub-Mark-32px.png);
191 | background-repeat: no-repeat;
192 | background-size: contain;
193 | }
194 | &:hover {
195 | background: #000;
196 | color: #fff;
197 | &:before {
198 | background: url(../Assets/GitHub-Mark-Light-32px.png);
199 | background-repeat: no-repeat;
200 | background-size: contain;
201 | }
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/webpack.config.production.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const cssnano = require('cssnano');
4 | const merge = require('webpack-merge');
5 | const HtmlWebpackPlugin = require('html-webpack-plugin');
6 | const BundlePlugin = require('webpack-bundle-analyzer');
7 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
8 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
9 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
10 | const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');
11 | const PrerenderSPAPlugin = require('prerender-spa-plugin');
12 | const webpackConfig = require('./webpack.config');
13 |
14 | const Renderer = PrerenderSPAPlugin.PuppeteerRenderer;
15 | const publicPath = '/';
16 |
17 | const mergeConfig = merge(webpackConfig, {
18 | mode: 'production',
19 |
20 | output: {
21 | path: path.join(__dirname, 'output'),
22 | publicPath: publicPath,
23 | filename: 'js/[name].[hash:12].js',
24 | chunkFilename: 'js/[id].[chunkhash:12].js'
25 | },
26 |
27 | devtool: '#source-map',
28 |
29 | module: {},
30 |
31 | optimization: {
32 | runtimeChunk: {
33 | name: 'manifest'
34 | },
35 | minimizer: [
36 | new UglifyJsPlugin({
37 | cache: true,
38 | parallel: true,
39 | uglifyOptions: {
40 | warnings: false,
41 | mangle: {
42 | toplevel: true
43 | }
44 | }
45 | }),
46 | new OptimizeCSSAssetsPlugin({
47 | assetNameRegExp: /\.css$/g,
48 | cssProcessor: cssnano,
49 | cssProcessorOptions: {
50 | autoprefixer: false,
51 | preset: [
52 | 'default',
53 | {
54 | discardComments: {
55 | removeAll: true
56 | }
57 | }
58 | ]
59 | }
60 | })
61 | ],
62 | splitChunks: {
63 | chunks: 'async',
64 | minSize: 30000,
65 | minChunks: 1,
66 | maxAsyncRequests: 5,
67 | maxInitialRequests: 3,
68 | name: false,
69 | cacheGroups: {
70 | vendor: {
71 | name: 'vendor',
72 | chunks: 'initial',
73 | priority: -10,
74 | reuseExistingChunk: false,
75 | test: /node_modules\/(.*)\.js/
76 | },
77 | styles: {
78 | name: 'styles',
79 | test: /\.(less|css)$/,
80 | chunks: 'all',
81 | minChunks: 1,
82 | reuseExistingChunk: true,
83 | enforce: true
84 | }
85 | }
86 | }
87 | },
88 |
89 | performance: {
90 | hints: false
91 | },
92 |
93 | plugins: [
94 | new webpack.DefinePlugin({
95 | 'process.env': {
96 | NODE_ENV: JSON.stringify('production'),
97 | __DEV__: process.env.NODE_ENV === 'production'
98 | }
99 | }),
100 |
101 | new webpack.NamedModulesPlugin(),
102 |
103 | new MiniCssExtractPlugin({
104 | filename: 'css/app.[name].css',
105 | chunkFilename: 'css/app.[contenthash:12].css'
106 | }),
107 |
108 | new HtmlWebpackPlugin({
109 | hash: false,
110 | template: './src/index.html',
111 | filename: 'index.html'
112 | }),
113 |
114 | new webpack.DllReferencePlugin({
115 | context: __dirname,
116 | manifest: require('./public/lib/min/manifest.json')
117 | }),
118 |
119 | new AddAssetHtmlPlugin([
120 | {
121 | filepath: path.resolve(__dirname, './public/lib/min/lib.aef84325a.js'),
122 | outputPath: 'lib/min',
123 | publicPath: `${publicPath}lib/min`,
124 | includeSourcemap: false
125 | }
126 | ]),
127 |
128 | new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /nb/),
129 |
130 | new BundlePlugin.BundleAnalyzerPlugin(),
131 |
132 | new PrerenderSPAPlugin({
133 | // Index.html is in the root directory.
134 | staticDir: path.join(__dirname, 'output'),
135 | outputDir: path.join(__dirname, 'output'),
136 | indexPath: path.join(__dirname, 'output', 'index.html'),
137 | routes: ['/'],
138 | // Optional minification.
139 | minify: {
140 | collapseBooleanAttributes: true,
141 | collapseWhitespace: true,
142 | decodeEntities: true,
143 | keepClosingSlash: true,
144 | sortAttributes: true
145 | },
146 |
147 | server: {
148 | // Normally a free port is autodetected, but feel free to set this if needed.
149 | port: 8001,
150 | proxy: {
151 | '/dist/': {
152 | target: 'http://127.0.0.1:8001',
153 | pathRewrite: { '^/dist': '/' }
154 | }
155 | }
156 | },
157 |
158 | postProcess(context) {
159 | // Remove /index.html from the output path if the dir name ends with a .html file extension.
160 | // For example: /dist/dir/special.html/index.html -> /dist/dir/special.html
161 | // context.html = context.html.replace(/\/dist/gi, '');
162 | return context;
163 | },
164 |
165 | renderer: new Renderer({
166 | renderAfterTime: 2000
167 | })
168 | })
169 | ]
170 | });
171 |
172 | mergeConfig.module.rules
173 | .filter((rule) => {
174 | const bool =
175 | rule.use &&
176 | Array.isArray(rule.use) &&
177 | rule.use.find((name) => {
178 | if (Object.prototype.toString.call(name) === '[object Object]') {
179 | name = name.loader;
180 | }
181 | return /css-loader/.test(name.split('?')[0]);
182 | });
183 | return bool;
184 | })
185 | .forEach((rule) => {
186 | rule.use[0] = MiniCssExtractPlugin.loader;
187 | });
188 |
189 | module.exports = mergeConfig;
190 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');
5 | const POSTCSS = require('./postcss.config');
6 |
7 | const isDebug = process.env.NODE_ENV === 'development';
8 |
9 | const baseConfig = {
10 | mode: 'development',
11 | entry: {
12 | vendor: ['classnames', 'immer'],
13 | app: isDebug
14 | ? [
15 | 'react-hot-loader/patch',
16 | 'webpack-dev-server/client?http://0.0.0.0:3000',
17 | 'webpack/hot/only-dev-server',
18 | './src/index'
19 | ]
20 | : ['./src/index']
21 | },
22 | resolve: {
23 | extensions: ['.js', '.jsx', '.json', '.web.js'],
24 | alias: {
25 | api: path.resolve(__dirname, 'src/api'),
26 | utils: path.resolve(__dirname, 'src/utils'),
27 | static: path.resolve(__dirname, 'src/static'),
28 | routes: path.resolve(__dirname, 'src/routes'),
29 | components: path.resolve(__dirname, 'src/components'),
30 | constants: path.resolve(__dirname, 'src/constants')
31 | }
32 | },
33 | devServer: {
34 | hot: true,
35 | contentBase: [path.resolve(__dirname, 'output'), path.resolve(__dirname, 'public')],
36 | port: 3000,
37 | host: '0.0.0.0',
38 | publicPath: '/',
39 | stats: {
40 | color: true
41 | },
42 | clientLogLevel: 'none',
43 | proxy: {
44 | '/api': {
45 | target: 'http://jsonplaceholder.typicode.com',
46 | pathRewrite: { '^/api': '' },
47 | changeOrigin: true
48 | }
49 | },
50 | historyApiFallback: true,
51 | disableHostCheck: true
52 | },
53 | output: {
54 | path: path.join(__dirname, 'output'),
55 | publicPath: '/',
56 | filename: 'js/[name].js',
57 | chunkFilename: 'js/[id].js'
58 | },
59 | devtool: '#source-map',
60 | module: {
61 | rules: [
62 | {
63 | test: /\.(js|jsx)$/,
64 | exclude: /node_modules/,
65 | loader: 'babel-loader',
66 | query: {
67 | compact: true,
68 | cacheDirectory: true
69 | }
70 | },
71 | {
72 | test: /\.less$/,
73 | exclude: /node_modules/,
74 | use: [
75 | 'style-loader',
76 | {
77 | loader: 'css-loader',
78 | options: {
79 | modules: true,
80 | importLoaders: 1,
81 | minimize: false,
82 | localIdentName: '[name]__[local]___[hash:base64:5]'
83 | }
84 | },
85 | {
86 | loader: 'postcss-loader',
87 | options: {
88 | plugins: POSTCSS.plugins,
89 | sourceMap: isDebug
90 | }
91 | },
92 | {
93 | loader: 'less-loader',
94 | options: {
95 | javascriptEnabled: true
96 | }
97 | }
98 | ]
99 | },
100 | {
101 | test: /\.less$/,
102 | include: /node_modules/,
103 | use: [
104 | 'style-loader',
105 | {
106 | loader: 'css-loader',
107 | options: {
108 | importLoaders: 1
109 | }
110 | },
111 | {
112 | loader: 'postcss-loader',
113 | options: {
114 | plugins: POSTCSS.plugins,
115 | sourceMap: isDebug
116 | }
117 | },
118 | {
119 | loader: 'less-loader',
120 | options: {
121 | javascriptEnabled: true
122 | }
123 | }
124 | ]
125 | },
126 | {
127 | test: /\.css$/,
128 | use: [
129 | 'style-loader',
130 | {
131 | loader: 'css-loader',
132 | options: {
133 | importLoaders: 1
134 | }
135 | },
136 | {
137 | loader: 'postcss-loader',
138 | options: {
139 | plugins: POSTCSS.plugins,
140 | sourceMap: isDebug
141 | }
142 | }
143 | ]
144 | },
145 | {
146 | test: /\.(jpe?g|png|gif|svg)$/i,
147 | use: [
148 | 'file-loader?hash=sha512&digest=hex&limit=8192&name=assets/[name].[hash:8].[ext]',
149 | {
150 | loader: 'image-webpack-loader',
151 | options: {
152 | optipng: {
153 | optimizationLevel: 7
154 | },
155 | gifsicle: {
156 | interlaced: false
157 | },
158 | pngquant: {
159 | quality: '65-90',
160 | speed: 4
161 | },
162 | mozjpeg: {
163 | quality: 65,
164 | progressive: true
165 | }
166 | }
167 | }
168 | ]
169 | },
170 | {
171 | test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
172 | use: 'url-loader??prefix=fonts/name=assets/[name].[hash:8].[ext]&limit=10000&mimetype=application/font-woff'
173 | },
174 | {
175 | test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
176 | use: 'file-loader?prefix=fonts/&name=assets/[name].[hash:8].[ext]&limit=10000&mimetype=font/opentype'
177 | }
178 | ]
179 | },
180 | optimization: {
181 | runtimeChunk: {
182 | name: 'manifest'
183 | },
184 | splitChunks: {
185 | chunks: 'async',
186 | minSize: 30000,
187 | minChunks: 1,
188 | maxAsyncRequests: 5,
189 | maxInitialRequests: 3,
190 | name: false,
191 | cacheGroups: {
192 | vendor: {
193 | name: 'vendor',
194 | chunks: 'initial',
195 | priority: -10,
196 | reuseExistingChunk: false,
197 | test: /node_modules\/(.*)\.js/
198 | }
199 | }
200 | }
201 | },
202 | performance: {
203 | hints: false
204 | },
205 |
206 | plugins: [
207 | new webpack.DefinePlugin({
208 | 'process.env': {
209 | NODE_ENV: JSON.stringify('development')
210 | },
211 | __DEV__: process.env.NODE_ENV === 'development'
212 | }),
213 |
214 | new webpack.NamedModulesPlugin(),
215 |
216 | new webpack.HotModuleReplacementPlugin(),
217 |
218 | new HtmlWebpackPlugin({
219 | hash: false,
220 | template: './src/index.html'
221 | }),
222 |
223 | new webpack.DllReferencePlugin({
224 | context: __dirname,
225 | manifest: require('./public/lib/debug/manifest.json')
226 | }),
227 |
228 | new AddAssetHtmlPlugin([
229 | {
230 | filepath: path.resolve(__dirname, './public/lib/debug/lib.js'),
231 | outputPath: 'lib/debug',
232 | publicPath: '/lib/debug',
233 | includeSourcemap: true
234 | }
235 | ]),
236 |
237 | new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /nb/)
238 | ]
239 | };
240 |
241 | module.exports = baseConfig;
242 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Webpack4升级完全指南
2 |
3 | > webpack4官方已经于近日升级到了V4.5的稳定版本,对应的一些必备插件(webpack-contrib)也陆续完成了更新支持,笔者在第一时间完成了项目由V3到V4的迁移,在此记录一下升级过程中遇到的种种问题和对应的解决手段,方便后续入坑者及时查阅,减少重复工作。
4 |
5 | ## 一、Node版本依赖重新调整
6 |
7 | 官方不再支持node4以下的版本,依赖node的环境版本>=6.11.5,当然考虑到最佳的es6特性实现,建议node版本可以升级到V8.9.4或以上版本,具体更新说明部分可以见:[webpack4更新日志](https://github.com/webpack/webpack/releases/tag/v4.0.0)
8 |
9 | ```javascript
10 | "engines": {
11 | "node": ">=6.11.5" // >=8.10.0 (recommendation version)
12 | }
13 | ```
14 |
15 |
16 | ## 二、用更加快捷的mode模式来优化配置文件
17 |
18 | webpack4中提供的mode有两个值:development和production,默认值是 production。mode是我们为减小生产环境构建体积以及节约开发环境的构建时间提供的一种优化方案,提供对应的构建参数项的默认开启或关闭,降低配置成本。
19 |
20 | ### 开启方式 1:直接在启动命令后加入参数
21 |
22 | ```javascript
23 | "scripts": {
24 | "dev": "webpack --mode development",
25 | "build": "webpack --mode production"
26 | }
27 | ```
28 |
29 | ### 开启方式 2:可以在配置文件中加入一个mode属性:
30 |
31 | ```javascript
32 | module.exports = {
33 | mode: 'production' // development
34 | };
35 | ```
36 |
37 | ### development模式下,将侧重于功能调试和优化开发体验,包含如下内容:
38 | > 1. 浏览器调试工具
39 | > 2. 注释、开发阶段的详细错误日志和提示
40 | > 3. 快速和优化的增量构建机制
41 |
42 | ### production模式下,将侧重于模块体积优化和线上部署,包含如下内容:
43 | > 1. 开启所有的优化代码
44 | > 2. 更小的bundle大小
45 | > 3. 去除掉只在开发阶段运行的代码
46 | > 4. Scope hoisting和Tree-shaking
47 | > 5. 自动启用uglifyjs对代码进行压缩
48 |
49 | webpack一直以来最饱受诟病的就是其配置门槛极高,配置内容复杂而繁琐,容易让人从入门到放弃,而它的后起之秀如rollup,parcel等均在配置流程上做了极大的优化,做到开箱即用,webpack在V4中应该也从中借鉴了不少经验来提升自身的配置效率,详见内容可以参考这篇文章[《webpack 4: mode and optimization》](https://medium.com/webpack/webpack-4-mode-and-optimization-5423a6bc597a)
50 |
51 |
52 | ## 三、再见commonchunk,你好optimization
53 |
54 | 从webpack4开始官方移除了commonchunk插件,改用了optimization属性进行更加灵活的配置,这也应该是从V3升级到V4的代码修改过程中最为复杂的一部分,下面的代码即是optimize.splitChunks 中的一些配置参考,
55 |
56 | ```javascript
57 | module.exports = {
58 | optimization: {
59 | runtimeChunk: {
60 | name: 'manifest'
61 | },
62 | minimizer: true, // [new UglifyJsPlugin({...})]
63 | splitChunks:{
64 | chunks: 'async',
65 | minSize: 30000,
66 | minChunks: 1,
67 | maxAsyncRequests: 5,
68 | maxInitialRequests: 3,
69 | name: false,
70 | cacheGroups: {
71 | vendor: {
72 | name: 'vendor',
73 | chunks: 'initial',
74 | priority: -10,
75 | reuseExistingChunk: false,
76 | test: /node_modules\/(.*)\.js/
77 | },
78 | styles: {
79 | name: 'styles',
80 | test: /\.(scss|css)$/,
81 | chunks: 'all',
82 | minChunks: 1,
83 | reuseExistingChunk: true,
84 | enforce: true
85 | }
86 | }
87 | }
88 | }
89 | }
90 | ```
91 |
92 | ### 从中我们不难发现,其主要变化有如下几个方面:
93 |
94 | > 1. commonchunk配置项被彻底去掉,之前需要通过配置两次new webpack.optimize.CommonsChunkPlugin来分别获取vendor和manifest的通用chunk方式已经做了整合,** 直接在optimization中配置runtimeChunk和splitChunks即可 ** ,提取功能也更为强大,具体配置见:[splitChunks](https://webpack.js.org/plugins/split-chunks-plugin/#optimization-splitchunks)
95 |
96 | >1. runtimeChunk可以配置成true,single或者对象,用自动计算当前构建的一些基础chunk信息,类似之前版本中的manifest信息获取方式。
97 |
98 | >1. webpack.optimize.UglifyJsPlugin现在也不需要了,只需要使用optimization.minimize为true就行,production mode下面自动为true,当然如果想使用第三方的压缩插件也可以在optimization.minimizer的数组列表中进行配置
99 |
100 | ## 四、ExtractTextWebpackPlugin调整,建议选用新的CSS文件提取插件mini-css-extract-plugin
101 |
102 | 由于webpack4以后对css模块支持的逐步完善和commonchunk插件的移除,在处理css文件提取的计算方式上也做了些调整,之前我们首选使用的[extract-text-webpack-plugin](https://github.com/webpack-contrib/extract-text-webpack-plugin)也完成了其历史使命,将让位于[mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin)
103 |
104 | ### 基本配置如下:
105 |
106 | ```javascript
107 | const MiniCssExtractPlugin = require("mini-css-extract-plugin");
108 | module.exports = {
109 | plugins: [
110 | new MiniCssExtractPlugin({
111 | // Options similar to the same options in webpackOptions.output
112 | // both options are optional
113 | filename: "[name].css",
114 | chunkFilename: "[id].css"
115 | })
116 | ],
117 | module: {
118 | rules: [
119 | {
120 | test: /\.css$/,
121 | use: [
122 | MiniCssExtractPlugin.loader, // replace ExtractTextPlugin.extract({..})
123 | "css-loader"
124 | ]
125 | }
126 | ]
127 | }
128 | }
129 | ```
130 |
131 | ### 生产环境下的配置优化:
132 |
133 | ```javascript
134 | const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
135 | const MiniCssExtractPlugin = require("mini-css-extract-plugin");
136 | const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
137 | module.exports = {
138 | optimization: {
139 | minimizer: [
140 | new UglifyJsPlugin({
141 | cache: true,
142 | parallel: true,
143 | sourceMap: true
144 | }),
145 | new OptimizeCSSAssetsPlugin({
146 | assetNameRegExp: /\.css$/g,
147 | cssProcessor: require('cssnano'),
148 | cssProcessorOptions: {
149 | autoprefixer: false,
150 | preset: [
151 | 'default',
152 | {
153 | discardComments: {
154 | removeAll: true
155 | }
156 | }
157 | ]
158 | }
159 | }) // use OptimizeCSSAssetsPlugin
160 | ]
161 | },
162 | plugins: [
163 | new MiniCssExtractPlugin({
164 | filename: 'css/app.[name].css',
165 | chunkFilename: 'css/app.[contenthash:12].css' // use contenthash *
166 | })
167 | ]
168 | }
169 |
170 | ```
171 |
172 | ### 将多个css chunk合并成一个css文件
173 |
174 | ```javascript
175 | const MiniCssExtractPlugin = require("mini-css-extract-plugin");
176 | module.exports = {
177 | optimization: {
178 | splitChunks: {
179 | cacheGroups: {
180 | styles: {
181 | name: 'styles',
182 | test: /\.scss|css$/,
183 | chunks: 'all', // merge all the css chunk to one file
184 | enforce: true
185 | }
186 | }
187 | }
188 | }
189 | }
190 | ```
191 |
192 |
193 | ## 五、其他调整项备忘
194 |
195 | > 1. NoEmitOnErrorsPlugin- > optimization.noEmitOnErrors(默认情况下处于生产模式)
196 | > 2. ModuleConcatenationPlugin- > optimization.concatenateModules(默认情况下处于生产模式)
197 | > 3. NamedModulesPlugin- > optimization.namedModules(在开发模式下默认开启)
198 | > 4. CommonsChunkPlugin 被删除 - > optimization.splitChunks
199 | > 5. webpack命令优化 -> 发布了独立的 [webpack-cli](https://webpack.js.org/api/cli/) 命令行工具包
200 | > 6. webpack-dev-server -> 建议升级到最新版本
201 | > 7. html-webpack-plugin -> 建议升级到的最新版本
202 | > 8. file-loader -> 建议升级到最新版本
203 | > 9. url-loader -> 建议升级到最新版本
204 |
205 | ## 六、参考工程
206 | [webpack4配置工程实例](https://github.com/taikongfeizhu/react-mobx-react-router4-boilerplate)
207 |
208 | ## 七、参阅资料
209 |
210 | 1. [webpack4](https://blog.csdn.net/qq_20334295/article/details/79401231)
211 | 2. [webpack4发布概览](https://zhuanlan.zhihu.com/p/34028750)
212 | 3. [webpack 4: mode and optimization](https://medium.com/webpack/webpack-4-mode-and-optimization-5423a6bc597a)
213 | 4. [webpack4新特性介绍](https://segmentfault.com/a/1190000013970017)
214 | 5. [webpack4升级指北](https://segmentfault.com/a/1190000013420383)
215 | 6. [webpack4升级指南以及从webpack3.x迁移](https://blog.csdn.net/qq_26733915/article/details/79446460)
216 |
--------------------------------------------------------------------------------
/public/lib/min/manifest.json:
--------------------------------------------------------------------------------
1 | {"name":"lib","content":{"./node_modules/prop-types/index.js":{"id":0,"buildMeta":{"providedExports":true}},"./node_modules/react/index.js":{"id":1,"buildMeta":{"providedExports":true}},"./node_modules/history/es/PathUtils.js":{"id":2,"buildMeta":{"exportsType":"namespace","providedExports":["addLeadingSlash","stripLeadingSlash","hasBasename","stripBasename","stripTrailingSlash","parsePath","createPath"]}},"./node_modules/invariant/browser.js":{"id":3,"buildMeta":{"providedExports":true}},"./node_modules/history/es/index.js":{"id":4,"buildMeta":{"exportsType":"namespace","providedExports":["createBrowserHistory","createHashHistory","createMemoryHistory","createLocation","locationsAreEqual","parsePath","createPath"]}},"./node_modules/warning/browser.js":{"id":5,"buildMeta":{"providedExports":true}},"./node_modules/react-router/node_modules/warning/warning.js":{"id":6,"buildMeta":{"providedExports":true}},"./node_modules/history/es/LocationUtils.js":{"id":7,"buildMeta":{"exportsType":"namespace","providedExports":["createLocation","locationsAreEqual"]}},"./node_modules/mobx/lib/mobx.module.js":{"id":8,"buildMeta":{"exportsType":"namespace","providedExports":["Reaction","untracked","IDerivationState","createAtom","spy","comparer","isObservableObject","isBoxedObservable","isObservableArray","ObservableMap","isObservableMap","transaction","observable","computed","isObservable","isObservableProp","isComputed","isComputedProp","extendObservable","observe","intercept","autorun","reaction","when","action","isAction","runInAction","keys","values","entries","set","remove","has","get","decorate","configure","onBecomeObserved","onBecomeUnobserved","flow","toJS","trace","getDependencyTree","getObserverTree","_resetGlobalState","_getGlobalState","getDebugName","getAtom","_getAdministration","_allowStateChanges","_allowStateChangesInsideComputed","isArrayLike","$mobx","_isComputingDerivation","onReactionError","_interceptReads"]}},"./node_modules/history/es/DOMUtils.js":{"id":9,"buildMeta":{"exportsType":"namespace","providedExports":["canUseDOM","addEventListener","removeEventListener","getConfirmation","supportsHistory","supportsPopStateOnHashChange","supportsGoWithoutReloadUsingHash","isExtraneousPopstateEvent"]}},"./node_modules/axios/lib/utils.js":{"id":10,"buildMeta":{"providedExports":true}},"./node_modules/react-router/es/Router.js":{"id":11,"buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router/es/matchPath.js":{"id":12,"buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router/es/generatePath.js":{"id":13,"buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router/es/Route.js":{"id":14,"buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router-dom/es/Router.js":{"id":15,"buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/history/es/createTransitionManager.js":{"id":16,"buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-dom/index.js":{"id":17,"buildMeta":{"providedExports":true}},"./node_modules/react-router-dom/es/Link.js":{"id":18,"buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router/es/MemoryRouter.js":{"id":19,"buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router/es/Prompt.js":{"id":20,"buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router/es/Redirect.js":{"id":21,"buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router/es/StaticRouter.js":{"id":22,"buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router/es/Switch.js":{"id":23,"buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router/es/withRouter.js":{"id":24,"buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router-dom/es/Route.js":{"id":25,"buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/path-to-regexp/index.js":{"id":26,"buildMeta":{"providedExports":true}},"./node_modules/react-router-dom/node_modules/warning/warning.js":{"id":27,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/defaults.js":{"id":28,"buildMeta":{"providedExports":true}},"./node_modules/object-assign/index.js":{"id":29,"buildMeta":{"providedExports":true}},"./node_modules/webpack/buildin/global.js":{"id":30,"buildMeta":{"providedExports":true}},"./node_modules/history/es/createBrowserHistory.js":{"id":31,"buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/history/es/createHashHistory.js":{"id":32,"buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/history/es/createMemoryHistory.js":{"id":33,"buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router-dom/es/BrowserRouter.js":{"id":34,"buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router-dom/es/HashRouter.js":{"id":35,"buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router-dom/es/MemoryRouter.js":{"id":36,"buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router-dom/es/NavLink.js":{"id":37,"buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router-dom/es/Prompt.js":{"id":38,"buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router-dom/es/Redirect.js":{"id":39,"buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router-dom/es/StaticRouter.js":{"id":40,"buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router-dom/es/Switch.js":{"id":41,"buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router-dom/es/generatePath.js":{"id":42,"buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router-dom/es/matchPath.js":{"id":43,"buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router-dom/es/withRouter.js":{"id":44,"buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/node-libs-browser/node_modules/process/browser.js":{"id":45,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/helpers/bind.js":{"id":46,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/adapters/xhr.js":{"id":47,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/core/createError.js":{"id":48,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/cancel/isCancel.js":{"id":49,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/cancel/Cancel.js":{"id":50,"buildMeta":{"providedExports":true}},"./node_modules/resolve-pathname/index.js":{"id":51,"buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/value-equal/index.js":{"id":52,"buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/hoist-non-react-statics/dist/hoist-non-react-statics.cjs.js":{"id":53,"buildMeta":{"providedExports":true}},"./node_modules/react/cjs/react.production.min.js":{"id":55,"buildMeta":{"providedExports":true}},"./node_modules/react-dom/cjs/react-dom.production.min.js":{"id":56,"buildMeta":{"providedExports":true}},"./node_modules/scheduler/index.js":{"id":57,"buildMeta":{"providedExports":true}},"./node_modules/scheduler/cjs/scheduler.production.min.js":{"id":58,"buildMeta":{"providedExports":true}},"./node_modules/react-router/es/index.js":{"id":59,"buildMeta":{"exportsType":"namespace","providedExports":["MemoryRouter","Prompt","Redirect","Route","Router","StaticRouter","Switch","generatePath","matchPath","withRouter"]}},"./node_modules/prop-types/factoryWithThrowingShims.js":{"id":60,"buildMeta":{"providedExports":true}},"./node_modules/prop-types/lib/ReactPropTypesSecret.js":{"id":61,"buildMeta":{"providedExports":true}},"./node_modules/path-to-regexp/node_modules/isarray/index.js":{"id":62,"buildMeta":{"providedExports":true}},"./node_modules/react-router-dom/es/index.js":{"id":63,"buildMeta":{"exportsType":"namespace","providedExports":["BrowserRouter","HashRouter","Link","MemoryRouter","NavLink","Prompt","Redirect","Route","Router","StaticRouter","Switch","generatePath","matchPath","withRouter"]}},"./node_modules/mobx-react/index.module.js":{"id":64,"buildMeta":{"exportsType":"namespace","providedExports":["propTypes","PropTypes","onError","observer","Observer","renderReporter","componentByNodeRegistery","componentByNodeRegistry","trackComponents","useStaticRendering","Provider","inject","disposeOnUnmount"]}},"./node_modules/mobx-react-router/dist/mobx-react-router.js":{"id":65,"buildMeta":{"providedExports":true}},"./node_modules/axios/index.js":{"id":66,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/axios.js":{"id":67,"buildMeta":{"providedExports":true}},"./node_modules/is-buffer/index.js":{"id":68,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/core/Axios.js":{"id":69,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/helpers/normalizeHeaderName.js":{"id":70,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/core/settle.js":{"id":71,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/core/enhanceError.js":{"id":72,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/helpers/buildURL.js":{"id":73,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/helpers/parseHeaders.js":{"id":74,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/helpers/isURLSameOrigin.js":{"id":75,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/helpers/btoa.js":{"id":76,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/helpers/cookies.js":{"id":77,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/core/InterceptorManager.js":{"id":78,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/core/dispatchRequest.js":{"id":79,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/core/transformData.js":{"id":80,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/helpers/isAbsoluteURL.js":{"id":81,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/helpers/combineURLs.js":{"id":82,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/cancel/CancelToken.js":{"id":83,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/helpers/spread.js":{"id":84,"buildMeta":{"providedExports":true}},"./node_modules/classnames/index.js":{"id":85,"buildMeta":{"providedExports":true}}}}
--------------------------------------------------------------------------------
/public/lib/debug/manifest.json:
--------------------------------------------------------------------------------
1 | {"name":"lib","content":{"./node_modules/react-router-dom/node_modules/warning/warning.js":{"id":"./node_modules/react-router-dom/node_modules/warning/warning.js","buildMeta":{"providedExports":true}},"./node_modules/react/cjs/react.development.js":{"id":"./node_modules/react/cjs/react.development.js","buildMeta":{"providedExports":true}},"./node_modules/object-assign/index.js":{"id":"./node_modules/object-assign/index.js","buildMeta":{"providedExports":true}},"./node_modules/prop-types/checkPropTypes.js":{"id":"./node_modules/prop-types/checkPropTypes.js","buildMeta":{"providedExports":true}},"./node_modules/prop-types/lib/ReactPropTypesSecret.js":{"id":"./node_modules/prop-types/lib/ReactPropTypesSecret.js","buildMeta":{"providedExports":true}},"./node_modules/react-dom/index.js":{"id":"./node_modules/react-dom/index.js","buildMeta":{"providedExports":true}},"./node_modules/react-dom/cjs/react-dom.development.js":{"id":"./node_modules/react-dom/cjs/react-dom.development.js","buildMeta":{"providedExports":true}},"./node_modules/scheduler/index.js":{"id":"./node_modules/scheduler/index.js","buildMeta":{"providedExports":true}},"./node_modules/scheduler/cjs/scheduler.development.js":{"id":"./node_modules/scheduler/cjs/scheduler.development.js","buildMeta":{"providedExports":true}},"./node_modules/webpack/buildin/global.js":{"id":"./node_modules/webpack/buildin/global.js","buildMeta":{"providedExports":true}},"./node_modules/scheduler/tracing.js":{"id":"./node_modules/scheduler/tracing.js","buildMeta":{"providedExports":true}},"./node_modules/scheduler/cjs/scheduler-tracing.development.js":{"id":"./node_modules/scheduler/cjs/scheduler-tracing.development.js","buildMeta":{"providedExports":true}},"./node_modules/react-router/es/index.js":{"id":"./node_modules/react-router/es/index.js","buildMeta":{"exportsType":"namespace","providedExports":["MemoryRouter","Prompt","Redirect","Route","Router","StaticRouter","Switch","generatePath","matchPath","withRouter"]}},"./node_modules/react-router/es/MemoryRouter.js":{"id":"./node_modules/react-router/es/MemoryRouter.js","buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router/node_modules/warning/warning.js":{"id":"./node_modules/react-router/node_modules/warning/warning.js","buildMeta":{"providedExports":true}},"./node_modules/prop-types/index.js":{"id":"./node_modules/prop-types/index.js","buildMeta":{"providedExports":true}},"./node_modules/prop-types/factoryWithTypeCheckers.js":{"id":"./node_modules/prop-types/factoryWithTypeCheckers.js","buildMeta":{"providedExports":true}},"./node_modules/history/es/index.js":{"id":"./node_modules/history/es/index.js","buildMeta":{"exportsType":"namespace","providedExports":["createBrowserHistory","createHashHistory","createMemoryHistory","createLocation","locationsAreEqual","parsePath","createPath"]}},"./node_modules/history/es/createBrowserHistory.js":{"id":"./node_modules/history/es/createBrowserHistory.js","buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/warning/browser.js":{"id":"./node_modules/warning/browser.js","buildMeta":{"providedExports":true}},"./node_modules/invariant/browser.js":{"id":"./node_modules/invariant/browser.js","buildMeta":{"providedExports":true}},"./node_modules/history/es/LocationUtils.js":{"id":"./node_modules/history/es/LocationUtils.js","buildMeta":{"exportsType":"namespace","providedExports":["createLocation","locationsAreEqual"]}},"./node_modules/resolve-pathname/index.js":{"id":"./node_modules/resolve-pathname/index.js","buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/value-equal/index.js":{"id":"./node_modules/value-equal/index.js","buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/history/es/PathUtils.js":{"id":"./node_modules/history/es/PathUtils.js","buildMeta":{"exportsType":"namespace","providedExports":["addLeadingSlash","stripLeadingSlash","hasBasename","stripBasename","stripTrailingSlash","parsePath","createPath"]}},"./node_modules/history/es/createTransitionManager.js":{"id":"./node_modules/history/es/createTransitionManager.js","buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/history/es/DOMUtils.js":{"id":"./node_modules/history/es/DOMUtils.js","buildMeta":{"exportsType":"namespace","providedExports":["canUseDOM","addEventListener","removeEventListener","getConfirmation","supportsHistory","supportsPopStateOnHashChange","supportsGoWithoutReloadUsingHash","isExtraneousPopstateEvent"]}},"./node_modules/history/es/createHashHistory.js":{"id":"./node_modules/history/es/createHashHistory.js","buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/history/es/createMemoryHistory.js":{"id":"./node_modules/history/es/createMemoryHistory.js","buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router/es/Router.js":{"id":"./node_modules/react-router/es/Router.js","buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router/es/Prompt.js":{"id":"./node_modules/react-router/es/Prompt.js","buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router/es/Redirect.js":{"id":"./node_modules/react-router/es/Redirect.js","buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router/es/generatePath.js":{"id":"./node_modules/react-router/es/generatePath.js","buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/path-to-regexp/index.js":{"id":"./node_modules/path-to-regexp/index.js","buildMeta":{"providedExports":true}},"./node_modules/path-to-regexp/node_modules/isarray/index.js":{"id":"./node_modules/path-to-regexp/node_modules/isarray/index.js","buildMeta":{"providedExports":true}},"./node_modules/react-router/es/Route.js":{"id":"./node_modules/react-router/es/Route.js","buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router/es/matchPath.js":{"id":"./node_modules/react-router/es/matchPath.js","buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router/es/StaticRouter.js":{"id":"./node_modules/react-router/es/StaticRouter.js","buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router/es/Switch.js":{"id":"./node_modules/react-router/es/Switch.js","buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router/es/withRouter.js":{"id":"./node_modules/react-router/es/withRouter.js","buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/hoist-non-react-statics/dist/hoist-non-react-statics.cjs.js":{"id":"./node_modules/hoist-non-react-statics/dist/hoist-non-react-statics.cjs.js","buildMeta":{"providedExports":true}},"./node_modules/react-router-dom/es/index.js":{"id":"./node_modules/react-router-dom/es/index.js","buildMeta":{"exportsType":"namespace","providedExports":["BrowserRouter","HashRouter","Link","MemoryRouter","NavLink","Prompt","Redirect","Route","Router","StaticRouter","Switch","generatePath","matchPath","withRouter"]}},"./node_modules/react-router-dom/es/BrowserRouter.js":{"id":"./node_modules/react-router-dom/es/BrowserRouter.js","buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react/index.js":{"id":"./node_modules/react/index.js","buildMeta":{"providedExports":true}},"./node_modules/react-router-dom/es/Router.js":{"id":"./node_modules/react-router-dom/es/Router.js","buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router-dom/es/HashRouter.js":{"id":"./node_modules/react-router-dom/es/HashRouter.js","buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router-dom/es/Link.js":{"id":"./node_modules/react-router-dom/es/Link.js","buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router-dom/es/MemoryRouter.js":{"id":"./node_modules/react-router-dom/es/MemoryRouter.js","buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router-dom/es/NavLink.js":{"id":"./node_modules/react-router-dom/es/NavLink.js","buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router-dom/es/Route.js":{"id":"./node_modules/react-router-dom/es/Route.js","buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router-dom/es/Prompt.js":{"id":"./node_modules/react-router-dom/es/Prompt.js","buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router-dom/es/Redirect.js":{"id":"./node_modules/react-router-dom/es/Redirect.js","buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router-dom/es/StaticRouter.js":{"id":"./node_modules/react-router-dom/es/StaticRouter.js","buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router-dom/es/Switch.js":{"id":"./node_modules/react-router-dom/es/Switch.js","buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router-dom/es/generatePath.js":{"id":"./node_modules/react-router-dom/es/generatePath.js","buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router-dom/es/matchPath.js":{"id":"./node_modules/react-router-dom/es/matchPath.js","buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/react-router-dom/es/withRouter.js":{"id":"./node_modules/react-router-dom/es/withRouter.js","buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/mobx/lib/mobx.module.js":{"id":"./node_modules/mobx/lib/mobx.module.js","buildMeta":{"exportsType":"namespace","providedExports":["Reaction","untracked","IDerivationState","createAtom","spy","comparer","isObservableObject","isBoxedObservable","isObservableArray","ObservableMap","isObservableMap","transaction","observable","computed","isObservable","isObservableProp","isComputed","isComputedProp","extendObservable","observe","intercept","autorun","reaction","when","action","isAction","runInAction","keys","values","entries","set","remove","has","get","decorate","configure","onBecomeObserved","onBecomeUnobserved","flow","toJS","trace","getDependencyTree","getObserverTree","_resetGlobalState","_getGlobalState","getDebugName","getAtom","_getAdministration","_allowStateChanges","_allowStateChangesInsideComputed","isArrayLike","$mobx","_isComputingDerivation","onReactionError","_interceptReads"]}},"./node_modules/node-libs-browser/node_modules/process/browser.js":{"id":"./node_modules/node-libs-browser/node_modules/process/browser.js","buildMeta":{"providedExports":true}},"./node_modules/mobx-react/index.module.js":{"id":"./node_modules/mobx-react/index.module.js","buildMeta":{"exportsType":"namespace","providedExports":["propTypes","PropTypes","onError","observer","Observer","renderReporter","componentByNodeRegistery","componentByNodeRegistry","trackComponents","useStaticRendering","Provider","inject","disposeOnUnmount"]}},"./node_modules/mobx-react-router/dist/mobx-react-router.js":{"id":"./node_modules/mobx-react-router/dist/mobx-react-router.js","buildMeta":{"providedExports":true}},"./node_modules/axios/index.js":{"id":"./node_modules/axios/index.js","buildMeta":{"providedExports":true}},"./node_modules/axios/lib/axios.js":{"id":"./node_modules/axios/lib/axios.js","buildMeta":{"providedExports":true}},"./node_modules/axios/lib/utils.js":{"id":"./node_modules/axios/lib/utils.js","buildMeta":{"providedExports":true}},"./node_modules/axios/lib/helpers/bind.js":{"id":"./node_modules/axios/lib/helpers/bind.js","buildMeta":{"providedExports":true}},"./node_modules/is-buffer/index.js":{"id":"./node_modules/is-buffer/index.js","buildMeta":{"providedExports":true}},"./node_modules/axios/lib/core/Axios.js":{"id":"./node_modules/axios/lib/core/Axios.js","buildMeta":{"providedExports":true}},"./node_modules/axios/lib/defaults.js":{"id":"./node_modules/axios/lib/defaults.js","buildMeta":{"providedExports":true}},"./node_modules/axios/lib/helpers/normalizeHeaderName.js":{"id":"./node_modules/axios/lib/helpers/normalizeHeaderName.js","buildMeta":{"providedExports":true}},"./node_modules/axios/lib/adapters/xhr.js":{"id":"./node_modules/axios/lib/adapters/xhr.js","buildMeta":{"providedExports":true}},"./node_modules/axios/lib/core/settle.js":{"id":"./node_modules/axios/lib/core/settle.js","buildMeta":{"providedExports":true}},"./node_modules/axios/lib/core/createError.js":{"id":"./node_modules/axios/lib/core/createError.js","buildMeta":{"providedExports":true}},"./node_modules/axios/lib/core/enhanceError.js":{"id":"./node_modules/axios/lib/core/enhanceError.js","buildMeta":{"providedExports":true}},"./node_modules/axios/lib/helpers/buildURL.js":{"id":"./node_modules/axios/lib/helpers/buildURL.js","buildMeta":{"providedExports":true}},"./node_modules/axios/lib/helpers/parseHeaders.js":{"id":"./node_modules/axios/lib/helpers/parseHeaders.js","buildMeta":{"providedExports":true}},"./node_modules/axios/lib/helpers/isURLSameOrigin.js":{"id":"./node_modules/axios/lib/helpers/isURLSameOrigin.js","buildMeta":{"providedExports":true}},"./node_modules/axios/lib/helpers/btoa.js":{"id":"./node_modules/axios/lib/helpers/btoa.js","buildMeta":{"providedExports":true}},"./node_modules/axios/lib/helpers/cookies.js":{"id":"./node_modules/axios/lib/helpers/cookies.js","buildMeta":{"providedExports":true}},"./node_modules/axios/lib/core/InterceptorManager.js":{"id":"./node_modules/axios/lib/core/InterceptorManager.js","buildMeta":{"providedExports":true}},"./node_modules/axios/lib/core/dispatchRequest.js":{"id":"./node_modules/axios/lib/core/dispatchRequest.js","buildMeta":{"providedExports":true}},"./node_modules/axios/lib/core/transformData.js":{"id":"./node_modules/axios/lib/core/transformData.js","buildMeta":{"providedExports":true}},"./node_modules/axios/lib/cancel/isCancel.js":{"id":"./node_modules/axios/lib/cancel/isCancel.js","buildMeta":{"providedExports":true}},"./node_modules/axios/lib/helpers/isAbsoluteURL.js":{"id":"./node_modules/axios/lib/helpers/isAbsoluteURL.js","buildMeta":{"providedExports":true}},"./node_modules/axios/lib/helpers/combineURLs.js":{"id":"./node_modules/axios/lib/helpers/combineURLs.js","buildMeta":{"providedExports":true}},"./node_modules/axios/lib/cancel/Cancel.js":{"id":"./node_modules/axios/lib/cancel/Cancel.js","buildMeta":{"providedExports":true}},"./node_modules/axios/lib/cancel/CancelToken.js":{"id":"./node_modules/axios/lib/cancel/CancelToken.js","buildMeta":{"providedExports":true}},"./node_modules/axios/lib/helpers/spread.js":{"id":"./node_modules/axios/lib/helpers/spread.js","buildMeta":{"providedExports":true}},"./node_modules/classnames/index.js":{"id":"./node_modules/classnames/index.js","buildMeta":{"providedExports":true}}}}
--------------------------------------------------------------------------------