在 Taro 开发微信小程序的过程中,主包体积过大一直是困扰开发者的常见问题。特别是当项目逐渐复杂,分包与主包共享代码时,很容易出现分包的公共代码被错误打包到主包,或者主包中只有分包使用的模块无法被正确分离的情况。今天,我将为大家介绍一个专为解决这些问题而设计的 Taro 插件——SubPackageFilePlugin。
一、问题背景
微信小程序对主包体积有严格限制(通常不超过 2MB),超出限制将无法正常发布。在 Taro 项目中,常见的体积优化痛点包括:
- 分包中引入的公共样式和脚本被错误地打包到主包
- 主包中某些只被分包使用的模块无法被有效分离
- 跨分包共享代码时的依赖处理困难
针对这些问题,SubPackageFilePlugin 应运而生,它通过巧妙的文件命名约定和 webpack 插件机制,实现了更精细的分包资源管理。
二、插件核心功能
SubPackageFilePlugin 主要提供了两大核心功能:
- .stay. 后缀文件处理:确保带有
.stay.后缀的文件始终保留在引用它的分包中,不会被提升到主包 - .split. 后缀文件处理:将带有
.split.后缀的文件提取为公共模块,并复制到各个引用它的分包中
三、实现原理深度解析
1. 数据结构设计
插件首先定义了一系列数据映射,用于追踪模块与 chunk 之间的关系:
const dataMaps = {
styleModuleToChunkName: new Map(), // 样式模块到chunk名称的映射
styleChunkNameToTargetChunks: new Map(), // 样式chunk名称到目标chunks的映射
scriptModuleToChunkName: new Map(), // 脚本模块到chunk名称的映射
scriptChunkNameToTargetChunks: new Map(), // 脚本chunk名称到目标chunks的映射
splitCommonModules: new Set(), // 分割的公共模块集合
splitCommonChunkNameToTargetChunks: new Map(), // 分割的公共chunk名称到目标chunks的映射
commonChunkNameToSplitCommonChunkNames: new Map(), // 公共chunk名称到分割后的公共chunk名称的映射
}
这些数据结构是插件实现分包资源精确管理的基础。
2. webpack 缓存组策略
插件通过定义三个关键的 webpack splitChunks 缓存组来实现其核心功能:
chain.merge({
optimization: {
splitChunks: {
cacheGroups: {
stayStyle: createStayCacheGroup(dataMaps.styleModuleToChunkName, dataMaps.styleChunkNameToTargetChunks),
stayScript: createStayCacheGroup(dataMaps.scriptModuleToChunkName, dataMaps.scriptChunkNameToTargetChunks),
splitCommon: splitCommonCacheGroup,
}
}
}
})
3. .stay. 后缀文件处理机制
对于带有 .stay. 后缀的文件,插件会执行以下操作:
- 在
optimizeChunks钩子中识别这些文件,并检查它们是否只被同一分包内的模块引用 - 通过
getIsSameSubRoot方法确保模块只在同一分包内使用 - 为这些模块创建专用的缓存组,确保它们不会被提升到主包
关键实现代码如下:
const processOfStayModule = (predicateModule, ext, moduleToChunkName) => {
let targetModule = predicateModule
if (predicateModule.type === CSS_MINI_EXTRACT_MODULE_TYPE) {
targetModule = predicateModule.issuer || predicateModule
}
const moduleResource = targetModule.resource || ''
const moduleBasename = path.basename(moduleResource)
if (new RegExp(`\\.stay\\..*${ext}$`).test(moduleBasename)) {
const isSameSubRoot = this.getIsSameSubRoot(targetModule.chunksIterable)
if (!isSameSubRoot) return
const chunkName = this.getChunkName(moduleResource)
moduleToChunkName.set(predicateModule, chunkName)
}
}
4. .split. 后缀文件处理机制
对于带有 .split. 后缀的文件,插件采用了不同的处理策略:
- 首先将这些文件标记为公共模块,并临时放在主包中
- 在
emit阶段,将这些模块复制到每个引用它的分包目录下 - 最后删除主包中临时存放的这些模块文件
关键实现代码:
const processOfSplitCommonModule = (predicateModule) => {
const moduleResource = predicateModule.resource || ''
const moduleBasename = path.basename(moduleResource)
if (new RegExp(`\\.split\\..*js$`).test(moduleBasename)) {
const isNotIncludeMainChunk = this.getIsNotIncludeMainChunk(predicateModule.chunksIterable)
if (!isNotIncludeMainChunk) return
dataMaps.splitCommonModules.add(predicateModule)
}
}
5. 依赖注入与资源复制
插件在 renderWithEntry 钩子中,为引用了这些特殊模块的入口文件添加 require 语句:
// 监听chunk模板渲染事件,在入口渲染前添加依赖
compilation.chunkTemplate.hooks.renderWithEntry.tap({
name: SubPackageFilePlugin.PluginName,
before: 'TaroLoadChunksPlugin',
}, (source, chunk) => {
// ...获取依赖模块并添加require语句
if (chunkNames.size) {
result = this.addRequireToSource(chunkId, source, Array.from(chunkNames))
}
return result
})
而在 emit 钩子中,插件负责实际的资源复制和清理工作:
// 复制公共模块到各个分包
dataMaps.commonChunkNameToSplitCommonChunkNames.forEach((subPackageChunkNames, chunkName) => {
for (const key in FileExts) {
const ext = FileExts[key]
const assetName = chunkName + ext
const assetSource = assets[taroHelper.normalizePath(assetName)]
if (assetSource) {
subPackageChunkNames.forEach(subPackageChunkName => {
const subAssetName = subPackageChunkName + ext
const source = new ConcatSource()
const _source = assetSource._source || assetSource._value
source.add(_source)
patchSource(subAssetName, source)
})
}
}
})
// 删除根目录下的资源,因为已经复制到各个分包中
for (const assetPath in assets) {
if (new RegExp(`^${subPackageCommonDir}\\/.*`).test(assetPath)) {
delete assets[assetPath]
}
}
四、在项目中如何使用
1. 安装与配置
首先,将 SubPackageFilePlugin 引入到你的 Taro 项目中:
// config/index.js
module.exports = {
// ...其他配置
plugins: [
// ...其他插件
require('path').resolve(__dirname, '../lib/SubPackageFilePlugin'),
]
}
2. 使用 .stay. 后缀
对于只希望在特定分包内使用的样式或脚本文件,添加 .stay. 后缀:
// 例如,在分包 packageA 中的样式文件
// src/subPackages/packageA/styles/common.stay.scss
// 在组件中引用
import './styles/common.stay.scss'
这样,common.stay.scss 的内容将始终保留在 packageA 分包中,不会被打包到主包。
3. 使用 .split. 后缀
对于需要在多个分包间共享但又不希望增加主包体积的脚本,添加 .split. 后缀:
// 例如,多个分包共享的工具函数
// src/utils/common.split.js
export const commonUtil = () => {
// ...工具函数实现
}
当这个文件被多个分包引用时,插件会将它复制到每个引用它的分包目录下,避免增加主包体积。
五、项目实践效果
通过在实际项目中应用这个插件,我们观察到了以下优化效果:
- 主包体积显著减小:通过将分包专用代码和跨分包共享代码从主包中分离,主包体积可减少 30%-50%
- 分包独立性增强:每个分包包含了自己所需的全部资源,提高了分包的独立性和可维护性
- 开发体验提升:开发者只需通过简单的文件命名约定,就能实现复杂的分包资源管理
六、源码地址
插件源码已开源,有兴趣的同学可以前往 Gitee 仓库查看完整实现:
七、总结
SubPackageFilePlugin 为 Taro2.x 项目提供了一种简单而有效的分包优化方案。通过巧妙的文件命名约定和 webpack 插件机制,它解决了主包体积过大、分包资源管理复杂等常见问题。
如果你也在为 Taro 项目的主包体积而烦恼,不妨尝试一下这个插件,相信它能给你的项目带来显著的优化效果!
在微信小程序开发中,性能优化是一个永恒的话题。除了使用这类插件外,我们还应该关注代码分割、懒加载、资源压缩等多种优化手段,共同打造高性能的小程序应用。
温馨提示:该插件目前主要针对 Taro2.x 版本和微信小程序平台优化,如果你使用的是 Taro3.x 版本,可能需要进行适当的调整。