背景
MPX是滴滴出品的一款增強型小程式跨端框架,其核心是對原生小程式功能的增強。具體的使用不是本文討論的範疇,想了解更多可以去官網瞭解更多。
回到正題,使用MPX開發小程式有一段時間了,該框架對不同包之間的共享資源有一套自己的構建輸出策略,其官網有這樣一段描述說明:
總結關鍵的兩點:
純js資源
:主包引用則輸出主包,或者分包之間共享也輸出到主包非js資源,包括wxml、樣式、圖片等
:主包引用則輸出主包,分包之間共享則輸出到各自分包
很好奇MPX內部是怎麼做到上面這種效果的,尤其是js資源,於是就拜讀了@mpxjs/webpack-plugin@2.6.61
揭開其實現的細節。
mpx怎麼實現的
首先簡單介紹下MPX
是怎麼整合小程式離散化的檔案結構,它基於webpack打包構建的,使用者在Webpack配置中只需要配置一個入口檔案app.mpx,它會基於依賴分析
和動態新增entry
的方式來整合小程式的離散化檔案,,loader會解析json配置檔案中的pages域
和usingComponents域
中宣告的路徑,通過動態新增entry的方式將這些檔案新增到Webpack的構建系統當中,並遞迴執行這個過程,直到整個專案中所有用到的.mpx
檔案都加入進來。
重點來了,MPX在輸出前,其藉助了webpack的SplitChunksPlugin
的能力將複用的模組抽取到一個外部的bundle中,確保最終生成的包中不包含重複模組。
js資源模組的輸出
@mpxjs/webpack-plugin
外掛是MPX基於webapck構建的核心,其會在webpack所有模組構建完成的finishMoudles
鉤子中來實現構建輸出策略,主要是配置SplitChunks
的cacheGroup,後續webpack程式碼優化階段會根據SplitChunks
的配置來輸出程式碼。
apply(compiler) {
...
// 拿到webpack預設配置物件splitChunks
let splitChunksOptions = compiler.options.optimization.splitChunks
// 刪除splitChunks配置後,webpack內部就不會例項化SplitChunkPlugin
delete compiler.options.optimization.splitChunks
// SplitChunkPlugin的例項化由mpx來接管,這樣可以拿到其例項可以後續對其options進行修正
let splitChunksPlugin = new SplitChunksPlugin(splitChunksOptions)
splitChunksPlugin.apply(compiler)
...
compilation.hooks.finishModules.tap('MpxWebpackPlugin', (modules) => {
// 自動跟進分包配置修改splitChunksPlugin配置
if (splitChunksPlugin) {
let needInit = false
Object.keys(mpx.componentsMap).forEach((packageName) => {
if (!splitChunksOptions.cacheGroups.hasOwnProperty(packageName)) {
needInit = true
splitChunksOptions.cacheGroups[packageName] = getPackageCacheGroup(packageName)
}
})
if (needInit) {
splitChunksPlugin.options = SplitChunksPlugin.normalizeOptions(splitChunksOptions)
}
}
})
可以看出在所有模組構建完成時,針對不同的packageName來生成其對應的cacheGroups
,主要體現在getPackageCacheGroup方法的實現。然後拿到SplitChunksPlugin
例項的控制程式碼,對其options進行重寫規格化。
function isChunkInPackage (chunkName, packageName) {
return (new RegExp(`^${packageName}\\/`)).test(chunkName)
}
function getPackageCacheGroup (packageName) {
if (packageName === 'main') {
return {
name: 'bundle',
minChunks: 2,
chunks: 'all'
}
} else {
return {
test: (module, chunks) => {
return chunks.every((chunk) => {
return isChunkInPackage(chunk.name, packageName)
})
},
name: `${packageName}/bundle`,
minChunks: 2,
minSize: 1000,
priority: 100,
chunks: 'all'
}
}
}
getPackageCacheGroup會為小程式的每個包生成一個程式碼分割組,也就是生成每個包對應的cacheGroups
。
例如一個小程式專案有主包和A、B兩個分包,其生成的cacheGroups
內容如下:
{
default: {
automaticNamePrefix: '',
reuseExistingChunk: true,
minChunks: 2,
priority: -20
},
vendors: {
automaticNamePrefix: 'vendors',
test: /[\\/]node_modules[\\/]/,
priority: -10
},
main: { name: 'bundle', minChunks: 2, chunks: 'all' },
A: {
test: [Function: test],
name: 'A/bundle',
minChunks: 2,
minSize: 1000,
priority: 100,
chunks: 'all'
},
B: {
test: [Function: test],
name: 'B/bundle',
minChunks: 2,
minSize: 1000,
priority: 100,
chunks: 'all'
}
分包程式碼分割輸出bundle的優先順序是最高的(priority: 100
),所以會優先處理分包中的打包;否則會執行main
中的程式碼打包規則,它會處理所有包之間的共享模組的打包以及主包中被複用的模組。
下面來看分包和主包的打包規則。
1、針對分包中的模組:
{
test: (module, chunks) => {
// 依賴當前模組的所有chunks是否都是當前分包下的chunk
return chunks.every((chunk) => {
return isChunkInPackage(chunk.name, packageName)
})
},
name: `${packageName}/bundle`,
minChunks: 2,
minSize: 1000,
priority: 100,
chunks: 'all'
}
分包中的模組被抽離到當前分包下的bundle檔案中,需滿足:
- 該模組沒有被其他包引用,包括主包和其他分包(
test函式邏輯
) - 至少被該分包下的2個
chunk
引用(minChunks:2
) - 抽離後的bundle大小最少滿足 約1kb(
minSize: 1000
)
2、針對主包中的模組:
{
name: 'bundle',
minChunks: 2,
chunks: 'all'
}
會抽取到主包的bundle檔案的條件:
- 該模組至少被2個chunk引用(
minChunks:2
),這個chunk不區分主分包中的chunk
3、針對分包間共享的模組:
分包間共享的模組,不滿足SplitChunks
為每個分包設定的分包獨享規則,即該模組只在當前分包引用,沒有在其他包中被引用過。
test: (module, chunks) => {
return chunks.every((chunk) => {
return isChunkInPackage(chunk.name, packageName)
})
}
所以,最終會走到main
的cacheGroup的打包規則中,也就是主包的打包規則中。
這樣,MPX通過配置SplitChunksOptions.cacheGroups
來實現將主包中的js模組和分包共享的js模組都輸出到主包,分包單獨引用的模組輸出到當前分包下。
元件和靜態資源
對於元件和靜態資源,MPX在webpack構建的thisCompilation
鉤子函式中會在compilation
上掛載一個有關打包的__mpx__
物件,包含靜態資源、元件資源、頁面資源等屬性,也包含靜態的非js資源的輸出處理等:
compiler.hooks.thisCompilation.tap('MpxWebpackPlugin', (compilation, { normalModuleFactory }) => {
...
if (!compilation.__mpx__) {
mpx = compilation.__mpx__ = {
...
componentsMap: {
main: {}
},
// 靜態資源(圖片,字型,獨立樣式)等,依照所屬包進行記錄,冗餘儲存,同上
staticResourcesMap: {
main: {}
},
...
// 元件和靜態資源的輸出規則如下:
// 1. 主包引用的資源輸出至主包
// 2. 分包引用且主包引用過的資源輸出至主包,不在當前分包重複輸出
// 3. 分包引用且無其他包引用的資源輸出至當前分包
// 4. 分包引用且其他分包也引用過的資源,重複輸出至當前分包
getPackageInfo: ({ resource, outputPath, resourceType = 'components', warn }) => {
let packageRoot = ''
let packageName = 'main'
const { resourcePath } = parseRequest(resource)
const currentPackageRoot = mpx.currentPackageRoot
const currentPackageName = currentPackageRoot || 'main'
const resourceMap = mpx[`${resourceType}Map`]
const isIndependent = mpx.independentSubpackagesMap[currentPackageRoot]
// 主包中有引用一律使用主包中資源,不再額外輸出
if (!resourceMap.main[resourcePath] || isIndependent) {
packageRoot = currentPackageRoot
packageName = currentPackageName
...
}
resourceMap[packageName] = resourceMap[packageName] || {}
const currentResourceMap = resourceMap[packageName]
let alreadyOutputed = false
if (outputPath) {
outputPath = toPosix(path.join(packageRoot, outputPath))
// 如果之前已經進行過輸出,則不需要重複進行
if (currentResourceMap[resourcePath] === outputPath) {
alreadyOutputed = true
} else {
currentResourceMap[resourcePath] = outputPath
}
} else {
currentResourceMap[resourcePath] = true
}
return {
packageName,
packageRoot,
outputPath,
alreadyOutputed
}
},
...
}
}
}
這樣webpack構建編譯非js資源時會呼叫compilation.__mpx__.getPackageInfo
方法返回非js的靜態資源的輸出路徑,在該方法內部制定瞭如下資源的的輸出規則:
- 主包引用的資源輸出至主包
- 分包引用且主包引用過的資源輸出至主包,不在當前分包重複輸出
- 分包引用且無其他包引用的資源輸出至當前分包
- 分包引用且其他分包也引用過的資源,重複輸出至當前分包
這樣,mpx在處理專案中的靜態資源時,會呼叫該方法獲得靜態資源的輸出路徑。
下面以一個簡單例子來說明,
例如對專案中的圖片會呼叫@mpxjs/webpack-plugin
提供的url-loader
進行處理,與webpack的url-loader
類似,對於圖片大小小於指定limit的進行base64
處理,否則使用file-loader
來輸出圖片(此時需要呼叫getPackageInfo方法獲取圖片的輸出路徑),相關程式碼:
let outputPath
if (options.publicPath) { // 優先loader配置的publicPath
outputPath = url
if (options.outputPathCDN) {
if (typeof options.outputPathCDN === 'function') {
outputPath = options.outputPathCDN(outputPath, this.resourcePath, context)
} else {
outputPath = toPosix(path.join(options.outputPathCDN, outputPath))
}
}
} else {
// 否則,呼叫getPackageInfo獲取輸出路徑
url = outputPath = mpx.getPackageInfo({
resource: this.resource,
outputPath: url,
resourceType: 'staticResources',
warn: (err) => {
this.emitWarning(err)
}
}).outputPath
}
...
this.emitFile(outputPath, content);
...
最終,圖片資源會呼叫compilation.__mpx__.getPackageInfo
方法來獲取圖片資源的輸出路徑進行產出。
同樣對於css資源
、wxml資源
以及json資源
,mpx內部是通過建立子編譯器來抽取的,這裡就不做深入介紹。
splitChunks的用法
webpack的splitChunks
外掛是用來進行程式碼拆分的,通過上面的分析可以看出MPX內部是通過內建splitChunks
的cacheGroups
配置項來主動實現對小程式js模組實現分割優化的。webpack常見的程式碼分割方式有三種:
- 多入口分割:webpack的
entry
配置項配置的手動入口,也包括可以使用compilation.addEntry
程式新增的入口 - 動態匯入:通過模組的行內函數來分離程式碼,如通過
import('./a')
- 防止重複:使用
splitChunks
來去重和分離chunk
前兩種在我們日常的開發中比較常見,第三種是通過webpack的optimization.splitChunks
配置項來配置的。
通常情況下,webpack配置項optimization.splitChunks
會有預設配置來實現程式碼分割,上面我們說到MPX在為不同包生成cacheGroups
時,細心的同學會發現我們最終生成的包多了兩個配置組:
{
default: {
automaticNamePrefix: '',
reuseExistingChunk: true,
minChunks: 2,
priority: -20
},
vendors: {
automaticNamePrefix: 'vendors',
test: /[\\/]node_modules[\\/]/,
priority: -10
},
...
}
這是webpack為optimization.splitChunks.cacheGroups
配置的預設組,除此之外optimization.splitChunks
還有一些其他預設配置項,如下程式碼所示:
splitChunks: {
chunks: "async",
minSize: 30000,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
上面預設配置的實現的效果是:滿足下面4個條件的模組程式碼會抽離成新的chunk
- 來自
node_modules
中的模組,或者至少被2個chunk複用的模組程式碼 - 分離出的chunk必須大於等於3000byte,約30kb
- 按需非同步載入chunk時,並行請求的最大數不超過5個
- 頁面初始載入時,並行請求的最大數不超過3個
下面來介紹下這些配置項的作用:
chunks
:表示webpack將對哪些chunk進行分割,可選值為async、all、initial- async:對於非同步載入的chunks進行分割
- initial:對非非同步載入的初始chunks進行分割
- all:對所有chunks進行分割
minSize
: 分割後的chunk要滿足的最小大小,否則不會分割minChunks
: 表示一個模組至少應被minChunks個chunk所包含才能分割maxAsyncRequests
: 表示按需載入非同步chunk時,並行請求的最大數目;這個數目包括當前請求的非同步chunk以及其所依賴chunk的請求maxInitialRequests
: 表示載入入口chunk時,並行請求的最大數目automaticNameDelimiter
: 表示拆分出的chunk的名稱連線符,預設為。如chunkvendors.jsname
: 設定chunk的檔名,預設為true,表示splitChunks基於chunk和cacheGroups的key自動命名。cacheGroups
: 通過它可以配置多個組,實現精細化分割程式碼;- 該物件配置屬性繼承
splitChunks
中除cacheGroups
外所有屬性,可以在該物件重新配置這些屬性值覆蓋splitChunks
中的值 - 該物件還有一些特有屬性如
test
、priority
和reuseExistingChunk
等
- 該物件配置屬性繼承
針對cacheGroups
配置補充一點:
cacheGroups
配置的每個組可以根據test設定條件,符合test條件的模組,就分配到該組。模組可以被多個組引用,但最終會根據priority
來決定打包到哪個組中。