多項(xiàng)目集成下的工程腳手架配置方案

一、背景
隨著項(xiàng)目的復(fù)雜和功能的增加,一個(gè)工程下可能存在多個(gè)項(xiàng)目,這個(gè)時(shí)候我們單獨(dú)開項(xiàng)目去開發(fā)的話項(xiàng)目代碼會(huì)冗余,項(xiàng)目后期的維護(hù)成本也很高,而代碼的冗余會(huì)造成靜態(tài)資源包加載時(shí)間變長(zhǎng)、執(zhí)行時(shí)間也會(huì)變長(zhǎng),進(jìn)而很直接的影響性能和體驗(yàn)。為了解決此問(wèn)題我們需要實(shí)現(xiàn)多項(xiàng)目的分模塊打包,且項(xiàng)目之間共享組件和依賴,運(yùn)行、打包時(shí)互不干擾。
二、應(yīng)用場(chǎng)景
以一個(gè)后臺(tái)管理系統(tǒng)為例,我們同時(shí)有運(yùn)營(yíng)管理系統(tǒng)、商家管理系統(tǒng)、設(shè)備管理系統(tǒng),還有一些內(nèi)部的管理系統(tǒng),這幾個(gè)系統(tǒng)的菜單管理、權(quán)限管理、用戶管理等相同業(yè)務(wù)模塊較多,業(yè)務(wù)組件以及UI組件都要遵循公司的規(guī)范,這種情況下就可以用一個(gè) ??repo?? 來(lái)管理這些系統(tǒng), 所有的設(shè)計(jì)文檔、源代碼、文件都放在一個(gè) ??repo?? 里面。
三、技術(shù)方案
本文基于vue-cli3,核心是 ??vue.config.js?? 文件。vue-cli2實(shí)現(xiàn)方法類似,核心是 ??webpack.config.js?? 文件,這里不做過(guò)多闡述。
1. 功能
- 項(xiàng)目區(qū)分命令化
 - 項(xiàng)目配置化
 - 路由模塊管理
 - 項(xiàng)目生成腳本化
 
2. 技術(shù)點(diǎn)
- process.argv [1] :獲取命令行參數(shù)
 - cross-env [2] :設(shè)置環(huán)境
 - fs-extra [3] :命令行生成項(xiàng)目
 - chalk [4] :命令行美化
 - inquirer [5] :命令行交互
 - node-progress [6] :加載進(jìn)度條
 
3. 思路
我們知道在 ??package.json?? 中有項(xiàng)目啟動(dòng)、打包的命令,我們可以從這里入手。我們的思路大概是這樣的:
- 通過(guò)命令行輸入的項(xiàng)目名稱打包指定項(xiàng)目 處理命令行參數(shù);
 - 配置公共文件和項(xiàng)目配置文件;
 - 設(shè)置當(dāng)前運(yùn)行/打包項(xiàng)目( 
project.js); - 打包項(xiàng)目所需的模塊和資源;
 
npm run dev projectA # 運(yùn)行開發(fā)環(huán)境下的projectA項(xiàng)目
npm run build:dev projectA # 打包開發(fā)環(huán)境下的projectA項(xiàng)目
npm run build projectA # 打包projectA項(xiàng)目
4. 目錄結(jié)構(gòu)
.
├── README.md
├── babel.config.js
├── config # 配置項(xiàng)
│ ├── build.js # 打包配置文件
│ ├── copy.js # 項(xiàng)目生成文件
│ ├── dev.js # 開發(fā)配置文件
│ ├── project.js # 獲取項(xiàng)目項(xiàng)目信息
│ └── projectConfig.js # 項(xiàng)目配置文件(和普通的腳手架配置項(xiàng)一樣)
├── package.json # 項(xiàng)目依賴
├── postcss.config.js # postcss配置文件
├── project # 工程信息配置
│ ├── index.js
│ ├── module # 公共路由模塊
│ └── projects # 公共項(xiàng)目信息
├── public
│ └── index.html
├── src
│ ├── assets # 公共資源文件
│ │ └── logo.png
│ ├── components # 公共組件
│ │ ├── 404.vue
│ │ └── main.vue
│ └── projects # 項(xiàng)目目錄(獨(dú)立的路由 狀態(tài)管理 接口請(qǐng)求)
│ ├── projectA
│ ├── projectB
│ └── projectC
├── temp # 項(xiàng)目模板文件(可根據(jù)項(xiàng)目需求定制)
│ ├── App.vue
│ ├── components
│ ├── main.js
│ ├── page
│ │ └── Home.vue
│ ├── router.js
│ └── store.js
├── vue.config.js # 核心配置文件
└── yarn.lock
13 directories, 26 files
好了,我們的視圖目錄結(jié)構(gòu)大概就是上面的樣子,我們期望的是打包 ??src?? 目錄下這個(gè) ??A項(xiàng)目?? 就像打包一個(gè)完整的項(xiàng)目一樣。那么如何實(shí)現(xiàn)這部分呢?
5. 流程圖

