多項目集成下的工程腳手架配置方案
一、背景
隨著項目的復雜和功能的增加,一個工程下可能存在多個項目,這個時候我們單獨開項目去開發的話項目代碼會冗余,項目后期的維護成本也很高,而代碼的冗余會造成靜態資源包加載時間變長、執行時間也會變長,進而很直接的影響性能和體驗。為了解決此問題我們需要實現多項目的分模塊打包,且項目之間共享組件和依賴,運行、打包時互不干擾。
二、應用場景
以一個后臺管理系統為例,我們同時有運營管理系統、商家管理系統、設備管理系統,還有一些內部的管理系統,這幾個系統的菜單管理、權限管理、用戶管理等相同業務模塊較多,業務組件以及UI組件都要遵循公司的規范,這種情況下就可以用一個 ??repo?
? 來管理這些系統, 所有的設計文檔、源代碼、文件都放在一個 ??repo?
? 里面。
三、技術方案
本文基于vue-cli3,核心是 ??vue.config.js?
? 文件。vue-cli2實現方法類似,核心是 ??webpack.config.js?
? 文件,這里不做過多闡述。
1. 功能
- 項目區分命令化
- 項目配置化
- 路由模塊管理
- 項目生成腳本化
2. 技術點
- process.argv [1] :獲取命令行參數
- cross-env [2] :設置環境
- fs-extra [3] :命令行生成項目
- chalk [4] :命令行美化
- inquirer [5] :命令行交互
- node-progress [6] :加載進度條
3. 思路
我們知道在 ??package.json?
? 中有項目啟動、打包的命令,我們可以從這里入手。我們的思路大概是這樣的:
- 通過命令行輸入的項目名稱打包指定項目 處理命令行參數;
- 配置公共文件和項目配置文件;
- 設置當前運行/打包項目(
project.js
); - 打包項目所需的模塊和資源;
npm run dev projectA # 運行開發環境下的projectA項目
npm run build:dev projectA # 打包開發環境下的projectA項目
npm run build projectA # 打包projectA項目
4. 目錄結構
.
├── README.md
├── babel.config.js
├── config # 配置項
│ ├── build.js # 打包配置文件
│ ├── copy.js # 項目生成文件
│ ├── dev.js # 開發配置文件
│ ├── project.js # 獲取項目項目信息
│ └── projectConfig.js # 項目配置文件(和普通的腳手架配置項一樣)
├── package.json # 項目依賴
├── postcss.config.js # postcss配置文件
├── project # 工程信息配置
│ ├── index.js
│ ├── module # 公共路由模塊
│ └── projects # 公共項目信息
├── public
│ └── index.html
├── src
│ ├── assets # 公共資源文件
│ │ └── logo.png
│ ├── components # 公共組件
│ │ ├── 404.vue
│ │ └── main.vue
│ └── projects # 項目目錄(獨立的路由 狀態管理 接口請求)
│ ├── projectA
│ ├── projectB
│ └── projectC
├── temp # 項目模板文件(可根據項目需求定制)
│ ├── App.vue
│ ├── components
│ ├── main.js
│ ├── page
│ │ └── Home.vue
│ ├── router.js
│ └── store.js
├── vue.config.js # 核心配置文件
└── yarn.lock
13 directories, 26 files
好了,我們的視圖目錄結構大概就是上面的樣子,我們期望的是打包 ??src?
? 目錄下這個 ??A項目?
? 就像打包一個完整的項目一樣。那么如何實現這部分呢?
5. 流程圖
6 項目配置
1) 修改package.json配置
這里就不得不提到 ??cross-env?
? 這個模塊,我們之前在生產、沙箱、測試、開發環境時也會用到這個命令。
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) 編寫項目代碼
此版本為 ??簡易demo?
? ,配置完運行命令和打包命令我們就可以編寫項目中的業務代碼了。
路徑: ??src/projects/projectA/App.vue?
?
<template>
<div id="app">
<img
alt="項目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="項目B"
src="https://dummyimage.com/300x300/ff55ee/fff&text=PROJECT-B"
/>
<router-view />
</div>
</template>
<style lang="scss">
......
</style>
3) 配置項目信息
在項目根目錄建立 ??config?
? 文件夾,在其中新建 ??projectsConfig.js?
? 寫入:
const projectName = require("./project");
const config = {
// $ 項目A
projectA: {
pages: {
index: {
entry: "src/projects/projectA/main.js",
template: "public/index.html",
filename: "index.html",
title: "projectA"
},
},
devServer: {
port: 7777, // 端口地址
}
},
// $ 項目B
projectB: {
pages: {
index: {
entry: "src/projects/projectB/main.js",
template: "public/index.html",
filename: "index.html",
title: "projectB"
},
},
devServer: {
port: 8888, // 端口地址
}
},
// $ 項目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];
// $ 這里導出的是當前運行項目的配置
module.exports = configObj;
4) 運行時配置
開始前先講下 ??process.argv?
? 它返回的是一個數組,其中包含啟動 Node.js 進程時傳入的命令行參數。第一個元素將是 ??process.execPath?
? , 第二個元素將是正在執行的 JavaScript文件的路徑,其余元素將是任何其他命令行參數。
const fse = require("fs-extra");
const chalk = require('chalk');
let projectName = process.argv[2]; // $ 獲取命令行項目名稱
if(!projectName) throw(chalk`{red.bold.bgWhite ------項目不存在,請檢查配置------}`);
console.log(chalk.red.bold(`正在運行---${projectName}項目`), `${process.env.NODE_ENV} 環境`, )
fse.writeFileSync('./config/project.js', `exports.name = '${projectName}'`)
let exec = require('child_process').execSync;
exec('npm run serve', {stdio: 'inherit'});
Tips:命令行參數是固定格式 ??npm run dev projectA?
? ,少了項目名稱會提示項目不存在。
5) 打包時配置
這里就比較簡單了,根據當前項目名稱進行打包即可
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
- 通過
process.argv
獲取當前命令行的項目名稱,判斷命令行的項目名稱是否在項目列表里,如果沒有給出異常提示; - 設置當前運行項目的腳手架信息;
- 終端命令提示;
const path = require('path')
const conf = require('./config/projectConfig'); // $ 當前項目
const chalk = require('chalk'); // $ 終端顏色設置插件
const ProgressBarPlugin = require('progress-bar-webpack-plugin'); // $ 進度條插件
const PROJECTNAME = require('./config/project.js').name;
if(!conf) throw(chalk`{black.bold.bgWhite ------項目不存在,請檢查配置 777------}`);
const assetsDir = ''
function getAssetPath (assetsDir, filePath) {
return assetsDir
? path.posix.join(assetsDir, filePath)
: filePath
}
module.exports = {
pages: conf.pages, // $ 當前項目頁面
outputDir: "dist/" + projectName + "/", // $ 設置輸出目錄名
assetsDir: 'static', // $ 增加static文件夾
lintOnSave: process.env.NODE_ENV !== 'production', // $ 是否在開發環境下通過 eslint-loader 在每次保存時 lint 代碼
productionSourceMap: false, // $ 是否需要生產環境的 source map
devServer: conf.devServer, // $ 看項目需求 可配可不配
configureWebpack: {
plugins: [
new ProgressBarPlugin({
width: 50, // 默認20,進度格子數量即每個代表進度數,如果是20,那么一格就是5。
// format: 'build [:bar] :percent (:elapsed seconds)',
format: chalk.blue.bold("build") + chalk.yellow('[:bar] ') + chalk.green.bold(':percent') + ' (:elapsed秒)',
// stream: process.stderr, // 默認stderr,輸出流
// complete: "~", // 默認“=”,完成字符
clear: false, // 默認true,完成時清除欄的選項
// renderThrottle: "", // 默認16,更新之間的最短時間(以毫秒為單位)
callback() { // 進度條完成時調用的可選函數
console.log(chalk.red.bold("---->>>>編譯完成<<<<----"))
}
}),
]
},
// $ 對內部的 webpack 配置進行更細粒度的修改
chainWebpack: config => {
// $ 修復HMR
config.resolve.symlinks(true);
// $ 制定環境打包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 提取至一個獨立的 CSS 文件中 (而不是動態注入到 JavaScript 中的 inline 代碼)
loaderOptions: {
sass: {
implementation: require('sass'),
fiber: require('fibers')
}
}
}
}
配置終端插件的效果圖:
7) 運行效果
寫到這里我們就建立一個完整的小vue項目了,我們運行看看效果:
npm run dev projectA
如圖:
8) 打包效果
npm run build:projectA
cd dist/projectA
live-server --port=9999
??live-server?
? 是一個具有實時加載功能的小型服務器,在項目中用live-server作為一個實時服務器查看開發的網頁或項目效果
7. 自動化生成模板項目
1) 流程圖
2) 思路整理
- 本文涉及到腳手架里邊與命令行交互的知識點,感興趣的可以拷貝文末
demo
去練習下; - 這里主要是針對新建的模板做拷貝處理,流程節點中執行拷貝命令后輸入的項目名稱提示在本地已存在是否需要刪除或者覆蓋,根據實際業務場景做處理,這里不做過多探討;
- 示例代碼涉及到的模板代碼存放在工程根目錄,也可以放在
src
目錄下,不做強制要求;
3) 執行命令
npm run copy
4) 示例代碼
fs-extra
:添加了未包含在原生fs模塊
中的文件系統方法,并向fs方法添加了promise支持;fse.pathExists
:判斷當前要拷貝的項目是否存在;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: "請輸入要生成的項目名稱",
},
])
.then((answers) => {
createProject(answers.projectName);
});
// $ 拷貝項目模板
const createProject = (projectName) => {
const currentTemp = path.join(`./src/projects/${projectName}`);
// $ 判斷當前要拷貝的項目是否存在
fse.pathExists(currentTemp, (err, exists) => {
console.log(err, exists); // $ => null, false
// $ 根據用戶選擇是否替換本項目或者刪除本項目
if (exists) {
// $ 這里也可以覆蓋原項目或者dong
inquirer
.prompt([
{
type: "input",
name: "projectName",
message: "項目已存在,請重新輸入項目名稱",
},
])
.then((answers) => {
createProject(answers.projectName);
});
// throw chalk`{red.bold.bgWhite >>> ${projectName} <<< 項目已經存在}`;
} else {
// $ 拷貝模板文件到指定目錄
fse.copy("./temp", path.join(`./src/projects/${projectName}`), (err) => {
// if (err) return console.error(err)
if (err)
throw chalk`{red.bold.bgWhite ------${projectName}項目拷貝失敗 ${err}------}`;
console.log(chalk.red.bold(`--->>>${projectName}項目拷貝成功`));
});
}
});
};
8 優缺點
優點:
- 方便統一管理項目;
- 項目之間共享組件和依賴;
- 運行、打包時互不干擾;
- 支持同時運行多個項目;
- 對于公共模塊一次提交可以解決所有子項目的問題;
缺點:
- 執行拷貝模板命令后生成的項目需要在
config/projectConfig.js
文件中手動配置項目信息; - 隨著項目的增加路由文件的提交在每次代碼的時候都需要進行
Code Review
,不然的話不熟悉項目的同學很可能會在解決沖突的過程中把沖突的模塊刪除; - 隨著程序規模的不斷增加,代碼量的增加,文檔的增加,整個
repo
會變得越來越大;
四、思考
有興趣的童鞋可以考慮以下兩個問題:
- 項目中有公共路由我們應該如何處理?
- 狀態管理和接口管理在這個工程下如何處理?
五、總結
通過以上的分析,我們應該對同一工程下多項目配置化打包的大概流程有基本的了解,而上邊的方案也只是其中的一種實現方式。寫本文的目的主要是給大家提供一種思路,以后在遇到工程需要定制化的時候就可以通過更改腳手架的配置來實現。
??Demo?
? :[https://github.com/licairen/multi_project_demo](