来源:微前端

微前端

概念

微前端借鉴了微服务的理念,将一个庞大的应用拆分成多个独立灵活的小型应用

每个应用都可以独立开发,独立运行,独立部署,还可以随意组合,降低了耦合度,更加灵活

特性

  • 技术栈无关:主框架不限制接入应用的技术栈,子应用可自主选择技术栈(vue,react,jq,ng等)

  • 独立开发/部署:各个团队之间仓库独立,单独部署,互不依赖

  • 增量升级:当一个应用庞大之后,技术升级或重构相当麻烦,而微应用具备渐进式升级的特性

  • 独立运行时:微应用之间运行时互不依赖,有独立的状态管理

场景

后台管理系统

最外面一层可以当主应用,里面可以放不同的子应用,子应用不受技术的限制

Web商店(未来趋势)

例如一些导航网站,可以提供微前端的接入

网站可以入驻该导航网站,还可以提供一些API增加交互,有点类似小程序

小程序可以调用微信的一些能力例如支付,扫码等;导航也可以提供一些API,网站接入之后提供API调用

微前端方案

iframe方案

特点:

  • 接入比较简单
  • 隔离完美

不足:

  • DOM割裂感严重,弹框只能在iframe里,而且有滚动条
  • 通讯非常麻烦,刷新iframe会导致url状态丢失
  • 前进后退按钮无效

qiankun方案

qiankun方案是基于single-spa的微前端方案,官网

特点:

  • html entry方式引入子应用,相比JS,entry极大的降低了应用改造的成本
  • 完备的沙箱方案:为JS沙箱做了SnapshotSandbox、LegacySandbox、ProxySandbox三套渐进增强方案;为CSS沙箱做了strictStyleIsolation、experimentalStyleIsolation两套适用不同场景的方案
  • 静态资源能够预加载

不足:

  • 适配成本较高,工程化、生命周期、静态资源路径、路由等都要做一系列的适配工作
  • CSS沙箱采用严格隔离会有各种问题,JS沙箱在某些场景下执行性能下降严重
  • 无法同时激活多个子应用,也不支持子应用保活
  • 无法支持Vite等ESModule脚本运行

