分享

前端多项目模块化实践

 quasiceo 2018-01-24

前端多项目模块化实践

2018 年 01 月 24 日
懒猫猫

已经连续加班快三个月了,最近抽个时间把一些心得记录下来,算是做个总结吧!

故事背景

公司的业务以做项目为主,主打的是电商行业,因此也决定了很多项目其实存在一定的共性。目前公司业绩不错(年底应该会有大把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

六、总结

通过这种方式,我们一方面能做到在项目中复用已开发的组件,同时也能实现组件的定制化,而且工程的引入量也会大大减少。

或许这个不是最好的方法,但是目前来说比较符合我们团队的实际情况,如果哪里大神有好的解决方案,还望告知,大家一起交流交流!

最后说一句:一入前端深似海,一路好走!!!

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多