time: 2022-03-07 17:34:53
author: heyunjiang
vite 核心功能
学习目的
done
vite 是如何编译 ts?调用的 esbuild.transform apidone
vue3 组件是如何被编译的?看看 @vitejs/plugin-vue 如何处理。vue 插件调用了 compiler api,options.compiler.compileTemplate
,也就是调用的 vue/compiler-sfc
编译 apidone
umd, cjs 如何被处理为 esm?esbuild.builddone
vite 插件时如何被调用的?通过实现一个 pluginContainer 来调用每个插件的相关配置为了解决心中的疑问,特此来分析源码实现。来看看核心流程
入口 bin/vite.js, require('../dist/node/cli')
,使用 cac 类似 command 库识别命令,如果是 dev 命令,则会执行如下代码
const { createServer } = await import('./server')
const server = await createServer({})
await server.listen()
createServer 核心流程
export async function createServer(
inlineConfig: InlineConfig = {}
): Promise<ViteDevServer> {
// 1 配置读取:识别 config 配置,生成 `config` 对象
const config = await resolveConfig(inlineConfig, 'serve', 'development')
const httpsOptions = await resolveHttpsConfig()
const middlewares = connect() as Connect.Server
// 2 http 服务、hmr 服务:生成 `httpServer`, `ws` 2个服务器对象
const httpServer = await resolveHttpServer(serverConfig, middlewares, httpsOptions)
const ws = createWebSocketServer(httpServer, config, httpsOptions)
const watcher = chokidar.watch(path.resolve(root), {})
const moduleGraph: ModuleGraph = new ModuleGraph()
// 3 插件容器 container 生成
const container = await createPluginContainer(config, moduleGraph, watcher)
const server: ViteDevServer = {
config,
middlewares,
httpServer,
watcher,
pluginContainer: container,
ws,
moduleGraph,
ssrTransform,
transformWithEsbuild,
transformRequest(url, options) {
return transformRequest(url, server, options)
},
listen(port?: number, isRestart?: boolean) {
return startServer(server, port, isRestart)
},
...
}
// 4 watcher 监听文件变更、新增、删除,实现 hmr
watcher.on('change', async (file) => {})
// 5 加载内置及自定义 http middleware 插件
middlewares.use(...)
// 6 执行 `buildStart` 钩子
await container.buildStart({})
// 7 执行依赖优化
server._optimizeDepsMetadata = await optimizeDeps()
return server
}
hmt client 关键代码
const socket = new WebSocket(`${socketProtocol}://${socketHost}`, 'vite-hmr')
socket.addEventListener('message', async ({ data }) => {
handleMessage(JSON.parse(data))
})
async function handleMessage(payload: HMRPayload) {
switch (payload.type) {
case 'update':
notifyListeners('vite:beforeUpdate', payload)
if (isFirstUpdate && hasErrorOverlay()) {
window.location.reload()
return
} else {
clearErrorOverlay()
isFirstUpdate = false
}
payload.updates.forEach((update) => {
if (update.type === 'js-update') {
queueUpdate(fetchUpdate(update))
} else {
let { path, timestamp } = update
path = path.replace(/\?.*/, '')
const el = Array.from(
document.querySelectorAll<HTMLLinkElement>('link')
).find((e) => e.href.includes(path))
if (el) {
const newPath = `${base}${path.slice(1)}${
path.includes('?') ? '&' : '?'
}t=${timestamp}`
el.href = new URL(newPath, el.href).href
}
console.log(`[vite] css hot updated: ${path}`)
}
})
break
}
}
let pending = false
let queued: Promise<(() => void) | undefined>[] = []
async function queueUpdate(p: Promise<(() => void) | undefined>) {
queued.push(p)
if (!pending) {
pending = true
await Promise.resolve()
pending = false
const loading = [...queued]
queued = []
;(await Promise.all(loading)).forEach((fn) => fn && fn())
}
}
hmr 归纳总结
总结归纳,在初始创建 server 期间,做了如下事情
依赖预构建大致代码
import { init, parse } from 'es-module-lexer'
import { build } from 'esbuild'
export async function optimizeDeps(
config: ResolvedConfig,
force = config.server.force,
asCommand = false,
newDeps?: Record<string, string>, // missing imports encountered after server has started
ssr?: boolean
) {
const dataPath = path.join(cacheDir, '_metadata.json')
// 1 创建 node_modules/.vite 缓存目录
fs.mkdirSync(cacheDir, { recursive: true })
// 添加 package.json 目的是让缓存中的文件都被识别为 esm
writeFile(
path.resolve(cacheDir, 'package.json'),
JSON.stringify({ type: 'module' })
)
// 2 依赖对象生成:通常识别是从 node_modules 中引入的依赖,也可以通过 optimizeDeps 配置
let ({ deps, missing } = await scanImports(config))
const flatIdDeps: Record<string, string> = {}
await init
// 3 分析依赖是否有 es export,使用 hasReExports 标识
for (const id in deps) {
const flatId = flattenId(id)
const filePath = (flatIdDeps[flatId] = deps[id])
const entryContent = fs.readFileSync(filePath, 'utf-8')
let exportsData: ExportsData
try {
exportsData = parse(entryContent) as ExportsData
} catch {}
for (const { ss, se } of exportsData[0]) {
const exp = entryContent.slice(ss, se)
if (/export\s+\*\s+from/.test(exp)) {
exportsData.hasReExports = true
}
}
idToExports[id] = exportsData
flatIdToExports[flatId] = exportsData
}
const start = performance.now()
// 4 使用 esbuild.build 构建依赖为 esm
const result = await build({
absWorkingDir: process.cwd(),
entryPoints: Object.keys(flatIdDeps),
bundle: true,
format: 'esm',
target: config.build.target || undefined,
external: config.optimizeDeps?.exclude,
logLevel: 'error',
splitting: true,
sourcemap: true,
outdir: cacheDir,
ignoreAnnotations: true,
metafile: true,
})
const meta = result.metafile!
const cacheDirOutputPath = path.relative(process.cwd(), cacheDir)
// 5 构建 server._optimizeDepsMetadata 对象
for (const id in deps) {
const entry = deps[id]
data.optimized[id] = {
file: normalizePath(path.resolve(cacheDir, flattenId(id) + '.js')),
src: entry,
needsInterop: needsInterop(
id,
idToExports[id],
meta.outputs,
cacheDirOutputPath
)
}
}
// 6 写入缓存
writeFile(dataPath, JSON.stringify(data, null, 2))
debug(`deps bundled in ${(performance.now() - start).toFixed(2)}ms`)
return data
}
归纳分析
问题归纳
接下来,分析代码 transform 流程
前面总结到了启动服务器之前,会执行 optimizeDeps 依赖预构建。 那么实际需要文件是在什么时候被 transform 的呢?以及导入是在哪里重写的呢?
在 createServer 流程中,前面知道了它会应用系列内置 middleware,其中有一个 middlewares.use(transformMiddleware(server))
。
粗略按照 koa middleware 的理解,在每次 http 请求时,会应用一次 transformMiddleware 返回的中间件
发现一:Promise.race 应用场景,请求超时,实际请求和超时配置只要有其中一个成功就行
await Promise.race([
server._pendingReload,
new Promise((_, reject) =>
setTimeout(reject, NEW_DEPENDENCY_BUILD_TIMEOUT)
)
])
transformMiddleware 是在 server/middleware 内
通过断点调试分析,transformMiddleware 内部调用了 transformRequest
函数,来看看它的核心实现
export function transformMiddleware(
server: ViteDevServer
) {
return async function viteTransformMiddleware(req, res, next) {
const result = await transformRequest(url, server, {
html: req.headers.accept?.includes('text/html')
})
return send(req, res, result.code, type, ...)
}
}
export function transformRequest(
url: string,
server: ViteDevServer,
options: TransformOptions = {}
): Promise<TransformResult | null> {
return doTransform(url, server, options)
}
async function doTransform(
url: string,
server: ViteDevServer,
options: TransformOptions
) {
let code, map
const loadResult = await pluginContainer.load(id, { ssr }) // id 为 main.ts url,通常 loadResult 为 null
if (loadResult == null) {
code = await fs.readFile(file, 'utf-8')
}
// code 是文件源码,transformResult 是编译后的文件了
const transformResult = await pluginContainer.transform(code, id, {
inMap: map,
ssr
})
code = transformResult.code!
map = transformResult.map
return (mod.transformResult = {
code,
map,
etag: getEtag(code, { weak: true })
} as TransformResult)
}
归纳分析
import { createApp } from 'vue'
会被重写为 import { createApp } from "/node_modules/.vite/vue.js?v=5f6d4d65";
问题:pluginContainer.transform 是怎么调用的?已经知道 @vitejs/plugin-vue 提供了 transform 配置,猜想是被插件容器顺序调用的,这里找寻一下
pluginContainer 核心源码
const container: PluginContainer = {
async transform(code, id, options) {
const ctx = new TransformContext(id, code, inMap as SourceMap)
for (const plugin of plugins) {
let result: TransformResult | string | undefined
try {
result = await plugin.transform.call(ctx as any, code, id, { ssr })
} catch (e) {
ctx.error(e)
}
code = result
}
return {
code,
map: ctx._getCombinedSourcemap()
}
},
}
归纳分析:pluginContainer.transform 是顺序调用插件的各种配置方法,猜想正确
扩展学习:ts, jsx, json 等文件是如何 transform 的呢?
在执行 vite 命令时,默认启动 dev 命令,此刻会做如下事情
通过前面学习 dev 运行流程时,发现对各种资源文件的处理都是通过插件来实现,vite 内部实现了一个 pluginContainer 来调用每个插件
在解决 vue3 编译问题,想要实现类似 ast 解析处理特定代码问题,就了解了 vite 如何编译 vue 的。
vue 组件的编译,是通过 @vitejs/plugin-vue 实现的。
load(id, opt) {
const { filename, query } = parseVueRequest(id)
if (query.vue) {
if (query.src) {
return fs.readFileSync(filename, 'utf-8')
}
const descriptor = getDescriptor(filename, options)!
let block: SFCBlock | null | undefined
if (query.type === 'script') {
// handle <scrip> + <script setup> merge via compileScript()
block = getResolvedScript(descriptor, ssr)
} else if (query.type === 'template') {
block = descriptor.template!
} else if (query.type === 'style') {
block = descriptor.styles[query.index!]
} else if (query.index != null) {
block = descriptor.customBlocks[query.index]
}
if (block) {
return {
code: block.content,
map: block.map as any
}
}
}
},
transform(code, id, opt) {
const { filename, query } = parseVueRequest(id)
// sub block request
const descriptor = query.src
? getSrcDescriptor(filename, query)!
: getDescriptor(filename, options)!
return transformMain(code, filename, options, this, ssr, customElementFilter(filename));
}
在查看这个插件源码时,发现它提供了 vite 插件要求的几个配置:buildStart, load, transform 等,先对 vite 插件做个基础学习总结
通用钩子: 在 build 时直接使用 rollup,在 dev 时创建插件容器调用 rollup 钩子。咋理解?
vite 特有钩子