0
点赞
收藏
分享

微信扫一扫

env文件实现探索

沉浸在自己的世界里 2022-03-25 阅读 75
vuejsnode.js

1.0、使用

  • 官方介绍:https://cli.vuejs.org/zh/guide/mode-and-env.html
1.1、文件命名格式
  • .env # 所有环境可用
  • .env.development # 开发环境可用
  • .env.test # 测试环境可用
  • .env.production # 生产环境可用
  • .env.自定义 # 自定义环境使用
  • .env.local # 所有环境可用,不提交git仓库
  • .env.xxxxx.local # 制定环境可用,不提交git仓库
1.2、环境设置
  • 默认模式
    • vue-cli-service serve 默认 development 模式
    • vue-cli-service test:unit 默认 test 模式
    • vue-cli-service buildvue-cli-service test:e2e 默认 production 模式
  • 自定义环境
    • 命令行参数修改环境 --mode 环境名称
      • 列子:vue-cli-service serve --mode prod
      • 这样并不会修改 NODE_ENV
  • NODE_ENV
    • 用途
      • NODE_ENV 将决定您的应用运行的模式,是开发,生产还是测试,因此也决定了创建哪种 webpack 配置。
      • 例如通过将 NODE_ENV 设置为 "test",Vue CLI 会创建一个优化过后的,并且旨在用于单元测试的 webpack 配置,它并不会处理图片以及一些对单元测试非必需的其他资源。
      • 同理,NODE_ENV=development 创建一个 webpack 配置,该配置启用热更新,不会对资源进行 hash 也不会打出 vendor bundles,目的是为了在开发的时候能够快速重新构建。
      • 当你运行 vue-cli-service build 命令时,无论你要部署到哪个环境,应该始终把 NODE_ENV 设置为 "production" 来获取可用于部署的应用程序。
    • 取值
      • 自动取值
        • 默认是 development
        • production 模式下被设置为 production
        • test 模式下被设置为 test
      • 手动设置
        • 在env文件中进行赋值即可
1.3、环境变量
  • .env 文件中,以 key = value 的形式设置
  • 在行首位置使用 # 进行注释
  • 只有 NODE_ENVBASE_URL 和以 VUE_APP_ 开头的变量能在vue文件中使用
1.4、环境变量的合并
  • 优先级:.env < .env.local < .env.xxxx < .env.xxxx.local < npm run xxx 之前process.env已有的变量
  • 不重名合并,重名以优先级最高的为准

2.0、探索

2.1、谁帮我们实现了在 .env 中设置环境变量?vue还是node呢?
  • 看看是不是node
    • 起一个项目

    • 项目文件夹下心间 .env 文件

      NODE_ENV = 'dev'
      
    • 运行 node index,打印 process.env.NODE_ENV

      undefined
      
  • 看来不是node 帮我们做的喽,那去vue中找找是怎么帮我们做的
