说明: 本文代码实现以vue为主,其他框架思路相同
场景一: 多环境配置
我们用vue-cli生成项目后,vue给我们仅提供了一个启动的命令和一个打包命令(如果你选择了单元测试等其他选项,还会有其他命令,但本篇文章我们不讨论它),这个启动命令是开发环境的,打包打的是生产环境的包。
但是如果我们还有测试环境以及预发布环境呢,这两个环境都是线上的,测试环境的接口、预发布环境的接口以及生产环境的接口都是不相同的,但除了开发环境外其他环境都是线上的,这个时候一个npm run build
似乎有点不够用了呢,因为我们运行npm run build
永远打的是生产环境包(也就是说发布到线上后你向后台请求的是生产环境的接口)。当然你也可以每次都手动打包,打包之前改一下接口地址,但是这样似乎有点low啊,而且如果你除了要改接口地址外还有其它部分需要改呢,这样就有点麻烦了。
所以我们场景一的目标,就是给不同的环境配置一个不同的打包命令,然后打不同环境的包,解决每次打包前修改配置,这样还有个好处就是如果你司采用Jenkins类的自动化部署工具,那么我们就可以提供不同的命令给Jenkins,然后彻底解放双手。
好,不说废话,我们直接开始解决问题;
vue-cli 2.X配置多环境打包命令
1,项目安装cross-env
,cross-env是node的一个设置和使用环境变量的脚本;
npm install cross-env -D
2,在项目的package.json
文件中,把scripts
对象的build
字段的值改为以下代码,实际就是修改npm run build
命令。同时再添加npm run build:test
命令和npm run build:pre
命令。
"scripts": {
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
"start": "npm run dev",
"build": "cross-env env_config=prod node build/build.js",
"build:test": "cross-env env_config=test node build/build.js",
"build:pre": "cross-env env_config=pre node build/build.js"
},
简单说一下,与原文件相比,我们改了build
字段的值,同时添加了build:test
和build:pre
属性,这样做之后相当于我们加了npm run build:test
和npm run build:pre
命令,我准备当运行npm run build
时打生产环境包,运行npm run build:test
时打测试环境包,npm run build:pre
打预发布环境包。
与原命令相比,我在node build/build.js
前加了cross-env env_config=prod
这点内容,这段东西主要在设置环境变量,可以在/build/build.js
文件内console.log('查看环境变量-------->', process.env.env_config)
,然后运行打包命令时会在控制台打印出来。
3,在项目的/build/build.js
文件内找到const spinner = ora('building for production...')
这行代码,将其改为const spinner = ora(`正在打${process.env.env_config}环境包...`)
,改这个主要是为了打包的时候方便知道正在打那个环境的。
4,在项目的/config/prod.env.js
文件内,将其内容修改。
'use strict'
module.exports = {
prod: {NODE_ENV: '"production"'},
test: {NODE_ENV: '"testing"'},
pre: {NODE_ENV: '"pre-release"'},
}
5,继续在项目的/build/webpack.prod.conf.js
文件内找到
new webpack.DefinePlugin({
'process.env': env
})
这段代码,然后将其改为
new webpack.DefinePlugin({
'process.env': env[process.env.env_config]
})
6,2.x修改完成,然后测试一波,在main.js
中写个判断打印出来看一波。
import Vue from 'vue'
import App from './App'
import router from './router'
Vue.config.productionTip = false
if (process.env.NODE_ENV === 'production') {
console.log('生产环境');
} else if(process.env.NODE_ENV === 'testing') {
console.log('测试环境');
} else if(process.env.NODE_ENV === 'pre-release') {
console.log('预发布环境');
}
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})
然后npm run build:pre
打个包,然后线上环境布一下,然后在控制台应该可以打印出来预发布环境
;
以上判断代码仅为测试效果,实际开发过程中,应该对请求进行单独封装,对请求域名设为变量,根据环境不同给变量设置不同的值。
vue-cli 3.X配置多环境打包命令
1,在项目根目录下新建3个文件,.env.test
、.env.prod
、.env.pre
,在文件内分别写入NODE_ENV = testing
、NODE_ENV = production
、NODE_ENV = pre-release
。
2,在项目的package.json
文件中,把scripts
对象的build
字段的值改为以下代码,实际就是修改npm run build
命令。同时再添加npm run build:test
命令和npm run build:pre
命令。(本段文字Ctrl C
自2.x第二步)
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build --mode prod",
"build:test": "vue-cli-service build --mode test",
"build:pre": "vue-cli-service build --mode pre",
"lint": "vue-cli-service lint"
}
3,完成2.X的第6步
4,3.x的实现比2.x简化了很多,官方文档说的很详细【传送门】,但是我们在项目内加了3个文件,每个文件又仅有一行代码,这就让人感觉很low了,如果有七八个环境我们不是需要加七八个文件,作为一个强迫症患者,怎么能容忍这种情况。所以在场景二中,我们会尝试使用一个文件来区分多个环境。
场景二:
基于场景一的情况下,我们目前开发了一套系统,现在计划拿这套系统去售卖,但每个公司都有不同的需求,我们的系统只能满足大部分功能,并不能完全满足所有公司。
这时候有超能力的公司就希望能基于我们我的系统再做些个性化的定制功能,这种情况下我们可以以我们通用系统为基本新起一个工程做定制化开发,或者在通用工程上再拉一个子分支做开发,但有定制化需求的公司数量很少还好,如果有几十个呢,如果通用工程做了迭代或者改了bug,那么不可避免会出现我们可能需要维护好多套相似类型的代码,那这个工程量就是非常巨大的,费时费力,我们场景二的目标就是解决这个问题。
所以如果我们可以把所有的定制化都写在通用版本的项目中,然后根据不同的打包命令可以打包出不同的系统,那么我们就可以只维护一套代码,从而完美解决问题。
具体的我们需要为每一个定制化版本写一个配置文件,当我们打包的时候引用不同的配置文件打不同的生产包。
备注: 实际开发过程中如有遇到这种场景,各个功能模块一定要解耦,解耦后就可以根据功能模块组合出各种功能不一的系统,一定要解耦,这点也很重要
假设: 由于实际项目不同,所以定个假设来解决问题。假设我们我们这个工程有10个页面(实际开发过程中应该是模块),其中5个页面是通用的,5个页面是A、B、C、D、E五家公司定制的,同时每家定制版要求系统上有他们公司logo、页面title加上公司名称。接下来我们解决这个问题
1,先在项目新建10个页面,同时为每个页面再建个路由
结构差不多如上。实际项目开发过程中,一个文件夹应该为一个功能模块,模块内在进行其他划分
2,在src目录下再新建个config文件夹,再接着在内部新建5个配置文件和一个index入口文件 配置文件内容
入口文件内容这步比较重要,首先我们在不同的配置中引入了不同的页面,通用页面之所以引入到了配置文件内,考虑到实际开发中有可能存在客户不需要这个功能,方便新加和删除。入口文件的作用是在之后的开发中我们只需要引入配置入口文件就可以了,当切换定制版的时候只需要修改入口文件就可以实现切换,方便快捷,如果说在需要配置文件的直接引用配置文件,那当我们切换定制版的时候改起来就是个很耗费精力的一件事了,所以入口文件在这里主要起个代理作用
3,在主路由文件内引入
4,同时由于每个定制版的页面标题都不一样,所以我们需要给路由加点东西,修改一下。
5,至此,我们完成了使用一套代码维护‘多个系统’,如果要切换为B公司的版本,我们只需要更换config/index.js
中的引入文件为B公司的配置。
6,这个样子每次打包不同定制版的时候还需要在config/index.js
中再改一行代码,这也很麻烦啊,能不能把这步也省略。肯定可以,接下来继续实现。
7,在项目根目录下再建个script
文件夹,然后在里面新建个build.js
文件,然后把打包命令再修改下。
8,再npm install -D shelljs inquirer chalk
,shelljs
是用来执行命令的,inquirer
是用来写交互式命令行的,chalk
是用来装扮命令行命令的(比如在命令行输入个红色的字)。说一下我们想要实现的效果,当我们在运行npm run build
的时候,我希望可以让我来选择我要部署什么样的版本,部署那个环境,我选择完成之后,它帮我修改我的配置文件修改完成之后再运行vue的打包命令进行打包。
9,安装完成之后,我们在build.js
文件写入以下内容;
const shell = require('shelljs');
const inquirer = require('inquirer');
const chalk = require('chalk');
const fs = require('fs');
const writeFile = (path, content) => {
return new Promise((reslove, reject) => {
fs.writeFile(path,
content,'utf8', () => {
reslove(true);
});
})
}
const release = new Map([
['customizeA定制版', "export * from './customizeA';"],
['customizeB定制版', "export * from './customizeB';"],
['customizeC定制版', "export * from './customizeC';"],
['customizeD定制版', "export * from './customizeD';"],
['customizeE定制版', "export * from './customizeE';"],
])
const env = new Map([
['生产环境', "NODE_ENV = production"],
['预发布环境', "NODE_ENV = pre-release"],
['测试环境', "NODE_ENV = testing"],
])
const build = async () => {
const res = await inquirer.prompt([
{
type: 'list',
name: 'release',
message: '请选择你要部署的版本?',
choices: ['customizeA定制版', 'customizeB定制版', 'customizeC定制版', 'customizeD定制版', 'customizeE定制版']
},
{
type: 'list',
name: 'env',
message: '请选择你要部署的环境?',
choices: ['生产环境', '预发布环境', '测试环境']
},
]);
await Promise.all([writeFile(`${process.cwd()}/src/config/index.js`, release.get(res.release)),writeFile(`${process.cwd()}/.env`, env.get(res.env))]);
console.log(chalk.green(`您要打包的是${res.env}---${res.release},正在为您打包......`));
shell.exec('vue-cli-service build');
}
build();
简单说一下我们究竟做了什么,首先当我们运行npm run build
的时候,实际就是用node去执行/script/build.js
文件去了,然后文件内部先执行build()
函数,然后会让用户选择版本和环境,用户选择完成我们会拿到用户的选择结果res
,根据用户的选择结果我们找一下究竟要引入的配置文件和要设置的环境变量,然后把内容写入到对应的文件中,另外说一下writeFile
是我们对node写入文件做的Promis
封装,最后写入完成我们就去执行打包命令了。
OK!完成!看下效果!
这个亚子确实没问题,使用也很方便!但如果我们使用Jenkins进行自动化部署,把这个命令给运维,让运维在Jenkins进行配置上下键选择,回车键确认的话,一定要穿好防护服或者给120提前打个电话,毕竟我第一次兴冲冲把这个操作给运维的时候,被运维追了十八条街,差点没被打死。
10,所以,为了自己安全,我决定改成命令直接部署。但是配置多个命令有超级难受,所以可不可以一个命令就搞定环境与版本选择,别说还真行,所以开搞。
首先npm install -D
11,在script
文件夹内在新建个cdmBuild.js
文件,在文件内这么写
const shell = require('shelljs');
const commander = require('commander');
const chalk = require('chalk');
const fs = require('fs');
const writeFile = (path, content) => {
return new Promise((reslove, reject) => {
fs.writeFile(path,
content,'utf8', () => {
reslove(true);
});
})
}
const Release = new Map([
['customizeA', "export * from './customizeA';"],
['customizeB', "export * from './customizeB';"],
['customizeC', "export * from './customizeC';"],
['customizeD', "export * from './customizeD';"],
['customizeE', "export * from './customizeE';"],
])
const Env = new Map([
['production', "NODE_ENV = production"],
['pre-release', "NODE_ENV = pre-release"],
['testing', "NODE_ENV = testing"],
])
const subcommand = commander.command('build <release> <env>');
subcommand.action(async (release, env) => {
if (!Release.has(release)) {
console.log(chalk.red(`傻子,没有${release}这个版本`));
return;
}
if (!Env.has(env)) {
console.log(chalk.red(`傻子,没有${env}这个环境`));
return;
}
await Promise.all([writeFile(`${process.cwd()}/src/config/index.js`, Release.get(release)),writeFile(`${process.cwd()}/.env`, Env.get(env))]);
console.log(chalk.green(`您要打包的是${env}---${release},正在为您打包......`));
shell.exec('vue-cli-service build');
});
commander.parse(process.argv);
12,在package.json
文件内修改下打包命令
13,之后的打包命令就变成了npm run build:cmd -- build <release> <env>
,release代表版本,env代表环境,如果我们要打包测试环境customizeD定制版,那么我们的命令就是npm run build:cmd -- build customizeD testing
。
14,大功告成,所以这么长又这么难记的命令就交给运维去配置了,如果我们自己用就npm run build
就好。
写在最后: 本文代码载体是vue,但解决思路适用于所有相同场景。本文重点在于解决思路不在于代码实现。
写在最后: 本篇文章至此就写完了,因为内容过多所以有些地方不是特别详细,遇到不明白的多百度多谷歌。同时,解决问题的方式从来不止一种,本文所述的方案,不过是我在实际开发中所遇到的场景自己的解决方式,如果你在实际开发中遇到了相同的场景可以使用我的方式,但请不要停止思考,也许你也会想到更好的解决方案。作为一名代码人实际开发中我们会遇到各种问题,遇到问题的时候,一定多思考,只要你能想到解决方式,剩下的不过是用代码翻译思想的过程,加油!
写在最后: 求给本文起标题,给场景二起标题,我自己起标题的水平实在是太惨不忍睹了。
另外!如果本文对你有帮助也请帮我点个赞,感谢!感谢!感谢!赞!赞!赞!你的一个赞对我很重要
本文所用第三方工具文档:
【Commander.js】、【shelljs】、【chalk.js】、【inquirer.js】
也许你对我的其他文章也有兴趣: