vite-生产构建流程
viterollupbuild process
vite
生产构建是通过获取用户配置和预设处理合并配置后函数式调用rollup
方法(write
、generate
)并返回包含了构建结果的Promise
以供后续对应钩子处理,熟悉生产构建流程有助于编写自定义插件以及实现特定业务需求.
cli build command
vite\packages\vite\src\node\cli.ts
ts
cli .command('build [root]', 'build for production') // .option('省略cli参数`)... .action(async (root: string, options: BuildOptions & GlobalCLIOptions) => { // 配置选项去重处理 filterDuplicateOptions(options) const { build } = await import('./build') // 移除全局标识 const buildOptions: BuildOptions = cleanOptions(options)
try { await build({ root, base: options.base, mode: options.mode, configFile: options.config, logLevel: options.logLevel, clearScreen: options.clearScreen, optimizeDeps: { force: options.force }, build: buildOptions, }) } catch (e) { createLogger(options.logLevel).error( colors.red(`error during build:\n${e.stack}`), { error: e }, ) process.exit(1) } finally { stopProfiler((message) => createLogger(options.logLevel).info(message)) } })
build function process
构建流程主要由resolveConfig
、buildOutputOptions
、resolveBuildOutputs
组成
ts
export async function build ( inlineConfig: InlineConfig = {}): Promise<RollupOutput | RollupOutput[] | RollupWatcher> { // 解析配置:处理用户配置及预设配置合并(省略部分细节) // 1. 解析vite config文件,通过esbuild对config文件进行build并获取处理后的用户配置信息, // 如果是函数则传入configEnv进行求值返回否则则直接返回该对象,最终和默认配置进行合并. // 2. 对worker plugins和user plugins进行扁平化、根据apply属性进行过滤再按照enforce // 的先后顺序进行排序处理. // 3. 通过runConfigHook对plugins.config进行hook排序并通过resolvePlugins注入内置plugins, // 如果是handler就调用求值并合并,否则将作为对象直接返回 // 4. 通过resolveOptions对resolve选项进行赋值(mainFields、extensions、alias...) // 5. 通过createResolver对optimizer和css中的@imports进行处理 // 6. 对worker plugins进行合并且根据order排序再runConfigHook,根据返回的config结果对 // worker选项进行赋值操作 // 7. 最终对以上的各个解析处理后的配置选项进行合并汇总最终返回 const config = await resolveConfig( inlineConfig, 'build', 'production', 'production', ) const options = config.build const ssr = !!options.ssr const libOptions = options.lib
// 省略building日志...
const resolve = (p: string) => path.resolve(config.root, p)
// 针对不同构建类型获取对应入口(lib、ssr、normal input),都没有的话则使用html作为入口文件 // 并分析index.html中引用的模块 const input = libOptions ? options.rollupOptions?.input || (typeof libOptions.entry === 'string' ? resolve(libOptions.entry) : Array.isArray(libOptions.entry) ? libOptions.entry.map(resolve) : Object.fromEntries( Object.entries(libOptions.entry).map(([alias, file]) => [ alias, resolve(file), ]), )) : typeof options.ssr === 'string' ? resolve(options.ssr) : options.rollupOptions?.input || resolve('index.html')
// 省略当构建ssr时,不能以html文件作为入口判断提示...
// 构建出口目录 const outDir = resolve(options.outDir)
// 对plugin的load、transform钩子注入ssr标识 > { ...options, ssr: true } const plugins = ( ssr ? config.plugins.map((p) => injectSsrFlagToHooks(p)) : config.plugins ) as Plugin[]
const userExternal = options.rollupOptions?.external let external = userExternal
// In CJS, we can pass the externals to rollup as is. In ESM, we need to // do it in the resolve plugin so we can add the resolved extension for // deep node_modules imports if (ssr && config.legacy?.buildSsrCjsExternalHeuristics) { external = await cjsSsrResolveExternal(config, userExternal) }
// 当optimizeDeps不为false时,初始化创建依赖优化 if (isDepsOptimizerEnabled(config, ssr)) { // 此步骤主要在dev阶段进行依赖分析和预打包,这里省略.. await initDepsOptimizer(config) }
const rollupOptions: RollupOptions = { context: 'globalThis', preserveEntrySignatures: ssr ? 'allow-extension' : libOptions ? 'strict' : false, cache: config.build.watch ? undefined : false, ...options.rollupOptions, input, plugins, external, onwarn(warning, warn) { onRollupWarning(warning, warn, config) } }
// 省略outputBuildError
let bundle: RollupBuild | undefined try { // 根据传进的output返回对应构建信息对象包含了mode=lib的情况最终会被push到normalizedOutputs中 const buildOutputOptions = (output: OutputOptions = {}): OutputOptions => { // 省略rollupOptions.output.output弃用提示判断
const ssrNodeBuild = ssr && config.ssr.target === 'node' const ssrWorkerBuild = ssr && config.ssr.target === 'webworker' const cjsSsrBuild = ssr && config.ssr.format === 'cjs'
const format = output.format || (cjsSsrBuild ? 'cjs' : 'es') // 对应模块后缀判断(cjs、mjs) const jsExt = ssrNodeBuild || libOptions ? resolveOutputJsExtension(format, getPkgJson(config.root)?.type) : 'js'
return { dir: outDir, // Default format is 'es' for regular and for SSR builds format, exports: cjsSsrBuild ? 'named' : 'auto', sourcemap: options.sourcemap, name: libOptions ? libOptions.name : undefined, // es2015 enables `generatedCode.symbols` // - #764 add `Symbol.toStringTag` when build es module into cjs chunk // - #1048 add `Symbol.toStringTag` for module default export generatedCode: 'es2015', entryFileNames: ssr ? `[name].${jsExt}` : libOptions ? ({ name }) => resolveLibFilename(libOptions, format, name, config.root, jsExt) : path.posix.join(options.assetsDir, `[name]-[hash].${jsExt}`), chunkFileNames: libOptions ? `[name]-[hash].${jsExt}` : path.posix.join(options.assetsDir, `[name]-[hash].${jsExt}`), assetFileNames: libOptions ? `[name].[ext]` : path.posix.join(options.assetsDir, `[name]-[hash].[ext]`), inlineDynamicImports: output.format === 'umd' || output.format === 'iife' || (ssrWorkerBuild && (typeof input === 'string' || Object.keys(input).length === 1)), ...output, } }
// 对于类库模式根据output是单个还是多个进行错误情况判断提示 const outputs = resolveBuildOutputs( options.rollupOptions?.output, libOptions, config.logger, ) const normalizedOutputs: OutputOptions[] = []
// 将通过buildOutputOptions分别处理后的rollup output配置项存放到数组中 if (Array.isArray(outputs)) { for (const resolvedOutput of outputs) { normalizedOutputs.push(buildOutputOptions(resolvedOutput)) } } else { normalizedOutputs.push(buildOutputOptions(outputs)) }
// 获取出口目录路径 const outDirs = normalizedOutputs.map(({ dir }) => resolve(dir!))
// 当使用build命令且watch为true(--watch、-w)时实时构建 if (config.build.watch) { config.logger.info(colors.cyan(`\nwatching for file changes...`))
const resolvedChokidarOptions = resolveChokidarOptions( config, config.build.watch.chokidar, )
const { watch } = await import('rollup') const watcher = watch({ ...rollupOptions, output: normalizedOutputs, watch: { ...config.build.watch, chokidar: resolvedChokidarOptions, }, })
watcher.on('event', (event) => { if (event.code === 'BUNDLE_START') { config.logger.info(colors.cyan(`\nbuild started...`)) if (options.write) { prepareOutDir(outDirs, options.emptyOutDir, config) } } else if (event.code === 'BUNDLE_END') { event.result.close() config.logger.info(colors.cyan(`built in ${event.duration}ms.`)) } else if (event.code === 'ERROR') { outputBuildError(event.error) } })
return watcher }
// 使用rollup函数式api,对处理后的rollup配置项进行bundle const { rollup } = await import('rollup') bundle = await rollup(rollupOptions)
// build阶段会进行文件产出 if (options.write) { prepareOutDir(outDirs, options.emptyOutDir, config) }
const res = [] // 将根据是否写入磁盘调用对应方法,将构建后的结果放到res中并返回 for (const output of normalizedOutputs) { res.push(await bundle[options.write ? 'write' : 'generate'](output)) } return Array.isArray(outputs) ? res : res[0] } catch (e) { outputBuildError(e) throw e } finally { if (bundle) await bundle.close() }}