【新東網(wǎng)技術(shù)大咖帶您走進(jìn)Webpack】談?wù)凴eact開(kāi)發(fā)神器webpack是什么鬼!
發(fā)布時(shí)間: 2016-09-23 15:13:35
文/謝海東 通信研發(fā)部
新東網(wǎng)自2001年成立以來(lái),掌握大數據、云計算、通信、物聯(lián)網(wǎng)及區塊鏈等信息技術(shù),擁有一支逾16年經(jīng)驗的強大IT團隊。為沉淀企業(yè)技術(shù)實(shí)力,繼續發(fā)揮行業(yè)優(yōu)勢,《東網(wǎng)快訊》特邀新東網(wǎng)技術(shù)大咖帶您走進(jìn)這些領(lǐng)先進(jìn)信息技術(shù),揭秘新東網(wǎng)16年來(lái)的技術(shù)成果,每周五發(fā)布。
基于上一期華西的老閆→_→介紹了React的衍生技術(shù)React Native(多么無(wú)私啊,請猛戳我的姊妹篇吧?。?/span>,這期我們就來(lái)談?wù)動(dòng)糜赗eact開(kāi)發(fā)和模塊管理的主流工具Webpack。
雖然Webpack是一個(gè)通用的工具,并不只適合于React,但由于很多涉及React的項目都使用了Webpack,尤其是還有react-hot-loader這樣的神器存在,于是很自然地,Webpack成為了最主流的React開(kāi)發(fā)工具。
Webpack有點(diǎn)類(lèi)似browserify,出自Facebook的Instagram團隊,但功能比browserify更為強大。其主要特性如下:
1. 同時(shí)支持CommonJS和AMD模塊(對于新項目,推薦直接使用CommonJS);
2. 串聯(lián)式模塊加載器以及插件機制,讓其具有更好的靈活性和擴展性,例如提供對CoffeeScript、ES6的支持;
3. 可以基于配置或者智能分析打包成多個(gè)文件,實(shí)現公共模塊或者按需加載;
4. 支持對CSS、圖片等資源進(jìn)行打包,從而無(wú)需借助Grunt或Gulp;
5. 開(kāi)發(fā)時(shí)在內存中完成打包,性能更快,完全可以支持開(kāi)發(fā)過(guò)程的實(shí)時(shí)打包需求;
6. 對sourcemap有很好的支持,易于調試。
Webpack將項目中用到的一切靜態(tài)資源都視之為模塊,模塊之間可以互相依賴(lài)。Webpack對它們進(jìn)行統一的管理以及打包發(fā)布,其官方主頁(yè)用下面這張圖來(lái)說(shuō)明Webpack的作用:
可以看到Webpack的目標就是對項目中的靜態(tài)資源進(jìn)行統一管理,為產(chǎn)品的最終發(fā)布提供優(yōu)秀的打包部署方案。
Webpack一般作為全局的npm模塊安裝:
npm install -g webpack
之后便有了全局的webpack命令,直接執行此命令會(huì )默認使用當前目錄的webpack.config.js作為配置文件。如果要指定另外的配置文件,可以執行:
webpack —config webpack.custom.config.js
盡管Webpack可以通過(guò)命令行來(lái)指定參數,但我們通常會(huì )將所有相關(guān)參數定義在配置文件中。一般我們會(huì )定義兩個(gè)配置文件,一個(gè)用于開(kāi)發(fā)時(shí),另外一個(gè)用于產(chǎn)品發(fā)布。生產(chǎn)環(huán)境下的打包文件不需要包含sourcemap等用于開(kāi)發(fā)時(shí)的代碼。配置文件通常放在項目根目錄之下,其本身也是一個(gè)標準的CommonJS模塊。
一個(gè)最簡(jiǎn)單的Webpack配置文件webpack.config.js如下所示:
module.exports = {
entry:[
'./app/main.js'
],
output: {
path: __dirname + '/assets/',
publicPath: "/assets/",
filename: 'bundle.js'
}
};
其中entry參數定義了打包后的入口文件,數組中的所有文件會(huì )按順序打包。每個(gè)文件進(jìn)行依賴(lài)的遞歸查找,直到所有相關(guān)模塊都被打包。output參數定義了輸出文件的位置,其中常用的參數包括:
· path: 打包文件存放的絕對路徑
· publicPath: 網(wǎng)站運行時(shí)的訪(fǎng)問(wèn)路徑
· filename: 打包后的文件名
現在來(lái)看如何打包一個(gè)React組件。假設有如下項目文件夾結構:
- react-sample
+ assets/
- js/
Hello.js
entry.js
index.html
webpack.config.js
其中Hello.js定義了一個(gè)簡(jiǎn)單的React組件,使用ES6語(yǔ)法:
var React = require('react');
class Hello extends React.Component {
render() {
return (
<h1>Hello {this.props.name}!h1>
);
}
}
module.exports= Hello;
entry.js是入口文件,將一個(gè)Hello組件輸出到界面:
var ReactDOM = require('react-dom');
var React = require('react');
var Hello = require('./Hello');
ReactDOM.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組件語(yǔ)法,需要對它們進(jìn)行預處理,這就要引入webpack的JSX加載器。因此在配置文件中加入如下配置:
module: {
loaders: [
{ test: /\.jsx?$/, loaders: ['jsx?harmony']}
]
}
加載器的概念稍后還會(huì )詳細介紹,這里只需要知道它能將JSX編譯成JavaScript并加載為Webpack模塊。這樣在當前目錄執行webpack命令之后,在assets目錄將生成bundle.js,打包了entry.js的內容。當瀏覽器打開(kāi)當前服務(wù)器上的index.html,將顯示“Hello Nate!”。這是一個(gè)非常簡(jiǎn)單的例子,演示了如何使用Webpack來(lái)進(jìn)行簡(jiǎn)單的React組件打包。
在實(shí)際項目中,代碼以模塊進(jìn)行組織,AMD是在CommonJS的基礎上考慮了瀏覽器的異步加載特性而產(chǎn)生的,可以讓模塊異步加載并保證執行順序。而CommonJS的require函數則是同步加載。在Webpack中更加推薦CommonJS方式去加載模塊,這種方式語(yǔ)法更加簡(jiǎn)潔直觀(guān)。即使在開(kāi)發(fā)時(shí),我們也是加載Webpack打包后的文件,通過(guò)sourcemap去進(jìn)行調試。
除了項目本身的模塊,我們也需要依賴(lài)第三方的模塊,現在比較常用的第三方模塊基本都通過(guò)npm進(jìn)行發(fā)布,使用它們已經(jīng)無(wú)需單獨下載管理,需要時(shí)執行npm install即可。例如,我們需要依賴(lài)jQuery,只需執行:
npm install jquery —save-dev
更多情況下我們是在項目的package.json中進(jìn)行依賴(lài)管理,然后通過(guò)直接執行npm install來(lái)安裝所有依賴(lài)。這樣在項目的代碼倉庫中并不需要存儲實(shí)際的第三方依賴(lài)庫的代碼。
安裝之后,在需要使用jquery的模塊中需要在頭部進(jìn)行引入:
var $ = require('jquery');
$('body').html('Hello Webpack!');
可以看到,這種以CommonJS的同步形式去引入其它模塊的方式代碼更加簡(jiǎn)潔。瀏覽器并不會(huì )實(shí)際的去同步加載這個(gè)模塊,require的處理是由Webpack進(jìn)行解析和打包的,瀏覽器只需要執行打包后的代碼。Webpack自身已經(jīng)可以完全處理JavaScript模塊的加載,但是對于React中的JSX語(yǔ)法,這就需要使用Webpack的擴展加載器來(lái)處理了。
除了提供模塊打包功能,Webpack還提供了一個(gè)基于Node.js Express框架的開(kāi)發(fā)服務(wù)器,它是一個(gè)靜態(tài)資源Web服務(wù)器,對于簡(jiǎn)單靜態(tài)頁(yè)面或者僅依賴(lài)于獨立服務(wù)的前端頁(yè)面,都可以直接使用這個(gè)開(kāi)發(fā)服務(wù)器進(jìn)行開(kāi)發(fā)。在開(kāi)發(fā)過(guò)程中,開(kāi)發(fā)服務(wù)器會(huì )監聽(tīng)每一個(gè)文件的變化,進(jìn)行實(shí)時(shí)打包,并且可以推送通知前端頁(yè)面代碼發(fā)生了變化,從而可以實(shí)現頁(yè)面的自動(dòng)刷新。
Webpack開(kāi)發(fā)服務(wù)器需要單獨安裝,同樣是通過(guò)npm進(jìn)行:
npm install -g webpack-dev-server
之后便可以運行webpack-dev-server命令來(lái)啟動(dòng)開(kāi)發(fā)服務(wù)器,然后通過(guò)localhost:8080/webpack-dev-server/訪(fǎng)問(wèn)到頁(yè)面了。默認情況下服務(wù)器以當前目錄作為服務(wù)器目錄。在React開(kāi)發(fā)中,我們通常會(huì )結合react-hot-loader來(lái)使用開(kāi)發(fā)服務(wù)器,因此這里不做太多介紹,只需要知道有這樣一個(gè)開(kāi)發(fā)服務(wù)器可以用于開(kāi)發(fā)時(shí)的內容實(shí)時(shí)打包和推送。
Webpack將所有靜態(tài)資源都認為是模塊,比如JavaScript,CSS,LESS,TypeScript,JSX,CoffeeScript,圖片等等,從而可以對其進(jìn)行統一管理。為此Webpack引入了加載器的概念,除了純JavaScript之外,每一種資源都可以通過(guò)對應的加載器處理成模塊。和大多數包管理器不一樣的是,Webpack的加載器之間可以進(jìn)行串聯(lián),一個(gè)加載器的輸出可以成為另一個(gè)加載器的輸入。比如LESS文件先通過(guò)less-load處理成css,然后再通過(guò)css-loader加載成css模塊,最后由style-loader加載器對其做最后的處理,從而運行時(shí)可以通過(guò)style標簽將其應用到最終的瀏覽器環(huán)境。
對于React的JSX也是如此,它通過(guò)jsx-loader來(lái)載入。jsx-loader專(zhuān)門(mén)用于載入React的JSX文件,Webpack的加載器支持參數,jsx-loader就可以添加?harmony參數使其支持ES6語(yǔ)法。為了讓W(xué)ebpack識別什么樣的資源應該用什么加載器去載入,需要在配置文件進(jìn)行配置:通過(guò)正則表達式對文件名進(jìn)行匹配。例如:
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都可以通過(guò)jsx-loader載入,因為jsx本身就是完全兼容JavaScript的,所以即使沒(méi)有JSX語(yǔ)法,普通JavaScript模塊也可以使用jsx-loader來(lái)載入。
加載器之間的級聯(lián)是通過(guò)感嘆號來(lái)連接,例如對于LESS資源,寫(xiě)法為style-loader!css-loader!less-loader。對于小型的圖片資源,也可以將其進(jìn)行統一打包,由url-loader實(shí)現,代碼中url-loader?limit=8192含義就是對于所有小于8192字節的圖片資源也進(jìn)行打包。這在一定程度上可以替代Css Sprites方案,用于減少對于小圖片資源的HTTP請求數量。
除了已有加載器,也可以自己實(shí)現自己的加載器,從而可以讓W(xué)ebpack統一管理項目特定的靜態(tài)資源。
Webpack本身具有運行時(shí)模塊替換功能,稱(chēng)之為Hot Module Replacement (HMR)。當某個(gè)模塊代碼發(fā)生變化時(shí),Webpack實(shí)時(shí)打包將其推送到頁(yè)面并進(jìn)行替換,從而無(wú)需刷新頁(yè)面就實(shí)現代碼替換。這個(gè)過(guò)程相對比較復雜,需要進(jìn)行多方面考慮和配置。而現在針對React出現了一個(gè)第三方react-hot-loader加載器,使用這個(gè)加載器就可以輕松實(shí)現React組件的熱替換,非常方便。其實(shí)正是因為React的每一次更新都是全局刷新的虛擬DOM機制,讓React組件的熱替換可以成為通用的加載器,從而極大提高開(kāi)發(fā)效率。
要使用react-hot-loader,首先通過(guò)npm進(jìn)行安裝:
npm install —save-dev react-hot-loader
之后,Webpack開(kāi)發(fā)服務(wù)器需要開(kāi)啟HMR參數hot,為了方便,我們創(chuàng )建一個(gè)名為server.js的文件用以啟動(dòng)Webpack開(kāi)發(fā)服務(wù)器:
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組件,我們需要在前端頁(yè)面中加入相應的代碼,用以接收Webpack推送過(guò)來(lái)的代碼模塊,進(jìn)而可以通知所有相關(guān)React組件進(jìn)行重新Render。加入這個(gè)代碼很簡(jiǎn)單:
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中啟動(dòng)Webpack開(kāi)發(fā)服務(wù)器的地址匹配。這樣,打包生成的文件就知道該從哪里去獲取動(dòng)態(tài)的代碼更新。下一步,我們需要讓W(xué)ebpack用react-hot-loader去加載React組件,如前面所介紹,這通過(guò)加載器配置完成:
loaders: [{
test: /\.js$/,
exclude: /node_modules/,
loader: 'react-hot!jsx-loader?harmony'
},
…
]
做完這些配置之后,使用Node.js運行server.js:
node server.js
即可啟動(dòng)開(kāi)發(fā)服務(wù)器并實(shí)現React組件的熱加載。為了方便,我們也可以在package.json中加入一節配置:
"scripts": {
"start": "node ./js/server.js"
}
從而通過(guò)npm start命令即可啟動(dòng)開(kāi)發(fā)服務(wù)器。
這樣,React的熱加載開(kāi)發(fā)環(huán)境即配置完成,任何修改只要以保存,就會(huì )在頁(yè)面上立刻體現出來(lái)。無(wú)論是對樣式修改,還是對界面渲染的修改,甚至事件綁定處理函數的修改,都可以立刻生效,不得不說(shuō)是提高開(kāi)發(fā)效率的神器。
盡管Webpack開(kāi)發(fā)服務(wù)器可以直接用于開(kāi)發(fā),但實(shí)際項目中我們基本都使用自己的Web服務(wù)器。這就需要我們能將Webpack的服務(wù)集成到已有服務(wù)器,來(lái)使用Webpack提供的模塊打包和加載功能。要實(shí)現這一點(diǎn)其實(shí)非常容易,只需要在載入打包文件時(shí)指定完整的URL地址,例如:
<script src="http://127.0.0.1:3000/assets/bundle.js">script>
這就告訴當前頁(yè)面應該去另外一個(gè)服務(wù)器獲得腳本資源文件,在之前我們已經(jīng)在配置文件中指定了開(kāi)發(fā)服務(wù)器的地址,因此打包后的文件也知道應該通過(guò)哪個(gè)地址去建立Socket IO來(lái)動(dòng)態(tài)加載模塊。整個(gè)資源架構如下圖所示:
將項目中的模塊打包成多個(gè)資源文件有兩個(gè)目的:
1. 將多個(gè)頁(yè)面的公用模塊獨立打包,從而可以利用瀏覽器緩存機制來(lái)提高頁(yè)面加載效率;
2. 減少頁(yè)面初次加載時(shí)間,只有當某功能被用到時(shí),才去動(dòng)態(tài)的加載。
Webpack提供了非常強大的功能讓你能夠靈活的對打包方案進(jìn)行配置。首先來(lái)看如何創(chuàng )建多個(gè)入口文件:
{
entry: { a: "./a", b: "./b" },
output: { filename: "[name].js" },
plugins: [ new webpack.CommonsChunkPlugin("init.js") ]
}
可以看到,配置文件中定義了兩個(gè)打包資源“a”和“b”,在輸出文件中使用方括號來(lái)獲得輸出文件名。而在插件設置中使用了CommonsChunkPlugin,Webpack中將打包后的文件都稱(chēng)之為“Chunk”。這個(gè)插件可以將多個(gè)打包后的資源中的公共部分打包成單獨的文件,這里指定公共文件輸出為“init.js”。這樣我們就獲得了三個(gè)打包后的文件,在html頁(yè)面中可以這樣引用:
<script src="init.js">script>
<script src="a.js">script>
<script src="b.js">script>
除了在配置文件中對打包文件進(jìn)行配置,還可以在代碼中進(jìn)行定義:require.ensure,例如:
require.ensure(["module-a", "module-b"], function(require) {
var a = require("module-a");
// ...
});
Webpack在編譯時(shí)會(huì )掃描到這樣的代碼,并對依賴(lài)模塊進(jìn)行自動(dòng)打包,運行過(guò)程中執行到這段代碼時(shí)會(huì )自動(dòng)找到打包后的文件進(jìn)行按需加載。
結合React介紹了Webpack的基本功能和用法,希望能讓大家對這個(gè)新興而強大的模塊管理工具有一個(gè)總體的認識,并能將其應用在實(shí)際的項目開(kāi)發(fā)中。