• 技术文章 >web前端 >Vue.js

    深入探讨vite是怎么解析.env文件的

    青灯夜游青灯夜游2023-01-24 05:30:02转载26

    使用vue框架开发前端项目时,我们部署的时候都会部署多套环境,往往开发、测试以及线上环境调用的接口域名都是不一样的。如何能做到区分呢?那就是使用环境变量和模式。

    使用vite构建的vue3项目中,可以在根目录下创建.env.[模式]文件定义一种或多种模式,并且在这个文件中定义的变量就是此模式的环境变量。定义了环境变量之后就可以通过import.meta.env.[变量名]的方式读取环境变量了。【相关推荐:vuejs视频教程web前端开发

    那么我们要来思考两个问题:第一,vite如何读取.env文件中定义的配置;第二,vite如何将.env文件中配置的环境变量挂载到import.meta.env环境变量上的。通过学习今天vite中的相关源代码我们就能够明白。欢迎阅读本文学习,如果有错误的地方请不吝指正。

    1.如何读取.env文件中定义的配置

    1.1 问题的着眼点是什么?

    首先我们看一下vite如何读取.env文件中定义的配置。如下图所示,在项目根目录下有.env.development,.env.fat,.env.uat,.env.pro四个模式文件,其中development模式是对应默认的开发环境用于本地开发,fat模式对应的也是开发环境用于自测,uat模式对应的是预发布环境用于测试团队测试,pro模式对应的是生产环境也叫线上环境用于客户使用。

    那么如何能够让vite知道我们要使用相关模式的文件呢?运行vite 或者vite build命令时可以通过--mode或者-m设置环境模式(详见文档),如下图所示:

    这就提示我们要了解vite如何读取定义环境变量的模式文件则需要从vite命令或者vite build命令入手,接下来从vite命令入手研究一下。

    1.2 vite的命令定义在何处?

    看一下vite的package.json文件(路径:vite/packages/vite/package.json):

    当我们使用vite命令的时候,会执行bin目录下的vite.js文件,看一下这个文件(路径:vite/packages/vite/bin/vite.js):

    可以看到这段代码的关键是执行start方法,而start方法是导入打包后的cli.js文件,那么这个打包后的文件对应的原文件是哪个文件呢?vite打包的时候是使用rollup的,所以我们看一下rollup的配置文件(路径:vite/packages/vite/rollup.config.ts):

    如上代码可以看出vite相关命令的定义在/src/node/cli.ts 这个文件当中。

    1.3 vite的命令是如何定义的?

    1.3.1 vite使用cac定义命令

    我们来看一下vite的命令是如何定义的(路径:vite/packages/vite/src/node/cli.ts):

    import { cac } from 'cac'
    
    const cli = cac('vite')
    cli
      .option('-m, --mode <mode>', `[string] set env mode`)
    cli
      .command('[root]', 'start dev server') // default command
      .alias('serve') // the command is called 'serve' in Vite's API
      .option('--port <port>', `[number] specify port`)
      .action(async (root: string, options: ServerOptions & GlobalCLIOptions) => {
        const { createServer } = await import('./server')
        try {
          const server = await createServer({
            root,
            base: options.base,
            mode: options.mode,
            configFile: options.config,
            logLevel: options.logLevel,
            clearScreen: options.clearScreen,
            optimizeDeps: { force: options.force },
            server: cleanOptions(options),
          })
      })

    如上代码所示,vite主要是使用cac这个命令行工具库定义命令的,解释一下这里使用到的cac的相关API:

    可以看出,当运行vite命令的时候会执行createServer方法,我们这里要注意参数mode就是我们运行命令时通过--mode 或者 -m指定的参数,下面来研究createServer方法。

    1.3.2 createServer

    看一下createServer方法(路径:createServervite/packages/vite/src/node/server/index.ts):

    import { resolveConfig } from '../config'
    export async function createServer(
      inlineConfig: InlineConfig = {},
    ): Promise<ViteDevServer> {
      const config = await resolveConfig(inlineConfig, 'serve')
    }

    可以看到createServer方法调用的是resolveConfig方法,下面看一下resolveConfig方法。

    1.3.3 resolveConfig

    resolveConfig方法的代码如下(路径;vite/packages/vite/src/node/config.ts):

    import { loadEnv, resolveEnvPrefix } from './env'
    export async function resolveConfig(
      inlineConfig: InlineConfig,
      command: 'build' | 'serve',
      defaultMode = 'development',
      defaultNodeEnv = 'development',
    ): Promise<ResolvedConfig> {
      const envDir = config.envDir
        ? normalizePath(path.resolve(resolvedRoot, config.envDir))
        : resolvedRoot
      const userEnv =
        inlineConfig.envFile !== false &&
        loadEnv(mode, envDir, resolveEnvPrefix(config))
      const resolvedConfig: ResolvedConfig = {
        command,
        mode,
        env: {
          ...userEnv,
          BASE_URL,
          MODE: mode,
          DEV: !isProduction,
          PROD: isProduction,
        },
      }
      const resolved: ResolvedConfig = {
        ...config,
        ...resolvedConfig,
      }
      return resolved
    }

    可以看到resolveConfig的主要工作:

    我们看到这里的关键代码是loadEnv(mode, envDir, resolveEnvPrefix(config))下面我就重点看一下loadEnv方法。

    1.3.4 loadEnv

    loadEnv方法是vite中一个比较核心的方法,也作为vite对外提供的一个JavaScript API,用于加载 envDir 中的 .env 文件。

    我们看一下loadEnv方法(路径:vite/packages/vite/src/node/env.ts):

    import { parse } from 'dotenv'
    import { arraify, lookupFile } from './utils'
    
    export function loadEnv(
      mode: string,
      envDir: string,
      prefixes: string | string[] = 'VITE_',
    ): Record<string, string> {
      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)))
        }),
      )
      // 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
        }
      }
    
      return env
    }

    如上代码所示理解loadEnv方法注意以下几个方面:

    关于dotenv可以学习川哥的文章,也可以看看笔者的源码共读语雀笔记。至此,我们了解了vite是如何读取.env文件中定义的环境变量了。下面我们研究第二个问题vite如何将.env中配置的环境变量挂载到import.meta.env环境变量上。

    2.如何将变量挂载到import.meta.env环境变量上

    2.1 vite的环境变量和import.meta

    Vite 在一个特殊的 import.meta.env 对象上暴露环境变量,有一些在所有情况下都可以使用的内建变量:

    import.meta.env.MODE: {string} 应用运行的模式。

    import.meta.env.BASE_URL: {string} 部署应用时的基本 URL。他由base 配置项决定。

    import.meta.env.PROD: {boolean} 应用是否运行在生产环境。

    import.meta.env.DEV: {boolean} 应用是否运行在开发环境 (永远与 import.meta.env.PROD相反)。

    import.meta.env.SSR: {boolean} 应用是否运行在 server 上。

    详见环境变量。这里我们要解释一下import.meta。它是一个给JavaScript模块暴露特定上下文的元数据属性的对象。它包含了这个模块的信息,比如说这个模块的URL。详见import.meta 的MDN文档。需要注意不可以在模块的外部使用import.meta,如下图所示:

    2.2 resolveConfig

    在上文中我们已经研究了resolveConfig的代码,我们再来看以下此方法中的另一段代码(路径:vite/packages/vite/src/node/config.ts):

    import {resolvePlugins,} from './plugins'
    
    export async function resolveConfig(
      inlineConfig: InlineConfig,
      command: 'build' | 'serve',
      defaultMode = 'development',
      defaultNodeEnv = 'development',
    ): Promise<ResolvedConfig> {
      (resolved.plugins as Plugin[]) = await resolvePlugins(
        resolved,
        prePlugins,
        normalPlugins,
        postPlugins,
      )
    }

    这里调用了resolvePlugins,接收resolved对象,此对象中含有开发者所指定的模式以及.env文件中的环境变量。我们接着看一下resolvePlugins方法。

    2.3 resolvePlugins

    节选resolvePlugins方法如下(路径:vite/packages/vite/src/node/plugins/index.ts):

    import { definePlugin } from './define'
    
    export async function resolvePlugins(
      config: ResolvedConfig,
      prePlugins: Plugin[],
      normalPlugins: Plugin[],
      postPlugins: Plugin[],
    ): Promise<Plugin[]> {
      return [
        //...
        definePlugin(config),
        //...
      ].filter(Boolean) as Plugin[]
    }

    resolvePlugins负责解析插件,这里面调用了definePlugin方法,我们看一下。

    2.4 definePlugin

    definePlugin的代码如下(路径:vite/packages/vite/src/node/plugins/define.ts):

    const importMetaKeys: Record<string, string> = {}
    const importMetaFallbackKeys: Record<string, string> = {}
    if (isBuild) {
      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`,
      })
    }

    这段代码的关键部分在于第8-10行的for循环,将.env文件中定义的环境变量挂在到了import.meta.env上。至此,如何也了解了vite是如何将环境变量挂在到import.meta.env环境变量上。

    总结

    通过阅读vite的源码,我们了解到vite处理.env文件时涉及到的两个关键问题:第一,vite如何读取.env文件中定义的配置;第二,vite如何将.env文件中配置的环境变量挂载到import.meta.env环境变量上。

    对于第一问题,我们了解到vite使用cac定义命令,当执行vite命令并通过--mode或者-m选项指定模式的时候,vite会拿到mode, 然后vite会去项目目录下查找对应.env.[模式]的文件并读取其内容,然后通过dotenv的parse方法解析文件内容,将定义的环境变量整合到resolved中。

    对于第二个问题,我们了解到vite的resolveConfig方法中会执行插件解析的方法resolvePlugins,而此方法又会调用definePlugin方法,在definePlugin方法中会完成将.env文件中定义的变量挂载到import.meta.env环境变量上。

    (学习视频分享:vuejs入门教程编程基础视频

    以上就是深入探讨vite是怎么解析.env文件的的详细内容,更多请关注php中文网其它相关文章!

    声明:本文转载于:掘金社区,如有侵犯,请联系admin@php.cn删除
    专题推荐:前端 Vite Vue.js
    上一篇:详解Vue3响应式的两大利器:ref与reactive 下一篇:自己动手写 PHP MVC 框架(40节精讲/巨细/新人进阶必看)

    相关文章推荐

    • Vite学习之深度解析“依赖扫描”• 聊聊怎么利用vite插件实现骨架屏自动化• 尤雨溪回应:Vite 真的比 Turbopack 慢 10 倍吗?• 一文带你详解Vue中的组件化编程• 深入聊聊vue3中的reactive()• 详解Vue3响应式的两大利器:ref与reactive
    1/1

    PHP中文网