likes
comments
collection
share

vue项目多环境配置方案

作者站长头像
站长
· 阅读数 40

简介

项目:使用vue-cli-service作为启动devServer以及打包构建工具的vue项目。

需求场景:

  • 开发:启动各环境的devServer,联调时可以在本地调用不同环境的后端接口
  • 构建:打包各环境的代码,不同环境对应的后端服务器地址不同,部署后各环境的前端项目会调用对应环境(开发、测试、uat、生产)的后端接口

开发过程中经常会出现以上使用场景,基于该背景,以下是从vue-cli-service源码出发梳理一套vue项目多环境配置方案过程中的一些记录(查看具体方案的可跳到最后的"方案实现"一节)

Mode

vue项目的多环境配置中需要重点关注的是vue-cli-service中的Mode(环境模式)的概念,vue-cli-service中有三种模式:

  • development:vue-cli-service serve执行时的默认模式。

  • test:vue-cli-service test:unit执行时的默认模式,安装@vue/test-utils插件后会注入该模式。

  • production:vue-cli-service build执行时的默认模式。 在执行命令时没有通过--mode命令行标识去指定模式的情况下,vue-cli-service会根据执行命令时不同的参数来推断一个默认的模式。

Mode与Environment

Mode的几个枚举值很容易与环境(Environment)的概念混淆,Mode有development/test/production几种,Environment可以有很多种,常见的有开发(development)/测试(test)/uat/生产(production)

  • Mode

    • 不同Mode下vue-cli-service会执行不同的操作,如development模式可以理解为当前执行vue-cli-service命令是在开发场景中执行的,vue-cli-service会自动生成一份针对开发优化的webpack配置并启用devServer,使得可以在本地打开项目以供调试。
    • development和production模式下各自生成的webpack配置会不一样,production模式会针对打包的代码进行优化,如production模式会添加代码压缩、分包、文件名添加hash值等配置。
  • Environment

    • 不同环境指的是打包出来的代码的运行环境,使用场景有:在不同环境中的代码调用的后端接口地址可能不同,如在开发服务器上的前端项目调用后端接口域名为https://dev.xxx.com/apis,测试服务器上部署的前端项目调用的后端接口域名为https://test.xxx.com/apis

    若概念混淆,当执行vue-cli-service build时Mode设置为了development而不是production,那么部署后的代码可能会出现没有代码压缩(体积很大)、文件名后没有hash值(每次更新都需要用户清空缓存刷新才能生效)等问题。

执行vue-cli-service xxx后发生了什么

该节只介绍mode相关的内容,其他部分不过多赘述。

PS:本节内容搭配@vue/cli-service的源码进行食用口感最佳。

1、入口

vue-cli-service命令的入口文件为@/vue/cli-service/bin/vue-cli-service.js,即执行vue-cli-service命令背后逻辑是执行该文件,查看可知通过vue-cli-service xxx传入的参数经过minimist处理后会传入service.run方法中。

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'
  ]
})
// vue-cli-service xxx命令后第一个参数,正常情况下为serve/build/test:unit
const command = args._[0]

service.run(command, args, rawArgv).catch(err => {
  error(err)
  process.exit(1)
})

2、确定mode

# @vue/cli-service/lib/Service.js
class Service{
    // modes为初始化过程中通过resolvePlugins方法加载@vue/cli-service/lib/commands/下的文件提供,具体可细看constructor中的内容,此处直接将加载后的结果展示以供理解mode的确定逻辑
    modes={
        "serve": "development",
        "test:unit": "test",
        "build": "production"
    }
    // name=serve|build|test:unit
    run(name, args = {}, rawArgv = []) {
        const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name])
    }
}

如上可知mode的推断逻辑(按优先级排列):

  • vue-cli-service serve --mode development:通过--mode指定mode优先级最高
  • vue-cli-service build --watch:执行该命令mode默认指定为development
  • 其余场景根据modes[name]自动匹配

3、初始化环境变量

调用service.loadEnv方法并通过dotenv库依次读取并加载项目根目录下的.env.[mode].local.env.[mode]文件,将内部的变量一一分配到process.env中。

4、确定NODE_ENV

# @vue/cli-service/lib/Service.js Service.loadEnv()
const defaultNodeEnv = (mode === 'production' || mode === 'test') ? 
      					mode : 'development'
