参考:https://github.com/advanced-frontend/daily-interview-question/issues/118

唉,写的我快吐血了。。。唉妈的。。。呜呜呜。。。。明天在写吧张京。。。

webpack-dev-server只负责启动服务和前置准备工作,所有文件相关的操作都抽离到webpack-dev-middleware库了,主要是本地文件的编译和输出以及监听,职责的划分更清晰了。

首先,介绍webpack-dev-server:
webpack-dev-server 主要包含了三个部分:
1.webpack: 负责编译代码
2.webpack-dev-middleware: 主要负责构建内存文件系统,把webpack的 OutputFileSystem 替换成 InMemoryFileSystem。同时作为Express的中间件拦截请求,从内存文件系统中把结果拿出来。
3.express:负责搭建请求路由服务。

工作流程:

  1. 启动dev-server,webpack开始构建,在编译期间会向 entry 文件注入热更新代码;
  2. Client 首次打开后,Server 和 Client 基于webSocket建立通讯渠道
  3. 修改文件,Server 端监听文件发送变动[^1],webpack开始编译,直到编译完成会会绑定一个监听事件setupHooks [^2]的方法去触发Done事件;【当监听到一次webpack编译结束,就会调用_sendStats方法通过websocket给浏览器发送通知,ok和hash事件,这样浏览器就可以拿到最新的hash值了,做检查更新逻辑。】

[^1] 监听文件的变动主要是webpack-dev-middleware来完成的,主要是通过
1、调用了compiler.watch方法

这个方法主要做了 2 件事:
(1) 首先对本地文件代码进行编译打包,也就是webpack的一系列编译流程。
(2) 其次编译结束后,开启对本地文件的监听,当文件发生变化,重新编译,编译完成之后继续监听。为什么代码的改动保存会自动编译,重新打包?这一系列的重新检测编译就归功于compiler.watch这个方法了。监听本地文件的变化主要是通过文件的生成时间是否有变化,这里就不细讲了。
2、执行setFs方法,这个方法主要目的就是将编译后的文件打包到内存。
这就是为什么在开发的过程中,你会发现dist目录没有打包后的代码,因为都在内存中。原因就在于访问内存中的代码比访问文件系统中的文件更快,而且也减少了代码写入文件的开销,这一切都归功于memory-fs。

// node_modules/webpack-dev-middleware/index.js
compiler.watch(options.watchOptions, (err) => {
    if (err) { /*错误处理*/ }
});

// 通过“memory-fs”库将打包后的文件写入内存
setFs(context, compiler);

[^2] 当监听到一次webpack编译结束,_sendStats方法就通过websoket给浏览器发送通知,检查下是否需要热更新。
_sendStats方法中的ok和hash【socket方法建立了websocket和服务端的连接,并注册了 2 个监听事件】。
1、hash事件,更新最新一次打包后的hash值。
2、ok事件,进行热更新检查。

// node_modules/webpack-dev-server/lib/Server.js
// 绑定监听事件
setupHooks() {
    const {done} = compiler.hooks;
    // 监听webpack的done钩子,tapable提供的监听方法
    done.tap('webpack-dev-server', (stats) => {
        this._sendStats(this.sockets, this.getStats(stats));
        this._stats = stats;
    });
};

// 通过websoket给客户端发消息
_sendStats() {
    this.sockWrite(sockets, 'hash', stats.hash);
    this.sockWrite(sockets, 'ok');
}
  1. Server通过websocket 发送消息告知 Client
  2. Client根据Server的消息(hash值和state状态),通过ajax请求获取 Server 的manifest描述文件;
  3. Client对比当前 modules tree ,再次发请求到 Server 端获取新的JS模块;
  4. Client获取到新的JS模块后,会更新 modules tree并替换掉现有的模块;
  5. 最后调用 module.hot.accept() 完成热更新;