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