深入理解vue项目中的.env环境变量配置文件生效原理

开始之前,先说下为什么要设置和读取环境变量

简而言之就是,通过环境变量传参,能让我们在不修改任务代码的情况下执行不同的逻辑
例如,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?

接下来我会边展示源码,边讲解生效原理,但大家只需要在原理讲解中看到代码时,再看源码即可。
为什么要展示源码?因为源码这层外衣,真的没有想象中那么难脱。

详解

  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方法,我们查看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方法内部调用了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伺服刚刚打包好的静态文件,如果改动代码并保存的话,它还会自动重新执行打包伺服过程并帮你刷新好浏览器页面,对,自己动,是不是很爽?

总结

node 通过 vue-cli-service工具(也称之为脚手架)将前端中使用process.env的地方,在build(构建或打包)时,替换为node环境中的process.env的值

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容