{"action":"created","issue":{"url":"https://api.github.com/repos/tmallfe/tmallfe.github.io/issues/23","labels_url":"https://api.github.com/repos/tmallfe/tmallfe.github.io/issues/23/labels{/name}","comments_url":"https://api.github.com/repos/tmallfe/tmallfe.github.io/issues/23/comments","events_url":"https://api.github.com/repos/tmallfe/tmallfe.github.io/issues/23/events","html_url":"https://github.com/tmallfe/tmallfe.github.io/issues/23","id":84446634,"number":23,"title":"轻松入门React和Webpack","user":{"login":"LingyuCoder","id":2663351,"avatar_url":"https://avatars.githubusercontent.com/u/2663351?v=3","gravatar_id":"","url":"https://api.github.com/users/LingyuCoder","html_url":"https://github.com/LingyuCoder","followers_url":"https://api.github.com/users/LingyuCoder/followers","following_url":"https://api.github.com/users/LingyuCoder/following{/other_user}","gists_url":"https://api.github.com/users/LingyuCoder/gists{/gist_id}","starred_url":"https://api.github.com/users/LingyuCoder/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/LingyuCoder/subscriptions","organizations_url":"https://api.github.com/users/LingyuCoder/orgs","repos_url":"https://api.github.com/users/LingyuCoder/repos","events_url":"https://api.github.com/users/LingyuCoder/events{/privacy}","received_events_url":"https://api.github.com/users/LingyuCoder/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"milestone":null,"comments":10,"created_at":"2015-06-03T09:01:45Z","updated_at":"2015-08-28T09:55:58Z","closed_at":null,"body":"最近在学习React.js，之前都是直接用最原生的方式去写React代码，发现组织起来特别麻烦，之前听人说用Webpack组织React组件得心应手，就花了点时间学习了一下，收获颇丰\r\n\r\n<!-- more -->\r\n\r\n## 说说React\r\n\r\n一个组件，有自己的结构，有自己的逻辑，有自己的样式，会依赖一些资源，会依赖某些其他组件。比如日常写一个组件，比较常规的方式：\r\n\r\n－ 通过前端模板引擎定义结构\r\n－ JS文件中写自己的逻辑\r\n－ CSS中写组件的样式\r\n－ 通过RequireJS、SeaJS这样的库来解决模块之间的相互依赖，\r\n那么在React中是什么样子呢？\r\n\r\n### 结构和逻辑\r\n\r\n在React的世界里，结构和逻辑交由JSX文件组织，React将模板内嵌到逻辑内部，实现了一个JS代码和HTML混合的JSX。\r\n\r\n#### 结构\r\n\r\n在JSX文件中，可以直接通过`React.createClass`来定义组件：\r\n\r\n```javascript\r\nvar CustomComponent = React.creatClass({\r\n    render: function(){\r\n        return (<div className=\"custom-component\"></div>);\r\n    }\r\n});\r\n```\r\n\r\n通过这种方式可以很方便的定义一个组件，组件的结构定义在render函数中，但这并不是简单的模板引擎，我们可以通过js方便、直观的操控组件结构，比如我想给组件增加几个节点：\r\n\r\n```javascript\r\nvar CustomComponent = React.creatClass({\r\n    render: function(){\r\n        var $nodes = ['h','e','l','l','o'].map(function(str){\r\n            return (<span>{str}</span>);\r\n        });\r\n        return (<div className=\"custom-component\">{$nodes}</div>);\r\n    }\r\n});\r\n```\r\n\r\n通过这种方式，React使得组件拥有灵活的结构。那么React又是如何处理逻辑的呢？\r\n\r\n#### 逻辑\r\n\r\n写过前端组件的人都知道，组件通常首先需要相应自身DOM事件，做一些处理。必要时候还需要暴露一些外部接口，那么React组件要怎么做到这两点呢？\r\n\r\n##### 事件响应\r\n\r\n比如我有个按钮组件，点击之后需要做一些处理逻辑，那么React组件大致上长这样：\r\n\r\n```javascript\r\nvar ButtonComponent = React.createClass({\r\n    render: function(){\r\n        return (<button>屠龙宝刀，点击就送</button>);\r\n    }\r\n});\r\n```\r\n\r\n点击按钮应当触发相应地逻辑，一种比较直观的方式就是给button绑定一个`onclick`事件，里面就是需要执行的逻辑了：\r\n\r\n```javascript\r\nfunction getDragonKillingSword() {\r\n    //送宝刀\r\n}\r\nvar ButtonComponent = React.createClass({\r\n    render: function(){\r\n        return (<button onclick=\"getDragonKillingSword()\">屠龙宝刀，点击就送</button>);\r\n    }\r\n});\r\n```\r\n\r\n但事实上`getDragonKillingSword()`的逻辑属于组件内部行为，显然应当包装在组件内部，于是在React中就可以这么写：\r\n\r\n```javascript\r\nvar ButtonComponent = React.createClass({\r\n    getDragonKillingSword: function(){\r\n        //送宝刀\r\n    },\r\n    render: function(){\r\n        return (<button onClick={this.getDragonKillingSword}>屠龙宝刀，点击就送</button>);\r\n    }\r\n});\r\n```\r\n\r\n这样就实现内部事件的响应了，那如果需要暴露接口怎么办呢？\r\n\r\n##### 暴露接口\r\n\r\n事实上现在`getDragonKillingSword`已经是一个接口了，如果有一个父组件，想要调用这个接口怎么办呢？\r\n\r\n父组件大概长这样：\r\n\r\n```javascript\r\nvar ImDaddyComponent = React.createClass({\r\n    render: function(){\r\n        return (\r\n            <div>\r\n                //其他组件\r\n                <ButtonComponent />\r\n                //其他组件\r\n            </div>\r\n        );\r\n    }\r\n});\r\n```\r\n\r\n那么如果想手动调用组件的方法，首先在ButtonComponent上设置一个`ref=\"\"`属性来标记一下，比如这里把子组件设置成`<ButtonComponent ref=\"getSwordButton\"/>`，那么在父组件的逻辑里，就可以在父组件自己的方法中通过这种方式来调用接口方法：\r\n\r\n```javascript\r\nthis.refs.getSwordButton.getDragonKillingSword();\r\n```\r\n\r\n看起来屌屌哒~那么问题又来了，父组件希望自己能够按钮点击时调用的方法，那该怎么办呢？\r\n\r\n##### 配置参数\r\n\r\n父组件可以直接将需要执行的函数传递给子组件：\r\n\r\n```javascript\r\n<ButtonComponent clickCallback={this.getSwordButtonClickCallback}/>\r\n```\r\n\r\n然后在子组件中调用父组件方法：\r\n\r\n```javascript\r\nvar ButtonComponent = React.createClass({\r\n    render: function(){\r\n        return (<button onClick={this.props.clickCallback}>屠龙宝刀，点击就送</button>);\r\n    }\r\n});\r\n```\r\n\r\n子组件通过`this.props`能够获取在父组件创建子组件时传入的任何参数，因此`this.props`也常被当做配置参数来使用\r\n\r\n屠龙宝刀每个人只能领取一把，按钮点击一下就应该灰掉，应当在子组件中增加一个是否点击过的状态，这又应当处理呢？\r\n\r\n##### 组件状态\r\n\r\n在React中，每个组件都有自己的状态，可以在自身的方法中通过`this.state`取到，而初始状态则通过`getInitialState()`方法来定义，比如这个屠龙宝刀按钮组件，它的初始状态应该是没有点击过，所以`getInitialState`方法里面应当定义初始状态`clicked: false`。而在点击执行的方法中，应当修改这个状态值为`click: true`：\r\n\r\n```javascript\r\nvar ButtonComponent = React.createClass({\r\n    getInitialState: function(){\r\n        //确定初始状态\r\n        return {\r\n            clicked: false\r\n        };\r\n    },\r\n    getDragonKillingSword: function(){\r\n        //送宝刀\r\n        \r\n        //修改点击状态\r\n        this.setState({\r\n            clicked: true\r\n        });\r\n    },\r\n    render: function(){\r\n        return (<button onClick={this.getDragonKillingSword}>屠龙宝刀，点击就送</button>);\r\n    }\r\n});\r\n```\r\n\r\n这样点击状态的维护就完成了，那么render函数中也应当根据状态来维护节点的样式，比如这里将按钮设置为`disabled`，那么render函数就要添加相应的判断逻辑：\r\n\r\n```javascript\r\nrender: function(){\r\n    var clicked = this.state.clicked;\r\n    if(clicked)\r\n        return (<button disabled=\"disabled\" onClick={this.getDragonKillingSword}>屠龙宝刀，点击就送</button>);\r\n    else \r\n        return (<button onClick={this.getDragonKillingSword}>屠龙宝刀，点击就送</button>);\r\n}\r\n```\r\n\r\n#### 小节\r\n\r\n这里简单介绍了通过JSX来管理组件的结构和逻辑，事实上React给组件还定义了很多方法，以及组件自身的生命周期，这些都使得组件的逻辑处理更加强大\r\n\r\n### 资源加载\r\n\r\nCSS文件定义了组件的样式，现在的模块加载器通常都能够加载CSS文件，如果不能一般也提供了相应的插件。事实上CSS、图片可以看做是一种资源，因为加载过来后一般不需要做什么处理。\r\n\r\nReact对这一方面并没有做特别的处理，虽然它提供了Inline Style的方式把CSS写在JSX里面，但估计没有多少人会去尝试，毕竟现在CSS样式已经不再只是简单的CSS文件了，通常都会去用Less、Sass等预处理，然后再用像postcss、myth、autoprefixer、cssmin等等后处理。资源加载一般也就简单粗暴地使用模块加载器完成了\r\n\r\n### 组件依赖\r\n\r\n组件依赖的处理一般分为两个部分：组件加载和组件使用\r\n\r\n#### 组件加载\r\n\r\nReact没有提供相关的组件加载方法，依旧需要通过`<script>`标签引入，或者使用模块加载器加载组件的JSX和资源文件。\r\n\r\n#### 组件使用\r\n\r\n如果细心，就会发现其实之前已经有使用的例子了，要想在一个组件中使用另外一个组件，比如在`ParentComponent`中使用`ChildComponent`，就只需要在`ParentComponent`的`render()`方法中写上`<ChildComponent />`就行了，必要的时候还可以传些参数。\r\n\r\n### 疑问\r\n\r\n到这里就会发现一个问题，React除了只处理了结构和逻辑，资源也不管，依赖也不管。是的，React将近两万行代码，连个模块加载器都没有提供，更与Angularjs，jQuery等不同的是，他还不带啥脚手架...没有Ajax库，没有Promise库，要啥啥没有...\r\n\r\n#### 虚拟DOM\r\n\r\n那它为啥这么大？因为它实现了一个虚拟DOM（Virtual DOM）。虚拟DOM是干什么的？这就要从浏览器本身讲起\r\n\r\n如我们所知，在浏览器渲染网页的过程中，加载到HTML文档后，会将文档解析并构建DOM树，然后将其与解析CSS生成的CSSOM树一起结合产生爱的结晶——RenderObject树，然后将RenderObject树渲染成页面（当然中间可能会有一些优化，比如RenderLayer树）。这些过程都存在与渲染引擎之中，渲染引擎在浏览器中是于JavaScript引擎（JavaScriptCore也好V8也好）分离开的，但为了方便JS操作DOM结构，渲染引擎会暴露一些接口供JavaScript调用。由于这两块相互分离，通信是需要付出代价的，因此JavaScript调用DOM提供的接口性能不咋地。各种性能优化的最佳实践也都在尽可能的减少DOM操作次数。\r\n\r\n而虚拟DOM干了什么？它直接用JavaScript实现了DOM树（大致上）。组件的HTML结构并不会直接生成DOM，而是映射生成虚拟的JavaScript DOM结构，React又通过在这个虚拟DOM上实现了一个 diff 算法找出最小变更，再把这些变更写入实际的DOM中。这个虚拟DOM以JS结构的形式存在，计算性能会比较好，而且由于减少了实际DOM操作次数，性能会有较大提升\r\n\r\n**道理我都懂，可是为什么我们没有模块加载器？**\r\n\r\n所以就需要Webpack了\r\n\r\n## 说说Webpack\r\n\r\n### 什么是Webpack？\r\n\r\n事实上它是一个打包工具，而不是像RequireJS或SeaJS这样的模块加载器，通过使用Webpack，能够像Node.js一样处理依赖关系，然后解析出模块之间的依赖，将代码打包\r\n\r\n### 安装Webpack\r\n\r\n首先得有Node.js\r\n\r\n然后通过`npm install -g webpack`安装webpack，当然也可以通过gulp来处理webpack任务，如果使用gulp的话就`npm install --save-dev gulp-webpack`\r\n\r\n### 配置Webpack\r\n\r\nWebpack的构建过程需要一个配置文件，一个典型的配置文件大概就是这样\r\n\r\n```javascript\r\nvar webpack = require('webpack');\r\nvar commonsPlugin = new webpack.optimize.CommonsChunkPlugin('common.js');\r\n\r\nmodule.exports = {\r\n    entry: {\r\n        entry1: './entry/entry1.js',\r\n        entry2: './entry/entry2.js'\r\n    },\r\n    output: {\r\n        path: __dirname,\r\n        filename: '[name].entry.js'\r\n    },\r\n    resolve: {\r\n        extensions: ['', '.js', '.jsx']\r\n    },\r\n    module: {\r\n        loaders: [{\r\n            test: /\\.js$/,\r\n            loader: 'babel-loader'\r\n        }, {\r\n            test: /\\.jsx$/,\r\n            loader: 'babel-loader!jsx-loader?harmony'\r\n        }]\r\n    },\r\n    plugins: [commonsPlugin]\r\n};\r\n```\r\n\r\n这里对Webpack的打包行为做了配置，主要分为几个部分：\r\n\r\n- entry：指定打包的入口文件，每有一个键值对，就是一个入口文件\r\n- output：配置打包结果，path定义了输出的文件夹，filename则定义了打包结果文件的名称，filename里面的`[name]`会由entry中的键（这里是entry1和entry2）替换\r\n- resolve：定义了解析模块路径时的配置，常用的就是extensions，可以用来指定模块的后缀，这样在引入模块时就不需要写后缀了，会自动补全\r\n- module：定义了对模块的处理逻辑，这里可以用loaders定义了一系列的加载器，以及一些正则。当需要加载的文件匹配test的正则时，就会调用后面的loader对文件进行处理，这正是webpack强大的原因。比如这里定义了凡是`.js`结尾的文件都是用`babel-loader`做处理，而`.jsx`结尾的文件会先经过`jsx-loader`处理，然后经过`babel-loader`处理。当然这些loader也需要通过`npm install`安装\r\n- plugins: 这里定义了需要使用的插件，比如commonsPlugin在打包多个入口文件时会提取出公用的部分，生成common.js\r\n\r\n\r\n当然Webpack还有很多其他的配置，具体可以参照它的[配置文档](http://webpack.github.io/docs/configuration.html#entry)\r\n\r\n### 执行打包\r\n\r\n如果通过`npm install -g webpack`方式安装webpack的话，可以通过命令行直接执行打包命令，比如这样：\r\n\r\n```shell\r\n$webpack --config webpack.config.js\r\n```\r\n\r\n这样就会读取当前目录下的webpack.config.js作为配置文件执行打包操作\r\n\r\n如果是通过gulp插件gulp-webpack，则可以在gulpfile中写上gulp任务：\r\n\r\n```javascript\r\nvar gulp = require('gulp');\r\nvar webpack = require('gulp-webpack');\r\nvar webpackConfig = require('./webpack.config');\r\ngulp.task(\"webpack\", function() {\r\n    return gulp\r\n        .src('./')\r\n        .pipe(webpack(webpackConfig))\r\n        .pipe(gulp.dest('./build'));\r\n});\r\n```\r\n\r\n### 组件编写\r\n\r\n#### 使用Babel提升逼格\r\n\r\nWebpack使得我们可以使用Node.js的CommonJS规范来编写模块，比如一个简单的Hello world模块，就可以这么处理：\r\n\r\n```javascript\r\nvar React = require('react');\r\n\r\nvar HelloWorldComponent = React.createClass({\r\n    displayName: 'HelloWorldComponent',\r\n    render: function() {\r\n        return (<div>Hello world</div>);\r\n    }\r\n});\r\n\r\nmodule.exports = HelloWorldComponent;\r\n```\r\n\r\n等等，这和之前的写法没啥差别啊，依旧没有逼格...程序员敲码要有geek范，要逼格than逼格，这太low了。现在都ES6了，React的代码也要写ES6，`babel-loader`就是干这个的。[Babel](https://babeljs.io/)能够将ES6代码转换成ES5。首先需要通过命令`npm install --save-dev babel-loader`来进行安装，安装完成后就可以使用了，一种使用方式是之前介绍的在`webpack.config.js`的loaders中配置，另一种是直接在代码中使用，比如：\r\n\r\n```javascript\r\nvar HelloWorldComponent = require('!babel!jsx!./HelloWorldComponent');\r\n```\r\n那我们应当如何使用Babel提升代码的逼格呢？改造一下之前的HelloWorld代码吧：\r\n\r\n```javascript\r\nimport React from 'react';\r\n\r\nexport default class HelloWorldComponent extends React.Component {\r\n    constructor() {\r\n        super();\r\n        this.state = {};\r\n    }\r\n    render() {\r\n        return (<div>Hello World</div>);\r\n    }\r\n}\r\n```\r\n\r\n这样在其他组件中需要引入HelloWorldComponent组件，就只要就可以了：\r\n\r\n```javascript\r\nimport HelloWorldComponent from './HelloWorldComponent'\r\n```\r\n\r\n怎么样是不是更有逼格了？通过import引入模块，还可以直接定义类和类的继承关系，这里也不再需要`getInitialState`了，直接在构造函数`constructor`中用`this.state = xxx`就好了\r\n\r\nBabel带来的当然还不止这些，在其帮助下还能尝试很多优秀的ES6特性，比如箭头函数，箭头函数的特点就是内部的this和外部保持一致，从此可以和`that`、`_this`说再见了\r\n\r\n```javascript\r\n['H', 'e', 'l', 'l', 'o'].map((c) => {\r\n    return (<span>{c}</span>);\r\n});\r\n```\r\n\r\n其他还有很多，具体可以参照[Babel的学习文档](https://babeljs.io/docs/learn-es6/)\r\n\r\n\r\n#### 样式编写\r\n\r\n我是一个强烈地Less依赖患者，脱离了Less直接写CSS就会出现四肢乏力、不想干活、心情烦躁等现象，而且还不喜欢在写Less时候加前缀，平常都是gulp+less+autoprefixer直接处理的，那么在Webpack组织的React组件中要怎么写呢？\r\n\r\n**没错，依旧是使用loader**\r\n\r\n可以在`webpack.config.js`的loaders中增加Less的配置：\r\n\r\n```javascript\r\n{\r\n  test: /\\.less$/,\r\n  loader: 'style-loader!css-loader!autoprefixer-loader!less-loader'\r\n}\r\n```\r\n\r\n通过这样的配置，就可以直接在模块代码中引入Less样式了：\r\n\r\n```javascript\r\nimport React from 'react';\r\n\r\nrequire('./HelloWorldComponent.less');\r\n\r\nexport default class HelloWorldComponent extends React.Component {\r\n    constructor() {\r\n        super();\r\n        this.state = {};\r\n    }\r\n    render() {\r\n        return (<div>Hello World</div>);\r\n    }\r\n}\r\n```\r\n\r\n#### 其他\r\n\r\nWebpack的loader为React组件化提供了很多帮助，像图片也提供了相关的loader：\r\n\r\n```javascript\r\n{ test: /\\.png$/, loader: \"url-loader?mimetype=image/png\" }\r\n```\r\n\r\n更多地loader可以移步[webpack的wiki](https://github.com/webpack/docs/wiki/list-of-loaders)\r\n\r\n##在Webpack下实时调试React组件\r\n\r\nWebpack和React结合的另一个强大的地方就是，在修改了组件源码之后，不刷新页面就能把修改同步到页面上。这里需要用到两个库`webpack-dev-server`和`react-hot-loader`。\r\n\r\n首先需要安装这两个库，`npm install --save-dev webpack-dev-server react-hot-loader`\r\n\r\n安装完成后，就要开始配置了，首先需要修改entry配置：\r\n\r\n```javascript\r\nentry: {\r\n  helloworld: [\r\n    'webpack-dev-server/client?http://localhost:3000',\r\n    'webpack/hot/only-dev-server',\r\n    './helloworld'\r\n  ]\r\n},\r\n```\r\n\r\n通过这种方式指定资源热启动对应的服务器，然后需要配置`react-hot-loader`到loaders的配置当中，比如我的所有组件代码全部放在scripts文件夹下：\r\n\r\n```javascript\r\n{\r\n  test: /\\.js?$/,\r\n  loaders: ['react-hot', 'babel'],\r\n  include: [path.join(__dirname, 'scripts')]\r\n}\r\n```\r\n\r\n最后配置一下plugins，加上热替换的插件和防止报错的插件：\r\n\r\n```javascript\r\nplugins: [\r\n  new webpack.HotModuleReplacementPlugin(),\r\n  new webpack.NoErrorsPlugin()\r\n]\r\n```\r\n\r\n这样配置就完成了，但是现在要调试需要启动一个服务器，而且之前配置里映射到`http://localhost:3000`，所以就在本地3000端口起个服务器吧，在项目根目录下面建个server.js：\r\n\r\n```javascript\r\nvar webpack = require('webpack');\r\nvar WebpackDevServer = require('webpack-dev-server');\r\nvar config = require('./webpack.config');\r\n\r\nnew WebpackDevServer(webpack(config), {\r\n  publicPath: config.output.publicPath,\r\n  hot: true,\r\n  historyApiFallback: true\r\n}).listen(3000, 'localhost', function (err, result) {\r\n  if (err) console.log(err);\r\n  console.log('Listening at localhost:3000');\r\n});\r\n```\r\n\r\n这样就可以在本地3000端口开启调试服务器了，比如我的页面是根目录下地`index.html`，就可以直接通过`http://localhost:3000/index.html`访问页面，修改React组件后页面也会被同步修改，这里貌似使用了websocket来同步数据。图是一个简单的效果：\r\n\r\n![Alt text](http://skyinlayerblog.qiniudn.com/blog/test.gif)\r\n\r\n## 结束\r\n\r\nReact的组件化开发很有想法，而Webpack使得React组件编写和管理更加方便，这里只涉及到了React和Webpack得很小一部分，还有更多的最佳实践有待在学习的路上不断发掘"},"comment":{"url":"https://api.github.com/repos/tmallfe/tmallfe.github.io/issues/comments/135723714","html_url":"https://github.com/tmallfe/tmallfe.github.io/issues/23#issuecomment-135723714","issue_url":"https://api.github.com/repos/tmallfe/tmallfe.github.io/issues/23","id":135723714,"user":{"login":"coderstudy","id":7262553,"avatar_url":"https://avatars.githubusercontent.com/u/7262553?v=3","gravatar_id":"","url":"https://api.github.com/users/coderstudy","html_url":"https://github.com/coderstudy","followers_url":"https://api.github.com/users/coderstudy/followers","following_url":"https://api.github.com/users/coderstudy/following{/other_user}","gists_url":"https://api.github.com/users/coderstudy/gists{/gist_id}","starred_url":"https://api.github.com/users/coderstudy/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/coderstudy/subscriptions","organizations_url":"https://api.github.com/users/coderstudy/orgs","repos_url":"https://api.github.com/users/coderstudy/repos","events_url":"https://api.github.com/users/coderstudy/events{/privacy}","received_events_url":"https://api.github.com/users/coderstudy/received_events","type":"User","site_admin":false},"created_at":"2015-08-28T09:55:58Z","updated_at":"2015-08-28T09:55:58Z","body":"恩"}}