探索webpack5新特性Module-federation
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
开发版本的依赖,期待正常版本的到来。
转载自:https://juejin.cn/post/6844904133837717511