距离尤大大在微博宣布「vite」的出现,不知不觉间已经过了 2 个月多。
当时,「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」这个过程做了什么。
提及「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
的逻辑。它看起来会是这样
<template>
<div>hello worlddiv>
template>
<script lang="ts">
import { Vue, Component } from 'vue-property-decorator'
@Component
export default class Helloworld extends Vue() {}
script>
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(....)
})
})
当然,在这之前还会涉及到对原模块代码的注入,让它具备拉取文件的能力。而这其中实现的细节就不去扣了,要不然有点喧兵夺主的感觉。
基于 native ES Module 的 devServer 是「vite」实现「HMR」的重要一环。总体来说,它会做这么两件事:
Plugin
,例如 sourceMapPlugin
、moduleRewritePlugin
、htmlRewritePlugin
等等。所以,本质上,devServer 做的最重要的一件事就是加载执行 Plugin
。目前,「vite」总共具备了 11 种 Plugin
。
这里大致列举几点 Plugin
会做:
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<State, Context>()
const server = resolveServer(config, app.callback())
const watcher = chokidar.watch(root, {
ignored: [/\bnode_modules\b/, /\b\.git\b/]
}) as HMRWatcher
const 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 any
return server
}
createServer
方法做了这么几件事:
koa
实例watcher
,并传入 context
中context
上下文传入并调用每一个 Plugin
到这里,「vite」的 devServer 的创建过程就已经完成。那么,接下来我们去领略一番属于「vite」的「HMR」过程!
在「vite」中「HMR」的实现是以 serverPluginHmr
这个 Plugin
为核心实现。这里我们以 .vue
文件的修改触发的「HMR」为例,这个过程会涉及三个 Plugin
:serverPluginHtml
、serverPluginHmr
、serverPluginVue
,这个过程看起来会是这样:
从前面的流程图可以看到,首先是 serverPluginHtml
这个 Plugin
向 index.html 中注入了获取 hmr
模块的代码:
export const htmlRewritePlugin: ServerPlugin = ({
root,
app,
watcher,
resolver,
config
}) => {
const devInjectionCode =
`\n\n`
const scriptRE = /(