if (process.env.NODE_ENV == null) {
    process.env.NODE_ENV = defaultNodeEnv
}
if (process.env.BABEL_ENV == null) {
    process.env.BABEL_ENV = defaultNodeEnv
}

以上为定义process.env.NODE_ENV以及process.env.BABEL_ENV的逻辑。此部分用到的mode为“确定mode”一节中所得到的值。

PS(PS又来了):由上可知mode会影响process.env.NODE_ENV以及process.env.BABEL_ENV值的确定。后续vue-cli-service生成webpack配置、使用babel的过程中会用到这两个变量,这就解释了为什么mode的正确使用这么重要。如只有当process.env.NODE_ENV=production时打包后的代码才会有代码压缩、文件名添加hash等效果。 除非目的明确,否则不要在env文件中指定NODE_ENV,否则在如“初始化环境变量”一节中所示,提前通过加载env文件指定了process.env.NODE_ENV的话会影响默认NODE_ENV值的定义。

webpack config对比

通过vue-cli-service inspect --mode xxx可以得到development和production模式下生成的webpack配置信息,部分区别如下:

vue项目多环境配置方案

vue项目多环境配置方案

vue项目多环境配置方案

方案实现

新目录结构如下:

vue项目多环境配置方案

环境相关变量文件从根目录移动至cli/environments/文件夹下,vue-cli-service原有逻辑会根据mode读取根目录下env文件。 cli/environments/下的env文件存放Environment相关变量,如:

# cli/environments/.env.dev
# 用于识别当前环境
VUE_APP_ENV=dev
# 接口服务器地址
API_HOST=https://dev.xxx.com/apis

以下是cli/index.js的内容,根据@vue/cli-service/bin/vue-cli-service.js改造,添加了加载指定位置env文件的逻辑

# cli/index.js
const { semver, error } = require("@vue/cli-shared-utils");
const requiredVersion = require("@vue/cli-service/package.json").engines.node;
const dotenv = require("dotenv");
const dotenvExpand = require("dotenv-expand");
const path = require("path");

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("@vue/cli-service/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];
const env = args.env || "dev";
loadEnv(env);

service.run(command, args, rawArgv).catch((err) => {
  error(err);
  process.exit(1);
});

/**
 * @description: 加载env文件
 * @param {String} env 环境(dev|test|uat|prod)
 */
function loadEnv(env) {
  try {
    const envOptions = ["dev", "test", "uat", "prod"];
    if (!envOptions.includes(env)) {
      throw new Error(
        `env: ${env} is invalid, options: ${JSON.stringify(envOptions)}`
      );
    }
    const envPath = path.resolve(process.cwd(), `cli/environments/.env.${env}`);
    const localPath = `${envPath}.local`;
    // 加载.env.local文件
    const localEnvConfig = dotenv.config({
      path: localPath,
      debug: process.env.DEBUG,
    });
    dotenvExpand(localEnvConfig);
    // 加载env文件
    const envConfig = dotenv.config({ path: envPath, debug: process.env.DEBUG });
    dotenvExpand(envConfig);
  } catch (err) {
    // 忽略文件不存在错误
    if (err.toString().indexOf("ENOENT") < 0) {
      error(err);
      process.exit(1)
    }
  }
}

最后修改package.json文件中的scripts:

# package.json
{
  "scripts": {
    "serve:dev": "node ./cli serve --env dev",
    "serve:test": "node ./cli serve --env test",
    "serve:uat": "node ./cli serve --env uat",
    "serve:prod": "node ./cli serve --env prod",
    "build:dev": "node ./cli build --env dev",
    "build:test": "node ./cli build --env test",
    "build:uat": "node ./cli build --env uat",
    "build:prod": "node ./cli build --env prod"
  }
}

总结

总结一下这套方案,主要做了以下工作:

  • 区分环境变量文件模式变量文件,模式变量文件放在项目根目录,vue-cli-service会自动加载。环境变量文件移动到/cli/environments/目录下,由/cli/index.js中添加的逻辑单独加载处理。
  • 新增/cli/index.js文件,vue-cli-service的原有逻辑迁移至该文件,并添加加载/cli/environments/目录下的环境变量文件的逻辑,手动处理需要的环境变量文件。

如有其他想法,欢迎指教

转载自:https://juejin.cn/post/7043384831366922277
评论
请登录