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

前言

webpack-dev-server配置熱更新看起來很簡單,但是實際上是有很多坑的,目前爲止我沒有搜到一篇深入講解這個的,如果你覺得它很簡單,那麼或許等你看完這篇文章你會有不一樣的看法。
由於HMR非常強大,本來這篇文章我是準備總結webpack-dev-server的,最後基本只總結了它的兩個參數:inlinehot,其它的配置我會另外再寫一篇文章講解。

模塊熱替換(Hot Module Replacement)

HMR是webpack最令人興奮的特性之一,當你對代碼進行修改並保存後,webpack 將對代碼重新打包,並將新的模塊發送到瀏覽器端,瀏覽器通過新的模塊替換老的模塊,這樣在不刷新瀏覽器的前提下就能夠對應用進行更新。HMR是一個非常值得去深入研究的東西,它絕不是目前我們看到的大多數技術文章說的配置一個hot參數這麼簡單,有興趣的小夥伴可以去看看它的實現原理,目前爲止我也只看過一點點。

其實實現HMR的插件有很多,webpack-dev-server只是其中的一個,當然也是優秀的一個,它能很好的與webpack配合。另外,webpack-dev-server只是用於開發環境的。

webpack-dev-server實現自動刷新

全局安裝: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爲了加快打包進程是將打包後的文件放到內存中的,所以我們在項目中是看不到它打包以後生成的文件/文件夾的,但是,這不代表我們就不用配置路徑了,配置過webpack.config.js的小夥伴都知道output.path這個參數是配置打包文件的保存路徑的,contentBase就和output.path是一樣的作用,如果不配置這個參數就會打包到項目的根路徑下。有關這幾個配置路徑的參數我會再寫一篇文章總結,這裏就不展開了。
當然你也可以選擇在命令行中啓動的時候加這個參數:

webpack-dev-server --content-base build/

webpack-dev-server支持兩種自動刷新方式:

  1. Iframe mode
  2. Inline mode

使用iframe模式不需要配置任何東西,只需要在你啓動的項目的端口號後面加上/webpack-dev-server/即可,比如:
http://localhost:8080/webpack-dev-server/
image.png

打開調試器可以看到webpack-dev-server在頁面中嵌入了一個

inline模式實在是個磨人的小妖精,官方文檔有關Inline mode的使用說明比較少,而且還極容易誤導人,再加上網上很多自己都沒搞清楚webpack-dev-server的博主的文章,就更容易讓人懵逼了。

誤導一:inline模式的HTML方式和Node.js方式都需要配置參數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這個參數的,這裏的inline模式其實就是三種配置方式,三選一就行。
- 在webpack.config.js裏面配置

module.exports = {
  ...
  devServer: {
    inline: true,
  },
}
  • 在HTML裏面添加<script src="http://localhost:8080/webpack-dev-server.js"></script>
  • 在node.js的配置文件裏面配置(以下摘自官網,後面我會詳解這個配置)
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);

誤導二:需要在entry屬性裏添加webpack-dev-server/client?http://«path»:«port»/

這個誤解應該來自於別的博客,我搜了很多文章都在entry里加了這句話,如果是開啓熱更新還會加webpack/hot/dev-server。這一點官網解釋的非常清楚,由於採用Node.js配置,webpack-dev-server模塊無法讀取webpack的配置,所以用戶必須手動去webpack.config.js的entry指定webpack-dev-server客戶端入口。意思是隻有采用Node.js方式纔會需要添加這句話,而且,我們並不需要去污染webpack.config.js文件,而是將這句代碼寫在Node.js 的配置文件裏:

config.entry.app.unshift("webpack-dev-server/client?http://localhost:8080/");

config.entry就是webpack.config.js的entry, entry是一個數組,這裏要注意一下你自己的entry配置,如果是

entry: [
    path.resolve(__dirname, './src/index.js')
],

那你應該寫成:
config.entry.unshift("webpack-dev-server/client?http://localhost:8080/");

還懵逼嗎?那我再多說兩句

以上這些亂七八糟的配置估計把你都看暈了吧,我再梳理一下有關inline模式的東西,HTML方式最簡單,在index.html頁面裏添加一個

"scripts": {
    "start": "webpack-dev-server 你的啓動參數可以寫在這裏也可以寫在devServer裏"
  },

如果使用Node.js方式,那麼即使你配置了devServer也會被忽略,真正起作用的應該是Node.js的server.js文件,這個文件作爲配置文件放在根目錄下。
此時啓動項目:

"scripts": {
    "start": "node server.js"
  },

webpack-dev-server實現熱更新(HMR)

注:以下配置都是針對inline模式,官方也只提了inline模式的配置方式。

熱更新可以做到在不刷新瀏覽器的前提下刷新頁面,熱更新的好處是:
- 保持刷新前的應用狀態(這一點在react裏是做不到的,具體原因看下面)
- 不浪費時間在等待不必要更新的組件被更新上面
- 調整CSS樣式的速度更快

採用非Node模式,添加hot: true,並且一定要指定output.publicPath,建議devServer.publicPathoutput.publicPath一樣。

webpack.config.js

const publicPath = '/';
const buildPath = 'build';

output: {
        path: path.resolve(__dirname, buildPath), //打包文件的輸出路徑
        filename: 'bundle.js', //打包文件名
        publicPath: publicPath, //重要!
    },
    devtool: 'inline-source-map',
    devServer: {
        publicPath: publicPath,
        contentBase: path.resolve(__dirname, buildPath),
        inline: true,
        hot: true,  
    },

