怎么办?微信小程序主包又双叒叕不够用了!!!

一、背景

古茗会员体系升级需要在小程序首页接入炫酷的动画,通过技术选型最终敲定了阿里出品的@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插件引入实现的babelwebpack插件,并修改微信小程序配置文件,注册对应分包

代码实现

  1. 定义全局函数asyncRequire标记异步化模块。
tsx 复制代码
/**
 * 异步加载三方依赖
 */
declare const asyncRequire: <T = any>(packagePath: string) => Promise<T>;
  1. 定义重试函数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); // 递归调用自己
  }
};
  1. 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'`);
          }
        }
      },
    },
  };
};
  1. 实现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,
    };
  }
}
  1. 定义Taro插件
    • 使用AsyncPackageBuildrequireAsyncPackageBabelTransformPlugin
    • 修改微信小程序配置文件,注册对应分包。
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修改微信小程序配置文件,注册对应分包

代码实现

  1. 修改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',
      },
    }]
  ],
}
  1. 修改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
          },
        },
      },
    });
  });
};
  1. 修改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;
};
  1. 插入异步模块样式文件至主包样式文件中
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));
        });
      });
    });
  }
}
  1. 定义Taro插件
    • 使用RequireStylesheetTransformBeforeCompressionreplaceWebpackRuntime
    • 修改微信小程序配置文件,注册对应分包。
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,在小程序首页为用户展示了炫酷的会员体系升级动画。完结撒花🎉🎉🎉🎉。

相关推荐
sunbyte4 分钟前
Three.js + React 实战系列-3D 个人主页 :完成 Navbar 导航栏组件
开发语言·javascript·react.js
小钰能吃三碗饭3 小时前
第十二篇:【React + AI】深度实践:从 LLM 集成到智能 UI 构建
前端·react.js·aigc
Nu113 小时前
前端大屏原理系列:拖拽组件到页面
前端·react.js·开源
前端大白话4 小时前
震惊!原来在React中用useRef Hook实现定时器这么简单!手把手教你告别内存泄漏
前端·react.js
晓得迷路了5 小时前
栗子前端技术周刊第 77 期 - tsdown、Astro 5.7、Bun v1.2.10...
前端·javascript·react.js
墨渊君6 小时前
React Native 入门指南: 构建 UI 的必备核心组件
前端·react native·react.js
涵信15 小时前
第十二节:原理深挖-React Fiber架构核心思想
前端·react.js·架构
ohMyGod_12315 小时前
React-useRef
前端·javascript·react.js
每一天,每一步15 小时前
AI语音助手 React 组件使用js-audio-recorder实现,将获取到的语音转成base64发送给后端,后端接口返回文本内容
前端·javascript·react.js