1. 程式人生 > 其它 >深入理解vue專案中的.env環境變數配置生效原理

深入理解vue專案中的.env環境變數配置生效原理

技術標籤:前端vue環境變數.envbuildwebpack

開始之前,先說下為什麼要設定和讀取環境變數

簡而言之就是,通過環境變數傳參,能讓我們在不修改任務程式碼的情況下執行不同的邏輯
例如,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=dev $ node config.js { env: 'prod' }

可以看到,通過設定環境變數,一套程式碼就能載入不同的配置了。除了第一次輸出是undefine外,其餘均正確輸出配置內容。所以一般還會設定預設值,多一層,更安全。

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?
在這裡插入圖片描述

接下來我會邊展示原始碼,邊講解生效原理,但大家只需要在原理講解中看到程式碼時,再看原始碼即可。
為什麼要展示原始碼?裝B?No,No,No,因為這是真的。。。

詳解

  1. 開發時,一般通過如下命令啟動服務:
    $ npm run dev
    
  2. 該命令實際呼叫的是 package.jsonscripts屬性內配置的命令,我們以開源專案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"
      },
      ...
    }
    
  3. 可以看到,它呼叫的是vue-cli-service serve命令,即
    $ npm run dev
    $ # 等效於
    $ vue-cli-service serve
    
  4. 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)
    })
    
  5. 該檔案內const service = new Service(process.env.VUE_CLI_CONTEXT || process.cwd())例項化了Service類,然後執行了run方法,我們檢視其部分原始碼:
    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方法內部呼叫了init方法來載入環境變數、載入使用者配置,應用外掛。而init方法內部又呼叫了loadEnv方法,在loadEnv方法內部,使用了dotenv(點選檢視)這個第三方庫來讀取.env環境變數配置檔案,所以前面提到的node自動載入.env的YY也確實是不成立的。到此,.env檔案何時開始載入就清楚了。。。
  7. 什麼,不夠?還想繼續深入?當然。.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
    
  8. 一句關鍵的註釋// process.env value 'wins' over .env file's value,翻譯過來就很明白了,程序的環境變數會覆蓋.env中的環境變數。
  9. 至此,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)
    });
    
  10. 再結合vue-cli-service的原始碼很容易發現它會呼叫webpacknode中的環境變數引入到前端專案中。即,vue專案中引用process.env的地方,會被webpack打包時替換為具體的值。因此,我們要通過修改os的環境變數覆蓋前端專案的環境變數時,一定要在執行構建命令之前設定好,否則包都生出來了,才開始設,是想要的就怪了
  11. 至此.env環境變數的生效的原理就結束了,沒有了。
  12. 還要?好吧,再來點兒。由於執行的是npm run dev命令,在打包構建完後,還會啟動一個web server伺服剛剛打包好的靜態檔案,如果改動程式碼並儲存的話,它還會自動重新執行打包伺服過程並幫你重新整理好瀏覽器頁面,對,自己動,是不很是貼心,方便

這次,真的沒有了。