vite 是如何解析用户配置的 .env 的

本文参加了由公众号@若川视野 发起的每周源码共读活动点击了解详情一起参与。

本篇是源码共读第40期 | vite 是如何解析用户配置的 .env 的,点击了解本期详情

我们在项目开发过程中,会使用一些 环境变量, webpack 通过 全局注册到 process.env 上, vite 通过读取env配置文件, 挂载到 import.meta.xxx上, 我们现在调试 vite 源码,学习整个过程

简介

整个env的解析涉及如下文件

  • packages/vite/bin/vite.js 入口文件
  • packages/vite/src/node/cli.ts cac 包的action 执行的对应 serve 或者 build动作, 定义vite dev 或者 vite build 命令的地方
  • packages/vite/src/node/config.ts 调用 env.ts里面的方法解析结果
  • packages/vite/src/node/env.ts 具体解析env的实现步骤

我们要简单的了解一下 cac 这个工具, 类似 commander 是命令行工具

shell 复制代码
vite dev

vite serve

vite

这3个命令都对应同一个

基础环境

vite@5.x 现在需要 node 18 或者 20 + 版本才可以运行

pnpm 作为包管理工具

本次调试代码为 vite@5.0.10

bash 复制代码
git clone https://github.com/vitejs/vite.git

安装依赖

shell 复制代码
pnpm i

pnpm run build

安装以后,生成构建产物

如果改动了源代码, 可以采用 pnpm run dev 使用 开发模式,监听文件变动

现在,我们查看 vite的入口文件, 项目根目录的 package.json

json 复制代码
{
  "bin": {
    "vite": "bin/vite.js"
  },
}

在项目根目录,执行

shell 复制代码
pnpm run dev

调试

  1. 打断点

入口从 start 函数开始, 它加载的是 dist/node/cli.js 文件, 这个文件我们前面已经 使用 build 命令将产物进行了构建输出

packages/vite/src/node/cli.ts 中的 命令里面加一个断点, 我们在VS codeF5 , 调试,会跳入这里, 本质还是 sourcemap 起作用,才从js映射到 ts文件

packages/vite/src/node/config.ts 会调用 loadEnv文件

默认 vite 的 mode 是 development

先设定 env 里面 要解析的前缀

packages/vite/src/node/env.ts

js 复制代码
export function resolveEnvPrefix({
  envPrefix = 'VITE_',
}) {
  envPrefix = arraify(envPrefix)
  if (envPrefix.includes('')) {
    throw new Error(
      `envPrefix option contains value '', which could lead unexpected exposure of sensitive information.`,
    )
  }
  return envPrefix
}

以 VITE_ 前缀的,才解析

我们看看vite 官方基础用法, 定义 vite.config.js

js 复制代码
import {defineConfig} from 'vite'

export default defineConfig({
  envPrefix: 'VITE_'
})

export default defineConfig({
  envPrefix: ['VITE_', 'TEST_']
})

可以修改 env的前置, 可以传一个字符串,或者一个数组, 表示只要是这个前缀的, vite 都解析 env里面的内容,并添加到 import.meta.env 上

vite-envprefix-官网文档

注意: envPrefix 不能为空字符串, 否则会报错

进入到 loadEnv 的具体实现

现在断点断住了,下面是 根据 mode , 拿到要解析的 .env文件

我们知道官网加载env规则如下

也就是说, 在 development 模式下,会加载4个文件, 如果这4个配置文件都存在的话。

js 复制代码
[
  '.env',
  '.env.local',
  '.env.development',
  '.env.development.local'
]

具体实现如下:

根据 mode , 拿到要解析的 .env文件, packages/vite/src/node/env.ts

js 复制代码
export function getEnvFilesForMode(mode: string): string[] {
  return [
    /** default file */ `.env`,
    /** local file */ `.env.local`,
    /** mode file */ `.env.${mode}`,
    /** mode local file */ `.env.${mode}.local`,
  ]
}

此时 mode 为 development , 则 会解析4个文件

js 复制代码
[
  '.env',
  '.env.local',
  '.env.development',
  '.env.development.local'
]

现在需要读取并解析 4个文件中的内容, 其实本质就是读取文件,拿到的就是字符串, 另外,还需要判断这4个文件是否存在,如果不存在,则不管,如果存在, 则使用 dotenv 解析成键值对

js 复制代码
import { parse } from 'dotenv'
import { expand } from 'dotenv-expand'

/**
如果是数组,则返回原始值, 否则包装成数组
*/
export function arraify(target) {
  return Array.isArray(target) ? target: [target]
}

