搭建带热更新功能的本地开发node server

引言

使用webpack有一段时间了,对其中的热更新的大概理解是:对某个模块作了修改,页面只作局部更新而不须要刷新整个页面来进行更新。这样就能节省由于整个页面刷新所产生开销的时间,模块热加载加快了开发的速度。javascript

热加载的基础是模块热替换(HMR,Hot Module Replacement)。css

具体的是:webpack能够监控文件的改动,在模块文件代码发生改动时,并发送 HMR 更新消息(HMR update)给HMR 运行时(HMR runtime)环境,它决定模块的替换,具体能够参考下图:html

HMR实现的具体效果能够先看下下图的效果:java

但是最近,亲自搭建一个webpack应用项目时,在实现开发环境的模块热更新时,遇到这样那样的问题。因为以前都是使用第三方插件来实现应用的热更新,它们都封装了实现热更新的一些细节,致使在不用第三方插件实现模块热更新时出现问题,其实仍是理解的不够深刻。因而在搞明白以后写下此文与你们分享。node

Hot Module Replacement(HMR)

webpack的自带的HMR插件HotModuleReplacementPlugin是使用webpack热更新功能的基础。其余的第三方插件如webpack-hot-middlewarereact-hot-loaderbabel-plugin-dva-hmr等等都是要配合webpack自带的HotModuleReplacementPlugin插件提供的api来实现代码的热更新。例以下面在某个模块中使用HMR代码一个例子:react