這裏有一個坑,官網說這樣配置以後它會自動添加HotModuleReplacementPlugin插件到配置文件裏,但是我卻發現報錯了:
image.png
一開始我是手動在plugins裏面添加new webpack.HotModuleReplacementPlugin(),(配置與使用Node方式一樣),這樣就可以正常啓動起來了,後來我無意間看到了一篇博客,說的是除了在devServer裏面寫,還要在啓動參數裏面加--hot

    "start": "webpack-dev-server --hot --open"

這樣webpack才能幫我們把HotModuleReplacementPlugin自動添加進來而不用我們再手動添加,--open也是一個比較好用的參數,可以幫我們自動打開瀏覽器窗口,這個參數如果寫在devServer也是沒用的。
我以前一直以爲寫在命令行裏面和寫在devServer是沒差的,現在看來是我太年輕了啊Q。

採用Node模式分三步走:
- webpack的entry添加:webpack/hot/dev-server
- webpack的plugins添加new webpack.HotModuleReplacementPlugin()
- webpack-dev-server添加hot: true

這裏我再說明一下,採用Node方式做不到自動將webpack/hot/dev-server添加到entry裏面,這和前面的自動刷新是一樣的。然後!!使用Node方式啓動也不能在命令行裏面添加啓動參數了,所以我們需要手動添加HotModuleReplacementPlugin,還有,--open自然也沒法用了,這時候要自動打開瀏覽器估計會麻煩一點,有興趣的小夥伴可以去研究一下create-react-app是怎麼配置這個的。

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,經測試不配置也是可以的。

webpack.config.js

plugins: [
        new HtmlWebpackPlugin({
            template: './public/index.html'
        }),
        new webpack.HotModuleReplacementPlugin(),
],

好的,選擇一個你喜歡的方式啓動起來吧,如果能在控制檯看到以下的信息,代表熱更新啓動起來了:

[HMR] Waiting for update signal from WDS...
[WDS] Hot Module Replacement enabled.

最後根據Hot Module Replacement的指示再添加一個NamedModulesPlugin,它的作用大概是更容易分析依賴:

plugins: [
        new webpack.HotModuleReplacementPlugin(),
        new webpack.NamedModulesPlugin(),
    ],

HMR真的開始發揮作用了嗎?

你大概要生氣了,我做了這麼多事情就配置了hot和inline兩個參數,現在你告訴我我的熱更新還不可用?我不要面子的嗎?
其實我也很煩,儘管官網看起來很簡單,但我卻花了很長時間來弄這個。我也以爲我弄好了,直到我看到了這個:
滾屏.gif

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

熱更新.gif

注意看當我代碼修改的時候,頁面並沒有刷新,並且左邊日誌能看到HMR開始工作打印的日誌。
而出現這兩種情況的原因是:前一個是修改的js,後一個是修改的css。

來自於devServer官方的解釋是(找了半天也沒找到)藉助於style-loaderCSS很容易實現HMR,而對於js,devServer會嘗試做HMR,如果不行就觸發整個頁面刷新。你問我什麼時候js更改纔會只觸發HMR,那你可以試着再加一個參數hotOnly: true試一試,這時候相當於禁用了自動刷新功能,然而devServer會告訴你這個文件不能被熱更新哦。
image.png

如果你覺得可以接受每次修改js都重刷頁面,那麼到這裏就可以了。如果你還想繼續追究下去,那麼繼續吧。

如果已經通過 HotModuleReplacementPlugin 啓用了模塊熱替換(Hot Module Replacement),則它的接口將被暴露在module.hot屬性下面。通常,用戶先要檢查這個接口是否可訪問,然後再開始使用它。
——引自webpack官網

其實很簡單,我們把整個項目的要被webpack編譯的文件都設置爲接受熱更新,而最簡單的方式就是在入口文件的地方添加:
src/index.js

if (module.hot) {
  module.hot.accept(() => {
    ReactDom.render(
        <App />,
        document.getElementById('root')
    )
  })
}

ReactDom.render(
    <App />,
    document.getElementById('root')
)

嘗試修改js文件,可以看到控制檯:
image.png
很棒,它終於起作用了。

你以爲的結局其實並不是結局。
OK,到這裏我是不是該寫點總結然後愉快的結束這篇文章了?嗯。。我只能說不能高興的太早。
還有什麼問題沒有解決?讓我們再看個經典的計時器栗子

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>  
        ) 
    }

現在讓我到頁面裏面執行幾次加減,只要讓count不停在初始值就好,然後修改js,看看熱更新的效果:

react熱更新.gif
它沒有保存上一次的狀態,而是回到了初始狀態0。如果希望熱更新還可以保留上一次的狀態,我們需要另一個插件:react-hot-loader

可以保存狀態的熱更新插件——react-hot-loader

webpack-dev-server的熱更新對於保存react狀態是無法做到的,所以纔有了react-hot-loader這個東西,這個不是必須配置的插件,至少我沒在create-react-app裏面看到它。不過如果你想要更新時可以保存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參數:
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一個組件然後讓它包裹在裏面,只能單獨抽離組件,否則會報錯。
現在可以見證奇蹟啦:
react熱更新1.gif

小結

這篇文章花了我一週多的時間,最後總算弄清楚了熱更新到底是怎麼回事,百度一搜全都是你只要配置一個hot: true就好啦,然後都沒弄明白這到底是熱更新還是自動刷新,可供參考的文檔只有官網,官網又講的太簡單,所以折騰了特別久。看不懂的小夥伴可以給我留言,或者我哪裏講的不對的都可以提出來。
我把項目放在github上了,使用Node方式和非Node方式時如何配置參數都放上去了,你配置時遇到問題了可以到這裏看一下:https://github.com/dengshasha/react-webpack
還有,如果還沒有開始webpack配置的話可以看看我的另一篇文章開始一個React項目(一)一個最簡單的webpack配置

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章