一. 需求
公司要做很多商场的会员小程序,包含九宫格、微门户两个基础的版本。其中有大部分的功能和页面是相同的,每一家还有一些定制的页面。之前都是分开分支做的,一个商场一个分支。如果是每一家定制的页面还好说,但如果是要添加通用的功能或者核心业务有改动的话,得对每套代码分别改动一次。容易有遗漏很不方便。基于这个原因,要做一个小程序多项目管理的研发。
二. 设计思想
之前本来想到使用webpack的copy-webpack-plugin插件来移动目录到最终目录中,但是有很多需要替换的东西,比如小程序appId、图片、主题色、需要的模块拼接成app.json等等。后来想到了node的shell.js,可以使用此库来完成上述操作。
三. 目录解释
3.1 主要目录解释
- apps:存放各个商场的小程序
- assets:存放公用的css、图片、js、工具库
- builds:执行的主要js文件
- commonPages:公用的功能和页面
- commonProjectJson:配置项(门店信息、小程序的project.config.json)
- components:公用组件
- dist:打包后真正要在微信开发者工具的代码
3.2 apps目录解释
- images:各个商场的个性化图片
- pages:定制的页面(如果是完全定制的json、wxml、wxss、js文件都要写,如果只是页面跟样式有变化只需要wxml、wxss文件)
- base64.js:用来存储小程序所用的背景图片 使用对象格式存储 {carBg:xxx} 使用module.exports = base64Json导出。
- config.js:用来存储各种背景颜色、配置项、需要的功能、票据等。
3.3 asstes目录解释
- css:公用的css
- images:公用的图片
- js:公用的js(工具方法、生成二维码的库)
- wxParse:富文本解析库
3.4 commonPages目录解释
-
pageageA:分包(有独立的一些功能。比如抽奖,可以采用分包的形式)
-
pages:共用的页面
-
app.js:共用的app.js 包括全局的一些变量(门店信息、请求环境等)以及共用方法
-
app.wxss:共用的样式
3.5 commonProjectJson目录解释
- mall.json:所有的门店信息(小程序appId、集团编号、门店编号、门店Id、跳转到别的小程序的appId跟环境、版本等)
- project.config.json:最基本的项目配置文件。其中appid使用变量@appId来赋值,方便后续替换。
四:核心代码完成
1. 关于打包
打包有两种方式。
一种是在命令中直接输入门店名称跟请求环境(npm run build 智慧全景 test)为什么还需要输入请求环境呢?因为会有给特定的商场提供定制功能,但只能用公司的小程序测试后才能上线。所以会监听如果是test的话就会替换小程序appId跟门店信息到公司的小程序进行测试。
另一种是直接npm run build。在命令行里面选择门店跟请求环境进行组合。适合门店名称比较长懒得每次都在命令输入进行打包的方式。
在pageage.json中输入打包入口
"scripts": {
"build": "node builds/build.js"
}
2.整合页面
2.1 实现打包
基于上面所说的,我们需要做的是获取出commonProjectJson/mall.json中所有的门店。其中有一个测试环境(智慧全景测试),其他的都是正式环境(智慧全景正式、新年华等)。要把所有的正式环境门店都获取出来用来自定义选择。使用inquirer库来进行命令行交互。
2.2 替换文件
替换模块目录、拼接除分包外的app.json
通过上面2.1的内容可以获取到门店名称跟环境变量,然后就可以替换app.js的门店信息,project.config.json中的appid。替换完成后就可以进行页面打包了。
接下来就要实现build方法了。包括根据每个门店的config.js中的needModule提取出所需的模块。先把公用的复制到dist目录,如果自身目录也有在进行覆盖。然后拼接app.json。
移动组件、替换静态资源(先把公用的移动然后在用自身的进行覆盖)、替换九宫格首页图、配置项等等
替换主题色、base64背景图、组装分包的app.json
如上图所示,push的时候会添加相同root的两条,因此要解决重复的数据后在进行拼接。
2.3 小优化
那每次修改运行商场的代码时候我们都要npm run bulid一次然后才能在开发者工具查看吗?这样似乎不合理。之前想的是如果公用的目录发生变化的话就循环一次所有的目录全部执行一次,但这样可能对那些不想修改的商场来说就会有问题,所以最后还是决定手动打包。使用chokidar库监听目录的变化,自动执行代码。
以上就是全部的核心代码了。但还有很多不足的地方,比如压缩图片的功能没实现,监听文件变化这一块也有待优化。或许还有除了shell.js之外更好的实现管理方式。
完整代码如下:
'use strict';
const ora = require('ora');
const shell = require('shelljs');
const inquirer = require('inquirer');
const chokidar = require('chokidar');
//!************************** 打包环境以及门店选择 **********************************
// 获取所有正式环境配置的门店
let mallJson = JSON.parse(shell.cat('commonProjectJson/mall.json').stdout);
//所有的门店
let mallArr = Object.keys(mallJson);
//所有正式环境的门店
let productionMallArr = mallArr.splice(1);
//直接运行npm run build 自己选门店跟环境的方式 暂时没想到比较好的判断npm run build后面有没有跟别的东西
let buildStr = process.argv[process.argv.length - 1];
var targetDirectory, env;
if (!!buildStr.includes('builds/build.js')) {
// 询问用户所属环境
const question = [
{
type: "confirm",
message: "环境是否是正式环境?",
name: 'env',
default: false
},
{
type: "list",
message: "请选择门店",
name: "mall",
choices: productionMallArr,
when: function (answers) {//当env为true的时候(正式环境的时候)才会提问当前问题
return answers.env
}
}
];
inquirer.prompt(question).then(answers => {
console.log('answers', answers);
// 如果选择的是测试
if (!answers.env) {
getValue(answers.env, '智慧全景测试');
} else {
getValue(answers.env, answers.mall);
}
}).catch(error => {
console.log('error', error);
return false
}
);
} else {
//从命令中获取打包的是哪个目录下的配置
//如 npm run build demo test获取到的是
targetDirectory = process.argv[process.argv.length - 2];//demo
env = process.argv[process.argv.length - 1];//test
let flag = `${env}==='test'` ? false : true;
getValue(flag, `${targetDirectory}`);
}
if (!targetDirectory || !env) {
console.error('请选择门店跟环境');
return false;
} else {
// 打印并开始动画
const spinner = ora('building ...');
spinner.start();
}
//之前想的只要公用目录的文件发生变化就循环所有目录名全部执行一次npm run build,但有的项目可能没要求这么上就会有问题,所以最后还是决定手动打包
//监听整个目录变化 自动执行npm run build 测试环境
chokidar.watch(['commonPages', `${targetDirectory}`], {
ignored: /(.DS_Store)/,
ignoreInitial: true
}).on('all', (event, path) => {
console.log('文件已修改', event, path);
shell.exec(`npm run build ${targetDirectory} ${env}`);
});
function build() {
//根据配置中的needModule来从公用页面中提取所需的模块
let demoStyle = require(`../apps/${targetDirectory}/config`);
let needModule = demoStyle.needModule;
//先删除原先的dist目录 创建一个dist目录 打包之后的文件都在dist目录下
shell.rm('-rf', 'dist');
//先把公用下的复制到dist目录 如若自身目录下也有 在进行覆盖
// params: 公用路径、自身路径、目标路径
let exitFlag = false; // 用来判断是否退出循环 终止操作
function copyPage(commonPage, selfPage, distPage) {
//首先判断公用目录下或者自身目录是否有此文件
if (!!shell.test('-d', commonPage) || !!shell.test('-d', selfPage)) {
//判断目标是否有此目录存在 没有就创建
if (!shell.test('-d', distPage)) {
shell.mkdir('-p', distPage)
}
//判断公用目录下是否有此目录 复制目录从common到dist
if (!!shell.test('-d', commonPage)) {
shell.cp('-R', commonPage, distPage);
}
//判断自身是否有此文件 有的话复制到dist下面进行覆盖
if (!!shell.test('-d', selfPage)) {
shell.cp('-R', selfPage, distPage);
}
} else {
//如若都没有 则报错 删除dist目录
console.error("暂无开发此功能");
shell.rm('-rf', 'dist');
exitFlag = true;
process.exit();
return false;
}
}
// *******************************复制文件***************************************
let pages = [];
for (var key in needModule) {
// 如果是分包的话
if (!!key.includes('package')) {
// 整合到dist目录下的文件
needModule[key].forEach(item => {
let commonPackageItem = `commonPages/${key}/pages/${item}`;
let customPackageItem = `apps/${targetDirectory}/${key}/pages/${item}`;
let distPackagePage = `dist/${key}/pages`;
copyPage(commonPackageItem, customPackageItem, distPackagePage);
});
} else {
// 整合到dist目录下的文件
needModule[key].forEach(item => {
//正常pages复制处理
let commonPageItem = `commonPages/${key}/${item}`;
let customPageItem = `apps/${targetDirectory}/${key}/${item}`;
let distPage = `dist/${key}`;
copyPage(commonPageItem, customPageItem, distPage);
//整合app.json里面的pages 这样会按照config.js里面needModule的顺序来push 如果按之前的写法 会按首字母a-z的顺序 这样在needModule里面指定的顺序就没用了
shell.ls('-R', `dist/pages/${item}`).forEach(function (file) {
if (!!file.includes('.wxss')) {
let pageUrl = `pages/${item}/${file.split('.wxss')[0]}`;
pages.push(pageUrl);
}
})
})
}
}
// 如果没此目录 阻止下面的操作
if (!!exitFlag) {
return false;
}
//!******************************* 静态图片资源 ***************************************
//解决静态图片icon冲突
shell.mkdir('-p', 'dist/assets');
//先把assets文件复制到dist/assets
shell.cp('-R', `assets`, 'dist');
shell.cp('-R', `components`, 'dist/components');
//整合差异 把demo/images也复制到dist/images下进行覆盖
shell.cp('-R', `apps/${targetDirectory}/images`, 'dist/assets/');
//todo 压缩图片方法 没找到好的压缩图片的方法
//!************************** project.config.json **********************************
shell.touch('dist/project.config.json');
//先把commonProjectJson/project.config.json文件复制到dist/project.config.json下
shell.cp('-R', 'commonProjectJson/project.config.json', 'dist/project.config.json');
//!************************* 替换app.js九宫格首页图 *********************************
shell.touch('dist/app.js');
shell.cp('-R', 'commonPages/app.js', 'dist/app.js');
shell.cp('-R', 'commonPages/app.wxss', 'dist/app.wxss');
//根据demo中config.js的配置信息替换@indexList
let indexList = JSON.stringify(demoStyle.indexList);
shell.sed('-i', '@indexList', indexList, 'dist/app.js');
shell.sed('-i', '@isFooterTab', demoStyle.isFooterTab, 'dist/app.js');
// *******************************组装app.json***************************************
//新建app.json文件
shell.touch('dist/app.json');
//组装app.json
let subpackages = [];
shell.ls('-R', 'dist').forEach(function (file) {
//提取dist目录下所有的wxss文件 wxml主要是因为icon的颜色只能在页面写
if (!!file.includes('.wxss') || !!file.includes('.wxml')) {
// 替换里面的@theme-color为主题色
shell.sed('-i', '@theme-color', demoStyle.themeColor, `dist/${file}`);
shell.sed('-i', '@shopBorderColor', demoStyle.shopBorderColor, `dist/${file}`);
shell.sed('-i', '@pointShopText', demoStyle.pointShopText, `dist/${file}`);
shell.sed('-i', '@pointRuleBg', demoStyle.pointRuleBg, `dist/${file}`);
shell.sed('-i', '@pointShopBg', demoStyle.pointShopBg, `dist/${file}`);
// 替换base64背景图片
let base64Json = require(`../apps/${targetDirectory}/base64`);
for (var key in base64Json) {
shell.sed('-i', `@${key}`, base64Json[key], `dist/${file}`);
}
}
// 组装app.json 如果是分包
if (!!file.includes('.wxss') && !!file.includes('package')) {
let subpackageItem = {};
// file: packageA/pages/index/index.wxss
let pageUrl = file.split('.wxss')[0]; //packageA/pages/index/index
subpackageItem.root = pageUrl.split('/')[0]; //packageA
subpackageItem.pages = [];
let pageItemUrl = pageUrl.substr(pageUrl.indexOf('/') + 1); //pages/index/index
subpackageItem.pages.push(pageItemUrl);
subpackages.push(subpackageItem);
/*[ { root: 'packageA', pages: [ 'pages/lottery/list' ] },
{ root: 'packageA', pages: [ 'pages/lottery/lottery' ] } ]*/
}
});
console.log('subpackages', subpackages);
// 解决重复package数据
let tempArr = [];
let newArr = [];
for (let i = 0; i < subpackages.length; i++) {
if (!tempArr.includes(subpackages[i].root)) {
newArr.push({
root: subpackages[i].root,
pages: subpackages[i].pages
});
tempArr.push(subpackages[i].root);
} else {
for (let j = 0; j < newArr.length; j++) {
if (newArr[j].root === subpackages[i].root) {
newArr[j].pages.push(subpackages[i].pages[0])
}
}
}
}
// 拼接内容
let pageJson = {
pages: pages,
serviceProviderTicket: demoStyle.serviceProviderTicket,
};
if (!!newArr.length) {
pageJson.subpackages = newArr;
}
//把pageJson写入dist/app.json
shell.echo(JSON.stringify(pageJson)).to('dist/app.json');
}
// 根据key获取value值并添加到app.js中
function getValue(env, key) {
let value;
if (!env) {
value = mallJson['智慧全景测试'];
} else {
value = mallJson[key];
}
if (!value) {
console.error('mall.json中没有此门店信息');
process.exit();
return false;
}
Object.assign(value, {
env: !env ? 'test' : 'production'
});
shell.sed('-i', '@env_mall', `${JSON.stringify(value)}`, 'dist/app.js');
//根据demo中config.js的配置信息替换@appId
shell.sed('-i', '@appId', `"${value.appId}"`, 'dist/project.config.json');
//打包的具体实现
targetDirectory = key || '智慧全景测试';
env = !env ? 'test' : 'production';
build();
}