[Webpack茶话会]-HMR

858 阅读5分钟

什么是HMR

全名:HotModuleReplacement(热更新),当本地代码发生变化后,通知浏览器进行代码更新,但不会进行状态改变。

怎么实现HMR

这里我们使用webpack-dev-middleware和webpack-hot-middleware

为什么不用webpack-dev-server?

  • webpack-dev-server封装的比较完整,难以从配置上看出HMR需要做哪些工作
  • webpack-dev-server在本地开发的时候较为快捷,但是在与node结合的时候,不太方便。
  • 原理大同小异,webpack-dev-server使用socketIo,而webpack-hot-middleware使用的SSE技术,进行与浏览器沟通

修改webpack配置

module.exports = {
  mode: 'development',
  context: __dirname,
  entry: [
    // 需要把这块代码一起打入bundle中
    'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000',
    // 真正的入口
    './client.js'
  ],
  output: {
    path: __dirname,
    publicPath: '/',
    filename: 'bundle.js'
  },
  devtool: '#source-map',
  plugins: [
    // 这段不要忘了
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoEmitOnErrorsPlugin()
  ],
};

node中

var app = express();
(function() {

  // Step 1: 创建webpackCompiler
  var webpack = require('webpack');
  var webpackConfig = require(process.env.WEBPACK_CONFIG ? process.env.WEBPACK_CONFIG './webpack.config');
  var compiler = webpack(webpackConfig);

  // Step 2: 接入webpack-dev中间件
  app.use(require("webpack-dev-middleware")(compiler, {
    logLevel'warn'publicPath: webpackConfig.output.publicPath
  }));

  // Step 3: 接入webpackHot中间件
  app.use(require("webpack-hot-middleware")(compiler, {
    log: console.log, path'/__webpack_hmr'heartbeat10 * 1000
  }));
})();

前端代码中

if (module.hot) {
  module.hot.accept();
}

这样就可以实现热更新了,下面我们来详细讲解下webpack-dev-middleware和webpack-hot-middleware是起到了什么作用

原理

第一步

  entry: [
    // 需要把这块代码一起打入bundle中
    'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000',
    // 真正的入口
    './client.js'
  ],
  • 重点来了:在entry里加入'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000',实际上就是找到这段代码合并到bundle包里。
  • 这个文件的主要作用就是创建EventSource实例,请求 /__webpack_hmr,监听building、built、sync事件
  function init() {
    source = new window.EventSource(options.path);
    source.onmessage = handleMessage;
  }
    
  // Eventsource收到消息,通知所有回调执行  
  function handleMessage(event) {
    lastActivity = new Date();
    for (var i = 0; i < listeners.length; i++) {
      listeners[i](event);
    }
  }
  return {
    addMessageListener: function(fn) {
      listeners.push(fn);
    },
  };
  • 回调里检查是否有更新,回调函数会利用HotModuleReplacementPlugin运行时代码进行更新;
check(){
    var result = module.hot.check(false, cb);
    if (result && result.then) {
      result.then(function(updatedModules) {
        // 如果updatedModules为null,则整体reload
        // 否则执行替换逻辑
        var applyResult = module.hot.apply();
      });
      result.catch(cb);
    }
}
  • module.hot实际就是HotModuleReplacementPlugin中的hotCheck
  • 调用hotDownloadManifest,向 server 端发送 Ajax 请求,服务端返回一个 Manifest文件(lasthash.hot-update.json),该 Manifest 包含了本次编译hash值 和 更新模块的chunk名,xxxx.hot-update.json
function hotCheck(apply) {      hotDownloadManifest(hotRequestTimeout).then(function(update) {}
  • 方法通过JSONP请求获取到最新的模块代码,main.xxxx.hotupdate.js
hotDownloadUpdateChunk(chunkId)
  • hotAddUpdateChunk动态更新模块代码,
  • 调用hotApply方法进行热更新

第二步

把webpackCompiler对象传入expres的中间件webpack-dev-middleware

  • 让webpack以watch模式运行
  // Start watching
  context.watching = context.compiler.watch(watchOptions, (error) => {
    if (error) {
      context.logger.error(error);
    }
  });
  • 并将文件系统改为内存文件系统,不会把打包后的资源写入磁盘而是在内存中处理;
  for (const compiler of compilers) {
    // eslint-disable-next-line no-param-reassign
    compiler.outputFileSystem = outputFileSystem;
  }
  context.outputFileSystem = outputFileSystem;
  • 将编译的文件返回
   async function processRequest() {
      const filename = getFilenameFromUrl(context, req.url);
      let content = context.outputFileSystem.readFileSync(filename);
      // Buffer
      content = handleRangeHeaders(context, content, req, res);
      // send Buffer
      res.send(content);
    }

第三步

把webpackCompiler对象传入expres的中间件webpack-hot-middleware

  • 监听webpack打包进度
    compiler.plugin('done', onDone);
  • 打包完成后
publishStats('built', latestStats,eventStream, opts.log);
// EventStream发送消息  
eventStream.publish({
name: name,
action: action,
time: stats.time,
hash: stats.hash,
warnings: stats.warnings || [],
errors: stats.errors || [],
modulesbuildModuleMap(stats.modules),
    });
  • 收到的请求如果url是'/webpack_hmr',则把它加入EventStream回调,进行心跳检测
  var middleware = function(req, res, next) {
    if (closed) return next();
    //请求的url地址不是/__webpack_hmr,则走下一个中间件
    if (!pathMatch(req.url, opts.path)) return next();
    // 
    eventStream.handler(req, res);
    if (latestStats) {
      // 给所有的http请求,发送EventStream
      publishStats('sync', latestStats, eventStream);
    }
  };

整体流程和总结

流程图

总结

整体HMR的流程并不复杂,主要的点就是监听webpack打包,server向客户端发送消息通知,客户端拉取最新的module后进行热更。 本篇文章Demo地址传送门