前言
vite在本地启动服务的时候,第一步会先解析配置
async function createServer(inlineConfig){
const config = await resolveConfig(inlineConfig, 'serve', 'development')
...
}
这个配置可能是来自于inlineCongfig,也有可能来自于vite.config.js配置文件,他们的关系如下
通过这个关系分析我们知道最终返回经过处理的resolveConfig,主要流程如下
流程大概就是这个样子,具体实现可以接着往下看
解析vite配置
参考文章:
juejin.cn/post/710491…
获取vite.config配置
这部分主要流程如下,最终的config就是我们的所有的配置
// 命令行传入的配置
let config = inlineConfig;
// 配置文件的依赖,配置文件通过esbuild打包后的metafile,热更新使用
let configFileDependencies = [];
// 获取配置文件中的配置项,与inlineConfig合并
const loadResult = await loadConfigFromFile(configEnv, configFile, config.root, config.logLevel);
config = mergeConfig(loadResult.config, config);
configFile = loadResult.path;
configFileDependencies = loadResult.dependencies;
loadConfigFromFile()
可以看到获取的主要逻辑都集中在loadConfigFromFile这个方法,具体实现如下
async function loadConfigFromFile(configEnv, configFile, configRoot = process.cwd(), logLevel) {
// 配置文件路径,下面是获取这个路径的步骤,一堆判断不重要
let resolvedPath;
...
// 判断是否是ESM,先看配置文件后缀,再看package.json中的type
let isESM = false;
...
// esbuild打包配置文件,目的是转换 TS 语法和获取参与打包的本地文件依赖dependencies
// dependencies:获取参与打包的本地文件依赖,可以从打包结果的 meta 数据中拿到。用于配置的热更新,参与打包的文件依赖改变,需要自动重启
const bundled = await bundleConfigFile(resolvedPath, isESM);
// 将打包之后的代码转成配置对象
const userConfig = await loadConfigFromBundledFile(resolvedPath, bundled.code, isESM);
const config = await (typeof userConfig === 'function'
? userConfig(configEnv)
: userConfig);
return {
path: normalizePath(resolvedPath),
config,
dependencies: bundled.dependencies
};
}
bundleConfigFile()
bundleConfigFile就是esbuild的使用了,主要就是一些配置的理解,除此之外还有两个内部实现的插件
plugins: [
{
name: 'externalize-deps',
setup(build) {
// 当引入另外一个模块时,如果匹配 filter 的正则表达式,则执行后面定义的回调
build.onResolve({ filter: /.*/ }, ({ path: id, importer }) => {
// 将裸模块设置为external
// 裸模块:例如 `import { createApp } from "vue"`,vue 就是没有任何路径的裸模块
// external:热更新时,只需要监听本地配置文件及本地依赖的更改,不需要监听 npm 包的改变
if (id[0] !== '.' && !path.isAbsolute(id)) {
return {
external: true
}
}
// 下面说的是,monorepo环境下,vite做了妥协,如果引用了其他workspace里面的文件,则将这个文件设置为external,即使违背了external的初衷
// 见 https://github.com/vitejs/vite/pull/9140
const idFsPath = path.resolve(path.dirname(importer), id)
const idPkgPath = lookupFile(idFsPath, [`package.json`], {
pathOnly: true
})
if (idPkgPath) {
const idPkgDir = path.dirname(idPkgPath)
if (path.relative(idPkgDir, fileName).startsWith('..')) {
return {
path: isESM ? pathToFileURL(idFsPath).href : idFsPath,
external: true
}
}
}
})
}
},
{
name: 'inject-file-scope-variables',
setup(build) {
// todo
build.onLoad({ filter: /\.[cm]?[jt]s$/ }, async (args) => {
const contents = await fs.promises.readFile(args.path, 'utf8')
const injectValues =
`const ${dirnameVarName} = ${JSON.stringify(
path.dirname(args.path)
)};` +
`const ${filenameVarName} = ${JSON.stringify(args.path)};` +
`const ${importMetaUrlVarName} = ${JSON.stringify(
pathToFileURL(args.path).href
)};`
return {
loader: args.path.endsWith('ts') ? 'ts' : 'js',
contents: injectValues + contents
}
})
}
}
]
对于第一个插件,来看一个真实的例子,下面是一个 vite.config.ts 的代码:
// vite.config.ts
import { defineConfig, splitVendorChunkPlugin } from 'vite'
import vuePlugin from '@vitejs/plugin-vue'
import { vueI18nPlugin } from './CustomBlockPlugin'
export default defineConfig({
plugins: [
vuePlugin({
reactivityTransform: true
}),
splitVendorChunkPlugin(),
vueI18nPlugin
]
})
经过 bundleConfigFile 函数的处理(并非 esbuild 的执行结果,bundleConfigFile 函数只取了部分的 esbuild 打包结果),有以下的执行结果:
{
code: '打包后的 js 代码文本',
dependencies: ["CustomBlockPlugin.ts", "vite.config.ts"]
}
dependencies 是参与打包的文件(依赖) ,取值为 Object.keys(result.metafile.inputs),裸模块并没有被打包进来
因此,一般情况下,dependencies 只有本地写的配置文件及本地依赖
loadConfigFromBundledFile()
除了bundleConfigFile还有一个方法loadConfigFromBundledFile,这里主要是想讲一下这个方法内部使用了一个自定义导入dynamicImport,代码如下
const dynamicImport = new Function('file', 'return import(file)')
我们知道加载ESM的文件可以使用import来引入,这么做的目的主要是 :使用 new Function 实现的动态 import,在构建打包 vite 源码时,不会被 Rollup 打包到 vite 的构建产物中,因为这里是字符串的import(file),只会在使用的时候去引入这个临时文件
为什么不能一起打包?
- 配置文件,不属于 vite 源码的一部分,不是 vite 源码的依赖,不能打包到 vite 源码
- 配置文件在 vite 源码打包过程中,并不存在
- 配置文件是在 vite 实际运行中,才被动态引入的
这里还要区分 vite 源码打包过程和 vite 打包项目的过程:
- vite 源码打包:打包产物是 vite 这个工具的代码
- vite 项目打包:打包产物是项目的代码,该过程才会有 vite 配置文件
解析插件,插件排序,执行插件config hook
vite有两种插件,一种是config.plugins中配置的普通插件,还有一种config.worker.plugins插件,这个专门为了web Worker提供的插件配置。关于web Worker可以看阮一峰的博客
vite在这部分对两种插件做了排序和扁平化,执行config hook
// 解析插件
const rawUserPlugins = (await asyncFlatten(config.plugins || [])).filter((p) => {
// 过滤假值
if (!p) {
return false;
}
// 如果没有apply,表示传过来的是对象且没有自定义apply参数
else if (!p.apply) {
return true;
}
// 如果apply是函数,则表示插件还没有执行,这里调用apply改变上下文并返回结果
else if (typeof p.apply === 'function') {
return p.apply({ ...config, mode }, configEnv);
}
// 这里就是自定义apply参数,如果apply为'build'则忽略
else {
return p.apply === command;
}
});
// 根据插件的enforce字段进行排序
const [prePlugins, normalPlugins, postPlugins] = sortUserPlugins(rawUserPlugins)
// 循环vite.config中的插件,如果有config钩子在这里执行
const userPlugins = [...prePlugins, ...normalPlugins, ...postPlugins];
for (const p of userPlugins) {
if (p.config) {
const res = await p.config(config, configEnv);
if (res) {
config = mergeConfig(config, res);
}
}
}
处理别名
// vite内部env和client别名,暂时不知道做什么用
// ENV_ENTRY:'D:\\document\\vue\\ANALYSIS\\vite\\packages\\vite\\dist\\client\\env.mjs'
// CLIENT_ENTRY: 'D:\\document\\vue\\ANALYSIS\\vite\\packages\\vite\\dist\\client\\client.mjs'
const clientAlias = [
{ find: /^[\/]?@vite\/env/, replacement: () => ENV_ENTRY },
{ find: /^[\/]?@vite\/client/, replacement: () => CLIENT_ENTRY }
];
// 解析vite.config中配置的别名和clientAlias
const resolvedAlias = normalizeAlias(mergeAlias(clientAlias, config.resolve?.alias || []));
const resolveOptions = {
...config.resolve,
alias: resolvedAlias
};
返回最终结果
加载.env文件
function loadEnv(
mode, // 模式
envDir, // .env文件路径
prefixes = 'VITE_'
) {
...
// 数组化
prefixes = arraify(prefixes)
// 环境变量对象
const env = {}
const envFiles = [
/** mode local file */ `.env.${mode}.local`,
/** mode file */ `.env.${mode}`,
/** local file */ `.env.local`,
/** default file */ `.env`
]
...
for (const file of envFiles) {
const path = lookupFile(envDir, [file], { pathOnly: true, rootDir: envDir })
// 如果环境变量文件存在,使用dotenv解析环境变量并返回
if (path) {
...
}
}
// e.g. { VITE_test:'test111' }
return env
}
解析baseUrl
开发模式下如果是'',/,./都会转成/,其他情况就是在判断写法合不合规范,例如要以/开头和结尾
解析打包配置
这块打包的时候再看
内部解析器
主要作用:
create an internal resolver to be used in special scenarios, e.g.
optimizer & handling css @imports