Vue多项目打包
Star Sea

导语

有时业务中遇到这样的场景,多个项目有很多共用的逻辑,后面又是独立部署。例如PC端和移动端,UI完全不同,但逻辑有很多能复用,但是这里我们计划是分开部署。这里仅说有这种场景,大致痛点如下。

  1. 分两个项目,则代码维护两套,复用逻辑要copy
  2. 合成一个项目:路由区分或者多页配置。这种上线部署的时候又会互相影响,部署则都刷新。且目录在一起,可扒取到另外一个项目的资源。

为此,需要一种方式,能代码共仓以复用逻辑,但是打包部署可以独立。

实现

实现该需求,有如下考虑:

  1. 既然可独立打包,那这里考虑打包的时候传参
  2. 多项目在打包配置上可能存在差异,可针对不同项目的灵活配置是必要的
  3. 涉及到部署,那部署的情况也是要考虑到的

    约定

    为方便表述,这里做个约定,我们的项目分为mobile、pc两个。

打包传参的处理

命令行打包,做以下区分:

1
2
3
4
5
6
7
8
9
10
11
# mobile
# 本地运行
npm run dev -- --page=mobile
# 打包
npm run build -- --page=mobile

# pc
# 本地运行
npm run dev -- --page=pc
# 打包
npm run build -- --page=pc

以上是参数的传入,接着解析参数,做成通用的,形如 --key=value的都可以被解析。
这里我们将参数写成 VUE_APP_PAGE的形式,这样项目代码逻辑中也可以使用这个参数,以便处理复用代码要区分不同项目的场景。
同时,这里实现getEnv函数,以便在其他打包逻辑中可以获取到自己想要的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 使用正则解析参数
function handleEnv() {
const reg = /--(\w+)=(.+)/;
const keysArr = process.argv.filter( e => reg.test(e));
keysArr.forEach( varStr => {
const varArr = varStr.match(reg);
process.env[`VUE_APP_${varArr[1].toLocaleUpperCase()}`] = varArr[2];
});
}

// 获取对应的参数,如果key不传则返回所有的,这里做一次缓存代理,避免重复解析
const getEnv = (() => {
let isHandleEnv = false;
return (key) => {
if (!isHandleEnv) {
handleEnv();
isHandleEnv = true;
}
return key ? process.env[key] :process.env;
}
})();

以上已经拿到了参数,接下来是参数用起来

处理打包配置

这里总的逻辑还是使用了pages配置项,不过每次只打包了一个page。首先说明文件,为了方便自定义配置,这里我们拆分文件:

  • common.config.js用来存储多个项目公用的配置
  • custom.config.js用来存储不同项目自定义的配置
  • vue.config.js合并common、custom的配置,生成最终的打包配置

common.config.js的配置举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
module.exports = {
publicPath: './',
assetsDir: 'static',
productionSourceMap: false,
lintOnSave: true,
devServer: {
host: '0.0.0.0',
port: 3000,
hotOnly: true,
proxy: {
'/': {
ws: true,
target: 'http://test.myproject.com/',
changeOrigin: true,
secure: false,
},
}
},
configureWebpack: config => {
// 这里写自定义的webpack配置
},
chainWebpack(config) {
// 这里我们设置了一下别名
config.resolve.alias
.set('@pc', resolve('./src/pc'))
.set('@mobile', resolve('./src/mobile'))
.set('@src', resolve('./src'))
}
}

custom.config.js的配置举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
module.exports = {
// 移动端的自定义配置
mobile: {
// page的自定义配置,后面会和默认的配置合并
page: {
title: 'mobile title'
}
},
// pc端的自定义配置
pc: {
page: {
title: 'pc title'
},
chainWebpack(config) {
// 这里我们设置了一下pc端的svg打包,但是移动端不需要,因此是在自定义配置
config.module
.rule('svg')
.exclude.add(resolve('src/platform/icons'))
.end()
config.module
.rule('icons')
.test(/\.svg$/)
.include.add(resolve('src/platform/icons'))
.end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({
symbolId: 'icon-[name]'
})
.end()
}
},
}

vue.config.js的配置,这块是重点,会结合上面两个配置生成最终的配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
const path = require('path');
const fs = require('fs');

// 导入以上两份配置
const commonConfig = require('./common.config')
const customConfig = require('./custom.config')
const { getEnv } = require('./utils');

// 获取到了当前的page参数
let pageName = getEnv('VUE_APP_PAGE');

// 我们将pc和mobile两个项目放到了 `src/pages` 目录下
const dirs = fs.readdirSync(path.join(__dirname, 'src/pages'))

// 异常处理,当pageName没匹配到时默认做pc处理
if (!dirs.includes(pageName)) {
console.warn(`${pageName} is not exist`);
pageName = 'pc';
}

// 获取对应页面的自定义配置,这里page参数单独拿出作为后面pages配置的合并,其他参数走通用的合并
const { page, ...pageCustomConfig } = customConfig[pageName] || { page: {} };

// 根据pageName生成对应的pages配置,这里合并上一步的page参数,目的是当两个项目index.html使用一份的时候,可以不放置两个html,而是使用一个,但是可以通过page参数指定title参数区分
const defaultPageConfig = {
pages: {
[pageName]: {
entry: `src/${pageName}/main.js`,
template: `public/${pageName}.html`,
filename: 'index.html',
...page
}
},
// ☆ 这里是关键,打包到dist下面对应项目的子目录,这样可以分开打包两个项目,互不影响
outputDir: `dist/${pageName}`
}

// 这里的config就是我们生成的基本配置啦,但是custom的配置还没处理
const config = {
...baseConfig,
...defaultPageConfig,
}

// 遍历自定义配置pageCustomConfig
for (const key in pageCustomConfig) {
if (Object.hasOwnProperty.call(pageCustomConfig, key)) {
// 这里拿到的配置有可能是值、也有可能是函数(例如:chainWebpack)
const valueOrFunction = pageCustomConfig[key];

// 当自定义页面配置为函数时,默认会合并自定义配置和baseConfig中的配置。
if (valueOrFunction instanceof Function) {

// 拿到了通用配置中相同项,做函数合并。
const baseFunction = config[key] || function() {};
config[key] = function() {
// 先调用自定义的配置的函数,如果返回true则代表自定义配置完全覆盖common配置,否则两者合并
const isReplaceBase = valueOrFunction.apply(null, arguments);
// 两者合并的情况,调用common中的配置
!isReplaceBase && baseFunction.apply(null, arguments);
}
} else {
// 不是函数,则直接覆盖
config[key] = valueOrFunction;
}
}
}

module.exports = config;

另外,有些情况可能仅在上面的自定义配置中无法完成,例如这里要修改postcss的配置完成移动端rem的配置,可以修改postcss.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const { getEnv } = require('./utils')

const plugins = {
autoprefixer: {},
}

// 使用 getEnv 获取当前的page参数,仅在移动端设置rem转化
if (getEnv('VUE_APP_PAGE') === 'mobile') {
plugins['postcss-pxtorem'] = {
rootValue: 16,
propList: ['*'],
unitPrecision: 5,
}
}

module.exports = {
plugins
}

package.json的修改

以上就完成了自定义的打包,但是本地运行还要每次传参就比较麻烦,改下package.json让项目更好用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"scripts": {
// 默认启动pc
"dev": "vue-cli-service serve",
// 启动移动端
"dev:collection": "npm run dev -- --page=mobile",
// 默认打包pc
"build": "vue-cli-service build",
// 两者一起打包
"build:all": "npm run build && npm run build:mobile",
"build:pc": "vue-cli-service build",
// 单独打包移动端
"build:mobile": "npm run build -- --page=mobile",
}
}

部署

完成以上打包之后,考虑下部署的事情。之前可能部署的方式是直接删除旧的前端包,部署新的。那现在情况不太一样了,pcmobile都要部署则可以这么干,但是如果只是部署mobile,那之前前端包中的pc就不能删除,这里我们改写下部署的 bash脚本。可能你不是bash脚本,但是原理一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#!/bin/bash
# 指定项目目录名称
PROJECT_DIR=my-test-project
# 指定项目目录放置的地方
BASE_DIR=/home/www
# 解压比对的临时目录
PROJECT_DIR_TEMP=${BASE_DIR}/${PROJECT_DIR}-temp
# 项目的根目录
PROJECT_ROOT_DIR=${BASE_DIR}/${PROJECT_DIR}

# 处理部署的时候新建项目目录
if [ ! -d ${PROJECT_ROOT_DIR} ];then
mkdir -p ${PROJECT_ROOT_DIR}
fi

# 创建临时目录
if [ ! -d ${PROJECT_DIR_TEMP} ];then
mkdir -p ${PROJECT_DIR_TEMP}
fi

# 拷贝前端包到临时目录,并解压操作
cp ${PROJECT_DIR}.zip ${PROJECT_DIR_TEMP}
cd ${PROJECT_DIR_TEMP}
unzip ${PROJECT_DIR}.zip
rm ${PROJECT_DIR}.zip

# 此处遍历前端包中的目录,有以下情况
# 1. 有pc、mobile两个目录,则是两个项目一块部署,之前两个目录均删除
# 2. 仅有一个目录,如仅有mobile目录,则只部署移动端,pc端代码不部署
for file in $PROJECT_DIR_TEMP/*
do
if [ -d "$file" ]
then
# 此处读取目录的名称,匹配最后一个 / 之后的名称
page_name=${file##*/}
echo $page_name

# 如果在项目根目录下存在当前目录,则删除掉该旧目录
if [ -d "$PROJECT_ROOT_DIR/$page_name" ]
then
rm -r "$PROJECT_ROOT_DIR/$page_name"
fi
fi
done

# 删除之后将临时目录下解压的文件全部移到项目根目录
mv $PROJECT_DIR_TEMP/* $PROJECT_ROOT_DIR

# 删除临时目录
rm -r $PROJECT_DIR_TEMP