export function loadEnv(mode, envDir, prefixes = 'VITE_') {

  // 让 环境变量中的前缀,支持多个, 一般是 VITE_ 开头
  // 可以配置成 数组 ["VITE_", "TEST_"]  配置多个
  prefixes = arraify(prefixes)

	const env = {}
  
  const envFiles = getEnvFilesForMode(mode)

	// 这里是几个步骤
	/**
  1. 遍历目标对象 envFiles
  2. 拼接完整的 文件路径
  3. 读取文件是否存在,如果.env 配置文件在用户电脑不存在,则跳过,什么都不做
  4. 文件存在, 读取文件内容,fs.readFileSync 是buffer
  5. 将 读取到的 buffer内容给  dotenv 包的 parse 函数, 解析成对象
  */
  
  const parsed = Object.fromEntries(
    envFiles.flatMap((file) => {
      const filePath = path.join(envDir, file)
      if (!tryStatSync(filePath)?.isFile()) return []

      // 使用 dotenv提供的parsed 解析内容
      return Object.entries(parse(fs.readFileSync(filePath)))
    }),
  )
}

// 使用dotenv-expand 扩展 env文件里面的变量,支持变量
expand({ parsed })

这里涉及到几个函数 Object.fromEntries , 还有 Object.entries

js 复制代码
let t1 = {
  VITE_PORT: '3000',
  VITE_BASE_URL: 'http://www.baidu.com',
  VITE_OA_URL: '${VITE_BASE_URL}/api/oa',
  test: '123'
}

let res = Object.entries(t1)

res 的结果

js 复制代码
[
    [
        "VITE_PORT",
        "3000"
    ],
    [
        "VITE_BASE_URL",
        "http://www.baidu.com"
    ],
    [
        "VITE_OA_URL",
        "${VITE_BASE_URL}/api/oa"
    ],
    [
        "test",
        "123"
    ]
]

现在需要变成 对象形式, 使用 Object.fromEntries

js 复制代码
let r1 = [
  [ 'VITE_PORT', '3000' ],
  [ 'VITE_BASE_URL', 'http://www.baidu.com' ],
  [ 'VITE_OA_URL', '${VITE_BASE_URL}/api/oa' ],
  [ 'test', '123' ],
  [ 'VITE_DIR', 'src' ]
]

let result = Object.fromEntries(r1)

拿到的结果:

js 复制代码
{
    "VITE_PORT": "3000",
    "VITE_BASE_URL": "http://www.baidu.com",
    "VITE_OA_URL": "${VITE_BASE_URL}/api/oa",
    "test": "123",
    "VITE_DIR": "src"
}

所以,我们在 env 文件中定义的字段,不要重复

现在,只是纯值, 里面还有变量 ,需要替换为真实值, 我们使用 dotenv-expand 扩展 dotenv的功能

js 复制代码
import { expand } from 'dotenv-expand'

expand({ parsed })

现在内容都解析出来了, 还需要对内容进行过滤, 只过滤出 符合要求前缀的字段

js 复制代码
for (const [key, value] of Object.entries(parsed)) {
  if (prefixes.some((prefix) => key.startsWith(prefix))) {
    env[key] = value
  }
}

到此, 我们已经完整的分析了, 从入口文件clicac 命令,再到 读取 env文件,以及解析 env文件内容,再进一步根据前缀进行过滤, 最后拿到解析后的 env 对象

完整独立代码

shell 复制代码
node 20.10.0
pnpm

上面是我们基于vite源码进行分析, 现在我们单独起一个项目,实现一下整个过程,包含完整的日志输出

目录结构

运行

js 复制代码
node .\vite\resolveenv.mjs

vite/resolveenv.mjs

js 复制代码
import path from 'path'
import fs from 'node:fs'
// 解析 env文件
import { parse } from 'dotenv'

import { expand } from 'dotenv-expand'

const envList = [
  '.env',
  '.env.development',
  '.env.development.local'
]

// 判断文件是否存在
function tryStatSync (file) {
  try {
    return fs.statSync(file, { throwIfNoEntry: false })
  } catch (error) {
    // 忽略错误
  }
}

// vite 源码中是 连贯嵌套写法,这里只是将每一个步骤拆分,可以打印输出查看内容
// packages/vite/src/node/env.ts  中 loadEnv 方法

