Vite 源码(二)vite启动流程以及如何获取config配置


theme: cyanosis

highlight: monokai

当在控制台输入yarn run dev时,执行对应源码的位置是node/cli.ts

``typescript import { cac } from 'cac' // 创建CLI实例,'vite'表示,在 help 和 version 命令中显示的名称 const cli = cac('vite') // 添加命令项 cli .option('-c, --config <file>',[string] use specified config file`) // ...

// dev cli .command('[root]') // default command .alias('serve') // 设置命令别名 .option('--host [host]', [string] specify hostname) // ... .option('--cors', [boolean] enable CORS) // 使用 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(async (root: string, options: ServerOptions & GlobalCLIOptions) => {})

cli.help() cli.version(require('../../package.json').version) cli.parse() ``` Vite 使用 cscjs搭建的命令

当执行这个命令会调用action的回调函数,看下主要代码 typescript // root: 如果执行的是 yarn run dev -- test 或者 npx vite test,则 root 参数为 'test' // options:命令行中的参数 .action(async (root: string, options: ServerOptions & GlobalCLIOptions) => { const { createServer } = await import('./server') try { const server = await createServer({ root, // 命令名 base: options.base, // 公共基础路径 // 环境模式,development | production mode: options.mode, configFile: options.config, // 配置文件目录 // 调整控制台输出的级别,默认为 'info',可选值 'info' | 'warn' | 'error' | 'silent' logLevel: options.logLevel, clearScreen: options.clearScreen, // 是否清空终端打印的信息 // 开发服务器配置,比如 host、port、open、https、cors、strictPort、force server: cleanOptions(options) }) await server.listen() })

createServer

createServer函数定义在src/node/server/index.ts里面,由于createServer函数代码比较多,这里只捡重要的说 ```typescript // 删减版,包含主要流程 export async function createServer(inlineConfig) { // 获取config配置 const config = await resolveConfig(inlineConfig, 'serve', 'development') // 获取项目根路径 const root = config.root // 获取本地服务器相关的配置 const serverConfig = config.server // 创建中间件实例 const middlewares = connect() as Connect.Server // 创建 http 服务器 const httpServer = await resolveHttpServer( serverConfig, middlewares, httpsOptions ) // 创建 WebSocket 服务器 const ws = createWebSocketServer(httpServer, config, httpsOptions) // ignored:忽略监听的文件;watchOptions:对应 server.watch 配置,传递给 chokidar 的文件系统监视器选项 const { ignored = [], ...watchOptions } = serverConfig.watch || {} // 通过 chokidar 监听文件 const watcher = chokidar.watch(path.resolve(root), { ignored: [ '/node_modules/', '/.git/', ...(Array.isArray(ignored) ? ignored : [ignored]), ], ignoreInitial: true, ignorePermissionErrors: true, disableGlobbing: true, ...watchOptions, }) as FSWatcher // 获取 所有插件 const plugins = config.plugins // 创建插件容器,是一个对象,对象的属性是 vite 支持的 rollup 的钩子函数,后面会介绍 // 比如 options、resolveId、load、transform const container = await createPluginContainer(config, watcher) // 创建Vite 的 ModuleGraph 实例,后面也会介绍 const moduleGraph = new ModuleGraph(container) // 声明 server 对象 const server: ViteDevServer = { config, // 包含命令行传入的配置 和 配置文件的配置 middlewares, get app() { return middlewares }, httpServer, // http 服务器 watcher, // 通过 chokidar 监听文件 pluginContainer: container, // vite 支持的 rollup 的钩子函数 ws, // WebSocket 服务器 moduleGraph, // ModuleGraph 实例 transformWithEsbuild, transformRequest(url, options) {}, listen(port?: number, isRestart?: boolean) {}, _optimizeDepsMetadata: null, _isRunningOptimizer: false, _registerMissingImport: null, _pendingReload: null, _pendingRequests: Object.create(null), } // 被监听文件发生变化时触发 watcher.on('change', async (file) => {}) // 添加文件时触发 watcher.on('add', (file) => {}) watcher.on('unlink', (file) => {}) // 执行插件中的 configureServer 钩子函数 // configureServer:https://vitejs.cn/guide/api-plugin.html#configureserver const postHooks: ((() => void) | void)[] = [] for (const plugin of plugins) { if (plugin.configureServer) { // configureServer 可以注册前置中间件,就是在内部中间件之前执行;也可以注册后置中间件 // 如果configureServer 返回一个函数,这个函数内部就是注册后置中间件,并将这些函数收集到 postHooks 中 postHooks.push(await plugin.configureServer(server)) } } // 接下来就是注册中间件 // base if (config.base !== '/') { middlewares.use(baseMiddleware(server)) } // ... // 主要转换中间件 middlewares.use(transformMiddleware(server)) // ... // 如果请求路径是 /结尾,则将路径修改为 /index.html if (!middlewareMode || middlewareMode === 'html') { middlewares.use(spaFallbackMiddleware(root)) } // 调用用户定义的后置中间件 postHooks.forEach((fn) => fn && fn())

