了解webpack的热更新

使用热更新小例子

js 复制代码
// dev-server.js
const HtmlWebpackPlugin = require("html-webpack-plugin")
const webpack = require("webpack")
const webpackDevServer = require('webpack-dev-server')
const path = require('path')

const config = {
  mode: 'development',
  entry: [
    './src/index.js',
    'webpack/hot/dev-server.js',
    'webpack-dev-server/client/index.js?hot=true'
  ],
  devtool: 'inline-source-map',
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new HtmlWebpackPlugin({
      title: 'hot module replacement'
    })
  ],
  output: {
    filename: '[name].[hash:4].[contenthash:8].js',
    path: path.resolve(__dirname, 'dist'),
    clean: true
  }
};

const compiler = webpack(config);
const server = new webpackDevServer({ hot: false, client: false }, compiler);

(async () => {
  await server.start()
  console.log('start...')
})();

// ./src/index.js
import print from "./print.js"

function component() {
  const btn = document.createElement('button')
  btn.innerText = 'click'
  btn.onclick = print
  return btn
}
const elem = component()
document.body.appendChild(elem)

if (module.hot) {
  module.hot.accept('./print.js', function () {
    console.log('update print.js...')
  })
}
// ./src/print.js
export default function print() {
  console.log('print')
}
  • 本例子摘取webpack官网,稍加改动
  • node dev-server.js就可以启动了

简略版webpack-dev-server内部

js 复制代码
const getExpress = memoize(() => require("express"));

class Server {
constructor(options = {}, compiler) {
    this.compiler = compiler;
    this.options = options;
    this.currentHash = undefined;
  }
  async start() {
      await this.initialize();
  }
  async initialize() {
      if (this.options.webSocketServer) {
      const compilers = this.compiler.compilers || [this.compiler];
      compilers.forEach((compiler) => {
        this.addAdditionalEntries(compiler);
        const webpack = compiler.webpack || require("webpack");
        if (this.options.hot) {
          const HMRPluginExists = compiler.options.plugins.find(
            (p) => p && p.constructor === webpack.HotModuleReplacementPlugin,
          );

          if (HMRPluginExists) {
            this.logger.warn(
              `"hot: true" automatically applies HMR plugin, you don't have to add it manually to your webpack configuration.`,
            );
          } else {
            // Apply the HMR plugin
            const plugin = new webpack.HotModuleReplacementPlugin();
            plugin.apply(compiler);
          }
        }
      });
    }
      this.setupHooks();
      this.setupApp();
      this.createServer();
      if (this.options.webSocketServer) {
          this.createWebSocketServer()
      }
  }
  addAdditionalEntries(compiler) {
    const additionalEntries = [];
    // ...
    if (this.options.client && ...) {
        additionalEntries.push(`${require.resolve("../client/index.js")}?${webSocketURLStr}`);
    }
    if (this.options.hot === "only") {
      additionalEntries.push(require.resolve("webpack/hot/only-dev-server"));
    } else if (this.options.hot) {
      additionalEntries.push(require.resolve("webpack/hot/dev-server"));
    }

    const webpack = compiler.webpack || require("webpack");

    for (const additionalEntry of additionalEntries) {
      new webpack.EntryPlugin(compiler.context, additionalEntry, {
        name: undefined,
      }).apply(compiler);
    }
  }
  setupHooks() {
    this.compiler.hooks.done.tap("webpack-dev-server", (stats) => {
        if (this.webSocketServer) {
          this.sendStats(this.webSocketServer.clients, this.getStats(stats));
        }
        this.stats = stats;
      }
    );
  }
  setupApp() {
      this.app = (new getExpress())();
  }
  createServer() {
      const { type, options } = this.options.server
      this.server = require((type)).createServer(
          options,
          this.app,
    );
    (this.server).on("connection", (socket) => {
        this.sockets.push(socket)
        socket.once("close", () => {
          this.sockets.splice(this.sockets.indexOf(socket), 1)
        })
      }
    );
  }
  createWebSocketServer() {
    this.webSocketServer = new (this.getServerTransport())(this);
    (this.webSocketServer).implementation.on("connection", (client) => {
        if (!this.stats) {
          return;
        }
        this.sendStats([client], this.getStats(this.stats), true);
      }
    );
  }
  sendStats(clients, stats, force) {
    this.currentHash = stats.hash;
    this.sendMessage(clients, "hash", stats.hash);
    this.sendMessage(clients, "ok");
  }
  sendMessage(clients, type, data, params) {
    for (const client of clients) {
      if (client.readyState === 1) {
        client.send(JSON.stringify({ type, data, params }));
      }
    }
  }
  getStats(statsObj) {
    const stats = Server.DEFAULT_STATS;
    return statsObj.toJson(stats);
  }
}

总结上述代码

  1. Server的参数可以在上个例子中得知
  2. 首先调用start方法,开始初始化,且调用HotModuleReplacementPlugin插件
  3. 在初始化过程中调用hooks,webpack在每次增量构建完成触发done并传递本次的统计信息stats,如果有webSocketServer,则向当前连接的客户端集合发送本次统计信息对象stats
  4. 接着构建app、server、websocketServer搭建服务器,用于向客户端发送信息,比如热更新{ type: "hash", data: "abcs..." },发送方法位于sendMessage

