前言
距离尤大大在微博宣布「vite」的出现,不知不觉间已经过了 5 个月多。
当时,「vite」只是支持对 .vue 文件的即时编译和 import
的 rewrite
,相应地「Plugin」也没有几个。并且,最初在「GitHub」上「vite」的 slogan 是这样的:
—— No-bundle Dev Server for Vue 3 Single-File Components.
可以看到,起初介绍「vite」是一个不需要打包的开发阶段的服务器。但是,现在再回首,这句 slogan 已经消失了,而「vite」也已经处于 「beta」 阶段。并且,不仅仅是一个开发阶段的服务器这么简单。相应地也实现了很多「Feature」,例如:Web Assembly、JSX 、CSS Pre-processors、Dev Server Proxy 等等。
有兴趣了解这些「Feature」的同学,可以移步GitHub自行阅读
这两个月的时间,「vite」发展的劲头是非(xue)常(bu)猛(dong)的。并且,也出现了很多关于「vite」的文章,可以说是:“ 如雨后春笋般,络绎不绝 ”。
那么,作为一名「Vue」爱好者,我同样对「vite」充满了好奇。所以,回到本次文章,我会先浅析 webpack-dev-server
的「HMR」,然后再循序渐进地讲解「vite」在「HMR」这个过程做了什么。
Webpack 的 HMR 过程
提及「HMR」,不可避免地是会想起现在我们家喻户晓的 webpack-dev-server
中的「HMR」。所以,我们先来了解一番webpack-dev-server
的「HMR」。
首先,我们先对「HMR」建立一个基础的认知。「HMR」 全称即 Hot Module Replacement。相比较「live load」,它具有以下优点:
- 可以实现局部更新,避免多余的资源请求,提高开发效率
- 在更新的时候可以保存应用原有状态
- 在代码修改和页面更新方面,实现所见即所得
而在 webpack-dev-server
中实现「HMR」的核心就是 HotModuleReplacementPlugin
,它是「Webpack」内置的「Plugin」。在我们平常开发中,之所以改一个文件,例如 .vue
文件,会触发「HMR」,是因为在 vue-loader
中已经内置了使用 HotModuleReplacementPlugin
的逻辑。它看起来会是这样
- Helloworld.vue
<template>
<div>hello worlddiv>
template>
<script lang="ts">import { Vue, Component } from 'vue-property-decorator'
@Componentexport default class Helloworld extends Vue() {}script>
- main.js(手动实现「HMR」效果)
import Vue from 'vue'
import HelloWorld from '_c/HelloWorld'
if (module.hot) {
module.hot.accept('_c/HelloWorld', ()=>{
// 拉取更新过的 HelloWorld.vue 文件
})
}
new Vue({
el: '#app',
template: ''
component: { HelloWorld }
})
那么,这个就是 webpack-dev-server
实现「HMR」的本质吗?显然不是,上面说的只是,如果你要通过 webpack-dev-server
实现「HMR」,你可以这么写来实现。
如果究其底层实现,是有两个关键的点:
1.与本地服务器建立「socket」连接,注册 hash
和 ok
两个事件,发生文件修改时,给客户端推送 hash
事件。客户端根据 hash
事件中返回的参数来拉取更新后的文件。
2.HotModuleReplacementPlugin
会在文件修改后,生成两个文件,用于被客户端拉取使用。例如:
hash.hot-update.json
{
"c": {
"chunkname": true
},
"h": "d69324ef62c3872485a2"
}
chunkname.d69324ef62c3872485a2.hot-update.js,这里的 chunkname
即上面 c
中对于 key
。
webpackHotUpdate("main",{
"./src/test.js":
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval(....)
})
})
当然,在这之前还会涉及到对原模块代码的注入,让它具备拉取文件的能力。而这其中实现的细节就不去扣了,要不然有点喧兵夺主的感觉。
有兴趣的同学可以去看看这篇文章一年前,我去面试,小姐姐问我webpack热更新原理,我跟她说了一小时
基于 native ES Module 的 devServer
基于 native ES Module 的 devServer 是「vite」实现「HMR」的重要一环。总体来说,它会做这么两件事:
- 初始化本地服务器
- 加载并执行对应的
Plugin
,例如sourceMapPlugin
、moduleRewritePlugin
、htmlRewritePlugin
等等。
所以,本质上,devServer 做的最重要的一件事就是加载执行 Plugin
。目前,「vite」总共具备了 11 种 Plugin
。
这里大致列举几点 Plugin
会做:
- 拦截请求,处理「ES Module」语法相关的代码,转化为浏览器可识别的「ES Module」语法,例如第三方模块的
import
转化为/@module/vue.js
- 对
.ts
、.vue
进行即时的编译以及sass
或less
的预编译 - 建立模块间的导入导出关系,即
importeeMap
和客户端建立socket
连接,用于实现「HMR」
这里就列举 devServer 几个常见的
Plugin
需要做的事,至于其他像wasmPlugin
、webWorkerPlugin
之类的Plugin
会做些什么,有兴趣的同学可以自行去了解。
然后,我们再从代码地角度看看它是怎么实现我们上述所说的:
1.首先,我们执行 vite
命令.实际上是运行 cli.js 这个文件,这里我摘取了其中核心的逻辑:
(async () => {
const { help, h, mode, m, version, v } = argv
...
const envMode = mode || m || defaultMode
const options = await resolveOptions(envMode)
// 开发环境下,我们会命中 runServer
if (!options.command || options.command === 'serve') {
runServe(options)
} else if (options.command === 'build') {
runBuild(options)
} else if (options.command === 'optimize') {
runOptimize(options)
} else {
console.error(chalk.red(`unknown command: ${options.command}`))
process.exit(1)
}
})()
async function runServe(options: UserConfig) {
// 在 createServer() 的时候会对 HRM、serverConfig 之类的进行初始化
const server = require('./server').createServer(options)
...
}
可以看到,在自执行函数中,我们会命中 runServer()
的逻辑,而它的核心是调用 server.js 文件中的 createServer()
。
createServer
方法:
export function createServer(config: ServerConfig): Server {
const {
...,
enableEsbuild = true
} = config
const app = new Koa()const server = resolveServer(config, app.callback())const watcher = chokidar.watch(root, {ignored: [/\bnode_modules\b/, /\b\.git\b/]
}) as HMRWatcherconst resolver = createResolver(root, resolvers, alias)const context: ServerPluginContext = {
...
watcher
...
}
app.use((ctx, next) => {Object.assign(ctx, context)
ctx.read = cachedRead.bind(null, ctx)return next()
})const resolvedPlugins = [
...,
moduleRewritePlugin,
hmrPlugin,
...
]// 核心逻辑执行 hmrPlugin
resolvedPlugins.forEach((m) => m && m(context))const listen = server.listen.bind(server)
server.listen = (async (port: number, ...args: any[]) => {
...
}) as anyreturn server
}
createServer
方法做了这么几件事:
- 创建一个
koa
实例 - 创建监听除了 node_modules 之外的文件的
watcher
,并传入context
中 - 将
context
上下文传入并调用每一个Plugin
到这里,「vite」的 devServer 的创建过程就已经完成。那么,接下来我们去领略一番属于「vite」的「HMR」过程!
vite 的 HMR 过程
在「vite」中「HMR」的实现是以 serverPluginHmr
这个 Plugin
为核心实现。这里我们以 .vue
文件的修改触发的「HMR」为例,这个过程会涉及三个 Plugin
:serverPluginHtml
、serverPluginHmr
、serverPluginVue
,这个过程看起来会是这样:
serverPluginHtml
从前面的流程图可以看到,首先是 serverPluginHtml
这个 Plugin
向 index.html 中注入了获取 hmr
模块的代码:
export const htmlRewritePlugin: ServerPlugin = ({
root,
app,
watcher,
resolver,
config
}) => {
const devInjectionCode =
`\n
const scriptRE = /(