底层原理:

  • JS沙箱使用proxy进行快照后,用with(window){}包裹,with内的window其实就是proxy.window,声明变量var name = 'yajue'时,实际这个变量挂到了proxy.window,而不是真正的window
  • CSS沙箱,前者为shadowDOM隔离,后者类似Vue的scoped([data-qiankun-426732]

micro-app 方案

micro-app是基于webcomponent+qiankun sandbox的微前端方案,官网

特点:

  • 使用webcomponet加载子应用,相比single-spa,这种注册监听方案更加优雅
  • 复用经过大量项目验证过的qiankun沙箱机制,使框架更加可靠
  • 组件式api更符合使用习惯,支持子应用保活
  • 降低子应用改造成本,提供静态资源预加载能力

不足:

  • 接入成本与qiankun相比有所降低,但是路由依然存在依赖(虚拟路由已解决)
  • 多应用激活后,无法保持各子应用的路由状态,刷新后会全部丢失(虚拟路由已解决)
  • CSS沙箱依然无法绝对隔离,但JS沙箱使用全局变量查找缓存,性能有所优化
  • 支持Vite运行,但必须使用plugin改造子应用,且其JS代码无法沙箱隔离,非常不友好
  • 对于不支持webcompnent的浏览器没有做降级处理

底层原理:

  • JS隔离跟qiankun类似,也是使用proxy + with
  • CSS隔离使用自定义前缀,类似scoped

EMP方案

EMP方案是基于Webpack5模块联邦的微前端方案,官网

特点:

  • Webpack联邦编译可以保证所有子应用依赖解耦
  • 应用间去中心化的调用、共享模块
  • 模块远程TS支持;

不足

  • 对Webpack强依赖,所以对老旧项目不友好
  • 没有有效的CSS沙箱和JS沙箱,需要靠用户自觉
  • 子应用保活、多应用激活无法实现
  • 主、子应用的路由可能冲突

底层原理:类似拆包,也可以叫模块共享,例如React模块可以共享给Vue项目用;Vue2的组件可以共享给Vue3用

无界方案

官网

特点:

  • 接入简单,只需四五行代码
  • 不需要针对Vite额外处理
  • 预加载
  • 应用保活机制

不足

  • 需要使用一个空的iframe隔离JS
  • 子应用的Axios需要自行适配
  • iframe沙箱的src设置了主应用的host,初始化iframe时,需要等iframe的location.origin,从’about:blank’初始化为主应用的host,此处采用计时器等待,不是很优雅

底层原理:

  • 使用shadowDom隔离CSS
  • 使用空的iframe隔离JS
  • 使用proxy通讯

微前端前置知识

webComponents

相当于原生JS实现的组件,拥有样式隔离、外部传参等特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./index.js"></script>
</head>

<body>
<!-- 自定义标签可以传参 -->
<ya-jue msg="114514" age="24"></ya-jue>
<div>我是div</div>

<template id="yajue">
<style>
div {
background-color: cyan;
}
</style>
<div>
我是template
</div>
</template>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
window.onload = ()=> {
// 自定义标签
class Yajue extends HTMLElement {
constructor() {
super()
// 创建shadowDOM 实现样式隔离
let dom = this.attachShadow({mode: 'open'})
// 获取template
let template = document.querySelector('#yajue') as HTMLTemplateElement

// 给shadowDOM绑定内容
// template的内容不能直接用于绑定,需要克隆
// cloneNode的参数为true,表示深度克隆(最深度并复制所有分叉)
dom.appendChild(template.content.cloneNode(true))

// 读取参数
console.log(this.getAttr('msg'), this.getAttr('age'))
}

// 根据键获取传参
private getAttr(attr: string) {
return this.getAttribute(attr)
}

// 生命周期:挂载
connectedCallback () {
console.log('类似Vue的Mounted')
}

// 生命周期:卸载
disconnectedCallback () {
console.log('类似Vue的Destory')
}

// 生命周期:更改
attributeChangedCallback (name: any, oldVal: any, newVal: any) {
console.log('类似Vue的Watch,有属性变化时自动触发')
}
}

// 挂载,标签名不能是驼峰命名
// 类似Vue组件,但是原生JS
window.customElements.define('ya-jue', Yajue)
}

monorepo架构

微前端通常是一个主应用+多个子应用的结构,如果每个子应用都得用install安装一次依赖,费时费力

采用monorepo架构,一次install即可安装所有子应用的依赖

目录结构

此处的vue-project是主应用,web文件夹内是数个子应用

配置monorepo

在根目录新建pnpm-workspace.yaml文件,写入配置:

1
2
3
4
5
packages:
# all packages in direct subdirs of packages/ 主应用
- 'vue-project/*'
# all packages in subdirs of components/ 子应用文件夹
- 'web/**'

配置完成后,在根目录使用一次install即可,pnpm会将所有公共依赖项抽离到外层,而里层依赖项都是最核心的

如果想在根目录单独开启某一个子应用服务,可以使用

1
pnpm -F [应用名] dev

如果想在根目录设立全局代码,使主应用和每个子应用都可引用,可以使用

1
pnpm -F [应用名] add [全局代码文件夹名]

无界微前端

简单使用

无界为一些框架封装了组件,可以直接使用,需要安装依赖:

1
2
3
npm i wujie-vue2 -S  # vue2用的
npm i wujie-vue3 -S # vue3用的
npm i wujie-react -S # react用的

此处主应用是Vue3,main.ts中,可以将wujie像插件一样应用:

1
2
3
4
5
6
7
8
import './assets/main.css'

import { createApp } from 'vue'
import App from './App.vue'
import Wujie from 'wujie-vue3' // 引入wujie基于vue3的组件封装

// 直接注册就能使用
createApp(App).use(Wujie).mount('#app')

在App.vue中使用组件引入子应用,其中name是子应用名、url是子应用地址:

1
2
3
4
5
6
7
8
9
10
11
<template>
<div>
<h1>我是主应用</h1>
<div style="display: flex;">
<!-- Vue3子应用 -->
<WujieVue name="vue3" url="http://127.0.0.1:5174/" width="50%"></WujieVue>
<!-- React子应用 -->
<WujieVue name="react" url="http://127.0.0.1:5175/" width="50%"></WujieVue>
</div>
</div>
</template>

两个子应用成功被引入主应用了:

wujie-vue3

wujie-vue3是作者封装的可在Vue3中快速使用的包,此处对封装过程进行还原

目录:

安装依赖:

1
2
3
4
5
pnpm i wujie
pnpm i vue -d
pnpm i webpack webpack-cli -d
pnpm i typescript -d
pnpm i ts-loader -d

初始化wujie子应用需要一些配置项,编写配置项类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import type { plugin } from 'wujie'
type lifecycle = (appWindow: Window) => any;
interface Props {
width: string;
height: string;
// 子应用的唯一标识
name: string;
// 渲染的子应用的url
url: string;
// 需要渲染的html, 如果已有则无需从url请求
html?: string;
// 渲染的容器
loading?: HTMLElement;
// 路由同步开关,false刷新无效,但是前进后退依然有效
sync?: boolean;
// 子应用短路径替换,路由同步时生效
prefix?: { [key: string]: string };
// 子是否应用保活模式,state不会丢失
alive?: boolean;
// 注入给子应用的数据
props?: { [key: string]: any };
// js是否采用fiber模式执行
fiber?: boolean;
// 子应用是否采用降级iframe方案
degrade?: boolean;
// 自定义运行iframe的属性
attrs?: { [key: string]: any };
// 自定义降级渲染iframe的属性 */
degradeAttrs?: { [key: string]: any };
// 代码替换钩子
replace?: (codeText: string) => string;
// 自定义fetch,资源和接口
fetch?: (input: RequestInfo, init?: RequestInit) => Promise<Response>;
// 子应用插件
plugins: Array<plugin>;
// 子应用生命周期
beforeLoad?: lifecycle;
// 没有做生命周期改造的子应用不会调用这些
beforeMount?: lifecycle;
afterMount?: lifecycle;
beforeUnmount?: lifecycle;
afterUnmount?: lifecycle;
// 非保活应用不会调用这些
activated?: lifecycle;
deactivated?: lifecycle;
};

export { Props }

封装wujie子应用的Vue组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import { defineComponent, h, getCurrentInstance, onMounted, watch, onBeforeUnmount } from 'vue'
import { startApp, bus } from 'wujie'
import type { PropType } from 'vue'
import { Props } from './type'
import type { App } from 'vue'

// 定义组件
const wujie = defineComponent({
// 定义传入的参数
// Vue的PropType:用于在用运行时props声明时,给一个prop标注更复杂的类型定义
props: {
width: { type: String, default: "" },
height: { type: String, default: "" },
name: { type: String, default: "", required: true },
loading: { type: HTMLElement, default: undefined },
url: { type: String, default: "", required: true },
sync: { type: Boolean, default: undefined },
prefix: { type: Object, default: undefined },
alive: { type: Boolean, default: undefined },
props: { type: Object, default: undefined },
attrs: { type: Object, default: undefined },
replace: { type: Function as PropType<Props['replace']>, default: undefined },
fetch: { type: Function as PropType<Props['fetch']>, default: undefined },
fiber: { type: Boolean, default: undefined },
degrade: { type: Boolean, default: undefined },
plugins: { type: Array as PropType<Props['plugins']>, default: null },
beforeLoad: { type: Function as PropType<Props['beforeLoad']>, default: null },
beforeMount: { type: Function as PropType<Props['beforeMount']>, default: null },
afterMount: { type: Function as PropType<Props['afterMount']>, default: null },
beforeUnmount: { type: Function as PropType<Props['beforeUnmount']>, default: null },
afterUnmount: { type: Function as PropType<Props['afterUnmount']>, default: null },
activated: { type: Function as PropType<Props['activated']>, default: null },
deactivated: { type: Function as PropType<Props['deactivated']>, default: null },
},

// setup函数模式
setup(props: Props, { emit }) {
// 读取组件实例(setup中不能直接用this读取实例)
const instance = getCurrentInstance()

const init = () => {
// wujie微前端初始化的方法
startApp({
name: props.name,
url: props.url,
el: instance?.refs.wujie as HTMLElement,
loading: props.loading,
alive: props.alive,
fetch: props.fetch,
props: props.props,
attrs: props.attrs,
replace: props.replace,
sync: props.sync,
prefix: props.prefix,
fiber: props.fiber,
degrade: props.degrade,
plugins: props.plugins,
beforeLoad: props.beforeLoad,
beforeMount: props.beforeMount,
afterMount: props.afterMount,
beforeUnmount: props.beforeUnmount,
afterUnmount: props.afterUnmount,
activated: props.activated,
deactivated: props.deactivated,
})
}

// 触发组件的事件
const handlerEmit = (event: string, ...args: any[])=> {
emit(event, ...args)
}

// 组件挂载时初始化wujie微前端应用
onMounted(()=> {
// wujie的全局侦听事件方法,用来将wujie事件中转成组件事件
bus.$onAll(handlerEmit)
init()
})

// 允许name和url传参改变,当传参改变时,重新加载应用
watch([props.name, props.url], ()=> {
init()
})

// 销毁组件
onBeforeUnmount(()=> {
// 取消事件
bus.$offAll(handlerEmit)
})

// 因为没有引入jsx支持,所以就使用h函数定义返回模板
return () => h('div', {
style: {
width: props.width,
height: props.height,
},
ref: 'wujie' // 方便后面读取本div
})
}
})

// Vue app.use(wujie)时,会调用install方法(这个方法),把组件注册上
wujie.install = function(app: App) {
app.component("WujieVue", wujie)
}

export default wujie

webpack打包配置,使用了新一代的JS编译器SWC而不是babel:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const { Configuration } = require("webpack")
const path = require('path')

/**
* @type {Configuration} // 配置类型提示
*/
const config = {
entry: './src/index.ts',
mode: "none",
output: {
filename: 'index.js',
path: path.resolve(__dirname, 'lib'),
// 开发项目时不会用到这些配置项,开发库时才使用
library: "wujieVue", // 全局变量模式时的全局变量名
libraryTarget: "umd", // 构建产物的模式,UMD包含AMD、CMD、CommonJS,但不包含ESM
umdNamedDefine: true
},
// 打包时排除
externals: {
vue: "vue",
wujie: "wujie"
},
module: {
rules: [
{
test: /\.ts$/,
use: 'swc-loader'
}
]
}
}

module.exports = config

为了打包ESM模块,还需要配置.swcrc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// swc配置文件,用于打包esm模块
{
"$schema": "https://json.schemastore.org/swcrc",
"jsc": {
"parser": {
"syntax": "typescript"
},
"target": "es5",
"loose": false,
"externalHelpers": false,
// Requires v1.2.50 or upper and requires target to be es2016 or upper.
"keepClassNames": false
},
"minify": false
}

编写声明文件index.d.ts:

1
2
3
4
5
6
7
8
import { bus, preloadApp, destroyApp, setupApp } from "wujie"
import type { App } from 'vue'

declare const WujieVue: {
install: (app: App) => void
}

export default WujieVue

配置package.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
{
"name": "test3",
"version": "1.0.0",
"description": "",
// 库读取的主文件
"main": "lib/index.js",
// 当模块化为ESM时库读取的主文件
"module": "esm/index.js",
// 命令行命令
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"lib": "webpack",
"esm": "swc src/index.ts -d esm"
},
// 发布到npm时发布的文件
"files": [
"lib",
"esm",
"index.d.ts"
],
"keywords": [],
"author": "",
"license": "ISC",
"types": "./index.d.ts",
"dependencies": {
"@swc/cli": "^0.1.62",
"@swc/core": "^1.3.78",
"swc-loader": "^0.2.3",
"wujie": "^1.0.18"
},
"devDependencies": {
"ts-loader": "^9.4.4",
"typescript": "^5.1.6",
"vue": "^3.3.4",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4"
}
}

