深入理解vue項目中的.env環(huán)境變量配置文件生效原理

開始之前誊酌,先說下為什么要設(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?

接下來我會邊展示源碼市框,邊講解生效原理,但大家只需要在原理講解中看到代碼時糕韧,再看源碼即可枫振。
為什么要展示源碼?因為源碼這層外衣萤彩,真的沒有想象中那么難脫粪滤。

詳解

  1. 開發(fā)時,一般通過如下命令啟動服務(wù):
    $ npm run dev
    
  2. 該命令實際調(diào)用的是 package.jsonscripts屬性內(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"
      },
      ...
    }
    
  3. 可以看到,它調(diào)用的是vue-cli-service serve命令愚墓,即
    $ npm run dev
    $ # 等效于
    $ vue-cli-service serve
    
  4. 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)
    })
    
  5. 該文件內(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)
      }
    }
    ```
    
  6. 可以很容易看出來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文件何時開始加載就清楚了籍救。习绢。。
  7. 什么,不夠闪萄?還想繼續(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
    
  8. 一句關(guān)鍵的注釋// process.env value 'wins' over .env file's value广鳍,翻譯過來就很明白了,進(jìn)程的環(huán)境變量會覆蓋.env中的環(huán)境變量吓妆。
  9. 至此赊时,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)
    });
    
  10. 再結(jié)合vue-cli-service的源碼很容易發(fā)現(xiàn)它會調(diào)用webpacknode中的環(huán)境變量引入到前端項目中。即沼瘫,vue項目中引用process.env的地方抬纸,會被webpack打包時替換為具體的值。因此耿戚,我們要通過修改os的環(huán)境變量覆蓋前端項目的環(huán)境變量時湿故,一定要在運行構(gòu)建命令之前設(shè)置好,否則包都生出來了溅话,才開始設(shè)晓锻,已經(jīng)晚了~
  11. 至此.env環(huán)境變量的生效的原理就結(jié)束了,沒有了飞几。
  12. 還要砚哆?好吧,再來點兒屑墨。由于執(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的值颠通。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市膀懈,隨后出現(xiàn)的幾起案子顿锰,更是在濱河造成了極大的恐慌,老刑警劉巖启搂,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件硼控,死亡現(xiàn)場離奇詭異,居然都是意外死亡胳赌,警方通過查閱死者的電腦和手機(jī)牢撼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來匈织,“玉大人浪默,你說我怎么就攤上這事牡直∽贺埃” “怎么了?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵碰逸,是天一觀的道長乡小。 經(jīng)常有香客問我,道長饵史,這世上最難降的妖魔是什么满钟? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮胳喷,結(jié)果婚禮上湃番,老公的妹妹穿的比我還像新娘。我一直安慰自己吭露,他們只是感情好吠撮,可當(dāng)我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著讲竿,像睡著了一般泥兰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上题禀,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天鞋诗,我揣著相機(jī)與錄音,去河邊找鬼迈嘹。 笑死削彬,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播糕篇,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼拌消!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起墩崩,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎侯勉,沒想到半個月后鹦筹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體址貌,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡铐拐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年练对,在試婚紗的時候發(fā)現(xiàn)自己被綠了遍蟋。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡螟凭,死狀恐怖虚青,靈堂內(nèi)的尸體忽然破棺而出螺男,到底是詐尸還是另有隱情,我是刑警寧澤下隧,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站淆院,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏迫筑。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一脯燃、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧辕棚,春花似錦邓厕、人聲如沸扁瓢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至伟桅,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間楣铁,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工赫冬, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人劲厌。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓哭廉,卻偏偏與公主長得像脊僚,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子遵绰,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容