首页文章关于

vite-生产构建流程

viterollupbuild process

vite生产构建是通过获取用户配置和预设处理合并配置后函数式调用rollup方法(writegenerate)并返回包含了构建结果的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

构建流程主要由resolveConfigbuildOutputOptionsresolveBuildOutputs组成

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()
}
}
Copyright © 2023, Daily learning records and sharing. Build by