- Published on
初识 vite 原理,vite 是如何启动项目的
- Authors
- Name
- 两万焦
Tags
我们使用 vite 的时候,只需要在 package.json 中定义一个简单的命令,就可以启动项目,那么这个简单的命令,是如何启动 vite 项目的呢,下面我们来详细介绍一下
"scripts": {
"dev": "vite"
},
执行命令行脚本
在执行 pnpm install
命令的时候,node_modules/.bin
下会创建多个命令行脚本。当执行 vite 命令的时候,就会找到 .bin
目录的下的 vite 脚本,从 vite 脚本中可以看到,执行的是 vite/bin/vite.js
文件
vite.js 中的核心内容就是执行了 start
方法,动态引入了 ../dist/node/cli.js
,这个地址是打包后的地址,在 vite 源码中,脚本地址在 packages/vite/src/node/cli.ts
function start() {
return import('../dist/node/cli.js')
}
start()
cli.ts 的核心功能是解析命令行参数并启动本地项目,解析命令行参数通过 cac 库,这里我们主要看启动本地项目的命令。在 cac 库中,通过 command
定义基础命令,通过 alias
方法定于命令别名,通过 option
方法定义命令行参数,最后通过 action
方法执行具体的操作
const cli = cac('vite')
cli
.command('[root]', 'start dev server') // default command
.alias('serve') // the command is called 'serve' in Vite's API
.alias('dev') // alias to align with the script name
.option('--host [host]', `[string] specify hostname`)
.option('--port <port>', `[number] specify port`)
.option('--https', `[boolean] use TLS + HTTP/2`)
.option('--open [path]', `[boolean | string] open browser on startup`)
.option('--cors', `[boolean] enable CORS`)
.option('--strictPort', `[boolean] exit if specified port is already in use`)
.option('--force', `[boolean] force the optimizer to ignore the cache and re-bundle`)
.action()
在 action
方法中,最核心的部分就是引入了 createServer
方法,通过 listen
启动本地 server
cli.action(async (/* 入参数 */) => {
filterDuplicateOptions(options)
// 核心:启动本地 server
const { createServer } = await import('./server')
try {
const server = await createServer({
root,
base: options.base,
mode: options.mode,
configFile: options.config,
logLevel: options.logLevel,
clearScreen: options.clearScreen,
optimizeDeps: { force: options.force },
server: cleanOptions(options),
})
await server.listen()
// 控制台输出本地 server 启动结果
const info = server.config.logger.info
server.printUrls()
// 定义控制台的操作快捷键
bindShortcuts(server, {})
} catch (e) {
process.exit(1)
}
})
这里我们小结一下,在输入命令启动项目时,通过寻找 node_module/.bin
目录下的 vite 脚本,再执行 cli.ts 文件中引入的 createServer
方法启动本地项目
接下来我们来分析核心的 createServer
方法
createServer
createServer
方法有八个核心步骤,如下图所示,每个步骤展开来都很可以很深入的分析,所以我计划在这篇文章中先整体介绍实现过程,在后续的文章中再深入的分析几个重要步骤的实现原理
第一步:配置参数解析
参数解析涉及三个部分
resolveConfig
方法解析 vite 核心配置,包括来自命令行、vite.config 文件的配置参数resolveHttpsConfig
方法解析 https 相关的配置,用来在开发环境模拟 httpsresolveChokidarOptions
方法解析 chokidar 相关配置,主要和监听文件变动相关
const config = await resolveConfig(inlineConfig, 'serve')
const { root, server: serverConfig } = config
const httpsOptions = await resolveHttpsConfig(config.server.https)
const { middlewareMode } = serverConfig
const resolvedWatchOptions = resolveChokidarOptions(config, {
disableGlobbing: true,
...serverConfig.watch,
})
第二步:创建 HTTP 和 WebSocket server
这一步通过 createHttpServer
创建一个 HTTP 服务器实例,根目录的 index.html 就是服务器的入口
通过 createWebSocketServer
创建 WebSocket 服务器,主要是用于实现热更新(HMR),当代码发生变化时,服务器通过 WebSocket 向客户端发送更新通知
const httpServer = middlewareMode
? null
: await resolveHttpServer(serverConfig, middlewares, httpsOptions)
const ws = createWebSocketServer(httpServer, config, httpsOptions)
if (httpServer) {
setClientErrorHandler(httpServer, config.logger)
}
第三步:启动 chokidar 启动监听文件
chokidar 能够创建一个文件监听器,监听文件和目录的变化,实时地响应文件的增删改操作,也是用于实现热更新功能
const watcher = chokidar.watch(
[root, ...config.configFileDependencies, config.envDir],
resolvedWatchOptions
) as FSWatcher
// 文件变化操作
watcher.on('change', async (file) => {
file = normalizePath(file)
moduleGraph.onFileChange(file)
await onHMRUpdate(file, false)
})
// 文件新增和删除操作
watcher.on('add', onFileAddUnlink)
watcher.on('unlink', onFileAddUnlink)
第四步:创建 ModuleGraph 实例
第四步通过 ModuleGraph
class 创建一个模块依赖图实例,模块依赖图主要用于维护各个模块之间的依赖关系,主要有两个用处
- 热更新过程中,通过模块依赖图获取所有相关依赖,保证正确完整的实现热更新
- 打包过程中,根据模块之间的依赖关系进行优化,比如将多个模块合并为一个请求、按需加载模块等,提高打包速度和加载性能
const moduleGraph: ModuleGraph = new ModuleGraph((url, ssr) =>
container.resolveId(url, undefined, { ssr })
)
第五步:创建插件容器
通过 createPluginContainer
方法创建插件容器,插件容器主要有三个功能
- 管理的插件的生命周期
- 在插件之间传递上下文对象,上下文对象包含 vite 的内部状态和配置信息,这样插件通过上下文对象就能访问和修改 vite 内部状态
- 根据插件的钩子函数,在特定的时机执行插件
const container = await createPluginContainer(config, moduleGraph, watcher)
第六步:定义 ViteDevServer 对象
ViteDevServer 对象就是 createServer
方法最终返回的对象,主要包含前几步创建的对象实例和启动 server 相关的核心方法
其中比较特殊的是 createDevHtmlTransformFn
方法,这个方法用于在开发环境下转换 index.html 文件,默认注入一段客户端代码 /@vite/client
,用于在客户端创建 WebSocket,接收服务端热更新传递的消息
const server: ViteDevServer = {
// ===== 核心属性 =====
config, // 配置属性
middlewares, // 中间件
httpServer, // HTTP server 实例
watcher, // chokidar 文件监听实例
pluginContainer: container, // 插件容器
ws, // WebSocket 实例
moduleGraph, // 模块依赖图
// ===== 核心方法 =====
// index.html 转换方法
transformIndexHtml: createDevHtmlTransformFn(server),
// 启动 server 方法
async listen(port?: number, isRestart?: boolean) {
;/.../
},
// 打开浏览器
openBrowser() {
;/.../
},
// 关闭 server
async close() {
;/.../
},
// 打印 url
printUrls() {
;/.../
},
// 重启 server
async restart(forceOptimize?: boolean) {
;/.../
},
}
第七步:执行 configureServer 定义函数
configureServer 主要用于配置开发服务器,比如在内部 connect 中添加自定义中间件。在这一步,从配置中获取所有 configureServer 钩子并放入 postHooks 钩子中,在内部中间中间件定义好之后,执行 postHooks 钩子
注意到 postHooks 是在处理 index.html 中间件之前执行,目的是为了自定义的中间件能够在返回 index.html 之前处理请求
const postHooks: ((() => void) | void)[] = []
for (const hook of config.getSortedPluginHooks('configureServer')) {
postHooks.push(await hook(server))
}
// 其他中间件定义...
// 执行 post 插件
postHooks.forEach((fn) => fn && fn())
// 处理 index.html 中间件
middlewares.use(indexHtmlMiddleware(server))
第八步:定义内部中间件
通过 connect 包创建 middlewares 中间件。中间件主要是用来处理 HTTP 请求和响应,通过定义一系列的中间件并且按照一定的顺序执行,每个中间件函数对请求和响应进行处理,然后将处理后的请求和响应传递给下一个中间件函数,直到最后一个中间件函数处理完毕并发送响应
import connect from 'connect'
const middlewares = connect() as Connect.Server
定义好 middlewares 之后,通过 use
方法添加启动项目阶段需要的中间件,一共有 14 个
// 计算操作执行时间
middlewares.use(timeMiddleware(root))
// 处理是否允许跨域
middlewares.use(corsMiddleware(typeof cors === 'boolean' ? {} : cors))
// 请求代理
middlewares.use(proxyMiddleware(httpServer, proxy, config))
// base 地址处理
middlewares.use(baseMiddleware(server))
// 通过 launch-editor-middleware,在代码编辑器打开指定的文件,跳转到指定行号
middlewares.use('/__open-in-editor', launchEditorMiddleware())
// 热更新 ping header 处理
middlewares.use(function viteHMRPingMiddleware(req, res, next) {
if (req.headers['accept'] === 'text/x-vite-ping') {
res.writeHead(204).end()
} else {
next()
}
})
// 处理 public 目录
middlewares.use(servePublicMiddleware(config.publicDir, config.server.headers))
// 响应请求之前,对请求的文件进行预处理
middlewares.use(transformMiddleware(server))
// 静态文件处理
middlewares.use(serveRawFsMiddleware(server))
middlewares.use(serveStaticMiddleware(root, server))
// 处理单页应用退回问题
middlewares.use(htmlFallbackMiddleware(root, config.appType === 'spa'))
// 处理 index.html
middlewares.use(indexHtmlMiddleware(server))
// 处理 404 情况
middlewares.use(function vite404Middleware(_, res) {
res.statusCode = 404
res.end()
})
// 错误处理
middlewares.use(errorMiddleware(server, middlewareMode))
总结
最后总结一下,在开发过程中,vite 启动命令执行后,实际执行的是 node_module/.bin 目录下的 vite 脚本,在解析命令行参数之后,通过执行 createServer.listen
方法启动 vite
createServer
方法主要有 8 个执行步骤,分别是
- 配置参数解析,包括 vite 核心配置、https 配置、chokidar 配置
- 创建 HTTP 和 WebSocket server,用于启动开发 server 和热更新通信
- 启动 chokidar 文件监听器,监听文件变化,实现热更新
- 创建 ModuleGraph 实例,记录模块依赖关系
- 创建插件容器,管理插件生命周期、执行过程、插件之间传递上下文
- 定义 ViteDevServer 对象,包含核心配置和启动开发 server 核心方法
- 执行 configureServer 定义函数,创建自定义中间件
- 定义内部中间件