简略版webpack-dev-server下的client

js 复制代码
var client = null;

var socket = function initSocket(url, handlers, reconnect) {
  client = new WebSocketClient(url);
  client.onOpen(function () {
    retries = 0;
    if (typeof reconnect !== "undefined") {
      maxRetries = reconnect;
    }
  });
  client.onClose(function () {
    if (retries === 0) {
      handlers.close();
    }
    client = null;
    if (retries < maxRetries) {
      var retryInMs = 1000 * Math.pow(2, retries) + Math.random() * 100;
      retries += 1;
      setTimeout(function () {
        socket(url, handlers, reconnect);
      }, retryInMs);
    }
  });
  client.onMessage(function (data) {
    var message = JSON.parse(data);
    if (handlers[message.type]) {
      handlers[message.type](message.data, message.params);
    }
  });
};
var status = {
  isUnloading: false,
  currentHash: typeof __webpack_hash__ !== "undefined" ? __webpack_hash__ : ""
};
var onSocketMessage = {
  ok: function ok() {
    sendMessage("Ok");
    reloadApp(options, status);
  },
  hash: function hash(_hash) {
    status.previousHash = status.currentHash;
    status.currentHash = _hash;
  },
  // ...
};
var socketURL = createSocketURL(parsedResourceQuery);
socket(socketURL, onSocketMessage, options.reconnect);
  • 在Server.js中sendStats有一行代码 <math xmlns="http://www.w3.org/1998/Math/MathML"> t h i s . s e n d M e s s a g e ( c l i e n t s , " o k " ) ; this.sendMessage(clients, "ok"); </math>this.sendMessage(clients,"ok");这样会触发onSocketMessage的ok方法,由此触发reloadApp方法
  • 当每次构建完触发done钩子函数,服务端向客户端发送hash事件

简略版reloadApp

js 复制代码
function reloadApp(_ref, status) {
  var hot = _ref.hot, liveReload = _ref.liveReload;

  function applyReload(rootWindow, intervalId) {
    clearInterval(intervalId);
    rootWindow.location.reload();
  }
  var search = self.location.search.toLowerCase();
  var allowToHot = search.indexOf("webpack-dev-server-hot=false") === -1;
  var allowToLiveReload = search.indexOf("webpack-dev-server-live-reload=false") === -1;
  if (hot && allowToHot) {
    hotEmitter.emit("webpackHotUpdate", status.currentHash);
  } else if (liveReload && allowToLiveReload) {
    var rootWindow = self;
    var intervalId = self.setInterval(function () {
      if (rootWindow.location.protocol !== "about:") {
        applyReload(rootWindow, intervalId);
      } else {
        rootWindow = rootWindow.parent;
        if (rootWindow.parent === rootWindow) {
          applyReload(rootWindow, intervalId);
        }
      }
    });
  }
}
  • 允许热更新则触发webpackHotUpdate事件,该事件在 <math xmlns="http://www.w3.org/1998/Math/MathML"> a d d i t i o n a l E n t r i e s . p u s h ( r e q u i r e . r e s o l v e ( " w e b p a c k / h o t / d e v − s e r v e r " ) ) additionalEntries.push(require.resolve("webpack/hot/dev-server")) </math>additionalEntries.push(require.resolve("webpack/hot/dev−server"))引入的模块中注册过

webpack的hot文件下dev-server

js 复制代码
if (module.hot) {
    var lastHash;
    var upToDate = function upToDate() {
        return lastHash.indexOf(__webpack_hash__) >= 0;
    };
    var check = function check() {
        module.hot
            .check(true)
            .then(function (updatedModules) {
                if (!updateModules) {
                    log("warning","[HMR] Cannot find update. " + (typeof window !== "undefined" ? "Need to do a full reload!" : "Please reload manually!"));
                    return;
                }
                if (!upToDate()) {
                    check();
                }
                
            })
    };
    var hotEmitter = require("./emitter");
    hotEmitter.on("webpackHotUpdate", function (currentHash) {
	lastHash = currentHash;
	if (!upToDate() && module.hot.status() === "idle") check();
    });
}
  • status 为 'idle'表示该进程正在等待调用 check
  • module.hot.check 会触发检查当前模块及其依赖模块是否有更新。如果有更新,webpack 将尝试进行热替换,将新的模块代码应用到运行中的应用程序,从而实现无需刷新页面即可更新代码
  • 接收到客户端发来的webpackHotUpdate信息,更新哈希值,并判断是否更新并且当前状态在等待check

HotModuleReplacementPlugin

