webpack4 + vue多頁面項目精細(xì)構(gòu)建思路
原文鏈接:https://zhangzippo.github.io/posts/2019/05/12/_20xx-05-10mutilpage.html
雖然當(dāng)前前端項目多以單頁面為主,但多頁面也并非一無是處,在一些情況下也是有用武之地的,比如:
- 項目龐大,各個業(yè)務(wù)模塊需要解耦
- SEO更容易優(yōu)化
- 沒有復(fù)雜的狀態(tài)管理問題
- 可以實現(xiàn)頁面單獨上線
前言
這里就第4點做一些解釋,也對多頁面的應(yīng)用場景做一個我認(rèn)為有價值的思路,在組內(nèi)的一個項目中,因為項目日益膨脹,拆分系統(tǒng)有一定困難,項目頁面達(dá)到200+個以上, 因此構(gòu)建速度十分緩慢,部署時間也很長,經(jīng)常因為文案的更改及一些簡單的bug修復(fù)就要進(jìn)行重新構(gòu)建,如果采用單頁面一方面構(gòu)建部署時間會隨著體量增大,另一方面在工程上不好進(jìn)行拆分。這時候多頁面就存在一種優(yōu)勢,我們可以在前端做一個空框架只包含菜單,內(nèi)容區(qū)域采用多頁面結(jié)構(gòu),當(dāng)我們部署上線時可以只針對單個頁面進(jìn)行上線,速度大幅度提升(單頁面內(nèi)部可以集成前端路由),這樣業(yè)務(wù)模塊間也可平滑解耦。
項目架構(gòu)
vue + typescript + webpack4
vue項目,并沒有使用vue-cli,原因是對于開發(fā)人員來說,了解構(gòu)建的詳細(xì)流程很重要,vue-cli這類工具的目的是快速實現(xiàn)項目的搭建,讓開發(fā)人員快速接手,快速進(jìn)入 業(yè)務(wù)代碼編寫,因此隱含的為我們做了很多事,很多構(gòu)建及本地開發(fā)的優(yōu)化等等,但對于開發(fā)人員來說了解每個步驟,每個細(xì)節(jié)是做什么的對自身成長很有幫助(尤其是組里的很多程序員都不愛使用高度封裝的東西)。
思路
對于多頁面來說,與單頁面對比無非就是以下幾個問題:
- entry入口文件為多個,需要考慮頁面多需要自動生成,少的話提前預(yù)置幾個就可以。
- htmlWebpackPlugin使用時也需要相應(yīng)的添加多個。
- 公共靜態(tài)資源提取的問題,splitchunkplugin是否需要使用的問題。
- 最后就是支持項目的部分構(gòu)建的功能實現(xiàn)
為達(dá)到我們的終極目標(biāo),也就是能夠部分代碼進(jìn)行構(gòu)建,我們將一個項目從業(yè)務(wù)角度進(jìn)行一個劃分,兩個層級,模塊和頁面,模塊代表一個具體業(yè)務(wù)場景,頁面代表這個業(yè)務(wù)場景的各個頁面,我們將支持進(jìn)行單/多模塊和單/多頁面的打包。
開始
首先先看一下我們的項目目錄結(jié)構(gòu)
├── build_tasks // 構(gòu)建腳本
├── config // 配置文件
├── src // 源碼路徑
└── static //build后文件路徑
src目錄:
src
├── global.js // 項目全局工具
├── modules // 模塊
│ ├── Layout.vue
│ ├── moduleA // 具體模塊名
│ │ └── pageA // 具體頁面名稱
│ │ ├── xx.js
│ │ ├── index.vue
自動生成entry
由于我們的頁面非常之多,因此我們肯定是需要自動生成entry文件的,并且這一步是需要在進(jìn)入webpack構(gòu)建流程之前就要做好的。我們創(chuàng)建一個build_entries.ts的文件,用于編寫創(chuàng)建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]*?)>/;
/**
* 判斷文件應(yīng)該采用的后綴
*/
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();
}
大概解釋下思路,我們規(guī)定項目目錄結(jié)構(gòu)為modules/xxmodle/xxpage,我們以命名為index.vue的頁面為入口頁面,為每個index.vue創(chuàng)建入口的js模版(getTemplete方法),生成的entry名稱為"模塊名.頁面名.js"。因為項目內(nèi)需要支持ts,因此我們還需要判斷vue內(nèi)的script標(biāo)簽的語言,以便創(chuàng)建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來剝離他們出來,不參與構(gòu)建,我們的項目中也可能會有我們自己的全局工具包,這部分代碼不適合提取,只需要在entry中再加入一個common的entry即可。對于單頁面內(nèi)是否需要使用splitchunk,在我的實踐中是沒有使用的,但是這個看情況,如果頁面引用的包確實比較大(畢竟vue這類框架包已經(jīng)被提取出去了,這個概率不大)那么可以使用splitchunk來分離,我目前的實踐是合并到一個頁面的js內(nèi),單頁面js在gzip后在200k以內(nèi)都可忍受。
下面放一下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構(gòu)建的文件名(因為每次構(gòu)建hash是不一樣的),為的是在使用webpackhtmlplugin的時候拿到這個結(jié)果注入到模版頁面中去。
生成的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使用時也是要添加多個的,這里根據(jù)生成的json拿到dll的文件名注入到模版頁面中。
按需打包
接下來我們要支持進(jìn)行按需構(gòu)建打包,支持單/多模塊以及單/多頁面的打包,這里怎么做呢,可以在構(gòu)建時傳入環(huán)境變量,然后在build_entry中判斷環(huán)境變量進(jìn)行局部打包,因為打包的入口是entry的數(shù)量決定的。
命令可以這樣構(gòu)成:
MODULES=xxx,xxx PAGES=sss,sss npm run build
build_entry相關(guān)代碼,在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;
上面的方法根據(jù)傳入的環(huán)境變量拼對應(yīng)的頁面及模塊路徑,通過glob的支持生成對應(yīng)的entyr進(jìn)行構(gòu)建。
多頁面線上發(fā)布
多頁面構(gòu)建完成之后就是發(fā)布流程,發(fā)布流程其實也會變的簡單,如果是單頁面每次構(gòu)建完成都要整體替換靜態(tài)文件(js,css),多頁面模式下,我們只需要替換對應(yīng)頁面的文件即可,一般的思路是頁面文件可以上傳到部署的服務(wù)器,然后靜態(tài)js,css等文件直接扔到CDN上即可,發(fā)布不會影響到其他頁面,即便出錯也不會影響項目,而且效率極高,這部分代碼就不展示了,只是提供思路,畢竟每個項目發(fā)布流程都不太一樣。
總結(jié)
以上是我對多頁面應(yīng)用場景的一個思路,它是有一定的適用場景的,比較適合大而全而且模塊劃分清晰的系統(tǒng)。