Vitest 源码解读(一)———— vite-node
vite-node 是一个 npm 库,用于在node 环境中使用 vite, 这个库在 vitest 中使用,因此了解 vite-node 会更有助于理解 vitest。
vite-node
既然在 node 环境中应用,那么客户端就不再是浏览器,而是 node 端,所以 vite-node 主要是提供了一个可以运行在 node 侧的客户端环境,即 ViteNodeRunner, 使客户端和server端可以在同一 node 环境上运行,接下来我们来看一下 viteNodeRunner 是如何实现的:
class ViteNodeRunner {
// 在浏览器中, 会直接加载模块,但在 node 中,需要手动加载模块
async executeFile(file: string) {
return await this.directRequest(file, [])
}
async directRequest(id, fsPath, callstack) {
// 调用 server 的 fetchModule 函数,用于转换源码,转换逻辑与 vite 相同
const { code: transformed } = await this.options.fetchModule(id)
// 构建上下文对象
const exports = {}
const context = this.prepareContext({
// esm transformed by Vite
__vite_ssr_import__: directRequest,
__vite_ssr_dynamic_import__: directRequest,
__vite_ssr_exports__: exports,
__vite_ssr_exportAll__: (obj: any) => exportAll(exports, obj),
__vite_ssr_import_meta__: { url },
// cjs compact
require: createRequire(url),
exports,
module: {
set exports(value) {
exportAll(exports, value)
exports.default = value
},
get exports() {
return exports.default
},
},
__filename,
__dirname: dirname(__filename),
})
// 在指定的上下文中执行 JavaScript 代码
const fn = vm.runInThisContext(`async (${Object.keys(context).join(',')})=>{{${transformed}\n}}`, {
filename: fsPath,
lineOffset: 0,
})
await fn(...Object.values(context))
return exports
}
}
vite-node 对原 server 的逻辑也进行了封装,具体见:
class ViteNodeServer {
// 钩子函数,用于转换源码
async fetchModule(id: string) {
const r = await this.transformRequest(id)
return { code: r?.code }
}
private async transformRequest(id: string) {
//直接调用 vite 的 transformRequest 函数
return await this.server.transformRequest(id, { ssr: true })
}
}
定义完 ViteNodeRunner 和 ViteNodeServer 之后,我们就可以在 node 环境中使用 vite 了。使用方式示例见:
import { createServer } from 'vite'
const server = await createServer({})
await server.pluginContainer.buildStart({})
const node = new ViteNodeServer(server)
const runner = new ViteNodeRunner({
root: server.config.root,
base: server.config.base,
// 作为参数传入
fetchModule(id) {
return node.fetchModule(id)
},
})
// 执行
for (const file of files)
await runner.executeFile(file)
await server.close()
vitest 中是如何接入 vite-node 的
在 vitest 中,需要多线程处理测试用例,因此需要考虑worker 线程和主线程的通信,那么结合 vite-node, vitest 是如何处理的呢?
整体流程如下, 这部分逻辑在 vitest 类的 start 函数中:
// 创建 server
// 在 config 中配置的 VitestPlugin 插件中初始化 ViteNodeServer
const server = await createServer(config)
await server.pluginContainer.buildStart({})
// 执行单测文件
await this.runFiles(files)
runFiles 函数是核心实现:
async function runFiles(files) {
// 先等待当前正在运行的测试用例完成
await this.runningPromise
this.runningPromise = (async() => {
if (!this.pool)
this.pool = createWorkerPool(this)
await this.pool.runTests(files, invalidates)
})()
.finally(() => {
// 测试用例执行完成,重置 runningPromise
this.runningPromise = undefined
})
return await this.runningPromise
}
可以看到,首先需要创建 pool, 这里 vitest 引用了 tinypool 这个库:
function createWorkerPool(ctx) {
// 创建 worker 线程, 指定要运行的文件 worker.js
const pool = new Tinypool({
filename: 'dist/worker.js',
})
const runWithFiles = (): RunWithFiles => {
return async(files) => {
// 针对每一个文件,创建一个 worker 线程
await Promise.all(files.map(async(file) => {
const channel = new MessageChannel()
const port = channel.port2
const workerPort = channel.port1
// 创建 rpc 通信通道
createBirpc({
fetch(id) {
// ViteNodeServer 提供的处理文件的方法,可供 worker 线程调用
return ctx.vitenode.fetchModule(id)
},
},{
post: v => port.postMessage(v),
on: fn => port.addListener('message', fn),
})
const data = {
port: workerPort,
config: ctx.config,
files: [file],
}
// 在工作池中,根据传入的上下文执行 worker.js 文件 的 run 函数,使用 workerPort 传递数据。
await pool.run(data, { transferList: [workerPort], 'run' })
port.close()
workerPort.close()
}))
}
}
return {
runTests: runWithFiles(),
}
}
下面我们来看一下具体在 worker.js 中,是如何执行测试用例的:
export async function run(ctx) {
init(ctx)
const { run } = await startViteNode(ctx)
return run(ctx.files, ctx.config)
}
function init(ctx) {
// 每一个 worker 线程有自己独立的 process 对象,初始化一些上下文信息
process.__vitest_worker__ = {
ctx,
// 创建 rpc 通道
rpc: createBirpc<WorkerRPC>(
{},
{
eventNames: ['onUserConsoleLog', 'onCollected', 'onWorkerExit'],
post(v) { ctx.port.postMessage(v) },
on(fn) { ctx.port.addListener('message', fn) },
},
),
}
}
function startViteNode(options) {
// 初始化 VitestRunner 对象
const runner = new VitestRunner({
fetchModule(id) {
// 通过 rpc 通道,调用 server 端提供的 fetch 方法
return process.__vitest_worker__.rpc.fetch(id)
},
})
// 执行 entry.js 文件
return await runner.executeFile('entry.js')
}
我们再继续看一下 entry.js 文件的实现, 该函数用于具体执行测试用例:
async function run(files, config) {
for (const file of files) {
// 收集测试用例
const files = await collectTests([file], config)
// 执行测试用例
await runSuites(files)
}
}
具体的执行测试用例的源码,将在下一节说明。