一、背景
古茗会员体系升级需要在小程序首页接入炫酷的动画,通过技术选型最终敲定了阿里出品的@galacean/effects
,编译后体积599.3KB
,小程序主包限制最大也就只有2MB
,这完全塞不下啊喂。这就不得不吐槽了,都2025
年了,大家都在用5G
网络了,微信小程序主包体积限制还是2MB
。
目前我们在古茗点单小程序中已经使用了以下优化方案:
- 页面分包:主包仅保留默认「启动页/TabBar页」,其他页面模块均迁移至分包
- 图片资源优化:上传本地资源,并通过Babel插件实现本地资源替换为网络地址。
- 公共模块分包:Taro项目配置mini.optimizeMainPackage,将主包未使用的公共模块打包至对应分包。
能优化的都优化了,完蛋没法发版了。又没有钞能力让微信多给我们几MB
,只能再想想其他办法了。。。
二、目标
- 能够使用
@galacean/effects
,在小程序首页展示炫酷的动画,且主包体积不能超2MB
- 沉淀通用的解决方案,优化其他三方依赖或业务组件在主包中的体积占用。
三、方案实现
主包塞不下,那就塞分包里,「分包异步化」闪亮登场!!!
核心思路:
- 将编译后体积占用较大的
JS
代码拆分到分包中 - 通过微信分包异步化的能力,跨分包引用
JS
代码
1. 实现V1:babel
收集异步化模块编译输出到指定分包
实现思路
- 定义一个标记函数
asyncRequire
babel
识别asyncRequire
收集异步化模块,并替换为跨分包引用JS
实现webpack
插件实现esbuild
编译收集的异步化模块,并输出到指定分包- 通过
taro
插件引入实现的babel
和webpack
插件,并修改微信小程序配置文件,注册对应分包
代码实现
- 定义全局函数
asyncRequire
标记异步化模块。
tsx
/**
* 异步加载三方依赖
*/
declare const asyncRequire: <T = any>(packagePath: string) => Promise<T>;
- 定义重试函数
promiseRetry
tsx
export const promiseRetry = async (fn: () => Promise<any>, retries = 3, delay = 1000) => {
try {
return await fn(); // 尝试执行传入的异步操作
} catch (error) {
if (retries <= 0) return Promise.reject(error); // 如果用尽重试次数,抛出错误
await new Promise((resolve) => setTimeout(resolve, delay)); // 等待指定的延迟时间
return promiseRetry(fn, retries - 1, delay); // 递归调用自己
}
};
babel
收集异步化模块路径,并替换为跨分包引用JS
实现
tsx
// 异步化模块路径
const asyncPackagePaths = new Set();
// 基于异步化模块文件路径生成模块编译后的分包名
const generateAsyncPackageName = (packagePath) => packagePath.replace(/\//g, '-');
// Babel 收集当前文件已引入的依赖
const getImportElements = (programPath) => {
const importElement = new Map();
programPath.traverse({
ImportDeclaration(importDeclarationPath) {
const { source, specifiers } = importDeclarationPath.node;
if (!importElement.has(source.value)) importElement.set(source.value, new Map());
const tempObj = importElement.get(source.value);
specifiers.forEach((specifier) => {
const { type } = specifier;
if (!tempObj.has(type)) tempObj.set(type, new Set());
tempObj.get(type).add(specifier?.local?.name);
});
},
});
return importElement;
};
// 插入依赖(用来引入 promiseRetry 函数的)
const injectImport = (programPath) => {
const importElements = getImportElements(programPath);
return (templateCode) => {
const templateAst = template.statement(templateCode)();
const { source, specifiers } = templateAst;
const needInjectSpecifiers = (() => {
if (!importElements.has(source.value)) return specifiers;
return specifiers.filter((specifier) => {
const { type } = specifier;
return !importElements.get(source.value).get(type)?.has(specifier?.local?.name);
});
})();
if (!needInjectSpecifiers.length) return;
programPath.node.body.unshift({ ...templateAst, specifiers: needInjectSpecifiers });
};
};
// 跨分包引用 js 代码实现
const requireAsyncPackageTemplateCode = `promiseRetry(() => __non_webpack_require__.async(ASYNC_REQUIRE_PATH), 3, 200)`;
// 兜底其他渠道实现(如:支付宝小程序、抖音小程序)
const defaultRequireTemplateCode = `new Promise((resolve) => resolve(require(PACKAGE_PATH)))`;
const generateDefaultRequireTemplateAst = template.expression(defaultRequireTemplateCode);
const generateRequireAsyncPackageTemplateAst = template.expression(requireAsyncPackageTemplateCode);
const requireAsyncPackageBabelTransformPlugin = (babel, options) => {
const { types } = babel;
const { asyncPackageDirPath } = options;
return {
visitor: {
CallExpression(callExpressionPath) {
// 检查是否是 asyncRequire 调用
if (types.isIdentifier(callExpressionPath.node.callee, { name: 'asyncRequire' })) {
const [packagePathNode] = callExpressionPath.node.arguments;
const isWeapp = /weapp/.test(process.env.TARO_ENV);
if (types.isStringLiteral(packagePathNode) && !isWeapp) {
const opt = { PACKAGE_PATH: packagePathNode };
// 使用模板创建新的 Promise 结构
const transformedExpression = generateDefaultRequireTemplateAst(opt);
callExpressionPath.replaceWith(transformedExpression);
}
// 确保 pkgName 是一个静态字符串
if (types.isStringLiteral(packagePathNode) && isWeapp) {
const packagePath = packagePathNode.value;
const packageName = generateAsyncPackageName(packagePath);
const asyncRequirePath = `${asyncPackageDirPath}/${packageName}/index.js`;
// 使用模板创建新的 Promise 结构
const opt = { ASYNC_REQUIRE_PATH: types.stringLiteral(asyncRequirePath) };
const transformedExpression = generateRequireAsyncPackageTemplateAst(opt);
asyncPackagePaths.add(packagePath);
// 替换原来的 asyncRequire 调用
callExpressionPath.replaceWith(transformedExpression);
// 找到 Program 节点
const programPath = callExpressionPath.findParent((p) => types.isProgram(p));
const inject = injectImport(programPath);
inject(`import {promiseRetry} from '@/utils'`);
}
}
},
},
};
};
- 实现
webpack
插件通过esbuild
构建异步模块输出至指定目录
tsx
// 异步化模块路径
const asyncPackagePaths = new Set();
// 基于异步化模块文件路径生成模块编译后的分包名
const generateAsyncPackageName = (packagePath) => packagePath.replace(/\//g, '-');
class AsyncPackageBuild {
constructor(options) {
this.options = options || {};
}
apply(compiler) {
compiler.hooks.afterEmit.tap('AnalyzeModulesPlugin', () => {
this.buildAsyncPackage();
});
}
buildAsyncPackage() {
const { outputPath, cacheFileDir } = this.options;
const buildPackage = async (packagePath) => {
const contents = `export * from '${packagePath}';`;
const outfile = `${outputPath}/${generateAsyncPackageName(packagePath)}/index.js`;
const buildOpt = this.generateBuildOption({ contents, outfile });
await build(buildOpt);
};
const finalAsyncPackagePaths = (() => {
if (!cacheFileDir) return Array.from(asyncPackagePaths);
const cacheFileDirPath = path.join(process.cwd(), cacheFileDir);
const cacheFilePath = path.join(cacheFileDirPath, 'async-package-cache.json');
if (!fs.existsSync(cacheFileDirPath)) fs.mkdirSync(cacheFileDirPath, { recursive: true });
if (!fs.existsSync(cacheFilePath)) fs.writeFileSync(cacheFilePath, '[]', 'utf8');
const cachePaths = JSON.parse(fs.readFileSync(cacheFilePath, 'utf8') || '[]');
const tempPaths = Array.from(new Set([...Array.from(asyncPackagePaths), ...cachePaths]));
fs.writeFileSync(cacheFilePath, JSON.stringify(tempPaths), 'utf8');
return tempPaths;
})();
finalAsyncPackagePaths.reduce((result, packagePath) => {
return result.then(() => buildPackage(packagePath));
}, Promise.resolve());
}
generateBuildOption(options) {
const { outfile, contents } = options;
const __isDev__ = process.env.NODE_ENV === 'development';
return {
stdin: {
contents, // 直接传递文件内容
resolveDir: __dirname, // 设置解析目录,用于解析相对路径的导入
sourcefile: 'index.ts', // 虚拟文件名,方便调试
loader: 'ts', // 指定内容的类型,TypeScript 需要指定为 'ts'
},
bundle: true,
outfile,
format: 'cjs',
target: ['es6'], // 编译到 ES5
drop: !__isDev__ ? ['console', 'debugger'] : [],
minify: true,
};
}
}
- 定义
Taro
插件- 使用
AsyncPackageBuild
与requireAsyncPackageBabelTransformPlugin
- 修改微信小程序配置文件,注册对应分包。
- 使用
tsx
module.exports = (ctx, options) => {
const { outputDir = 'async-packages', cacheFileDir } = options;
const outputPath = `${ctx.paths.outputPath}/${outputDir}`;
const asyncPackageDirPath = `~/${outputDir}`;
const isWeapp = /weapp/.test(process.env.TARO_ENV);
ctx.modifyWebpackChain(({ chain }) => {
// eslint-disable-next-line prettier/prettier
if (isWeapp) chain.plugin('AsyncPackageBuild').use(AsyncPackageBuild, [{ outputPath, cacheFileDir }]);
chain.module
.rule('script')
.use('babelLoader')
.tap((opts) => {
const pluginConfig = [requireAsyncPackageBabelTransformPlugin, { asyncPackageDirPath }];
return { ...opts, plugins: [pluginConfig, ...(opts.plugins || [])] };
})
.end();
});
ctx.modifyBuildAssets(({ assets }) => {
const appJSON = assets['app.json'];
if (!appJSON || !isWeapp) return;
// 读取微信小程序配置文件内容
const appJSONContent = JSON.parse(appJSON.source());
// 生成异步分包包名
const asyncPackageNames = Array.from(asyncPackagePaths).map((asyncPackagePath) => {
return `${outputDir}/${generateAsyncPackageName(asyncPackagePath)}`;
});
// 生成异步分包配置
const asyncPackagesConfig = asyncPackageNames.map((asyncPackageName) => {
return { root: asyncPackageName, pages: [] };
});
const { resolveAlias: curResolveAlias = {}, subpackages: curSubpackages = [] } = appJSONContent;
// 设置小程序路径别名
appJSONContent.resolveAlias = { ...curResolveAlias, '~/*': '/*' };
// 注册异步加载分包
appJSONContent.subpackages = [...curSubpackages, ...asyncPackagesConfig];
// 覆写微信小程序配置文件
assets['app.json'] = new webpackSources.RawSource(JSON.stringify(appJSONContent, null, 2));
});
};
业务中使用
这里使用的是由@galacean/effects/weapp
构建的产物mp-weapp-galacean-effects
tsx
import Taro from '@tarojs/taro';
import React, { useEffect, useRef } from 'react';
const id = 'webglCanvas'
const systemInfo = Taro.getSystemInfoSync();
export const Animation:React.FC = async () => {
const animationRef = useRef();
const initAnimation = async () => {
// 这里引用了npm库编译后的js文件,源自 https://www.galacean.com/effects/user/pq9u1sqdbtugp997#E9gHP
animationRef.current = await asyncRequire('@/assets/libs/mp-weapp-galacean-effects');
// ....
}
useEffect(() => {
initAnimation();
}, []);
return (
<Canvas
type="webgl"
id={id}
width={`${systemInfo.screenWidth}`}
height={`${systemInfo.screenHeight}`}
/>
)
};
2. 实现V2:SplitChunk
拆分异步模块到指定分包
实现V1完成了目标「能够使用@galacean/effects
,在小程序首页展示炫酷的动画,且主包体积不能超2MB
」
但目标「沉淀通用的解决方案,优化其他三方依赖或业务组件在主包中的体积占用」还存在以下问题
- 针对组件打包如何处理样式和图片资源文件?
- 针对项目中配置的
babel
插件如何保持一致? - 针对项目中配置的路径别名如何保持一致?
能不能将项目中的相关配置在异步模块的esbuild
编译中再配置配置一遍?当然可以,但还会产生其他问题比如:
- 要实现与
webpack
一致的能力需要额外引入相关的plugin
,存在较大的负担 - 不熟悉的同学增加编译相关的配置容易遗漏
esbuild
配置
怎么解决这些问题?
实现思路
- 使用
import('modulePath')
动态引入异步模块 - 使用
splitChunk
拆分异步模块至指定分包 - 修改
webpack runtime
实现跨分包引用JS
(webpack引入异步模块默认通过创建Script
实现) - 通过
taro
修改微信小程序配置文件,注册对应分包
代码实现
- 修改
babel
配置taro
默认开启dynamic-import-node
会将异步的import('path')
转换为同步的require('path')
- 所以需要前置关闭,当然多渠道的话也可以做渠道区分。
tsx
// babel-preset-taro 更多选项和默认值:
// https://github.com/NervJS/taro/blob/next/packages/babel-preset-taro/README.md
module.exports = {
presets: [
['taro', {
framework: 'react',
ts: true,
loose: false,
useBuiltIns: false,
"dynamic-import-node": process.env.TARO_ENV !== 'weapp', // 在原有基础上添加这个配置即可
targets: {
ios: '9',
android: '5',
},
}]
],
}
- 修改
splitChunk
配置- 将异步模块
JS
文件拆分到指定分包。 - 由于
CSS
无法跨分包引用,故聚合异步模块CSS
文件输出至根目录由主包引用。
- 将异步模块
tsx
import type { IPluginContext } from '@tarojs/service';
interface DynamicPackOpts {
dynamicModuleJsDir: string;
dynamicModuleStyleFile: string;
}
const dynamicPackOptsDefaultOpt: DynamicPackOpts = {
dynamicModuleJsDir: 'dynamic-common',
dynamicModuleStyleFile: 'dynamic-common',
};
export default (ctx: IPluginContext, pluginOpts: DynamicPackOpts) => {
const finalOpts = { ...dynamicPackOptsDefaultOpt, ...pluginOpts };
if (process.env.TARO_ENV !== 'weapp') return;
ctx.modifyWebpackChain(({ chain }) => {
chain.optimization.merge({
splitChunks: {
cacheGroups: {
common: {
name: 'common',
minChunks: 2,
priority: 1,
enforce: true
},
vendors: {
name: 'vendors',
minChunks: 2,
test: (module: any) => /[\\/]node_modules[\\/]/.test(module.resource),
priority: 10,
enforce: true
},
[`${finalOpts.dynamicModuleJsDir}-js`]: {
name: (module: any) => `${finalOpts.dynamicModuleJsDir}/${module.buildInfo.hash}`,
chunks: 'async',
test: /\.(js|jsx|ts|tsx)$/,
enforce: true
},
[`${finalOpts.dynamicModuleJsDir}-js-common`]: {
name: `${finalOpts.dynamicModuleJsDir}/common`,
chunks: 'async',
minChunks: 2,
priority: 2,
test: /\.(js|jsx|ts|tsx)$/,
enforce: true
},
[`${finalOpts.dynamicModuleStyleFile}-css`]: {
name: finalOpts.dynamicModuleStyleFile,
chunks: 'async',
test: /\.(css|less|scss|sass)$/,
enforce: true
},
},
},
});
});
};
- 修改
webpack``runtime
实现跨分包引用JS
实现- 实现
webpack
插件,在编译产物压缩输出前进行代码转换。
- 实现
tsx
import webpack from 'webpack';
type CompilationAssets = Record<string, Source>;
interface Opt {
test?: RegExp;
transform: (code: string, assets: CompilationAssets) => string;
}
export const PLUGIN_NAME = 'TransformBeforeCompression';
export class TransformBeforeCompression {
private readonly opt: Opt;
constructor(opt: Opt) {
this.opt = opt;
}
apply(compiler: webpack.Compiler) {
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation: webpack.Compilation) => {
const stage = webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE; // 压缩前
compilation.hooks.processAssets.tap({ name: PLUGIN_NAME, stage }, (assets) => {
const { test, transform } = this.opt;
const assetNames = Object.keys(assets);
assetNames.forEach((assetName) => {
if (!test || !test.test(assetName)) return;
const originalSource = assets[assetName].source();
const transformResult = transform(originalSource as string, assets as CompilationAssets);
compilation.updateAsset(assetName, new webpack.sources.RawSource(transformResult));
});
});
});
}
}
- 通过
babel
实现代码转换逻辑
tsx
import { NodePath, template } from '@babel/core';
import generator from '@babel/generator';
import * as parser from '@babel/parser';
import traverse from '@babel/traverse';
import * as types from '@babel/types';
import type { AssignmentExpression, VariableDeclarator } from '@babel/types';
import type { CompilationAssets, DynamicPackOpts } from './types';
interface Opts extends DynamicPackOpts {
assets: CompilationAssets;
}
const webpackLoadDynamicModuleTemplateDep = `
var loadedDynamicModules = {};
var loadDynamicModule = function (dynamicModulePath) {
var loadDynamicModuleFn = loadDynamicModuleFnMap[dynamicModulePath];
return loadDynamicModuleFn ? loadDynamicModuleFn() : Promise.reject();
};
var promiseRetry = function (apply,retries = 6,delay = 500) {
return apply().catch(function (error) {
if (retries <= 0) return Promise.reject(error);
return new Promise(function (resolve) {
setTimeout(resolve, delay);
})
.then(function () {
return promiseRetry(apply, retries - 1, delay)
});
})
}
`;
const webpackLoadDynamicModuleTemplate = `
__webpack_require__.l = function (dynamicModulePath, done, key, chunkId) {
if (inProgress[dynamicModulePath]) {
inProgress[dynamicModulePath].push(done);
return;
}
const target = { src: dynamicModulePath };
if (loadedDynamicModules[dynamicModulePath]) return done({ type: 'loaded', target });
promiseRetry(function () {
return loadDynamicModule(dynamicModulePath)
})
.then(function () {
return done({ type: 'loaded', target })
}).catch(function () {
return done({ type:'error', target })
});
};
`;
// eslint-disable-next-line prettier/prettier
const replaceWebpackLoadScriptFn = (
assignmentExpressionNodePath: NodePath<AssignmentExpression>,
opts: Opts
) => {
const { left, right } = assignmentExpressionNodePath.node || {};
if (!types.isMemberExpression(left)) return;
if (!types.isFunctionExpression(right)) return;
if (!types.isIdentifier(left.object, { name: '__webpack_require__' })) return;
if (!types.isIdentifier(left.property, { name: 'l' })) return;
const isProcessed = right.params.some((item) => {
return types.isIdentifier(item, { name: 'dynamicModulePath' });
});
if (isProcessed) return;
const { assets, dynamicModuleJsDir } = opts;
const dynamicAssets = Object.keys(assets).filter((assetName) => {
return new RegExp(`^${dynamicModuleJsDir}/.`).test(assetName);
});
const loadDynamicModuleFnMapCode = (() => {
const tempCode = dynamicAssets.map((dynamicAsset) => {
return `'/${dynamicAsset}':function (){ return require.async('${dynamicAsset}'); }`;
});
return `var loadDynamicModuleFnMap = {${tempCode.join(',')}}`;
})();
const templateCodeAst = template.ast(webpackLoadDynamicModuleTemplate);
const templateCodeDepAst = template.ast(webpackLoadDynamicModuleTemplateDep);
const loadDynamicModuleFnMapAst = template.ast(loadDynamicModuleFnMapCode);
assignmentExpressionNodePath.replaceWith(templateCodeAst as any);
assignmentExpressionNodePath.insertBefore(templateCodeDepAst);
assignmentExpressionNodePath.insertBefore(loadDynamicModuleFnMapAst);
};
const webpackLoadDynamicModuleStylesheetTemplate = `
loadStylesheet = function () {
return Promise.resolve()
}
`;
const replaceLoadStylesheetFn = (nodePath: NodePath<VariableDeclarator>) => {
const { id, init } = nodePath.node || {};
if (!types.isIdentifier(id, { name: 'loadStylesheet' })) return;
if (!types.isFunctionExpression(init)) return;
const isProcessed = !init.params.length;
if (isProcessed) return;
const templateCodeAst = template.expression(webpackLoadDynamicModuleStylesheetTemplate)();
nodePath.replaceWith(templateCodeAst);
};
const removeCreateStylesheetFn = (nodePath: NodePath<VariableDeclarator>) => {
const { id } = nodePath.node || {};
if (!types.isIdentifier(id, { name: 'createStylesheet' })) return;
nodePath.remove();
};
const removeFindStylesheetFn = (nodePath: NodePath<VariableDeclarator>) => {
const { id } = nodePath.node || {};
if (!types.isIdentifier(id, { name: 'findStylesheet' })) return;
nodePath.remove();
};
export const replaceWebpackRuntime = (code: string, opts: Opts) => {
const ast = parser.parse(code); // 将代码解析为 AST
traverse(ast, {
AssignmentExpression: (nodePath: NodePath<AssignmentExpression>) => {
replaceWebpackLoadScriptFn(nodePath, opts);
},
VariableDeclarator(nodePath: NodePath<VariableDeclarator>) {
replaceLoadStylesheetFn(nodePath);
removeCreateStylesheetFn(nodePath);
removeFindStylesheetFn(nodePath);
},
});
return generator(ast).code;
};
- 插入异步模块样式文件至主包样式文件中
tsx
import webpack from 'webpack';
interface Opt {
test?: RegExp;
styleSheetPath?: string;
}
export const PLUGIN_NAME = 'RequireStylesheet';
export class RequireStylesheet {
private readonly opt: Opt;
constructor(opt: Opt) {
this.opt = opt;
}
apply(compiler: webpack.Compiler) {
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation: webpack.Compilation) => {
const stage = webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE; // 压缩前
compilation.hooks.processAssets.tap({ name: PLUGIN_NAME, stage }, (assets) => {
const { test, styleSheetPath } = this.opt;
const assetNames = Object.keys(assets);
assetNames.forEach((assetName) => {
if (!test || !styleSheetPath || !test.test(assetName)) return;
const originalSource = assets[assetName].source();
const transformResult = (originalSource as string).concat(`@import '${styleSheetPath}';`);
compilation.updateAsset(assetName, new webpack.sources.RawSource(transformResult));
});
});
});
}
}
- 定义
Taro
插件- 使用
RequireStylesheet
、TransformBeforeCompression
与replaceWebpackRuntime
- 修改微信小程序配置文件,注册对应分包。
- 使用
tsx
import type { IPluginContext } from '@tarojs/service';
import { RawSource } from 'webpack-sources';
import { replaceWebpackRuntime } from './replace-webpack-runtime';
import { RequireStylesheet, PLUGIN_NAME as RequireStylesheetPluginName } from './require-stylesheet';
import { TransformBeforeCompression, PLUGIN_NAME as TransformBeforeCompressionPluginName } from './transform-before-compression';
interface DynamicPackOpts {
dynamicModuleJsDir: string;
dynamicModuleStyleFile: string;
}
const dynamicPackOptsDefaultOpt: DynamicPackOpts = {
dynamicModuleJsDir: 'dynamic-common',
dynamicModuleStyleFile: 'dynamic-common',
};
export default (ctx: IPluginContext, pluginOpts: DynamicPackOpts) => {
const finalOpts = { ...dynamicPackOptsDefaultOpt, ...pluginOpts };
if (process.env.TARO_ENV !== 'weapp') return;
ctx.modifyWebpackChain(({ chain }) => {
chain.plugin(TransformBeforeCompressionPluginName).use(TransformBeforeCompression, [
{
test: /^runtime\.js$/,
transform: (code, assets) => {
return replaceWebpackRuntime(code, { ...finalOpts, assets });
},
},
]);
chain.plugin(RequireStylesheetPluginName).use(RequireStylesheet, [
{
test: /^app.wxss$/,
styleSheetPath: `./${finalOpts.dynamicModuleStyleFile}.wxss`,
},
]);
});
ctx.modifyBuildAssets(({ assets }) => {
const curAppJSON = assets['app.json'];
if (!curAppJSON) return;
const curAppJSONContent = JSON.parse(curAppJSON.source());
const dynamicPackagesConfig = { root: finalOpts.dynamicModuleJsDir, pages: [] };
const { resolveAlias = {}, subpackages = [] } = curAppJSONContent;
const newAppJSONContent = {
...curAppJSONContent,
subpackages: [...subpackages, dynamicPackagesConfig],
resolveAlias: {
...resolveAlias,
[`${finalOpts.dynamicModuleJsDir}/*`]: `/${finalOpts.dynamicModuleJsDir}/*`,
},
};
assets['app.json'] = new RawSource(JSON.stringify(newAppJSONContent, null, 2));
});
};
业务中使用
组件分包异步化
tsx
import React, { Suspense, ComponentType, ComponentProps } from 'react';
import type { OrigingComponentPropsType } from './origin-component'
export type { OrigingComponentPropsType } from './origin-component'
const LazyComponent = React.lazy(async ()=>{
const tempRes = await import('./origin-component')
return {default:tempRes.OrigingComponent}
})
export const Component:React.FC<OrigingComponentPropsType> = (props)=>{
return (
<Suspense fallback={null}>
<LazyComponent {...props}/>
</Suspense>
)
}
函数分包异步化
tsx
const Component:React.FC = ()=>{
const handleClick = async ()=>{
const fn = await import('./fn')
fn()
}
return <View onClick={handleClick}>click</View>
}
四、总结
成功塞下@galacean/effects
,在小程序首页为用户展示了炫酷的会员体系升级动画。完结撒花🎉🎉🎉🎉。