前言
webpack-dev-server配置熱更新看起來很簡(jiǎn)單,但是實(shí)際上是有很多坑的,目前為止我沒有搜到一篇深入講解這個(gè)的,如果你覺得它很簡(jiǎn)單,那么或許等你看完這篇文章你會(huì)有不一樣的看法。
由于HMR非常強(qiáng)大,本來這篇文章我是準(zhǔn)備總結(jié)webpack-dev-server的,最后基本只總結(jié)了它的兩個(gè)參數(shù):inline和hot,其它的配置我會(huì)另外再寫一篇文章講解。
模塊熱替換(Hot Module Replacement)
HMR是webpack最令人興奮的特性之一,當(dāng)你對(duì)代碼進(jìn)行修改并保存后,webpack 將對(duì)代碼重新打包,并將新的模塊發(fā)送到瀏覽器端,瀏覽器通過新的模塊替換老的模塊,這樣在不刷新瀏覽器的前提下就能夠?qū)?yīng)用進(jìn)行更新。HMR是一個(gè)非常值得去深入研究的東西,它絕不是目前我們看到的大多數(shù)技術(shù)文章說的配置一個(gè)hot參數(shù)這么簡(jiǎn)單,有興趣的小伙伴可以去看看它的實(shí)現(xiàn)原理,目前為止我也只看過一點(diǎn)點(diǎn)。
其實(shí)實(shí)現(xiàn)HMR的插件有很多,webpack-dev-server只是其中的一個(gè),當(dāng)然也是優(yōu)秀的一個(gè),它能很好的與webpack配合。另外,webpack-dev-server只是用于開發(fā)環(huán)境的。
webpack-dev-server實(shí)現(xiàn)自動(dòng)刷新
全局安裝:npm install webpack-dev-server --g (全局安裝以后才可以直接在命令行使用webpack-dev-server)
本地安裝:npm install webpack-dev-server --save-dev
在webpack的配置文件里添加webpack-dev-server的配置:
module.exports = {
devServer: {
contentBase: path.resolve(__dirname, 'build'),
},
}
webpack-dev-server為了加快打包進(jìn)程是將打包后的文件放到內(nèi)存中的,所以我們?cè)陧?xiàng)目中是看不到它打包以后生成的文件/文件夾的,但是,這不代表我們就不用配置路徑了,配置過webpack.config.js的小伙伴都知道output.path這個(gè)參數(shù)是配置打包文件的保存路徑的,contentBase就和output.path是一樣的作用,如果不配置這個(gè)參數(shù)就會(huì)打包到項(xiàng)目的根路徑下。有關(guān)這幾個(gè)配置路徑的參數(shù)我會(huì)再寫一篇文章總結(jié),這里就不展開了。
當(dāng)然你也可以選擇在命令行中啟動(dòng)的時(shí)候加這個(gè)參數(shù):
webpack-dev-server --content-base build/
webpack-dev-server支持兩種自動(dòng)刷新方式:
- Iframe mode
- Inline mode
使用iframe模式不需要配置任何東西,只需要在你啟動(dòng)的項(xiàng)目的端口號(hào)后面加上/webpack-dev-server/即可,比如:
http://localhost:8080/webpack-dev-server/

