{"action":"created","issue":{"url":"https://api.github.com/repos/brickspert/blog/issues/1","repository_url":"https://api.github.com/repos/brickspert/blog","labels_url":"https://api.github.com/repos/brickspert/blog/issues/1/labels{/name}","comments_url":"https://api.github.com/repos/brickspert/blog/issues/1/comments","events_url":"https://api.github.com/repos/brickspert/blog/issues/1/events","html_url":"https://github.com/brickspert/blog/issues/1","id":254803621,"number":1,"title":"从零搭建React全家桶框架教程","user":{"login":"brickspert","id":12526493,"avatar_url":"https://avatars0.githubusercontent.com/u/12526493?v=4","gravatar_id":"","url":"https://api.github.com/users/brickspert","html_url":"https://github.com/brickspert","followers_url":"https://api.github.com/users/brickspert/followers","following_url":"https://api.github.com/users/brickspert/following{/other_user}","gists_url":"https://api.github.com/users/brickspert/gists{/gist_id}","starred_url":"https://api.github.com/users/brickspert/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/brickspert/subscriptions","organizations_url":"https://api.github.com/users/brickspert/orgs","repos_url":"https://api.github.com/users/brickspert/repos","events_url":"https://api.github.com/users/brickspert/events{/privacy}","received_events_url":"https://api.github.com/users/brickspert/received_events","type":"User","site_admin":false},"labels":[{"id":692997318,"url":"https://api.github.com/repos/brickspert/blog/labels/react","name":"react","color":"0e8a16","default":false}],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":257,"created_at":"2017-09-02T10:30:31Z","updated_at":"2018-03-20T07:14:44Z","closed_at":null,"author_association":"OWNER","body":"# 从零搭建React全家桶框架教程\r\n\r\n源码地址：[https://github.com/brickspert/react-family](https://github.com/brickspert/react-family)\r\n提问反馈：[blog](https://github.com/brickspert/blog/issues/1)\r\n\r\n## 大家阅读的时候，照着目录来阅读哦，有些章节不在文章里面。要点链接的~\r\n\r\n# 目录\r\n\r\n1. [写在前面](#intro)\r\n2. [说明](#explain)\r\n3. [init项目](#init)\r\n4. [webpack](#webpack)\r\n5. [babel](#babel)\r\n6. [react](#react)\r\n7. [命令优化](#command)\r\n8. [react-router](#react-router)\r\n9. [webpack-dev-server](#webpack-dev-server)\r\n10. [模块热替换（Hot Module Replacement）](#hot-module-replacement)\r\n11. [文件路径优化](#path)\r\n12. [redux](#redux)\r\n13. [devtool优化](#devtool)\r\n14. [编译css](#css)\r\n15. [编译图片](#image)\r\n16. [按需加载](#code-split)\r\n17. [缓存](#cache)\r\n18. [HtmlWebpackPlugin](#HtmlWebpackPlugin)\r\n19. [提取公共代码](#common-code)\r\n20. [生产坏境构建](#product)\r\n21. [文件压缩](#uglify)\r\n22. [指定环境](#env)\r\n23. [优化缓存](#op-cache)\r\n24. [public path](#publicPath)\r\n25. [打包优化](#op-build)\r\n26. [抽取css](#extract-text)\r\n27. [使用`axios`和`middleware`优化API请求](#op-http)\r\n28. [合并提取`webpack`公共配置 webpack-common-config](https://github.com/brickspert/blog/issues/1#issuecomment-326883588)(2017-09-04)\r\n29. [优化目录结构并增加404页面](https://github.com/brickspert/blog/issues/1#issuecomment-326907901)(2017-09-04)\r\n30. [加入 babel-plugin-transform-runtime 和 babel-polyfill](https://github.com/brickspert/blog/issues/1#issuecomment-330022980)(2017-09-17)\r\n31. [集成PostCSS](https://github.com/brickspert/blog/issues/1#issuecomment-330024531)(2017-09-17)\r\n32. [redux 模块热替换配置](https://github.com/brickspert/blog/issues/1#issuecomment-332186457)(2017-09-26)\r\n33. [模拟AJAX数据之Mock.js](https://github.com/brickspert/blog/issues/1#issuecomment-346024891)(2017-11-21)\r\n34. [使用 CSS Modules](https://github.com/brickspert/blog/issues/1#issuecomment-351330256)(2017-12-13)\r\n35. [使用 json-server 代替 Mock.js](https://github.com/brickspert/blog/issues/1#issuecomment-352456060)(2017-12-18)\r\n36. [问题修复](https://github.com/brickspert/blog/issues/1#issuecomment-363419989)\r\n# <div id=\"intro\">写在前面</div>\r\n\r\n当我第一次跟着项目做`react`项目的时候，由于半截加入的，对框架了解甚少，只能跟着别人的样板写。对整个框架没有一点了解。\r\n\r\n做项目，总是要解决各种问题的，所以每个地方都需要去了解，但是对整个框架没有一个整体的了解，实在是不行。\r\n\r\n期间，我也跟着别人的搭建框架的教程一步一步的走，但是经常因为自己太菜，走不下去。在经过各种蹂躏之后，对整个框架也有一个大概的了解，\r\n我就想把他写下来，让后来的菜鸟能跟着我的教程对`react`全家桶有一个全面的认识。\r\n\r\n我的这个教程，从新建根文件夹开始，到成型的框架，每个文件为什么要建立？建立了干什么？每个依赖都是干什么的？一步一步写下来，供大家学习。\r\n\r\n当然，这个框架我以后会一直维护的，**也希望大家能一起来完善这个框架**，如果您有任何建议，欢迎在这里留言，欢迎`fork`源码[react-family](https://github.com/brickspert/react-family)。\r\n\r\n我基于该框架`react-family`又做了一个兼容IE8的版本，教程在这里[react-family框架兼容IE8教程](https://github.com/brickspert/blog/issues/5)。\r\n\r\n# <div id=\"explain\">说明</div>\r\n\r\n1. 每个命令行块都是以**根目录**为基础的。例如下面命令行块，都是基于根目录的。\r\n\r\n```javascript\r\ncd src/pages\r\nmkdir Home\r\n```\r\n\r\n2. 技术栈均是目前最新的。\r\n\r\n* react 15.6.1\r\n* react-router-dom 4.2.2\r\n* redux 3.7.2\r\n* webpack 3.5.5\r\n\r\n3. 目录说明\r\n```javascript\r\n│  .babelrc                          #babel配置文件\r\n│  package-lock.json\r\n│  package.json\r\n│  README.MD\r\n│  webpack.config.js                 #webpack生产配置文件\r\n│  webpack.dev.config.js             #webpack开发配置文件\r\n│  \r\n├─dist\r\n├─public                             #公共资源文件\r\n└─src                                #项目源码\r\n    │  index.html                    #index.html模板\r\n    │  index.js                      #入口文件\r\n    │  \r\n    ├─component                      #组建库\r\n    │  └─Hello\r\n    │          Hello.js\r\n    │          \r\n    ├─pages                          #页面目录\r\n    │  ├─Counter\r\n    │  │      Counter.js\r\n    │  │      \r\n    │  ├─Home\r\n    │  │      Home.js\r\n    │  │      \r\n    │  ├─Page1\r\n    │  │  │  Page1.css                #页面样式\r\n    │  │  │  Page1.js\r\n    │  │  │  \r\n    │  │  └─images                    #页面图片\r\n    │  │          brickpsert.jpg\r\n    │  │          \r\n    │  └─UserInfo\r\n    │          UserInfo.js\r\n    │          \r\n    ├─redux\r\n    │  │  reducers.js\r\n    │  │  store.js\r\n    │  │  \r\n    │  ├─actions\r\n    │  │      counter.js\r\n    │  │      userInfo.js\r\n    │  │      \r\n    │  ├─middleware\r\n    │  │      promiseMiddleware.js\r\n    │  │      \r\n    │  └─reducers\r\n    │          counter.js\r\n    │          userInfo.js\r\n    │          \r\n    └─router                        #路由文件\r\n            Bundle.js\r\n            router.js\r\n            \r\n\r\n```\r\n# <div id=\"init\">init项目</div>\r\n\r\n1. 创建文件夹并进入\r\n\r\n    `mkdir react-family && cd react-family`\r\n\r\n2.  init npm\r\n\r\n    `npm init` 按照提示填写项目基本信息\r\n\r\n# <div id=\"webpack\">webpack</div>\r\n\r\n1. 安装 `webpack`\r\n    \r\n    `npm install --save-dev webpack@3`\r\n    \r\n    Q: 什么时候用`--save-dev`，什么时候用`--save`？\r\n    \r\n    A:  `--save-dev` 是你开发时候依赖的东西，`--save` 是你发布之后还依赖的东西。看[这里](https://segmentfault.com/q/1010000005163089)\r\n    \r\n2. 根据[webpack文档](https://doc.webpack-china.org/guides/getting-started)编写最基础的配置文件\r\n\r\n    新建`webpack`开发配置文件 `touch webpack.dev.config.js` \r\n    \r\n    `webpack.dev.config.js`\r\n    \r\n    ```javascript\r\n    const path = require('path');\r\n    \r\n    module.exports = {\r\n     \r\n        /*入口*/\r\n        entry: path.join(__dirname, 'src/index.js'),\r\n        \r\n        /*输出到dist文件夹，输出文件名字为bundle.js*/\r\n        output: {\r\n            path: path.join(__dirname, './dist'),\r\n            filename: 'bundle.js'\r\n        }\r\n    };\r\n    ```\r\n    \r\n3. 学会使用`webpack`编译文件\r\n    \r\n    新建入口文件\r\n    \r\n    `mkdir src && touch ./src/index.js`\r\n    \r\n    `src/index.js` 添加内容\r\n    \r\n    `document.getElementById('app').innerHTML = \"Webpack works\"`\r\n    \r\n    现在我们执行命令 `webpack --config webpack.dev.config.js`\r\n    \r\n    > webpack如果没有全局安装，这里会报错哦。命令建议全局安装，如果编译有问题看这里 [https://github.com/brickspert/blog/issues/1#issuecomment-326899255](https://github.com/brickspert/blog/issues/1#issuecomment-326899255)\r\n\r\n    我们可以看到生成了`dist`文件夹和`bundle.js`。\r\n    \r\n4. 现在我们测试下~\r\n    \r\n    `dist`文件夹下面新建一个`index.html`\r\n    \r\n    `touch ./dist/index.html`\r\n    \r\n    `dist/index.html`填写内容\r\n    \r\n    ```html\r\n    <!doctype html>\r\n    <html lang=\"en\">\r\n    <head>\r\n        <meta charset=\"UTF-8\">\r\n        <title>Document</title>\r\n    </head>\r\n    <body>\r\n    <div id=\"app\"></div>\r\n    <script type=\"text/javascript\" src=\"./bundle.js\" charset=\"utf-8\"></script>\r\n    </body>\r\n    </html>\r\n    ```\r\n    \r\n    用浏览器打开`index.html`,可以看到`Webpack works`!\r\n    \r\n    ![webpack](http://oqcb8djlw.bkt.clouddn.com/webpack.png)\r\n    \r\n    现在回头看下，我们做了什么或者说`webpack`做了什么。\r\n\r\n    把入口文件 `index.js` 经过处理之后，生成 `bundle.js`。就这么简单。\r\n    \r\n# <div id=\"babel\">babel</div>\r\n    \r\n>Babel 把用最新标准编写的 JavaScript 代码向下编译成可以在今天随处可用的版本。 这一过程叫做“源码到源码”编译， 也被称为转换编译。\r\n\r\n通俗的说，就是我们可以用ES6, ES7等来编写代码，Babel会把他们统统转为ES5。\r\n   \r\n   * [babel-core](https://github.com/babel/babel/tree/master/packages/babel-core) 调用Babel的API进行转码\r\n   * [babel-loader](https://github.com/babel/babel-loader)\r\n   * [babel-preset-es2015](https://github.com/babel/babel/tree/master/packages/babel-preset-es2015) 用于解析 ES6\r\n   * [babel-preset-react](https://github.com/babel/babel/tree/master/packages/babel-preset-react) 用于解析 JSX \r\n   * [babel-preset-stage-0](https://github.com/babel/babel/tree/master/packages/babel-preset-stage-0) 用于解析 ES7 提案\r\n   \r\n   `npm install --save-dev babel-core  babel-loader babel-preset-es2015 babel-preset-react babel-preset-stage-0`\r\n   \r\n   \r\n   新建`babel`配置文件`.babelrc`\r\n   \r\n   `touch .babelrc`\r\n   \r\n   `.babelrc`\r\n   \r\n   ```json\r\n    {\r\n      \"presets\": [\r\n        \"es2015\",\r\n        \"react\",\r\n        \"stage-0\"\r\n      ],\r\n      \"plugins\": []\r\n    }\r\n\r\n   ```\r\n   \r\n   修改`webpack.dev.config.js`，增加`babel-loader`！\r\n   \r\n   ```json\r\n    /*src文件夹下面的以.js结尾的文件，要使用babel解析*/\r\n    /*cacheDirectory是用来缓存编译结果，下次编译加速*/\r\n    module: {\r\n        rules: [{\r\n            test: /\\.js$/,\r\n            use: ['babel-loader?cacheDirectory=true'],\r\n            include: path.join(__dirname, 'src')\r\n        }]\r\n    }\r\n```\r\n   \r\n   现在我们简单测试下，是否能正确转义ES6~\r\n    \r\n   修改 `src/index.js`\r\n   \r\n   ```javascript\r\n    /*使用es6的箭头函数*/\r\n    var func = str => {\r\n        document.getElementById('app').innerHTML = str;\r\n    };\r\n    func('我现在在使用Babel!');\r\n```\r\n   \r\n   执行打包命令`webpack --config webpack.dev.config.js`\r\n   \r\n   浏览器打开`index.html`，我们看到正确输出了`我现在在使用Babel!`\r\n   \r\n   ![babel](http://oqcb8djlw.bkt.clouddn.com/babel.png)\r\n   \r\n   然后我们打开打包后的`bundle.js`,翻页到最下面,可以看到箭头函数被转换成普通函数了！\r\n   \r\n   ![babel-bundle.png](http://oqcb8djlw.bkt.clouddn.com/babel-bundle.png)\r\n   \r\n   Q: `babel-preset-state-0`,`babel-preset-state-1`,`babel-preset-state-2`,`babel-preset-state-3`有什么区别？\r\n   \r\n   A: 每一级包含上一级的功能，比如 `state-0`包含`state-1`的功能，以此类推。`state-0`功能最全。具体可以看这篇文章：[babel配置-各阶段的stage的区别](https://www.vanadis.cn/2017/03/18/babel-stage-x/)\r\n   \r\n   \r\n   \r\n参考地址：\r\n\r\n1. https://segmentfault.com/a/1190000008159877\r\n\r\n2. http://www.ruanyifeng.com/blog/2016/01/babel.html\r\n\r\n    \r\n# <div id=\"react\">react</div>\r\n\r\n`npm install --save react react-dom`\r\n\r\n修改 `src/index.js`使用`react`\r\n\r\n```javascript\r\nimport React from 'react';\r\nimport ReactDom from 'react-dom';\r\n\r\nReactDom.render(\r\n    <div>Hello React!</div>, document.getElementById('app'));\r\n```\r\n\r\n执行打包命令`webpack --config webpack.dev.config.js`\r\n\r\n打开`index.html` 看效果。\r\n\r\n我们简单做下改进，把`Hello React`放到组件里面。体现组件化~\r\n\r\n```javascript\r\ncd src\r\nmkdir component\r\ncd component\r\nmkdir Hello\r\ncd Hello\r\ntouch Hello.js\r\n```\r\n按照React语法，写一个Hello组件\r\n\r\n```javascript\r\nimport React, {Component} from 'react';\r\n\r\nexport default class Hello extends Component {\r\n    render() {\r\n        return (\r\n            <div>\r\n                Hello,React!\r\n            </div>\r\n        )\r\n    }\r\n}\r\n```\r\n\r\n然后让我们修改`src/index.js`，引用`Hello`组件！\r\n\r\n`src/index.js`\r\n\r\n```javascript\r\nimport React from 'react';\r\nimport ReactDom from 'react-dom';\r\nimport Hello from './component/Hello/Hello';\r\n\r\nReactDom.render(\r\n    <Hello/>, document.getElementById('app'));\r\n```\r\n在**根目录**执行打包命令\r\n\r\n`webpack --config webpack.dev.config.js`\r\n\r\n打开`index.html`看效果咯~\r\n\r\n# <div id=\"command\">命令优化</div>\r\n\r\nQ：每次打包都得在根目录执行这么一长串命令`webpack --config webpack.dev.config.js`,能不打这么长吗？\r\n\r\nA：修改`package.json`里面的`script`，增加`dev-build`。\r\n\r\n`package.json`\r\n\r\n```json\r\n  \"scripts\": {\r\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\",\r\n    \"dev-build\": \"webpack --config webpack.dev.config.js\"\r\n  }\r\n```\r\n现在我们打包只需要执行`npm run dev-build`就可以啦！\r\n\r\n参考地址：\r\n\r\nhttp://www.ruanyifeng.com/blog/2016/10/npm_scripts.html\r\n\r\n# <div id=\"react-router\">react-router</div>\r\n\r\n`npm install  --save react-router-dom`\r\n\r\n新建`router`文件夹和组件\r\n\r\n```\r\ncd src\r\nmkdir router && touch router/router.js\r\n```\r\n按照`react-router`[文档](http://reacttraining.cn/web/guides/quick-start)编辑一个最基本的`router.js`。包含两个页面`home`和`page1`。\r\n\r\n`src/router/router.js`\r\n```javascript\r\nimport React from 'react';\r\n\r\nimport {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';\r\n\r\nimport Home from '../pages/Home/Home';\r\nimport Page1 from '../pages/Page1/Page1';\r\n\r\n\r\nconst getRouter = () => (\r\n    <Router>\r\n        <div>\r\n            <ul>\r\n                <li><Link to=\"/\">首页</Link></li>\r\n                <li><Link to=\"/page1\">Page1</Link></li>\r\n            </ul>\r\n            <Switch>\r\n                <Route exact path=\"/\" component={Home}/>\r\n                <Route path=\"/page1\" component={Page1}/>\r\n            </Switch>\r\n        </div>\r\n    </Router>\r\n);\r\n\r\nexport default getRouter;\r\n```\r\n\r\n新建页面文件夹\r\n\r\n```\r\ncd src\r\nmkdir pages\r\n```\r\n\r\n新建两个页面 `Home`,`Page1`\r\n\r\n```\r\ncd src/pages\r\nmkdir Home && touch Home/Home.js\r\nmkdir Page1 && touch Page1/Page1.js\r\n```\r\n\r\n填充内容：\r\n\r\n`src/pages/Home/Home.js`\r\n\r\n```javascript\r\nimport React, {Component} from 'react';\r\n\r\nexport default class Home extends Component {\r\n    render() {\r\n        return (\r\n            <div>\r\n                this is home~\r\n            </div>\r\n        )\r\n    }\r\n}\r\n```\r\nPage1.js\r\n\r\n```javascript\r\nimport React, {Component} from 'react';\r\n\r\nexport default class Page1 extends Component {\r\n    render() {\r\n        return (\r\n            <div>\r\n                this is Page1~\r\n            </div>\r\n        )\r\n    }\r\n}\r\n```\r\n\r\n现在路由和页面建好了，我们在入口文件`src/index.js`引用Router。\r\n\r\n修改`src/index.js`\r\n\r\n```javascript\r\nimport React from 'react';\r\nimport ReactDom from 'react-dom';\r\n\r\nimport getRouter from './router/router';\r\n\r\nReactDom.render(\r\n    getRouter(), document.getElementById('app'));\r\n```\r\n\r\n\r\n现在执行打包命令`npm run dev-build`。打开`index.html`查看效果啦！\r\n\r\n那么问题来了~我们发现点击‘首页’和‘Page1’没有反应。不要惊慌，这是正常的。\r\n\r\n我们之前一直用这个路径访问`index.html`，类似这样：`file:///F:/react/react-family/dist/index.html`。\r\n这种路径了，不是我们想象中的路由那样的路径`http://localhost:3000`~我们需要配置一个简单的WEB服务器，指向\r\n`index.html`~有下面两种方法来实现\r\n\r\n1. `Nginx`, `Apache`, `IIS`等配置启动一个简单的的WEB服务器。\r\n2. 使用`webpack-dev-server`来配置启动WEB服务器。\r\n\r\n下一节，我们来使用第二种方法启动服务器。这一节的DEMO，先放这里。\r\n\r\n参考地址\r\n\r\n1. http://www.jianshu.com/p/e3adc9b5f75c\r\n\r\n2. http://reacttraining.cn/web/guides/quick-start\r\n\r\n# <div id=\"webpack-dev-server\">webpack-dev-server</div>\r\n\r\n简单来说，`webpack-dev-server`就是一个小型的静态文件服务器。使用它，可以为`webpack`打包生成的资源文件提供Web服务。\r\n\r\n`npm install webpack-dev-server@2 --save-dev`\r\n\r\n> 2017.11.16补充：这里webpack-dev-server需要全局安装，要不后面用的时候要写相对路径。需要再执行这个 `npm install webpack-dev-server@2 -g`\r\n\r\n修改`webpack.dev.config.js`,增加`webpack-dev-server`的配置。\r\n\r\n`webpack.dev.config.js`\r\n\r\n```javascript\r\n    devServer: {\r\n        contentBase: path.join(__dirname, './dist')\r\n    }\r\n```\r\n\r\n现在执行\r\n\r\n`webpack-dev-server --config webpack.dev.config.js`\r\n\r\n\r\n浏览器打开[http://localhost:8080](http://localhost:8080)，OK,现在我们可以点击`首页`,`Page1`了，\r\n看URL地址变化啦！我们看到`react-router`已经成功了哦。\r\n\r\nQ: `--content-base`是什么？\r\n\r\nA：`URL的根目录。如果不设定的话，默认指向项目根目录。`\r\n\r\n**重要提示：webpack-dev-server编译后的文件，都存储在内存中，我们并不能看见的。你可以删除之前遗留的文件`dist/bundle.js`，\r\n仍然能正常打开网站！**\r\n\r\n每次执行`webpack-dev-server --config webpack.dev.config.js`,要打很长的命令，我们修改`package.json`，增加`script->start`:\r\n\r\n```javascript\r\n  \"scripts\": {\r\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\",\r\n    \"dev-build\": \"webpack --config webpack.dev.config.js\",\r\n    \"start\": \"webpack-dev-server --config webpack.dev.config.js\"\r\n  }\r\n```\r\n下次执行`npm start`就可以了。\r\n\r\n既然用到了`webpack-dev-server`，我们就看看[它的其他的配置项](https://doc.webpack-china.org/configuration/dev-server)。\r\n看了之后，发现有几个我们可以用的。\r\n\r\n* color（CLI only） `console`中打印彩色日志\r\n* historyApiFallback 任意的`404`响应都被替代为`index.html`。有什么用呢？你现在运行\r\n`npm start`，然后打开浏览器，访问`http://localhost:8080`,然后点击`Page1`到链接`http://localhost:8080/page1`，\r\n然后刷新页面试试。是不是发现刷新后`404`了。为什么？`dist`文件夹里面并没有`page1.html`,当然会`404`了，所以我们需要配置\r\n`historyApiFallback`，让所有的`404`定位到`index.html`。\r\n* host 指定一个`host`,默认是`localhost`。如果你希望服务器外部可以访问，指定如下：`host: \"0.0.0.0\"`。比如你用手机通过IP访问。\r\n* hot 启用`Webpack`的模块热替换特性。关于热模块替换，我下一小节专门讲解一下。\r\n* port 配置要监听的端口。默认就是我们现在使用的`8080`端口。\r\n* proxy 代理。比如在 `localhost:3000` 上有后端服务的话，你可以这样启用代理：\r\n```javascript\r\n    proxy: {\r\n      \"/api\": \"http://localhost:3000\"\r\n    }\r\n```\r\n* progress（CLI only） 将编译进度输出到控制台。\r\n\r\n根据这几个配置，修改下我们的`webpack-dev-server`的配置~\r\n\r\n`webpack.dev.config.js`\r\n\r\n```javascript\r\n    devServer: {\r\n        port: 8080,\r\n        contentBase: path.join(__dirname, './dist'),\r\n        historyApiFallback: true,\r\n        host: '0.0.0.0'\r\n    }\r\n```\r\n`CLI ONLY`的需要在命令行中配置\r\n\r\n`package.json`\r\n\r\n```javascript\r\n\"start\": \"webpack-dev-server --config webpack.dev.config.js --color --progress\"\r\n```\r\n\r\n现在我们执行`npm start` 看看效果。是不是看到打包的时候有百分比进度？在`http://localhost:8080/page1`页面刷新是不是没问题了？\r\n用手机通过局域网IP是否可以访问到网站？\r\n\r\n参考地址：\r\n\r\n1. https://segmentfault.com/a/1190000006670084\r\n2. https://webpack.js.org/guides/development/#using-webpack-dev-server\r\n\r\n# <div id=\"hot-module-replacement\">模块热替换（Hot Module Replacement）</div>\r\n\r\n到目前，当我们修改代码的时候，浏览器会自动刷新，不信你可以去试试。（如果你的不会刷新，看看这个[调整文本编辑器](#editor)）\r\n\r\n我相信看这个教程的人，应该用过别人的框架。我们在修改代码的时候，浏览器不会刷新，只会更新自己修改的那一块。我们也要实现这个效果。\r\n\r\n我们看下[webpack模块热替换](https://doc.webpack-china.org/guides/hot-module-replacement)教程。\r\n\r\n我们接下来要这么修改\r\n\r\n`package.json` 增加 `--hot`\r\n\r\n```javascript\r\n\"start\": \"webpack-dev-server --config webpack.dev.config.js --color --progress --hot\"\r\n```\r\n`src/index.js` 增加`module.hot.accept()`,如下。当模块更新的时候，通知`index.js`。\r\n\r\n`src/index.js`\r\n\r\n```javascript\r\nimport React from 'react';\r\nimport ReactDom from 'react-dom';\r\n\r\nimport getRouter from './router/router';\r\n\r\nif (module.hot) {\r\n    module.hot.accept();\r\n}\r\n\r\nReactDom.render(\r\n    getRouter(), document.getElementById('app'));\r\n\r\n```\r\n\r\n现在我们执行`npm start`，打开浏览器，修改`Home.js`,看是不是不刷新页面的情况下，内容更新了？惊不惊喜？意不意外？\r\n\r\n**做模块热替换，我们只改了几行代码，非常简单的。纸老虎一个~**\r\n\r\n现在我需要说明下我们命令行使用的`--hot`，可以通过配置`webpack.dev.config.js`来替换，\r\n向文档上那样,修改下面三处。但我们还是用`--hot`吧。下面的方式我们知道一下就行，我们不用。同样的效果。\r\n\r\n```javascript\r\nconst webpack = require('webpack');\r\n\r\ndevServer: {\r\n    hot: true\r\n}\r\n\r\nplugins:[\r\n     new webpack.HotModuleReplacementPlugin()\r\n]\r\n```\r\n\r\n`HRM`配置其实有两种方式，一种`CLI`方式，一种`Node.js API`方式。我们用到的就是`CLI`方式，比较简单。\r\n`Node.js API`方式，就是建一个`server.js`等等，网上大部分教程都是这种方式，这里不做讲解了。\r\n\r\n\r\n你以为模块热替换到这里就结束了？no~no~no~\r\n\r\n上面的配置对`react`模块的支持不是很好哦。\r\n\r\n例如下面的`demo`，当模块热替换的时候，`state`会重置，这不是我们想要的。\r\n\r\n修改`Home.js`,增加计数`state`\r\n\r\n`src/pages/Home/Home.js`\r\n\r\n```javascript\r\nimport React, {Component} from 'react';\r\n\r\nexport default class Home extends Component {\r\n    constructor(props) {\r\n        super(props);\r\n        this.state = {\r\n            count: 0\r\n        }\r\n    }\r\n\r\n    _handleClick() {\r\n        this.setState({\r\n            count: ++this.state.count\r\n        });\r\n    }\r\n\r\n    render() {\r\n        return (\r\n            <div>\r\n                this is home~<br/>\r\n                当前计数：{this.state.count}<br/>\r\n                <button onClick={() => this._handleClick()}>自增</button>\r\n            </div>\r\n        )\r\n    }\r\n}\r\n```\r\n\r\n你可以测试一下，当我们修改代码的时候，`webpack`在更新页面的时候，也把`count`初始为0了。\r\n\r\n为了在`react`模块更新的同时，能保留`state`等页面中其他状态，我们需要引入[react-hot-loader](https://github.com/gaearon/react-hot-loader)~\r\n\r\n\r\nQ:　请问`webpack-dev-server`与`react-hot-loader`两者的热替换有什么区别？\r\n\r\nA:  区别在于`webpack-dev-serve`r自己的`--hot`模式只能即时刷新页面，但状态保存不住。因为`React`有一些自己语法(JSX)是`HotModuleReplacementPlugin`搞不定的。\r\n而`react-hot-loader`在`--hot`基础上做了额外的处理，来保证状态可以存下来。（来自[segmentfault](https://segmentfault.com/q/1010000005612845)）\r\n\r\n下面我们来加入`react-hot-loader v3`,\r\n\r\n安装依赖\r\n\r\n`npm install react-hot-loader@next --save-dev`\r\n\r\n根据[文档](https://gaearon.github.io/react-hot-loader/getstarted/)，\r\n我们要做如下几个修改~\r\n\r\n1. `.babelrc` 增加 `react-hot-loader/babel`\r\n\r\n`.babelrc`\r\n\r\n```javascript\r\n{\r\n  \"presets\": [\r\n    \"es2015\",\r\n    \"react\",\r\n    \"stage-0\"\r\n  ],\r\n  \"plugins\": [\r\n    \"react-hot-loader/babel\"\r\n  ]\r\n}\r\n\r\n```\r\n\r\n2. `webpack.dev.config.js`入口增加`react-hot-loader/patch`\r\n\r\n`webpack.dev.config.js`\r\n\r\n```javascript\r\n    entry: [\r\n        'react-hot-loader/patch',\r\n        path.join(__dirname, 'src/index.js')\r\n    ]\r\n```\r\n\r\n3. `src/index.js`修改如下\r\n\r\n`src/index.js`\r\n\r\n```javascript\r\nimport React from 'react';\r\nimport ReactDom from 'react-dom';\r\nimport {AppContainer} from 'react-hot-loader';\r\n\r\nimport getRouter from './router/router';\r\n\r\n/*初始化*/\r\nrenderWithHotReload(getRouter());\r\n\r\n/*热更新*/\r\nif (module.hot) {\r\n    module.hot.accept('./router/router', () => {\r\n        const getRouter = require('./router/router').default;\r\n        renderWithHotReload(getRouter());\r\n    });\r\n}\r\n\r\nfunction renderWithHotReload(RootElement) {\r\n    ReactDom.render(\r\n        <AppContainer>\r\n            {RootElement}\r\n        </AppContainer>,\r\n        document.getElementById('app')\r\n    )\r\n}\r\n\r\n```\r\n\r\n现在，执行`npm start`，试试。是不是修改页面的时候，`state`不更新了？\r\n\r\n参考文章：\r\n\r\n1. https://github.com/gaearon/react-hot-loader/issues/243\r\n\r\n\r\n# <div id=\"path\">文件路径优化</div>\r\n\r\n做到这里，我们简单休息下。做下优化~\r\n\r\n在之前写的代码中，我们引用组件，或者页面时候，写的是相对路径~\r\n\r\n比如`src/router/router.js`里面，引用`Home.js`的时候就用的相对路径\r\n\r\n```javascript\r\nimport Home from '../pages/Home/Home';\r\n```\r\nwebpack提供了一个别名配置，就是我们无论在哪个路径下，引用都可以这样\r\n\r\n```javascript\r\nimport Home from 'pages/Home/Home';\r\n```\r\n下面我们来配置下，修改`webpack.dev.config.js`，增加别名~\r\n\r\n`webpack.dev.config.js`\r\n\r\n```javascript\r\n    resolve: {\r\n        alias: {\r\n            pages: path.join(__dirname, 'src/pages'),\r\n            component: path.join(__dirname, 'src/component'),\r\n            router: path.join(__dirname, 'src/router')\r\n        }\r\n    }\r\n```\r\n\r\n然后我们把之前使用的绝对路径统统改掉。\r\n\r\n`src/router/router.js`\r\n\r\n```javascript\r\n\r\nimport Home from 'pages/Home/Home';\r\nimport Page1 from 'pages/Page1/Page1';\r\n```\r\n\r\n`src/index.js`\r\n\r\n```javascript\r\nimport getRouter from 'router/router';\r\n```\r\n\r\n我们这里约定，下面，我们会默认配置需要的别名路径，不再做重复的讲述哦。\r\n\r\n# <div id=\"redux\">redux</div>\r\n\r\n接下来，我们就要就要就要集成`redux`了。\r\n\r\n要对`redux`有一个大概的认识，可以阅读阮一峰前辈的[Redux 入门教程（一）：基本用法](http://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_one_basic_usages.html)\r\n\r\n如果要对`redux`有一个非常详细的认识，我推荐阅读[中文文档](http://cn.redux.js.org/index.html)，写的非常好。读了这个教程，有一个非常深刻的感觉，`redux`并没有任何魔法。\r\n\r\n**不要被各种关于 reducers, middleware, store 的演讲所蒙蔽 ---- Redux 实际是非常简单的。**\r\n\r\n当然，我这篇文章是写给新手的，如果看不懂上面的文章，或者不想看，没关系。先会用，多用用就知道原理了。\r\n\r\n开始整代码！我们就做一个最简单的计数器。自增，自减，重置。\r\n\r\n先安装`redux` `npm install --save redux`\r\n\r\n初始化目录结构\r\n\r\n```javascript\r\ncd src\r\nmkdir redux\r\ncd redux\r\nmkdir actions\r\nmkdir reducers\r\ntouch reducers.js\r\ntouch store.js\r\ntouch actions/counter.js\r\ntouch reducers/counter.js\r\n```\r\n\r\n先来写`action`创建函数。**通过`action`创建函数，可以创建`action`~**\r\n`src/redux/actions/counter.js`\r\n\r\n```javascript\r\n/*action*/\r\n\r\nexport const INCREMENT = \"counter/INCREMENT\";\r\nexport const DECREMENT = \"counter/DECREMENT\";\r\nexport const RESET = \"counter/RESET\";\r\n\r\nexport function increment() {\r\n    return {type: INCREMENT}\r\n}\r\n\r\nexport function decrement() {\r\n    return {type: DECREMENT}\r\n}\r\n\r\nexport function reset() {\r\n    return {type: RESET}\r\n}\r\n```\r\n\r\n再来写`reducer`,**`reducer`是一个纯函数，接收`action`和旧的`state`,生成新的`state`.**\r\n\r\n`src/redux/reducers/counter.js`\r\n\r\n```javascript\r\nimport {INCREMENT, DECREMENT, RESET} from '../actions/counter';\r\n\r\n/*\r\n* 初始化state\r\n */\r\n\r\nconst initState = {\r\n    count: 0\r\n};\r\n/*\r\n* reducer\r\n */\r\nexport default function reducer(state = initState, action) {\r\n    switch (action.type) {\r\n        case INCREMENT:\r\n            return {\r\n                count: state.count + 1\r\n            };\r\n        case DECREMENT:\r\n            return {\r\n                count: state.count - 1\r\n            };\r\n        case RESET:\r\n            return {count: 0};\r\n        default:\r\n            return state\r\n    }\r\n}\r\n\r\n```\r\n\r\n一个项目有很多的`reducers`,我们要把他们整合到一起\r\n\r\n`src/redux/reducers.js`\r\n\r\n```javascript\r\nimport counter from './reducers/counter';\r\n\r\nexport default function combineReducers(state = {}, action) {\r\n    return {\r\n        counter: counter(state.counter, action)\r\n    }\r\n}\r\n```\r\n到这里，我们必须再理解下一句话。\r\n\r\n**`reducer`就是纯函数，接收`state` 和 `action`，然后返回一个新的 `state`。**\r\n\r\n看看上面的代码，无论是`combineReducers`函数也好，还是`reducer`函数也好，都是接收`state`和`action`，\r\n返回更新后的`state`。区别就是`combineReducers`函数是处理整棵树，`reducer`函数是处理树的某一点。\r\n\r\n接下来，我们要创建一个`store`。\r\n\r\n前面我们可以使用 `action` 来描述“发生了什么”，使用`action`创建函数来返回`action`。\r\n\r\n还可以使用 `reducers` 来根据 `action` 更新 `state` 。\r\n\r\n那我们如何提交`action`？提交的时候，怎么才能触发`reducers`呢？\r\n\r\n`store` 就是把它们联系到一起的对象。`store` 有以下职责：\r\n\r\n* 维持应用的 `state`；\r\n* 提供 `getState()` 方法获取 `state`；\r\n* 提供 `dispatch(action)` 触发`reducers`方法更新 `state`；\r\n* 通过` subscribe(listener)` 注册监听器;\r\n* 通过 `subscribe(listener)` 返回的函数注销监听器。\r\n\r\n`src/redux/store.js`\r\n\r\n```javascript\r\nimport {createStore} from 'redux';\r\nimport combineReducers from './reducers.js';\r\n\r\nlet store = createStore(combineReducers);\r\n\r\nexport default store;\r\n```\r\n到现在为止，我们已经可以使用`redux`了~\r\n\r\n下面我们就简单的测试下\r\n\r\n```javascript\r\ncd src\r\ncd redux\r\ntouch testRedux.js\r\n```\r\n`src/redux/testRedux.js`\r\n\r\n```javascript\r\nimport {increment, decrement, reset} from './actions/counter';\r\n\r\nimport store from './store';\r\n\r\n// 打印初始状态\r\nconsole.log(store.getState());\r\n\r\n// 每次 state 更新时，打印日志\r\n// 注意 subscribe() 返回一个函数用来注销监听器\r\nlet unsubscribe = store.subscribe(() =>\r\n    console.log(store.getState())\r\n);\r\n\r\n// 发起一系列 action\r\nstore.dispatch(increment());\r\nstore.dispatch(decrement());\r\nstore.dispatch(reset());\r\n\r\n// 停止监听 state 更新\r\nunsubscribe();\r\n```\r\n当前文件夹执行命令\r\n```\r\nwebpack testRedux.js build.js\r\n\r\nnode build.js\r\n```\r\n是不是看到输出了`state`变化？\r\n```\r\n{ counter: { count: 0 } }\r\n{ counter: { count: 1 } }\r\n{ counter: { count: 0 } }\r\n{ counter: { count: 0 } }\r\n\r\n```\r\n\r\n做这个测试，就是为了告诉大家，`redux`和`react`没关系，虽说他俩能合作。\r\n\r\n到这里，我建议你再理下`redux`的数据流，看看[这里](http://cn.redux.js.org/docs/basics/DataFlow.html)。\r\n\r\n1. 调用`store.dispatch(action)`提交`action`。\r\n2. `redux store`调用传入的`reducer`函数。把当前的`state`和`action`传进去。\r\n3. 根 `reducer` 应该把多个子 `reducer` 输出合并成一个单一的 `state` 树。\r\n4. `Redux store` 保存了根 `reducer` 返回的完整 `state` 树。\r\n\r\n就是酱紫~~\r\n\r\n\r\n这会`webpack.dev.config.js`路径别名增加一下，后面好写了。\r\n\r\n`webpack.dev.config.js`\r\n  ```json\r\n          alias: {\r\n              ...\r\n              actions: path.join(__dirname, 'src/redux/actions'),\r\n              reducers: path.join(__dirname, 'src/redux/reducers'),\r\n              redux: path.join(__dirname, 'src/redux')\r\n          }\r\n  \r\n  ```\r\n\r\n把前面的相对路径都改改。\r\n\r\n下面我们开始搭配`react`使用。\r\n\r\n写一个`Counter`页面\r\n\r\n```\r\ncd src/pages\r\nmkdir Counter\r\ntouch Counter/Counter.js\r\n```\r\n`src/pages/Counter/Counter.js`\r\n\r\n```javascript\r\nimport React, {Component} from 'react';\r\n\r\nexport default class Counter extends Component {\r\n    render() {\r\n        return (\r\n            <div>\r\n                <div>当前计数为(显示redux计数)</div>\r\n                <button onClick={() => {\r\n                    console.log('调用自增函数');\r\n                }}>自增\r\n                </button>\r\n                <button onClick={() => {\r\n                    console.log('调用自减函数');\r\n                }}>自减\r\n                </button>\r\n                <button onClick={() => {\r\n                    console.log('调用重置函数');\r\n                }}>重置\r\n                </button>\r\n            </div>\r\n        )\r\n    }\r\n}\r\n\r\n```\r\n\r\n修改路由，增加`Counter`\r\n\r\n`src/router/router.js`\r\n\r\n```javascript\r\nimport React from 'react';\r\n\r\nimport {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';\r\n\r\nimport Home from 'pages/Home/Home';\r\nimport Page1 from 'pages/Page1/Page1';\r\nimport Counter from 'pages/Counter/Counter';\r\n\r\nconst getRouter = () => (\r\n    <Router>\r\n        <div>\r\n            <ul>\r\n                <li><Link to=\"/\">首页</Link></li>\r\n                <li><Link to=\"/page1\">Page1</Link></li>\r\n                <li><Link to=\"/counter\">Counter</Link></li>\r\n            </ul>\r\n            <Switch>\r\n                <Route exact path=\"/\" component={Home}/>\r\n                <Route path=\"/page1\" component={Page1}/>\r\n                <Route path=\"/counter\" component={Counter}/>\r\n            </Switch>\r\n        </div>\r\n    </Router>\r\n);\r\n\r\nexport default getRouter;\r\n```\r\n`npm start`看看效果。\r\n\r\n\r\n下一步，我们让`Counter`组件和`Redux`联合起来。使`Counter`能获得到`Redux`的`state`，并且能发射`action`。\r\n\r\n当然我们可以使用刚才测试`testRedux`的方法，手动监听~手动引入`store`~但是这肯定很麻烦哦。\r\n\r\n`react-redux`提供了一个方法`connect`。\r\n\r\n> 容器组件就是使用 store.subscribe() 从 Redux state 树中读取部分数据，并通过 props 来把这些数据提供给要渲染的组件。你可以手工来开发容器组件，但建议使用 React Redux 库的 connect() 方法来生成，这个方法做了性能优化来避免很多不必要的重复渲染。\r\n\r\n\r\n`connect`接收两个参数，一个`mapStateToProps`,就是把`redux`的`state`，转为组件的`Props`，还有一个参数是`mapDispatchToprops`,\r\n就是把发射`actions`的方法，转为`Props`属性函数。\r\n\r\n\r\n先来安装`react-redux`\r\n\r\n`npm install --save react-redux`\r\n\r\n`src/pages/Counter/Counter.js`\r\n\r\n```javascript\r\nimport React, {Component} from 'react';\r\nimport {increment, decrement, reset} from 'actions/counter';\r\n\r\nimport {connect} from 'react-redux';\r\n\r\nclass Counter extends Component {\r\n    render() {\r\n        return (\r\n            <div>\r\n                <div>当前计数为{this.props.counter.count}</div>\r\n                <button onClick={() => this.props.increment()}>自增\r\n                </button>\r\n                <button onClick={() => this.props.decrement()}>自减\r\n                </button>\r\n                <button onClick={() => this.props.reset()}>重置\r\n                </button>\r\n            </div>\r\n        )\r\n    }\r\n}\r\n\r\nconst mapStateToProps = (state) => {\r\n    return {\r\n        counter: state.counter\r\n    }\r\n};\r\n\r\nconst mapDispatchToProps = (dispatch) => {\r\n    return {\r\n        increment: () => {\r\n            dispatch(increment())\r\n        },\r\n        decrement: () => {\r\n            dispatch(decrement())\r\n        },\r\n        reset: () => {\r\n            dispatch(reset())\r\n        }\r\n    }\r\n};\r\n\r\nexport default connect(mapStateToProps, mapDispatchToProps)(Counter);\r\n```\r\n\r\n下面我们要传入`store`\r\n\r\n>所有容器组件都可以访问 Redux store，所以可以手动监听它。一种方式是把它以 props 的形式传入到所有容器组件中。但这太麻烦了，因为必须要用 store 把展示组件包裹一层，仅仅是因为恰好在组件树中渲染了一个容器组件。\r\n\r\n>建议的方式是使用指定的 React Redux 组件 <Provider> 来 魔法般的 让所有容器组件都可以访问 store，而不必显示地传递它。只需要在渲染根组件时使用即可。\r\n\r\n`src/index.js`\r\n\r\n```javascript\r\nimport React from 'react';\r\nimport ReactDom from 'react-dom';\r\nimport {AppContainer} from 'react-hot-loader';\r\nimport {Provider} from 'react-redux';\r\nimport store from './redux/store';\r\n\r\nimport getRouter from 'router/router';\r\n\r\n/*初始化*/\r\nrenderWithHotReload(getRouter());\r\n\r\n/*热更新*/\r\nif (module.hot) {\r\n    module.hot.accept('./router/router', () => {\r\n        const getRouter = require('router/router').default;\r\n        renderWithHotReload(getRouter());\r\n    });\r\n}\r\n\r\nfunction renderWithHotReload(RootElement) {\r\n    ReactDom.render(\r\n        <AppContainer>\r\n            <Provider store={store}>\r\n                {RootElement}\r\n            </Provider>\r\n        </AppContainer>,\r\n        document.getElementById('app')\r\n    )\r\n}\r\n\r\n```\r\n\r\n到这里我们就可以执行`npm start`，打开[localhost:8080/counter](localhost:8080/counter)看效果了。\r\n\r\n**但是你发现`npm start`一直报错**\r\n```javascript\r\nERROR in ./node_modules/react-redux/es/connect/mapDispatchToProps.js\r\nModule not found: Error: Can't resolve 'redux' in 'F:\\Project\\react\\react-family\\node_modules\\react-redux\\es\\connect'\r\n\r\nERROR in ./src/redux/store.js\r\nModule not found: Error: Can't resolve 'redux' in 'F:\\Project\\react\\react-family\\src\\redux'\r\n```\r\n\r\nWTF？这个错误困扰了半天。我说下为什么造成这个错误。我们引用`redux`的时候这样用的\r\n\r\n`import {createStore} from 'redux'`\r\n\r\n然而，我们在`webapck.dev.config.js`里面这样配置了\r\n\r\n```javascript\r\n    resolve: {\r\n        alias: {\r\n            ...\r\n            redux: path.join(__dirname, 'src/redux')\r\n        }\r\n    }\r\n```\r\n\r\n然后`webapck`编译的时候碰到`redux`都去`src/redux`去找了。但是找不到啊。所以我们把`webpack.dev.config.js`里面`redux`这一行删除了，就好了。\r\n并且把使用我们自己使用`redux`文件夹的地方改成相对路径哦。\r\n\r\n现在你可以`npm start`去看效果了。\r\n\r\n这里我们再缕下（可以读[React 实践心得：react-redux 之 connect 方法详解](http://taobaofed.org/blog/2016/08/18/react-redux-connect/)）\r\n\r\n1. `Provider`组件是让所有的组件可以访问到`store`。不用手动去传。也不用手动去监听。\r\n\r\n2. `connect`函数作用是从 `Redux state` 树中读取部分数据，并通过 props 来把这些数据提供给要渲染的组件。也传递`dispatch(action)`函数到`props`。\r\n\r\n接下来，我们要说异步`action`\r\n\r\n参考地址： http://cn.redux.js.org/docs/advanced/AsyncActions.html\r\n\r\n想象一下我们调用一个异步`get`请求去后台请求数据：\r\n\r\n1. 请求开始的时候，界面转圈提示正在加载。`isLoading`置为`true`。\r\n2. 请求成功，显示数据。`isLoading`置为`false`,`data`填充数据。\r\n3. 请求失败，显示失败。`isLoading`置为`false`，显示错误信息。\r\n\r\n下面，我们以向后台请求用户基本信息为例。\r\n\r\n1. 我们先创建一个`user.json`，等会请求用，相当于后台的API接口。\r\n\r\n```javascript\r\ncd dist\r\nmkdir api\r\ncd api\r\ntouch user.json\r\n```\r\n`dist/api/user.json`\r\n\r\n```javascript\r\n{\r\n  \"name\": \"brickspert\",\r\n  \"intro\": \"please give me a star\"\r\n}\r\n```\r\n\r\n2. 创建必须的`action`创建函数。\r\n\r\n```javascript\r\ncd src/redux/actions\r\ntouch userInfo.js\r\n```\r\n\r\n`src/redux/actions/userInfo.js`\r\n\r\n```javascript\r\nexport const GET_USER_INFO_REQUEST = \"userInfo/GET_USER_INFO_REQUEST\";\r\nexport const GET_USER_INFO_SUCCESS = \"userInfo/GET_USER_INFO_SUCCESS\";\r\nexport const GET_USER_INFO_FAIL = \"userInfo/GET_USER_INFO_FAIL\";\r\n\r\nfunction getUserInfoRequest() {\r\n    return {\r\n        type: GET_USER_INFO_REQUEST\r\n    }\r\n}\r\n\r\nfunction getUserInfoSuccess(userInfo) {\r\n    return {\r\n        type: GET_USER_INFO_SUCCESS,\r\n        userInfo: userInfo\r\n    }\r\n}\r\n\r\nfunction getUserInfoFail() {\r\n    return {\r\n        type: GET_USER_INFO_FAIL\r\n    }\r\n}\r\n```\r\n我们创建了请求中，请求成功，请求失败三个`action`创建函数。\r\n\r\n3. 创建`reducer`\r\n\r\n再强调下，`reducer`是根据`state`和`action`生成新`state`的**纯函数**。\r\n\r\n```javascript\r\ncd src/redux/reducers\r\ntouch userInfo.js\r\n```\r\n`src/redux/reducers/userInfo.js`\r\n\r\n```javascript\r\nimport {GET_USER_INFO_REQUEST, GET_USER_INFO_SUCCESS, GET_USER_INFO_FAIL} from 'actions/userInfo';\r\n\r\n\r\nconst initState = {\r\n    isLoading: false,\r\n    userInfo: {},\r\n    errorMsg: ''\r\n};\r\n\r\nexport default function reducer(state = initState, action) {\r\n    switch (action.type) {\r\n        case GET_USER_INFO_REQUEST:\r\n            return {\r\n                ...state,\r\n                isLoading: true,\r\n                userInfo: {},\r\n                errorMsg: ''\r\n            };\r\n        case GET_USER_INFO_SUCCESS:\r\n            return {\r\n                ...state,\r\n                isLoading: false,\r\n                userInfo: action.userInfo,\r\n                errorMsg: ''\r\n            };\r\n        case GET_USER_INFO_FAIL:\r\n            return {\r\n                ...state,\r\n                isLoading: false,\r\n                userInfo: {},\r\n                errorMsg: '请求错误'\r\n            };\r\n        default:\r\n            return state;\r\n    }\r\n}\r\n```\r\n\r\n**这里的`...state`语法，是和别人的`Object.assign()`起同一个作用，合并新旧state。我们这里是没效果的，但是我建议都写上这个哦**\r\n\r\n组合`reducer`\r\n\r\n`src/redux/reducers.js`\r\n\r\n```javascript\r\nimport counter from 'reducers/counter';\r\nimport userInfo from 'reducers/userInfo';\r\n\r\nexport default function combineReducers(state = {}, action) {\r\n    return {\r\n        counter: counter(state.counter, action),\r\n        userInfo: userInfo(state.userInfo, action)\r\n    }\r\n}\r\n```\r\n\r\n4. 现在有了`action`，有了`reducer`，我们就需要调用把`action`里面的三个`action`函数和网络请求结合起来。\r\n\r\n    * 请求中 `dispatch getUserInfoRequest`\r\n    * 请求成功 `dispatch getUserInfoSuccess`\r\n    * 请求失败 `dispatch getUserInfoFail`\r\n\r\n`src/redux/actions/userInfo.js`增加\r\n\r\n```javascript\r\nexport function getUserInfo() {\r\n    return function (dispatch) {\r\n        dispatch(getUserInfoRequest());\r\n\r\n        return fetch('http://localhost:8080/api/user.json')\r\n            .then((response => {\r\n                return response.json()\r\n            }))\r\n            .then((json) => {\r\n                    dispatch(getUserInfoSuccess(json))\r\n                }\r\n            ).catch(\r\n                () => {\r\n                    dispatch(getUserInfoFail());\r\n                }\r\n            )\r\n    }\r\n}\r\n```\r\n\r\n我们这里发现，别的`action`创建函数都是返回`action`对象：\r\n```javascript\r\n{type: xxxx}\r\n```\r\n但是我们现在的这个`action`创建函数 `getUserInfo`则是返回函数了。\r\n\r\n为了让`action`创建函数除了返回`action`对象外，还可以返回函数，我们需要引用`redux-thunk`。\r\n\r\n`npm install --save redux-thunk`\r\n\r\n这里涉及到`redux`中间件`middleware`，我后面会讲到的。你也可以读这里[Middleware](http://cn.redux.js.org/docs/advanced/Middleware.html)。\r\n\r\n简单的说，中间件就是`action`在到达`reducer`，先经过中间件处理。我们之前知道`reducer`能处理的`action`只有这样的`{type:xxx}`，所以我们使用中间件来处理\r\n函数形式的`action`，把他们转为标准的`action`给`reducer`。这是`redux-thunk`的作用。\r\n使用`redux-thunk`中间件\r\n\r\n我们来引入`redux-thunk`中间件\r\n\r\n`src/redux/store.js`\r\n\r\n```javascript\r\nimport {createStore, applyMiddleware} from 'redux';\r\nimport thunkMiddleware from 'redux-thunk';\r\nimport combineReducers from './reducers.js';\r\n\r\nlet store = createStore(combineReducers, applyMiddleware(thunkMiddleware));\r\n\r\nexport default store;\r\n```\r\n\r\n到这里，`redux`这边OK了，我们来写个组件验证下。\r\n\r\n```javascript\r\ncd src/pages\r\nmkdir UserInfo\r\ncd UserInfo\r\ntouch UserInfo.js\r\n```\r\n`src/pages/UserInfo/UserInfo.js`\r\n\r\n```javascript\r\nimport React, {Component} from 'react';\r\nimport {connect} from 'react-redux';\r\nimport {getUserInfo} from \"actions/userInfo\";\r\n\r\nclass UserInfo extends Component {\r\n\r\n    render() {\r\n        const {userInfo, isLoading, errorMsg} = this.props.userInfo;\r\n        return (\r\n            <div>\r\n                {\r\n                    isLoading ? '请求信息中......' :\r\n                        (\r\n                            errorMsg ? errorMsg :\r\n                                <div>\r\n                                    <p>用户信息：</p>\r\n                                    <p>用户名：{userInfo.name}</p>\r\n                                    <p>介绍：{userInfo.intro}</p>\r\n                                </div>\r\n                        )\r\n                }\r\n                <button onClick={() => this.props.getUserInfo()}>请求用户信息</button>\r\n            </div>\r\n        )\r\n    }\r\n}\r\n\r\nexport default connect((state) => ({userInfo: state.userInfo}), {getUserInfo})(UserInfo);\r\n\r\n\r\n```\r\n\r\n这里你可能发现`connect`参数写法不一样了，`mapStateToProps`函数用了`es6`简写，`mapDispatchToProps`用了`react-redux`提供的简单写法。\r\n\r\n增加路由\r\n`src/router/router.js`\r\n```javascript\r\nimport React from 'react';\r\n\r\nimport {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';\r\n\r\nimport Home from 'pages/Home/Home';\r\nimport Page1 from 'pages/Page1/Page1';\r\nimport Counter from 'pages/Counter/Counter';\r\nimport UserInfo from 'pages/UserInfo/UserInfo';\r\n\r\nconst getRouter = () => (\r\n    <Router>\r\n        <div>\r\n            <ul>\r\n                <li><Link to=\"/\">首页</Link></li>\r\n                <li><Link to=\"/page1\">Page1</Link></li>\r\n                <li><Link to=\"/counter\">Counter</Link></li>\r\n                <li><Link to=\"/userinfo\">UserInfo</Link></li>\r\n            </ul>\r\n            <Switch>\r\n                <Route exact path=\"/\" component={Home}/>\r\n                <Route path=\"/page1\" component={Page1}/>\r\n                <Route path=\"/counter\" component={Counter}/>\r\n                <Route path=\"/userinfo\" component={UserInfo}/>\r\n            </Switch>\r\n        </div>\r\n    </Router>\r\n);\r\n\r\nexport default getRouter;\r\n```\r\n\r\n现在你可以执行`npm start`去看效果啦！\r\n\r\n![redux](http://oqcb8djlw.bkt.clouddn.com/redux.png)\r\n\r\n到这里`redux`集成基本告一段落了，后面我们还会有一些优化。\r\n\r\n## combinReducers优化\r\n\r\n`redux`提供了一个`combineReducers`函数来合并`reducer`，不用我们自己合并哦。写起来简单，但是意思和我们\r\n自己写的`combinReducers`也是一样的。\r\n\r\n`src/redux/reducers.js`\r\n\r\n```javascript\r\nimport {combineReducers} from \"redux\";\r\n\r\nimport counter from 'reducers/counter';\r\nimport userInfo from 'reducers/userInfo';\r\n\r\n\r\nexport default combineReducers({\r\n    counter,\r\n    userInfo\r\n});\r\n```\r\n\r\n# <div id=\"devtool\">devtool优化</div>\r\n\r\n现在我们发现一个问题，代码哪里写错了，浏览器报错只报在`build.js`第几行。\r\n\r\n![错误图片](http://oqcb8djlw.bkt.clouddn.com/error-build.png)\r\n\r\n这让我们分析错误无从下手。看[这里](https://doc.webpack-china.org/configuration/devtool)。\r\n\r\n我们增加`webpack`配置`devtool`！\r\n\r\n`webpack.dev.config.js`增加\r\n\r\n```javascript\r\ndevtool: 'inline-source-map'\r\n```\r\n这次看错误信息是不是提示的很详细了？\r\n\r\n![错误图片](http://oqcb8djlw.bkt.clouddn.com/error-build-2.png)\r\n\r\n同时，我们在`srouce`里面能看到我们写的代码，也能打断点调试哦~\r\n\r\n![错误图片](http://oqcb8djlw.bkt.clouddn.com/source.png)\r\n\r\n# <div id=\"css\">编译css</div>\r\n\r\n先说这里为什么不用`scss`，因为`Windows`使用`node-sass`，需要先安装[ Microsoft Windows SDK for Windows 7 and .NET Framework 4](https://www.microsoft.com/en-us/download/details.aspx?id=8279)。\r\n我怕有些人copy这份代码后，没注意，运行不起来。所以这里不用`scss`了，如果需要，自行编译哦。\r\n\r\n`npm install css-loader style-loader --save-dev`\r\n\r\n`css-loader`使你能够使用类似`@import` 和 `url(...)`的方法实现 `require()`的功能； \r\n\r\n`style-loader`将所有的计算后的样式加入页面中； 二者组合在一起使你能够把样式表嵌入`webpack`打包后的JS文件中。\r\n\r\n`webpack.dev.config.js` `rules`增加\r\n\r\n```javascript\r\n{\r\n   test: /\\.css$/,\r\n   use: ['style-loader', 'css-loader']\r\n}\r\n```\r\n\r\n我们用`Page1`页面来测试下\r\n\r\n```javascript\r\ncd src/pages/Page1\r\ntouch Page1.css\r\n```\r\n`src/pages/Page1/Page1.css`\r\n\r\n```javascript\r\n.page-box {\r\n    border: 1px solid red;\r\n}\r\n```\r\n`src/pages/Page1/Page1.js`\r\n\r\n```javascript\r\nimport React, {Component} from 'react';\r\n\r\nimport './Page1.css';\r\n\r\nexport default class Page1 extends Component {\r\n    render() {\r\n        return (\r\n            <div className=\"page-box\">\r\n                this is page1~\r\n            </div>\r\n        )\r\n    }\r\n}\r\n```\r\n\r\n好了，现在`npm start`去看效果吧。\r\n\r\n# <div id=\"image\">编译图片</div>\r\n\r\n`npm install --save-dev url-loader file-loader`\r\n\r\n`webpack.dev.config.js` `rules`增加\r\n\r\n```javascript\r\n{\r\n    test: /\\.(png|jpg|gif)$/,\r\n    use: [{\r\n        loader: 'url-loader',\r\n        options: {\r\n            limit: 8192\r\n        }\r\n    }]\r\n}\r\n```\r\n`options limit 8192`意思是，小于等于8K的图片会被转成`base64`编码，直接插入HTML中，减少`HTTP`请求。\r\n\r\n我们来用`Page1` 测试下\r\n\r\n```javascript\r\ncd src/pages/Page1\r\nmkdir images\r\n```\r\n给`images`文件夹放一个图片。\r\n\r\n修改代码，引用图片\r\n\r\n`src/pages/Page1/Page1.js`\r\n\r\n```javascript\r\nimport React, {Component} from 'react';\r\n\r\nimport './Page1.css';\r\n\r\nimport image from './images/brickpsert.jpg';\r\n\r\nexport default class Page1 extends Component {\r\n    render() {\r\n        return (\r\n            <div className=\"page-box\">\r\n                this is page1~\r\n                <img src={image}/>\r\n            </div>\r\n        )\r\n    }\r\n}\r\n```\r\n\r\n可以去看看效果啦。\r\n\r\n# <div id=\"code-split\">按需加载</div>\r\n\r\n为什么要实现按需加载？\r\n\r\n我们现在看到，打包完后，所有页面只生成了一个`build.js`,当我们首屏加载的时候，就会很慢。因为他也下载了别的页面的`js`了哦。\r\n\r\n如果每个页面都打包了自己单独的JS，在进入自己页面的时候才加载对应的js，那首屏加载就会快很多哦。\r\n\r\n在 `react-router 2.0`时代， 按需加载需要用到的最关键的一个函数，就是`require.ensure()`，它是按需加载能够实现的核心。\r\n\r\n在4.0版本，官方放弃了这种处理按需加载的方式，选择了一个更加简洁的处理方式。\r\n\r\n[传送门](https://reacttraining.com/react-router/web/guides/code-splitting)\r\n\r\n根据官方示例，我们开搞\r\n\r\n1. `npm install bundle-loader --save-dev`\r\n\r\n2. 新建`bundle.js`\r\n\r\n```javascript\r\ncd src/router\r\ntouch Bundle.js\r\n```\r\n`src/router/Bundle.js`\r\n\r\n```javascript\r\nimport React, {Component} from 'react'\r\n\r\nclass Bundle extends Component {\r\n    state = {\r\n        // short for \"module\" but that's a keyword in js, so \"mod\"\r\n        mod: null\r\n    };\r\n\r\n    componentWillMount() {\r\n        this.load(this.props)\r\n    }\r\n\r\n    componentWillReceiveProps(nextProps) {\r\n        if (nextProps.load !== this.props.load) {\r\n            this.load(nextProps)\r\n        }\r\n    }\r\n\r\n    load(props) {\r\n        this.setState({\r\n            mod: null\r\n        });\r\n        props.load((mod) => {\r\n            this.setState({\r\n                // handle both es imports and cjs\r\n                mod: mod.default ? mod.default : mod\r\n            })\r\n        })\r\n    }\r\n\r\n    render() {\r\n        return this.props.children(this.state.mod)\r\n    }\r\n}\r\n\r\nexport default Bundle;\r\n```\r\n\r\n3. 改造路由器\r\n\r\n`src/router/router.js`\r\n\r\n```javascript\r\nimport React from 'react';\r\n\r\nimport {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';\r\n\r\nimport Bundle from './Bundle';\r\n\r\nimport Home from 'bundle-loader?lazy&name=home!pages/Home/Home';\r\nimport Page1 from 'bundle-loader?lazy&name=page1!pages/Page1/Page1';\r\nimport Counter from 'bundle-loader?lazy&name=counter!pages/Counter/Counter';\r\nimport UserInfo from 'bundle-loader?lazy&name=userInfo!pages/UserInfo/UserInfo';\r\n\r\nconst Loading = function () {\r\n    return <div>Loading...</div>\r\n};\r\n\r\nconst createComponent = (component) => (props) => (\r\n    <Bundle load={component}>\r\n        {\r\n            (Component) => Component ? <Component {...props} /> : <Loading/>\r\n        }\r\n    </Bundle>\r\n);\r\n\r\nconst getRouter = () => (\r\n    <Router>\r\n        <div>\r\n            <ul>\r\n                <li><Link to=\"/\">首页</Link></li>\r\n                <li><Link to=\"/page1\">Page1</Link></li>\r\n                <li><Link to=\"/counter\">Counter</Link></li>\r\n                <li><Link to=\"/userinfo\">UserInfo</Link></li>\r\n            </ul>\r\n            <Switch>\r\n                <Route exact path=\"/\" component={createComponent(Home)}/>\r\n                <Route path=\"/page1\" component={createComponent(Page1)}/>\r\n                <Route path=\"/counter\" component={createComponent(Counter)}/>\r\n                <Route path=\"/userinfo\" component={createComponent(UserInfo)}/>\r\n            </Switch>\r\n        </div>\r\n    </Router>\r\n);\r\n\r\nexport default getRouter;\r\n```\r\n现在你可以`npm start`，打开浏览器，看是不是进入新的页面，都会加载自己的JS的~\r\n\r\n但是你可能发现，名字都是`0.bundle.js`这样子的，这分不清楚是哪个页面的`js`呀！\r\n\r\n我们修改下`webpack.dev.config.js`,加个`chunkFilename`。`chunkFilename`是除了`entry`定义的入口`js`之外的`js`~\r\n\r\n```javascript\r\n    output: {\r\n        path: path.join(__dirname, './dist'),\r\n        filename: 'bundle.js',\r\n        chunkFilename: '[name].js'\r\n    }\r\n```\r\n现在你运行发现名字变成`home.js`,这样的了。棒棒哒！\r\n\r\n那么问题来了`home`是在哪里设置的？`webpack`怎么知道他叫`home`？\r\n\r\n其实在这里我们定义了，`router.js`里面\r\n\r\n`import Home from 'bundle-loader?lazy&name=home!pages/Home/Home';`\r\n\r\n看到没。这里有个`name=home`。嘿嘿。\r\n\r\n参考地址：\r\n1. http://www.jianshu.com/p/8dd98a7028e0\r\n2. https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/guides/code-splitting.md\r\n3. https://segmentfault.com/a/1190000007949841\r\n4. http://react-china.org/t/webpack-react-router/10123\r\n5. https://juejin.im/post/58f9717e44d9040069d06cd6\r\n\r\n# <div id=\"cache\">缓存</div>\r\n\r\n想象一下这个场景~\r\n\r\n我们网站上线了，用户第一次访问首页，下载了`home.js`，第二次访问又下载了`home.js`~\r\n\r\n这肯定不行呀，所以我们一般都会做一个缓存，用户下载一次`home.js`后，第二次就不下载了。\r\n\r\n有一天，我们更新了`home.js`，但是用户不知道呀，用户还是使用本地旧的`home.js`。出问题了~\r\n\r\n怎么解决？每次代码更新后，打包生成的名字不一样。比如第一次叫`home.a.js`，第二次叫`home.b.js`。\r\n\r\n文档[看这里](https://doc.webpack-china.org/guides/caching)\r\n\r\n我们照着文档来\r\n\r\n`webpack.dev.config.js`\r\n\r\n```javascript\r\n    output: {\r\n        path: path.join(__dirname, './dist'),\r\n        filename: '[name].[hash].js',\r\n        chunkFilename: '[name].[chunkhash].js'\r\n    }\r\n```\r\n每次打包都用增加`hash`~\r\n\r\n现在我们试试，是不是修改了文件，打包后相应的文件名字就变啦？\r\n\r\n![package](http://oqcb8djlw.bkt.clouddn.com/package.png)\r\n\r\n但是你可能发现了，网页打开报错了~因为你`dist/index.html`里面引用`js`名字还是`bundle.js`老名字啊,改成新的名字就可以啦。\r\n\r\n啊~那岂不是我每次编译打包，都得去改一下js名字？欲知后事如何，且看下节分享。\r\n\r\n\r\n# <div id=\"HtmlWebpackPlugin\">HtmlWebpackPlugin</div>\r\n\r\n这个插件，每次会自动把js插入到你的模板`index.html`里面去。\r\n\r\n`npm install html-webpack-plugin --save-dev`\r\n\r\n新建模板`index.html`\r\n\r\n```javascript\r\ncd src\r\ntouch index.html\r\n```\r\n`src/index.html`\r\n\r\n```javascript\r\n<!doctype html>\r\n<html lang=\"en\">\r\n<head>\r\n    <meta charset=\"UTF-8\">\r\n    <title>Document</title>\r\n</head>\r\n<body>\r\n<div id=\"app\"></div>\r\n</body>\r\n</html>\r\n```\r\n\r\n修改`webpack.dev.config.js`，增加`plugin`\r\n\r\n```javascript\r\nvar HtmlWebpackPlugin = require('html-webpack-plugin');\r\n\r\n    plugins: [new HtmlWebpackPlugin({\r\n        filename: 'index.html',\r\n        template: path.join(__dirname, 'src/index.html')\r\n    })],\r\n```\r\n\r\n`npm start`运行项目，看看是不是能正常访问啦。~\r\n\r\n说明一下：`npm start`打包后的文件存在内存中，你看不到的。~ 你可以把遗留`dist/index.html`删除掉了。\r\n\r\n# <div id=\"common-code\">提取公共代码</div>\r\n\r\n想象一下，我们的主文件，原来的`bundle.js`里面是不是包含了`react`,`redux`,`react-router`等等\r\n这些代码？？这些代码基本上不会改变的。但是，他们合并在`bundle.js`里面，每次项目发布，重新请求`bundle.js`的时候，相当于重新请求了\r\n`react`等这些公共库。浪费了~\r\n\r\n我们把`react`这些不会改变的公共库提取出来，用户缓存下来。从此以后，用户再也不用下载这些库了，无论是否发布项目。嘻嘻。\r\n\r\n`webpack`文档给了教程，[看这里](https://doc.webpack-china.org/guides/caching#-extracting-boilerplate-)\r\n\r\n`webpack.dev.config.js`\r\n\r\n```javascript\r\n    var webpack = require('webpack');\r\n\r\n    entry: {\r\n        app: [\r\n            'react-hot-loader/patch',\r\n            path.join(__dirname, 'src/index.js')\r\n        ],\r\n        vendor: ['react', 'react-router-dom', 'redux', 'react-dom', 'react-redux']\r\n    }\r\n    \r\n        /*plugins*/\r\n        new webpack.optimize.CommonsChunkPlugin({\r\n            name: 'vendor'\r\n        })\r\n```\r\n\r\n把`react`等库生成打包到`vendor.hash.js`里面去。\r\n\r\n但是你现在可能发现编译生成的文件`app.[hash].js`和`vendor.[hash].js`生成的`hash`一样的，这里是个问题，因为呀，你每次修改代码,都会导致`vendor.[hash].js`名字改变，那我们提取出来的意义也就没了。其实文档上写的很清楚，\r\n```javascript\r\n   output: {\r\n        path: path.join(__dirname, './dist'),\r\n        filename: '[name].[hash].js', //这里应该用chunkhash替换hash\r\n        chunkFilename: '[name].[chunkhash].js'\r\n    }\r\n```\r\n但是无奈，如果用`chunkhash`，会报错。和`webpack-dev-server --hot`不兼容，具体[看这里](https://github.com/webpack/webpack-dev-server/issues/377)。\r\n\r\n现在我们在配置开发版配置文件，就向`webpack-dev-server`妥协，因为我们要用他。问题先放这里，等会我们配置正式版`webpack.config.js`的时候要解决这个问题。\r\n\r\n\r\n# <div id=\"product\">生产坏境构建</div>\r\n\r\n>开发环境(development)和生产环境(production)的构建目标差异很大。在开发环境中，我们需要具有强大的、具有实时重新加载(live reloading)或热模块替换(hot module replacement)能力的 source map 和 localhost server。而在生产环境中，我们的目标则转向于关注更小的 bundle，更轻量的 source map，以及更优化的资源，以改善加载时间。由于要遵循逻辑分离，我们通常建议为每个环境编写彼此独立的 webpack 配置。\r\n\r\n文档[看这里](https://doc.webpack-china.org/guides/production)\r\n\r\n我们要开始做了~\r\n\r\n```javascript\r\ntouch webpack.config.js\r\n```\r\n在`webpack.dev.config.js`的基础上先做以下几个修改~\r\n\r\n1. 先删除`webpack-dev-server`相关的东西~\r\n2. `devtool`的值改成`cheap-module-source-map`\r\n3. 刚才说的`hash`改成`chunkhash`\r\n\r\n`webpack.config.js`\r\n\r\n```javascript\r\nconst path = require('path');\r\nvar HtmlWebpackPlugin = require('html-webpack-plugin');\r\nvar webpack = require('webpack');\r\n\r\nmodule.exports = {\r\n    devtool: 'cheap-module-source-map',\r\n    entry: {\r\n        app: [\r\n            path.join(__dirname, 'src/index.js')\r\n        ],\r\n        vendor: ['react', 'react-router-dom', 'redux', 'react-dom', 'react-redux']\r\n    },\r\n    output: {\r\n        path: path.join(__dirname, './dist'),\r\n        filename: '[name].[chunkhash].js',\r\n        chunkFilename: '[name].[chunkhash].js'\r\n    },\r\n    module: {\r\n        rules: [{\r\n            test: /\\.js$/,\r\n            use: ['babel-loader'],\r\n            include: path.join(__dirname, 'src')\r\n        }, {\r\n            test: /\\.css$/,\r\n            use: ['style-loader', 'css-loader']\r\n        }, {\r\n            test: /\\.(png|jpg|gif)$/,\r\n            use: [{\r\n                loader: 'url-loader',\r\n                options: {\r\n                    limit: 8192\r\n                }\r\n            }]\r\n        }]\r\n    },\r\n    plugins: [\r\n        new HtmlWebpackPlugin({\r\n            filename: 'index.html',\r\n            template: path.join(__dirname, 'src/index.html')\r\n        }),\r\n        new webpack.optimize.CommonsChunkPlugin({\r\n            name: 'vendor'\r\n        })\r\n    ],\r\n\r\n    resolve: {\r\n        alias: {\r\n            pages: path.join(__dirname, 'src/pages'),\r\n            component: path.join(__dirname, 'src/component'),\r\n            router: path.join(__dirname, 'src/router'),\r\n            actions: path.join(__dirname, 'src/redux/actions'),\r\n            reducers: path.join(__dirname, 'src/redux/reducers')\r\n        }\r\n    }\r\n};\r\n```\r\n\r\n在`package.json`增加打包脚本\r\n\r\n`\"build\":\"webpack --config webpack.config.js\"`\r\n\r\n然后执行`npm run build`~看看`dist`文件夹是不是生成了我们发布要用的所有文件哦？\r\n\r\n接下来我们还是要优化正式版配置文件~\r\n\r\n# <div id=\"uglify\">文件压缩</div>\r\n\r\n`webpack`使用`UglifyJSPlugin`来压缩生成的文件。\r\n\r\n`npm i --save-dev uglifyjs-webpack-plugin`\r\n\r\n`webpack.config.js`\r\n\r\n```javascript\r\nconst UglifyJSPlugin = require('uglifyjs-webpack-plugin')\r\n\r\nmodule.exports = {\r\n  plugins: [\r\n    new UglifyJSPlugin()\r\n  ]\r\n}\r\n```\r\n`npm run build`发现打包文件大小减小了好多。\r\n\r\n![uglify](http://oqcb8djlw.bkt.clouddn.com/uglify.png)\r\n\r\n# <div id=\"env\">指定环境</div>\r\n\r\n>许多 library 将通过与 process.env.NODE_ENV 环境变量关联，以决定 library 中应该引用哪些内容。例如，当不处于生产环境中时，某些 library 为了使调试变得容易，可能会添加额外的日志记录(log)和测试(test)。其实，当使用 process.env.NODE_ENV === 'production' 时，一些 library 可能针对具体用户的环境进行代码优化，从而删除或添加一些重要代码。我们可以使用 webpack 内置的 DefinePlugin 为所有的依赖定义这个变量：\r\n\r\n`webpack.config.js`\r\n\r\n```javascript\r\nmodule.exports = {\r\n  plugins: [\r\n       new webpack.DefinePlugin({\r\n          'process.env': {\r\n              'NODE_ENV': JSON.stringify('production')\r\n           }\r\n       })\r\n  ]\r\n}\r\n```\r\n\r\n`npm run build`后发现`vendor.[hash].js`又变小了。\r\n\r\n![uglify](http://oqcb8djlw.bkt.clouddn.com/product.png)\r\n\r\n# <div id=\"op-cache\">优化缓存</div>\r\n\r\n刚才我们把`[name].[hash].js`变成`[name].[chunkhash].js`后，`npm run build`后，\r\n发现`app.xxx.js`和`vendor.xxx.js`不一样了哦。\r\n\r\n但是现在又有一个问题了。\r\n\r\n你随便修改代码一处，例如`Home.js`，随便改变个字，你发现`home.xxx.js`名字变化的同时，\r\n`vendor.xxx.js`名字也变了。这不行啊。这和没拆分不是一样一样了吗？我们本意是`vendor.xxx.js`\r\n名字永久不变，一直缓存在用户本地的。~\r\n\r\n[官方文档](https://doc.webpack-china.org/guides/caching)推荐了一个插件[HashedModuleIdsPlugin](https://doc.webpack-china.org/plugins/hashed-module-ids-plugin)\r\n\r\n```javascript\r\n    plugins: [\r\n        new webpack.HashedModuleIdsPlugin()\r\n    ]\r\n```\r\n现在你打包，修改代码再试试，是不是名字不变啦？错了，现在打包，我发现名字还是变了，经过比对文档，我发现还要加一个`runtime`代码抽取，\r\n\r\n```javascript\r\nnew webpack.optimize.CommonsChunkPlugin({\r\n    name: 'runtime'\r\n})\r\n```\r\n加上这句话就好了~为什么呢？看下[解释](https://doc.webpack-china.org/concepts/manifest)。\r\n\r\n**注意，引入顺序在这里很重要。CommonsChunkPlugin 的 'vendor' 实例，必须在 'runtime' 实例之前引入。**\r\n\r\n# <div id=\"publicPath\">public path</div>\r\n\r\n想象一个场景，我们的静态文件放在了单独的静态服务器上去了，那我们打包的时候，如何让静态文件的链接定位到静态服务器呢？\r\n\r\n看文档[Public Path](https://doc.webpack-china.org/guides/public-path)\r\n\r\n`webpack.config.js` `output` 中增加一个`publicPath`，我们当前用`/`，相对于当前路径，如果你要改成别的`url`，就改这里就好了。\r\n\r\n```javascript\r\n    output: {\r\n        publicPath : '/'\r\n    }\r\n```\r\n\r\n# <div id=\"op-build\">打包优化</div>\r\n\r\n你现在打开`dist`，是不是发现好多好多文件，每次打包后的文件在这里混合了？我们希望每次打包前自动清理下`dist`文件。\r\n\r\n`npm install clean-webpack-plugin --save-dev`\r\n\r\n`webpack.config.js`\r\n\r\n```javascript\r\nconst CleanWebpackPlugin = require('clean-webpack-plugin');\r\n\r\n\r\nplugins: [\r\n    new CleanWebpackPlugin(['dist'])\r\n]\r\n```\r\n现在`npm run build`试试，是不是之前的都清空了。当然我们之前的`api`文件夹也被清空了，不过没关系哦~本来就是测试用的。\r\n\r\n# <div id=\"extract-text\">抽取css</div>\r\n\r\n目前我们的`css`是直接打包进`js`里面的，我们希望能单独生成`css`文件。\r\n\r\n我们使用[extract-text-webpack-plugin](https://github.com/webpack-contrib/extract-text-webpack-plugin)来实现。\r\n\r\n`npm install --save-dev extract-text-webpack-plugin`\r\n\r\n`webpack.config.js`\r\n\r\n```javascript\r\nconst ExtractTextPlugin = require(\"extract-text-webpack-plugin\");\r\n\r\nmodule.exports = {\r\n  module: {\r\n    rules: [\r\n      {\r\n        test: /\\.css$/,\r\n        use: ExtractTextPlugin.extract({\r\n          fallback: \"style-loader\",\r\n          use: \"css-loader\"\r\n        })\r\n      }\r\n    ]\r\n  },\r\n  plugins: [\r\n     new ExtractTextPlugin({\r\n         filename: '[name].[contenthash:5].css',\r\n         allChunks: true\r\n     })\r\n  ]\r\n}\r\n```\r\n`npm run build`后发现单独生成了`css`文件哦\r\n\r\n\r\n# <div id=\"op-http\">使用`axios`和`middleware`优化API请求</div>\r\n\r\n先安装下[axios](https://github.com/mzabriskie/axios)\r\n\r\n`npm install --save axios`\r\n\r\n我们之前项目的一次API请求是这样写的哦~\r\n\r\n`action`创建函数是这样的。比我们现在写的`fetch`简单多了。\r\n\r\n```javascript\r\nexport function getUserInfo() {\r\n    return {\r\n        types: [GET_USER_INFO_REQUEST, GET_USER_INFO_SUCCESS, GET_USER_INFO_FAIL],\r\n        promise: client => client.get(`http://localhost:8080/api/user.json`)\r\n        afterSuccess:(dispatch,getState,response)=>{\r\n            /*请求成功后执行的函数*/\r\n        },\r\n        otherData:otherData\r\n    }\r\n}\r\n```\r\n然后在dispatch(getUserInfo())后，通过`redux`中间件来处理请求逻辑。\r\n\r\n中间件的教程看[这里](http://cn.redux.js.org/docs/advanced/Middleware.html)\r\n\r\n我们想想中间件的逻辑\r\n\r\n1. 请求前`dispatch` `REQUEST`请求。\r\n2. 成功后`dispatch` `SUCCESS`请求，如果定义了`afterSuccess()`函数，调用它。\r\n3. 失败后`dispatch` `FAIL`请求。\r\n\r\n来写一个\r\n\r\n```javascript\r\ncd src/redux\r\nmkdir middleware\r\ncd middleware\r\ntouch promiseMiddleware.js\r\n```\r\n`src/redux/middleware/promiseMiddleware.js`\r\n\r\n```javascript\r\nimport axios from 'axios';\r\n\r\nexport default  store => next => action => {\r\n    const {dispatch, getState} = store;\r\n    /*如果dispatch来的是一个function，此处不做处理，直接进入下一级*/\r\n    if (typeof action === 'function') {\r\n        action(dispatch, getState);\r\n        return;\r\n    }\r\n    /*解析action*/\r\n    const {\r\n        promise,\r\n        types,\r\n        afterSuccess,\r\n        ...rest\r\n    } = action;\r\n\r\n    /*没有promise，证明不是想要发送ajax请求的，就直接进入下一步啦！*/\r\n    if (!action.promise) {\r\n        return next(action);\r\n    }\r\n\r\n    /*解析types*/\r\n    const [REQUEST,\r\n        SUCCESS,\r\n        FAILURE] = types;\r\n\r\n    /*开始请求的时候，发一个action*/\r\n    next({\r\n        ...rest,\r\n        type: REQUEST\r\n    });\r\n    /*定义请求成功时的方法*/\r\n    const onFulfilled = result => {\r\n        next({\r\n            ...rest,\r\n            result,\r\n            type: SUCCESS\r\n        });\r\n        if (afterSuccess) {\r\n            afterSuccess(dispatch, getState, result);\r\n        }\r\n    };\r\n    /*定义请求失败时的方法*/\r\n    const onRejected = error => {\r\n        next({\r\n            ...rest,\r\n            error,\r\n            type: FAILURE\r\n        });\r\n    };\r\n\r\n    return promise(axios).then(onFulfilled, onRejected).catch(error => {\r\n        console.error('MIDDLEWARE ERROR:', error);\r\n        onRejected(error)\r\n    })\r\n}\r\n\r\n```\r\n\r\n修改`src/redux/store.js`来应用这个中间件\r\n\r\n```javascript\r\nimport {createStore, applyMiddleware} from 'redux';\r\nimport combineReducers from './reducers.js';\r\n\r\nimport promiseMiddleware from './middleware/promiseMiddleware'\r\n\r\nlet store = createStore(combineReducers, applyMiddleware(promiseMiddleware));\r\n\r\nexport default store;\r\n```\r\n\r\n修改`src/redux/actions/userInfo.js`\r\n\r\n```javascript\r\nexport const GET_USER_INFO_REQUEST = \"userInfo/GET_USER_INFO_REQUEST\";\r\nexport const GET_USER_INFO_SUCCESS = \"userInfo/GET_USER_INFO_SUCCESS\";\r\nexport const GET_USER_INFO_FAIL = \"userInfo/GET_USER_INFO_FAIL\";\r\n\r\nexport function getUserInfo() {\r\n    return {\r\n        types: [GET_USER_INFO_REQUEST, GET_USER_INFO_SUCCESS, GET_USER_INFO_FAIL],\r\n        promise: client => client.get(`http://localhost:8080/api/user.json`)\r\n    }\r\n}\r\n```\r\n\r\n是不是简单清新很多啦？\r\n\r\n修改`src/redux/reducers/userInfo.js`\r\n\r\n```javascript\r\n        case GET_USER_INFO_SUCCESS:\r\n            return {\r\n                ...state,\r\n                isLoading: false,\r\n                userInfo: action.result.data,\r\n                errorMsg: ''\r\n            };\r\n```\r\n`action.userInfo`修改成了`action.result.data`。你看中间件，请求成功，会给`action`增加一个`result`字段来存储响应结果哦~不用手动传了。\r\n\r\n`npm start`看看我们的网络请求是不是正常哦。\r\n\r\n\r\n\r\n# <div id=\"editor\">调整文本编辑器</div>\r\n\r\n使用自动编译代码时，可能会在保存文件时遇到一些问题。某些编辑器具有“安全写入”功能，可能会影响重新编译。\r\n\r\n要在一些常见的编辑器中禁用此功能，请查看以下列表：\r\n\r\n * Sublime Text 3 - 在用户首选项(user preferences)中添加 atomic_save: \"false\"。\r\n * IntelliJ - 在首选项(preferences)中使用搜索，查找到 \"safe write\" 并且禁用它。\r\n * Vim - 在设置(settings)中增加 :set backupcopy=yes。\r\n * WebStorm - 在 Preferences > Appearance & Behavior > System Settings 中取消选中 Use \"safe write\"。\r\n  "},"comment":{"url":"https://api.github.com/repos/brickspert/blog/issues/comments/374498264","html_url":"https://github.com/brickspert/blog/issues/1#issuecomment-374498264","issue_url":"https://api.github.com/repos/brickspert/blog/issues/1","id":374498264,"user":{"login":"brickspert","id":12526493,"avatar_url":"https://avatars0.githubusercontent.com/u/12526493?v=4","gravatar_id":"","url":"https://api.github.com/users/brickspert","html_url":"https://github.com/brickspert","followers_url":"https://api.github.com/users/brickspert/followers","following_url":"https://api.github.com/users/brickspert/following{/other_user}","gists_url":"https://api.github.com/users/brickspert/gists{/gist_id}","starred_url":"https://api.github.com/users/brickspert/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/brickspert/subscriptions","organizations_url":"https://api.github.com/users/brickspert/orgs","repos_url":"https://api.github.com/users/brickspert/repos","events_url":"https://api.github.com/users/brickspert/events{/privacy}","received_events_url":"https://api.github.com/users/brickspert/received_events","type":"User","site_admin":false},"created_at":"2018-03-20T07:14:44Z","updated_at":"2018-03-20T07:14:44Z","author_association":"OWNER","body":"@BenlovedLamphere https://github.com/brickspert/blog/issues/3"}}
{"actor":{"display_login":"brickspert"}}