開始一個(gè)React項(xiàng)目(二) 徹底弄懂webpack-dev-server的熱更新

前言

webpack-dev-server配置熱更新看起來很簡(jiǎn)單,但是實(shí)際上是有很多坑的,目前為止我沒有搜到一篇深入講解這個(gè)的,如果你覺得它很簡(jiǎn)單,那么或許等你看完這篇文章你會(huì)有不一樣的看法。
由于HMR非常強(qiáng)大,本來這篇文章我是準(zhǔn)備總結(jié)webpack-dev-server的,最后基本只總結(jié)了它的兩個(gè)參數(shù):inlinehot,其它的配置我會(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)刷新方式:

  1. Iframe mode
  2. Inline mode

使用iframe模式不需要配置任何東西,只需要在你啟動(dòng)的項(xiàng)目的端口號(hào)后面加上/webpack-dev-server/即可,比如:
http://localhost:8080/webpack-dev-server/

image.png

打開調(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.jsdevServer里面,或者直接寫在命令行里面,具體寫法參考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.publicPathoutput.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ò)了:

image.png

解決方法一:手動(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è):


滾屏.gif

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

熱更新.gif

注意看當(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è)文件不能被熱更新哦。

image.png

如果你覺得可以接受每次修改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):


image.png

很棒,它終于起作用了。

你以為的結(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,看看熱更新的效果:

react熱更新.gif

它沒有保存上一次的狀態(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上的教程走就行。

  1. 下載:npm install --save react-hot-loader
  2. 接著,添加babel配置:
{
    test: /\.js$/,
    loader: 'babel-loader',
    query: {
        presets: ['env', 'react'],
        plugins: ["react-hot-loader/babel"] //增加
    }
}
  1. entry參數(shù):
entry: [
    'react-hot-loader/patch', //添加
    path.resolve(__dirname, './src/index.js')
],
  1. 修改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)在可以見證奇跡啦:


react熱更新1.gif

小結(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配置 。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 構(gòu)建一個(gè)小項(xiàng)目——FlyBird,學(xué)習(xí)webpack和react。(本文成文于2017/2/25) 從webpac...
    布蕾布蕾閱讀 17,126評(píng)論 31 98
  • 無意中看到zhangwnag大佬分享的webpack教程感覺受益匪淺,特此分享以備自己日后查看,也希望更多的人看到...
    小小字符閱讀 8,367評(píng)論 7 35
  • webpack 介紹 webpack 是什么 為什么引入新的打包工具 webpack 核心思想 webpack 安...
    yxsGert閱讀 6,664評(píng)論 2 71
  • 在現(xiàn)在的前端開發(fā)中,前后端分離、模塊化開發(fā)、版本控制、文件合并與壓縮、mock數(shù)據(jù)等等一些原本后端的思想開始...
    Charlot閱讀 5,659評(píng)論 1 32
  • 文丨紅瑀 【系列連載】生活在臺(tái)灣(目錄) 2015年開始,隨著孩子逐漸長(zhǎng)大獨(dú)立,我開始頻繁往返臺(tái)港深三地,一方面想...
    紅瑀閱讀 1,758評(píng)論 17 19

友情鏈接更多精彩內(nèi)容