2.2、vue是如何实现可以在 .env 中设置环境变量?
  • 1、新建一个vue项目,配置.env文件,试一下发现可以在页面中拿到设置的变量

  • 2、回顾一下,整个过成我们就 npm run serve 跑了下

  • 3、去看看 package.json
    在这里插入图片描述

  • 4、猜测是 vue-cli-service 实现的,去看看脚本源码

    • 找啊找,在 node_modules/@vue/cli-service/bin/vue-cli-service.js 找到了源码

      #!/usr/bin/env node
      
      const { semver, error } = require('@vue/cli-shared-utils')
      const requiredVersion = require('../package.json').engines.node
      
      if (!semver.satisfies(process.version, requiredVersion, { includePrerelease: true })) {
        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',
          'inline-vue',
          'watch',
          // serve
          'open',
          'copy',
          'https',
          // inspect
          'verbose'
        ]
      })
      const command = args._[0]
      
      service.run(command, args, rawArgv).catch(err => {
        error(err)
        process.exit(1)
      })
      
    • 我们看到脚本内主要用了 service.run 方法

  • 5、去看看service

    const fs = require('fs')
    const path = require('path')
    const debug = require('debug')
    const merge = require('webpack-merge')
    const Config = require('webpack-chain')
    const PluginAPI = require('./PluginAPI')
    const dotenv = require('dotenv')
    const dotenvExpand = require('dotenv-expand')
    const defaultsDeep = require('lodash.defaultsdeep')
    const { chalk, warn, error, isPlugin, resolvePluginId, loadModule, resolvePkg } = require('@vue/cli-shared-utils')
    
    const { defaults, validate } = require('./options')
    
    module.exports = class Service {
      constructor (context, { plugins, pkg, inlineOptions, useBuiltIn } = {}) {
        process.VUE_CLI_SERVICE = this
        this.initialized = false
        this.context = context
        this.inlineOptions = inlineOptions
        this.webpackChainFns = []
        this.webpackRawConfigFns = []
        this.devServerConfigFns = []
        this.commands = {}
        // Folder containing the target package.json for plugins
        this.pkgContext = context
        // package.json containing the plugins
        this.pkg = this.resolvePkg(pkg)
        // If there are inline plugins, they will be used instead of those
        // found in package.json.
        // When useBuiltIn === false, built-in plugins are disabled. This is mostly
        // for testing.
        this.plugins = this.resolvePlugins(plugins, useBuiltIn)
        // pluginsToSkip will be populated during run()
        this.pluginsToSkip = new Set()
        // resolve the default mode to use for each command
        // this is provided by plugins as module.exports.defaultModes
        // so we can get the information without actually applying the plugin.
        this.modes = this.plugins.reduce((modes, { apply: { defaultModes }}) => {
          return Object.assign(modes, defaultModes)
        }, {})
      }
    
      resolvePkg (inlinePkg, context = this.context) {
        if (inlinePkg) {
          return inlinePkg
        }
        const pkg = resolvePkg(context)
        if (pkg.vuePlugins && pkg.vuePlugins.resolveFrom) {
          this.pkgContext = path.resolve(context, pkg.vuePlugins.resolveFrom)
          return this.resolvePkg(null, this.pkgContext)
        }
        return pkg
      }
    
      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 }) => {
          if (this.pluginsToSkip.has(id)) return
          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 = envPath => {
          try {
            const env = dotenv.config({ path: envPath, debug: process.env.DEBUG })
            dotenvExpand(env)
            logger(envPath, 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
          }
        }
      }
    
      setPluginsToSkip (args) {
        const skipPlugins = args['skip-plugins']
        const pluginsToSkip = skipPlugins
          ? new Set(skipPlugins.split(',').map(id => resolvePluginId(id)))
          : new Set()
    
        this.pluginsToSkip = pluginsToSkip
      }
    
      resolvePlugins (inlinePlugins, useBuiltIn) {
        const idToPlugin = id => ({
          id: id.replace(/^.\//, 'built-in:'),
          apply: require(id)
        })
    
        let plugins
    
        const builtInPlugins = [
          './commands/serve',
          './commands/build',
          './commands/inspect',
          './commands/help',
          // config plugins are order sensitive
          './config/base',
          './config/css',
          './config/prod',
          './config/app'
        ].map(idToPlugin)
    
        if (inlinePlugins) {
          plugins = useBuiltIn !== false
            ? builtInPlugins.concat(inlinePlugins)
            : inlinePlugins
        } else {
          const projectPlugins = Object.keys(this.pkg.devDependencies || {})
            .concat(Object.keys(this.pkg.dependencies || {}))
            .filter(isPlugin)
            .map(id => {
              if (
                this.pkg.optionalDependencies &&
                id in this.pkg.optionalDependencies
              ) {
                let apply = () => {}
                try {
                  apply = require(id)
                } catch (e) {
                  warn(`Optional dependency ${id} is not installed.`)
                }
    
                return { id, apply }
              } else {
                return idToPlugin(id)
              }
            })
          plugins = builtInPlugins.concat(projectPlugins)
        }
    
        // Local plugins
        if (this.pkg.vuePlugins && this.pkg.vuePlugins.service) {
          const files = this.pkg.vuePlugins.service
          if (!Array.isArray(files)) {
            throw new Error(`Invalid type for option 'vuePlugins.service', expected 'array' but got ${typeof files}.`)
          }
          plugins = plugins.concat(files.map(file => ({
            id: `local:${file}`,
            apply: loadModule(`./${file}`, this.pkgContext)
          })))
        }
    
        return plugins
      }
    
      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])
    
        // --skip-plugins arg may have plugins that should be skipped during init()
        this.setPluginsToSkip(args)
    
        // 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)
      }
    
      resolveChainableWebpackConfig () {
        const chainableConfig = new Config()
        // apply chains
        this.webpackChainFns.forEach(fn => fn(chainableConfig))
        return chainableConfig
      }
    
      resolveWebpackConfig (chainableConfig = this.resolveChainableWebpackConfig()) {
        if (!this.initialized) {
          throw new Error('Service must call init() before calling resolveWebpackConfig().')
        }
        // get raw config
        let config = chainableConfig.toConfig()
        const original = config
        // apply raw config fns
        this.webpackRawConfigFns.forEach(fn => {
          if (typeof fn === 'function') {
            // function with optional return value
            const res = fn(config)
            if (res) config = merge(config, res)
          } else if (fn) {
            // merge literal values
            config = merge(config, fn)
          }
        })
    
        // #2206 If config is merged by merge-webpack, it discards the __ruleNames
        // information injected by webpack-chain. Restore the info so that
        // vue inspect works properly.
        if (config !== original) {
          cloneRuleNames(
            config.module && config.module.rules,
            original.module && original.module.rules
          )
        }
    
        // check if the user has manually mutated output.publicPath
        const target = process.env.VUE_CLI_BUILD_TARGET
        if (
          !process.env.VUE_CLI_TEST &&
          (target && target !== 'app') &&
          config.output.publicPath !== this.projectOptions.publicPath
        ) {
          throw new Error(
            `Do not modify webpack output.publicPath directly. ` +
            `Use the "publicPath" option in vue.config.js instead.`
          )
        }
    
        if (
          !process.env.VUE_CLI_ENTRY_FILES &&
          typeof config.entry !== 'function'
        ) {
          let entryFiles
          if (typeof config.entry === 'string') {
            entryFiles = [config.entry]
          } else if (Array.isArray(config.entry)) {
            entryFiles = config.entry
          } else {
            entryFiles = Object.values(config.entry || []).reduce((allEntries, curr) => {
              return allEntries.concat(curr)
            }, [])
          }
    
          entryFiles = entryFiles.map(file => path.resolve(this.context, file))
          process.env.VUE_CLI_ENTRY_FILES = JSON.stringify(entryFiles)
        }
    
        return config
      }
    
      loadUserOptions () {
        // vue.config.c?js
        let fileConfig, pkgConfig, resolved, resolvedFrom
        const esm = this.pkg.type && this.pkg.type === 'module'
    
        const possibleConfigPaths = [
          process.env.VUE_CLI_SERVICE_CONFIG_PATH,
          './vue.config.js',
          './vue.config.cjs'
        ]
    
        let fileConfigPath
        for (const p of possibleConfigPaths) {
          const resolvedPath = p && path.resolve(this.context, p)
          if (resolvedPath && fs.existsSync(resolvedPath)) {
            fileConfigPath = resolvedPath
            break
          }
        }
    
        if (fileConfigPath) {
          if (esm && fileConfigPath === './vue.config.js') {
            throw new Error(`Please rename ${chalk.bold('vue.config.js')} to ${chalk.bold('vue.config.cjs')} when ECMAScript modules is enabled`)
          }
    
          try {
            fileConfig = loadModule(fileConfigPath, this.context)
    
            if (typeof fileConfig === 'function') {
              fileConfig = fileConfig()
            }
    
            if (!fileConfig || typeof fileConfig !== 'object') {
              // TODO: show throw an Error here, to be fixed in v5
              error(
                `Error loading ${chalk.bold(fileConfigPath)}: should export an object or a function that returns object.`
              )
              fileConfig = null
            }
          } catch (e) {
            error(`Error loading ${chalk.bold(fileConfigPath)}:`)
            throw e
          }
        }
    
        // package.vue
        pkgConfig = this.pkg.vue
        if (pkgConfig && typeof pkgConfig !== 'object') {
          error(
            `Error loading vue-cli config in ${chalk.bold(`package.json`)}: ` +
            `the "vue" field should be an object.`
          )
          pkgConfig = null
        }
    
        if (fileConfig) {
          if (pkgConfig) {
            warn(
              `"vue" field in package.json ignored ` +
              `due to presence of ${chalk.bold('vue.config.js')}.`
            )
            warn(
              `You should migrate it into ${chalk.bold('vue.config.js')} ` +
              `and remove it from package.json.`
            )
          }
          resolved = fileConfig
          resolvedFrom = 'vue.config.js'
        } else if (pkgConfig) {
          resolved = pkgConfig
          resolvedFrom = '"vue" field in package.json'
        } else {
          resolved = this.inlineOptions || {}
          resolvedFrom = 'inline options'
        }
    
        if (resolved.css && typeof resolved.css.modules !== 'undefined') {
          if (typeof resolved.css.requireModuleExtension !== 'undefined') {
            warn(
              `You have set both "css.modules" and "css.requireModuleExtension" in ${chalk.bold('vue.config.js')}, ` +
              `"css.modules" will be ignored in favor of "css.requireModuleExtension".`
            )
          } else {
            warn(
              `"css.modules" option in ${chalk.bold('vue.config.js')} ` +
              `is deprecated now, please use "css.requireModuleExtension" instead.`
            )
            resolved.css.requireModuleExtension = !resolved.css.modules
          }
        }
    
        // normalize some options
        ensureSlash(resolved, 'publicPath')
        if (typeof resolved.publicPath === 'string') {
          resolved.publicPath = resolved.publicPath.replace(/^\.\//, '')
        }
        removeSlash(resolved, 'outputDir')
    
        // validate options
        validate(resolved, msg => {
          error(
            `Invalid options in ${chalk.bold(resolvedFrom)}: ${msg}`
          )
        })
    
        return resolved
      }
    }
    
    function ensureSlash (config, key) {
      const val = config[key]
      if (typeof val === 'string') {
        config[key] = val.replace(/([^/])$/, '$1/')
      }
    }
    
    function removeSlash (config, key) {
      if (typeof config[key] === 'string') {
        config[key] = config[key].replace(/\/$/g, '')
      }
    }
    
    function cloneRuleNames (to, from) {
      if (!to || !from) {
        return
      }
      from.forEach((r, i) => {
        if (to[i]) {
          Object.defineProperty(to[i], '__ruleNames', {
            value: r.__ruleNames
          })
          cloneRuleNames(to[i].oneOf, r.oneOf)
        }
      })
    }
    
    
    • run 方法内通过 init 方法加载环境变量、加载用户配置、应用插件
    • init 方法内通过 loadenv 方法来加载env
    • loadenv 中通过 dotenv 库实现获取文件内的变量,并设置。
  • 6、得到结论:vue通过dotenv依赖,实现可以在 .env 中配置环境变量

