开始一个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在页面中嵌入了一个<iframe>标签来实现热更新,具体原理我还没去研究,有兴趣的小伙伴可以自行搜索。此时试着更改src/index.js发现页面已经可以自动刷新了。

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页面里添加一个<script>标签就行了,如果不想用Node.js配置,直接用webpack-dev-server,那么配置参数可以写在webpack.config.jsdevServer里面,或者直接写在命令行里面,具体写法参考https://webpack.js.org/configuration/dev-server/,它会注明哪些参数是只能用于CLI(命令行)的。此时启动项目:

"scripts": {
    "start": "webpack-dev-server 你的启动参数可以写在这里也可以写在devServer里"
  },

如果使用Node.js方式,那么即使你配置了devServer也会被忽略,真正起作用的应该是Node.js的server.js文件,这个文件作为配置文件放在根目录下。
此时启动项目:

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

webpack-dev-server实现模块热替换(HMR)

注:以下配置都是针对inline模式,官方的意思好像是只有inline模式支持模块热替换

HMR可以做到在不刷新浏览器的前提下刷新页面,HMR的好处是:

  • 保持刷新前的应用状态(这一点在react里是做不到的,具体原因看下面)
  • 不浪费时间在等待不必要更新的组件被更新上面
  • 调整CSS样式的速度更快

HMR配置有两种方式:Node方式和非Node方式。

非Node方式

非Node方式有关webpack-dev-server的配置都在webpack.config.js的devServer参数里,首先开启HMR,添加配置参数hot: true,并且一定要指定output.publicPath,如果不指定会导致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,  
    },
}

这里又有一个坑,估计也有小伙伴看到过有的文章说还需要添加HotModuleReplacementPlugin到plugins里面,而官网很清楚的说了,当我们添加了hot: true以后,它会自动帮我们加这个插件的,但是!!报错了:

image.png

解决方法一:手动添加到plugins里面:

module.exports = {
    plugins: [
        new webpack.HotModuleReplacementPlugin(), //添加
        new webpack.NamedModulesPlugin(), //添加,官方推荐的帮助分析依赖的插件
    ],
}

解决方法二:在命令行里再添加--hot参数:

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

这是我在另一篇博客里面看到的,我一直以为命令行和devServer里面配置二选一就好了,结果!!是我太年轻啊Q。

命令行还有一个比较好用的参数--open可以自动打开浏览器,这个参数也只限于命令行使用。

    "start": "webpack-dev-server --hot --open"
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(),
        new webpack.NamedModulesPlugin(),
],

好的,选择一个你喜欢的方式启动起来吧,如果能在控制台看到以下的信息,代表热更新启动起来了:

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

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一个组件然后让它包裹在<AppContainer>里面,只能单独抽离组件,否则会报错。
现在可以见证奇迹啦:


react热更新1.gif

小结

这篇文章花了我一周多的时间,最后总算弄清楚了热更新到底是怎么回事,百度一搜全都是你只要配置一个hot: true就好啦,然后都没弄明白这到底是热更新还是自动刷新,可供参考的文档只有官网,官网又讲的太简单,所以折腾了特别久。看不懂的小伙伴可以给我留言。
我把项目放在github上了,使用Node方式和非Node方式时如何配置参数都放上去了,你配置时遇到问题了可以到这里看一下:https://github.com/dengshasha/react-webpack
还有,如果还没有开始webpack配置的话可以看看我的另一篇文章开始一个React项目(一)一个最简单的webpack配置

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容

  • 构建一个小项目——FlyBird,学习webpack和react。(本文成文于2017/2/25) 从webpac...
    布蕾布蕾阅读 16,749评论 31 98
  • 无意中看到zhangwnag大佬分享的webpack教程感觉受益匪浅,特此分享以备自己日后查看,也希望更多的人看到...
    小小字符阅读 8,080评论 7 35
  • webpack 介绍 webpack 是什么 为什么引入新的打包工具 webpack 核心思想 webpack 安...
    yxsGert阅读 6,325评论 2 71
  • 在现在的前端开发中,前后端分离、模块化开发、版本控制、文件合并与压缩、mock数据等等一些原本后端的思想开始...
    Charlot阅读 5,386评论 1 32
  • 文丨红瑀 【系列连载】生活在台湾(目录) 2015年开始,随着孩子逐渐长大独立,我开始频繁往返台港深三地,一方面想...
    红瑀阅读 1,509评论 17 19