使用vue框架開發前端專案時,我們部署的時候都會部署多套環境,往往開發、測試以及線上環境呼叫的介面網域都是不一樣的。如何能做到區分呢?那就是使用環境變數和模式。
使用vite建構的vue3專案中,可以在根目錄下建立.env.[模式]檔案定義一種或多種模式,並且在這個檔案中定義的變數就是此模式的環境變數。定義了環境變數之後就可以透過import.meta.env.[變數名]的方式讀取環境變數了。 【相關推薦:vuejs影片教學、web前端開發】
那麼我們要來思考兩個問題:第一,vite如何讀取.env檔中定義的配置;第二,vite如何將.env檔案中配置的環境變數掛載到import.meta.env環境變數上的。透過學習今天vite中的相關原始碼我們就能夠明白。歡迎閱讀本文學習,如果有錯誤的地方請不吝指正。
#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定義指令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方法。
看一下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方法。
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的主要工作:
首先确定.env文件的路径
然后调用loadEnv方法加载解析.env文件,将结果赋值给userEnv
最后返回整个解析后的配置
我们看到这里的关键代码是loadEnv(mode, envDir, resolveEnvPrefix(config))
下面我就重点看一下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方法注意以下几个方面:
该方法接收三个参数,分别是模式、.env文件的路径还有环境变量的前缀。
使用递归方法lookupFile找到.env文件的路径,使用fs.readFileSync读取文件。
使用dotenv提供的方法解析.env文件内容。
关于dotenv可以学习川哥的文章,也可以看看笔者的源码共读语雀笔记。至此,我们了解了vite是如何读取.env文件中定义的环境变量了。下面我们研究第二个问题vite如何将.env中配置的环境变量挂载到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环境变量上。
以上是深入探討vite是怎麼解析.env檔的的詳細內容。更多資訊請關注PHP中文網其他相關文章!