if (!middlewareMode || middlewareMode === 'html') {
    // 如果请求的url是 html 则调用插件中所有的 transformIndexHtml 钩子函数,转换html,并将转换后的 html 代码发送给客户端
    middlewares.use(indexHtmlMiddleware(server))
    // handle 404s
    middlewares.use(function vite404Middleware(_, res) {
        res.statusCode = 404
        res.end()
    })
}
if (!middlewareMode && httpServer) {
    // 重写 httpServer.listen,在服务器启动前预构建
    const listen = httpServer.listen.bind(httpServer)
    httpServer.listen = (async (port: number, ...args: any[]) => {}) as any
} else {}
return server

} `` createServer函数的大体流程如下 - 获取config配置 - 创建 http 服务器httpServer - 创建 WebSocket 服务器ws - 通过 chokidar 创建监听器watcher - 创建一个兼容rollup钩子函数的对象container - 创建模块图谱实例moduleGraph - 声明server对象 - 注册watcher回调 - 执行插件中的configureServer钩子函数(注册用户定义的前置中间件),并收集用户定义的后置中间件 - 注册中间件 - 注册用户定义的后置中间件 - 注册转换html文件的中间件和未找到文件的404中间件 - 重写httpServer.listen - 返回server`对象

整体逻辑就是,调用createServer函数,拿到返回值后调用server.listen(),在这个过程中会对预构建依赖包并开启本地开发服务器。

小结

Vite 冷启动为什么快

Vite 运行 Dev 命令后只做了两件事情,一是启动了本地服务器并注册了一些中间件;二是使用 ESbuild 预构建模块。之后就一直躺着,直到浏览器以 http 方式发来 ESM 规范的模块请求时,Vite 才开始“「按需编译」”被请求的模块。

相对于 Webpack

Webpack 启动后会做一堆事情,经历一条很长的编译打包链条,从入口开始需要逐步经历语法解析、依赖收集、代码转译、打包合并、代码优化,最终将高版本的、离散的源码编译打包成低版本、高兼容性的产物代码,在 Node 运行时下性能必然有问题。

继续

调用server.listen()的逻辑在预构建一节中会详细介绍。回到createServer函数,接下来会详细分析下面几个点 1. 如何获取config配置 2. 创建一个兼容 rollup 钩子函数的对象container,这个是一个什么对象 3. 模块图谱实例moduleGraph是什么样子的

如何获取config配置

createServer函数中,调用resolveConfig获取config配置 typescript // inlineConfig 命令行传入的配置 const config = await resolveConfig(inlineConfig, 'serve', 'development') resolveConfig函数也有很多内容,我们分块来看。

```typescript export async function resolveConfig( inlineConfig: InlineConfig, command: 'build' | 'serve', // 命令 defaultMode = 'development' // 环境 ): Promise { let config = inlineConfig // 命令行中的配置项 let configFileDependencies: string[] = [] let mode = inlineConfig.mode || defaultMode

const configEnv = {
  mode, // 环境,开发环境下是 development
  command // 命令,开发环境下是 serve
}

let { configFile } = config // 配置文件路径

// ...

``` 刚进入函数,定义了 5 个变量,后面会用

Vite 是怎么找到配置文件的

继续向下 typescript if (configFile !== false) { // 查找配置文件并获取配置文件中的 config 配置 const loadResult = await loadConfigFromFile( configEnv, configFile, // 命令行传入的配置文件路径 config.root, config.logLevel ) if (loadResult) { // 合并配置 config = mergeConfig(loadResult.config, config) // 获取配置文件绝对路径 configFile = loadResult.path // 获取 vite.config.js 中导入的非第三方文件列表(比如自定义插件、方法文件等) configFileDependencies = loadResult.dependencies } } loadConfigFromFile函数中,也分为两步

  1. 查找、校验配置文件的路径,并判断文件类型(是不是ts、是不是遵循ESM规范)
  2. 根据路径和类型获取文件内容
获取配置文件路径

```typescript // loadConfigFromFile 函数内 let resolvedPath: string | undefined let isTS = false let isMjs = false // 是不是 ESM 规范的文件 let dependencies: string[] = [] try { // 如果 package.json 中的 type 属性是 module,则说明配置文件遵循 ESM 规范 const pkg = lookupFile(configRoot, ['package.json']) if (pkg && JSON.parse(pkg).type === 'module') { isMjs = true } } catch (e) {} // 如果命令行中指定了配置文件路径,则获取此路径的绝对路径,并判断是不是 ts文件或者遵循 ESM 规范的文件 if (configFile) { resolvedPath = path.resolve(configFile) isTS = configFile.endsWith('.ts')

if (configFile.endsWith('.mjs')) {
    isMjs = true
}

} else { // 此时没有指定配置文件路径 // 在项目根路径上查找 vite.config.js const jsconfigFile = path.resolve(configRoot, 'vite.config.js') // 如果存在,则将路径赋值给 resolvedPath if (fs.existsSync(jsconfigFile)) { resolvedPath = jsconfigFile } // 和上述逻辑相同,在项目根路径上查找 vite.config.mjs // 如果找到了,将路径赋值给 resolvedPath。并将 isMjs 置为 true if (!resolvedPath) { /* ... / } // 和上述逻辑相同,在项目根路径上查找 vite.config.ts // 如果找到了,将路径赋值给 resolvedPath。并将 isTS 置为 true if (!resolvedPath) { / ... */ } } // 如果没有,抛出异常 if (!resolvedPath) { debug('no config file found.') return null } `` 查找过程很简单 - 从package.json中判断配置文件是否遵循 ESM 规范 - 如果指定了配置文件路径,则校验该路径并判断文件类型 - 如果没有指定配置文件,从项目根目录按顺序查找vite.config.jsvite.config.mjsvite.config.ts`;并判断文件类型

找到配置文件后,开始获取配置文件内容

获取配置文件内容

```typescript let userConfig: UserConfigExport | undefined

// 如果遵循 ESM 规范 if (isMjs) { const fileUrl = require('url').pathToFileURL(resolvedPath) // 如果是 ts 文件 if (isTS) { // 通过 ESbuild 打包文件,第二个参数表示打包后的文件类型,true是遵循ESM规范 const bundled = await bundleConfigFile(resolvedPath, true) // bundleConfigFile内调用的esbuild 的配置中设置了 metafile: true 用于生成依赖关系 // 并且手写了一个 esbuild 的 plugin,不会将第三方库打包在 bundle 中,即生成的依赖也不会包含第三方库 // 所以这里的 dependencies 内容,只包含用户自己写的文件 dependencies = bundled.dependencies // 新建 js 文件,并将打包后的代码写入文件中 fs.writeFileSync(resolvedPath + '.js', bundled.code) // 通过 import() 动态加载刚创建的 js 文件,并获取导出内容 userConfig = (await dynamicImport(${fileUrl}.js?t=${Date.now()})) .default // 删除刚创建的文件 fs.unlinkSync(resolvedPath + '.js') } else { // 直接动态加载该文件,并获取导出内容 userConfig = (await dynamicImport(${fileUrl}?t=${Date.now()})).default } } `` 如果**明确知道**配置文件遵循ESM规范,则通过import()`的方式加载文件获取导出内容。对于ts文件通过 ESbuild 打包文件。

还有一种情况就是配置文件是js文件,并且package.json中没有明确指出type: "module",此时这个js文件要么遵循ESM,要么遵循CommonJS。继续看代码 typescript try { let userConfig: UserConfigExport | undefined if (isMjs) { const fileUrl = require('url').pathToFileURL(resolvedPath) if (isTS) {} else {} } // 如果 userConfig 为空,先尝试直接加载文件,假设遵循commonjs if (!userConfig && !isTS && !isMjs) { try { // 清空 require 中的缓存 delete require.cache[require.resolve(resolvedPath)] // 重新 require userConfig = require(resolvedPath) } catch (e) {} } // 如果 userConfig 依然没有 // 说明配置文件有几种可能,ts文件、遵循ESM、package.json中设置type是module但是配置文件遵循CommonJS规范 if (!userConfig) { // 通过 esbuild 打包成 CommonJS,因为不确定该配置文件到底是遵循什么规范 const bundled = await bundleConfigFile(resolvedPath) // 获取依赖信息 dependencies = bundled.dependencies // 获取配置 // 这里用这个函数的主要作用是即可以获取遵循ESM规范的导出,又可以获取遵循CommonJS规范的导出 userConfig = await loadConfigFromBundledFile(resolvedPath, bundled.code) } // 如果 配置文件导出的是一个函数,则执行该函数 const config = await( typeof userConfig === 'function' ? userConfig(configEnv) : userConfig ) if (!isObject(config)) { throw new Error(`config must export or return an object.`) } // 返回 配置文件路径、配置文件导出内容、以及用户自定义导入 return { path: normalizePath(resolvedPath), config, dependencies, // 自定义组件列表 } } catch (e) {} 这里就不过多解释了,注释已经很清楚了。最后就是获取到了配置文件的导出内容,并返回一个对象。要注意下对象内的属性 bash path: 配置文件路径, config: 配置文件导出内容, dependencies: 非第三方导入(包含用户自定义的插件) 到此,已经拿到了配置文件内容,接下来就是需要合并喝规范化配置项,方便后续使用

合并和规范配置项

typescript // 查找配置文件并获取配置文件中的 config 配置 const loadResult = await loadConfigFromFile(/* ... */) if (loadResult) { // 合并配置 config = mergeConfig(loadResult.config, config) // 获取配置文件绝对路径 configFile = loadResult.path // 非第三方导入的文件(比如自定义插件) configFileDependencies = loadResult.dependencies } 调用mergeConfig函数合并命令行配置和vite.config.js的配置。配置合并完成之后,就开始处理配置项

处理配置

plugins

typescript // 获取打包环境 development、production mode = inlineConfig.mode || config.mode || mode configEnv.mode = mode // 根据 apply 属性将当前环境不支持的 plugins 过滤掉 const rawUserPlugins = (config.plugins || []).flat().filter((p) => { if (!p) { return false } else if (!p.apply) { return true } else if (typeof p.apply === 'function') { return p.apply({ ...config, mode }, configEnv) } else { return p.apply === command } }) as Plugin[] 自定义插件的apply属性表示在什么环境下执行。上面这段代码的意思是 - 如果没有apply,表示在开发、生产环境下都会添加到rawUserPlugins等待执行 - 如果apply是一个函数,函数返回值是true,添加到rawUserPlugins等待执行 - 如果apply的属性值等于当前环境字符串(servebuild),则添加到rawUserPlugins等待执行

过滤完之后,对rawUserPlugins中所有插件分类,根据enforce的属性值分类,代码如下 ```typescript // /** * 属性值为 pre:表示提前执行的插件,放到 prePlugins 中 * 属性值为 post:表示最后执行的插件,放到 postPlugins 中 * 没有设置或者设置的是其他属性值:表示正常执行的插件,放到 normalPlugins 中 */ const [prePlugins, normalPlugins, postPlugins] = sortUserPlugins(rawUserPlugins)

export function sortUserPlugins(plugins) { const prePlugins: Plugin[] = [] const postPlugins: Plugin[] = [] const normalPlugins: Plugin[] = []

if (plugins) {
    plugins.flat().forEach((p) => {
        if (p.enforce === 'pre') prePlugins.push(p)
        else if (p.enforce === 'post') postPlugins.push(p)
        else normalPlugins.push(p)
    })
}

return [prePlugins, normalPlugins, postPlugins]

} 接下来就是执行所有自定义插件的`config`钩子函数 typescript const userPlugins = [...prePlugins, ...normalPlugins, ...postPlugins] for (const p of userPlugins) { // 执行所有自定义插件的 config 钩子函数 // 并传入vite的配置项和 configEnv // configEnv(对象内部: mode: 'development'|'production', command: 'serve'|'build' ) if (p.config) { const res = await p.config(config, configEnv) // 也就是说 config 钩子函数可以修改配置项,并返回新的配置项 // 拿到新的配置项之后,让新的配置项和老的配置项合并 if (res) { config = mergeConfig(config, res) } } } `` 这里有一个需要注意的点就是,config`钩子函数可以修改配置项并返回新的配置项,拿到新配置项之后会合并新老配置项

最后会将 Vite 自带的插件和用户自定义的插件合并,后面会说

root 处理

typescript const resolvedRoot = normalizePath( config.root ? path.resolve(config.root) : process.cwd() ) 获取config.root配置项的绝对路径或当前node命令执行时所在的文件夹目录

注意区分一下process.cwd()__dirname

  • process.cwd():指当前node命令执行时所在的文件夹目录
  • __dirname是指被执行js文件所在的文件夹目录

alias

```typescript // 创建新的alias // /^[\/]?@vite\/env/ 替换成 'vite/dist/client/env.mjs' // /^[\/]?@vite\/client/ 替换成 'vite/dist/client/client.mjs' const clientAlias = [ { find: /^[\/]?@vite\/env/, replacement: () => ENVENTRY }, { find: /^[\/]?@vite\/client/, replacement: () => CLIENTENTRY }, ]

// 将 clientAlias 和 配置项中的 alias 合并并返回 const resolvedAlias = mergeAlias( clientAlias, config.resolve?.alias || config.alias || [] ) `` 合并完成之后的alias数据结构和上面的clientAlias`一致。

resolve

typescript // 获取 resolve 所有配置项 const resolveOptions: ResolvedConfig['resolve'] = { dedupe: config.dedupe, ...config.resolve, alias: resolvedAlias, } 拼接 resolve 配置

.env 文件

typescript // 如果没有设置 config.envDir 则获取项目根路径 const envDir = config.envDir ? normalizePath(path.resolve(resolvedRoot, config.envDir)) : resolvedRoot // 获取所有.env 文件中的属性 const userEnv = inlineConfig.envFile !== false && loadEnv(mode, envDir, resolveEnvPrefix(config)) loadEnv函数根据envDir依次查找下面4个文件

  1. .env.development.local
  2. .env.development
  3. .env.local
  4. .env

如果找到了调用dotenv解析该.env文件。

如果设置了config.envPrefix则只获取config.envPrefix前缀的变量。如果没设置config.envPrefix则获取VITE_开头的变量。

其他配置

```typescript // 解析 base const BASE_URL = resolveBaseUrl(config.base, command === 'build', logger) // 生产环境相关 const resolvedBuildOptions = resolveBuildOptions(config.build)

// 获取 package.json 路径 const pkgPath = lookupFile(resolvedRoot, [package.json], true /* pathOnly */) // 获取/设置缓存目录,默认是 node_modules/.vite const cacheDir = config.cacheDir ? path.resolve(resolvedRoot, config.cacheDir) : pkgPath && path.join(path.dirname(pkgPath), node_modules/.vite)

// 指定其他文件类型作为静态资源处理(这样导入它们就会返回解析后的 URL) const assetsFilter = config.assetsInclude ? createFilter(config.assetsInclude) // 构造一个过滤函数,该函数可用于确定是否应该对某些模块进行操作 : () => false

// 创建在特殊场景中使用的内部解析器 // 比如,预构建时,用于解析路径 const createResolver: ResolvedConfig['createResolver'] = (options) => { let aliasContainer: PluginContainer | undefined let resolverContainer: PluginContainer | undefined return async (id, importer, aliasOnly, ssr) => {} }

const { publicDir } = config // 获取静态资源地址 const resolvedPublicDir = publicDir !== false && publicDir !== '' ? path.resolve( resolvedRoot, typeof publicDir === 'string' ? publicDir : 'public' ) : '' ```

上述配置处理完成之后,创建resolved对象,并拼接配置

resolved对象

typescript const resolved: ResolvedConfig = { ...config, configFile: configFile ? normalizePath(configFile) : undefined, // 配置文件路径 configFileDependencies, // vite.config.js 中非第三方包的导入,比如自定义插件 inlineConfig, // 命令行中的配置 root: resolvedRoot, // 项目根目录 base: BASE_URL, // 公共基础路径, /my-app/index.html resolve: resolveOptions, // 文件解析时的相关配置 publicDir: resolvedPublicDir, // 静态资源服务的文件夹 cacheDir, // 缓存目录,默认 node_modules/.vite command, // serve | build mode, // development | production isProduction, // 是否是生产环境 plugins: userPlugins, // 自定义 plugins server: resolveServerOptions(resolvedRoot, config.server), build: resolvedBuildOptions, env: { ...userEnv, // .env 文件 BASE_URL, MODE: mode, DEV: !isProduction, PROD: isProduction, }, assetsInclude(file: string) { // 一个函数,用于获取传入的 file 是否能作为静态资源处理,如果能,导入它们就会返回解析后的 URL return DEFAULT_ASSETS_RE.test(file) || assetsFilter(file) }, logger, createResolver, // 特殊场景中使用的内部解析器,预构建文件中会说 optimizeDeps: { ...config.optimizeDeps, esbuildOptions: { // esbuild 配置 keepNames: config.optimizeDeps?.keepNames, preserveSymlinks: config.resolve?.preserveSymlinks, ...config.optimizeDeps?.esbuildOptions, }, }, } 这个resolved对象就是最后处理完的所有配置。最后resolveConfig函数也会返回这个resolved对象。

从上面resolved.optimizeDeps.esbuildOptions可以看出,如果想配置esbuild的配置项,可以通过下面的方式 bash { resolve: { preserveSymlinks: boolean } optimizeDeps: { keepNames: boolean esbuildOptions: {} } }

Vite 自带的插件和用户自定义的插件合并

处理插件的时候说过最后还会整合Vite自带的插件,代码如下 typescript // 将vite自带插件和用户定义插件安顺序组合,并返回 ;(resolved.plugins as Plugin[]) = await resolvePlugins( resolved, prePlugins, normalPlugins, postPlugins ) // 只包含开发环境中使用的插件 export async function resolvePlugins( config: ResolvedConfig, prePlugins: Plugin[], normalPlugins: Plugin[], postPlugins: Plugin[] ): Promise<Plugin[]> { return [ preAliasPlugin(), aliasPlugin({ entries: config.resolve.alias }), ...prePlugins, // 自定义前置插件 config.build.polyfillModulePreload ? modulePreloadPolyfillPlugin(config) : null, resolvePlugin({ ...config.resolve, root: config.root, isProduction: config.isProduction, ssrConfig: config.ssr, asSrc: true, }), htmlInlineScriptProxyPlugin(config), cssPlugin(config), config.esbuild !== false ? esbuildPlugin(config.esbuild) : null, jsonPlugin( { namedExports: true, ...config.json, } ), wasmPlugin(config), webWorkerPlugin(config), assetPlugin(config), ...normalPlugins, // 自定义插件 definePlugin(config), cssPostPlugin(config), ...postPlugins // 自定义后置插件 } 所有插件拼接好之后,调用所有自定义插件configResolved钩子函数 typescript await Promise.all(userPlugins.map((p) => p.configResolved?.(resolved)))

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值