Skip to content
Joe Zhang edited this page Feb 1, 2023 · 1 revision

概述

shared 通常指向host 端和 remote 端相同的模块(例如公共依赖的库)。主要为了 防止host 端和 remote 端重复加载相同依赖产生问题。

打包阶段

生成shared文件

打包阶段依赖于 rollup 的打包功能,当你在 rollup.config.js/vite.config.js 中配置如下代码时

plugins:[
  federation ({
    shared:['vue','vuex']
  })
]

federation 插件会在 rollup options 钩子时读取 shared 相关配置

使用this.emitFile()来为配置了shared的库生成单独的chunk(例如:__federation_shared_vue.js)。因为shared特性依赖于preserveSignature属性的配置,该方法可以更改单个chunk的配置而不会影响用户的配置

type EmittedChunk = {
  type: 'chunk';
  id: string;
  name?: string;
  fileName?: string;
  implicitlyLoadedAfterOneOf?: string[];
  importer?: string;
  preserveSignature?: 'strict' | 'allow-extension' | 'exports-only' | false;
};

生成辅助函数

  • __federation_fn_import.js
  • __federation_lib_semver.js
  • satisfy.js ??

vite 的特殊处理(待刷新)

Vite 2.9 之后不再修改manualChunks,但用户可以通过配置 splitVendorChunkPlugin 实现原来的效果

如果没有配置 manualChunks,vite 默认会提供一个如下的 manualChunks 实现

function createMoveToVendorChunkFn (config: ResolvedConfig): GetManualChunk {
  const cache = new Map<string, boolean>()
  return (id, { getModuleInfo }) => {
    if (
      id.includes ('node_modules') &&
      !isCSSRequest (id) &&
      staticImportedByEntry (id, getModuleInfo, cache)
    ) {
      return 'vendor'
    }
  }
}

简单理解就是如果 moduleId 包含了 node_modules,就会将这部分全部打包到 vendor 中,也就是将所有 shared 相关的配置全部打包到一个文件中。

我们需要的是每一个 shared 配置都单独打一个包,所以如果检测到用户配置了 manualChunks 函数实现(不论是用户配置的还是 vite 配置的),都将会代理这个函数,简要实现如下

if (typeof outputOption.manualChunks === 'function') {
  outputOption.manualChunks = new Proxy (outputOption.manualChunks, {
    apply (target, thisArg, argArray) {
      const moduleId = argArray [0]
      //  if id is in shareModuleIds , return id ,else return vite function value
      let find = ''
      for (const sharedMapElement of sharedMap) {
        //shared key,such as vue,vuex...
        const key = sharedMapElement [0]
        //shared values,such as version and so on
        const value = sharedMapElement [1]
        if (value.get ('dependencies')?.has (id)) {
          find = key
          break
        }
      }
      return find ? find : target (argArray [0], argArray [1])
    }
  })
}

也就是如果 moduleId 如果属于 shared 依赖,将会把这部分依赖放到对应的 shared module 中,不属于就继续走用户配置的配置函数,这样就能将 sharedvitevendor 文件中抽离出来打成单独的 chunk

运行阶段

我们需要将原来直接静态import shared模块替换为使用自定义的动态import函数。以配置了shared:['vue']为例,在expose组件A中使用vue的源码:

import { ref } from 'vue'

会被federation插件修改为

import { importShared } from './__federation_fn_import.js';
const { ref } = await importShared('vue');

importShared() 函数逻辑

importShared是shared导入的关键方法,主要逻辑为

function importShared(name){
  // 1. 如果有缓存,从缓存中取
    return moduleCache[name]
  // 2. 从runtime运行时(host)取
    return getSharedFromRuntime()
  // 3. 从本地Local取(remote)
    return getSharedFromLocal()
}
  • getSharedFromRuntime()

    优先在全局globalThis上查询__federation_shared__,如果查询到并且版本符合配置要求则会优先使用。一个示例的全局shared配置对象如下:

    {
        "vue":{
        "3.2.45":{
        get() =>{
            __federation_import('./__federation_shared_vue.js')
        },
    	"loaded":1
    	}
      }
    }

    globalThis上的shared来自于哪里呢?

    在注册远程组件时,host端会将自身能提供的shared信息提供给remoteinit方法(可以在remoteEntry.js文件中找到),remote将会把这些信息写入全局globalThis。因此这里的./__federation_shared_vue.js文件对应的是host端的文件路径。

  • getSharedFromLocal()

    需要注意这里从本地(remote端)加载shared。因此需要加载remote端的./__federation_shared_vue.js

importShared() 替换时机

  • 方案1:在bundle阶段替换

当前是在generateBundle代码生成阶段替换,代码逻辑位于transformImportFn(),当chunk中有import _federation_shared_xxx.js时需要替换。

需要将

import { ref } from './_federation_shared_xxx.js'

替换为

const { ref } = await importShared('xxx');

但是这里存在一个问题,以react为例,生成的_federation_shared_react.js是一个facade chunk,内容为:

import './index-160ec1ee.js'

react 库的实际内容在index-160ec1ee.js,而其他的chunk都是依赖index-160ec1ee.js,并没有依赖_federation_shared_react.js。导致替换失败,没有生成期望的importShared('react')

  • 方案2:在transform阶段替换

  • 方案3:manualChunks

    无法关闭tree shaking,导致生成的shared可能会缺少导出,运行时报错。