6 項(xiàng)目配置
1) 修改package.json配置
這里就不得不提到 ??cross-env?? 這個(gè)模塊,我們之前在生產(chǎn)、沙箱、測(cè)試、開發(fā)環(huán)境時(shí)也會(huì)用到這個(gè)命令。
npm i --save-dev cross-env
代碼:
"scripts": {
  "serve": "vue-cli-service serve",
  "build": "vue-cli-service build",
  "lint": "vue-cli-service lint",
  "dev": "cross-env NODE_ENV=development node config/dev.js",
  "test": "cross-env NODE_ENV=test node config/dev.js",
  "pre": "cross-env NODE_ENV=preview node config/dev.js",
  "prd": "cross-env NODE_ENV=production node config/dev.js",
  "build:dev": "cross-env NODE_ENV=development node config/build.js",
  "build:test": "cross-env NODE_ENV=test node config/build.js",
  "build:pre": "cross-env NODE_ENV=preview node config/build.js",
  "build:prd": "cross-env NODE_ENV=production node config/build.js",
  "copy": "node config/copy.js"
}2) 編寫項(xiàng)目代碼
此版本為 ??簡(jiǎn)易demo?? ,配置完運(yùn)行命令和打包命令我們就可以編寫項(xiàng)目中的業(yè)務(wù)代碼了。
路徑: ??src/projects/projectA/App.vue??
<template>
<div id="app">
<img
alt="項(xiàng)目A"
src="https://dummyimage.com/300x300/FF0097/fff&text=PROJECT-A"
/>
<router-view />
</div>
</template>
<style lang="scss">
......
</style>
路徑: ??src/projects/projectB/App.vue??
<template>
<div id="app">
<img
alt="項(xiàng)目B"
src="https://dummyimage.com/300x300/ff55ee/fff&text=PROJECT-B"
/>
<router-view />
</div>
</template>
<style lang="scss">
......
</style>
3) 配置項(xiàng)目信息
在項(xiàng)目根目錄建立 ??config?? 文件夾,在其中新建 ??projectsConfig.js?? 寫入:
const projectName = require("./project");
const config = {
  // $ 項(xiàng)目A
  projectA: {
    pages: {
      index: {
        entry: "src/projects/projectA/main.js",
        template: "public/index.html",
        filename: "index.html",
        title: "projectA"
      },
    },
    devServer: {
      port: 7777, // 端口地址
     }
  },
  // $ 項(xiàng)目B
  projectB: {
    pages: {
      index: {
        entry: "src/projects/projectB/main.js",
        template: "public/index.html",
        filename: "index.html",
        title: "projectB"
      },
    },
    devServer: {
      port: 8888, // 端口地址
     }
  },
  // $ 項(xiàng)目C
  projectC: {
    pages: {
      index: {
        entry: "src/projects/projectC/main.js",
        template: "public/index.html",
        filename: "index.html",
        title: "projectC"
      },
    },
    devServer: {
      port: 9999, // 端口地址
     }
  },
};
const configObj = config[projectName.name];
// $ 這里導(dǎo)出的是當(dāng)前運(yùn)行項(xiàng)目的配置
module.exports = configObj;
4) 運(yùn)行時(shí)配置
開始前先講下 ??process.argv?? 它返回的是一個(gè)數(shù)組,其中包含啟動(dòng) Node.js 進(jìn)程時(shí)傳入的命令行參數(shù)。第一個(gè)元素將是 ??process.execPath?? , 第二個(gè)元素將是正在執(zhí)行的 JavaScript文件的路徑,其余元素將是任何其他命令行參數(shù)。

const fse = require("fs-extra");
const chalk = require('chalk');
let projectName = process.argv[2]; // $ 獲取命令行項(xiàng)目名稱
if(!projectName) throw(chalk`{red.bold.bgWhite ------項(xiàng)目不存在,請(qǐng)檢查配置------}`);
console.log(chalk.red.bold(`正在運(yùn)行---${projectName}項(xiàng)目`), `${process.env.NODE_ENV} 環(huán)境`, )
fse.writeFileSync('./config/project.js', `exports.name = '${projectName}'`)
let exec = require('child_process').execSync;
exec('npm run serve', {stdio: 'inherit'});Tips:命令行參數(shù)是固定格式 ??npm run dev projectA?? ,少了項(xiàng)目名稱會(huì)提示項(xiàng)目不存在。

