为什么要写这个文章
想必使用Taro开发过偏大型小程序应用的同学经常会遇到这么个问题:小程序的主包体积超过了2M了,没办法预览、发布。
微信小程序官网给出的包限制:
目前小程序分包大小有以下限制:
- 整个小程序所有分包大小不超过 20M
- 单个分包/主包大小不能超过 2M
为什么使用Taro开发那么容易主包就超了,是框架不行还是我使用方式错了?呃呃呃...
相信使用过Taro开发的同学都知道,我们写的代码最终都会经过webpack里的插件SplitChunksPlugin去做chunks的拆分,Taro对这个插件的完整的默认配置可以看这里
...
common: {
name: config.isBuildPlugin ? 'plugin/common' : 'common',
minChunks: 2,
priority: 1
}
...
只要被两个chunk引用的文件,就被打包到主包的common,而我们的分包的每个页面打包完后都是一个独立的chunk,那就是只要分包里有两个页面引用了同一个文件,这个文件就会被打包到common.js,这明显不是我们想要的结果。
了解了前因后果,我们自然要解决它了,这也是我写这篇文章的原因,希望给有这个问题的同学们一些自己的经验和建议。
接下去我主要介绍下一些比较有效的减小主包体积的几种方式,会涉及到Taro2x以及3x,像一些常规的方法:如减少本地图片的使用,按需引入组件,删除无用代码等等这些这里不介绍了。
减小主包体积方法:
一、使用框架本身提供的optimizeMainPackage
module.exports = {
// ...
mini: {
// ...
optimizeMainPackage: {
enable: true
}
}
}
这个特性3.2.9版本开始支持,但这个版本在window下会报错,在3.2.12才修复。但是目前还有几个问题,
-
在开发环境下会报错某个文件找不到,具体原因可以自行去看这个issue,目前好像还未修复。解决方法:
-
issue里有comment提出将暂存的sub-common移到项目外,避免被ide监测,这个需要去改node_modules下的
@tarojs/mini-runner/dist/plugins/MiniSplitChunksPlugin.js
文件。 -
只在生产环境下才开启这个功能,在config/prod.js下加上面的配置,对于比较懒得同学可以先这样,相信官方会很快修复这个bug。
-
-
报
Cannot read property 'getCacheGroup' of null
,可以在上面那个issue找到解决方法。
二、模仿optimizeMainPackage
有人的taro版本3.2.9版本以下,暂时无法升级到3.2.9版本,怎么办?其实方法一说到底就是一个webpack插件,那我们找一个支持optimizeMainPackage的taro项目,复制node_modules/@tarojs/mini-runner/dist/plugins/MiniSplitChunksPlugin.js
,放到你自己的项目下作为本地插件,通过webpackChain导入插件不就行了吗。
这种方式在taro2x和taro3x配置略有不同,下面我分别介绍:
taro3x
taro3x很简单,做如下配置就可以了。
// config/index.js
const MiniSplitChunksPlugin = require('你copy下来的代码的文件地址');
...
webpackChain: function (chain) {
chain.plugin('optimizeMainPackage')
// 这里加before,是因为该插件必须在miniPlugin前面执行
.use(MiniSplitChunksPlugin, [{ exclude: true }]).before('miniPlugin');
}
...
taro2x
taro2x就比较麻烦了,说下思路:
- 要修改
MiniSplitChunksPlugin.js
文件,因为他使用的很多tarojs/helper
的很多辅助方法在taro2是没有,所以我们需要去补充上。 - 需要新增一个
app.config.js
文件,插件会去读取这个文件的主包/分包的配置,因为taro3已经改成使用了app.config.js作为路由的配置文件了。
这个方式整体改动较大,而且每新加一个页面还要配置app.js
和app.config.js
两个地方,比较麻烦。由于配置下来的代码比较多,我就不在这里贴了,有需要的可把项目clone下来自己看。
感兴趣、有能力的同学也可以自己去改插件的代码,改成直接去app.tsx去读取配置,就不需要用到app.config.js
。
三、修改splitChunks
说实话有了上面两种方法,基本可以满足各种情况了。有人会觉得对于taro2.x使用上诉方法过于麻烦,这里也提供了另一种方法供参考。这是官方提供的能力addChunkPages,要先通过webpack
配置optimization.splitChunks 来单独抽离分包的公共文件,然后通过 mini.addChunkPages
为分包页面配置引入分包的公共文件。
举个列子,有如下目录结构:
└── src
├── page # 主包
│ ├── index
│ └── index.tsx
└── subpackage # 分包
├── page1
├── index.tsx # 引用了utils.ts
├── page2
├── index.tsx # 引用了utils.ts
├── utils.ts
我们希望utils
只被打包到subpackage
,我们可以这样做:
addChunkPages(pages, pagesNames) {
pagesNames.forEach(pagename => {
if (/subpackage\//.test(pagename)) {
pages.set(pagename, ['subpackage/subpackage-common']);
}
});
},
webpackChain: function (chain) {
chain.merge({
optimization: {
splitChunks: {
cacheGroups: {
subpackage: {
name: 'subpackage/subpackage-common',
minChunks: 2,
test: (module, chunks) => {
return /\/subpackage\//.test(module.resource)
},
priority: 200
}
}
}
}
})
}
打包完成后,utils.ts
就被打包到subpackage/subpackage-common.js
文件下了。
四、混合开发
相信有很多人,在接触并使用taro的时候,已经有一个使用原生代码开发的一个小程序了,而且短时间并不能废弃,所以就出现了使用taro开发,打包完成后复制代码到主包中,作为分包。
我们可以参考这种方式,分包是打包完成后复制到主包的,所以也就不会出现分包的模块被打包到主包的common.js中了,也算是一个减小主包体积的方式了。
taro2x
-
创建一个空的小程序环境(使用taro打包出来的小程序不行,具体原因还不清楚),作为主包使用。你的主包可能提供一些公共变量、方法供各个分包使用,可以在小程序的App实例上挂载。
// app.js import sdk from "./sdk" App({ ... SDK: sdk, ... }) // 在分包中引用 const app = Taro.getApp(); app.SDK.request();
-
使用taro创建你的分包,由于直接作为分包,导致该taro打包后的app.js文件不会被执行,我们只能手动将其一些依赖引入到每个page,这里不能直接去引入
app.js
文件的原因是会导致分包的app对象会覆盖主包的app对象。// config/index.js mini: { ... outputRoot: '你在主包要放分包的地址', // 对每个分包的页面都导入app.js addChunkPages(pages, pagesNames) { pagesNames.forEach(page => { // 根据自己实际情况配,可能会没有common、vendors,可以直接复制打包后app.js头部的require pages.set(page, ['runtime', 'common', 'vendors']) }); }, ... }
有人可能会觉得这样还得自己去判断是否有这个文件比较麻烦,我这边也提供了一个webpack插件帮你自动添加(借鉴了taro的
plugin-indie
插件的代码),// config/index.js ... webpackChain(chain, webpack) { chain.plugin('AutoRequirePlugin').use(new AutoRequirePlugin()) }, ...
// webpack插件 const { ConcatSource } = require('webpack-sources') const path = require('path'); class AutoRequirePlugin { constructor(event) { this.needRequireChunkNames = ['runtime', 'common', 'vendors', 'taro']; } addRequireToSource(id, modules, needRequireChunks) { const source = new ConcatSource() needRequireChunks.forEach(chunk => { source.add(`require(${JSON.stringify(this.promoteRelativePath(path.relative(id, chunk.name)))});\n`) }) source.add('\n') source.add(modules) source.add(';') return source } promoteRelativePath(fPath) { const fPathArr = fPath.split(path.sep); let dotCount = 0; fPathArr.forEach(item => { if (item.indexOf('..') >= 0) { dotCount++; } }); if (dotCount === 1) { fPathArr.splice(0, 1, '.'); return fPathArr.join('/'); } if (dotCount > 1) { fPathArr.splice(0, 1); return fPathArr.join('/'); } return normalizePath(fPath); } getIdOrName(chunk) { if (typeof chunk.id === 'string') { return chunk.id } return chunk.name } apply(compiler) { compiler.hooks.make.tap('AutoRequirePlugin', (compilation) => { let needRequireChunks = []; compilation.hooks.afterOptimizeChunks.tap('afterOptimizeChunks', (chunks) => { needRequireChunks = chunks.filter(chunk => this.needRequireChunkNames.includes(chunk.name)).reverse(); }) compilation.chunkTemplate.hooks.renderWithEntry.tap('renderWithEntry', (modules, chunk) => { if (!chunk.entryModule) return const entryModule = chunk.entryModule.rootModule ? chunk.entryModule.rootModule : chunk.entryModule const { miniType } = entryModule const id = this.getIdOrName(chunk) if (miniType === 'PAGE' || miniType === 'STATIC') { return this.addRequireToSource(id, modules, needRequireChunks) } }) }) } } module.exports = AutoRequirePlugin;
taro3x
taro官网说是在taro3.0.25+提供了这种混合开发的能力,具体可以自行看文档,讲的很详细。
总结
以上就是我能总结出来的使用taro减小主包体积的几种方式,可以根据自己的业务场景选择一种适合你的。
由于个人能力和时间有限,并不是每个方法都研究透了,而且也不敢保证,经过我的魔改的代码后不会出问题。此篇主要目的是提供个人对taro减小包体积的一个思路。如果各位大佬有更好的方法或发现什么错误,欢迎指出。