dcloudio / uni-app

A cross-platform framework using Vue.js
https://uniapp.dcloud.io
Apache License 2.0
40.17k stars 3.64k forks source link

【探讨】关于 `vite` 和 `webpack` 的分包优化的探讨 #5025

Open Vanisper opened 4 months ago

Vanisper commented 4 months ago

引语

  1. 以下是uniapp2, 也就是基于webpack打包的vue2版本的分包优化策略的详细实现 https://github.com/dcloudio/uni-app/blob/fb00949fa49dd7d5d68a43700b7be62e6c0b4af3/packages/vue-cli-plugin-uni/lib/split-chunks.js#L114

  2. 以下是基于vite打包的vue3版本的rollup的打包配置

    TODO: 此处代码是有待优化的地方

https://github.com/dcloudio/uni-app/blob/1c19012e38fb2049ad7775009d9bb0157965910b/packages/uni-mp-vite/src/plugin/build.ts#L118

问题

本人的项目(uniapp-vue3版本)在小程序这边的包大小已经到了一个瓶颈阶段,该分包的都分包了,公共组件也是尽可能的少了;

甚至魔改了uniapp的源码实现了小程序那边支持的异步分包功能,实现了主包异步引用分包组件的能力;

但是随着业务功能的增多,以上分包优化策略已不满足,小程序发版受到严重的影响,急需实现与vue2版本的分包优化一致功能。

探讨

本人研究了uniapp2版本的webpack的分包优化的具体实现,正在尝试对vite的分包优化进行实现(此工作正在进行中,突发奇想发个issue想知道官方有没有考虑这一块)。

不知官方对此处rollup的打包配置有没有什么指导性的建议,以实现与vue2版本的一样的分包优化的效果。

问题整理

https://github.com/Vanisper/uniapp-bundle-optimizer 我做的分包优化解决方案的一个整理,请阅读readme指引操作。

Vanisper commented 4 months ago

已实现vite下的分包优化,下面贴出patch相关部分:

基于 @dcloudio__uni-mp-vite@3.0.0-alpha-4010520240507001 版本的补丁

diff --git a/dist/plugin/build.js b/dist/plugin/build.js
index 47598739e43a1f8049b54fcd756411b42a5dc0af..5c77f79dfd9be6df4ec18517c833034bc55b393b 100644
--- a/dist/plugin/build.js
+++ b/dist/plugin/build.js
@@ -91,9 +91,86 @@ function isVueJs(id) {
     return id.includes('\0plugin-vue:export-helper');
 }
 const chunkFileNameBlackList = ['main', 'pages.json', 'manifest.json'];
