> 原文:http://www.infoq.com/cn/articles/react-and-webpack
> 作者:王沛
上一篇我們對React有了一個總體的認識,在介紹其中的技術細節之前,我們首先來了解一下用于React開發和模塊管理的主流工具Webpack。稱之為React開發神器有點標題黨了,不過Webpack確實是筆者見過的功能最為強大的前端模塊管理和打包工具。雖然Webpack是一個通用的工具,并不只適合于React,但是很多React的文章或者項目都使用了Webpack,尤其是[react-hot-loader](https://github.com/gaearon/react-hot-loader)這樣的神器存在,讓Webpack成為最主流的React開發工具。
CommonJS和AMD是用于JavaScript模塊管理的兩大規范,前者定義的是模塊的同步加載,主要用于NodeJS;而后者則是異步加載,通過requirejs等工具適用于前端。隨著npm成為主流的JavaScript組件發布平臺,越來越多的前端項目也依賴于npm上的項目,或者自身就會發布到npm平臺。因此,讓前端項目更方便的使用npm上的資源成為一大需求。于是誕生了類似[browserify](http://browserify.org/)這樣的工具,代碼中可以使用require函數直接以同步語法形式引入npm模塊,打包后再由瀏覽器執行。
Webpack其實有點類似browserify,出自Facebook的Instagram團隊,但功能比browserify更為強大。其主要特性如下:
1. 同時支持[CommonJS](http://wiki.commonjs.org/wiki/Modules/1.1)和[AMD](https://github.com/amdjs/amdjs-api/wiki/AMD)模塊(對于新項目,推薦直接使用CommonJS);
2. 串聯式模塊加載器以及插件機制,讓其具有更好的靈活性和擴展性,例如提供對CoffeeScript、ES6的支持;
3. 可以基于配置或者智能分析打包成多個文件,實現公共模塊或者按需加載;
4. 支持對CSS,圖片等資源進行打包,從而無需借助Grunt或Gulp;
5. 開發時在內存中完成打包,性能更快,完全可以支持開發過程的實時打包需求;
6. 對sourcemap有很好的支持,易于調試。
Webpack將項目中用到的一切靜態資源都視之為模塊,模塊之間可以互相依賴。Webpack對它們進行統一的管理以及打包發布,其官方主頁用下面這張圖來說明Webpack的作用:

可以看到Webpack的目標就是對項目中的靜態資源進行統一管理,為產品的最終發布提供最優的打包部署方案。本文就將圍繞React對其相關用法做一個總體介紹,從而能讓你將其應用在自己的實際項目之中。
**目錄**
[TOC]
## 安裝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來使用開發服務器,因此這里不做太多介紹,只需要知道有這樣一個開發服務器可以用于開發時的內容實時打包和推送。詳細配置和用法可以參考[官方文檔](http://webpack.github.io/docs/webpack-dev-server.html)。
## 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](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/CSS_Image_Sprites)方案,用于減少對于小圖片資源的HTTP請求數量。
除了已有加載器,你也可以自己[實現自己的加載器](http://webpack.github.io/docs/how-to-write-a-loader.html),從而可以讓Webpack統一管理項目特定的靜態資源。現在也已經有很多第三方的加載器實現常見靜態資源的打包管理,可以參考Webpack主頁上的[加載器列表](http://webpack.github.io/docs/list-of-loaders.html)。
## React開發神器:react-hot-loader
Webpack本身具有運行時模塊替換功能,稱之為[Hot Module Replacement](http://webpack.github.io/docs/hot-module-replacement.html)?(HMR)。當某個模塊代碼發生變化時,Webpack實時打包將其推送到頁面并進行替換,從而無需刷新頁面就實現代碼替換。這個過程相對比較復雜,需要進行多方面考慮和配置。而現在針對React出現了一個第三方[react-hot-loader](https://github.com/gaearon/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](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](https://github.com/supnate/react-tab-selector)上,大家可以參考。
這樣,React的熱加載開發環境即配置完成,任何修改只要以保存,就會在頁面上立刻體現出來。無論是對樣式修改,還是對界面渲染的修改,甚至事件綁定處理函數的修改,都可以立刻生效,不得不說是提高開發效率的神器。
## 將Webpack開發服務器集成到已有服務器
盡管Webpack開發服務器可以直接用于開發,但實際項目中我們可能必須使用自己的Web服務器。這就需要我們能將Webpack的服務集成到已有服務器,來使用Webpack提供的模塊打包和加載功能。要實現這一點其實非常容易,只需要在載入打包文件時指定完整的URL地址,例如:
~~~
<script src="http://127.0.0.1:3000/assets/bundle.js"></script>
~~~
這就告訴當前頁面應該去另外一個服務器獲得腳本資源文件,在之前我們已經在配置文件中指定了開發服務器的地址,因此打包后的文件也知道應該通過哪個地址去建立Socket IO來動態加載模塊。整個資源架構如下圖所示:

## 打包成多個資源文件
將項目中的模塊打包成多個資源文件有兩個目的:
1. 將多個頁面的公用模塊獨立打包,從而可以利用瀏覽器緩存機制來提高頁面加載效率;
2. 減少頁面初次加載時間,只有當某功能被用到時,才去動態的加載。
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示例組件](https://github.com/supnate/react-tab-selector)項目中,大家可以參考。除了這里介紹的功能,Webpack還有許多強大的特性,例如插件機制、支持動態表達式的require、打包文件的智能重組、性能優化、代碼混淆等等。限于篇幅不再一一介紹,其[官方文檔](http://webpack.github.io/docs/)也非常完善,需要時可以參考。