参考答案

简介

在看webpack-dev-server源码之前,我们要先弄明白webpack-dev-server是个什么,它能做哪些事情。

我们知道用webpack可以打包我们的项目文件,然后部署上线,但是在开发过程中,我们想实时看到代码变更后我们的项目效果时,我们就会启动一个服务来监听代码文件变化,并将新的变更及时的展现在我们的浏览器上,极大的提高了我们的开发效率,这就是webpack-dev-server带给我们的东西。

版本

  • webpack-dev-server:4.7.4

📢注意

为了方便阅读文章里使用的代码都是精简后主要流程的伪代码。

流程图

为了便于串联起来理解,整理了一份主要步骤的流程图

命令行启动

当我们在命令行敲下npm run start,一般后面都是运行:

"start": "webpack serve --open",

这里webpack就会基于我们webpack.config.js里的配置创建一个compiler,然后基于compilerdevServer相关配置生成一个WepackDevServer实例,该实例会启动一个express服务来帮我们监听静态资源变化并更新。 我们以下面这段代码为例开始我们的源码探索:

"use strict"; const Webpack = require("webpack"); const WebpackDevServer = require("../../../lib/Server"); const webpackConfig = require("./webpack.config"); const compiler = Webpack(webpackConfig); const devServerOptions = { ...webpackConfig.devServer, open: true }; const server = new WebpackDevServer(devServerOptions, compiler); const runServer = async () => { console.log("Starting server..."); await server.start(); }; runServer();

因为我们是在研究wepack-dev-server,这里我们主要关注server.start()方法里发生了什么。

start

async start() { // 这里主要是对我们的配置进行校验和补充(没配置的加默认项) await this.normalizeOptions(); // 配置devserver服务的域名和端口 this.options.host = await Server.getHostname(this.options.host); this.options.port = await Server.getFreePort(this.options.port); // 初始化client和dev-server,以plugin的形式挂到compiler上,添加hooks插件,实例化express服务等 await this.initialize(); const listenOptions = { host: this.options.host, port: this.options.port }; // 启动express服务 await ( new Promise((resolve) => { (this.server).listen(listenOptions, () => { resolve(); }); }) ); // websocket长连接 if (this.options.webSocketServer) { this.createWebSocketServer(); } this.logStatus(); if (typeof this.options.onListening === "function") { this.options.onListening(this); } }

初始化

上面的await this.initialize();这里做了很多事情,来详细看下:

// 1. 加载client和dev-server文件,以plugin的形式挂到compiler上 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")); } if (typeof webpack.EntryPlugin !== "undefined") { for (const additionalEntry of additionalEntries) { new webpack.EntryPlugin(compiler.context, additionalEntry, { // eslint-disable-next-line no-undefined name: undefined, }).apply(compiler); } } // 2. 挂载模块热替换插件 const plugin = new webpack.HotModuleReplacementPlugin(); plugin.apply(compiler); // 这里主要是在webpack编译完成的done钩子函数中进行消息广播给客户端 this.setupHooks(); // 创建一个express实例 this.setupApp(); // 给express实例添加请求头header检测 this.setupHostHeaderCheck(); // dev中间件,修改webpack打包输出方式,在webpack不同钩子注册回调,启动webpack编译代码,从内存中读取数据流等 this.setupDevMiddleware(); // 处理客户端请求 this.setupBuiltInRoutes(); // 监听文件变化 this.setupWatchFiles(); // 监听静态文件变化 this.setupWatchStaticFiles(); // 根据用户配置添加一些中间件,比如:代理 this.setupMiddlewares(); // 基于express实例创建服务 this.createServer();

setupHooks

setupHooks主要做的就是在webpackdone钩子上挂了个给客户端广播消息的回调,通过这个回调,客户端就能知道项目工程代码有更新,这时候客户端就会发请求给express服务去获取最新的webpack打包的代码。

this.compiler.hooks.done.tap( "webpack-dev-server", (stats) => { if (this.webSocketServer) { // 给客户端发消息,包括更新类型,状态,hash等 this.sendStats(this.webSocketServer.clients, this.getStats(stats)); } this.stats = stats; } );

setupDevMiddleware

setupDevMiddleware 函数返回结果是 express 标准的 middleware 用于处理浏览器静态资源的请求。执行过程中显示初始化了一个 context 对象,默认非 lazy 模式,开启了 webpackwatch 模式开始启动编译。

然后将 compiler 的原来基于 fs 模块的 outputFileSystem 替换成 memory-fs模块的实例。memory-fs 是实现了 nodefs api 的基于内存的 fileSystem,这意味着 webpack 编译后的资源不会被输出到硬盘而是内存。最后将真正处理请求的 middleware 返回装载在 express 上。

// 启动webpack编译代码 context.compiler.watch(watchOptions, errorHandler); // 将webpack打包文件改成写入内存 outputFileSystem = memfs.createFsFromVolume(new memfs.Volume()); // 不同钩子注册回调 context.compiler.hooks.watchRun.tap("webpack-dev-middleware", invalid); context.compiler.hooks.invalid.tap("webpack-dev-middleware", invalid); context.compiler.hooks.done.tap("webpack-dev-middleware", done);

更新

上面就是npm run start把项目跑起来经历的过程,接下来就是我们对项目代码进行开发后实现视图更新了。

因为webpack-dev-server使用的是webpackwatch模式进行的编译,当我们更新了代码后,webpack是能够监听到代码变化的,代码变化后,webpack会再次将我们的项目代码进行打包编译,编译完成后,就会触发done钩子函数了。

在上面初始化的时候,我们是在done钩子上挂载了回调的。

  1. 是上面setupHooks里的websocketServer对客户端进行消息广播,通知客户端项目代码有更新了。
this.compiler.hooks.done.tap( "webpack-dev-server", (stats) => { if (this.webSocketServer) { // 给客户端发消息,包括更新类型,状态,hash等 this.sendStats(this.webSocketServer.clients, this.getStats(stats)); } this.stats = stats; } );
  1. 当客户端接收到websocket广播的消息后,会触发reloadApp方法(webpack打包时注入进去的),reloadApp会根据广播消息里的更新类型选择是页面更新liveReload还是模块更新hotReload

  2. 在客户端更新页面时,会去请求类似c390bbe0037a0dd079a6.hot-update.jsonmain.c390bbe0037a0dd079a6.hot-update.js这样的两个文件,这两个文件是webpack 使用了 HotModuleReplacementPlugin 编译时,每次增量编译就会多产出的两个文件, 分别是描述 chunk 更新的 manifest文件和更新过后的 chunk 文件。

  3. 拿到这两个增量文件后,再去请求express服务器去获取最新编译打包的bundle.js

  4. 根据更新类型,选择是两个增量文件和bundle.js比对局部更新还是页面更新。