预加载

使用

使用预加载,需要从wujie的实例导出preloadApp,参数需和startApp一致,预加载必须开启exec选项

1
2
3
4
5
6
7
8
9
10
11
12
import { createApp } from 'vue'
import App from './App.vue'
import Wujie from 'wujie-vue3'
import { preloadApp } from 'wujie'

const app = createApp(App)

app.use(Wujie).mount('#app')

// 设置预加载,就是这么简单~
preloadApp({name:"vue3", url:"http://127.0.0.1:5174", exec: true})
preloadApp({name:"react", url:"http://127.0.0.1:5175", exec: true})

原理

配置项中,fiber的默认值为true,由于子应用的执行会阻塞主应用的渲染线程,当fiber为true时,JS会采取类似react fiber的模式进行间断执行

每个JS文件的执行都被包裹在requestIdleCallback方法中,每执行一个JS可以返回响应外部的输入,但颗粒度取决于JS文件

如果子应用单个JS文件过大,可以通过拆包的形式降低,使fiber模式效益最大化

MDNwindow.requestIdleCallback() 方法接收一个函数,这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序


requestIdleCallback会在浏览器空闲时执行,一般电脑的屏幕刷新率为60Hz(一秒钟对屏幕进行60次刷新)

浏览器的一帧(约16.6ms)做了很多事:

  1. 处理用户事件event,例如click,input、change等
  2. 执行定时器任务
  3. 执行requestAnimationFrame
  4. 执行DOM的回流与重绘
  5. 计算更新图层的绘制指令
  6. 绘制指令合并主线程

