微前端架构设计之构建生成重复 moduleId 的原因和解决办法

在我们的 vue-mfe 微前端项目中,出现了重复的 moduleId。第一次我们的解决办法是使用增大 hash-module-ids 的 hashDigestLength 到 8 位,vue-cli3 默认是 4 位,Webpack 默认也是 4 位。但是随着 SubApp 资源的增多,还是出现了重复。于是不得不排查了下生成重复的 moduleId 的原因:

案例

construction SubApp 的 portal.entry.js 中使用了 import commonService from './service/commonService',而在material SubApp portal.entry.js 中也存在相同路径的引用import commonService from './service/commonService'

然后在 constructionmaterial 两个 SubApp 之间就出现了冲突,比如说先加载construction,那么material加载的commonService就是先前被加载的moduleCache[modueId]

Q1: 为什么会出现相同的 modueId 在不同的 webpack 构建上下文中?

因为 webpack hashModuleId 的构建方式是基于当前相对路径生成 moduleId,分别看下文档和代码:

文档:

This plugin will cause hashes to be based on the relative path of the module, generating a four character string as the module id. Suggested for use in production.

代码:

apply(compiler) {
		const options = this.options;
		compiler.hooks.compilation.tap("HashedModuleIdsPlugin", compilation => {
			const usedIds = new Set();
			compilation.hooks.beforeModuleIds.tap(
				"HashedModuleIdsPlugin",
				modules => {
					for (const module of modules) {
						if (module.id === null && module.libIdent) {
              // 这就是相对路径位置,调用 module.libIdent 方法
							const id = module.libIdent({
								context: this.options.context || compiler.options.context
							});
							const hash = createHash(options.hashFunction);
							hash.update(id);
							const hashId = /** @type {string} */ (hash.digest(
								options.hashDigest
							));
							let len = options.hashDigestLength;
							while (usedIds.has(hashId.substr(0, len))) len++;
							module.id = hashId.substr(0, len);
							usedIds.add(module.id);
						}
					}
				}
			);
		});
	}

而调用 module.libIdent 方法返回是这样的字符串:

有 loader 的会加上 loader 路径: css:./node_modules/css-loader/index.js?!./node_modules/postcss-loader/src/index.js?!./src/components/virtual-table/table.css",

js: ./node_modules/css-loader/lib/css-base.js

而在我们的 SubApp 项目中因为两个路径一致,则生成的 hashId 就成了一样一样的了。

Q2: 如何修复?

我的解决办法重写了 HashedModuleIdsPlugin,主要就是添加了一个 id 选项,用来标识当前不同的 SubApp 上下文:

"use strict"
const createHash = require("webpack/lib/util/createHash")

const validateOptions = require("schema-utils")
const schema = require("webpack/schemas/plugins/HashedModuleIdsPlugin.json")
const extendSchema = {
  ...schema,
  properties: {
    ...schema.properties,
    id: {
      description:
        "The identifier to generates the unique hash module id between different Sub-App.",
      type: "string",
    },
  },
}

/** @typedef {import("webpack/declarations/plugins/HashedModuleIdsPlugin").HashedModuleIdsPluginOptions} HashedModuleIdsPluginOptions */

class EnhancedHashedModuleIdsPlugin {
  /**
   * @param {HashedModuleIdsPluginOptions=} options options object
   */
  constructor(options) {
    if (!options) options = {}

    validateOptions(extendSchema, options, "Hashed Module Ids Plugin")

    /** @type {HashedModuleIdsPluginOptions} */
    this.options = Object.assign(
      {
        id: "id",
        context: null,
        hashFunction: "md4",
        hashDigest: "base64",
        hashDigestLength: 4,
      },
      options
    )
  }

  apply(compiler) {
    const options = this.options
    compiler.hooks.compilation.tap(
      "EnhancedHashedModuleIdsPlugin",
      (compilation) => {
        const usedIds = new Set()
        compilation.hooks.beforeModuleIds.tap(
          "EnhancedHashedModuleIdsPlugin",
          (modules) => {
            for (const module of modules) {
              if (module.id === null && module.libIdent) {
                // 用 id 再加上 libIdent 返回的结果
                const id =
                  this.options.id +
                  " " +
                  module.libIdent({
                    context: this.options.context || compiler.options.context,
                  })
                const hash = createHash(options.hashFunction)
                hash.update(id)
                const hashId = /** @type {string} */ (hash.digest(
                  options.hashDigest
                ))
                let len = options.hashDigestLength
                while (usedIds.has(hashId.substr(0, len))) len++
                module.id = hashId.substr(0, len)
                usedIds.add(module.id)
              }
            }
          }
        )
      }
    )
  }
}

module.exports = EnhancedHashedModuleIdsPlugin

然后在 vue.config.js 中:

const WebpackEnhancedId = require("./plugins/webpack-enhanced-id-plugin")

new WebpackEnhancedId({
	// 将包名和包版本号对应的 ID 传进去
	id: PACKAGE_NAME + " " + PACKAGE_VERSION,
	context: api.getCwd(),
	hashDigestLength: 8,
})

Webpack hash

因为完全不是 hash 的问题,导致我们走了点弯路。怪自己开始没有认真看代码,摆手。

hash 的主要目的是为了用来命中缓存,无论是浏览器缓存还是服务器静态文件缓存。使用不同的 hash type 是为了应对不同的缓存策略。跟打包构建moduleId没有任何关系。

hash:

每次构建都会生成当前构建的 hash id,所有的 bundled files 都是相同的 hash id。

Unique hash generated for every build, The hash of the module identifier

Hash number will be generated for each build. Generated Hash Number will be same for all the bundled files.

contenthash:

根据抽取的内容生成的 hash id,因此每个资源都有其对应的 hash id。

Hashes generated for extracted content, the hash of the content of a file, which is different for each asset

Hash number will be generated based on the entrypoints and it will be different for all the files.

chunkhash:

根据每个 chunk 的生成 hash id,即每个分块的 hash。

Hashes based on each chunks' content, The hash of the chunk content

Hash will be generated only if you made any changes in the particular file and each file will be having the unique hash number.

Bundle, Chunk and Module

Bundle: Produced from a number of distinct modules, bundles contain the final versions of source files that have already undergone the loading and compilation process.

Chunk: This webpack-specific term is used internally to manage the bundling process. Bundles are composed out of chunks, of which there are several types (e.g. entry and child). Typically, chunks directly correspond with the output bundles however, there are some configurations that don't yield a one-to-one relationship.

Module: Discrete chunks of functionality that provide a smaller surface area than a full program. Well-written modules provide solid abstractions and encapsulation boundaries which make up a coherent design and clear purpose.

What are module, chunk and bundle in webpack? 举个例子:

{
  entry: {
    foo: ["webpack/hot/only-dev-server.js","./src/foo.js"],
    bar: ["./src/bar.js"]
  },
  output: {
    path: "./dist",
    filename: "[name].js"
  }
}
  • Modules: "webpack/hot/only-dev-server.js", "./src/foo.js", "./src/bar.js" ( + any other modules that are dependencies of these entry points!)
  • Chunks: foo, bar
  • Bundles: foo, bar

References

posted @ 2020-06-08 19:30  月光宝盒造梦师  阅读(1076)  评论(0编辑  收藏  举报