使用自動(dòng)刷新
在開(kāi)發(fā)階段,修改源碼是不可避免的操作。對(duì)于開(kāi)發(fā)網(wǎng)頁(yè)來(lái)說(shuō),要想看到修改后的效果,需要刷新瀏覽器讓其重新運(yùn)行最新的代碼才行。借助自動(dòng)化的手段,可以把這些重復(fù)的操作交給代碼去幫我們完成,在監(jiān)聽(tīng)到本地源碼文件發(fā)生變化時(shí),自動(dòng)重新構(gòu)建出可運(yùn)行的代碼后再控制瀏覽器刷新。Webpack把這些功能都內(nèi)置了,并且還提供多種方案可選。
文件監(jiān)聽(tīng)
文件監(jiān)聽(tīng)是在發(fā)現(xiàn)源碼文件發(fā)生變化時(shí),自動(dòng)重新構(gòu)建出新的輸出文件。
Webpack官方提供了兩大模塊,一個(gè)是核心的webpack,一個(gè)是webpack-dev-server擴(kuò)展模塊。 而文件監(jiān)聽(tīng)功能是webpack模塊提供的。
Webpack支持文件監(jiān)聽(tīng)相關(guān)的配置項(xiàng)如下:
module.export = {
// 只有在開(kāi)啟監(jiān)聽(tīng)模式時(shí),watchOptions 才有意義
// 默認(rèn)為 false,也就是不開(kāi)啟
watch: true,
// 監(jiān)聽(tīng)模式運(yùn)行時(shí)的參數(shù)
// 在開(kāi)啟監(jiān)聽(tīng)模式時(shí),才有意義
watchOptions: {
// 不監(jiān)聽(tīng)的文件或文件夾,支持正則匹配
// 默認(rèn)為空
ignored: /node_modules/,
// 監(jiān)聽(tīng)到變化發(fā)生后會(huì)等300ms再去執(zhí)行動(dòng)作,防止文件更新太快導(dǎo)致重新編譯頻率太高
// 默認(rèn)為 300ms
aggregateTimeout: 300,
// 判斷文件是否發(fā)生變化是通過(guò)不停的去詢(xún)問(wèn)系統(tǒng)指定文件有沒(méi)有變化實(shí)現(xiàn)的
// 默認(rèn)每隔1000毫秒詢(xún)問(wèn)一次
poll: 1000
}
}
要讓W(xué)ebpack開(kāi)啟監(jiān)聽(tīng)模式,有兩種方式:
- 在配置文件
webpack.config.js中設(shè)置watch: true。 - 在執(zhí)行啟動(dòng)Webpack命令時(shí),帶上
--watch參數(shù),完整命令是webpack --watch。
文件監(jiān)聽(tīng)工作原理
在 Webpack 中監(jiān)聽(tīng)一個(gè)文件發(fā)生變化的原理是定時(shí)的去獲取這個(gè)文件的最后編輯時(shí)間,每次都存下最新的最后編輯時(shí)間,如果發(fā)現(xiàn)當(dāng)前獲取的和最后一次保存的最后編輯時(shí)間不一致,就認(rèn)為該文件發(fā)生了變化。 配置項(xiàng)中的watchOptions.poll就是用于控制定時(shí)檢查的周期,具體含義是每隔多少毫秒檢查一次。
當(dāng)發(fā)現(xiàn)某個(gè)文件發(fā)生了變化時(shí),并不會(huì)立刻告訴監(jiān)聽(tīng)者,而是先緩存起來(lái),收集一段時(shí)間的變化后,再一次性告訴監(jiān)聽(tīng)者。 配置項(xiàng)中的watchOptions.aggregateTimeout就是用于配置這個(gè)等待時(shí)間。這樣做的目的是因?yàn)槲覀冊(cè)诰庉嫶a的過(guò)程中可能會(huì)高頻的輸入文字導(dǎo)致文件變化的事件高頻的發(fā)生,如果每次都重新執(zhí)行構(gòu)建就會(huì)讓構(gòu)建卡死。
對(duì)于多個(gè)文件來(lái)說(shuō),原理相似,只不過(guò)會(huì)對(duì)列表中的每一個(gè)文件都定時(shí)的執(zhí)行檢查。 但是這個(gè)需要監(jiān)聽(tīng)的文件列表是怎么確定的呢?默認(rèn)情況下Webpack會(huì)從配置的Entry文件出發(fā),遞歸解析出Entry文件所依賴(lài)的文件,把這些依賴(lài)的文件都加入到監(jiān)聽(tīng)列表中去。
由于保存文件的路徑和最后編輯時(shí)間需要占用內(nèi)存,定時(shí)檢查周期檢查需要占用CPU以及文件I/O,所以最好減少需要監(jiān)聽(tīng)的文件數(shù)量和降低檢查頻率。
優(yōu)化文件監(jiān)聽(tīng)性能
開(kāi)啟監(jiān)聽(tīng)模式時(shí),默認(rèn)情況下會(huì)監(jiān)聽(tīng)配置的Entry文件和所有其遞歸依賴(lài)的文件。 在這些文件中會(huì)有很多存在于node_modules下。 在大多數(shù)情況下我們都不可能去編輯node_modules下的文件,而是編輯自己建立的源碼文件。所以一個(gè)很大的優(yōu)化點(diǎn)就是忽略掉node_modules下的文件,不監(jiān)聽(tīng)它們。相關(guān)配置如下:
module.export = {
watchOptions: {
// 不監(jiān)聽(tīng)的 node_modules 目錄下的文件
ignored: /node_modules/,
}
}
有時(shí)你可能會(huì)覺(jué)得node_modules目錄下的第三方模塊有bug,想修改第三方模塊的文件,然后在自己的項(xiàng)目中試試。 在這種情況下如果使用了以上優(yōu)化方法,我們需要重啟構(gòu)建以看到最新效果。 但這種情況畢竟是非常少見(jiàn)的。
除了忽略掉部分文件的優(yōu)化外,還有如下兩種方法:
-
watchOptions.aggregateTimeout值越大性能越好,因?yàn)檫@能降低重新構(gòu)建的頻率。 -
watchOptions.poll值越大越好,因?yàn)檫@能降低檢查的頻率。
但兩種優(yōu)化方法的后果是會(huì)讓你感覺(jué)到監(jiān)聽(tīng)模式的反應(yīng)和靈敏度降低了。
自動(dòng)刷新瀏覽器
監(jiān)聽(tīng)到文件更新后的下一步是去刷新瀏覽器,webpack模塊負(fù)責(zé)監(jiān)聽(tīng)文件,webpack-dev-server模塊則負(fù)責(zé)刷新瀏覽器。 在使用webpack-dev-server 模塊去啟動(dòng)webpack模塊時(shí),webpack模塊的監(jiān)聽(tīng)模式默認(rèn)會(huì)被開(kāi)啟。 webpack模塊會(huì)在文件發(fā)生變化時(shí)告訴webpack-dev-server模塊。
自動(dòng)刷新的原理
控制瀏覽器刷新有三種方法:
- 借助瀏覽器擴(kuò)展去通過(guò)瀏覽器提供的接口刷新,WebStorm IDE的LiveEdit功能就是這樣實(shí)現(xiàn)的。
- 往要開(kāi)發(fā)的網(wǎng)頁(yè)中注入代理客戶(hù)端代碼,通過(guò)代理客戶(hù)端去刷新整個(gè)頁(yè)面。
- 把要開(kāi)發(fā)的網(wǎng)頁(yè)裝進(jìn)一個(gè)
iframe中,通過(guò)刷新iframe去看到最新效果。
DevServer支持第2、3種方法,第2種是DevServer默認(rèn)采用的刷新方法。
通過(guò)DevServer啟動(dòng)構(gòu)建后,你會(huì)看到如下日志:
> webpack-dev-server
Project is running at http://localhost:8080/
webpack output is served from /
Hash: e4e2f9508ac286037e71
Version: webpack 3.5.5
Time: 1566ms
Asset Size Chunks Chunk Names
bundle.js 1.07 MB 0 [emitted] [big] main
bundle.js.map 1.27 MB 0 [emitted] main
[115] multi (webpack)-dev-server/client?http://localhost:8080 ./main.js 40 bytes {0} [built]
[116] (webpack)-dev-server/client?http://localhost:8080 5.83 kB {0} [built]
[117] ./node_modules/url/url.js 23.3 kB {0} [built]
[120] ./node_modules/querystring-es3/index.js 127 bytes {0} [built]
[123] ./node_modules/strip-ansi/index.js 161 bytes {0} [built]
[125] ./node_modules/loglevel/lib/loglevel.js 6.74 kB {0} [built]
[126] (webpack)-dev-server/client/socket.js 856 bytes {0} [built]
[158] (webpack)-dev-server/client/overlay.js 3.6 kB {0} [built]
[159] ./node_modules/ansi-html/index.js 4.26 kB {0} [built]
[163] (webpack)/hot nonrecursive ^\.\/log$ 170 bytes {0} [built]
[165] (webpack)/hot/emitter.js 77 bytes {0} [built]
[167] ./main.js 2.28 kB {0} [built]
+ 255 hidden modules
細(xì)心的你會(huì)觀(guān)察到輸出的bundle.js中包含了以下七個(gè)模塊:
[116] (webpack)-dev-server/client?http://localhost:8080 5.83 kB {0} [built]
[117] ./node_modules/url/url.js 23.3 kB {0} [built]
[120] ./node_modules/querystring-es3/index.js 127 bytes {0} [built]
[123] ./node_modules/strip-ansi/index.js 161 bytes {0} [built]
[125] ./node_modules/loglevel/lib/loglevel.js 6.74 kB {0} [built]
[126] (webpack)-dev-server/client/socket.js 856 bytes {0} [built]
[158] (webpack)-dev-server/client/overlay.js 3.6 kB {0} [built]
這七個(gè)模塊就是代理客戶(hù)端的代碼,它們被打包進(jìn)了要開(kāi)發(fā)的網(wǎng)頁(yè)代碼中。
在瀏覽器中打開(kāi)網(wǎng)址http://localhost:8080/后,在瀏覽器的開(kāi)發(fā)者工具中你會(huì)發(fā)現(xiàn)由代理客戶(hù)端向DevServer發(fā)起的WebSocket連接:

優(yōu)化自動(dòng)刷新的性能
devServer.inline是用來(lái)控制是否往Chunk中注入代理客戶(hù)端的,默認(rèn)會(huì)注入。 事實(shí)上,在開(kāi)啟inline時(shí),DevServer會(huì)為每個(gè)輸出的Chunk中注入代理客戶(hù)端的代碼,當(dāng)你的項(xiàng)目需要輸出的Chunk有很多個(gè)時(shí),這會(huì)導(dǎo)致你的構(gòu)建緩慢。 其實(shí)要完成自動(dòng)刷新,一個(gè)頁(yè)面只需要一個(gè)代理客戶(hù)端就行了,DevServer之所以粗暴的為每個(gè)Chunk都注入,是因?yàn)樗恢滥硞€(gè)網(wǎng)頁(yè)依賴(lài)哪幾個(gè)Chunk,索性就全部都注入一個(gè)代理客戶(hù)端。 網(wǎng)頁(yè)只要依賴(lài)了其中任何一個(gè)Chunk,代理客戶(hù)端就被注入到網(wǎng)頁(yè)中去。
這里優(yōu)化的思路是關(guān)閉還不夠優(yōu)雅的inline模式,只注入一個(gè)代理客戶(hù)端。 為了關(guān)閉inline模式,在啟動(dòng)DevServer時(shí),可通過(guò)執(zhí)行命令webpack-dev-server --inline false(也可以在配置文件中設(shè)置),這時(shí)輸出的日志如下:
> webpack-dev-server --inline false
Project is running at http://localhost:8080/webpack-dev-server/
webpack output is served from /
Hash: 5a43fc44b5e85f4c2cf1
Version: webpack 3.5.5
Time: 1130ms
Asset Size Chunks Chunk Names
bundle.js 750 kB 0 [emitted] [big] main
bundle.js.map 897 kB 0 [emitted] main
[81] ./main.js 2.29 kB {0} [built]
+ 169 hidden modules
和前面的不同在于
- 入口網(wǎng)址變成了
http://localhost:8080/webpack-dev-server/ -
bundle.js中不再包含代理客戶(hù)端的代碼了
在瀏覽器中打開(kāi)網(wǎng)址http://localhost:8080/webpack-dev-server/后,你會(huì)看到如下效果:

要開(kāi)發(fā)的網(wǎng)頁(yè)被放進(jìn)了一個(gè)iframe中,編輯源碼后,iframe會(huì)被自動(dòng)刷新。 同時(shí)你會(huì)發(fā)現(xiàn)構(gòu)建時(shí)間從1566ms減少到了1130ms,說(shuō)明優(yōu)化生效了。構(gòu)建性能提升的效果在要輸出的Chunk數(shù)量越多時(shí)會(huì)顯得越突出。
如果你不想通過(guò)iframe的方式去訪(fǎng)問(wèn),但同時(shí)又想讓網(wǎng)頁(yè)保持自動(dòng)刷新功能,你需要手動(dòng)往網(wǎng)頁(yè)中注入代理客戶(hù)端腳本,往index.html中插入以下標(biāo)簽:
<!--注入 DevServer 提供的代理客戶(hù)端腳本,這個(gè)服務(wù)是 DevServer 內(nèi)置的-->
<script src="http://localhost:8080/webpack-dev-server.js"></script>
給網(wǎng)頁(yè)注入以上腳本后,獨(dú)立打開(kāi)的網(wǎng)頁(yè)就能自動(dòng)刷新了。但是要注意在發(fā)布到線(xiàn)上時(shí)記得刪除掉這段用于開(kāi)發(fā)環(huán)境的代碼。
開(kāi)啟模塊熱替換
要做到實(shí)時(shí)預(yù)覽,除了使用自動(dòng)刷新刷新整個(gè)網(wǎng)頁(yè)外,DevServer還支持一種叫做模塊熱替換(Hot Module Replacement)的技術(shù)可在不刷新整個(gè)網(wǎng)頁(yè)的情況下做到超靈敏的實(shí)時(shí)預(yù)覽。 原理是當(dāng)一個(gè)源碼發(fā)生變化時(shí),只重新編譯發(fā)生變化的模塊,再用新輸出的模塊替換掉瀏覽器中對(duì)應(yīng)的老模塊。
模塊熱替換技術(shù)的優(yōu)勢(shì)有:
- 實(shí)時(shí)預(yù)覽反應(yīng)更快,等待時(shí)間更短。
- 不刷新瀏覽器能保留當(dāng)前網(wǎng)頁(yè)的運(yùn)行狀態(tài),例如在使用Redux來(lái)管理數(shù)據(jù)的應(yīng)用中搭配模塊熱替換能做到代碼更新時(shí)Redux中的數(shù)據(jù)還保持不變。
總的來(lái)說(shuō)模塊熱替換技術(shù)很大程度上的提高了開(kāi)發(fā)效率和體驗(yàn)。
模塊熱替換的原理
模塊熱替換的原理和自動(dòng)刷新原理類(lèi)似,都需要往要開(kāi)發(fā)的網(wǎng)頁(yè)中注入一個(gè)代理客戶(hù)端用于連接DevServer和網(wǎng)頁(yè), 不同在于模塊熱替換獨(dú)特的模塊替換機(jī)制。
DevServer默認(rèn)不會(huì)開(kāi)啟模塊熱替換模式,要開(kāi)啟該模式,只需在啟動(dòng)時(shí)帶上參數(shù)--hot,完整命令是webpack-dev-server --hot。
除了通過(guò)在啟動(dòng)時(shí)帶上--hot參數(shù),還可以通過(guò)接入Plugin實(shí)現(xiàn),相關(guān)代碼如下:
const HotModuleReplacementPlugin = require('webpack/lib/HotModuleReplacementPlugin');
module.exports = {
entry:{
// 為每個(gè)入口都注入代理客戶(hù)端
main:['webpack-dev-server/client?http://localhost:8080/', 'webpack/hot/dev-server','./src/main.js'],
},
plugins: [
// 該插件的作用就是實(shí)現(xiàn)模塊熱替換,實(shí)際上當(dāng)啟動(dòng)時(shí)帶上 `--hot` 參數(shù),會(huì)注入該插件,生成 .hot-update.json 文件。
new HotModuleReplacementPlugin(),
],
devServer:{
// 告訴 DevServer 要開(kāi)啟模塊熱替換模式
hot: true,
}
};
在啟動(dòng)Webpack時(shí)帶上參數(shù)--hot其實(shí)就是自動(dòng)為你完成以上配置。啟動(dòng)后日志如下:
> webpack-dev-server --hot
Project is running at http://localhost:8080/
webpack output is served from /
webpack: wait until bundle finished: /
webpack: wait until bundle finished: /bundle.js
Hash: fe62ac6b753c1d98961b
Version: webpack 3.5.5
Time: 3563ms
Asset Size Chunks Chunk Names
bundle.js 1.11 MB 0 [emitted] [big] main
bundle.js.map 1.33 MB 0 [emitted] main
[50] (webpack)/hot/log.js 1.04 kB {0} [built]
[118] multi (webpack)-dev-server/client?http://localhost:8080 webpack/hot/dev-server ./main.js 52 bytes {0} [built]
[119] (webpack)-dev-server/client?http://localhost:8080 5.83 kB {0} [built]
[120] ./node_modules/url/url.js 23.3 kB {0} [built]
[126] ./node_modules/strip-ansi/index.js 161 bytes {0} [built]
[128] ./node_modules/loglevel/lib/loglevel.js 6.74 kB {0} [built]
[129] (webpack)-dev-server/client/socket.js 856 bytes {0} [built]
[161] (webpack)-dev-server/client/overlay.js 3.6 kB {0} [built]
[166] (webpack)/hot nonrecursive ^\.\/log$ 170 bytes {0} [built]
[168] (webpack)/hot/dev-server.js 1.61 kB {0} [built]
[169] (webpack)/hot/log-apply-result.js 1.31 kB {0} [built]
[170] ./main.js 2.35 kB {0} [built]
+ 262 hidden modules
可以看出bundle.js代理客戶(hù)端相關(guān)的代碼包含九個(gè)文件:
[119] (webpack)-dev-server/client?http://localhost:8080 5.83 kB {0} [built]
[120] ./node_modules/url/url.js 23.3 kB {0} [built]
[126] ./node_modules/strip-ansi/index.js 161 bytes {0} [built]
[128] ./node_modules/loglevel/lib/loglevel.js 6.74 kB {0} [built]
[129] (webpack)-dev-server/client/socket.js 856 bytes {0} [built]
[161] (webpack)-dev-server/client/overlay.js 3.6 kB {0} [built]
[166] (webpack)/hot nonrecursive ^\.\/log$ 170 bytes {0} [built]
[168] (webpack)/hot/dev-server.js 1.61 kB {0} [built]
[169] (webpack)/hot/log-apply-result.js 1.31 kB {0} [built]
相比于自動(dòng)刷新的代理客戶(hù)端,多出了后三個(gè)用于模塊熱替換的文件,也就是說(shuō)代理客戶(hù)端更大了。
修改源碼main.css文件后,新輸出了如下日志:
webpack: Compiling...
Hash: 18f81c959118f6230623
Version: webpack 3.5.5
Time: 551ms
Asset Size Chunks Chunk Names
bundle.js 1.11 MB 0 [emitted] [big] main
0.ea11a51f97f2b52bca7d.hot-update.js 353 bytes 0 [emitted] main
ea11a51f97f2b52bca7d.hot-update.json 43 bytes [emitted]
bundle.js.map 1.33 MB 0 [emitted] main
0.ea11a51f97f2b52bca7d.hot-update.js.map 577 bytes 0 [emitted] main
[68] ./node_modules/css-loader!./main.css 217 bytes {0} [built]
[166] (webpack)/hot nonrecursive ^\.\/log$ 170 bytes {0} [built]
+ 275 hidden modules
webpack: Compiled successfully.
DevServer新生成了一個(gè)用于替換老模塊的補(bǔ)丁文件0.ea11a51f97f2b52bca7d.hot-update.js,同時(shí)在瀏覽器開(kāi)發(fā)工具中也能看到請(qǐng)求這個(gè)補(bǔ)丁的抓包:

可見(jiàn)補(bǔ)丁中包含了main.css文件新編譯出來(lái)CSS代碼,網(wǎng)頁(yè)中的樣式也立刻變成了源碼中描述的那樣。
但當(dāng)你修改main.js文件時(shí),會(huì)發(fā)現(xiàn)模塊熱替換沒(méi)有生效,而是整個(gè)頁(yè)面被刷新了,為什么修改main.js文件時(shí)會(huì)這樣呢?
Webpack為了讓使用者在使用了模塊熱替換功能時(shí)能靈活地控制老模塊被替換時(shí)的邏輯,可以在源碼中定義一些代碼去做相應(yīng)的處理。
把main.js文件改為如下:
import React from 'react';
import { render } from 'react-dom';
import { AppComponent } from './AppComponent';
import './main.css';
render(<AppComponent/>, window.document.getElementById('app'));
// 只有當(dāng)開(kāi)啟了模塊熱替換時(shí) module.hot 才存在
if (module.hot) {
// accept 函數(shù)的第一個(gè)參數(shù)指出當(dāng)前文件接受哪些子模塊的替換,這里表示只接受 ./AppComponent 這個(gè)子模塊
// 第2個(gè)參數(shù)用于在新的子模塊加載完畢后需要執(zhí)行的邏輯
module.hot.accept(['./AppComponent'], () => {
// 新的 AppComponent 加載成功后重新執(zhí)行下組建渲染邏輯
render(<AppComponent/>, window.document.getElementById('app'));
});
}
其中的module.hot是當(dāng)開(kāi)啟模塊熱替換后注入到全局的API,用于控制模塊熱替換的邏輯。
現(xiàn)在修改AppComponent.js文件,把Hello,Webpack改成Hello,World,你會(huì)發(fā)現(xiàn)模塊熱替換生效了。 但是當(dāng)你編輯main.js時(shí),你會(huì)發(fā)現(xiàn)整個(gè)網(wǎng)頁(yè)被刷新了。為什么修改這兩個(gè)文件會(huì)有不一樣的表現(xiàn)呢?
當(dāng)子模塊發(fā)生更新時(shí),更新事件會(huì)一層層往上傳遞,也就是從AppComponent.js文件傳遞到main.js文件, 直到有某層的文件接受了當(dāng)前變化的模塊,也就是main.js文件中定義的module.hot.accept(['./AppComponent'], callback),這時(shí)就會(huì)調(diào)用callback函數(shù)去執(zhí)行自定義邏輯。如果事件一直往上拋到最外層都沒(méi)有文件接受它,就會(huì)直接刷新網(wǎng)頁(yè)。
那為什么沒(méi)有地方接受過(guò).css文件,但是修改所有的.css文件都會(huì)觸發(fā)模塊熱替換呢?原因在于style-loader會(huì)注入用于接受CSS的代碼。
請(qǐng)不要把模塊熱替換技術(shù)用于線(xiàn)上環(huán)境,它是專(zhuān)門(mén)為提升開(kāi)發(fā)效率生的。
優(yōu)化模塊熱替換
在發(fā)生模塊熱替換時(shí),你會(huì)在瀏覽器的控制臺(tái)中看到類(lèi)似這樣的日志:

其中的Updated modules: 68是指ID為68的模塊被替換了,這對(duì)開(kāi)發(fā)者來(lái)說(shuō)很不友好,因?yàn)殚_(kāi)發(fā)者不知道ID和模塊之間的對(duì)應(yīng)關(guān)系,最好是把替換了的模塊的名稱(chēng)輸出出來(lái)。Webpack內(nèi)置的NamedModulesPlugin插件可以解決該問(wèn)題,修改Webpack配置文件接入該插件:
const NamedModulesPlugin = require('webpack/lib/NamedModulesPlugin');
module.exports = {
plugins: [
// 顯示出被替換模塊的名稱(chēng)
new NamedModulesPlugin(),
],
};
重啟構(gòu)建后你會(huì)發(fā)現(xiàn)瀏覽器中的日志更加友好了:

除此之外,模塊熱替換還面臨著和自動(dòng)刷新一樣的性能問(wèn)題,因?yàn)樗鼈兌夹枰O(jiān)聽(tīng)文件變化和注入客戶(hù)端。 要優(yōu)化模塊熱替換的構(gòu)建性能,思路是:監(jiān)聽(tīng)更少的文件,忽略掉node_modules目錄下的文件。 但是其中提到的關(guān)閉默認(rèn)的inline模式手動(dòng)注入代理客戶(hù)端的優(yōu)化方法不能用于在使用模塊熱替換的情況下, 原因在于模塊熱替換的運(yùn)行依賴(lài)在每個(gè)Chunk中都包含代理客戶(hù)端的代碼。