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

616 阅读2分钟

辅助在线工具:

ts在线编译器

vite源码在线查看

一,vite调试

node版本号:18.4.0

1,源码下载

git clone https://github.com/vitejs/vite

2,项目介绍

  • create-vite 这个是用来生成Vite工程的,在npm init vite@latest的时候调用
  • 几种核心的plugin 包括vuereact
  • vite 这个是真正的Vite源码
  • 项目启动运行调试等操作都使用cnpm指令

3,依赖下载

cd packages/vite
cnpm install // 这里一定要有cnpm

4,启动(debug模式)

vite/packages/vite/package.json

image.png

5,调试env相关内容 vite/playground/env/package.json

image.png

二,vite解析用户配置

学习vite是如何解析用户配置之前,先简单回顾一下vite的启动流程

1,vite项目启动时,执行对应的bin指令对应的ts文件

image.png

2,执行cli.ts文件

image.png

cli.ts中使用cac定义了不同的指令。cac的参考文档

vite服务启动时,触发下图中的action

image.png

3,执行createServer方法

解析配置

image.png

4,执行resolveConfig方法

  • 获取.env文件的路径
  • 调用loadEnv方法加载解析.env文件,将结果赋值给userEnv
  • 返回整个解析后的配置

image.png

5,resolveEnvPrefix方法

envPrefix配置项未配置时,env变量默认以VITE_开头

image.png

我们知道vite配置中有一个envPrefix配置项,该配置项指定开头的变量都可以透传给客户端,当前我们调试的内置项目配置参数如下:

image.png

6,loadEnv方法

  • lookupFile方法读取本地对应的env文件,并使用parseexpand方法将文件中的配置解析成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 可选。表示初始值。
  • Object.prototype.hasOwnProperty.call(environment, key)判断对象是否有该key
  • Object.fromEntries(): 链接
  • Object.entries(): 链接