背景
??公司前端項(xiàng)目,是由Vite+Vue3+ts搭建的單頁面項(xiàng)目粘咖,但是隨著需求增多瓮下,發(fā)現(xiàn)越來越多的頁面互相之前沒有關(guān)聯(lián)關(guān)系钝域,耦合度極低例证,項(xiàng)目逐漸變大后會導(dǎo)致每個獨(dú)立頁面的啟動速度慢,打包時間長胀葱,而且每次發(fā)布打包都會影響線上所有頁面(雖然可能代碼沒改動,但是引用的三方npm包庆锦、公共組件等可能會變化)轧葛,這些都有可能導(dǎo)致線上其他頁面被修改尿扯,測試力度不夠的話都可能會導(dǎo)致各類的隱藏bug衷笋。
??由此產(chǎn)生了搭建多頁面項(xiàng)目的想法。
技術(shù)棧
需求
- 腳本自動創(chuàng)建新頁面蚜锨,包括app.vue、index.html晨抡、main.ts则剃、views文件夾棍现;
- 單獨(dú)調(diào)試(dev)和打包(build)某個頁面己肮;
- 同時調(diào)試(dev)和打包(build)所有頁面;
開始娄柳!
一赤拒、目錄結(jié)構(gòu)
├── README.md
├── dist //打包輸出目錄
├── node_modules //三方
├── public //公共靜態(tài)資源
├── scripts //腳本(打包这敬、創(chuàng)建新頁面)
│ ├── template //創(chuàng)建子頁面的模版
│ ├── newPage.mjs //創(chuàng)建子頁面的腳本
│ └── build.cjs //打包所有頁面的腳本
├── src
│ ├── arrets //公共靜態(tài)資源
│ ├── components //公共組件
│ ├── imgs //圖片
│ ├── utils //公共方法
│ ├── services //公共請求
│ └── pages //多頁面文件夾
├── pages.json //子頁面描述說明集合文件
├── .env.development //開發(fā)-環(huán)境變量
├── .env.prerelease //預(yù)發(fā)-環(huán)境變量
├── .env.test //測試-環(huán)境變量
├── .env.production //生產(chǎn)-環(huán)境變量
├── .eslintrc.cjs //eslint 配置
├── .gitignore //git 提交忽略文件
├── .prettierignore //prettier 忽略文件
├── .prettierrc.json //prettier 配置
├── tsconfig.json //ts 配置
├── vite.config.ts //vite 配置
├── package.json
├── package-lock.json
二鹅颊、新建項(xiàng)目
vite創(chuàng)建vue項(xiàng)目堪伍,創(chuàng)建一個基礎(chǔ)模板就行觅闽,選擇ts蛉拙,其他router,store吮廉,sass等等隨意宦芦,不做贅述轴脐。
npm 安裝 prettier大咱、eslint、chalk(可以給打印臺的文字加顏色)
不安裝的話其中有些腳本可能會報錯溯捆。
這幾個包涉及到的邏輯不影響功能厦瓢,如果看得懂的話可以將對應(yīng)的腳本修改碳锈、優(yōu)化或刪除售碳,主要就是格式化文本,打印臺輸出文字變色提醒等
配置vite.config.ts
路徑別名间景、配置靜態(tài)資源目錄艺智、配置代理解決測試地址跨域問題等等
詳細(xì)介紹不再這里贅述倘要,自行查閱。
// vite.config.ts
...
resolve: {
alias: {
'@': path.join(__dirname, './src'),
'@pages': path.join(__dirname, './src/pages')
}
}
...
base: './' // 靜態(tài)資源基礎(chǔ)路徑
...
server: {
host: 'localhost', // 指定主機(jī)名
port: 8080, // 指定端口
hmr: true, // 開啟熱更新
open: true, // 在服務(wù)器啟動時自動在瀏覽器中打開應(yīng)用程序
proxy: { // 代理解決跨域問題
'/request': {
target: 'http://localhost:8081/',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/request/, '')
}
}
}
...
配置tsconfig.json
配置可以酌情使用十拣,其中重要的是include封拧,需要包含scripts下的文件,否則會有些報錯夭问。
// tsconfig.json
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"composite": true,
// "target": "esnext", //用于指定 TS 最后編譯出來的 ES 版本
"types": ["vite/client", "node"], //要包含的類型聲明文件名列表
"useDefineForClassFields": true, //將 class 聲明中的字段語義從 [[Set]] 變更到 [[Define]]
"module": "esnext", // 設(shè)置編譯后代碼使用的模塊化系統(tǒng):commonjs | UMD | AMD | ES2020 | ESNext | System
"moduleResolution": "node", // 模塊解析策略泽西,ts默認(rèn)用node的解析策略,即相對的方式導(dǎo)入
"strict": true, //開啟所有的嚴(yán)格檢查
"jsx": "preserve", //在 `.tsx`文件里支持JSX: `"React"`或 `"Preserve"`
"sourceMap": false, // 生成目標(biāo)文件的sourceMap文件
"resolveJsonModule": true, //允許導(dǎo)入擴(kuò)展名為“.json”的模塊
"isolatedModules": true, //確保每個文件都可以在不依賴其他導(dǎo)入的情況下安全地進(jìn)行傳輸
"esModuleInterop": true, //支持導(dǎo)入 CommonJs 模塊
"lib": ["esnext", "dom", "ES2015"], //TS需要引用的庫缰趋,即聲明文件捧杉,es5 默認(rèn)引用dom、es5秘血、scripthost,如需要使用es的高級版本特性灰粮,通常都需要配置,如es8的數(shù)組新特性需要引入"ES2019.Array",
// "noLib": false, //不包含默認(rèn)的庫文件( lib.d.ts)
"skipLibCheck": true, //忽略所有的聲明文件( *.d.ts)的類型檢查
"allowJs": true, // 允許編譯器編譯JS韧骗,JSX文件
"noEmit": true, // 不輸出文件,即編譯后不會生成任何js文件
"allowImportingTsExtensions": true,
"allowSyntheticDefaultImports": true, //允許從沒有設(shè)置默認(rèn)導(dǎo)出的模塊中默認(rèn)導(dǎo)入些侍。這并不影響代碼的輸出淋样,僅為了類型檢查。默認(rèn)值:module === "system" 或設(shè)置了 --esModuleInterop 且 module 不為 es2015 / esnext
"baseUrl": "./", //// 解析非相對模塊的基地址余指,默認(rèn)是當(dāng)前目錄
"paths": {
"@/*": ["src/*"], //解決引入報錯 找不到模塊“@/xxxx” 或其相應(yīng)的類型聲明
"@pages/*": ["src/pages/*"]
}
},
"include": [
"src/**/*.ts",
"src/**/*.js",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
"src/**/*.json",
"src/env.d.ts",
"src/global.d.ts",
"scripts/**/*.ts",
"scripts/**/*.vue"
],
"exclude": ["vite.config.ts", "src/**/__tests__/*"]
}
三淮韭、實(shí)現(xiàn)需求1粱腻,腳本創(chuàng)建新頁面
命令
npm run new-page
腳本使用的是node.js,主要使用的是fs模塊,操作文件夾及文件。其中各種方法的含義自行查詢
1. 將scripts文件夾復(fù)制到自己的項(xiàng)目中 git地址
2. package.json中添加命令
// package.json
"scripts": {
...
"new-page": "node ./scripts/newPage.mjs"
}
3. 執(zhí)行npm run new-page;
??提示"請輸入要生成的頁面"皮服,輸入規(guī)則是 a:b蕴侧,a表示頁面目錄裹纳,b表示頁面描述。輸入后回車,會在src/pages/下創(chuàng)建名為a的文件夾,并將/scripts/template/下的所有文件全部復(fù)制到a文件夾下恨狈。
??可以在template里添加自己項(xiàng)目的模板文件吗氏,比如router往产,store等等看自己項(xiàng)目的需求
4. newPage.mjs文件介紹
4.1 使用process.stdin獲取在控制臺中的輸入的內(nèi)容兴喂,即a:b蘑险;然后使用fs.mkdirSync創(chuàng)建文件夾,{recursive: true}表示允許創(chuàng)建多級目錄堆缘,例如:a/aa/aaa:b麻车;
```mjs
// newPage.mjs
process.stdin.on('data', async (chunk) => {
// 獲取輸入的信息
const content = String(chunk).trim().toString();
const inputSearch = content.search(':');
if (inputSearch == -1) {
errorLog('格式錯誤赁咙,請重新輸入');
return;
}
// 拆分用戶輸入的名稱和描述
inputName = content.split(':')[0];
inputDesc = content.split(':')[1] || inputName;
log(`將在 /src/pages 目錄下創(chuàng)建 ${inputName} 文件夾猿涨,并復(fù)制模板`);
const targetPath = resolve('./src/pages', inputName);
// 判斷同名文件夾是否存在
const pageExists = fs.existsSync(targetPath);
if (pageExists) {
errorLog('頁面已經(jīng)存在俺附,請重新輸入');
return;
}
// 創(chuàng)建目錄并復(fù)制文件
fs.mkdirSync(targetPath, { recursive: true });
successLog(`創(chuàng)建完成`);
...
...
})
```
4.2 創(chuàng)建文件成功后,用fs.copyFileSync撮奏,遞歸復(fù)制各級文件
// newPage.mjs
...
...
const sourcePath = resolve('./scripts/template');
copyFile(sourcePath, targetPath);
successLog(`模板復(fù)制完成`);
...
...
const copyFile = (sourcePath, targetPath) => {
const sourceFile = fs.readdirSync(sourcePath, { withFileTypes: true });
sourceFile.forEach((file) => {
const newSourcePath = path.resolve(sourcePath, file.name);
const newTargetPath = path.resolve(targetPath, file.name);
//isDirectory() 判斷這個文件是否是文件夾青自,是就繼續(xù)遞歸復(fù)制其內(nèi)容
if (file.isDirectory()) {
isExist(newTargetPath);
copyFile(newSourcePath, newTargetPath);
} else {
fs.copyFileSync(newSourcePath, newTargetPath);
}
});
};
4.3 將文件復(fù)制完成后,重寫兩個文件淤井,pages.json肛炮、index.html。
??pages.json是用于記錄當(dāng)前已創(chuàng)建的所有文件名稱和描述汰具,一是為了構(gòu)建所有頁面時使用,二是為了去重验靡,防止新舊頁面名稱一樣導(dǎo)致原頁面被重置。
??index.html是項(xiàng)目啟動的根頁面亚情,包含本項(xiàng)目中所有的頁面列表黄伊,方便快速打開想要調(diào)試的頁面
// newPage.mjs
/**
* 重寫pages.json
*/
async function setPagesFile(jsonData) {
// 通過writeFile改變數(shù)據(jù)內(nèi)容
log(`正在重寫pages.json文件`);
prettier.resolveConfig(resolve('./', '.prettierrc.json'));
const formatted = await prettier.format(JSON.stringify(jsonData), { parser: 'json' });
fs.writeFile(path.resolve('./', 'pages.json'), formatted, 'utf-8', (err) => {
if (err) throw err;
successLog(`重寫完成`);
setHtmlFile();
});
}
/**
* 重寫根目錄下的index.html勿锅,方便本地調(diào)試
*/
async function setHtmlFile(pageObj) {
log(`正在重寫根目錄下的index.html文件`, pageObj);
// 先獲取html文件原內(nèi)容
await fs.readFile(path.resolve('./', 'index.html'), 'utf-8', async (err, data) => {
if (err) throw err;
// 找到"<body>"位置,向其后插入用于跳轉(zhuǎn)的標(biāo)簽
const bodyTagIndex = data.indexOf('<body>');
if (bodyTagIndex === -1) {
console.error('<body> 標(biāo)簽未找到');
return;
}
// 在 <body> 后插入 <p> 標(biāo)簽
const insertIndex = bodyTagIndex + '<body>'.length;
const newContent = `${data.slice(0, insertIndex)}<p><a href="./src/pages/${inputName}/index.html">${inputDesc}</a></p>${data.slice(insertIndex)}`;
// 將新得到的字符串格式化
prettier.resolveConfig(resolve('./', '.prettierrc.json'));
const formatted = await prettier.format(newContent, { parser: 'html' });
fs.writeFile(path.resolve('./', 'index.html'), formatted, 'utf-8', (err) => {
if (err) throw err;
successLog(`重寫完成`);
process.stdin.emit('end');
});
});
}
四瞒大、實(shí)現(xiàn)需求2内列,單獨(dú)調(diào)試(dev)和打包(build)某個頁面
命令
npm run dev --page=a;
npm run build --page=a;
介紹 (主要修改vite.config.ts文件中的配置)
1. getBuildEnterPages()
根據(jù)命令中--page的值 動態(tài)配置build時的頁面入口嫩与,返回給 build.rollupOptions.input
// vite.config.ts
/**
* 獲取build時的頁面入口
* 該方法只支持單頁面的打包,不能支持全量打包,全量打包需要執(zhí)行build.mjs腳本
*/
const getBuildEnterPages = () => {
if (!npm_config_page && npm_lifecycle_event !== 'dev') {
errorLog('請?jiān)诿钚泻笠?`--page=頁面目錄` 格式指定頁面目錄塞椎!');
process.exit();
}
if (npm_lifecycle_event === 'build') {
infoLog('正在打包');
}
// 打包指定頁面钱雷,遍歷pages.json骨坑,判斷頁面是否存在
const filterArr = pages.filter(
(item) => item.chunk.toLowerCase() == npm_config_page.toLowerCase()
);
if (!filterArr.length && npm_lifecycle_event !== 'dev') {
errorLog('不存在此頁面礁遣,請檢查頁面目錄斑芜!');
process.exit();
}
return {
[npm_config_page]: resolve(__dirname, `src/pages/${npm_config_page}/index.html`)
};
};
// defineConfig
export default defineConfig({
...
build: {
rollupOptions: {
input: getBuildEnterPages()// 指定打包頁面入口
}
}
...
});
2. getEnterRoot()
動態(tài)修改root目錄
// vite.config.ts
/**
* 動態(tài)修改項(xiàng)目根目錄入口
* 1. 為了build后的文件結(jié)構(gòu)。不然index.html的目錄結(jié)構(gòu)太深了/dist/src/pages/a/index.html
* 2. 修改root目錄可以實(shí)現(xiàn)dev單頁面還是dev全部頁面
*/
const getEnterRoot = () => {
// 如果是dev祟霍,且沒有指定--page則直接啟動所有頁面
if (!npm_config_page && npm_lifecycle_event === 'dev') {
return resolve(__dirname);
}
// 遍歷pages.json杏头,判斷頁面是否存在
const filterArr = pages.filter(
(item) => item.chunk.toLowerCase() == npm_config_page.toLowerCase()
);
if (!filterArr.length) {
errorLog('不存在此頁面,請檢查頁面目錄浅碾!');
errorLog('命令以 `--page=頁面目錄` 格式指定頁面目錄!');
errorLog('若要打包全部頁面則需要執(zhí)行`npm run build-all`');
process.exit();
}
return resolve(__dirname, `src/pages/${npm_config_page}`);
};
// defineConfig
export default defineConfig({
...
root: getEnterRoot(),
...
});
3. 修改打包后的輸出路徑
// vite.config.ts
// defineConfig
export default defineConfig({
...
build: {
outDir: resolve(__dirname, `dist/${npm_config_page}`), // 指定打包后的文件輸出路徑 npm_config_page即 --page的值
}
...
});
4. 修改環(huán)境變量路徑 envDir
// vite.config.ts
// defineConfig
export default defineConfig({
...
envDir: resolve(__dirname), // 由于修改了root地址大州,所以需要重新指回環(huán)境變量的路徑為根目錄
...
});
五、實(shí)現(xiàn)需求3垂谢,同時調(diào)試(dev)和打包(build)所有頁面
命令
npm run dev;
npm run build-all;
1. npm run dev
dev所有頁面就是root配置成項(xiàng)目根目錄即可厦画,其實(shí)第四步里已經(jīng)實(shí)現(xiàn)了
npm run dev
啟動所有頁面了。如果想嚴(yán)謹(jǐn)一點(diǎn),比如必須
npm run dev-all
根暑,可以改造一下getBuildEnterPages
和getEnterRoot()
方法力试。并在package.json中添加命令
"dev-all": "vite"
// package.json
"scripts": {
...
"dev-all": "vite"
...
}
const getBuildEnterPages = () => {
if (npm_lifecycle_event === 'dev-all') {
return {
[npm_config_page]: resolve(__dirname)
};
}
if (!npm_config_page && npm_lifecycle_event !== 'dev') {
errorLog('請?jiān)诿钚泻笠?`--page=頁面目錄` 格式指定頁面目錄!');
process.exit();
}
if (npm_lifecycle_event === 'build') {
infoLog('正在打包');
}
// 打包指定頁面排嫌,遍歷pages.json畸裳,判斷頁面是否存在
const filterArr = pages.filter(
(item) => item.chunk.toLowerCase() == npm_config_page.toLowerCase()
);
if (!filterArr.length && npm_lifecycle_event !== 'dev') {
errorLog('不存在此頁面,請檢查頁面目錄淳地!');
process.exit();
}
return {
[npm_config_page]: resolve(__dirname, `src/pages/${npm_config_page}/index.html`)
};
};
const getEnterRoot = () => {
// 如果是dev-all怖糊,則返回整個項(xiàng)目的根目錄
if (!npm_config_page && npm_lifecycle_event === 'dev-all') {
return resolve(__dirname);
}
// 遍歷pages.json,判斷頁面是否存在
const filterArr = pages.filter(
(item) => item.chunk.toLowerCase() == npm_config_page.toLowerCase()
);
if (!filterArr.length) {
errorLog('不存在此頁面颇象,請檢查頁面目錄伍伤!');
errorLog('命令以 `--page=頁面目錄` 格式指定頁面目錄!');
errorLog('若要打包全部頁面則需要執(zhí)行`npm run build-all`');
process.exit();
}
return resolve(__dirname, `src/pages/${npm_config_page}`);
};
2. npm run build-all
在package.json中添加命令
"build-all": "node ./scripts/build.cjs"
,其實(shí)就是node執(zhí)行scripts文件夾中的build.mjs腳本腳本邏輯很簡單遣钳,就是獲取pages.json中的所有頁面信息扰魂,然后根據(jù)記錄的信息生成
npm run build --page=
,for循環(huán)執(zhí)行所有命令蕴茴,打包構(gòu)建所有頁面劝评。待優(yōu)化的點(diǎn):配置環(huán)境。如:
npm run build-all test
倦淀,npm run build-all development
蒋畜,npm run build-all prerelease
。
// package.json
"scripts": {
...
"build-all": "node ./scripts/build.cjs",
...
}
// build.cjs
const { exec } = require('child_process');
const pagesArray = require('../pages.json');
// 獲取命令行參數(shù)
const args = process.argv;
// 配置環(huán)境 比如npm run build-all:test npm run build-all:development 暫時沒啟用晃听,待優(yōu)化
const commandLineArgs = args.slice(2);
for (let i = 0; i < pagesArray.length; i++) {
const page = pagesArray[i];
// 定義要執(zhí)行的命令
const commandToExecute = `npm run build${commandLineArgs[0] === 'test' ? ':test' : ''} --page=${page.chunk}`;
exec(
commandToExecute,
(error, stdout, stderr) => {
if (error) {
console.error(`打包出錯: ${error.message}`);
return;
}
console.log(`打包成功(${commandToExecute}):\n${stdout}`);
}
);
}
其他
打包后會生成 tsconfig.tsbuildinfo
vite.config.ts.timestamp-1730464365481-a0d864a250d37.mjs
文件百侧,這兩個文件都可以隨時刪除,且記錄在.gitignore忽略文件中
tsconfig.tsbuildinfo
用于記錄構(gòu)建的文件信息能扒,以便下次構(gòu)建是跳過未發(fā)生變化的文件佣渴,提高構(gòu)建速度
vite.config.ts.timestamp-1730464365481-a0d864a250d37.mjs
當(dāng)build構(gòu)建失敗報錯時會生成該文件,用于記錄構(gòu)建信息的初斑,方便定位失敗原因
參考文檔
?? https://juejin.cn/post/7223286759630127159#heading-23;
?? 這篇文章寫的很詳細(xì)辛润,但是吐槽一下:內(nèi)容太多了,除了多頁面項(xiàng)目的搭建见秤,還加了一些其他方面的東西砂竖,有點(diǎn)冗余。其實(shí)可以分多篇文章介紹的鹃答。