#構建思路
雖然當前前端項目多以單頁面為主,但多頁面也並非一無是處,在一些情況下也是有用武之地的,比如:
- 項目龐大,各個業務模塊需要解耦
- SEO更容易優化
- 沒有復雜的狀態管理問題
- 可以實現頁面單獨上線
#前言
這里就第4點做一些解釋,也對多頁面的應用場景做一個我認為有價值的思路,在組內的一個項目中,因為項目日益膨脹,拆分系統有一定困難,項目頁面達到200+個以上, 因此構建速度十分緩慢,部署時間也很長,經常因為文案的更改及一些簡單的bug修復就要進行重新構建,如果采用單頁面一方面構建部署時間會隨着體量增大,另一方面在工程上不好進行拆分。這時候多頁面就存在一種優勢,我們可以在前端做一個空框架只包含菜單,內容區域采用多頁面結構,當我們部署上線時可以只針對單個頁面進行上線,速度大幅度提升(單頁面內部可以集成前端路由),這樣業務模塊間也可平滑解耦。
#項目架構
vue + typescript + webpack4 vue項目,並沒有使用vue-cli,原因是對於開發人員來說,了解構建的詳細流程很重要,vue-cli這類工具的目的是快速實現項目的搭建,讓開發人員快速接手,快速進入 業務代碼編寫,因此隱含的為我們做了很多事,很多構建及本地開發的優化等等,但對於開發人員來說了解每個步驟,每個細節是做什么的對自身成長很有幫助(尤其是組里的很多程序員都不愛使用高度封裝的東西)。
#思路
對於多頁面來說,與單頁面對比無非就是以下幾個問題:
- entry入口文件為多個,需要考慮頁面多需要自動生成,少的話提前預置幾個就可以。
- htmlWebpackPlugin使用時也需要相應的添加多個。
- 公共靜態資源提取的問題,splitchunkplugin是否需要使用的問題。
- 最后就是支持項目的部分構建的功能實現
為達到我們的終極目標,也就是能夠部分代碼進行構建,我們將一個項目從業務角度進行一個划分,兩個層級,模塊和頁面,模塊代表一個具體業務場景,頁面代表這個業務場景的各個頁面,我們將支持進行單/多模塊和單/多頁面的打包。
#開始
首先先看一下我們的項目目錄結構
├── build_tasks // 構建腳本
├── config // 配置文件
├── src // 源碼路徑
└── static //build后文件路徑
src目錄:
src
├── global.js // 項目全局工具
├── modules // 模塊 │
├── Layout.vue │
├── moduleA // 具體模塊名 │
│ └── pageA // 具體頁面名稱 │
├── xx.js │
├── index.vue
#自動生成entry
由於我們的頁面非常之多,因此我們肯定是需要自動生成entry文件的,並且這一步是需要在進入webpack構建流程之前就要做好的。我們創建一個build_entries.ts的文件,用於編寫創建entry流程,這里放一些核心代碼
const getTemplate = pagePath => { return ( ` import App from '${pagePath}'; import Vue from 'vue'; new Vue({ render: function (h) { return h(App); } }).$mount('#app');`); } const scriptReg = /<script([\s\S]*?)>/; /** * 判斷文件應該采用的后綴 */ const getSuffix = (source: string): string => { const matchArr = source.match(scriptReg) || []; if(matchArr[1].includes('ts')){ return '.ts' } return '.js'; }; const generateEntries = () => { const entries = {}; /***一些前置代碼拿到pages*/ if (!pages.length) return entries; // 清除entries rimraf.sync(entryPath+'/*.*'); pages.forEach(page => { const relativePage = path.relative(vueRoot, page); const source = fs.readFileSync(page, 'utf8'); const suffix = getSuffix(source); const pageEntry = path.resolve(entryPath, relativePage.replace(/\/index\.vue$/, '').replace(/\//g, '.')) + suffix; const entryName = path.basename(pageEntry, suffix); entries[entryName] = pageEntry if (fs.existsSync(pageEntry)) return; const pagePath = path.resolve(vueRoot, relativePage); const template = getTemplate(pagePath); fs.writeFileSync(pageEntry, template, 'utf-8'); }); return entries } export const getEntriesInfos = ()=>{ return generateEntries(); }
大概解釋下思路,我們規定項目目錄結構為modules/xxmodle/xxpage,我們以命名為index.vue的頁面為入口頁面,為每個index.vue創建入口的js模版(getTemplete方法),生成的entry名稱為"模塊名.頁面名.js"。因為項目內需要支持ts,因此我們還需要判斷vue內的script標簽的語言,以便創建ts格式的entry還是js格式的entry。 我們的webpack配置:
const entries = getEntriesInfos(); const common = { entry: entries, output: { filename: `[name]-[hash].bundle.js`, path: path.resolve(rootPath, 'static'), publicPath, },
#公共文件提取
因為我們是多頁面,每個頁面都需要加載核心的包(如vue,element-ui,lodash等等)而這類包我們是不常變化的,因此我們需要使用webpack的dllplugin來剝離他們出來,不參與構建,我們的項目中也可能會有我們自己的全局工具包,這部分代碼不適合提取,只需要在entry中再加入一個common的entry即可。對於單頁面內是否需要使用splitchunk,在我的實踐中是沒有使用的,但是這個看情況,如果頁面引用的包確實比較大(畢竟vue這類框架包已經被提取出去了,這個概率不大)那么可以使用splitchunk來分離,我目前的實踐是合並到一個頁面的js內,單頁面js在gzip后在200k以內都可忍受。 下面放一下dll的配置 webpack.dll.config.ts
const commonLibs = ['vue','element-ui','moment', 'lodash'] export default { mode: 'production', entry: { commonLibs }, output: { path: path.join(__dirname, 'dll_libs'), filename: 'dll.[name].[hash:8].min.js', library: '[name]', // publicPath: '/static/' }, plugins: [ new CleanWebpackPlugin(), new webpack.DllPlugin({ context: __dirname, path: path.join(__dirname, 'dll_libs/', '[name]-manifest.json'), name: '[name]' }), new assetsWebpackPlugin({ filename: 'dll_assets.json', path: path.join(__dirname,'assets/') }) ] } as webpack.Configuration;
如代碼所示我們將'vue','element-ui','moment', 'lodash'這幾個組件提取打成一個公共包命名為commonLib,這里使用了assetsWebpackPlugin用於生成一個json文件,記錄每次dll構建的文件名(因為每次構建hash是不一樣的),為的是在使用webpackhtmlplugin的時候拿到這個結果注入到模版頁面中去。 生成的json記錄類似:
{"commonLibs":{"js":"dll.commonLibs.51be3e86.min.js"}}
這樣我們就可以在webpack配置文件中取到這個名字:
const dllJson = require('./assets/dll_assets.json');
for(let entryKey in entries){
if(entryKey!== 'global'){
common.plugins.push(
new HtmlWebpackPlugin({
title: allConfiguration[entryKey].title,
isDebug: process.env.DEBUG,
filename: `${entryKey}.html`,
template: 'index.html',
chunks:['global', entryKey, ],
chunksSortMode: 'manual',
dll_common_assets: process.env.NODE_ENV !== 'production'?'./dll_libs/' + dllJson.commonLibs.js : publicPath + 'dll_libs/' + dllJson.commonLibs.js,
}),
)
}
}
因為是多頁面,因此我們webpackhtml使用時也是要添加多個的,這里根據生成的json拿到dll的文件名注入到模版頁面中。
#按需打包
接下來我們要支持進行按需構建打包,支持單/多模塊以及單/多頁面的打包,這里怎么做呢,可以在構建時傳入環境變量,然后在build_entry中判斷環境變量進行局部打包,因為打包的入口是entry的數量決定的。 命令可以這樣構成:
MODULES=xxx,xxx PAGES=sss,sss npm run build
build_entry相關代碼,在generateEntries方法中
const entries = {};
const buildModules = process.env.MODULES || '*';
const buildPages = process.env.PAGES || '*';
const filePaths = `${!buildModules.includes(',') ? buildModules : '{'+buildModules+'}'}/${!buildPages.includes(',') ? buildPages : '{'+buildPages+'}'}/*.vue`
const pages = glob.sync(path.resolve(vueRoot, filePaths)).filter(file =>{
return /index\.vue$/.test(file) || [];
})
if (!pages.length) return entries;
上面的方法根據傳入的環境變量拼對應的頁面及模塊路徑,通過glob的支持生成對應的entyr進行構建。
#多頁面線上發布
多頁面構建完成之后就是發布流程,發布流程其實也會變的簡單,如果是單頁面每次構建完成都要整體替換靜態文件(js,css),多頁面模式下,我們只需要替換對應頁面的文件即可,一般的思路是頁面文件可以上傳到部署的服務器,然后靜態js,css等文件直接扔到CDN上即可,發布不會影響到其他頁面,即便出錯也不會影響項目,而且效率極高,這部分代碼就不展示了,只是提供思路,畢竟每個項目發布流程都不太一樣。
#總結
以上是我對多頁面應用場景的一個思路,它是有一定的適用場景的,比較適合大而全而且模塊划分清晰的系統。
原文鏈接:https://zhangzippo.github.io/posts/2019/05/12/_20xx-05-10mutilpage.html