如果一帧时间内,该做的事全都做完了,那么剩下的时间就是空闲时间(但一般来说这个时间很少)

如果完全没有任务执行,浏览器就会获得50ms空闲时间

1
2
3
4
5
requestIdleCallback(function(deadline) {
// 目前的空闲时间时长(单位为ms)
// 可以用来做预加载中的一些判断,如果大于0就是有空闲时间
console.log(deadline.timeRemaining())
})

react也有预加载机制,但是react并未使用requestIdleCallback

据说是requestIdleCallback经过测试可能会使一帧超过16.6ms,这样视觉上就会变卡

所以react16中使用requestAnimationFrame + postMessage进行了实现

react18又换为MessageChannel实现,采用队列方式去执行任务

另外,即使将setTimeout的延时设置为0 也会产生最小4ms毫秒的延迟

传参

wujie将子应用的JS存放于iframe内,所以可以通过windows和子应用通讯

全局变量

在主应用内定义一个全局变量:

1
var yajue = 114514

子应用可以访问到它:

1
console.log(window.parent.yajue)

props

在主应用通过props给子应用注入参数:

1
<WujieVue :props="omg" name="vue3" url="http://127.0.0.1:5174/" width="50%" v-if="show"></WujieVue>

子应用可以获取到它:

1
console.log(window.$wujie.props)