5) 打包時(shí)配置
這里就比較簡(jiǎn)單了,根據(jù)當(dāng)前項(xiàng)目名稱進(jìn)行打包即可
const projectName = process.argv[2]
const fse = require("fs-extra");
fse.writeFileSync('./config/project.js', `exports.name = '${projectName}'`)
const str = 'npm run build'
const exec = require('child_process').execSync;
exec(str, {stdio: 'inherit'});
6) 配置Vue CLI
- 通過(guò) 
process.argv獲取當(dāng)前命令行的項(xiàng)目名稱,判斷命令行的項(xiàng)目名稱是否在項(xiàng)目列表里,如果沒有給出異常提示; - 設(shè)置當(dāng)前運(yùn)行項(xiàng)目的腳手架信息;
 - 終端命令提示;
 
const path = require('path')
const conf = require('./config/projectConfig'); // $ 當(dāng)前項(xiàng)目
const chalk = require('chalk'); // $ 終端顏色設(shè)置插件
const ProgressBarPlugin = require('progress-bar-webpack-plugin'); // $ 進(jìn)度條插件
const PROJECTNAME = require('./config/project.js').name;
if(!conf) throw(chalk`{black.bold.bgWhite ------項(xiàng)目不存在,請(qǐng)檢查配置 777------}`);
const assetsDir = ''
function getAssetPath (assetsDir, filePath) {
  return assetsDir
    ? path.posix.join(assetsDir, filePath)
    : filePath
}
module.exports = {
  pages: conf.pages, // $ 當(dāng)前項(xiàng)目頁(yè)面
  outputDir: "dist/" + projectName + "/", // $ 設(shè)置輸出目錄名
  assetsDir: 'static',  // $ 增加static文件夾
  lintOnSave: process.env.NODE_ENV !== 'production', // $ 是否在開發(fā)環(huán)境下通過(guò) eslint-loader 在每次保存時(shí) lint 代碼
  productionSourceMap: false, // $ 是否需要生產(chǎn)環(huán)境的 source map
  devServer: conf.devServer, // $ 看項(xiàng)目需求 可配可不配
  configureWebpack: {
    plugins: [
      new ProgressBarPlugin({  
        width: 50,                     // 默認(rèn)20,進(jìn)度格子數(shù)量即每個(gè)代表進(jìn)度數(shù),如果是20,那么一格就是5。
        // format: 'build [:bar] :percent (:elapsed seconds)',
        format: chalk.blue.bold("build") + chalk.yellow('[:bar] ') + chalk.green.bold(':percent') + ' (:elapsed秒)',
        // stream: process.stderr,        // 默認(rèn)stderr,輸出流
        // complete: "~",                 // 默認(rèn)“=”,完成字符
        clear: false,                  // 默認(rèn)true,完成時(shí)清除欄的選項(xiàng)
        // renderThrottle: "",            // 默認(rèn)16,更新之間的最短時(shí)間(以毫秒為單位)
        callback() {                   // 進(jìn)度條完成時(shí)調(diào)用的可選函數(shù)
          console.log(chalk.red.bold("---->>>>編譯完成<<<<----"))
        }
    }),
    ]
  },
  // $ 對(duì)內(nèi)部的 webpack 配置進(jìn)行更細(xì)粒度的修改
  chainWebpack: config => {
    // $ 修復(fù)HMR
    config.resolve.symlinks(true);
    // $ 制定環(huán)境打包js路徑
    const filename = getAssetPath(
      assetsDir,
      `static/js/[name].js`
    )
    config.mode('production').devtool(false).output.filename(filename).chunkFilename(filename)
    config.performance.set('hints', false)
  },
  css: {
    extract: false // $ 是否將組件中的 CSS 提取至一個(gè)獨(dú)立的 CSS 文件中 (而不是動(dòng)態(tài)注入到 JavaScript 中的 inline 代碼)
    loaderOptions: {
      sass: {
        implementation: require('sass'),
        fiber: require('fibers')
      }
    }
  }
}配置終端插件的效果圖:

7) 運(yùn)行效果
寫到這里我們就建立一個(gè)完整的小vue項(xiàng)目了,我們運(yùn)行看看效果:
npm run dev projectA
如圖:

8) 打包效果
npm run build:projectA
cd dist/projectA
live-server --port=9999
??live-server?? 是一個(gè)具有實(shí)時(shí)加載功能的小型服務(wù)器,在項(xiàng)目中用live-server作為一個(gè)實(shí)時(shí)服務(wù)器查看開發(fā)的網(wǎng)頁(yè)或項(xiàng)目效果



