编者按:自 2013 年 Facebook 发布以来,React 吸引了越来越多的开发者,基于它的衍生技术,如 React Native、React Canvas 等也层出不穷。InfoQ 精心策划“深入浅出React ”系列文章,为读者剖析React 开发的技术细节。
上一篇我们对React 有了一个总体的认识,在介绍其中的技术细节之前,我们首先来了解一下用于React 开发和模块管理的主流工具Webpack。称之为React 开发神器有点标题党了,不过Webpack 确实是笔者见过的功能最为强大的前端模块管理和打包工具。虽然Webpack 是一个通用的工具,并不只适合于React,但是很多React 的文章或者项目都使用了Webpack,尤其是 react-hot-loader 这样的神器存在,让 Webpack 成为最主流的 React 开发工具。
CommonJS 和 AMD 是用于 JavaScript 模块管理的两大规范,前者定义的是模块的同步加载,主要用于 NodeJS;而后者则是异步加载,通过 requirejs 等工具适用于前端。随着 npm 成为主流的 JavaScript 组件发布平台,越来越多的前端项目也依赖于 npm 上的项目,或者自身就会发布到 npm 平台。因此,让前端项目更方便的使用 npm 上的资源成为一大需求。于是诞生了类似 browserify 这样的工具,代码中可以使用 require 函数直接以同步语法形式引入 npm 模块,打包后再由浏览器执行。
Webpack 其实有点类似 browserify,出自 Facebook 的 Instagram 团队,但功能比 browserify 更为强大。其主要特性如下:
- 同时支持 CommonJS 和 AMD 模块(对于新项目,推荐直接使用 CommonJS);
- 串联式模块加载器以及插件机制,让其具有更好的灵活性和扩展性,例如提供对 CoffeeScript、ES6 的支持;
- 可以基于配置或者智能分析打包成多个文件,实现公共模块或者按需加载;
- 支持对 CSS,图片等资源进行打包,从而无需借助 Grunt 或 Gulp;
- 开发时在内存中完成打包,性能更快,完全可以支持开发过程的实时打包需求;
- 对 sourcemap 有很好的支持,易于调试。
Webpack 将项目中用到的一切静态资源都视之为模块,模块之间可以互相依赖。Webpack 对它们进行统一的管理以及打包发布,其官方主页用下面这张图来说明 Webpack 的作用:
可以看到 Webpack 的目标就是对项目中的静态资源进行统一管理,为产品的最终发布提供最优的打包部署方案。本文就将围绕 React 对其相关用法做一个总体介绍,从而能让你将其应用在自己的实际项目之中。
安装 Webpack,并加载一个简单的 React 组件
Webpack 一般作为全局的 npm 模块安装:
npm install -g webpack
之后便有了全局的 webpack 命令,直接执行此命令会默认使用当前目录的 webpack.config.js 作为配置文件。如果要指定另外的配置文件,可以执行:
webpack —config webpack.custom.config.js
尽管 Webpack 可以通过命令行来指定参数,但我们通常会将所有相关参数定义在配置文件中。一般我们会定义两个配置文件,一个用于开发时,另外一个用于产品发布。生产环境下的打包文件不需要包含 sourcemap 等用于开发时的代码。配置文件通常放在项目根目录之下,其本身也是一个标准的 CommonJS 模块。
一个最简单的 Webpack 配置文件 webpack.config.js 如下所示:
module.exports = { entry:[ './app/main.js' ], output: { path: __dirname + '/assets/', publicPath: "/assets/", filename: 'bundle.js' } };
其中 entry 参数定义了打包后的入口文件,数组中的所有文件会按顺序打包。每个文件进行依赖的递归查找,直到所有相关模块都被打包。output 参数定义了输出文件的位置,其中常用的参数包括:
- path: 打包文件存放的绝对路径
- publicPath: 网站运行时的访问路径
- filename: 打包后的文件名
现在来看如何打包一个 React 组件。假设有如下项目文件夹结构:
- react-sample + assets/ - js/ Hello.js entry.js index.html webpack.config.js
其中 Hello.js 定义了一个简单的 React 组件,使用 ES6 语法:
var React = require('react'); class Hello extends React.Component { render() { return ( <h1>Hello {this.props.name}!</h1> ); } }
entry.js 是入口文件,将一个 Hello 组件输出到界面:
var React = require('react'); var Hello = require('./Hello'); React.render(<Hello name="Nate" />, document.body);
index.html 的内容如下:
<html> <head></head> <body> <script src="/assets/bundle.js"></script> </body> </html>
在这里 Hello.js 和 entry.js 都是 JSX 组件语法,需要对它们进行预处理,这就要引入 webpack 的 JSX 加载器。因此在配置文件中加入如下配置:
module: { loaders: [ { test: /\.jsx?$/, loaders: ['jsx?harmony']} ] }
加载器的概念稍后还会详细介绍,这里只需要知道它能将 JSX 编译成 JavaScript 并加载为 Webpack 模块。这样在当前目录执行 webpack 命令之后,在 assets 目录将生成 bundle.js,打包了 entry.js 的内容。当浏览器打开当前服务器上的 index.html,将显示“Hello Nate!”。这是一个非常简单的例子,演示了如何使用 Webpack 来进行最简单的 React 组件打包。
加载 AMD 或 CommonJS 模块
在实际项目中,代码以模块进行组织,AMD 是在 CommonJS 的基础上考虑了浏览器的异步加载特性而产生的,可以让模块异步加载并保证执行顺序。而 CommonJS 的require
函数则是同步加载。在 Webpack 中笔者更加推荐 CommonJS 方式去加载模块,这种方式语法更加简洁直观。即使在开发时,我们也是加载 Webpack 打包后的文件,通过 sourcemap 去进行调试。
除了项目本身的模块,我们也需要依赖第三方的模块,现在比较常用的第三方模块基本都通过 npm 进行发布,使用它们已经无需单独下载管理,需要时执行npm install
即可。例如,我们需要依赖 jQuery,只需执行:
npm install jquery —save-dev
更多情况下我们是在项目的 package.json 中进行依赖管理,然后通过直接执行 npm install 来安装所有依赖。这样在项目的代码仓库中并不需要存储实际的第三方依赖库的代码。
安装之后,在需要使用 jquery 的模块中需要在头部进行引入:
var $ = require('jquery'); $('body').html('Hello Webpack!');
可以看到,这种以 CommonJS 的同步形式去引入其它模块的方式代码更加简洁。浏览器并不会实际的去同步加载这个模块,require 的处理是由 Webpack 进行解析和打包的,浏览器只需要执行打包后的代码。Webpack 自身已经可以完全处理 JavaScript 模块的加载,但是对于 React 中的 JSX 语法,这就需要使用 Webpack 的扩展加载器来处理了。
Webpack 开发服务器
除了提供模块打包功能,Webpack 还提供了一个基于 Node.js Express 框架的开发服务器,它是一个静态资源 Web 服务器,对于简单静态页面或者仅依赖于独立服务的前端页面,都可以直接使用这个开发服务器进行开发。在开发过程中,开发服务器会监听每一个文件的变化,进行实时打包,并且可以推送通知前端页面代码发生了变化,从而可以实现页面的自动刷新。
Webpack 开发服务器需要单独安装,同样是通过 npm 进行:
npm install -g webpack-dev-server
之后便可以运行 webpack-dev-server 命令来启动开发服务器,然后通过 localhost:8080/webpack-dev-server/ 访问到页面了。默认情况下服务器以当前目录作为服务器目录。在 React 开发中,我们通常会结合 react-hot-loader 来使用开发服务器,因此这里不做太多介绍,只需要知道有这样一个开发服务器可以用于开发时的内容实时打包和推送。详细配置和用法可以参考官方文档。
Webpack 模块加载器(Loaders)
Webpack 将所有静态资源都认为是模块,比如 JavaScript,CSS,LESS,TypeScript,JSX,CoffeeScript,图片等等,从而可以对其进行统一管理。为此 Webpack 引入了加载器的概念,除了纯 JavaScript 之外,每一种资源都可以通过对应的加载器处理成模块。和大多数包管理器不一样的是,Webpack 的加载器之间可以进行串联,一个加载器的输出可以成为另一个加载器的输入。比如 LESS 文件先通过 less-load 处理成 css,然后再通过 css-loader 加载成 css 模块,最后由 style-loader 加载器对其做最后的处理,从而运行时可以通过 style 标签将其应用到最终的浏览器环境。
对于 React 的 JSX 也是如此,它通过 jsx-loader 来载入。jsx-loader 专门用于载入 React 的 JSX 文件,Webpack 的加载器支持参数,jsx-loader 就可以添加?harmony 参数使其支持 ES6 语法。为了让 Webpack 识别什么样的资源应该用什么加载器去载入,需要在配置文件进行配置:通过正则表达式对文件名进行匹配。例如:
module: { preLoaders: [{ test: /\.js$/, exclude: /node_modules/, loader: 'jsxhint' }], loaders: [{ test: /\.js$/, exclude: /node_modules/, loader: 'react-hot!jsx-loader?harmony' }, { test: /\.less/, loader: 'style-loader!css-loader!less-loader' }, { test: /\.(css)$/, loader: 'style-loader!css-loader' }, { test: /\.(png|jpg)$/, loader: 'url-loader?limit=8192' }] }
可以看到,该使用什么加载器完全取决于这里的配置,即使对于 JSX 文件,我们也可以用 js 作为后缀,从而所有的 JavaScript 都可以通过 jsx-loader 载入,因为 jsx 本身就是完全兼容 JavaScript 的,所以即使没有 JSX 语法,普通 JavaScript 模块也可以使用 jsx-loader 来载入。
加载器之间的级联是通过感叹号来连接,例如对于 LESS 资源,写法为 style-loader!css-loader!less-loader。对于小型的图片资源,也可以将其进行统一打包,由 url-loader 实现,代码中url-loader?limit=8192
含义就是对于所有小于 8192 字节的图片资源也进行打包。这在一定程度上可以替代 Css Sprites 方案,用于减少对于小图片资源的 HTTP 请求数量。
除了已有加载器,你也可以自己实现自己的加载器,从而可以让Webpack 统一管理项目特定的静态资源。现在也已经有很多第三方的加载器实现常见静态资源的打包管理,可以参考Webpack 主页上的加载器列表。
React 开发神器:react-hot-loader
Webpack 本身具有运行时模块替换功能,称之为 Hot Module Replacement (HMR)。当某个模块代码发生变化时,Webpack 实时打包将其推送到页面并进行替换,从而无需刷新页面就实现代码替换。这个过程相对比较复杂,需要进行多方面考虑和配置。而现在针对 React 出现了一个第三方 react-hot-loader 加载器,使用这个加载器就可以轻松实现 React 组件的热替换,非常方便。其实正是因为 React 的每一次更新都是全局刷新的虚拟 DOM 机制,让 React 组件的热替换可以成为通用的加载器,从而极大提高开发效率。
要使用 react-hot-loader,首先通过 npm 进行安装:
npm install —save-dev react-hot-loader
之后,Webpack 开发服务器需要开启 HMR 参数 hot,为了方便,我们创建一个名为 server.js 的文件用以启动 Webpack 开发服务器:
var webpack = require('webpack'); var WebpackDevServer = require('webpack-dev-server'); var config = require('../webpack.config'); new WebpackDevServer(webpack(config), { publicPath: config.output.publicPath, hot: true, noInfo: false, historyApiFallback: true }).listen(3000, '127.0.0.1', function (err, result) { if (err) { console.log(err); } console.log('Listening at localhost:3000'); });
为了热加载 React 组件,我们需要在前端页面中加入相应的代码,用以接收 Webpack 推送过来的代码模块,进而可以通知所有相关 React 组件进行重新 Render。加入这个代码很简单:
entry: [ 'webpack-dev-server/client?http://127.0.0.1:3000', // WebpackDevServer host and port 'webpack/hot/only-dev-server', './scripts/entry' // Your appʼs entry point ]
需要注意的是,这里的 client? http://127.0.0.1:3000 需要和在 server.js 中启动 Webpack 开发服务器的地址匹配。这样,打包生成的文件就知道该从哪里去获取动态的代码更新。下一步,我们需要让 Webpack 用 react-hot-loader 去加载 React 组件,如上一节所介绍,这通过加载器配置完成:
loaders: [{ test: /\.js$/, exclude: /node_modules/, loader: 'react-hot!jsx-loader?harmony' }, … ]
做完这些配置之后,使用 Node.js 运行 server.js:
node server.js
即可启动开发服务器并实现 React 组件的热加载。为了方便,我们也可以在 package.json 中加入一节配置:
"scripts": { "start": "node ./js/server.js" }
从而通过 npm start 命令即可启动开发服务器。示例代码也上传在 Github 上,大家可以参考。
这样,React 的热加载开发环境即配置完成,任何修改只要以保存,就会在页面上立刻体现出来。无论是对样式修改,还是对界面渲染的修改,甚至事件绑定处理函数的修改,都可以立刻生效,不得不说是提高开发效率的神器。
将 Webpack 开发服务器集成到已有服务器
尽管 Webpack 开发服务器可以直接用于开发,但实际项目中我们可能必须使用自己的 Web 服务器。这就需要我们能将 Webpack 的服务集成到已有服务器,来使用 Webpack 提供的模块打包和加载功能。要实现这一点其实非常容易,只需要在载入打包文件时指定完整的 URL 地址,例如:
<script src="http://127.0.0.1:3000/assets/bundle.js"></script>
这就告诉当前页面应该去另外一个服务器获得脚本资源文件,在之前我们已经在配置文件中指定了开发服务器的地址,因此打包后的文件也知道应该通过哪个地址去建立 Socket IO 来动态加载模块。整个资源架构如下图所示:
打包成多个资源文件
将项目中的模块打包成多个资源文件有两个目的:
- 将多个页面的公用模块独立打包,从而可以利用浏览器缓存机制来提高页面加载效率;
- 减少页面初次加载时间,只有当某功能被用到时,才去动态的加载。
Webpack 提供了非常强大的功能让你能够灵活的对打包方案进行配置。首先来看如何创建多个入口文件:
{ entry: { a: "./a", b: "./b" }, output: { filename: "[name].js" }, plugins: [ new webpack.CommonsChunkPlugin("init.js") ] }
可以看到,配置文件中定义了两个打包资源“a”和“b”,在输出文件中使用方括号来获得输出文件名。而在插件设置中使用了 CommonsChunkPlugin,Webpack 中将打包后的文件都称之为“Chunk”。这个插件可以将多个打包后的资源中的公共部分打包成单独的文件,这里指定公共文件输出为“init.js”。这样我们就获得了三个打包后的文件,在 html 页面中可以这样引用:
<script src="init.js"></script> <script src="a.js"></script> <script src="b.js"></script>
除了在配置文件中对打包文件进行配置,还可以在代码中进行定义:require.ensure,例如:
require.ensure(["module-a", "module-b"], function(require) { var a = require("module-a"); // ... });
Webpack 在编译时会扫描到这样的代码,并对依赖模块进行自动打包,运行过程中执行到这段代码时会自动找到打包后的文件进行按需加载。
小结
本文结合 React 介绍了 Webpack 的基本功能和用法,希望能让大家对这个新兴而强大的模块管理工具有一个总体的认识,并能将其应用在实际的项目开发中。笔者也将其应用在之前提供的 React 示例组件项目中,大家可以参考。除了这里介绍的功能,Webpack 还有许多强大的特性,例如插件机制、支持动态表达式的 require、打包文件的智能重组、性能优化、代码混淆等等。限于篇幅不再一一介绍,其官方文档也非常完善,需要时可以参考。
感谢徐川对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群)。
评论