为防止报错,可以添加声明文件:

1
2
3
4
5
6
7
declare global {
interface Window {
$wujie: {
props: Record<string, any>
}
}
}

Event Bus

主应用可以发送或监听事件:

1
2
3
4
5
6
7
// 监听事件
bus.$on('vue3', (data: any)=> {
console.log(data, "主应用收到事件啦!")
})

// 发送事件
bus.$emit('main', "主应用发话啦!")

子应用也可以:

1
2
3
4
5
6
7
// 发送事件
window.$wujie.bus.$emit('vue3', '子应用发话啦!')

// 监听事件
window.$wujie.bus.$on('main', (data: any)=> {
console.log(data, "子应用收到事件啦!")
})

模块联邦技术

模块联邦技术是否是一种微前端尚且存在争议,但它可以让多个独立构建的应用之间动态地调用彼此的模块

这使得应用可轻易地被拆分,做到跨应用模块共享

模块联邦技术和webpack5强耦合,是内置的webpack5插件


构建两个项目,host(主应用)和remote(远程应用):


remote项目可以使用自己的模块

remote/bootstrap.js:

1
2
3
4
5
6
import { addList } from "./list"

let app = document.getElementById("app")

app.innerHTML = "<h2>remote</h2>"
addList()

remote/list.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 这个模块将会暴露给host项目

// 创建容器
let wrap = document.createElement("div")

// 数据列表
let list = [
{name:"aaa",age:21},
{name:"bbb",age:22},
{name:"ccc",age:23},
{name:"ddd",age:24},
{name:"eee",age:25}
]

// 将数据列表格式化成p段落,并加进容器内
list.forEach(item=> {
let p = document.createElement("p")
p.innerHTML = `${item.name} - ${item.age}`
wrap.appendChild(p)
})

// 将容器加进页面
export const addList = ()=> {
document.body.appendChild(wrap)
}

remote/index.js:

1
2
// 一种规范,异步初始化项目
import('./bootstrap')


host项目想要使用remote项目的模块

host/bootstrap.js:

1
2
3
4
5
6
import("remote/addList").then(({ addList }) => {
let app = document.getElementById("app")

app.innerHTML = `<h2>host</h2>`
addList()
})

host/index.js:

1
import("./bootstrap")

但这样一定会报错,因为没有配置模块联邦


remote项目的webpack配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const { Configuration } = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
// 我就是内置的模块联邦插件
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin")

/**
* @type { Configuration }
*/
const config = {
mode: 'none',
entry: './index.js',
output: {
filename: 'bundle.js'
},
devServer: {
port: 11451,
},
plugins: [
new HtmlWebpackPlugin({
template: './index.html'
}),
new ModuleFederationPlugin({
// 本项目叫remote
name: "remote",
// 导出文件名,exposes暴露的文件都会放在这个导出文件里
filename: "remoteEntry.js",
// 暴露
exposes: {
// 导出的模块名: 暴露的模块的路径
"./addList": "/list.js"
}
})
]
}

module.exports = config

host项目的webpack配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const { Configuration } = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin")

/**
* @type { Configuration }
*/
const config = {
mode: 'none',
entry: './index.js',
output: {
filename: 'bundle.js'
},
devServer: {
port: 11452,
},
plugins: [
new HtmlWebpackPlugin({
template: './index.html'
}),
new ModuleFederationPlugin({
name: "host",
remotes: {
// 远程项目名(对应ModuleFederationPlugin配置中的name)@项目导出文件的地址
remote: "remote@http://localhost:11451/remoteEntry.js"
}
})
]
}

module.exports = config

成功了,host项目能使用remote项目的模块了


将主应用host打包后,会发现,主应用使用了CDN方式导入远程应用的模块

假设多个项目需要共用一个模块,使用NPM包,每个项目都需要install,倘若包版本更新,还得再给每个项目install新版本;而CDN引入时,一定会引入当前的最新版本