+
+// #region subpackage
+const UNI_SUBPACKAGES = process.UNI_SUBPACKAGES || {};
+const subPkgsInfo = Object.values(UNI_SUBPACKAGES);
+const normalFilter = ({ independent }) => !independent;
+const independentFilter = ({ independent }) => independent;
+const map2Root = ({ root }) => root + '/';
+const subPackageRoots = subPkgsInfo.map(map2Root);
+const normalSubPackageRoots = subPkgsInfo.filter(normalFilter).map(map2Root);
+const independentSubpackageRoots = subPkgsInfo.filter(independentFilter).map(map2Root);
+
+// id处理器:将id中的moduleId转换为相对于inputDir的路径并去除查询参数后缀
+function moduleIdProcessor(id) {
+    let inputDir = (0, uni_cli_shared_1.normalizePath)(process.env.UNI_INPUT_DIR);
+    // 确保inputDir以斜杠结尾
+    if (!inputDir.endsWith('/')) {
+        inputDir += '/';
+    }
+
+    const normalized = (0, uni_cli_shared_1.normalizePath)(id);
+    const name = normalized.split('?')[0];
+    // 从name中剔除inputDir前缀
+    const updatedName = name.replace(inputDir, '');
+
+    return updatedName;
+}
+// 查找模块列表中是否有属于子包的模块
+const findSubPackages = function (importers) {
+    return importers.reduce((pkgs, item) => {
+        const pkgRoot = normalSubPackageRoots.find(root => moduleIdProcessor(item).indexOf(root) === 0);
+        pkgRoot && pkgs.add(pkgRoot);
+        return pkgs;
+    }, new Set())
+}
+// 判断是否有主包(是否被主包引用)
+const hasMainPackage = function (importers) {
+    return importers.some(item => {
+        return !subPackageRoots.some(root => moduleIdProcessor(item).indexOf(root) === 0);
+    })
+}
+// 判断该模块引用的模块是否有跨包引用的组件
+const hasMainPackageComponent = function (moduleInfo, subPackageRoot) {
+    if (moduleInfo.id && moduleInfo.importedIdResolutions) {
+        for (let index = 0; index < moduleInfo.importedIdResolutions.length; index++) {
+            const m = moduleInfo.importedIdResolutions[index];
+            
+            if (m && m.id) {
+                const name = moduleIdProcessor(m.id);
+                // 判断是否为组件
+                if (
+                    name.indexOf('.vue') !== -1 ||
+                    name.indexOf('.nvue') !== -1
+                ) {
+                    // 判断存在跨包引用的情况(该组件的引用路径不包含子包路径,就说明跨包引用了)
+                    if (name.indexOf(subPackageRoot) === -1) {
+                        if (process.env.UNI_OPT_TRACE) {
+                            console.log('move module to main chunk:', moduleInfo.id,
+                                'from', subPackageRoot, 'for component in main package:', name)
+                        }
+
+                        // 独立分包除外
+                        const independentRoot = independentSubpackageRoots.find(root => name.indexOf(root) >= 0)
+                        if (!independentRoot) {
+                            return true
+                        }
+                    }
+                } else {
+                    return hasMainPackageComponent(m, subPackageRoot)
+                }
+            }
+        }
+    }
+    return false;
+}
+// #endregion
+
 function createMoveToVendorChunkFn() {
     const cache = new Map();
     const inputDir = (0, uni_cli_shared_1.normalizePath)(process.env.UNI_INPUT_DIR);
+    const UNI_OPT_TRACE = process.env.UNI_OPT_TRACE === 'true' ? true : false;
     return (id, { getModuleInfo }) => {
         const normalizedId = (0, uni_cli_shared_1.normalizePath)(id);
         const filename = normalizedId.split('?')[0];
@@ -114,6 +191,20 @@ function createMoveToVendorChunkFn() {
                 }
                 return;
             }
+            if (UNI_OPT_TRACE) {
+                // 如果这个资源只属于一个子包,并且其调用组件的不存在跨包调用的情况,那么这个模块就会被加入到对应的子包中。
+                const moduleInfo = getModuleInfo(id) || {};
+                const importers = moduleInfo.importers || []; // 依赖当前模块的模块id
+                const matchSubPackages = findSubPackages(importers);
+                if (
+                    matchSubPackages.size === 1 &&
+                    !hasMainPackage(importers) &&
+                    !hasMainPackageComponent(moduleInfo, matchSubPackages.values().next().value)
+                ) {
+                    debugChunk(`${matchSubPackages.values().next().value}common/vendor`, normalizedId);
+                    return `${matchSubPackages.values().next().value}common/vendor`;
+                }
+            }
             // 非项目内的 js 资源,均打包到 vendor
             debugChunk('common/vendor', normalizedId);
             return 'common/vendor';
diff --git a/dist/plugins/manifestJson.js b/dist/plugins/manifestJson.js
index 95af5ee04442d5407cb1a1d8c8d27a769aae7d06..ab65b0608c8bd35479dc7c3a50ac3a582dd29f92 100644
--- a/dist/plugins/manifestJson.js
+++ b/dist/plugins/manifestJson.js
@@ -27,6 +27,12 @@ function uniManifestJsonPlugin(options) {
         if (options.project) {
             userProjectFilename = findUserProjectConfigFile(inputDir, options.project.config);
         }
+        // #region 分包优化参数获取
+        const manifestJson = (0, uni_cli_shared_1.parseManifestJsonOnce)(inputDir);
+        const platformOptions = manifestJson[platform] || {};
+        const optimization = platformOptions.optimization || {};
+        process.env.UNI_OPT_TRACE = !!optimization.subPackages;
+        // #endregion
         return {
             name: 'uni:mp-manifest-json',
             enforce: 'pre',
diff --git a/dist/plugins/pagesJson.js b/dist/plugins/pagesJson.js
index 4aad1ad03113710a2f63dbc61f977d148aa194e0..691eac2ac414fd1d271db3050756238644087a37 100644
--- a/dist/plugins/pagesJson.js
+++ b/dist/plugins/pagesJson.js
@@ -61,6 +61,8 @@ function uniPagesJsonPlugin(options) {
                         (0, uni_cli_shared_1.addMiniProgramPageJson)(name, pageJsons[name]);
                     }
                 });
+                // 将 pages.json 的内容挂载到 process.UNI_SUBPACKAGES,方便后续插件使用
+                process.UNI_SUBPACKAGES = appJson.subPackages || {};
                 return {
                     code: `import './${uni_cli_shared_1.MANIFEST_JSON_JS}'\n` + importPagesCode(appJson),
                     map: { mappings: '' },
Vanisper commented 4 months ago

下一步需要做的是将这部分抽离成 vite 的插件。

相关链接:rollupOptions.output.manualChunks


插件化实现进行中,见此项目此处(说明见此处),插件化todos:

Vanisper commented 4 months ago

补丁更新: 先前的补丁通过在两个内置的json解析、处理的插件中, 获取需要的子包数据、manifest分包优化是否开启, 将这两个数据分别挂载到process.UNI_SUBPACKAGESprocess.env.UNI_OPT_TRACE,供后续插件的使用 但是由于这两个插件的执行顺序可能在 build.js 之后,导致环境变量无法生效, 故单独建立 env.js 脚本,在 @dcloudio__uni-mp-vite 项目入口,引入此脚本以初始化相关环境变量。


基于 @dcloudio__uni-mp-vite@3.0.0-alpha-4010520240507001 版本的补丁(优化版本)

diff --git a/dist/env.js b/dist/env.js
new file mode 100644
index 0000000000000000000000000000000000000000..364a5c722dd0774027e550899bc6eee9c8a04c4f
--- /dev/null
+++ b/dist/env.js
@@ -0,0 +1,22 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+    return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+const path_1 = __importDefault(require("path"));
+const fs_1 = require("fs");
+const uni_cli_shared_1 = require("@dcloudio/uni-cli-shared");
+const platform = process.env.UNI_PLATFORM;
+const inputDir = process.env.UNI_INPUT_DIR;
+
+// #region 分包优化参数获取
+const manifestJson = (0, uni_cli_shared_1.parseManifestJsonOnce)(inputDir);
+const platformOptions = manifestJson[platform] || {};
+const optimization = platformOptions.optimization || {};
+process.env.UNI_OPT_TRACE = !!optimization.subPackages;
+
+const pagesJsonPath = path_1.default.resolve(inputDir, 'pages.json');
+const jsonStr = fs_1.readFileSync(pagesJsonPath, 'utf8');
+const { appJson } = (0, uni_cli_shared_1.parseMiniProgramPagesJson)(jsonStr, platform, { subpackages: true });
+process.UNI_SUBPACKAGES = appJson.subPackages || {};
+// #endregion
\ No newline at end of file
diff --git a/dist/index.js b/dist/index.js
index f343787fe9d9aa1ac97f15a91078814ce747f9ed..6795d4f25cedc4ca7408cbe530b5ac4aa5c0ca2e 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -1,5 +1,7 @@
 "use strict";
 Object.defineProperty(exports, "__esModule", { value: true });
+// 引入一个初始化环境变量的脚本
+require("./env");
 const shared_1 = require("@vue/shared");
 const uni_cli_shared_1 = require("@dcloudio/uni-cli-shared");
 const plugin_1 = require("./plugin");
diff --git a/dist/plugin/build.js b/dist/plugin/build.js
index 47598739e43a1f8049b54fcd756411b42a5dc0af..61ac0227f4587def6bb28baae63ddc789341fe3d 100644
--- a/dist/plugin/build.js
+++ b/dist/plugin/build.js
@@ -91,9 +91,87 @@ function isVueJs(id) {
     return id.includes('\0plugin-vue:export-helper');
 }
 const chunkFileNameBlackList = ['main', 'pages.json', 'manifest.json'];
+
+// #region subpackage
+const UNI_SUBPACKAGES = process.UNI_SUBPACKAGES || {};
+const subPkgsInfo = Object.values(UNI_SUBPACKAGES);
+const normalFilter = ({ independent }) => !independent;
+const independentFilter = ({ independent }) => independent;
+const map2Root = ({ root }) => root + '/';
+const subPackageRoots = subPkgsInfo.map(map2Root);
+const normalSubPackageRoots = subPkgsInfo.filter(normalFilter).map(map2Root);
+const independentSubpackageRoots = subPkgsInfo.filter(independentFilter).map(map2Root);
+
+// id处理器:将id中的moduleId转换为相对于inputDir的路径并去除查询参数后缀
+function moduleIdProcessor(id) {
+    let inputDir = (0, uni_cli_shared_1.normalizePath)(process.env.UNI_INPUT_DIR);
+    // 确保inputDir以斜杠结尾
+    if (!inputDir.endsWith('/')) {
+        inputDir += '/';
+    }
+
+    const normalized = (0, uni_cli_shared_1.normalizePath)(id);
+    const name = normalized.split('?')[0];
+    // 从name中剔除inputDir前缀
+    const updatedName = name.replace(inputDir, '');
+
+    return updatedName;
+}
+// 查找模块列表中是否有属于子包的模块
+const findSubPackages = function (importers) {
+    return importers.reduce((pkgs, item) => {
+        const pkgRoot = normalSubPackageRoots.find(root => moduleIdProcessor(item).indexOf(root) === 0);
+        pkgRoot && pkgs.add(pkgRoot);
+        return pkgs;
+    }, new Set())
+}
+// 判断是否有主包(是否被主包引用)
+const hasMainPackage = function (importers) {
+    return importers.some(item => {
+        return !subPackageRoots.some(root => moduleIdProcessor(item).indexOf(root) === 0);
+    })
+}
+// 判断该模块引用的模块是否有跨包引用的组件
+const hasMainPackageComponent = function (moduleInfo, subPackageRoot) {
+    if (moduleInfo.id && moduleInfo.importedIdResolutions) {
+        for (let index = 0; index < moduleInfo.importedIdResolutions.length; index++) {
+            const m = moduleInfo.importedIdResolutions[index];
+            
+            if (m && m.id) {
+                const name = moduleIdProcessor(m.id);
+                // 判断是否为组件
+                if (
+                    name.indexOf('.vue') !== -1 ||
+                    name.indexOf('.nvue') !== -1
+                ) {
+                    // 判断存在跨包引用的情况(该组件的引用路径不包含子包路径,就说明跨包引用了)
+                    if (name.indexOf(subPackageRoot) === -1) {
+                        if (process.env.UNI_OPT_TRACE) {
+                            console.log('move module to main chunk:', moduleInfo.id,
+                                'from', subPackageRoot, 'for component in main package:', name)
+                        }
+
+                        // 独立分包除外
+                        const independentRoot = independentSubpackageRoots.find(root => name.indexOf(root) >= 0)
+                        if (!independentRoot) {
+                            return true
+                        }
+                    }
+                } else {
+                    return hasMainPackageComponent(m, subPackageRoot)
+                }
+            }
+        }
+    }
+    return false;
+}
+// #endregion
+
 function createMoveToVendorChunkFn() {
     const cache = new Map();
     const inputDir = (0, uni_cli_shared_1.normalizePath)(process.env.UNI_INPUT_DIR);
+    const UNI_OPT_TRACE = process.env.UNI_OPT_TRACE === 'true' ? true : false;
+    console.log('分包优化开启状态:', UNI_OPT_TRACE);
     return (id, { getModuleInfo }) => {
         const normalizedId = (0, uni_cli_shared_1.normalizePath)(id);
         const filename = normalizedId.split('?')[0];
@@ -114,6 +192,20 @@ function createMoveToVendorChunkFn() {
                 }
                 return;
             }
+            if (UNI_OPT_TRACE) {
+                // 如果这个资源只属于一个子包,并且其调用组件的不存在跨包调用的情况,那么这个模块就会被加入到对应的子包中。
+                const moduleInfo = getModuleInfo(id) || {};
+                const importers = moduleInfo.importers || []; // 依赖当前模块的模块id
+                const matchSubPackages = findSubPackages(importers);
+                if (
+                    matchSubPackages.size === 1 &&
+                    !hasMainPackage(importers) &&
+                    !hasMainPackageComponent(moduleInfo, matchSubPackages.values().next().value)
+                ) {
+                    debugChunk(`${matchSubPackages.values().next().value}common/vendor`, normalizedId);
+                    return `${matchSubPackages.values().next().value}common/vendor`;
+                }
+            }
             // 非项目内的 js 资源,均打包到 vendor
             debugChunk('common/vendor', normalizedId);
             return 'common/vendor';
zlinggnilz commented 4 months ago

我原本写了一个简单的,参考博主的做了一些修改,已经用上了,感谢。

Vanisper commented 1 month ago

我原本写了一个简单的,参考博主的做了一些修改,已经用上了,感谢。

实现了插件化了哈,见这里的说明

aymonYU commented 1 month ago

m