likes
comments
collection
share

探索webpack5新特性Module-federation

作者站长头像
站长
· 阅读数 67

webpack5的一个重要特性是Module-federation,本文从为什么会出现这个特性,到怎么使用,是什么原理,以及一些应用场景,最后再给出一个demo,从简介绍下该特性。

Why

Module federation,直译就叫模块联邦吧,是webpack5提供的一个令人激动的插件,可能改变未来几年的前端打包方式,那这个插件的功能与目的是什么呢?

  • 功能:阅读过webpack3或webpack4构建结果的童鞋应该知道,webpack对外只提供了一个全局的webpackJsonp数组(注意不是方法),每个异步chunk加载后通过该数组将自身的modules push(该push方法实际上被劫持修改过的)到内部webpack_modules这个对象上,内部变量可以访问到该对象,但外部是无法获取到的,完全属于“暗箱操作”,这也导致了无法跟外界环境进行模块“联邦”,这也是为什么webpack5中引进了模块联邦机制。通过该机制,可以让构建后的代码库动态的运行时的跑在另一个代码库中。
  • 目的:通过细化功能模块、组件复用、共享第三方库、runtime dependencies线上加载npm包等,可以更好的服务于多页应用、微前端等开发模式。

How

简单介绍下如何使用这个插件。

正常情况下需要配置引用方跟被引用方各自的配置项,非正常情况下引用方不用配置也可以直接使用。当然一个模块本身可以作为一个资源声明被引用,也可以作为引用方声明需要引用的其他资源,这两者都基于ModuleFederationPlugin插件进行串联配置,作为一个插件,它一定是要方便被使用的。

被引用方配置

  • name:必传且唯一,作为关键名称用于第三方引用,相当于一个alias,引用方式${name}/${expose}
  • library: 声明一个挂载在全局下的变量名,其中name即为umd的name
  • filename: 构建后的chunk名称
  • Exposes: 作为被引用方最关键的配置项,用于暴露对外提供的modules模块
  • shared: 声明共享的第三方资源
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin")

new ModuleFederationPlugin({
  name: "zLib",
  library: { type: "var", name: "zLib" },
  filename: "zLib.js",
  exposes: {
    utils: "./src/utils.js"
  },
  shared: ['lodash']
})

引用方配置

  • remotes:作为引用方最关键的配置项,用于声明需要引用的远程资源包的名称与模块名称
  • 其他配置项同上,这里也可以同时声明exposes字段将自身的模块资源暴露给外部使用
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin")

new ModuleFederationPlugin({
  name: "zLocal",
  library: { type: "var", name: "zLocal" },
  remotes: {
    zLib: "zLib"
  },
  shared: ["lodash"]
})

在引用远程资源的项目中使用时,需要先远程资源入口文件引入,可以异步加载,也可以使用script标签引入。这一个功能是向全局挂载一个zComp变量,并提供一个get方法用于获取模块。

<script src="/zLib/dist/zLib.js"></script>

当需要引用某个资源模块时,通过import('远程资源包名称/模块名')的方式直接引入。

import('zLib/utils').then(({ timeDelayFn }) => {
  timeDelayFn(function () {
    console.log('from remote utils fn')
  }, 1000)
})

What

查看构建后代码可以看到,以上功能是基于改写的webpack_require.e这个方法实现的,webpack5之前该方法只用于通过jsonp的方式加载异步chunk,并在then微任务中引用加载后的modules。但在webpack5中该方法升级了,每次调用的时候会遍历执行webpack_require.f对象上的3个方法,分别为:

  • webpack_require.f.overridables,用于将A chunk中的共享modules合并到B chunk中的webpck_require.O
  • webpack_require.f.remotes,用于将B chunk中的共享modules加载到A chunk中的webpack_modules
  • webpack_require.f.jsonp,用于加载异步chunk,并注入指定entry chunk中的webpack_modules

3个方法的执行逻辑是这样的:

  • 执行overridables,检查当前需要加载的chunk是否是在配置项中被声明为shared共享资源,如果在__webpack_require__.O上能找到对应资源,则直接使用,不再去请求资源
  • 执行remotes,检查当前需要加载的chunk是否在配置项中声明为remote远程资源,如果通过get方法能在另一个应用中找到对应modules,则异步加载远程资源使用,并缓存到当前__webpack_modules__对象上
  • 执行jsonp,将直接向指定地址异步加载资源
// __webpack_require__.f.overridables
__webpack_require__.f.overridables = (chunkId, promises) => {
  if (__webpack_require__.o(chunkMapping, chunkId)) {
    chunkMapping[chunkId].forEach((id) => {
      if (__webpack_modules__[id]) return
      promises.push(Promise.resolve((__webpack_require__.O[idToNameMapping[id]] || fallbackMapping[id])()).then((factory) => {
        __webpack_modules__[id] = (module) => { // 模拟注入modules操作
          module.exports = factory()
        }
      }))
    })
  }
}

// __webpack_require__.f.remotes
__webpack_require__.f.remotes = (chunkId, promises) => {
  if (__webpack_require__.o(chunkMapping, chunkId)) {
    chunkMapping[chunkId].forEach((id) => {
      if (__webpack_modules__[id]) return
      var data = idToExternalAndNameMapping[id]
     promises.push(Promise.resolve(__webpack_require__(data[0]).get(data[1])).then((factory) => {
        __webpack_modules__[id] = (module) => {
          module.exports = factory()
        }
      }))
    })
  }
}

// jsonp部分代码太长,基本原理就是创建script标签插入页面是实现资源加载,下面有模拟实现
__webpack_require__.f.j = () => {}

