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 build
和vue-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_ENV
,BASE_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
方法来加载envloadenv
中通过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_ENV
,BASE_URL
和以 VUE_APP_
开头的变量?
- 前端打包实际上靠的是
webpack
(这里细说webpack
了,简单理解它能将前端项目重新整理为新的静态文件供浏览器加载即可),查看webpack文档- https://webpack.js.org/plugins/environment-plugin/
- vue项目中引用
process.env
的地方,会被webpack
打包时替换为具体的值。