webpack-dev-server 启动本地服务
我们根据 webpack-dev-server
的 package.json
中的 bin
命令,可以找到命令的入口文件 bin/webpack-dev-server.js
// node_modules/webpack-dev-server/bin/webpack-dev-server.js
// 生成webpack编译主引擎 compiler
let compiler = webpack(config)
// 启动本地服务
let server = new Server(compiler, options, log)
server.listen(options.port, options.host, (err) => {
if (err) {
throw err
}
})
本地服务代码
// node_modules/webpack-dev-server/lib/Server.js
class Server {
constructor() {
this.setupApp()
this.createServer()
}
setupApp() {
// 依赖了express
this.app = new express()
}
createServer() {
this.listeningApp = http.createServer(this.app)
}
listen(port, hostname, fn) {
return this.listeningApp.listen(port, hostname, (err) => {
// 启动express服务后,启动websocket服务
this.createSocketServer()
})
}
}
这一小节代码主要做了三件事:
- 启动
webpack
,生成compiler
实例。 - 使用
express
框架启动本地server
,让浏览器可以请求本地的静态资源。 - 本地
server
启动之后,再去启动websocket
服务,通过websocket
,可以建立本地服务和浏览器的双向通信。
在启动服务前执行了一些特殊操作,具体在 webpack-dev-server/lib/Server.js
修改 webpack.config.js 的 entry 配置
启动本地服务前,调用了 updateCompiler(this.compiler)
方法。这个方法中有 2 段关键性代码。一个是获取 websocket
客户端代码路径,另一个是根据配置获取 webpack
热更新代码路径。
// 获取websocket客户端代码
const clientEntry = `${require.resolve(
'../../client/'
)}?${domain}${sockHost}${sockPath}${sockPort}`
// 根据配置获取热更新代码
let hotEntry
if (options.hotOnly) {
hotEntry = require.resolve('webpack/hot/only-dev-server')
} else if (options.hot) {
hotEntry = require.resolve('webpack/hot/dev-server')
}
修改后的 webpack
入口配置相当于下面这样:
{
entry: {
index: [
// 上面获取的clientEntry
'xxx/node_modules/webpack-dev-server/client/index.js?<http://localhost:8080>',
// 上面获取的hotEntry
'xxx/node_modules/webpack/hot/dev-server.js',
// 开发配置的入口
'./src/index.js'
]
}
}
自动在入口增加了 2 个文件:
node_modules/webpack-dev-server/client/index.js
这个文件用于 websocket
的,因为 websoket
是双向通信,我们在第 1 步 webpack-dev-server
初始化的过程中,启动的是本地服务端的 websocket
。但浏览器还没有和服务端通信的代码,因此需要把 websocket
客户端通信代码添加到入口文件中。
node_modules/webpack/hot/dev-server.js
该文件主要是用于检查更新逻辑
监听 webpack 编译结束
修改好入口配置后,调用了 setupHooks
方法。 该方法是用来注册监听事件的,监听每次 webpack
编译完成。
// 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;
});
};
当监听到一次 webpack
编译结束,就会调用 _sendStats
方法通过 websocket
给浏览器发送通知,ok
和hash
事件,这样浏览器就可以拿到最新的hash
值了,做检查更新逻辑。
// 通过websoket给客户端发消息
_sendStats() {
this.sockWrite(sockets, 'hash', stats.hash);
this.sockWrite(sockets, 'ok');
}
webpack 监听文件变化
每次修改代码,就会触发编译。说明还需要监听本地代码的变化,主要是通过 setupDevMiddleware
方法实现的。
这个方法主要使用了webpack-dev-middleware
库
// node_modules/webpack-dev-middleware/index.js
compiler.watch(options.watchOptions, (err) => {
if (err) {
/*错误处理*/
}
})
// 通过“memory-fs”库将打包后的文件写入内存
setFs(context, compiler)
- 调用了
compiler.watch
方法, 该方法主要做了两件事- 对本地文件代码进行编译打包。
- 编译结束后,开启对本地文件的监听,当文件发生变化,重新编译,编译完成之后继续监听。(监听本地文件的变化主要是通过文件的生成时间是否有变化)
- 执行
setFs
方法,这个方法主要目的就是将编译后的文件打包到内存。使用的memory-fs
库。
浏览器接收到热更新的通知
当文件发生变化,就触发重新编译。同时还监听了每次编译结束的事件。当监听到一次 webpack
编译结束,_sendStats
方法就通过websoket
给浏览器发送通知,检查下是否需要热更新。
浏览器是接收到 websocket
的消息主要是通过一开始编译时添加的入口文件
'xxx/node_modules/webpack-dev-server/client/index.js?<http://localhost:8080>'
这个文件的代码会被打包到 bundle.js
中,运行在浏览器中。来看下这个文件的核心代码吧。
// webpack-dev-server/client/index.js
var socket = require('./socket')
var onSocketMessage = {
hash: function hash(_hash) {
// 更新currentHash值
status.currentHash = _hash
},
ok: function ok() {
sendMessage('Ok')
// 进行更新检查等操作
reloadApp(options, status)
}
}
// 连接服务地址socketUrl,?<http://localhost:8080>,本地服务地址
socket(socketUrl, onSocketMessage)
function reloadApp() {
if (hot) {
log.info('[WDS] App hot update...')
// hotEmitter是EventEmitter的实例
var hotEmitter = require('webpack/hot/emitter')
hotEmitter.emit('webpackHotUpdate', currentHash)
}
}
socket
方法建立了websocket
和服务端的连接,并注册了 2 个监听事件。
hash
事件,更新最新一次打包后的 hash 值。ok
事件,进行热更新检查
热更新检查事件是调用reloadApp
方法。
这就需要上面提到的第二个入口文件
'xxx/node_modules/webpack/hot/dev-server.js'
文件代码如下
// node_modules/webpack/hot/dev-server.js
var check = function check() {
module.hot
.check(true)
.then(function (updatedModules) {
// 容错,直接刷新页面
if (!updatedModules) {
window.location.reload()
return
}
// 热更新结束,打印信息
if (upToDate()) {
log('info', '[HMR] App is up to date.')
}
})
.catch(function (err) {
window.location.reload()
})
}
var hotEmitter = require('./emitter')
hotEmitter.on('webpackHotUpdate', function (currentHash) {
lastHash = currentHash
check()
})
这里 webpack
监听到了 webpackHotUpdate
事件,并获取最新的hash
值,然后终于进行检查更新了。检查更新调用的是module.hot.check
方法。该方法来自于HotModuleReplacementPlugin
HotModuleReplacementPlugin
配置热更新和不配置时 bundle.js
的区别。
-
配置了
HotModuleReplacementPlugin
或--hot
的 -
没有配置
可以看到 moudle
新增了一个属性为 hot
,再看 bundle.js
中 hotCreateModule
方法
所以上面提到的 module.hot.check
其实就是 hotCheck
方法
moudle.hot.check 开始热更新
module.hot.check
方法执行
-
利用上一次保存的
hash
值,调用hotDownloadManifest
发送xxx/hash.hot-update.json
的ajax
请求; -
请求结果获取热更新模块,以及下次热更新的
Has
h 标识,并进入热更新准备阶段。hotAvailableFilesMap = update.c // 需要更新的文件
hotUpdateNewHash = update.h // 更新下次热更新hash值
hotSetStatus('prepare') // 进入热更新准备状态 -
调用
hotDownloadUpdateChunk
发送xxx/hash.hot-update.js
请求,通过JSONP
方式。
function hotDownloadUpdateChunk(chunkId) {
var script = document.createElement('script')
script.charset = 'utf-8'
script.src =
__webpack_require__.p +
'' +
chunkId +
'.' +
hotCurrentHash +
'.hot-update.js'
if (null) script.crossOrigin = null
document.head.appendChild(script)
}
请求到的hash.hot-update.js
的如下
可以看到,新编译后的代码是在一个webpackHotUpdate
函数体内部的。也就是要立即执行webpackHotUpdate
这个方法。
webpackHotUpdate
方法
window['webpackHotUpdate'] = function (chunkId, moreModules) {
hotAddUpdateChunk(chunkId, moreModules)
}
hotAddUpdateChunk
方法会把更新的模块moreModules
赋值给全局全量hotUpdate
。hotUpdateDownloaded
方法会调用hotApply
进行代码的替换。
function hotAddUpdateChunk(chunkId, moreModules) {
// 更新的模块moreModules赋值给全局全量hotUpdate
for (var moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
hotUpdate[moduleId] = moreModules[moduleId]
}
}
// 调用hotApply进行模块的替换
hotUpdateDownloaded()
}
hotApply 热更新模块替换
热更新的核心逻辑就在hotApply
方法了。
- 删除过期的模块
var queue = outdatedModules.slice()
while (queue.length > 0) {
moduleId = queue.pop()
// 从缓存中删除过期的模块
module = installedModules[moduleId]
// 删除过期的依赖
delete outdatedDependencies[moduleId]
// 存储了被删掉的模块id,便于更新代码
outdatedSelfAcceptedModules.push({
module: moduleId
})
}
- 将新的模块添加到
modules
中
appliedUpdate[moduleId] = hotUpdate[moduleId]
for (moduleId in appliedUpdate) {
if (Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {
modules[moduleId] = appliedUpdate[moduleId]
}
}
- 通过
__webpack_require__
执行相关模块的代码
for (i = 0; i < outdatedSelfAcceptedModules.length; i++) {
var item = outdatedSelfAcceptedModules[i]
moduleId = item.module
try {
// 执行最新的代码
__webpack_require__(moduleId)
} catch (err) {
// ...容错处理
}
}
总结
HRM 总流程图
代码详细执行流程图