開始之前誊酌,先說下為什么要設(shè)置和讀取環(huán)境變量
簡而言之就是,通過環(huán)境變量傳參蹭睡,能讓我們在不修改任務(wù)代碼的情況下執(zhí)行不同的邏輯。
例如赶么,dev環(huán)境要加載dev配置肩豁,prod環(huán)境要加載prod配置。
config.js
configs = {
dev: {env: 'dev'},
prod: {env: 'prod'}
}
config = configs[process.env.NODE_ENV]
console.log(config)
打開終端辫呻,執(zhí)行以下命令
$ node config.js
undefined
$
$ # linux 通過 export name=value 設(shè)置環(huán)境變量
$ # 查看指定環(huán)境變量的值清钥,用 echo $name
$ # 查看全部環(huán)境變量只需要 export 回車即可
$ # 刪除一個環(huán)境變量用 unset name
$ # 以下環(huán)境該環(huán)境變量設(shè)置只在當(dāng)前終端會話中生效
$
$ export NODE_ENV=dev
$ node config.js
{ env: 'dev' }
$
$ export NODE_ENV=prod
$ node config.js
{ env: 'prod' }
可以看到,通過設(shè)置環(huán)境變量印屁,一套代碼就能加載不同的配置了循捺。除了第一次輸出是undefined
外,其余均正確輸出配置內(nèi)容雄人。所以一般還會設(shè)置缺省值从橘,多一層,更安全础钠。
config.js
config = configs[process.env.NODE_ENV || 'dev' ]
上面的示例簡單介紹了環(huán)境變量的作用恰力,更多姿勢可自行腦補(bǔ),解鎖旗吁。
我有個朋友說:如果有的話踩萎,他也想看看,所以歡迎留言~
示例使用的是node運行很钓,vue作為前端項目香府,運行在客戶的瀏覽器中,沒有process
全局對象码倦,不像node項目企孩,運行在后端os中,有process
全局對象袁稽,這里我們只使用process.env
~~所以理論上vue是不能通過process.env
讀到后端os的環(huán)境變量的勿璃,事實也確實如此。。补疑。
這就完了嗎歧沪?當(dāng)然不是。
在vue項目開發(fā)過程中莲组,通常會發(fā)現(xiàn)目錄下有.env
開頭的環(huán)境變量配置文件诊胞,有些人以為node啟動時會自動加載當(dāng)前路徑下的.env
文件到環(huán)境變量,真的嗎胁编?當(dāng)然不是厢钧。
而且就算這個YY成立鳞尔,變量也只是node能訪問嬉橙,瀏覽器中是沒有的,那為什么在前端開發(fā)過程中也經(jīng)常能遇到調(diào)用process.env
的代碼呢寥假?why?
接下來我會邊展示源碼市框,邊講解生效原理,但大家只需要在原理講解中看到代碼時糕韧,再看源碼即可枫振。
為什么要展示源碼?因為源碼這層外衣萤彩,真的沒有想象中那么難脫粪滤。
詳解
- 開發(fā)時,一般通過如下命令啟動服務(wù):
$ npm run dev
- 該命令實際調(diào)用的是
package.json
的scripts
屬性內(nèi)配置的命令雀扶,我們以開源項目vue-element-admin(點擊查看)為例杖小,查看它的package.json
內(nèi)的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" }, ... }
- 可以看到,它調(diào)用的是
vue-cli-service serve
命令愚墓,即$ npm run dev $ # 等效于 $ vue-cli-service serve
-
vue-cli-service
命令調(diào)用的是node_modules/@vue/cli-service/bin/vue-cli-service.js
內(nèi)的代碼予权,查看源碼#!/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) })
- 該文件內(nèi)
const service = new Service(process.env.VUE_CLI_CONTEXT || process.cwd())
實例化了Service
類,然后執(zhí)行了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
方法內(nèi)部調(diào)用了init
方法來加載環(huán)境變量扫腺、加載用戶配置,應(yīng)用插件村象。而init
方法內(nèi)部又調(diào)用了loadEnv
方法笆环,在loadEnv
方法內(nèi)部,使用了dotenv(點擊查看)這個第三方庫來讀取.env
環(huán)境變量配置文件厚者,所以前面提到的node自動加載.env
的YY也確實是不成立的躁劣。到此,.env
文件何時開始加載就清楚了籍救。习绢。。 - 什么,不夠闪萄?還想繼續(xù)深入梧却?當(dāng)然。
.env
中的環(huán)境變量還是僅在node
進(jìn)程的process.env
對象中(別忘了我們是通過npm run dev
命令啟動的程序)败去,那么如果os
和.env
文件內(nèi)的環(huán)境變量重名時放航,誰的優(yōu)先級高呢?查看 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
- 一句關(guān)鍵的注釋
// process.env value 'wins' over .env file's value
广鳍,翻譯過來就很明白了,進(jìn)程的環(huán)境變量會覆蓋.env
中的環(huán)境變量吓妆。 - 至此赊时,
node
進(jìn)程中環(huán)境變量的值已經(jīng)確定完畢,但還是沒有解決前端項中為何能使用process.env
的問題行拢。對祖秒,終于該熟悉的道具登場了:webpack
。前端打包實際上靠的是webpack
(這里不再細(xì)說webpack
了舟奠,簡單理解它能將前端項目重新整理為新的靜態(tài)文件供瀏覽器加載即可)竭缝,查看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) });
- 再結(jié)合
vue-cli-service
的源碼很容易發(fā)現(xiàn)它會調(diào)用webpack
將node
中的環(huán)境變量引入到前端項目中。即沼瘫,vue項目中引用process.env
的地方抬纸,會被webpack
打包時替換為具體的值。因此耿戚,我們要通過修改os的環(huán)境變量覆蓋前端項目的環(huán)境變量時湿故,一定要在運行構(gòu)建命令之前設(shè)置好,否則包都生出來了溅话,才開始設(shè)晓锻,已經(jīng)晚了~ - 至此
.env
環(huán)境變量的生效的原理就結(jié)束了,沒有了飞几。 - 還要砚哆?好吧,再來點兒屑墨。由于執(zhí)行的是
npm run dev
命令躁锁,在打包構(gòu)建完后,還會啟動一個web server
伺服剛剛打包好的靜態(tài)文件卵史,如果改動代碼并保存的話战转,它還會自動重新執(zhí)行打包伺服過程并幫你刷新好瀏覽器頁面,對以躯,自己動槐秧,是不是很爽啄踊?
總結(jié)
node 通過 vue-cli-service
工具(也稱之為腳手架)將前端中使用process.env
的地方,在build
(構(gòu)建或打包)時刁标,替換為node環(huán)境中的process.env
的值颠通。