网上大量篇幅在讲如何分包,降低图片资源等等策略,但针对一些陈年老项目大家会发现,这些策略都已做过,那么该如何降低包体积呢?
在 uni-app 小程序开发中,主包体积超限 是最常见的痛点之一。官方默认的split-chunks策略存在一个硬伤:一个 JS 模块只要被 2 个及以上分包同时引用,即便主包完全不用,也会被强制打包进主包common/vendor,直接导致主包体积暴涨,甚至无法发布。
为了解决这个问题,写一个插件,专门针对小程序平台,自动将多分包共享的模块从主包剥离,分别下沉到对应分包的 vendor 中,精准瘦身主包。
一、先搞懂:为什么主包会莫名其妙变大
uni-app 小程序默认分包优化规则:
- 仅被单个分包使用的模块 → 自动打包到该分包 vendor(最优)
- 被主包 + 任意分包使用的模块 → 打包进主包 vendor(合理)
- 被2 个及以上分包 使用、主包未使用的模块 → 强制打包进主包 vendor( 问题根源)
这就导致:很多公共工具类、组件、工具库,只要被多个分包共享,就会挤爆主包体积。
二、插件核心能力
SplitUtilPlugin 专门解决上述第 3 种场景:
- 仅针对小程序平台生效,不影响 H5/APP
- 自动识别:多分包共享、主包未引用的模块
- 自动剥离:从主包
common/vendor移除 - 自动分发:分别打包到对应分包的
common/vendor - 安全兜底:保留主包引用的模块,不破坏业务逻辑
- 可视化日志:清晰查看模块移动记录,方便排查
三、完整插件代码
javascript
/**
* SplitUtilPlugin
* 解决 uni-app 主包体积过大问题
* 核心:将【多分包共享、主包未使用】的模块从主包vendor移至对应分包vendor
*/
'use strict'
const path = require('path')
const GraphHelpers = require('webpack/lib/GraphHelpers')
const PLUGIN_NAME = 'SplitUtilPlugin'
const MAIN_VENDOR = 'common/vendor'
// 路径标准化
function normalizePath(p) {
return (p || '').replace(/\\/g, '/')
}
// 根据名称查找chunk
function findChunkByName(chunks, name) {
return chunks.find((c) => c.name === name)
}
class SplitUtilPlugin {
apply(compiler) {
// 仅小程序平台生效
const platform = process.env.UNI_PLATFORM || process.env.VUE_APP_PLATFORM
const isMP = platform && platform.startsWith('mp-')
if (!isMP) {
console.log(`[${PLUGIN_NAME}] 非小程序平台,插件跳过。`)
return
}
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
// webpack优化阶段:splitChunks已完成,chunk归属已确定
compilation.hooks.optimizeChunkModules.tap(
PLUGIN_NAME,
(chunks, modules) => {
// 获取所有分包根目录
const subPackageRoots = Object.keys(
process.UNI_SUBPACKAGES || {},
).map((root) => normalizePath(root) + '/')
if (subPackageRoots.length === 0) {
console.warn(`[${PLUGIN_NAME}] 未配置分包,跳过。`)
return
}
const chunkArr = Array.from(chunks)
// 查找主包vendor
const mainVendorChunk = findChunkByName(chunkArr, MAIN_VENDOR)
if (!mainVendorChunk) {
console.log(`[${PLUGIN_NAME}] 未找到主包vendor,跳过。`)
return
}
// 判断chunk所属分包
const getSubPackageRoot = (chunk) => {
const name = normalizePath(chunk.name || '')
return subPackageRoots.find((root) => name.startsWith(root)) || null
}
// 判断是否为主包chunk
const isMainPackageChunk = (chunk) => !getSubPackageRoot(chunk)
// 统计数据
let movedCount = 0
let skippedMainRef = 0
let skippedSinglePkg = 0
// 遍历所有模块
for (const mod of modules) {
// 仅处理真实JS文件,跳过CSS/虚拟模块
if (!mod.resource || mod.type === 'css/mini-extract') continue
const modChunks = mod.getChunks()
// 仅处理当前只在主包vendor中的模块
if (modChunks.length !== 1 || modChunks[0].name !== MAIN_VENDOR)
continue
// 收集所有引用该模块的业务chunk
const issuerChunks = new Set()
if (mod.reasons) {
for (const reason of mod.reasons) {
if (reason.module) {
for (const rc of reason.module.getChunks()) {
issuerChunks.add(rc)
}
}
}
}
// 兜底:获取真实引用chunk
const referenceChunks = issuerChunks.size > 0
? Array.from(issuerChunks)
: chunkArr.filter(c => c.name !== MAIN_VENDOR && c.hasModule?.(mod))
// 安全规则:主包直接引用的模块,不移动
const hasMainRef = referenceChunks.some(isMainPackageChunk)
if (hasMainRef) {
skippedMainRef++
continue
}
// 收集所有引用该模块的分包
const targetPkgRoots = new Set()
for (const rc of referenceChunks) {
const root = getSubPackageRoot(rc)
if (root) targetPkgRoots.add(root)
}
// 仅处理多分包共享模块(单分包uni已自动优化)
if (targetPkgRoots.size < 2) {
skippedSinglePkg++
continue
}
// 为每个目标分包创建/关联vendor chunk
for (const pkgRoot of targetPkgRoots) {
const targetChunkName = normalizePath(
path.join(pkgRoot.replace(/\/$/, ''), MAIN_VENDOR),
)
let pkgChunk = findChunkByName(chunkArr, targetChunkName)
// 不存在则新建分包vendor
if (!pkgChunk) {
const group = compilation.addChunkInGroup(targetChunkName)
pkgChunk = group.chunks[0]
chunkArr.push(pkgChunk)
}
// 连接模块与分包vendor
GraphHelpers.connectChunkAndModule(pkgChunk, mod)
}
// 从主包vendor移除该模块
GraphHelpers.disconnectChunkAndModule(mainVendorChunk, mod)
movedCount++
// 打印移动日志
console.log(`[${PLUGIN_NAME}] 移动[${movedCount}]: ${normalizePath(mod.resource)} -> 分包: ${Array.from(targetPkgRoots).join(', ')}`)
}
// 最终统计
console.log(`[${PLUGIN_NAME}] 处理完成:移动=${movedCount} | 跳过(主包引用)=${skippedMainRef} | 跳过(单分包)=${skippedSinglePkg}`)
},
)
})
}
}
module.exports = SplitUtilPlugin
四、vue.config.js 配置教程
- 将插件文件放在项目根目录 或
src目录下 - 打开项目根目录的
vue.config.js,添加配置:
javascript
const SplitUtilPlugin = require('./split-util-plugin')
module.exports = {
configureWebpack: {
plugins: [
// 注入分包优化插件
new SplitUtilPlugin()
]
}
}