let r1 = envList.flatMap(file => {
  /**
   * 拿到完整的文件路径
   * D:\owner\node-base-example\cac-demo\vite\.env
    D:\owner\node-base-example\cac-demo\vite\.env.development
    D:\owner\node-base-example\cac-demo\vite\.env.development.local
   */
  const filePath = path.join(process.cwd(), 'vite', file)

  // 尝试解析文件,读取文件是否存在
  // console.log(tryStatSync(filePath));

  /**
   * Stats {
    dev: 2793138336,
    mode: 33206,
    nlink: 1,
    uid: 0,
    gid: 0,
    rdev: 0,
    blksize: 4096,
    ino: 562949958349813,
    size: 0,
    blocks: 0,
    atimeMs: 1704270080630.521,
    mtimeMs: 1704270080630.521,
    ctimeMs: 1704270080630.521,
    birthtimeMs: 1704270080630.521,
    atime: 2024-01-03T08:21:20.631Z,
    mtime: 2024-01-03T08:21:20.631Z,
    ctime: 2024-01-03T08:21:20.631Z,
    birthtime: 2024-01-03T08:21:20.631Z
}
   */

  // 如果不是文件,就跳过
  if (!tryStatSync(filePath)?.isFile()) return []

  // 符合配置的.env文件,也就是项目根目录配置了这个文件的,就进入这个流程

  let content = fs.readFileSync(filePath)

  // console.log(content);
  // return Object.entries(parse())

  // 读取到的 content 是二进制内容 buffer
  // 使用 dotenv包进行解析
  let t1 = parse(content)
  // console.log('t1');
  // console.log(t1);

  // { VITE_PORT: '3000' }
  // { VITE_DIR: 'src' }
  // console.log(t1);

  return Object.entries(t1)
})

// [ [ 'VITE_PORT', '3000' ], [ 'VITE_DIR', 'src' ] ]
console.log('r1');
console.log(r1);

// 将数组对象变成 纯对象
const parsed = Object.fromEntries(r1)

/**
 {
  VITE_PORT: '3000',
  VITE_BASE_URL: 'http://www.baidu.com',
  VITE_OA_URL: '${VITE_BASE_URL}/api/oa',
  VITE_DIR: 'src'
}
 */
// console.log(parsed);
// 现在上述的 parsed 只是一个对象,并不具备 在 env中支持变量的行为

let t2 = expand({ parsed: parsed })

// console.log(t2);

/**
 * t2 = {
  parsed: {
    VITE_PORT: '3000',
    VITE_BASE_URL: 'http://www.baidu.com',
    VITE_OA_URL: 'http://www.baidu.com/api/oa',
    VITE_DIR: 'src'
  }
}
 */

// expand 修改的是 parsed 变量的引用, 所以,可以不定义 t2接收值


/**
解析以后,就是将变量进行了替换
{
  VITE_PORT: '3000',
  VITE_BASE_URL: 'http://www.baidu.com',
  VITE_OA_URL: 'http://www.baidu.com/api/oa',
  VITE_DIR: 'src'
}
 */
console.log(parsed);

// 检查env环境变量里面的前缀, 如果前缀以 VITE_ 开头的, 则保存到 env = {}  对象中

const env = {}

// 这里写死, vite源码中是默认值是 VITE_, 用户可以在 vite.config.js 中 修改配置 envPrefix 传递多个前缀
const prefixes = ['VITE_']

for (const [key, value] of Object.entries(parsed)) {
  // 如果 解析后的前缀,符合 前缀要求,则存储到 env 对象上
  if (prefixes.some((prefix) => key.startsWith(prefix))) {
    env[key] = value
  }
}

console.log('最终解析的结果');
console.log(env);

测试用的env文件

vite/.env

ini 复制代码
VITE_PORT = 3000

VITE_BASE_URL = 'http://www.baidu.com'

VITE_OA_URL = ${VITE_BASE_URL}/api/oa

test = 123

vite/.env.development

ini 复制代码
VITE_DIR = src

最终解析的结果

css 复制代码
{
  VITE_PORT: '3000',
  VITE_BASE_URL: 'http://www.baidu.com',       
  VITE_OA_URL: 'http://www.baidu.com/api/oa',  
  VITE_DIR: 'src'
}

独立代码仅仅是对 vite源码进行了细化,日志输出,增加注释等。 其实思路是一模一样的。

相关推荐
qq_5895681013 分钟前
Echarts+vue电商平台数据可视化——后台实现笔记
vue.js·信息可视化·echarts
2401_882727571 小时前
低代码配置式组态软件-BY组态
前端·后端·物联网·低代码·前端框架
NoneCoder1 小时前
CSS系列(36)-- Containment详解
前端·css
anyup_前端梦工厂1 小时前
初始 ShellJS:一个 Node.js 命令行工具集合
前端·javascript·node.js
5hand1 小时前
Element-ui的使用教程 基于HBuilder X
前端·javascript·vue.js·elementui
GDAL2 小时前
vue3入门教程:ref能否完全替代reactive?
前端·javascript·vue.js
六卿2 小时前
react防止页面崩溃
前端·react.js·前端框架
z千鑫2 小时前
【前端】详解前端三大主流框架:React、Vue与Angular的比较与选择
前端·vue.js·react.js
m0_748256143 小时前
前端 MYTED单篇TED词汇学习功能优化
前端·学习
小白学前端6664 小时前
React Router 深入指南:从入门到进阶
前端·react.js·react