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