我们知道,Vite 构建环境分为开发环境和生产环境,不同环境会有不同的构建策略,但不管是哪种环境,Vite 都会首先解析用户配置。那接下来,我就与你分析配置解析过程中 Vite 到底做了什么?即Vite是如何加载配置文件的。转自:公号 —— SegmentFault思否
https://mp.weixin.qq.com/s/U8YnbRBt9dZnEbCTuSEcpg
流程梳理
我们先来梳理整体的流程,Vite 中的配置解析由 resolveConfig 函数来实现,你可以对照源码一起学习。第一步是解析配置文件的内容,然后与命令行配置合并。值得注意的是,后面有一个记录 configFileDependencies 的操作。因为配置文件代码可能会有第三方库的依赖,所以当第三方库依赖的代码更改时,Vite 可以通过 HMR 处理逻辑中记录的 configFileDependencies 检测到更改,再重启 DevServer ,来保证当前生效的配置永远是最新的。// 这里的 config 是命令行指定的配置,如 vite --configFile=xxx
let { configFile } = config
if (configFile !== false) {
// 默认都会走到下面加载配置文件的逻辑,除非你手动指定 configFile 为 false
const loadResult = await loadConfigFromFile(
configEnv,
configFile,
config.root,
config.logLevel
)
if (loadResult) {
// 解析配置文件的内容后,和命令行配置合并
config = mergeConfig(loadResult.config, config)
configFile = loadResult.path
configFileDependencies = loadResult.dependencies
}
}
接着,Vite 会拿到这些过滤且排序完成的插件,依次调用插件 config 钩子,进行配置合并。// resolve plugins
const rawUserPlugins = (config.plugins || []).flat().filter((p) => {
if (!p) {
return false
} else if (!p.apply) {
return true
} else if (typeof p.apply === 'function') {
// apply 为一个函数的情况
return p.apply({ ...config, mode }, configEnv)
} else {
return p.apply === command
}
}) as Plugin[]
// 对用户插件进行排序
const [prePlugins, normalPlugins, postPlugins] =
sortUserPlugins(rawUserPlugins)
然后,解析项目的根目录即 root 参数,默认取 process.cwd()的结果。// run config hooks
const userPlugins = [...prePlugins, ...normalPlugins, ...postPlugins]
for (const p of userPlugins) {
if (p.config) {
const res = await p.config(config, configEnv)
if (res) {
// mergeConfig 为具体的配置合并函数,大家有兴趣可以阅读一下实现
config = mergeConfig(config, res)
}
}
}
紧接着处理 alias ,这里需要加上一些内置的 alias 规则,如 @vite/env、@vite/client 这种直接重定向到 Vite 内部的模块。// resolve root
const resolvedRoot = normalizePath(
config.root ? path.resolve(config.root) : process.cwd()
)
// resolve alias with internal client alias
const resolvedAlias = mergeAlias(
clientAlias,
config.resolve?.alias || config.alias || []
)
const resolveOptions: ResolvedConfig['resolve'] = {
dedupe: config.dedupe,
...config.resolve,
alias: resolvedAlias
}
// load .env files
const envDir = config.envDir
? normalizePath(path.resolve(resolvedRoot, config.envDir))
: resolvedRoot
const userEnv =
inlineConfig.envFile !== false &&
loadEnv(mode, envDir, resolveEnvPrefix(config))
// 解析 base url
const BASE_URL = resolveBaseUrl(config.base, command === 'build', logger)
// 解析生产环境构建配置
const resolvedBuildOptions = resolveBuildOptions(config.build)
// resolve cache directory
const pkgPath = lookupFile(resolvedRoot, [`package.json`], true /* pathOnly */)
// 默认为 node_module/.vite
const cacheDir = config.cacheDir
? path.resolve(resolvedRoot, config.cacheDir)
: pkgPath && path.join(path.dirname(pkgPath), `node_modules/.vite`)
const assetsFilter = config.assetsInclude
? createFilter(config.assetsInclude)
: () => false
assetsInclude(file: string) {
return DEFAULT_ASSETS_RE.test(file) || assetsFilter(file)
}
const createResolver: ResolvedConfig['createResolver'] = (options) => {
let aliasContainer: PluginContainer | undefined
let resolverContainer: PluginContainer | undefined
// 返回的函数可以理解为一个解析器
return async (id, importer, aliasOnly, ssr) => {
let container: PluginContainer
if (aliasOnly) {
container =
aliasContainer ||
// 新建 aliasContainer
} else {
container =
resolverContainer ||
// 新建 resolveContainer
}
return (await container.resolveId(id, importer, undefined, ssr))?.id
}
}
const resolve = config.createResolver()
// 调用以拿到 react 路径
rseolve('react', undefined, undefined, false)
const { publicDir } = config
const resolvedPublicDir =
publicDir !== false && publicDir !== ''
? path.resolve(
resolvedRoot,
typeof publicDir === 'string' ? publicDir : 'public'
)
: ''
const resolved: ResolvedConfig = {
...config,
configFile: configFile ? normalizePath(configFile) : undefined,
configFileDependencies,
inlineConfig,
root: resolvedRoot,
base: BASE_URL
... //其他配置
}
先生成完整插件列表传给 resolve.plugins,而后调用每个插件的 configResolved 钩子函数。其中 resolvePlugins 内部细节比较多,插件数量比较庞大,我们暂时不去深究具体实现,编译流水线这一小节再来详细介绍。;(resolved.plugins as Plugin[]) = await resolvePlugins(
resolved,
prePlugins,
normalPlugins,
postPlugins
)
// call configResolved hooks
await Promise.all(userPlugins.map((p) => p.configResolved?.(resolved)))
加载配置文件详解
首先,我们来看一下加载配置文件 (loadConfigFromFile) 的实现:const loadResult = await loadConfigFromFile(/*省略传参*/)
try {
const pkg = lookupFile(configRoot, ['package.json'])
if (pkg && JSON.parse(pkg).type === 'module') {
isMjs = true
}
} catch (e) {
}
let isTS = false
let isESM = false
let dependencies: string[] = []
// 如果命令行有指定配置文件路径
if (configFile) {
resolvedPath = path.resolve(configFile)
// 根据后缀判断是否为 ts 或者 esm,打上 flag
isTS = configFile.endsWith('.ts')
if (configFile.endsWith('.mjs')) {
isESM = true
}
} else {
// 从项目根目录寻找配置文件路径,寻找顺序:
// - vite.config.js
// - vite.config.mjs
// - vite.config.ts
// - vite.config.cjs
const jsconfigFile = path.resolve(configRoot, 'vite.config.js')
if (fs.existsSync(jsconfigFile)) {
resolvedPath = jsconfigFile
}
if (!resolvedPath) {
const mjsconfigFile = path.resolve(configRoot, 'vite.config.mjs')
if (fs.existsSync(mjsconfigFile)) {
resolvedPath = mjsconfigFile
isESM = true
}
}
if (!resolvedPath) {
const tsconfigFile = path.resolve(configRoot, 'vite.config.ts')
if (fs.existsSync(tsconfigFile)) {
resolvedPath = tsconfigFile
isTS = true
}
}
if (!resolvedPath) {
const cjsConfigFile = path.resolve(configRoot, 'vite.config.cjs')
if (fs.existsSync(cjsConfigFile)) {
resolvedPath = cjsConfigFile
isESM = false
}
}
}
let userConfig: UserConfigExport | undefined
if (isESM) {
const fileUrl = require('url').pathToFileURL(resolvedPath)
// 首先对代码进行打包
const bundled = await bundleConfigFile(resolvedPath, true)
dependencies = bundled.dependencies
// TS + ESM
if (isTS) {
fs.writeFileSync(resolvedPath + '.js', bundled.code)
userConfig = (await dynamicImport(`${fileUrl}.js?t=${Date.now()}`))
.default
fs.unlinkSync(resolvedPath + '.js')
debug(`TS + native esm config loaded in ${getTime()}`, fileUrl)
}
// JS + ESM
else {
userConfig = (await dynamicImport(`${fileUrl}?t=${Date.now()}`)).default
debug(`native esm config loaded in ${getTime()}`, fileUrl)
}
}
const bundled = await bundleConfigFile(resolvedPath, true)
// 记录依赖
dependencies = bundled.dependencies
fs.writeFileSync(resolvedPath + '.js', bundled.code)
userConfig = (await dynamicImport(`${fileUrl}.js?t=${Date.now()}`)).default
fs.unlinkSync(resolvedPath + '.js')
export const dynamicImport = new Function('file', 'return import(file)')
你可能会问,为什么要用 new Function 包裹?这是为了避免打包工具处理这段代码,比如 Rollup 和 TSC,类似的手段还有 eval。你可能还会问,为什么 import 路径结果要加上时间戳 query?这其实是为了让 dev server 重启后仍然读取最新的配置,避免缓存。bundleConfigFile 函数的主要功能是通过 Esbuild 将配置文件打包,拿到打包后的 bundle 代码以及配置文件的依赖 (dependencies)。而接下来的事情就是考虑如何加载 bundle 代码了,这也是 loadConfigFromBundledFile 要做的事情。// 对于 js/ts 均生效
// 使用 esbuild 将配置文件编译成 commonjs 格式的 bundle 文件
const bundled = await bundleConfigFile(resolvedPath)
dependencies = bundled.dependencies
// 加载编译后的 bundle 代码
userConfig = await loadConfigFromBundledFile(resolvedPath, bundled.code)
async function loadConfigFromBundledFile(
fileName: string,
bundledCode: string
): Promise<UserConfig> {
const extension = path.extname(fileName)
const defaultLoader = require.extensions[extension]!
require.extensions[extension] = (module: NodeModule, filename: string) => {
if (filename === fileName) {
;(module as NodeModuleWithCompile)._compile(bundledCode, filename)
} else {
defaultLoader(module, filename)
}
}
// 清除 require 缓存
delete require.cache[require.resolve(fileName)]
const raw = require(fileName)
const config = raw.__esModule ? raw.default : raw
require.extensions[extension] = defaultLoader
return config
}
// 默认加载器
const defaultLoader = require.extensions[extension]!
// 拦截原生 require 对于`.js`或者`.ts`的加载
require.extensions[extension] = (module: NodeModule, filename: string) => {
// 针对 vite 配置文件的加载特殊处理
if (filename === fileName) {
;(module as NodeModuleWithCompile)._compile(bundledCode, filename)
} else {
defaultLoader(module, filename)
}
}
Module._extensions['.js'] = function (module, filename) {
var content = fs.readFileSync(filename, 'utf8')
module._compile(stripBOM(content), filename)
}
Module.prototype._compile = function (content, filename) {
var self = this
var args = [self.exports, require, self, filename, dirname]
return compiledWrapper.apply(self.exports, args)
}
const raw = require(fileName)
const config = raw.__esModule ? raw.default : raw
// 恢复原生的加载方法
require.extensions[extension] = defaultLoader
// 返回配置
return config
// 处理是函数的情况
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,
// esbuild 打包过程中搜集的依赖
dependencies
}
三、总结
下面我们来总结一下 Vite 配置解析的整体流程和加载配置文件的方法: