开始之前,先说下为什么要设置和读取环境变量
简而言之就是,通过环境变量传参,能让我们在不修改任务代码的情况下执行不同的逻辑。
例如,dev环境要加载dev配置,prod环境要加载prod配置。
config.js
configs = {
dev: {env: 'dev'},
prod: {env: 'prod'}
}
config = configs[process.env.NODE_ENV]
console.log(config)
打开终端,执行以下命令
$ node config.js
undefined
$
$ # linux 通过 export name=value 设置环境变量
$ # 查看指定环境变量的值,用 echo $name
$ # 查看全部环境变量只需要 export 回车即可
$ # 删除一个环境变量用 unset name
$ # 以下环境该环境变量设置只在当前终端会话中生效
$
$ export NODE_ENV=dev
$ node config.js
{ env: 'dev' }
$
$ export NODE_ENV=prod
$ node config.js
{ env: 'prod' }
可以看到,通过设置环境变量,一套代码就能加载不同的配置了。除了第一次输出是undefined
外,其余均正确输出配置内容。所以一般还会设置缺省值,多一层,更安全。
config.js
config = configs[process.env.NODE_ENV || 'dev' ]
上面的示例简单介绍了环境变量的作用,更多姿势可自行脑补,解锁。
我有个朋友说:如果有的话,他也想看看,所以欢迎留言~
示例使用的是node运行,vue作为前端项目,运行在客户的浏览器中,没有process
全局对象,不像node项目,运行在后端os中,有process
全局对象,这里我们只使用process.env
~~所以理论上vue是不能通过process.env
读到后端os的环境变量的,事实也确实如此。。。
这就完了吗?当然不是。
在vue项目开发过程中,通常会发现目录下有.env
开头的环境变量配置文件,有些人以为node启动时会自动加载当前路径下的.env
文件到环境变量,真的吗?当然不是。
而且就算这个YY成立,变量也只是node能访问,浏览器中是没有的,那为什么在前端开发过程中也经常能遇到调用process.env
的代码呢?why?
接下来我会边展示源码,边讲解生效原理,但大家只需要在原理讲解中看到代码时,再看源码即可。
为什么要展示源码?因为源码这层外衣,真的没有想象中那么难脱。
详解
- 开发时,一般通过如下命令启动服务:
$ npm run dev
- 该命令实际调用的是
package.json
的scripts
属性内配置的命令,我们以开源项目vue-element-admin(点击查看)为例,查看它的package.json
内的scripts
配置:{ "name": "vue-element-admin", "scripts": { "dev": "vue-cli-service serve", "lint": "eslint --ext .js,.vue src", "build:prod": "vue-cli-service build", "build:stage": "vue-cli-service build --mode staging", "preview": "node build/index.js --preview", "new": "plop", "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml", "test:unit": "jest --clearCache && vue-cli-service test:unit", "test:ci": "npm run lint && npm run test:unit" }, ... }
- 可以看到,它调用的是
vue-cli-service serve
命令,即$ npm run dev $ # 等效于 $ vue-cli-service serve
-
vue-cli-service
命令调用的是node_modules/@vue/cli-service/bin/vue-cli-service.js
内的代码,查看源码#!/usr/bin/env node const semver = require('semver') const { error } = require('@vue/cli-shared-utils') const requiredVersion = require('../package.json').engines.node if (!semver.satisfies(process.version, requiredVersion)) { error( `You are using Node ${process.version}, but vue-cli-service ` + `requires Node ${requiredVersion}.\nPlease upgrade your Node version.` ) process.exit(1) } const Service = require('../lib/Service') const service = new Service(process.env.VUE_CLI_CONTEXT || process.cwd()) const rawArgv = process.argv.slice(2) const args = require('minimist')(rawArgv, { boolean: [ // build 'modern', 'report', 'report-json', 'watch', // serve 'open', 'copy', 'https', // inspect 'verbose' ] }) const command = args._[0] service.run(command, args, rawArgv).catch(err => { error(err) process.exit(1) })
- 该文件内
const service = new Service(process.env.VUE_CLI_CONTEXT || process.cwd())
实例化了Service
类,然后执行了run
方法,我们查看Service
的部分源码:class Service { init (mode = process.env.VUE_CLI_MODE) { if (this.initialized) { return } this.initialized = true this.mode = mode // load mode .env if (mode) { this.loadEnv(mode) } // load base .env this.loadEnv() // load user config const userOptions = this.loadUserOptions() this.projectOptions = defaultsDeep(userOptions, defaults()) debug('vue:project-config')(this.projectOptions) // apply plugins. this.plugins.forEach(({ id, apply }) => { apply(new PluginAPI(id, this), this.projectOptions) }) // apply webpack configs from project config file if (this.projectOptions.chainWebpack) { this.webpackChainFns.push(this.projectOptions.chainWebpack) } if (this.projectOptions.configureWebpack) { this.webpackRawConfigFns.push(this.projectOptions.configureWebpack) } } loadEnv (mode) { const logger = debug('vue:env') const basePath = path.resolve(this.context, `.env${mode ? mode : ''}`) const localPath = `${basePath}.local` const load = path => { try { const env = dotenv.config({ path, debug: process.env.DEBUG }) dotenvExpand(env) logger(path, env) } catch (err) { // only ignore error if file is not found if (err.toString().indexOf('ENOENT') < 0) { error(err) } } } load(localPath) load(basePath) // by default, NODE_ENV and BABEL_ENV are set to "development" unless mode // is production or test. However the value in .env files will take higher // priority. if (mode) { // always set NODE_ENV during tests // as that is necessary for tests to not be affected by each other const shouldForceDefaultEnv = ( process.env.VUE_CLI_TEST && !process.env.VUE_CLI_TEST_TESTING_ENV ) const defaultNodeEnv = (mode === 'production' || mode === 'test') ? mode : 'development' if (shouldForceDefaultEnv || process.env.NODE_ENV == null) { process.env.NODE_ENV = defaultNodeEnv } if (shouldForceDefaultEnv || process.env.BABEL_ENV == null) { process.env.BABEL_ENV = defaultNodeEnv } } } async run (name, args = {}, rawArgv = []) { // resolve mode // prioritize inline --mode // fallback to resolved default modes from plugins or development if --watch is defined const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name]) // load env variables, load user config, apply plugins this.init(mode) args._ = args._ || [] let command = this.commands[name] if (!command && name) { error(`command "${name}" does not exist.`) process.exit(1) } if (!command || args.help || args.h) { command = this.commands.help } else { args._.shift() // remove command itself rawArgv.shift() } const { fn } = command return fn(args, rawArgv) } } ```
- 可以很容易看出来
run
方法内部调用了init
方法来加载环境变量、加载用户配置,应用插件。而init
方法内部又调用了loadEnv
方法,在loadEnv
方法内部,使用了dotenv(点击查看)这个第三方库来读取.env
环境变量配置文件,所以前面提到的node自动加载.env
的YY也确实是不成立的。到此,.env
文件何时开始加载就清楚了。。。 - 什么,不够?还想继续深入?当然。
.env
中的环境变量还是仅在node
进程的process.env
对象中(别忘了我们是通过npm run dev
命令启动的程序),那么如果os
和.env
文件内的环境变量重名时,谁的优先级高呢?查看 5. 中的dotenvExpand(env)
方法源码,我们会看到'use strict' var dotenvExpand = function (config) { var interpolate = function (env) { var matches = env.match(/\$([a-zA-Z0-9_]+)|\${([a-zA-Z0-9_]+)}/g) || [] matches.forEach(function (match) { var key = match.replace(/\$|{|}/g, '') // process.env value 'wins' over .env file's value var variable = process.env[key] || config.parsed[key] || '' // Resolve recursive interpolations variable = interpolate(variable) env = env.replace(match, variable) }) return env } for (var configKey in config.parsed) { var value = process.env[configKey] || config.parsed[configKey] if (config.parsed[configKey].substring(0, 2) === '\\$') { config.parsed[configKey] = value.substring(1) } else if (config.parsed[configKey].indexOf('\\$') > 0) { config.parsed[configKey] = value.replace(/\\\$/g, '$') } else { config.parsed[configKey] = interpolate(value) } } for (var processKey in config.parsed) { process.env[processKey] = config.parsed[processKey] } return config } module.exports = dotenvExpand
- 一句关键的注释
// process.env value 'wins' over .env file's value
,翻译过来就很明白了,进程的环境变量会覆盖.env
中的环境变量。 - 至此,
node
进程中环境变量的值已经确定完毕,但还是没有解决前端项中为何能使用process.env
的问题。对,终于该熟悉的道具登场了:webpack
。前端打包实际上靠的是webpack
(这里不再细说webpack
了,简单理解它能将前端项目重新整理为新的静态文件供浏览器加载即可),查看webpack文档
https://webpack.js.org/plugins/environment-plugin/new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 'process.env.DEBUG': JSON.stringify(process.env.DEBUG) });
- 再结合
vue-cli-service
的源码很容易发现它会调用webpack
将node
中的环境变量引入到前端项目中。即,vue项目中引用process.env
的地方,会被webpack
打包时替换为具体的值。因此,我们要通过修改os的环境变量覆盖前端项目的环境变量时,一定要在运行构建命令之前设置好,否则包都生出来了,才开始设,已经晚了~ - 至此
.env
环境变量的生效的原理就结束了,没有了。 - 还要?好吧,再来点儿。由于执行的是
npm run dev
命令,在打包构建完后,还会启动一个web server
伺服刚刚打包好的静态文件,如果改动代码并保存的话,它还会自动重新执行打包伺服过程并帮你刷新好浏览器页面,对,自己动,是不是很爽?
总结
node 通过 vue-cli-service
工具(也称之为脚手架)将前端中使用process.env
的地方,在build
(构建或打包)时,替换为node环境中的process.env
的值。