2.3、.env 文件的优先级
  • 环境变量是就是一个特殊的变量,所以设置环境变量,就是一个赋值的过程。

    • 那么所谓的优先级有两种情况
      • 1、重复赋值
      • 2、取值问题
  • 重复赋值

    • 1、我们看一下源码

          // load mode .env
          if (mode) {
            this.loadEnv(mode)
          }
          // load base .env
          this.loadEnv()
      
      
       const basePath = path.resolve(this.context, `.env${mode ? `.${mode}` : ``}`)
       const localPath = `${basePath}.local`
       load(localPath)
       load(basePath)
      
    • 2、通过两部分源码可以看出

      • 先加载 .env.xxxx 再加载 .env
      • 先加载 .xxxx.local 再加载 .xxxx
    • 3、所以 这里不是使用 重复赋值的方式实现优先级的

  • 取值问题

    • 1、在 loadEnv 里面,有使用 dotenv.config 方法,去看看他们的源码

    • 2、 dotenv.config 的部分源码

       Object.keys(parsed).forEach(function (key) {
            if (!Object.prototype.hasOwnProperty.call(process.env, key)) {
              process.env[key] = parsed[key]
            } else if (debug) {
              log(`"${key}" is already defined in \`process.env\` and will not be overwritten`)
            }
          })
      
    • 3、从这里可以看到, process.env 没有的key才会进行赋值

    • 4、结合上面提到的加载顺序,就能得到结论

      • .env < .env.local < .env.xxxx < .env.xxxx.local< npm run xxx 之前process.env已有的变量
2.4、vue文件中为什么能访问环境变量?为什么只能访问 NODE_ENVBASE_URL 和以 VUE_APP_ 开头的变量?
  • 前端打包实际上靠的是 webpack(这里细说 webpack了,简单理解它能将前端项目重新整理为新的静态文件供浏览器加载即可),查看webpack文档
    • https://webpack.js.org/plugins/environment-plugin/
  • vue项目中引用 process.env的地方,会被 webpack打包时替换为具体的值。
举报

相关推荐

0 条评论