前端多專案模組化實踐

懶貓貓發表於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

六、總結

通過這種方式,我們一方面能做到在專案中複用已開發的元件,同時也能實現元件的定製化,而且工程的引入量也會大大減少。

或許這個不是最好的方法,但是目前來說比較符合我們團隊的實際情況,如果哪裡大神有好的解決方案,還望告知,大家一起交流交流!

最後說一句:一入前端深似海,一路好走!!!

相關文章