7. 自動(dòng)化生成模板項(xiàng)目
1) 流程圖

2) 思路整理
- 本文涉及到腳手架里邊與命令行交互的知識(shí)點(diǎn),感興趣的可以拷貝文末 
demo去練習(xí)下; - 這里主要是針對(duì)新建的模板做拷貝處理,流程節(jié)點(diǎn)中執(zhí)行拷貝命令后輸入的項(xiàng)目名稱提示在本地已存在是否需要?jiǎng)h除或者覆蓋,根據(jù)實(shí)際業(yè)務(wù)場(chǎng)景做處理,這里不做過(guò)多探討;
 - 示例代碼涉及到的模板代碼存放在工程根目錄,也可以放在 
src目錄下,不做強(qiáng)制要求; 
3) 執(zhí)行命令
npm run copy
4) 示例代碼
fs-extra:添加了未包含在原生fs模塊中的文件系統(tǒng)方法,并向fs方法添加了promise支持;fse.pathExists:判斷當(dāng)前要拷貝的項(xiàng)目是否存在;fse.copy:拷貝模板文件到指定目錄;
const fse = require("fs-extra");
const chalk = require("chalk");
const path = require("path");
const inquirer = require("inquirer");
inquirer
  .prompt([
    {
      type: "input",
      name: "projectName",
      message: "請(qǐng)輸入要生成的項(xiàng)目名稱",
    },
  ])
  .then((answers) => {
    createProject(answers.projectName);
  });
// $ 拷貝項(xiàng)目模板
const createProject = (projectName) => {
  const currentTemp = path.join(`./src/projects/${projectName}`);
  // $ 判斷當(dāng)前要拷貝的項(xiàng)目是否存在
  fse.pathExists(currentTemp, (err, exists) => {
    console.log(err, exists); // $ => null, false
    // $ 根據(jù)用戶選擇是否替換本項(xiàng)目或者刪除本項(xiàng)目
    if (exists) {
      // $ 這里也可以覆蓋原項(xiàng)目或者dong
      inquirer
        .prompt([
          {
            type: "input",
            name: "projectName",
            message: "項(xiàng)目已存在,請(qǐng)重新輸入項(xiàng)目名稱",
          },
        ])
        .then((answers) => {
          createProject(answers.projectName);
        });
      //   throw chalk`{red.bold.bgWhite >>> ${projectName} <<< 項(xiàng)目已經(jīng)存在}`;
    } else {
      // $ 拷貝模板文件到指定目錄
      fse.copy("./temp", path.join(`./src/projects/${projectName}`), (err) => {
        // if (err) return console.error(err)
        if (err)
          throw chalk`{red.bold.bgWhite ------${projectName}項(xiàng)目拷貝失敗 ${err}------}`;
        console.log(chalk.red.bold(`--->>>${projectName}項(xiàng)目拷貝成功`));
      });
    }
  });
};8 優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
- 方便統(tǒng)一管理項(xiàng)目;
 - 項(xiàng)目之間共享組件和依賴;
 - 運(yùn)行、打包時(shí)互不干擾;
 - 支持同時(shí)運(yùn)行多個(gè)項(xiàng)目;
 - 對(duì)于公共模塊一次提交可以解決所有子項(xiàng)目的問(wèn)題;
 
缺點(diǎn):
- 執(zhí)行拷貝模板命令后生成的項(xiàng)目需要在 
config/projectConfig.js文件中手動(dòng)配置項(xiàng)目信息; - 隨著項(xiàng)目的增加路由文件的提交在每次代碼的時(shí)候都需要進(jìn)行 
Code Review,不然的話不熟悉項(xiàng)目的同學(xué)很可能會(huì)在解決沖突的過(guò)程中把沖突的模塊刪除; - 隨著程序規(guī)模的不斷增加,代碼量的增加,文檔的增加,整個(gè) 
repo會(huì)變得越來(lái)越大; 
四、思考
有興趣的童鞋可以考慮以下兩個(gè)問(wèn)題:
- 項(xiàng)目中有公共路由我們應(yīng)該如何處理?
 - 狀態(tài)管理和接口管理在這個(gè)工程下如何處理?
 
五、總結(jié)
通過(guò)以上的分析,我們應(yīng)該對(duì)同一工程下多項(xiàng)目配置化打包的大概流程有基本的了解,而上邊的方案也只是其中的一種實(shí)現(xiàn)方式。寫本文的目的主要是給大家提供一種思路,以后在遇到工程需要定制化的時(shí)候就可以通過(guò)更改腳手架的配置來(lái)實(shí)現(xiàn)。
??Demo?? :[https://github.com/licairen/multi_project_demo](















 
 
 










 
 
 
 