打開調(diào)試器可以看到webpack-dev-server在頁面中嵌入了一個(gè)<iframe>標(biāo)簽來實(shí)現(xiàn)熱更新,具體原理我還沒去研究,有興趣的小伙伴可以自行搜索。此時(shí)試著更改src/index.js發(fā)現(xiàn)頁面已經(jīng)可以自動(dòng)刷新了。
inline模式實(shí)在是個(gè)磨人的小妖精,官方文檔有關(guān)Inline mode的使用說明比較少,而且還極容易誤導(dǎo)人,再加上網(wǎng)上很多自己都沒搞清楚webpack-dev-server的博主的文章,就更容易讓人懵逼了。
誤導(dǎo)一:inline模式的HTML方式和Node.js方式都需要配置參數(shù)inline才能生效。
文檔把HTML方式和Node.js方式都稱為inline模式,以至于很多人都誤解了這兩種用法,但是文檔里有這么一句話:
Inline mode with Node.js API
There is no inline: true flag in the webpack-dev-server configuration, because the webpack-dev-server module has no access to the webpack configuration.
意思是使用Node.js方式是沒有inline這個(gè)參數(shù)的,這里的inline模式其實(shí)就是三種配置方式,三選一就行。
- 在webpack.config.js里面配置
module.exports = {
...
devServer: {
inline: true,
},
}
- 在HTML里面添加
<script src="http://localhost:8080/webpack-dev-server.js"></script> - 在node.js的配置文件里面配置(以下摘自官網(wǎng),后面我會(huì)詳解這個(gè)配置)
var config = require("./webpack.config.js");
config.entry.app.unshift("webpack-dev-server/client?http://localhost:8080/");
var compiler = webpack(config);
var server = new WebpackDevServer(compiler, {...});
server.listen(8080);
誤導(dǎo)二:需要在entry屬性里添加webpack-dev-server/client?http://?path?:?port?/
這個(gè)誤解應(yīng)該來自于別的博客,我搜了很多文章都在entry里加了這句話,如果是開啟熱更新還會(huì)加webpack/hot/dev-server。這一點(diǎn)官網(wǎng)解釋的非常清楚,由于采用Node.js配置,webpack-dev-server模塊無法讀取webpack的配置,所以用戶必須手動(dòng)去webpack.config.js的entry指定webpack-dev-server客戶端入口。意思是只有采用Node.js方式才會(huì)需要添加這句話,而且,我們并不需要去污染webpack.config.js文件,而是將這句代碼寫在Node.js 的配置文件里:
config.entry.app.unshift("webpack-dev-server/client?http://localhost:8080/");
config.entry就是webpack.config.js的entry, entry是一個(gè)數(shù)組,這里要注意一下你自己的entry配置,如果是
entry: [
path.resolve(__dirname, './src/index.js')
],
那你應(yīng)該寫成:
config.entry.unshift("webpack-dev-server/client?http://localhost:8080/");
還懵逼嗎?那我再多說兩句
以上這些亂七八糟的配置估計(jì)把你都看暈了吧,我再梳理一下有關(guān)inline模式的東西,HTML方式最簡(jiǎn)單,在index.html頁面里添加一個(gè)<script>標(biāo)簽就行了,如果不想用Node.js配置,直接用webpack-dev-server,那么配置參數(shù)可以寫在webpack.config.js的devServer里面,或者直接寫在命令行里面,具體寫法參考https://webpack.js.org/configuration/dev-server/,它會(huì)注明哪些參數(shù)是只能用于CLI(命令行)的。此時(shí)啟動(dòng)項(xiàng)目:
"scripts": {
"start": "webpack-dev-server 你的啟動(dòng)參數(shù)可以寫在這里也可以寫在devServer里"
},
如果使用Node.js方式,那么即使你配置了devServer也會(huì)被忽略,真正起作用的應(yīng)該是Node.js的server.js文件,這個(gè)文件作為配置文件放在根目錄下。
此時(shí)啟動(dòng)項(xiàng)目:
"scripts": {
"start": "node server.js"
},
webpack-dev-server實(shí)現(xiàn)模塊熱替換(HMR)
注:以下配置都是針對(duì)inline模式,官方的意思好像是只有inline模式支持模塊熱替換
HMR可以做到在不刷新瀏覽器的前提下刷新頁面,HMR的好處是:
- 保持刷新前的應(yīng)用狀態(tài)(這一點(diǎn)在react里是做不到的,具體原因看下面)
- 不浪費(fèi)時(shí)間在等待不必要更新的組件被更新上面
- 調(diào)整CSS樣式的速度更快
HMR配置有兩種方式:Node方式和非Node方式。
非Node方式
非Node方式有關(guān)webpack-dev-server的配置都在webpack.config.js的devServer參數(shù)里,首先開啟HMR,添加配置參數(shù)hot: true,并且一定要指定output.publicPath,如果不指定會(huì)導(dǎo)致HMR無法工作,建議devServer.publicPath和output.publicPath一樣。
webpack.config.js
const publicPath = '/';
const buildPath = 'build';
module.exports = {
//...
output: {
path: path.resolve(__dirname, buildPath),
filename: 'bundle.js',
publicPath: publicPath, //添加
},
devServer: {
publicPath: publicPath,
contentBase: path.resolve(__dirname, buildPath),
publicPath: publicPath, //添加
inline: true, //添加
hot: true,
},
}
這里又有一個(gè)坑,估計(jì)也有小伙伴看到過有的文章說還需要添加HotModuleReplacementPlugin到plugins里面,而官網(wǎng)很清楚的說了,當(dāng)我們添加了hot: true以后,它會(huì)自動(dòng)幫我們加這個(gè)插件的,但是??!報(bào)錯(cuò)了:

解決方法一:手動(dòng)添加到plugins里面:
module.exports = {
plugins: [
new webpack.HotModuleReplacementPlugin(), //添加
new webpack.NamedModulesPlugin(), //添加,官方推薦的幫助分析依賴的插件
],
}
解決方法二:在命令行里再添加--hot參數(shù):
"start": "webpack-dev-server --hot"
這是我在另一篇博客里面看到的,我一直以為命令行和devServer里面配置二選一就好了,結(jié)果!!是我太年輕啊Q。
命令行還有一個(gè)比較好用的參數(shù)--open可以自動(dòng)打開瀏覽器,這個(gè)參數(shù)也只限于命令行使用。
"start": "webpack-dev-server --hot --open"
Node方式
分三步走:
- webpack的entry添加:
webpack/hot/dev-server - webpack的plugins添加
new webpack.HotModuleReplacementPlugin() - webpack-dev-server添加
hot: true
這里我再說明一下,采用Node方式做不到自動(dòng)將webpack/hot/dev-server添加到entry里面,這和前面的自動(dòng)刷新是一樣的。然后?。∈褂肗ode方式啟動(dòng)也不能在命令行里面添加啟動(dòng)參數(shù)了,所以我們需要手動(dòng)添加HotModuleReplacementPlugin,還有,--open自然也沒法用了,這時(shí)候要自動(dòng)打開瀏覽器估計(jì)會(huì)麻煩一點(diǎn),有興趣的小伙伴可以去研究一下create-react-app是怎么配置這個(gè)的。
server.js
config.entry.unshift("webpack-dev-server/client?http://localhost:8080/", 'webpack/hot/dev-server');
let server = new WebpackDevServer(compiler, {
contentBase: config.output.path,
publicPath: config.output.publicPath,
hot: true
...
});
注:我不太清楚這里是否必須要配置publicPath,經(jīng)測(cè)試不配置也是可以的。
webpack.config.js
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html'
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(),
],
好的,選擇一個(gè)你喜歡的方式啟動(dòng)起來吧,如果能在控制臺(tái)看到以下的信息,代表熱更新啟動(dòng)起來了:
[HMR] Waiting for update signal from WDS...
[WDS] Hot Module Replacement enabled.
HMR真的開始發(fā)揮作用了嗎?
你大概要生氣了,我做了這么多事情就配置了hot和inline兩個(gè)參數(shù),現(xiàn)在你告訴我我的熱更新還不可用?我不要面子的嗎?
其實(shí)我也很煩,盡管官網(wǎng)看起來很簡(jiǎn)單,但我卻花了很長(zhǎng)時(shí)間來弄這個(gè)。我也以為我弄好了,直到我看到了這個(gè):

我修改了src/index.js文件并保存,注意看右邊調(diào)試器的變化,它打印了[WDS] App updated.Recompiling等信息,然后瀏覽器刷新,左邊界面更新。
這,不是HMR的功勞。我們不配置HMR,只配置自動(dòng)刷新就是這種效果。
再看一個(gè)真正的熱更新:

注意看當(dāng)我代碼修改的時(shí)候,頁面并沒有刷新,并且左邊日志能看到HMR開始工作打印的日志。
而出現(xiàn)這兩種情況的原因是:前一個(gè)是修改的js,后一個(gè)是修改的css。
來自于devServer官方的解釋是(找了半天也沒找到)借助于style-loaderCSS很容易實(shí)現(xiàn)HMR,而對(duì)于js,devServer會(huì)嘗試做HMR,如果不行就觸發(fā)整個(gè)頁面刷新。你問我什么時(shí)候js更改才會(huì)只觸發(fā)HMR,那你可以試著再加一個(gè)參數(shù)hotOnly: true試一試,這時(shí)候相當(dāng)于禁用了自動(dòng)刷新功能,然而devServer會(huì)告訴你這個(gè)文件不能被熱更新哦。

