- 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
- **这是源码共读的第40期,:链接**
辅助在线工具:
一,vite调试
node版本号:18.4.0
1,源码下载
git clone https://github.com/vitejs/vite
2,项目介绍
create-vite这个是用来生成Vite工程的,在npm init vite@latest的时候调用- 几种核心的
plugin包括vue、react等 vite这个是真正的Vite源码- 项目启动运行调试等操作都使用
cnpm指令
3,依赖下载
cd packages/vite
cnpm install // 这里一定要有cnpm
4,启动(debug模式)
vite/packages/vite/package.json
5,调试env相关内容
vite/playground/env/package.json
二,vite解析用户配置
学习vite是如何解析用户配置之前,先简单回顾一下vite的启动流程
1,vite项目启动时,执行对应的bin指令对应的ts文件
2,执行cli.ts文件
cli.ts中使用cac定义了不同的指令。cac的参考文档
在vite服务启动时,触发下图中的action
3,执行createServer方法
解析配置
4,执行resolveConfig方法
- 获取
.env文件的路径 - 调用
loadEnv方法加载解析.env文件,将结果赋值给userEnv - 返回整个解析后的配置
5,resolveEnvPrefix方法
envPrefix配置项未配置时,env变量默认以VITE_开头
我们知道vite配置中有一个envPrefix配置项,该配置项指定开头的变量都可以透传给客户端,当前我们调试的内置项目配置参数如下:
6,loadEnv方法
- 用
lookupFile方法读取本地对应的env文件,并使用parse和expand方法将文件中的配置解析成object
export function loadEnv(
mode: string,
envDir: string,
prefixes: string | string[] = 'VITE_',
): Record<string, string> {
if (mode === 'local') {
throw new Error(
`"local" cannot be used as a mode name because it conflicts with ` +
`the .local postfix for .env files.`,
)
}
prefixes = arraify(prefixes)
const env: Record<string, string> = {}
const envFiles = [
/** default file */ `.env`,
/** local file */ `.env.local`,
/** mode file */ `.env.${mode}`,
/** mode local file */ `.env.${mode}.local`,
]
const parsed = Object.fromEntries(
envFiles.flatMap((file) => {
const path = lookupFile(envDir, [file], {
pathOnly: true,
rootDir: envDir,
})
if (!path) return []
return Object.entries(parse(fs.readFileSync(path)))
}),
)
try {
// let environment variables use each other
expand({ parsed })
} catch (e) {
// custom error handling until https://github.com/motdotla/dotenv-expand/issues/65 is fixed upstream
// check for message "TypeError: Cannot read properties of undefined (reading 'split')"
if (e.message.includes('split')) {
throw new Error(
'dotenv-expand failed to expand env vars. Maybe you need to escape `$`?',
)
}
throw e
}
// only keys that start with prefix are exposed to client
for (const [key, value] of Object.entries(parsed)) {
if (prefixes.some((prefix) => key.startsWith(prefix))) {
env[key] = value
} else if (
key === 'NODE_ENV' &&
process.env.VITE_USER_NODE_ENV === undefined
) {
// NODE_ENV override in .env file
process.env.VITE_USER_NODE_ENV = value
}
}
// check if there are actual env variables starting with VITE_*
// these are typically provided inline and should be prioritized
for (const key in process.env) {
if (prefixes.some((prefix) => key.startsWith(prefix))) {
env[key] = process.env[key] as string
}
}
return env
}
7,parse方法
- 将文件流解析成
object
const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg
// Parser src into an Object
function parse (src) {
const obj = {}
// Convert buffer to string
let lines = src.toString()
// Convert line breaks to same format
lines = lines.replace(/\r\n?/mg, '\n')
let match
while ((match = LINE.exec(lines)) != null) {
const key = match[1]
// Default undefined or null to empty string
let value = (match[2] || '')
// Remove whitespace
value = value.trim()
// Check if double quoted
const maybeQuote = value[0]
// Remove surrounding quotes
value = value.replace(/^(['"`])([\s\S]*)\1$/mg, '$2')
// Expand newlines if double quoted
if (maybeQuote === '"') {
value = value.replace(/\\n/g, '\n')
value = value.replace(/\\r/g, '\r')
}
// Add to object
obj[key] = value
}
8,expand函数
- 扩展环境变量
function _interpolate (envValue, environment, config) {
const matches = envValue.match(/(.?\${*[\w]*(?::-[\w/]*)?}*)/g) || []
return matches.reduce(function (newEnv, match, index) {
const parts = /(.?)\${*([\w]*(?::-[\w/]*)?)?}*/g.exec(match)
if (!parts || parts.length === 0) {
return newEnv
}
const prefix = parts[1]
let value, replacePart
if (prefix === '\\') {
replacePart = parts[0]
value = replacePart.replace('\\$', '$')
} else {
const keyParts = parts[2].split(':-')
const key = keyParts[0]
replacePart = parts[0].substring(prefix.length)
// process.env value 'wins' over .env file's value
value = Object.prototype.hasOwnProperty.call(environment, key)
? environment[key]
: (config.parsed[key] || keyParts[1] || '')
// If the value is found, remove nested expansions.
if (keyParts.length > 1 && value) {
const replaceNested = matches[index + 1]
matches[index + 1] = ''
newEnv = newEnv.replace(replaceNested, '')
}
// Resolve recursive interpolations
value = _interpolate(value, environment, config)
}
return newEnv.replace(replacePart, value)
}, envValue)
}
function expand (config) {
// if ignoring process.env, use a blank object
const environment = config.ignoreProcessEnv ? {} : process.env
for (const configKey in config.parsed) {
const value = Object.prototype.hasOwnProperty.call(environment, configKey) ? environment[configKey] : config.parsed[configKey]
config.parsed[configKey] = _interpolate(value, environment, config)
}
// PATCH: don't write to process.env
// for (const processKey in config.parsed) {
// environment[processKey] = config.parsed[processKey]
// }
return config
}
三,如何将配置挂载在import.meta.env环境变量上
回到resolveConfig方法中,继续执行代码。处理过的配置参数ResolvedConfig
const resolvedConfig: ResolvedConfig = {
configFile: configFile ? normalizePath(configFile) : undefined,
configFileDependencies: configFileDependencies.map((name) =>
normalizePath(path.resolve(name)),
),
inlineConfig,
root: resolvedRoot,
base: resolvedBase.endsWith('/') ? resolvedBase : resolvedBase + '/',
rawBase: resolvedBase,
resolve: resolveOptions,
publicDir: resolvedPublicDir,
cacheDir,
command,
mode,
ssr,
isWorker: false,
mainConfig: null,
isProduction,
plugins: userPlugins,
server,
build: resolvedBuildOptions,
preview: resolvePreviewOptions(config.preview, server),
env: {
...userEnv,
BASE_URL,
MODE: mode,
DEV: !isProduction,
PROD: isProduction,
},
assetsInclude(file: string) {
return DEFAULT_ASSETS_RE.test(file) || assetsFilter(file)
},
logger,
packageCache: new Map(),
createResolver,
optimizeDeps: {
disabled: 'build',
...optimizeDeps,
esbuildOptions: {
preserveSymlinks: resolveOptions.preserveSymlinks,
...optimizeDeps.esbuildOptions,
},
},
worker: resolvedWorkerOptions,
appType: config.appType ?? (middlewareMode === 'ssr' ? 'custom' : 'spa'),
experimental: {
importGlobRestoreExtension: false,
hmrPartialAccept: false,
...config.experimental,
},
getSortedPlugins: undefined!,
getSortedPluginHooks: undefined!,
}
1,resolvePlugins中的definePlugin方法
const env: Record<string, any> = {
...config.env,
SSR: !!config.build.ssr,
}
for (const key in env) {
importMetaKeys[`import.meta.env.${key}`] = JSON.stringify(env[key])
}
Object.assign(importMetaFallbackKeys, {
'import.meta.env.': `({}).`,
'import.meta.env': JSON.stringify(config.env),
'import.meta.hot': `false`,
})
四,总结
-
reduce函数语法:
arr.reduce(function(prev,cur,index,arr){
...
}, init);
参数:
prev 必需。累计器累计回调的返回值; 表示上一次调用回调时的返回值,或者初始值 init;
cur 必需。表示当前正在处理的数组元素;
index 可选。表示当前正在处理的数组元素的索引,若提供 init 值,则起始索引为- 0,否则起始索引为1;
arr 可选。表示原数组;
init 可选。表示初始值。