webpack-dev-server 一些设计点

整体流程

webpack-dev-server 通过 express 构建服务,以下是它的整体流程:

  1. validateOptions:使用 schema-utils 校验配置选项。
  2. normalizeOptions:处理选项,混入默认配置。
  3. updateCompiler:酌情为 webpack 添加 HotModuleReplacementPlugin 插件,socket 通信客户端脚本。
  4. 将选项内容赋值给 Server 实例。
  5. 绑定 webpack 钩子,在编译结束后通过 socket 通信将编译信息发送到客户端。
  6. 创建 express 实例,挂载 webpack-dev-middleware 中间件。
  7. 应用 features 特性,参见下文。
  8. 启动 express 服务。

features 特性

webpack-dev-server 首先通过封装 features 特性添加操作集合,然后根据 options 选项将本次启用的特性添加到 runnableFeatures 特性列表中,最后依次执行特性添加操作函数,为 express 服务添加特性。关于代理,可参阅 http-proxy-middleware 源码解读

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
class Server {
setupFeatures() {
const features = {
compress: () => {// 压缩静态资源
if (this.options.compress) {
this.setupCompressFeature();
}
},
proxy: () => {// 代理
if (this.options.proxy) {
this.setupProxyFeature();
}
},
historyApiFallback: () => {// history 跳转
if (this.options.historyApiFallback) {
this.setupHistoryApiFallbackFeature();
}
},
contentBaseFiles: () => {// 静态资源
this.setupStaticFeature();
},
contentBaseIndex: () => {// 静态资源目录服务
this.setupServeIndexFeature();
},
watchContentBase: () => {// 监测静态资源
this.setupWatchStaticFeature();
},
before: () => {// pre-action
if (typeof this.options.before === 'function') {
this.setupBeforeFeature();
}
},
middleware: () => {// 应用 webpack-dev-middleware
this.setupMiddleware();
},
after: () => {// post-action
if (typeof this.options.after === 'function') {
this.setupAfterFeature();
}
},
headers: () => {// 变更响应头
this.setupHeadersFeature();
},
magicHtml: () => {// 为 js 资源添加访问页面
this.setupMagicHtmlFeature();
},
};

const runnableFeatures = [];

if (this.options.compress) {
runnableFeatures.push('compress');
}

runnableFeatures.push('before', 'headers', 'middleware');

if (this.options.proxy) {
runnableFeatures.push('proxy', 'middleware');
}

if (this.options.contentBase !== false) {
runnableFeatures.push('contentBaseFiles');
}

if (this.options.historyApiFallback) {
runnableFeatures.push('historyApiFallback', 'middleware');

if (this.options.contentBase !== false) {
runnableFeatures.push('contentBaseFiles');
}
}

this.serveIndex = this.serveIndex || this.serveIndex === undefined;

if (this.options.contentBase && this.serveIndex) {
runnableFeatures.push('contentBaseIndex');
}

if (this.options.watchContentBase) {
runnableFeatures.push('watchContentBase');
}

runnableFeatures.push('magicHtml');

if (this.options.after) {
runnableFeatures.push('after');
}

(this.options.features || runnableFeatures).forEach((feature) => {
features[feature]();
});
}
}

socket 通信

webpack-dev-server 根据 options.transportMode 选项分别采用 sockjs, websocket 或用户自定义实现类通信。sockjs 是一种实现,它首先会尝试使用 websocket 协议通信;在不支持 websocket 协议的浏览器中,它会降级采用长轮询的方式进行通信。webpack-dev-server 的服务端整体处理流程为(客户端需要添加对应的 socket 脚本):

  1. 根据 options.transportMode.server 选取 SocketServer 的实现类。
  2. 在 express 服务启动期间,创建 SocketServer 实例,并监听事件的方式收集每个连接 app.sockets = [connection]。
  3. 通过 app.sockWrite 为每个连接 connection 推送消息。

使用 socket 通信的场景主要有以下几类:

  • webpack 编译 stats,包含日志级别信息。
  • overlay 页面显示编译错误。
  • 通过 webpack.ProgressPlugin 获得的编译进度。
  • hot 热更新,包含 liveReload 页面是否刷新标识。
  • 通过 chokidar 监测 contentBase 静态资源更新(小提示:webpack-dev-server 通过 serve-index 为静态资源提供目录信息)。