如果你覺得可以接受每次修改js都重刷頁面,那么到這里就可以了。如果你還想繼續(xù)追究下去,那么繼續(xù)吧。
如果已經(jīng)通過
HotModuleReplacementPlugin啟用了模塊熱替換(Hot Module Replacement),則它的接口將被暴露在module.hot屬性下面。通常,用戶先要檢查這個(gè)接口是否可訪問,然后再開始使用它。
——引自webpack官網(wǎng)
其實(shí)很簡(jiǎn)單,我們把整個(gè)項(xiàng)目的要被webpack編譯的文件都設(shè)置為接受熱更新,而最簡(jiǎn)單的方式就是在入口文件的地方添加:
src/index.js
if (module.hot) {
module.hot.accept(() => {
ReactDom.render(
<App />,
document.getElementById('root')
)
})
}
ReactDom.render(
<App />,
document.getElementById('root')
)
嘗試修改js文件,可以看到控制臺(tái):

很棒,它終于起作用了。
你以為的結(jié)局其實(shí)并不是結(jié)局。
OK,到這里我是不是該寫點(diǎn)總結(jié)然后愉快的結(jié)束這篇文章了?嗯。。我只能說不能高興的太早。
還有什么問題沒有解決?讓我們?cè)倏磦€(gè)經(jīng)典的計(jì)時(shí)器栗子
constructor(props) {
super(props);
this.state = {
count: 0
}
}
add() {
this.setState((preState) => {
return{
count: preState.count + 1
}
})
}
sub() {
this.setState((preState) => {
return{
count: preState.count - 1
}
})
}
render() {
return(
<div className="container">
<h1>{this.state.count}</h1>
<button onClick={() => this.add()}>count+1</button>
<br/>
<button onClick={() => this.sub()}>count-1</button>
<h1>Hello, React</h1>
</div>
)
}
現(xiàn)在讓我到頁面里面執(zhí)行幾次加減,只要讓count不停在初始值就好,然后修改js,看看熱更新的效果:

它沒有保存上一次的狀態(tài),而是回到了初始狀態(tài)0。如果希望熱更新還可以保留上一次的狀態(tài),我們需要另一個(gè)插件:react-hot-loader
可以保存狀態(tài)的熱更新插件——react-hot-loader
webpack-dev-server的熱更新對(duì)于保存react狀態(tài)是無法做到的,所以才有了react-hot-loader這個(gè)東西,這個(gè)不是必須配置的插件,至少我沒在create-react-app里面看到它。不過如果你想要更新時(shí)可以保存state,這是必須的。
讓我們接著配置它吧,照著github上的教程走就行。
- 下載:
npm install --save react-hot-loader - 接著,添加babel配置:
{
test: /\.js$/,
loader: 'babel-loader',
query: {
presets: ['env', 'react'],
plugins: ["react-hot-loader/babel"] //增加
}
}
- entry參數(shù):
entry: [
'react-hot-loader/patch', //添加
path.resolve(__dirname, './src/index.js')
],
- 修改index.js
import React, { Component } from 'react';
import ReactDom from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import Home from './pages/Home';
if (module.hot) {
module.hot.accept(() => {
ReactDom.render(
<AppContainer>
<Home />
</AppContainer>,
document.getElementById('root')
)
})
}
ReactDom.render(
<AppContainer>
<Home />
</AppContainer>,
document.getElementById('root')
)
這里要注意一下,index.js里面不能直接render一個(gè)組件然后讓它包裹在<AppContainer>里面,只能單獨(dú)抽離組件,否則會(huì)報(bào)錯(cuò)。
現(xiàn)在可以見證奇跡啦:

小結(jié)
這篇文章花了我一周多的時(shí)間,最后總算弄清楚了熱更新到底是怎么回事,百度一搜全都是你只要配置一個(gè)hot: true就好啦,然后都沒弄明白這到底是熱更新還是自動(dòng)刷新,可供參考的文檔只有官網(wǎng),官網(wǎng)又講的太簡(jiǎn)單,所以折騰了特別久??床欢男』锇榭梢越o我留言。
我把項(xiàng)目放在github上了,使用Node方式和非Node方式時(shí)如何配置參數(shù)都放上去了,你配置時(shí)遇到問題了可以到這里看一下:https://github.com/dengshasha/react-webpack
還有,如果還沒有開始webpack配置的話可以看看我的另一篇文章開始一個(gè)React項(xiàng)目(一)一個(gè)最簡(jiǎn)單的webpack配置 。