【源码共读】第40期 | vite 是如何解析用户配置的 .env

1. 前言

2. 前置知识

cac简单用法案例

Object相关静态方法

dotenv-expand

fs模块操作

3. 源码解析

3.1 使用场景

参考打包环境区分,在使用vite区分环境是,我们通常定义多个env文件,如:

打包使用env环境变量则使用loadEnv进行引入

本文就vite环境变量的解析引入作分析。

3.2 解析流程

解析流程如下:

3.2 主要函数说明

对于使用的函数作简单说明,具体在调试步骤详细讲解:

  1. bin/vite.js:start函数:这个函数是用来启动 Vite 的服务器的。其中包括一些初始化的设置,使用cac例如读取用户设置和默认配置,创建服务器等。

  2. src/node/cli.ts:createServer函数:创建一个新的 Vite 服务器的。

  3. src/node/server/index.ts:_createServer函数:这个函数是在内部实际创建 Vite 服务器的函数。它调用 http.createServer() 来创建一个新的 HTTP 服务器,并设置一些基础的请求处理逻辑。

  4. src/node/config.ts:resolveConfig函数:此函数用于解析用户提供的配置,将其合并到默认配置中。如果用户没有提供配置,那么将使用默认配置。

  5. src/node/env.ts:resolveEnvPrefix函数:此函数用于从环境变量中解析出带有特定前缀的键。

  6. src/node/env.ts:loadEnv函数:这个函数用于加载 .env 文件到 process.env。`

  7. dotenv包的parse方法:此方法用于将字符串或缓冲区转换为一个 JavaScript 对象,对象的键值对表示环境变量的键值对。

  8. dotenv-expand包的expand函数:此函数用于处理环境变量值中嵌入的其他环境变量。比如 DB_HOST=localhostDB_URL=$DB_HOST:5432,它会把 DB_URL 里的 $DB_HOST 替换成真正的值 localhost

  9. dotenv-expand包的_interpolate函数:此函数是 expand 函数的辅助函数,用于实际执行环境变量值中的插值替换操作。

  10. resolvePlugins:此函数的主要任务是生成一个包含所有插件的列表。它接收用户配置和默认配置,然后合并这两者。如果同一个插件在用户配置和默认配置中都出现,则用户配置将覆盖默认配置。返回的是一个插件数组,这些插件按照预定的顺序排列。

  11. definePlugin:此函数用于创建新的 Vite 插件。插件是一个对象,该对象可以包含多种处理程序,用于对特定类型的文件进行操作。此处进行import.meta.env对象挂载,其他不做深究。

4. 源码调试

  1. 在vite项目中,我们定义两个env文件:
js 复制代码
// .env
VITE_BASE = vite
VITE_BASE_EXPAND = ${VITE_BASE}_expand


// .env.dev
NODE_ENV = dev
BASE = /
VITE_BASE_URL = http://localhost:8080
  1. 使用node bin/vite.js --mode dev,在bin/vite.js中打断点进入

  2. 进入src/node/cli.ts,此处使用cac进行命令参数收集,调用后进入action函数

  3. 进入src/node/server/index.ts, 调用_createServer 传入{ws:true} 调用resolveConfig

js 复制代码
export async function resolveConfig(
  inlineConfig: InlineConfig,
  command: 'build' | 'serve',
  defaultMode = 'development',
  defaultNodeEnv = 'development',
): Promise<ResolvedConfig> {
  let config = inlineConfig // 传入的配置项
  let configFileDependencies: string[] = []
  let mode = inlineConfig.mode || defaultMode // 方式开发环境
  const isNodeEnvSet = !!process.env.NODE_ENV // false node什么环境

  // user config may provide an alternative mode. But --mode has a higher priority
  mode = inlineConfig.mode || config.mode || mode
  configEnv.mode = mode

 // load .env files
  const envDir = config.envDir
    ? normalizePath(path.resolve(resolvedRoot, config.envDir))
    : resolvedRoot
  const userEnv =
    inlineConfig.envFile !== false &&
    loadEnv(mode, envDir, resolveEnvPrefix(config)) // 前缀标识 resolveEnvPrefix
  return resolved
}
  1. 使用loadEnv进行加载解析,直接进入loadEnv函数
js 复制代码
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`,
  ]
// [['name','jack'],['age',20]] => {name:'jack',age:20}
  const parsed = Object.fromEntries(
    envFiles.flatMap((file) => {// [1,2,3] => [2,4,6]
      const filePath = path.join(envDir, file) // "C:\\Users\\admin\\Desktop\\ruochuan\\vite\\packages\\vite\\.env"
      if (!tryStatSync(filePath)?.isFile()) return [] // 获取文件信息 是否有文件

      return Object.entries(parse(fs.readFileSync(filePath)))// {name:'jack'}=>['name','jack']
    }),
  )

  // let environment variables use each other
  // 将环境变量中的${xxx} $xxx 替换
  expand({ parsed })

  // 读取解析文件后的对象
  for (const [key, value] of Object.entries(parsed)) {
    if (prefixes.some((prefix) => key.startsWith(prefix))) {
      env[key] = value
    }
  }

  // 读取process.env中是否有prefix前缀的变量
  for (const key in process.env) {
    if (prefixes.some((prefix) => key.startsWith(prefix))) {
      env[key] = process.env[key] as string
    }
  }

  return env
}

前缀转换,解析对应文件转换成对象,判断环境

expand({parsed}) 主要进行变量替换

resolveConfig调用返回结果

  1. src/node/plugins/index.ts的resolvePlugins函数解析插件
  1. 调用了src/node/plugins/define.ts的definePlugin,作用将.env文件中定义的环境变量挂在到了import.meta.env上

5. 总结

总结下Vite中进行环境变量加载过程:

  1. 使用cac读取环境配置,使用resolveConfig读取配置;
  2. 核心是loadEnv加载解析过程,vite中默认定义了加载的环境文件格式 .env.${mode},loadEnv使用dotenvparse方法解析文件成对象,使用dotenv-expandexpand方法进行变量替换,最后将prefix前缀的变量进行返回。
  3. resolvePlugins解析插件,definePlugin将变量挂载到import.meta对象上

通过本次的学习了:

  • Object中的相关方法如fromEntries、entries等
  • 学习了dotenv-expand的解析
  • 了解vite中环境变量的配置方式
  • 了解了cac包的使用

每天进步一小点O^O.


源码分析:vite是如何解析.env文件的?

相关推荐
长天一色几秒前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
NiNg_1_23418 分钟前
npm、yarn、pnpm之间的区别
前端·npm·node.js
秋殇与星河21 分钟前
CSS总结
前端·css
BigYe程普42 分钟前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发
余生H1 小时前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈
程序员-珍1 小时前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
axihaihai1 小时前
网站开发的发展(后端路由/前后端分离/前端路由)
前端
流烟默1 小时前
Vue中watch监听属性的一些应用总结
前端·javascript·vue.js·watch
2401_857297911 小时前
招联金融2025校招内推
java·前端·算法·金融·求职招聘
茶卡盐佑星_2 小时前
meta标签作用/SEO优化
前端·javascript·html