已经连续加班快三个月了,最近抽个时间把一些心得记录下来,算是做个总结吧!
故事背景
公司的业务以做项目为主,主打的是电商行业,因此也决定了很多项目其实存在一定的共性.目前公司业绩不错(年底应该会有大把 money 吧),经常多个项目并行,这也暴露了整个团队存在的问题:
多项目并行,导致人力资源不够(本猿在三线城市,有两把刷子的兄弟不好招)
代码复用率低,重复劳动过多(即使有 50% 相似度的项目,可能也要重新开发,太 low 了)
开发质量难以控制,测试成本高
项目难以按时完成,经常会 delay
因此如何能找到一个经济实惠符合我们团队的情况的解决方案迫在眉睫!
本猿团队的技术栈是 vue+webpack,因此我的解决方案可能不适合其他技术栈
解决方案一
这个其实是个失败的方案,在这里先介绍一下,鄙视一下自己!
总体思路在通过项目编号,在页面中控制组件的渲染,比如:
<component-a v-if="projectId === 10000"></component-a>
<component-a v-if="projectId === 10001"></component-a>
这种方式造成的悲剧显而易见:
随着项目越来越多,业务逻辑越来越复杂,代码体积会越来越大,前端渲染的速度很受影响
代码逻辑耦合性强,多人合作的时候,容易发生冲突,造成不可知的 bug
解决方案二
目前这个方案正在实践中,暂时还能满足目前的业务需求!
一,组件和页面目录结构
组件目录 components 和页面目录 pages 类似,所以这里以 components 中的 TabBar(底部导航栏)为例.
我们可以看到 TabBar 下有三个文件 0.vue,29006.vue 和 index.js:
0.vue 是标准的底部导航栏组件
29006.vue 是项目号为 29006 项目定制的底部导航栏
index.js 是整个 TabBar 组件的入口,这个文件怎么生成,请看下面介绍
以此类推,在 pages/Home(首页)中,我们也可以看到标准的首页,为项目号为 29006 项目定制的首页和首页入口文件 index.js
二,资源文件结构
样式和图片文件跟之前介绍的 components 一样,用过项目号作为文件名加以区分
三,页面引用组件和路由加载页面
每个组件都有 index.js 作为入口,因此在页面中引入组件的话,只需:
import TabBar from '../../components/TabBar';
同样每个页面也有 index.js 作为入口,因此在路由中引入组件的话,只需:
{
path: '/home',
name: 'home',
title: '首页',
component(resolve) {
require.ensure(['../pages/Home'], () => {
resolve(require('../pages/Home'));
});
},
meta: {requiresAuth: false}
}
四,项目配置化
工程的整体结构大家已经了解,应该明白接下去的关键就是如何根据项目号生成不同的 index.js 入口文件和资源文件
项目生成脚 create-platform.js 本如下:
const glob = require('glob');
const fsExtra = require('fs-extra')
const platform = process.argv[2]; // 项目号
const vueFile = `${platform}.vue`; // 与项目匹配的vue文件
const defaultVueFile = `0.vue`; // 标准的vue文件
const fs = require('fs');
// 获取指定路径下的入口文件
function getEntries(globPath) {
let files = glob.sync(globPath);
let paths = [];
files.forEach((filepath) => {
let split = filepath.split('/');
let path = split.slice(0, split.length - 1);
path = path.join('/');
paths.push(`${path}`);
});
paths = dedupe(paths);
return paths;
}
// 数组去重
function dedupe(array){
return Array.from(new Set(array));
}
// 写入index.js入口文件
async function writeIndexJS (path, fileName) {
let split = path.split('/');
let componentName = split[split.length - 1];
let f = `${path}/index.js`
try {
await fsExtra.outputFile(f, `import ${componentName} from './${fileName}';\r\nexport default ${componentName};`);
} catch (err) {
console.error(err)
}
}
async function copyFile (src, dest) {
try {
await fsExtra.copy(src, dest, { overwrite: true })
} catch (err) {
console.error(err)
}
}
// 创建index.js
function createIndexJS(paths) {
paths.forEach(async (path) => {
let exists = await fsExtra.pathExists(`${path}/${vueFile}`);
if (exists) {
writeIndexJS(path, vueFile);
} else {
writeIndexJS(path, defaultVueFile);
}
});
}
// copy懒加载所需图片
async function copyLazyLoad() {
let exists = await fsExtra.pathExists(`./src/assets/images/lazy-load/list/${platform}.png`);
if (exists) {
copyFile(`./src/assets/images/lazy-load/list/${platform}.png`, `./static/lazy-load/list.png`);
} else {
copyFile(`./src/assets/images/lazy-load/list/0.png`, `./static/lazy-load/list.png`);
}
exists = await fsExtra.pathExists(`./src/assets/images/lazy-load/thumbnail/${platform}.png`);
if (exists) {
copyFile(`./src/assets/images/lazy-load/thumbnail/${platform}.png`, `./static/lazy-load/thumbnail.png`);
} else {
copyFile(`./src/assets/images/lazy-load/thumbnail/0.png`, `./static/lazy-load/thumbnail.png`);
}
}
// copy样式文件
async function copyLess() {
let exists = await fsExtra.pathExists(`./src/assets/CSS/main/${platform}.less`);
if (exists) {
copyFile(`./src/assets/css/main/${platform}.less`, `./src/assets/css/main.less`);
} else {
copyFile(`./src/assets/css/main/0.less`, `./src/assets/css/main.less`);
}
exists = await fsExtra.pathExists(`./src/assets/css/theme/${platform}.less`);
if (exists) {
copyFile(`./src/assets/css/theme/${platform}.less`, `./src/assets/css/theme.less`);
} else {
copyFile(`./src/assets/css/theme/0.less`, `./src/assets/css/theme.less`);
}
}
let paths = getEntries('./src/components/**/*.vue'); // 获得入口components目录下的文件
createIndexJS(paths);
paths = getEntries('./src/pages/**/*.vue'); // 获得入口pages目录下的文件
createIndexJS(paths);
copyLazyLoad();
copyLess();
脚本运行命令:
node create-platform.js 29006
脚本解释
const platform = process.argv[2]; // 以命令行的第三个参数项目号
const vueFile = `${platform}.vue`; // 根据项目号生成匹配的vue文件
const defaultVueFile = `0.vue`; // 标准的vue文件
// 获取指定路径下的入口文件
function getEntries(globPath) {
.........
}
// 创建index.js
function createIndexJS(paths) {
// 遍历目录
paths.forEach(async (path) => {
// 判断目录中是否存在和项目号匹配的vue文件,如果有就使用该文件,如果没有则使用标准的0.vue
let exists = await fsExtra.pathExists(`${path}/${vueFile}`);
if (exists) {
writeIndexJS(path, vueFile);
} else {
writeIndexJS(path, defaultVueFile);
}
});
}
// 写入index.js入口文件
async function writeIndexJS (path, fileName) {
let split = path.split('/');
let componentName = split[split.length - 1]; // 以目录名作为组件和页面名称
let f = `${path}/index.js`
try {
await fsExtra.outputFile(f, `import ${componentName} from './${fileName}';\r\nexport default ${componentName};`);
} catch (err) {
console.error(err)
}
}
这里只介绍了几个主要的方法,其他的 copyLazyLoad 和 copyLess 方法,原理其实一样,不一一介绍!
五,结果
运行命令
node create-platform.js 29006
TabBar 下面的 index.js 文件内容如下:
import TabBar from './29006.vue';
export default TabBar;
这是因为 TabBar 下有 29006.vue 这个跟项目号匹配的组件文件
HomeCategoryColumn 下面的 index.js 文件内容如下:
import HomeCategoryColumn from './0.vue';
export default HomeCategoryColumn;
这是因为 HomeCategoryColumn 下并没有跟项目 29006 匹配的文件,因此使用了标准的 0.vue
六,总结
通过这种方式,我们一方面能做到在项目中复用已开发的组件,同时也能实现组件的定制化,而且工程的引入量也会大大减少.
或许这个不是最好的方法,但是目前来说比较符合我们团队的实际情况,如果哪里大神有好的解决方案,还望告知,大家一起交流交流!
最后说一句:一入前端深似海,一路好走!!!
来源: https://juejin.im/post/5a67dc5951882573351a770e