js 复制代码
function hotCheck(applyOnUpdate) {
  if (currentStatus !== "idle") {
    throw new Error("check() is only allowed in idle status");
  }
  return setStatus("check")
    .then($hmrDownloadManifest$) // 下载增量文件
    .then(function (update) {
      if (!update) {
        return setStatus(applyInvalidatedModules() ? "ready" : "idle").then(
          function () {
            return null;
          }
        );
      }

      return setStatus("prepare").then(function () {
        var updatedModules = [];
        currentUpdateApplyHandlers = [];

        return Promise.all(
          Object.keys($hmrDownloadUpdateHandlers$).reduce(function (
            promises,
            key
          ) {
            $hmrDownloadUpdateHandlers$[key](
              update.c,
              update.r,
              update.m,
              promises,
              currentUpdateApplyHandlers,
              updatedModules
            );
            return promises;
          }, [])
        ).then(function () {
          return waitForBlockingPromises(function () {
            if (applyOnUpdate) {
              // 替换更新代码
              return internalApply(applyOnUpdate);
            } else {
              return setStatus("ready").then(function () {
                return updatedModules;
              });
            }
          });
        });
      });
    });
}

function accept (dep, callback, errorHandler) {
    if (dep === undefined) hot._selfAccepted = true;
    else if (typeof dep === "function") hot._selfAccepted = dep;
    else if (typeof dep === "object" && dep !== null) {
        for (var i = 0; i < dep.length; i++) {
            hot._acceptedDependencies[dep[i]] = callback || function () {};
            hot._acceptedErrorHandlers[dep[i]] = errorHandler;
        }
    } else {
            hot._acceptedDependencies[dep] = callback || function () {};
            hot._acceptedErrorHandlers[dep] = errorHandler;
    }
}

function internalApply(options) {
options = options || {};
var results = currentUpdateApplyHandlers.map(function (handler) {
    return handler(options);
});
currentUpdateApplyHandlers = undefined;

results.forEach(function (result) {
    if (result.dispose) result.dispose();
});

var outdatedModules = [];
results.forEach(function (result) {
    if (result.apply) {
        var modules = result.apply(reportError);
	if (modules) {
            for (var i = 0; i < modules.length; i++) {
                outdatedModules.push(modules[i]);
            }
        }
    }
});

return Promise.all([...]).then(function () {
    return setStatus("idle").then(function () {
        return outdatedModules;
    });
});
}
  • $hmrDownloadManifest$在其他函数中替换为RuntimeGlobals.hmrDownloadManifest
js 复制代码
// RuntimeGlobals.js
// function downloading the update manifest
exports.hmrDownloadManifest = "__webpack_require__.hmrM";

// array with handler functions to download chunk updates
exports.hmrDownloadUpdateHandlers = "__webpack_require__.hmrC";

// object with all hmr module data for all modules
exports.hmrModuleData = "__webpack_require__.hmrD";
  • 这些变量会在其他地方赋值为函数
js 复制代码
Template.asString([`${RuntimeGlobals.hmrDownloadManifest} = ${runtimeTemplate.basicFunction("", [
		'if (typeof fetch === "undefined") throw new Error("No browser support: need fetch API");',
		`return fetch(${RuntimeGlobals.publicPath} + ${
			RuntimeGlobals.getUpdateManifestFilename
		}()).then(${runtimeTemplate.basicFunction("response", [
			"if(response.status === 404) return; // no update available",
			'if(!response.ok) throw new Error("Failed to fetch update manifest " + response.statusText);',
			"return response.json();"
		])});`
	])};`
])

总结

  • 首先内部向入口额外添加HotModuleReplacementPlugin、创建客户端webSocket和注册webpackHotUpdate监听事件
  • 接着使用done钩子在每次构建完向客户端发送hashok事件
    • HotModuleReplacementPlugin会注入一段代码:hmrDownloadManifest、hmrDownloadUpdateHandlers、check等
  • 接着创建服务端,并在connection事件中记录与之连接的客户端,然后发送hashok消息,客户端接受到后调用reloadApp方法,并触发webpackHotUpdate事件,执行该事件的回调函数,执行check方法,其方法包括module.hot.check
  • module.hot.check方法去请求增量文件,然后下载更新的chunk,执行module.hot.accept代码实现无刷新更新
  • 对于后续文件更新,首先对文件进行监听,有改动则触发webpack重新编译,之后触发done钩子
相关推荐
速盾cdn5 分钟前
速盾:vue的cdn是干嘛的?
服务器·前端·网络
四喜花露水38 分钟前
Vue 自定义icon组件封装SVG图标
前端·javascript·vue.js
前端Hardy1 小时前
HTML&CSS: 实现可爱的冰墩墩
前端·javascript·css·html·css3
web Rookie1 小时前
JS类型检测大全:从零基础到高级应用
开发语言·前端·javascript
Au_ust1 小时前
css:基础
前端·css
帅帅哥的兜兜1 小时前
css基础:底部固定,导航栏浮动在顶部
前端·css·css3
yi碗汤园1 小时前
【一文了解】C#基础-集合
开发语言·前端·unity·c#
就是个名称1 小时前
购物车-多元素组合动画css
前端·css
编程一生2 小时前
回调数据丢了?
运维·服务器·前端
丶21362 小时前
【鉴权】深入了解 Cookie:Web 开发中的客户端存储小数据
前端·安全·web