Where

说那么多有什么用?那么可能的应用场景大概有:

  • 微前端,通过shared与remote提供公共依赖资源载入,减少线上体积与便于维护。
  • 编译提速,可以将node_modules资源提前打包好,通过runtime方式引用,编译时只构建项目源文件。
  • 多页应用资源复用,包括运行时依赖引入、组件复用、甚至整个页面共享。

Demo

嘘寒问暖,不如巨款,千言万语,不如一例,不管上面讲的懂不懂,看demo你就懂了!

本地以vue项目为例,假设当前项目中需要一个input组件以及一个button组件,你可以把它想象成就是在引用elementUI中的组件,但是,我们又不想直接把这两个UI组件打包到本地资源中,很显然,照以往方式会使用vue组件懒加载的方式编写,如下:

<template>
  <section class="module-federation">
    <myButton />
    <myInput />
  </section>
</template>

<script>
const myButton = () => import('./myButton.vue')
const myInput = () => import('./myInput.vue')

export default {
  components: { myButton, myInput }
}
</script>

通过上面这种方式可以把组件打包成一个独立的chunk进行异步加载,但该chunk文件只能被当前项目引用,无法提供给其他的项目使用,因为该chunk被注入到项目的modules对象之后无法被外界访问到。正因为这个局限性,所以引入我们的模块联邦,这里我们可以不需要对原有项目进行改动,只需配置共享资源的构建配置项:

new ModuleFederationPlugin({
  name: "zComp",
  library: { type: "var", name: "zComp" },
  filename: "zComp.js",
  exposes: {
    myButton: "./src/myButton.vue",
    myInput: './src/myInput.vue'
  },
  shared: ['vue']
})

配置资源包名称为zComp,暴露2个UI组件myButton与myInput,如果通过remote配置的项目可以直接使用zComp/myButton的方式引用,但这里我们不改动原有项目,所以采用另一种方式zComp.get('myButton')间接引用。

基本思路是异步加载共享的资源包文件zComp.js,该文件会在window上挂载一个zComp全局变量,该变量上有2个方法,分别为get与override,上面也介绍过,get用于获取共享资源包的共享资源,override用于分享当前项目下的资源。这里重点讲get方法,调用之后返回的是一个promise类型的工厂函数:

// 调用get方法之后会先检查需要加载组件的依赖项
// 返回值为() => __webpack_require__("./src/myButton.vue")
Promise.all([
  __webpack_require__.e("vue"),
  __webpack_require__.e("src_myButton_vue")
]).then(() => () => __webpack_require__("./src/myButton.vue"))

继续调用该工厂函数去引用对应module资源,该资源会通过webpackJsonp被缓存到共享资源包自身的webpack_modules中。拿到module资源也就拿到了组件,接下来就是自由落体自由发挥的时候了。

源代码:

<template>
  <section class="module-federation">
    <h1>webpack5 module-federation</h1>
    <component :is="remoteButton" />
    <component :is="remoteInput" />
  </section>
</template>

<script>
import { asyncJsonp } from '@/utils'
export default {
  data () {
    return {
      remoteButton: null,
      remoteInput: null
    }
  },
  mounted () {
    this.getRemoteComp()
    this.getRemoteLib()
  },
  methods: {
    async getRemoteComp () {
      await asyncJsonp('/static/common-shared/zComp.js')
      console.log('zComp chunk loaded')
      // 引用button组件
      const buttonFactory = await zComp.get('myButton')
      this.remoteButton = buttonFactory().default
      // 引用input组件
      const inputFactory = await zComp.get('myInput')
      this.remoteInput = inputFactory().default
    },
    async getRemoteLib () {
      await asyncJsonp('/static/common-shared/zLib.js')
      console.log('zLib chunk loaded')
      // 引用另一个共享资源包中的utils工具库
      const factory = await zLib.get('utils')
      const utils = factory()
      utils.timeDelayFn(() => {
        console.log('from remote utils fn log')
      }, 2000)
    }
  }
}

其中asyncJsonp方法借鉴了webpack Jsonp的实现简单封装成promise:

export const asyncJsonp = (() => {
  const cacheMap = {}
  return (path, delay = 120) => {
    if (!path || cacheMap[path]) return
    return new Promise((resolve, reject) => {
      const script = document.createElement('script')
      script.charset = 'utf-8'
      script.timeout = delay
      script.src = path

      const onScriptComplete = (event) => {
        script.onerror = script.onload = null
        clearTimeout(timeout)
        if (event.type === 'load') {
          cacheMap[path] = true
          return resolve()
        }
        const error = new Error()
        error.name = 'Loading chunk failed.'
        error.type = event.type
        error.url = path
        reject(error)
      }

      const timeout = setTimeout(() => {
        onScriptComplete({ type: 'timeout', target: script })
      }, delay * 1000)

      script.onerror = script.onload = onScriptComplete
      document.head.appendChild(script)
    })
  }
})()

Summary

  • 通过重写webpack_require.e引入overrides、remotes、jsonp三个方法实现不同应用间的依赖共享、组件复用、异步加载。不过这里需要注意不同应用入口资源的引入顺序。
  • 不同应用间的modules共享本质上是借助了window全局作为桥梁,通过get与override方法连接了不同应用。
  • 当前webpack5-beta14版本尚未加入ModuleFederationPlugin该插件,实践的时候需要安装git://github.com/webpack/webpack.git#dev-1开发版本的依赖,期待正常版本的到来。

demo代码地址🤚

转载自:https://juejin.cn/post/6844904133837717511
评论
请登录