if (module.hot) {
   module.hot.accept('./containers/rootContainer.js', () => {
     const NextRootContainer = require('./containers/rootContainer.js').default;
     render(<NextRootContainer />, document.getElementById('react-root'));
   }
 }

固然HotModuleReplacementPlugin为可使用HMR的模块提供了module.hot,它为一个对象,其含有不少api,具体能够参考这里。这样利用插件提供的这些api能够为模块实现自定义的热更新逻辑。webpack

可是,在开发过程当中,大家可能也发现了,咱们并无为项目中的每一个模块提供这种多余的HMR代码,尽管全部代码都有可能变化。那么当这些代码没有HMR代码的模块发生变化时,他是如何实现热更新的呢?这就要说到webpack HMR更新的冒泡(bubble)机制。具体能够看下图所展现的冒泡机制:git

从图中能够看出:github

  • 模块C发生了变化,可是模块C没有用HMR代码捕获变化,则模块C的变化消息将冒泡到依赖C模块的其余模块A和B中。web

  • 模块B因为使用了HMR代码进行捕获变化,那么应用的变化就按照代码进行了更新。而且不会再冒泡了。

  • 模块A因为一样没有HMR代码捕获变化,一样将变动消息冒泡到依赖A模块的模块entry中。

  • 入口entry模块没有HMR代码捕获变化的话:

    • 一、 若项目使用webpack-dev-server的webpack/hot/dev-server,则页面会刷新整个页面来加载变化;若使用webapck/hot/only-dev-server的话,不会刷新页面,会在控制台展现一些有用的信息供开发者参考。具体能够参考这里

    • 二、若为webpack-hot-middleware配置了reload:true,那么页面就会整个刷新来加载加载变化,这就变成liveroad模式;不然webpack就不知道如何加载变化模块,控制台也会有对应的提示。

例如,在本人的实例中,修改了searchForm.jsx模块,能够在控制台清晰的看到,它一直冒泡到入口模块index.js。以下图:

开发过程当中遇到的问题

在用webpack构建的项目中,在开发阶段咱们为了实现开发过程代码的热更新,若是对使用HMR不熟悉,可能会遇到这样或者那样的问题。下面就在本人开发过程当中遇到过:

一、在cli中使用带--hot选项的webpack-dev-server命令时,不要在webpack的配置文件在配置HMR插件。

不然会报下面的错误,具体可参考这里

注意:

webpack-dev-server的node api模式下配置hot: true仍然须要在webpack配置文件中配置该插件

重要更新:

\(\color{#FF0000}{该规则已不是问题,目前的webpack4已作了处理,即若webpack的配置项配置过HMR插件就不作处理,没有配置则会主动帮咱们添加。}\)

其中源码以下:

[].concat(config).forEach((config) => {
      config.entry = prependEntry(config.entry || './src');

      if (options.hot || options.hotOnly) {
        config.plugins = config.plugins || [];
        if (
          !config.plugins.find(
            (plugin) =>
              plugin.constructor === webpack.HotModuleReplacementPlugin
          )
        ) {
          config.plugins.push(new webpack.HotModuleReplacementPlugin());
        }
      }
    });

二、在不使用第三方HMR库,纯搭建本身的本地node server时,必定要在项目的入口模块添加module.hot.accept代码来接受更新消息以实现热更新。

在本人另外一个项目中,使用dora插件系列的dora-plugin-webpack-hmr插件来实现热更新,因为没有在入口模块添加HMR代码来接受变动,致使模块一产生变化就刷新整个页面。

具体是由于:dora-plugin-webpack-hmr使用webpack-hot-middleware时,默认配置了其reload:true(参考这里),因此每次修改都会刷新整个页面。

第三方HMR插件/库的实现细节

前面说到,要想实现webpack的HMR功能,须要两点:webpack配置HMR入口文件添加HMR代码。两者缺一不可,不然模块热更新就会失败。

可是,在开发过程当中,咱们可能根本没有配置过上面所说的两点;这主要是由于咱们在项目中使用第三方HMR插件或者库,它们自动替咱们完成这些;要么是两者都会给配置掉,要么就配置其中之一。 比方在本人项目中使用过的dora-plugin-webpack-hmrbabel-plugin-dva-hmr,以及Gaearon大神的react-hot-loader;下面就来讲说他们的他们为咱们作了什么隐蔽的事。

dora-plugin-webpack-hmr

该插件是为dora系列的插件,主要用在基于dora的项目中。该插件是基于webpack-hot-middleware库来实现热加载的,它主要为咱们作了两件事:

  • 代码更新没有捕获时会刷新整个页面来加载更新。 也就是为webpack-hot-middleware的reload属性默认配置true,可看源码1

  • 自动为webpack配置项添加HMR插件配置。具体看源码2这样,咱们使用该插件就不须要在webpack中配置HMR,不然会遇到常见问题1中的状况。

因此:

使用dora-plugin-webpack-hmr插件仍是须要在入口模块添加module.hot.accept来接受更新,不然达不到热更新效果。

babel-plugin-dva-hmr

该插件是与dva配套的,用在使用dva框架下的代码热更新插件。该插件自动替咱们在入口模块添加HMR代码,具体可看源码3,开发环境下入口模块添加的代码以下图:

由此该插件只帮咱们在入口模块添加HMR代码接受变动,可是它没有帮咱们在webpack中配置HMR,这样HMR的api是不能用的。因此:

使用babel-plugin-dva-hmr插件还须要在webpack配置项中配置HMR。

react-hot-loader@<3.0.0

该loader的目的是:保持组件状态的热更新。即不只达到模块的热更新,还要保持各个模块的状态不会丢失,具体可参考Gaearon大神的Hot Reloading in React。它如何保持状态不在本文范围,可自行查询。

在该loader的3.0.0版本前,与babel-plugin-dva-hmr插件相似,它也是自动为咱们在模块中注入接受更新的HMR代码而没有在webpack配置项自动添加HMR配置,具体可参考源码4。可是它与前者不一样是:它为每一个启用该loader的js文件都注入接受更新的HMR代码

例如,在webpack.config.js中为js文件配置该插件:

//这样src目录下的全部.js文件都将被自动添加HMR热更新代码
loaders: [{
      test: /\.js$/,
      loaders: ['react-hot', 'babel'],
      include: path.join(__dirname, 'src')
    }]

自动添加的有关HMR代码以下,只截取部分代码:

可是一样的,

咱们须要在webpack配置项中添加HMR插件配置。

注意:
react-hot-loader在3.0.0版本以后就废弃掉该方式,不会自动添加HMR热更新代码,须要开发者在项目入口模块手动添加HMR代码,参考这里

搭建带HMR的本地开发node sever

以前,与webpack配合的webpack-dev-server服务,经过配置就能够实现代码热更新,可是隐藏了实现细节。下面咱们手动搭建一个自带HMR功能的本地开发node sever。

一、使用webpack-dev-middleware搭建本地服务

webpack-dev-server就是基于webpack-dev-middleware来搭建内部node server。咱们搭建本身的开发环境就用它来直接搭建。

二、使用webpack-hot-middleware来实现客户端与服务端的通讯以接受更新

该模块只是负责客户端与服务器通讯及接受变化,可是如何实现根据热加载来完成应用的无缝变化衔接就超出了该模块的范围,正如其官网所描述:

This module is only concerned with the mechanisms to connect a browser client to a webpack server & receive updates. It will subscribe to changes from the server and execute those changes using webpack's HMR API. Actually making your application capable of using hot reloading to make seamless changes is out of scope, and usually handled by another library.

这句话的意思是:

What this means in practice, is you either need to add some code which calls module.hot.accept(), or use a plugin which can automatically add this code to your modules - otherwise webpack doesn't know how to apply the hot update.

也就是, 要么你在模块中增长调用module.hot.accept()的代码,要么使用第三方插件自动的为你模块添加这些代码;不然webpack不知道怎么更新这些模块。具体能够参考这里

另外,要使用HMR功能,须要在webpack的配置项的每一个入口项数组中添加webpack-hot-middleware/client,即:

entry: {
    index: ['./src/index','webpack-hot-middleware/client']
 }

三、配置HMR

正如上文所描述的,它分为两步:

  • 首先,要在webpack的配置项plugins须要配置HMR插件即
plugins: [ new webpack.HotModuleReplacementPlugin()]
  • 其次,须要在项目的入口模块中添加HMR代码捕获变化以作热更新。例以下面:
if(module.hot){
   module.hot.accpet() //接受模块更新的事件,同时阻止这个事件继续冒泡
}

若为每一个模块添加HMR代码来热更新对应的模块机制是不可取的,这会产生大量冗余代码,极不推荐这种作法,除非像第三方插件那样自动帮咱们完成。

通常在入口模块添加module.hot的相关api来更新具体变化,入口模块没有添加的话就不会达到热更新的效果,浏览器控制台也会出现以下警告(前提是webpack-hot-middleware的reload配置为false):

在浏览器控制台中出现这样一句提示:

This is usually because the modules which have changed (and their parents) do not know how to hot reload themselves.

正如提示所说的,修改某个子模块时,若不在模块自己或者顶级的入口模块添加热更新接受机制,那么产生变化的模块及其父模块不知道怎么加载他们。

最终,用户自定义的开发环境node server具体的核心开发代码以下:

//dev-server.js 文件
var webpackDevMiddleware = require('webpack-dev-middleware');
var webpackHotMiddleware = require('webpack-hot-middleware');

Object.keys(webpackConfig.entry).forEach(function(name){
  webpackConfig.entry[name] = ['webpack-hot-middleware/client'].concat(webpackConfig.entry[name]);
})
var compiler = webpack(webpackConfig);

var devMiddleware = webpackDevMiddleware(compiler, {
    publicPath: webpackConfig.output.publicPath,
    hot: true,
    noInfo: true,
    stats: {
      colors: true
    }
  });
var hotMiddleware = webpackHotMiddleware(compiler);
app.use(devMiddleware);
app.use(hotMiddleware);

app.listen(port, function(err){
  if(err){
    console.log(err);
  }else {
    var url = 'http://localhost:' + port;
    console.log("listening on port %s", port);
  }
})

另外,咱们可能会想到,在使用redux的react项目中,这种热更新会致使应用的state丢失,为了防止state随热更新而丢失,通常须要在针对reducer的修改来实现进行state的保存,最经常使用的作法是在store模块下添加以下reducer热更新代码:

if(module.hot){
    module.hot.accept('../reducers/index.js', ()=>{
      const nextReducer = require('../reducers/index.js');
      store.replaceReducer(nextReducer || nextReducer.default);
    })
  }

至此,一个带HMR代码热更新功能的本地开发node server就搭建成功了。

其余文件热更新的实现

上面的带HMR热更新功能的node server虽已搭建,可是就能知足咱们的开发需求了么?我想答案是否认的。上面的热更新实际上是针对js文件的热更新,也就是说对js文件的变动作热更新。在实际项目中,咱们修改的可不只仅是js文件,还有css文件html文件等等,这些都须要考虑热更新。

一、html文件的热更新

在项目中,咱们使用html-webpack-plugin来生成webpack spa页面。因为该插件不支持HMR,为了支持html的HMR,咱们须要利用webpack-hot-middleware提供对外接口来实现。具体须要三步:

  • 首先,在上面的dev-server.js中为html-webpack-plugin钩子html-webpack-plugin-after-emit增长回调,释放一个信号表示html页面已经构建完成。
// dev-server.js
compiler.plugin('compilation', function (compilation) {//webpack编译完成
  //在这个插件合成出页面以后,添加一个回调,调用中间件emit一个action为reload的事件,对应另外一边client订阅的事件,实现浏览器的刷新
  compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
    hotMiddleware.publish({action: 'reload'})
    cb()
  })
});
  • 其次,为html页面构建完成后添加回调,用于实现热更新逻辑
// 新建一个build/dev-client.js文件
var hotClient = require('webpack-hot-middleware/client');
// 添加一个订阅事件,当监听到 event.action === 'reload' 时执行页面刷新
hotClient.subscribe(function (event) {
    if (event.action === 'reload') {
        window.location.reload()
    }
})
  • 最后,修改webpack的entry,为其添加前缀,即第二步建立的文件build/dev-client
// 在webpack配置中设置
Object.keys(config.entry).forEach(function (name, i) {
    config.entry[name] =  ['./build/dev-client'].concat(config.entry[name])
})

至此,html文件的热更新就完成了,不过这里不是真正意义上的热更新,而是刷新整个页面。

二、css文件的热更新

通常状况下,webpack项目中的css处理都是经过 extract-text-webpack-plugin 插件把css抽离到单独css文件中,但使人遗憾的是该插件是不支持热加载的,具体能够参考issue

可是,可喜的是webpack的style-loader是支持css热加载的。 该插件经过js建立一个 style 标签,而后注入内联的css。

因此,按照上面描述,要想实现css的热加载,只须要: 开发环境不要用extract-text-webpack-plugin插件,而是用style-loader代替。 可是,这种作法被开发者狠狠的吐槽了,而且还列出的缘由:

  • 用隔离的css文件能更好的调试

  • 开发和生产环境的尽量的一致,能够保证尽量少的bug

吐槽归吐槽,官方仍是没有提供热加载支持,可是社区出现了extract-text-webpack-plugin支持热加载的各类实现方式,虽然有些是hack,可是能工做的很好啊,例以下面的列举的实现:

  • 相似于html文件热更新,采用事件通知机制来实现,能够参加这里

  • 将引入js模块中的css模块文件,如require('<path to css file>')这行代码抽取成一个单独的js文件,并在该js文件实现模块更新接收,能够参考这里

  • 基于webpack2热加载机制的事件实现(参考这里以及基于此为避免FOUC升级实现)

  • 用一个babel插件css-hot-loader来实现。

该插件的实现原理:

每次热加载都是一个 js 文件的修改,每一个 css 文件在 webpack 中也是一个 js 模块,那么只须要在这个 css 文件对应的模块里面加一段代码就能够实现 css 文件的更新了(具体的是更新外链link的地址url,为其添加时间戳),它会自动在每一个css文件中添加以下代码:

if(module.hot) {
      // ${Date.now()}
      const cssReload = require(${loaderUtils.stringifyRequest(this, require.resolve('./hotModuleReplacement'))})(${JSON.stringify(options)});
      module.hot.dispose(cssReload);
      module.hot.accept(undefined, cssReload);
    }

最终对应的CSS文件编译生成的代码多是这样子:

// removed by extract-text-webpack-plugin
    if(module.hot) {
      // 1498744720173
      const cssReload = require("../../../node_modules/css-hot-loader/hotModuleReplacement.js")({"fileMap":"{fileName}"});
      module.hot.dispose(cssReload);
      module.hot.accept(undefined, cssReload);
    }
  
/*****************
 ** WEBPACK FOOTER
 ** ./src/routes/main.less
 ** module id = 636
 ** module chunks = 1
 **/

三、其余配置文件变更的更新

这里不说代码热更新,而是提供一种代码变更更新机制。

在项目中,咱们能够很容易实现js、css和html文件的热更新;可是,咱们有没有想到过,在咱们项目中其余文件变动时也要加载变化后的文件,例如项目中package.json或者webpack.config.js配置文件发生了变化,咱们也想浏览器有所反应而不是无动于衷,那么咱们能够监控这些文件的变化来实现。具体:

  • 在上述的dev-server.js文件中用chokidar添加对指定文件的监控,好比webpack.config.js
//dev-server.js
var chokidar = require('chokidar');
chokidar.watch(path.resolve(process.cwd(), 'webpack.dev.conf.js')).on('change', function(){
  process.send('restart'); //向父进程传递消息信号
})
  • 建立本地node server主入口文件,用于建立dev-server.js对应的子进程。
//dev-server-main.js
var cp = require('child_process');
function start(){
  const p = cp.fork(__dirname + '/dev-server.js');
  p.on('message', function(data){
    if(data === 'restart'){
      p.kill('SIGINT');
      start();
    }
  })
}
if(!process.send){
  start();
}
  • 最后用node dev-server-main.js开启服务

这样,就能够实现修改webpack.config.js达到从新加载配置的目的。不过它的作法是webpack从新对项目编译。

参考文献

相关文章
相关标签/搜索