webpack - 多入口多文件项目根据maxsize拆包后HtmlWebpackPlugin引入<script>失效问题

296 阅读4分钟

记一次诡异的BUG-多入口多文件项目根据maxsize拆包后HtmlWebpackPlugin引入<script>失效问题

项目场景:

    "vue": "^2.6.10",
    "@vue/cli-service": "^4.5.12",

多入口,多页面拆包项目

需求: 甲方爸爸觉得打包后的包太大了,需要前端将包拆分每个小于1mb。


问题描述

用了webpack里optimization.splitChunks.maxSize属性。结果导致打包后,打包结果<script>自动引入到html失效。

性能没有提升,反而大大降低!!!!


原因分析:

理解为什么要进行代码分割?

代码分割最基本的任务是分离出第三方依赖库,第三方库的内容基本不变,从而contentHash不变,避免没有必要的重复打包,并利用浏览器缓存避免冗余的客户端加载。另外当项目发布新版本时就可以使用客户端原来的缓存文件,提升访问速度,减少白屏。或者,代码分割也可以提供对脚本在整个加载周期内的加载时机的控制能力。

从上面的例子整个的生命周期来看,我们将原本一次就可以加载完的脚本拆分为了两次,这无疑会加重服务端的性能开销,毕竟建立TCP连接是一种开销很大的操作,但这样做却可以换来对渲染节奏的控制和用户体验的提升,异步模块和懒加载模块从宏观上来讲实际上都属于代码分割的范畴。code splitting最极端的状况其实就是拆分成打包前的原貌,也就是源码直接上线。


image.png

代码分割的本质,就是在“源码直接上线”和“打包为唯一的脚本main.bundle.js”这两种极端方案之间寻找一种更符合实际场景的中间状态,用可接受的服务器性能压力增加来换取更好的用户体验。

 splitChunks提供了更精确的分割策略,但是似乎无法直接通过html-webpack-plugin配置参数来动态解决分割后代码的注入问题,因为分包名称是不确定的。这个场景在使用chunks:'async'默认配置时是不存在的,因为异步模块的引用代码是不需要以

问题小结:
当chunks配置项设置为all或initial时,就会有问题,例如上面示例中,通过在html-webpack-plugin中配置excludeChunks可以去除page和about这两个chunk,但是却无法提前排除vendors-about-page这个chunk,因为打包前无法知道是否会生成这样一个chunk。这个场景笔者并没有找到现成的解决方案,对此场景有需求的读者也许可以通过使用html-webpack-plugin的事件扩展来处理此类场景,也可以使用折中方案,就是第一次打包后记录下新生成的chunk名称,按需填写至html-webpack-plugin的chunks配置项里。 参考:blog.51cto.com/u_15214399/…

我解决啦引入问题!!!

我的想法

从参考里得知 webpack 里 这个插件HtmlWebpackPlugin默认配置的属性chunks(包名配置):[common, vendors, index] 对应的是splitChunks里的name属性。从而将 chunk-common.js chunk-vendors.js index.js 这三个包引入到html。

so!我尝试在插件执行前,把这个chunk属性里把所有拆的包名收集到配置进去。OK,就找到一个插件NamedChunksPlugin,获取到所有打包后的chunk名,在HtmlWebpackPlugin执行前更改其chunks属性。

const chunksList = [];
module.exports = {
  chainWebpack: (config) => {
    //在合适的位置插入代码拆分逻辑
    config.optimization.splitChunks({
      cacheGroups: {
        common: {
          name: "chunk-common",
          maxSize: 500 * 1000,
          minChunks: 2,
          priority: -20,
          chunks: "initial",
          reuseExistingChunk: true,
        },
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          priority: -3,
          name: "chunk-vendors",
          chunks: "initial",
          enforce: true,
          reuseExistingChunk: true,
        },
      },
    });
    config
      .plugin("named-chunks")
      .before("html-admin")
      .use(require("webpack/lib/NamedChunksPlugin"), [
        (chunk) => {
          if (chunk.name) {
            chunksList.push(chunk.name);
            return chunk.name;
          }
          const hash = require("hash-sum");
          const joinedHash = hash(
            Array.from(chunk.modulesIterable, (m) => m.id).join("_")
          );
          chunksList.push(`chunk-${joinedHash}`);
          return `chunk-${joinedHash}`;
        },
      ])
      .end();
    /**
     * 根据路由驱动页面的 runtime 代码默认情况是包含在 build 后的 app.hash.js 内的,如果我们改动其他路由,就会导致 runtime 代码改变。
     * 从而不光我们改动的路由对应的页面 js 会变,含 runtime 代码的 app.hash.js 也会变,对用户体验是非常不友好的。为了解决这个问题要设定 runtime 代码单独抽取打包:
     */
    config.optimization.runtimeChunk("single");

    config.plugin("html-index").tap((config) => {
      config[0].chunks = chunksList;
      return config;
    });
    config.plugin("html-admin").tap((config) => {
      config[0].chunks = chunksList;
      return config;
    });
    // when there are many pages, it will cause too many meaningless requests
    config.plugins.delete("prefetch-admin");
    config.plugins.delete("prefetch-index");
  },
};

结论总结

结论: 白搭,通过性能测试后,这种拆包并未达到明显的性能提升。请求速度提升不多,但解析js花费时间突增。

顺带贴一下 NamedChunksPlugin插件


/*
	MIT License http://www.opensource.org/licenses/mit-license.php
	Author Tobias Koppers @sokra
*/
"use strict";

class NamedChunksPlugin {
	static defaultNameResolver(chunk) {
		return chunk.name || null;
	}

	constructor(nameResolver) {
		this.nameResolver = nameResolver || NamedChunksPlugin.defaultNameResolver;
	}

	apply(compiler) {
		compiler.hooks.compilation.tap("NamedChunksPlugin", compilation => {
			compilation.hooks.beforeChunkIds.tap("NamedChunksPlugin", chunks => {
				for (const chunk of chunks) {
					if (chunk.id === null) {
						chunk.id = this.nameResolver(chunk);
					}
				}
			});
		});
	}
}

module.exports = NamedChunksPlugin;

class ChunkTestPlugin {
	constructor(options) {
		this.options = options || {};
	}

	apply(compiler) {
		const options = this.options;
		const minSizeReduce = options.minSizeReduce || 1.5;

		compiler.hooks.compilation.tap("ChunkTestPlugin", compilation => {
			compilation.hooks.optimizeChunks.tap("ChunkTestPlugin", chunks => {
				const chunkGraph = compilation.chunkGraph;

				let combinations = [];
				for (const a of chunks) {
					if (a.canBeInitial()) continue;
					for (const b of chunks) {
						if (b.canBeInitial()) continue;
						if (b === a) break;

						const aSize = chunkGraph.getChunkSize(b, {
							chunkOverhead: 0
						});
						const bSize = chunkGraph.getChunkSize(a, {
							chunkOverhead: 0
						});
						const abSize = chunkGraph.getIntegratedChunksSize(b, a, {
							chunkOverhead: 0
						});
						const improvement = (aSize + bSize) / abSize;

						combinations.push({
							a,
							b,
							improvement
						});
					}
				}

				combinations.sort((a, b) => {
					return b.improvement - a.improvement;
				});

				const pair = combinations[0];

				if (!pair) return;
				if (pair.improvement < minSizeReduce) return;

				chunkGraph.integrateChunks(pair.b, pair.a);
				compilation.chunks.delete(pair.a);
				return true;
			});
		});
	}
}

module.exports = ChunkTestPlugin; 
————————————————
版权声明:本文为CSDN博主「前端码农小王」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_53225741/article/details/128779464