微前端无界
无界
官方文档
主应用
1、引入
// 无框架时使用'wujie'
import Wujie from 'wujie'
// 当结合框架时使用'wujie-xxx'
// import Wujie from "wujie-vue2";
// import Wujie from "wujie-vue3";
// import Wujie from "wujie-react";
const { bus, setupApp, preloadApp, startApp, destroyApp } = Wujie
提示
如果主应用是 vue 框架可直接使用 wujie-vue,react 框架可直接使用 wujie-react
2、设置子应用
【非必须】由于 preloadApp
和 startApp
的参数重复,为了避免重复输入,可以通过 setupApp
来统一设置默认参数。
setupApp({
name: '唯一id',
url: '子应用地址',
exec: true,
el: '容器',
sync: true,
})
3-1、启动子应用
startApp({ name: '唯一id' })
3-2、预加载
preloadApp({ name: '唯一id' })
3-3、以组件形式调用
无界支持以组件的形式使用。
vue
安装
# vue2 框架
npm i wujie-vue2 -S
# vue3 框架
npm i wujie-vue3 -S
引入
// main.js
// vue2
import WujieVue from 'wujie-vue2'
// vue3
// import WujieVue from "wujie-vue3";
// 全局注册组件(以vue为例)
Vue.use(WujieVue)
const { bus, setupApp, preloadApp, startApp, destroyApp } = WujieVue
使用
使用 组件,相当于使用了startApp
来调用,因此可以忽略startApp
的使用了!!
<template>
<!-- 单例模式,name相同则复用一个无界实例,改变url则子应用重新渲染实例到对应路由 -->
<WujieVue
width="100%"
height="100%"
name="vue2"
:url="vue2Url"
:sync="true"
:fetch="fetch"
:props="props"
:beforeLoad="beforeLoad"
:beforeMount="beforeMount"
:afterMount="afterMount"
:beforeUnmount="beforeUnmount"
:afterUnmount="afterUnmount"
></WujieVue>
<!-- 子应用通过$wujie.bus.$emit(event, args)出来的事件都可以直接@event来监听 -->
</template>
<script>
// import hostMap from "./hostMap";
export default {
computed: {
vue2Url() {
// 这里拼接成子应用的域名(例如://localhost:7200/home)
return hostMap('//localhost:7200/') + `#/${this.$route.params.path}`
},
},
}
</script>
// hostMap.js
const map = {
'//localhost:7100/': '//wujie-micro.github.io/demo-react17/',
'//localhost:7200/': '//wujie-micro.github.io/demo-vue2/',
'//localhost:7300/': '//wujie-micro.github.io/demo-vue3/',
'//localhost:7500/': '//wujie-micro.github.io/demo-vite/',
}
export default function hostMap(host) {
if (process.env.NODE_ENV === 'production') return map[host]
return host
}
WujieVue组件
接收的参数如下:
WujieVue组件
接收的参数基本上与startApp
的一致。
不同之处在于startApp
有html
、el
,没有width
、height
。
const wujieVueOptions = {
name: 'WujieVue',
props: {
width: { type: String, default: '' },
height: { type: String, default: '' },
name: { type: String, default: '' },
loading: { type: HTMLElement, default: undefined },
url: { type: String, default: '' },
sync: { type: Boolean, default: false },
prefix: { type: Object, default: undefined },
alive: { type: Boolean, default: false },
props: { type: Object, default: undefined },
replace: { type: Function, default: undefined },
fetch: { type: Function, default: undefined },
fiber: { type: Boolean, default: true },
degrade: { type: Boolean, default: false },
plugins: { type: Array, default: null },
beforeLoad: { type: Function, default: null },
beforeMount: { type: Function, default: null },
afterMount: { type: Function, default: null },
beforeUnmount: { type: Function, default: null },
afterUnmount: { type: Function, default: null },
activated: { type: Function, default: null },
deactivated: { type: Function, default: null },
loadError: { type: Function, default: null },
},
}
子应用改造
无界对子应用的侵入非常小,在满足跨域条件下子应用可以不用改造。
1、前提
子应用的资源和接口的请求都在主域名发起,所以会有跨域问题,子应用必须做cors 设置。
app.use((req, res, next) => {
// 路径判断等等
res.set({
'Access-Control-Allow-Credentials': true,
'Access-Control-Allow-Origin': req.headers.origin || '*',
'Access-Control-Allow-Headers': 'X-Requested-With,Content-Type',
'Access-Control-Allow-Methods': 'PUT,POST,GET,DELETE,OPTIONS',
'Content-Type': 'application/json; charset=utf-8',
})
// 其他操作
})
2、生命周期改造
改造入口函数:
- 将子应用路由的创建、实例的创建渲染挂载到
window.__WUJIE_MOUNT
函数上 - 将实例的销毁挂载到
window.__WUJIE_UNMOUNT
上 - 如果子应用的实例化是在异步函数中进行的,在定义完生命周期函数后,请务必主动调用无界的渲染函数
window.__WUJIE.mount()
具体操作可以参考下面示例
// vue 2
if (window.__POWERED_BY_WUJIE__) {
let instance
window.__WUJIE_MOUNT = () => {
const router = new VueRouter({ routes })
instance = new Vue({ router, render: (h) => h(App) }).$mount('#app')
}
window.__WUJIE_UNMOUNT = () => {
instance.$destroy()
}
} else {
new Vue({ router: new VueRouter({ routes }), render: (h) => h(App) }).$mount('#app')
}
// vue 3
if (window.__POWERED_BY_WUJIE__) {
let instance
window.__WUJIE_MOUNT = () => {
const router = createRouter({ history: createWebHistory(), routes })
instance = createApp(App)
instance.use(router)
instance.mount('#app')
}
window.__WUJIE_UNMOUNT = () => {
instance.unmount()
}
} else {
createApp(App)
.use(createRouter({ history: createWebHistory(), routes }))
.mount('#app')
}
// vite
declare global {
interface Window {
// 是否存在无界
__POWERED_BY_WUJIE__?: boolean;
// 子应用mount函数
__WUJIE_MOUNT: () => void;
// 子应用unmount函数
__WUJIE_UNMOUNT: () => void;
// 子应用无界实例
__WUJIE: { mount: () => void };
}
}
if (window.__POWERED_BY_WUJIE__) {
let instance: any;
window.__WUJIE_MOUNT = () => {
const router = createRouter({ history: createWebHistory(), routes });
instance = createApp(App)
instance.use(router);
instance.mount("#app");
};
window.__WUJIE_UNMOUNT = () => {
instance.unmount();
};
/*
由于vite是异步加载,而无界可能采用fiber执行机制
所以mount的调用时机无法确认,框架调用时可能vite
还没有加载回来,这里采用主动调用防止用没有mount
无界mount函数内置标记,不用担心重复mount
*/
window.__WUJIE.mount()
} else {
createApp(App).use(createRouter({ history: createWebHistory(), routes })).mount("#app");
}
// react
if (window.__POWERED_BY_WUJIE__) {
window.__WUJIE_MOUNT = () => {
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
)
}
window.__WUJIE_UNMOUNT = () => {
ReactDOM.unmountComponentAtNode(document.getElementById('root'))
}
} else {
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
)
}
无界功能介绍
运行模式
无界有三种运行模式:单例模式
、保活模式
、重建模式
其中保活模式
、重建模式
子应用无需做任何改造,而单例模式
则需要做生命周期改造!!!
在微前端框架中,子应用会随着主应用页面的打开和关闭反复的激活和销毁(单例模式:生命周期模式)。而在无界微前端框架中,子应用还可以以其他方式进行处理(保活模式、重建模式),这样会进入完全不同的处理流程。
保活模式
子应用的 alive
设置为 true
时进入保活模式
,内部的数据和路由的状态不会随着页面切换而丢失。
在保活模式下,子应用只会进行一次渲染,页面发生切换时承载子应用 dom
的 webcomponent
会保留在内存中,当子应用重新激活时无界会将内存中的 webcomponent
重新挂载到容器上。
注意:
- 保活模式下改变
url
子应用的路由不会发生变化,需要采用 通信 的方式对子应用路由进行跳转。- 保活的子应用的实例不会销毁,子应用被切走了也可以响应
bus
事件,非保活的子应用切走了监听的事件也会全部销毁,需要等下次重新mount
后重新监听。
单例模式
子应用的 alive
为 false
且进行了生命周期改造时进入单例模式
。
子应用页面如果切走,会调用 window.__WUJIE_UNMOUNT
销毁子应用当前实例,子应用页面如果切换回来,会调用 window.__WUJIE_MOUNT
渲染子应用新的实例。
在单例式下,改变 url
子应用的路由会发生跳转到对应路由。
注意:
如果主应用上有多个菜单栏用到了子应用的不同页面,在每个页面启动该子应用的时候将name
设置为同一个,这样可以共享一个wujie
实例,承载子应用js
的iframe
也实现了共享,不同页面子应用的url
不同,切换这个子应用的过程相当于:销毁当前应用实例 => 同步新路由 => 创建新应用实例
。
重建模式(一般不使用,非常消耗资源)
子应用既没有设置为保活模式,也没有进行生命周期的改造则进入了重建模式
,每次页面切换不仅会销毁承载子应用 dom
的 webcomponent
,还会销毁承载子应用 js
的 iframe
,相应的 wujie
实例和子应用实例都会被销毁。
重建模式下改变 url
子应用的路由会跳转对应路由,但是在 路由同步 场景并且子应用的路由同步参数已经同步到主应用 url
上时则无法生效,因为改变 url
后会导致子应用销毁重新渲染,此时如果有同步参数则同步参数的优先级最高。
生命周期
无界提供的生命周期,与 vue 的生命周期设计非常类似。
其中,保活模式
下,才会执行activated
,deactivated
,其余的生命周期在单例模式
下都会执行。
beforeLoad
类型:
type lifecycle = (appWindow: Window) => any;
子应用开始加载静态资源前触发
beforeMount
类型:
type lifecycle = (appWindow: Window) => any;
子应用渲染(调用window.__WUJIE_MOUNT
)前触发
afterMount
类型:
type lifecycle = (appWindow: Window) => any;
子应用渲染(调用window.__WUJIE_MOUNT
)后触发
beforeUnmount
类型:
type lifecycle = (appWindow: Window) => any;
子应用卸载(调用window.__WUJIE_UNMOUNT
)前触发
afterUnmount
类型:
type lifecycle = (appWindow: Window) => any;
子应用卸载(调用window.__WUJIE_UNMOUNT
)后触发
activated
类型:
type lifecycle = (appWindow: Window) => any;
子应用保活模式
下,进入时触发
deactivated
类型:
type lifecycle = (appWindow: Window) => any;
子应用保活模式
下,离开时触发
loadError
类型:
type loadErrorHandler = (url: string, e: Error) => any;
子应用加载资源失败后触发
通讯功能
无界提供三种方式进行通信
props 通信
主应用可以通过props
注入数据和方法:
<WujieVue :props="{ data: xxx, methods: xxx }"></WujieVue>
子应用可以通过$wujie
来获取:
const props = window.$wujie?.props // {data: xxx, methods: xxx}
注意:
子应用是通过全局属性$wujie
获取props
,而不是在生命周期中获取!!!
window 通信
由于子应用运行的 iframe
的 src
和主应用是同域的,所以相互可以直接通信
主应用调用子应用的全局数据:
window.document.querySelector('iframe[name=子应用id]').contentWindow.xxx
子应用调用主应用的全局数据:
window.parent.xxx
eventBus 通信
无界提供一套去中心化的通信方案,主应用和子应用、子应用和子应用都可以通过这种方式方便的进行通信, 详见 EventBus Api
主应用使用方式:
// 如果使用wujie
import { bus } from "wujie";
// 如果使用wujie-vue
import WujieVue from "wujie-vue";
const { bus } = WujieVue;
// 如果使用wujie-react
import WujieReact from "wujie-react";
const { bus } = WujieReact;
// 主应用监听事件
bus.$on("事件名字", function (arg1, arg2, ...) {});
// 主应用发送事件
bus.$emit("事件名字", arg1, arg2, ...);
// 主应用取消事件监听
bus.$off("事件名字", function (arg1, arg2, ...) {});
子应用使用方式:
// 子应用监听事件
window.$wujie?.bus.$on("事件名字", function (arg1, arg2, ...) {});
// 子应用发送事件
window.$wujie?.bus.$emit("事件名字", arg1, arg2, ...);
// 子应用取消事件监听
window.$wujie?.bus.$off("事件名字", function (arg1, arg2, ...) {});
预加载
tips:预加载能力可以极大的提升子应用打开的首屏时间,但同时在首次渲染时阻塞主应用,当预加载多个子应用时,会出现比较长的白屏时间!!!
预加载
预加载指的是在应用空闲的时候 requestIdleCallback
将所需要的静态资源提前从网络中加载到内存中,详见 preloadApp
预执行
预执行指的是在应用空闲的时候将子应用提前渲染出来,可以进一步提升子应用打开时间。
只需要在 preloadApp
中将 exec
设置为 true
即可。
注意:由于子应用提前渲染可能会导致阻塞主应用的线程,所以无界提供了类似 react-fiber 方式来防止阻塞线程,详见 fiber
路由同步
路由同步
路由同步会将子应用路径的 path+query+hash
通过 window.encodeURIComponent
编码后挂载在主应用 url
的查询参数上,其中 key
值为子应用的 name。
开启路由同步后,刷新浏览器或者将 url
分享出去子应用的路由状态都不会丢失,当一个页面存在多个子应用时无界支持所有子应用路由同步,浏览器刷新、前进、后退子应用路由状态也都不会丢失
开启参数 sync
注意
只有无界实例在初次实例化的时候才会从
url
上读回路由信息,一旦实例化完成后续只会单向的将子应用路由同步到主应用url
上
重点:
wujie 提供了路由同步的功能,主应用无需注册子应用路由,也可以实现跨应用的跳转动作。
此功能非常 nice,解决了像 qiankun 这样的微前端框架,父子应用之间路由注册的老大难问题。
短路径(路由前缀)
无界提供短路径的能力,当子应用的 url
过长时,可以通过配置 prefix
来缩短子应用同步到主应用的路径,无界在选取短路径的时候,按照匹配最长路径原则选取短路径。
完成匹配后子应用匹配到的路径将被{短路径} + 剩余路径
的方式挂载到主应用 url
上,注意在匹配路径的时候请不要带上域名。
示例:
<WujieVue
width="100%"
height="100%"
name="xxx"
:url="xxx"
:sync="true"
:prefix="{
prod: '/example/prod',
test: '/example/test',
prodId: '/example/prod/debug?id=',
}"
></WujieVue>
此时子应用不同路径将转换如下:
/example/prod/hello => {prod}/hello
/example/test/name => {test}/name
/example/prod/debug?id=5&age=10 => {prodId}5&age=10
路由跳转
主应用为 history 模式
子应用 A 要打开子应用 B
以 vue 主应用为例,子应用 A 的 name 为 A, 主应用 A 页面的路径为/pathA
,子应用 B 的 name 为 B,主应用 B 页面的路径为/pathB
主应用 A 页面:
<template>
<!-- 子应用 A -->
<wujie-vue name="A" url="//hostA.com" :props="{jump}" ></WujieVue>
</template>
<script>
export default {
methods: {
jump(location) {
this.$router.push(location);
}
}
}
</script>
子应用 A 通过调用主应用传递的 jump 函数,跳转到子应用 B 的页面
// 子应用 A 点击跳转处理函数
function handleJump() {
window.$wujie?.props.jump({ path: '/pathB' })
}
子应用 A 要打开子应用 B 的指定路由
上面的方法,A 应用只能跳转到应用 B 的在主应用的默认路由,如果需要跳转到 B 应用的指定路由比如 /test
:
- 子应用 B 开启路由同步能力
- 子应用的点击跳转函数:
// 子应用 A 点击跳转处理函数
function handleJump() {
window.$wujie?.props.jump({ path: '/pathB', query: { B: '/test' } })
}
由于跳转后的链接的查询参数带上了 B 应用的路径信息,而子应用 B 开启了路由同步的能力,所以能从 url 上读回需要同步的路径,注意这种办法只有在 B 应用未曾激活过才生效。
子应用 B 为保活应用
如果子应用 B 是保活应用并且没有被打开过,也就是还没有实例化,上述的打开指定路由的方式可以正常工作,但如果子应用 B 已经实例化,保活应用的内部数据和路由状态都会保存下来不随子应用切换而丢失。
这时如果要打开子应用 B 的指定路由可以使用通信的方式 :
子应用 A 点击跳转处理函数
// 子应用 A 点击跳转处理函数
function handleJump() {
window.$wujie?.bus.$emit('routeChange', '/test')
}
子应用 B
// 子应用 B 监听并跳转
window.$wujie?.bus.$on('routeChange', (path) => this.$router.push({ path }))
主应用为 hash 模式
当主应用为 hash 模式时,主应用路由的 query 参数会挂载到 hash 的值后面,而无界路由同步读取的是 url 的 query 查询参数,所以需要手动的挂载查询参数
子应用 A 要打开子应用 B
同上
子应用 A 要打开子应用 B 的指定路由
- 主应用 的 jump 修改:
<template>
<wujie-vue name="A" url="//hostA.com" :props="{jump}"></wujie-vue>
</template>
<script>
export default {
methods: {
jump(location, query) {
// 跳转到主应用B页面
this.$router.push(location);
const url = new URL(window.location.href);
url.search = query
// 手动的挂载url查询参数
window.history.replaceState(null, "", url.href);
}
}
</script>
-
子应用 B 开启路由同步能力
-
子应用的点击跳转函数:
function handleJump() {
window.$wujie?.props.jump({ path: "/pathB" } , `?B=${window.encodeURIComponent("/test")}`});
}
子应用 B 为保活应用
同上
插件系统
无界的插件体系主要是方便用户在运行时去修改子应用代码从而避免去改动仓库代码,详见API
html-loader
无界提供插件在运行时对子应用的 html 文本进行修改
- 示例
const plugins = [
{
// 对子应用的template进行的aaa替换成bbb
htmlLoader: (code) => {
return code.replace('aaa', 'bbb');
},
}
];
js-excludes
如果用户想加载子应用的时候,不执行子应用中的某些js
文件
那么这些工作可以放置在js-excludes
中进行
- 示例
const plugins = [
// 子应用的 http://xxxxx.js 或者符合正则 /test\.js/ 脚本将不在子应用中进行
{ jsExcludes: ["http://xxxxx.js", /test\.js/] },
];
js-ignores
如果用户想子应用自己加载某些js
文件(通过script
标签),而非框架劫持加载(通常会导致跨域)
那么这些工作可以放置在js-ignores
中进行
- 示例
const plugins = [
// 子应用的 http://xxxxx.js 或者符合正则 /test\.js/ 脚本将由子应用自行加载
{ jsIgnores: ["http://xxxxx.js", /test\.js/] },
];
警告
jsIgnores 中的 js 文件由于是子应用自行加载没有对 location 进行劫持,如果有对 window.location.href 进行操作复制请务必替换成 window.$wujie.location.href 的操作,否则子应用的沙箱会被取代掉
js-before-loaders
如果用户想在html
中所有的js
之前做:
- 在子应用运行一个
src="http://xxxxx"
的脚本 - 在子应用中运行一个内联的 js 脚本
<script>content</script>
- 执行一个回调函数
那么这些工作可以放置在js-before-loaders
中进行
- 示例
const plugins = [
{
// 在子应用所有的js之前
jsBeforeLoaders: [
// 插入一个外联脚本
{ src: "http://xxxx.js" },
// 插入一个内联监本
{ content: 'console.log("test")' },
// 执行一个回调,打印子应用名字
{
callback(appWindow) {
console.log("js-before-loader-callback", appWindow.__WUJIE.id);
},
},
],
},
];
js-loader
如果用户想将子应用的某个js
脚本的代码进行替换,可以在这个地方进行处理
- 示例
const plugins = [
{
// 将url为aaa.js的脚本中的aaa替换成bbb
// code 为脚本代码、url为脚本的地址(内联脚本为'')、base为子应用当前的地址
jsLoader: (code, url, base) => {
if (url === "aaa.js") return code.replace("aaa", "bbb");
},
},
];
警告
- 对于 esm 脚本不会经过 js-loader 插件处理
- 对于 js-ignores 脚本不会经过 js-loader 插件处理
js-after-loader
如果用户想在html
中所有的js
之后做:
- 在子应用运行一个
src="http://xxxxx"
的脚本 - 在子应用中运行一个内联的 js 脚本
<script>content</script>
- 执行一个回调函数
那么这些工作可以放置在js-after-loaders
中进行
- 示例
const plugins = [
{
jsAfterLoaders: [
// 插入一个外联脚本
{ src: "http://xxxx.js" },
// 插入一个内联监本
{ content: 'console.log("test")' },
// 执行一个回调,打印子应用名字
{
callback(appWindow) {
console.log("js-after-loader-callback", appWindow.__WUJIE.id);
},
},
],
},
];
css-excludes
如果用户想加载子应用的时候,不加载子应用中的某些css
文件
那么这些工作可以放置在css-excludes
中进行
- 示例
const plugins = [
// 子应用的 http://xxxxx.css 脚本将不在子应用中加载
{ cssExcludes: ["http://xxxxx.css" /test\.css/] },
];
css-ignores
如果用户想子应用自己加载某些css
文件(通过link
标签),而非框架劫持加载(通常会导致跨域)
那么这些工作可以放置在css-ignores
中进行
- 示例
const plugins = [
// 子应用的 http://xxxxx.css 或者符合正则 /test\.css/ 脚本将由子应用自行加载
{ cssIgnores: ["http://xxxxx.css", /test\.css/] },
];
css-before-loaders
如果用户想在html
中所有的css
之前做:
- 插入一个
src="http://xxxxx"
的外联样式脚本 - 插入一个
<style>content</style>
的内联样式脚本
那么这些工作可以放置在css-before-loaders
中进行
- 示例
const plugins = [
{
// 在子应用所有的css之前
cssBeforeLoaders: [
//在加载html所有的样式之前添加一个外联样式
{ src: "https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" },
//在加载html所有的样式之前添加一个内联样式
{ content: "img{width: 300px}" },
],
},
];
css-loader
无界提供插件在运行时对子应用的css
文本进行修改
- 示例
const plugins = [
{
// 对css脚本动态的进行替换
// code 为样式代码、url为样式的地址(内联样式为'')、base为子应用当前的地址
cssLoader: (code, url, base) => {
console.log("css-loader", url, code.slice(0, 50) + "...");
return code;
},
},
];
css-after-loaders
如果用户想在html
中所有的css
之后做:
- 插入一个
src="http://xxxxx"
的外联样式脚本 - 插入一个
<style>content</style>
的内联样式脚本
那么这些工作可以放置在css-after-loaders
中进行
- 示例
const plugins = [
{
cssAfterLoaders: [
//在加载html所有样式之后添加一个外联样式
{ src: "https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" },
//在加载html所有样式之后添加一个内联样式
{ content: "img{height: 300px}" },
],
},
];
windowAddEventListenerHook
子应用的window
添加监听事件时执行的回调函数
- 示例
无界子应用的dom
渲染在webcomponent
中,js
在iframe
中运行,往往子应用在外部的容器滚动,所以监听window
的scroll
事件是无效的,可以将处理window
的scroll
事件绑定在滚动容器中
const plugins = [
{
windowAddEventListenerHook(iframeWindow, type, handler, options) {
container.addEventListener(type, handler, options);
},
},
];
windowRemoveEventListenerHook
子应用的window
移除监听事件时执行的回调函数
- 示例
const plugins = [
{
windowAddEventListenerHook(iframeWindow, type, handler, options) {
container.addEventListener(type, handler, options);
},
windowRemoveEventListenerHook(iframeWindow, type, handler, options) {
container.removeEventListener(type, handler, options);
},
},
];
documentAddEventListenerHook
子应用的document
添加监听事件时执行的回调函数
- 示例
无界子应用的dom
渲染在webcomponent
中,js
在iframe
中运行,往往子应用在外部的容器滚动,所以监听document
的scroll
事件是无效的,可以将处理document
的scroll
事件绑定在滚动容器中
const plugins = [
{
documentAddEventListenerHook(iframeWindow, type, handler, options) {
container.addEventListener(type, handler, options);
},
},
];
documentRemoveEventListenerHook
子应用的document
移除监听事件时执行的回调函数
- 示例
const plugins = [
{
documentAddEventListenerHook(iframeWindow, type, handler, options) {
container.addEventListener(type, handler, options);
},
documentRemoveEventListenerHook(iframeWindow, type, handler, options) {
container.removeEventListener(type, handler, options);
},
},
];
appendOrInsertElementHook
子应用往body
、head
插入元素后执行的回调函数
- 示例
const plugins = [
{
// element 为真正插入的元素,iframeWindow 为子应用的 window, rawElement为原始插入元素
appendOrInsertElementHook(element, iframeWindow, rawElement) {
console.log(element, iframeWindow, rawElement)
}
},
];
patchElementHook
子应用创建元素后执行的回调函数
- 示例
const plugins = [
{
patchElementHook(element, iframeWindow ) {
console.log(element, iframeWindow )
}
},
];
降级处理
无界提供无感知的降级方案
在非降级场景下,子应用的dom
在webcomponent
中,运行环境在iframe
中,iframe
对dom
的操作通过proxy
来代理到webcomponent
上,而webcomponent
和proxy
IE
都无法支持,这里采用另一个的iframe
替换webcomponent
,用Object.defineProperty
替换proxy
来做代理的方案
注意
无界并没有对 es6 代码进行 polyfill,因为每个用户对浏览器的兼容程度是不一样的引入的 polyfill 也不一致,如果需要在较低版本的浏览器中运行,需要用户自行 通过 babel 来添加 polyfill。
优点:
- 降级的行为由框架判断,当浏览器不支持时自动降级
- 降级后,应用之间也保证了绝对的隔离度
- 代码无需做任何改动,之前的预加载、保活还有通信的代码都生效,用户不需要为了降级做额外的代码改动导致降级前后运行的代码不一致
- 用户也可以强制降级,比如说当前浏览器对
webcomponent
和proxy
是支持的,但是用户还是想将 dom 运行在 iframe 中,就可以将degrade
设置为 true
缺点:
- 弹窗只能在子应用内部
- 由于无法使用
proxy
,无法劫持子应用的location
,导致访问window.location.host
的时候拿到的是主应用的host
,子应用可以从$wujie.location
中拿到子应用正确的host
API 说明
主应用
setupApp
-
类型:
Function
-
参数:
cacheOptions
-
返回值:void
type lifecycle = (appWindow: Window) => any
type loadErrorHandler = (url: string, e: Error) => any
type baseOptions = {
/** 唯一性用户必须保证 */
name: string
/** 需要渲染的url */
url: string
/** 需要渲染的html, 如果用户已有则无需从url请求 */
html?: string
/** 代码替换钩子 */
replace?: (code: string) => string
/** 自定义fetch */
fetch?: (input: RequestInfo, init?: RequestInit) => Promise<Response>
/** 注入给子应用的属性 */
props?: { [key: string]: any }
/** 自定义运行iframe的属性 */
attrs?: { [key: string]: any }
/** 自定义降级渲染iframe的属性 */
degradeAttrs?: { [key: string]: any }
/** 子应用采用fiber模式执行 */
fiber?: boolean
/** 子应用保活,state不会丢失 */
alive?: boolean
/** 子应用采用降级iframe方案 */
degrade?: boolean
/** 子应用插件 */
plugins?: Array<plugin>
/** 子应用生命周期 */
beforeLoad?: lifecycle
beforeMount?: lifecycle
afterMount?: lifecycle
beforeUnmount?: lifecycle
afterUnmount?: lifecycle
activated?: lifecycle
deactivated?: lifecycle
loadError?: loadErrorHandler
}
type preOptions = baseOptions & {
/** 预执行 */
exec?: boolean
}
type startOptions = baseOptions & {
/** 渲染的容器 */
el: HTMLElement | string
/**
* 路由同步开关
* 如果false,子应用跳转主应用路由无变化,但是主应用的history还是会增加
* https://html.spec.whatwg.org/multipage/history.html#the-history-interface
*/
sync?: boolean
/** 子应用短路径替换,路由同步时生效 */
prefix?: { [key: string]: string }
/** 子应用加载时loading元素 */
loading?: HTMLElement
}
type optionProperty = 'url' | 'el'
/**
* 合并 preOptions 和 startOptions,并且将 url 和 el 变成可选
*/
type cacheOptions = Omit<preOptions & startOptions, optionProperty> & Partial<Pick<startOptions, optionProperty>>
- 详情:
setupApp
设置子应用默认属性,非必须。startApp
、preloadApp
会从这里获取子应用默认属性,如果有相同的属性则会直接覆盖
startApp
-
类型:
Function
-
参数:
startOption
-
返回值:
Promise<Function>
type lifecycle = (appWindow: Window) => any;
type loadErrorHandler = (url: string, e: Error) => any;
type startOption {
/** 唯一性用户必须保证 */
name: string;
/** 需要渲染的 url */
url: string;
/** 需要渲染的 html, 如果用户已有则无需从 url 请求 */
html?: string;
/** 渲染的容器 */
el: HTMLElement | string;
/** 子应用加载时 loading 元素 */
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;
/** 子应用资源加载失败后调用 */
loadError?: loadErrorHandler
};
- 详情:
startApp
启动子应用,异步返回destroy
函数,可以销毁子应用,一般不建议用户调用,除非清楚的理解其作用
警告
一般情况下不需要主动调用destroy
函数去销毁子应用,除非主应用再也不会打开这个子应用了,子应用被主动销毁会导致下次打开该子应用有白屏时间
name
、replace
、fetch
、alive
、degrade
这五个参数在preloadApp
和startApp
中须保持严格一致,否则子应用的渲染可能出现异常
name
-
类型:
String
-
详情: 子应用唯一标识符
技巧
如果主应用上有多个菜单栏用到了子应用的不同页面,在每个页面启动该子应用的时候建议将 name 设置为同一个,这样可以共享一个实例
url
-
类型:
String
-
详情: 子应用的路径地址
- 如果子应用为
单例模式
,改变 url 则可以让子应用跳转到对应子路由 - 如果子应用为
保活模式
,改变 url 则无效,需要采用 通信 的方式对子应用路由进行跳转 - 如果子应用为
重建模式
,改变 url 子应用的路由会跳转对应路由,但是在 路由同步 场景并且子应用的路由同步参数已经同步到主应用 url 上时则无法生效,因为改变 url 后会导致子应用销毁重新渲染,此时如果有同步参数则同步参数的优先级最高
- 如果子应用为
html
-
类型:
String
-
详情: 子应用的 html,设置后子应用将直接读取该值,没有设置则子应用通过 url 请求获取
el
-
类型:
HTMLElement | string
-
详情: 子应用渲染容器,子应用渲染容器的最好设置好宽高防止渲染问题,在 webcomponent 元素上无界还设置了 wujie_iframe 的 class 方便用户自定义样式
loading
-
类型:
HTMLElement
-
详情: 自定义的
loading
元素,如果不想出现默认加载,可以赋值一个空元素:document.createElement('span')
sync
-
默认值:
false
-
类型:
Boolean
-
详情: 路由同步模式,开启后无界会将子应用的 name 作为一个 url 查询参数,实时同步子应用的路径作为这个查询参数的值,这样分享 URL 或者刷新浏览器子应用路由都不会丢失。
警告
这个同步是单向的,只有打开 URL 或者刷新浏览器的时候,子应用才会从 URL 中读回路由,假如关闭路由同步模式,浏览器前进后退可以正常作用到子应用,但是浏览器刷新后子应用的路由会丢失
prefix
-
类型:
{[key: string]: string }
-
详情: 短路径的能力,当子应用开启路由同步模式后,如果子应用链接过长,可以采用短路径替换的方式缩短同步的链接。
alive
-
默认值:
false
-
类型:
Boolean
-
详情:
保活模式,子应用实例
instance
和webcomponent
都不会销毁,子应用的状态和路由都不会丢失,切换子应用只是对webcomponent
的热插拔如果子应用不想做生命周期改造,子应用切换又不想有白屏时间,可以采用保活模式
如果主应用上有多个菜单栏跳转到子应用的不同页面,此时不建议采用保活模式。因为子应用在保活状态下 startApp 无法更改子应用路由,不同菜单栏无法跳转到指定子应用路由,推荐
单例模式
技巧
预执行模式结合保活模式可以实现类似 ssr 的效果,包括页面数据的请求和渲染全部提前完成,用户可以瞬间打开子应用
props
-
类型:
{ [key: string]: any }
-
详情: 注入给子应用的数据
fiber
-
默认值:
true
-
类型:
Boolean
-
详情:
js 的执行模式,由于子应用的执行会阻塞主应用的渲染线程,当设置为 true 时 js 采取类似 react fiber 的模式方式间断执行,每个 js 文件的执行都包裹在 requestidlecallback 中,每执行一个 js 可以返回响应外部的输入,但是这个颗粒度是 js 文件,如果子应用单个 js 文件过大,可以通过拆包的方式降低达到 fiber 模式效益最大化
技巧
打开主应用就需要加载的子应用可以将 fiber 设置为 false 来加快加载速度
其他场景建议采用默认值
degrade
-
默认值:
false
-
类型:
Boolean
-
详情:
主动降级设置,无界方案采用了 proxy 和 webcomponent 等技术,在有些浏览器上可能出现不兼容的情况,此时无界会自动进行降级,采用一个的 iframe 替换 webcomponent,用 Object.defineProperty 替换 proxy,理论上可以兼容到 IE 9,但是用户也可以将 degrade 设置为 true 来主动降级
警告
一旦采用降级方案,弹窗由于在 iframe 内部将无法覆盖整个应用
attrs
-
类型:
{ [key: string]: any }
-
详情: 自定义 iframe 属性,子应用运行在 iframe 内,attrs 可以允许用户自定义 iframe 的属性
replace
-
类型:
(codeText: string) => string
-
详情: 全局代码替换钩子
技巧
replace 函数可以在运行时处理子应用的代码,如果子应用不方便修改代码,可以在这里进行代码替换,子应用的 html、js、css 代码均会做替换
fetch
-
类型:
(input: RequestInfo, init?: RequestInit) => Promise<Response>
-
详情: 自定义 fetch,添加自定义 fetch 后,子应用的静态资源请求和采用了 fetch 的接口请求全部会走自定义 fetch
技巧
对于需要携带 cookie 的请求,可以采用自定义 fetch 方式实现:(url, options) => window.fetch(url, { …options, credentials: “include” })
plugins
- 类型:
Array<plugin>
interface ScriptObjectLoader {
/** 脚本地址,内联为空 */
src?: string
/** 脚本是否为 module 模块 */
module?: boolean
/** 脚本是否为 async 执行 */
async?: boolean
/** 脚本是否设置 crossorigin */
crossorigin?: boolean
/** 脚本 crossorigin 的类型 */
crossoriginType?: 'anonymous' | 'use-credentials' | ''
/** 内联 script 的代码 */
content?: string
/** 执行回调钩子 */
callback?: (appWindow: Window) => any
}
interface StyleObjectLoader {
/** 样式地址, 内联为空 */
src?: string
/** 样式代码 */
content?: string
}
type eventListenerHook = (
iframeWindow: Window,
type: string,
handler: EventListenerOrEventListenerObject,
options?: boolean | AddEventListenerOptions
) => void
interface plugin {
/** 处理 html 的 loader */
htmlLoader?: (code: string) => string
/** js 排除列表 */
jsExcludes?: Array<string | RegExp>
/** js 忽略列表 */
jsIgnores?: Array<string | RegExp>
/** 处理 js 加载前的 loader */
jsBeforeLoaders?: Array<ScriptObjectLoader>
/** 处理 js 的 loader */
jsLoader?: (code: string, url: string, base: string) => string
/** 处理 js 加载后的 loader */
jsAfterLoaders?: Array<ScriptObjectLoader>
/** css 排除列表 */
cssExcludes?: Array<string | RegExp>
/** css 忽略列表 */
cssIgnores?: Array<string | RegExp>
/** 处理 css 加载前的 loader */
cssBeforeLoaders?: Array<StyleObject>
/** 处理 css 的 loader */
cssLoader?: (code: string, url: string, base: string) => string
/** 处理 css 加载后的 loader */
cssAfterLoaders?: Array<StyleObject>
/** 子应用 window addEventListener 钩子回调 */
windowAddEventListenerHook?: eventListenerHook
/** 子应用 window removeEventListener 钩子回调 */
windowRemoveEventListenerHook?: eventListenerHook
/** 子应用 document addEventListener 钩子回调 */
documentAddEventListenerHook?: eventListenerHook
/** 子应用 document removeEventListener 钩子回调 */
documentRemoveEventListenerHook?: eventListenerHook
/** 子应用 向 body、head 插入元素后执行的钩子回调 */
appendOrInsertElementHook?: <T extends Node>(element: T, iframeWindow: Window) => void
/** 子应用劫持元素的钩子回调 */
patchElementHook?: <T extends Node>(element: T, iframeWindow: Window) => void
/** 用户自定义覆盖子应用 window 属性 */
windowPropertyOverride?: (iframeWindow: Window) => void
/** 用户自定义覆盖子应用 document 属性 */
documentPropertyOverride?: (iframeWindow: Window) => void
}
- 详情: 无界插件,在运行时动态的修改子应用代理。
beforeLoad
-
类型:
(appWindow: Window) => any
-
详情: 生命周期钩子,加载子应用前调用
beforeMount
-
类型:
(appWindow: Window) => any
-
详情: 生命周期钩子,子应用 mount 之前调用
afterMount
-
类型:
(appWindow: Window) => any
-
详情: 生命周期钩子,子应用 mount 之后调用
beforeUnmount
-
类型:
(appWindow: Window) => any
-
详情: 生命周期钩子,子应用 unmount 之前调用
afterUnmount
-
类型:
(appWindow: Window) => any
-
详情: 生命周期钩子,子应用 unmount 之后调用
activated
-
类型:
(appWindow: Window) => any
-
详情: 生命周期钩子,保活子应用进入时触发
deactivated
-
类型:
(appWindow: Window) => any
-
详情: 生命周期钩子,保活子应用离开时触发
loadError
-
类型:
(url: string, e: Error) => any
-
详情: 生命周期钩子,子应用加载资源失败后触发
注意
如果子应用没有做生命周期改造,beforeMount、afterMount、beforeUnmount、afterUnmount 这四个生命周期都不会调用,非保活子应用 activated、deactivated 这两个生命周期不会调用
preloadApp
-
类型:
Function
-
参数:
preOption
type lifecycle = (appWindow: Window) => any;
type loadErrorHandler = (url: string, e: Error) => any;
type preOptions {
/** 唯一性用户必须保证 */
name: string;
/** 需要渲染的 url */
url: string;
/** 需要渲染的 html, 如果用户已有则无需从 url 请求 */
html?: string;
/** 注入给子应用的数据 */
props?: { [key: string]: any };
/** 自定义运行 iframe 的属性 */
attrs?: { [key: string]: any };
/** 自定义降级渲染 iframe 的属性 */
degradeAttrs?: { [key: string]: any };
/** 代码替换钩子 */
replace?: (code: string) => string;
/** 自定义 fetch,资源和接口 */
fetch?: (input: RequestInfo, init?: RequestInit) => Promise<Response>;
/** 子应用保活模式,state 不会丢失 */
alive?: boolean;
/** 预执行模式 */
exec?: boolean;
/** js 采用 fiber 模式执行 */
fiber?: boolean;
/** 子应用采用降级 iframe 方案 */
degrade?: boolean;
/** 子应插件 */
plugins: Array<plugin>;
/** 子应用生命周期 */
beforeLoad?: lifecycle;
/** 没有做生命周期改造的子应用不会调用 */
beforeMount?: lifecycle;
afterMount?: lifecycle;
beforeUnmount?: lifecycle;
afterUnmount?: lifecycle;
/** 非保活应用不会调用 */
activated?: lifecycle;
deactivated?: lifecycle;
/** 子应用资源加载失败后调用 */
loadError?: loadErrorHandler
};
- 详情: 预加载可以极大的提升子应用首次打开速度
警告
- 资源的预加载会占用主应用的网络线程池
- 资源的预执行会阻塞主应用的渲染线程
name
、replace
、fetch
、alive
、degrade
这五个参数在preloadApp
和startApp
中须保持严格一致,否则子应用的渲染可能出现异常
name
-
类型:
String
-
详情: 子应用唯一标识符
技巧
如果主应用上有多个菜单栏用到了子应用的不同页面,在每个页面启动该子应用的时候建议将 name 设置为同一个,这样可以共享一个实例
url
-
类型:
String
-
详情: 子应用的路径地址
html
-
类型:
String
-
详情: 子应用的 html,设置后子应用将直接读取该值,没有设置则子应用通过 url 请求获取
props
-
类型:
{ [key: string]: any }
-
详情: 注入给子应用的数据
警告
exec
为true
此时子应用代码会预执行,如果子应用运行依赖props
的数据则须传入props
或者子应用做好兼容props
不存在,否则子应用运行可能报错
attrs
-
类型:
{ [key: string]: any }
-
详情: 自定义
iframe
属性,子应用运行在iframe
内,attrs
可以允许用户自定义iframe
的属性
replace
-
类型:
(codeText: string) => string
-
详情: 全局代码替换钩子
技巧
replace 函数可以在运行时处理子应用的代码,如果子应用不方便修改代码,可以在这里进行代码替换,子应用的 html、js、css 代码均会做替换
fetch
-
类型:
(input: RequestInfo, init?: RequestInit) => Promise<Response>
-
详情: 自定义 fetch,添加自定义 fetch 后,子应用的静态资源请求和采用了 fetch 的接口请求全部会走自定义 fetch
技巧
对于需要携带 cookie 的请求,可以采用自定义 fetch 方式实现:
(url, options) => window.fetch(url, { ...options, credentials: "include" })
alive
-
默认值:
false
-
类型:
Boolean
-
详情:
保活模式,子应用实例instance
和webcomponent
都不会销毁,子应用的状态和路由都不会丢失,切换子应用只是对webcomponent
和容器的热插拔如果子应用不想做生命周期改造,子应用切换又不想有白屏时间,可以采用保活模式
如果主应用上有多个菜单栏跳转到子应用的不同页面,此时不建议采用保活模式。因为子应用在保活状态下
startApp
无法更改子应用路由,不同菜单栏无法跳转到指定子应用路由,推荐单例模式技巧
预执行模式结合保活模式可以实现类似 ssr 的效果,包括页面数据的请求和渲染全部提前完成,用户可以瞬间打开子应用
exec
-
默认值:
false
-
类型:
Boolean
-
详情: 预执行模式,为 false 时只会预加载子应用的资源,为 true 时会预执行子应用代码,极大的加快子应用打开速度
fiber
-
默认值:
true
-
类型:
Boolean
-
详情:
js 的执行模式,由于子应用的执行会阻塞主应用的渲染线程,当设置为 true 时 js 采取类似
react fiber
的模式方式间断执行,每个 js 文件的执行都包裹在requestidlecallback
中,每执行一个 js 可以返回响应外部的输入,但是这个颗粒度是 js 文件,如果子应用单个 js 文件过大,可以通过拆包的方式降低达到fiber
模式效益最大化技巧
打开主应用就需要加载的子应用可以将
fiber
设置为 false 来加快加载速度其他场景建议采用默认值
degrade
-
默认值:
false
-
类型:
Boolean
-
详情:
主动降级设置,无界方案采用了
proxy
和webcomponent
等技术,在有些浏览器上可能出现不兼容的情况,此时无界会自动进行降级,采用一个的iframe
替换webcomponent
,用Object.defineProperty
替换 proxy,理论上可以兼容到 IE 9,但是用户也可以将degrade
设置为 true 来主动降级警告
一旦采用降级方案,弹窗由于在 iframe 内部将无法覆盖整个应用
plugins
- 类型:
Array<plugin>
interface ScriptObjectLoader {
/** 脚本地址,内联为空 */
src?: string
/** 脚本是否为 module 模块 */
module?: boolean
/** 脚本是否为 async 执行 */
async?: boolean
/** 脚本是否设置 crossorigin */
crossorigin?: boolean
/** 脚本 crossorigin 的类型 */
crossoriginType?: 'anonymous' | 'use-credentials' | ''
/** 内联 script 的代码 */
content?: string
/** 执行回调钩子 */
callback?: (appWindow: Window) => any
}
interface StyleObjectLoader {
/** 样式地址, 内联为空 */
src?: string
/** 样式代码 */
content?: string
}
type eventListenerHook = (
iframeWindow: Window,
type: string,
handler: EventListenerOrEventListenerObject,
options?: boolean | AddEventListenerOptions
) => void
interface plugin {
/** 处理 html 的 loader */
htmlLoader?: (code: string) => string
/** js 排除列表 */
jsExcludes?: Array<string | RegExp>
/** js 忽略列表 */
jsIgnores?: Array<string | RegExp>
/** 处理 js 加载前的 loader */
jsBeforeLoaders?: Array<ScriptObjectLoader>
/** 处理 js 的 loader */
jsLoader?: (code: string, url: string, base: string) => string
/** 处理 js 加载后的 loader */
jsAfterLoaders?: Array<ScriptObjectLoader>
/** css 排除列表 */
cssExcludes?: Array<string | RegExp>
/** css 忽略列表 */
cssIgnores?: Array<string | RegExp>
/** 处理 css 加载前的 loader */
cssBeforeLoaders?: Array<StyleObject>
/** 处理 css 的 loader */
cssLoader?: (code: string, url: string, base: string) => string
/** 处理 css 加载后的 loader */
cssAfterLoaders?: Array<StyleObject>
/** 子应用 window addEventListener 钩子回调 */
windowAddEventListenerHook?: eventListenerHook
/** 子应用 window removeEventListener 钩子回调 */
windowRemoveEventListenerHook?: eventListenerHook
/** 子应用 document addEventListener 钩子回调 */
documentAddEventListenerHook?: eventListenerHook
/** 子应用 document removeEventListener 钩子回调 */
documentRemoveEventListenerHook?: eventListenerHook
/** 子应用 向 body、head 插入元素后执行的钩子回调 */
appendOrInsertElementHook?: <T extends Node>(element: T, iframeWindow: Window) => void
/** 子应用劫持元素的钩子回调 */
patchElementHook?: <T extends Node>(element: T, iframeWindow: Window) => void
/** 用户自定义覆盖子应用 window 属性 */
windowPropertyOverride?: (iframeWindow: Window) => void
/** 用户自定义覆盖子应用 document 属性 */
documentPropertyOverride?: (iframeWindow: Window) => void
}
- 详情: 无界插件,在运行时动态的修改子应用代理。
beforeLoad
-
类型:
(appWindow: Window) => any
-
详情: 生命周期钩子,加载子应用前调用
beforeMount
-
类型:
(appWindow: Window) => any
-
详情: 生命周期钩子,子应用 mount 之前调用
afterMount
-
类型:
(appWindow: Window) => any
-
详情: 生命周期钩子,子应用 mount 之后调用
beforeUnmount
-
类型:
(appWindow: Window) => any
-
详情: 生命周期钩子,子应用 unmount 之前调用
afterUnmount
-
类型:
(appWindow: Window) => any
-
详情: 生命周期钩子,子应用 unmount 之后调用
activated
-
类型:
(appWindow: Window) => any
-
详情: 生命周期钩子,保活子应用进入时触发
deactivated
-
类型:
(appWindow: Window) => any
-
详情: 生命周期钩子,保活子应用离开时触发
loadError
-
类型:
(url: string, e: Error) => any
-
详情: 生命周期钩子,子应用加载资源失败后触发
注意
如果子应用没有做生命周期改造,beforeMount、afterMount、beforeUnmount、afterUnmount 这四个生命周期都不会调用,非保活子应用 activated、deactivated 这两个生命周期不会调用
destroyApp
-
类型:
Function
-
参数:
string
,子应用name
-
返回值:
void
主动销毁子应用,承载子应用的 iframe
和 shadowRoot
都会被销毁,无界实例也会被销毁,相当于所有的缓存都被清空,除非后续不会再使用子应用,否则都不应该主动销毁。
子应用
全局变量
无界会在子应用的window对象
中注入一些全局变量:
declare global {
interface Window {
// 是否存在无界
__POWERED_BY_WUJIE__?: boolean;
// 子应用公共加载路径
__WUJIE_PUBLIC_PATH__: string;
// 原生的querySelector
__WUJIE_RAW_DOCUMENT_QUERY_SELECTOR__: typeof Document.prototype.querySelector;
// 原生的querySelectorAll
__WUJIE_RAW_DOCUMENT_QUERY_SELECTOR_ALL__: typeof Document.prototype.querySelectorAll;
// 原生的window对象
__WUJIE_RAW_WINDOW__: Window;
// 子应用沙盒实例
__WUJIE: WuJie;
// 子应用mount函数
__WUJIE_MOUNT: () => void;
// 子应用unmount函数
__WUJIE_UNMOUNT: () => void;
// 注入对象
$wujie: {
bus: EventBus;
shadowRoot?: ShadowRoot;
props?: { [key: string]: any };
location?: Object;
};
}
}
window.__POWERED_BY_WUJIE__
是否存在无界。
返回值:Boolean
window.__WUJIE_PUBLIC_PATH__
子应用公共加载路径
返回值:String
window.__WUJIE_RAW_DOCUMENT_QUERY_SELECTOR__
原生的 querySelector
返回值:typeof Document.prototype.querySelector
window.__WUJIE_RAW_DOCUMENT_QUERY_SELECTOR_ALL__
原生的 querySelectorAll
返回值:typeof Document.prototype.querySelectorAll
window.__WUJIE_RAW_WINDOW__
原生的 window 对象
返回值:Window 对象
window.__WUJIE
子应用沙盒实例
返回值:WuJie 实例
window.__WUJIE_MOUNT
子应用 mount 函数
返回值:() => void
window.__WUJIE_UNMOUNT
子应用 unmount 函数
返回值:() => void
window.$wujie
无界对子应用注入了 w u j i e 对象,可以通过 wujie对象,可以通过 wujie对象,可以通过wujie 或者 window.$wujie 获取
- 类型:
{ bus: EventBus; shadowRoot?: ShadowRoot; props?: { [key: string]: any }; location?: Object; }
window.$wujie.bus
同 bus
window.$wujie.shadowRoot
- 类型:
ShadowRoot
子应用的渲染容器 shadow DOM
window.$wujie.props
- 类型:
{ [key: string]: any }
主应用注入的数据
window.$wujie.location
-
类型:
Object
-
由于子应用的
location.host
拿到的是主应用的host
,无界提供了一个正确的location
挂载到挂载到$wujie 上 -
当采用
vite
编译框架时,由于script
的标签type
为module
,所以无法采用闭包的方式将location
劫持代理,子应用所有采用window.location.host
的代码需要统一修改成$wujie.location.host
-
当子应用发生降级时,由于
proxy
无法正常工作导致location
无法代理,子应用所有采用window.location.host
的代码需要统一修改成$wujie.location.host
-
当采用非
vite
编译框架时,proxy
代理了window.location
,子应用代码无需做任何更改。
公共使用
bus
- 类型:
EventBus
type callback = (...args: Array<any>) => any
export declare class EventBus {
private id
private eventObj
constructor(id: string)
$on(event: string, fn: callback): EventBus
/** 任何$emit都会导致监听函数触发,第一个参数为事件名,后续的参数为$emit的参数 */
$onAll(fn: (event: string, ...args: Array<any>) => any): EventBus
$once(event: string, fn: callback): void
$off(event: string, fn: callback): EventBus
$offAll(fn: callback): EventBus
$emit(event: string, ...args: Array<any>): EventBus
$clear(): EventBus
}
- 详情: 去中心化的事件平台,类 Vue 的事件 api,支持链式调用。
$on
-
类型:
(event: string, fn: callback) => EventBus
-
参数:
{string} event
事件名{callback} fn
回调参数
-
详情: 监听事件并提供回调
$onAll
-
类型:
(fn: (event: string, ...args: Array<any>) => any) => EventBus
-
参数:
{callback} fn
回调参数
-
详情: 监听所有事件并提供回调,回调函数的第一个参数是事件名
$once
-
类型:
(event: string, fn: callback) => void
-
参数:
{string} event
事件名{callback} fn
回调参数
-
详情: 一次性的监听事件
$off
-
类型:
(event: string, fn: callback) => EventBus
-
参数:
{string} event
事件名{callback} fn
回调参数
-
详情: 取消事件监听
$offAll
-
类型:
(fn: callback) => EventBus
-
参数:
{callback} fn
回调参数
-
详情: 取消监听所有事件
$emit
-
类型:
(event: string, ...args: Array<any>) => EventBus
-
参数:
{string} event
事件名{Array<any>} args
其他回调参数
-
详情: 触发事件
$clear
-
类型: Function
-
详情: 清空 EventBus 实例下所有监听事件
警告
子应用在被销毁或者重新渲染(非保活状态)时框架会自动调用清空上次渲染所有的订阅事件
子应用内部组件的渲染可能导致反复订阅(比如在 mounted 生命周期调用 w u j i e . b u s . wujie.bus. wujie.bus.on),需要用户在 unmount 生命周期内手动调用 w u j i e . b u s . wujie.bus. wujie.bus.off 来取消订阅
原理
沙箱机制
应用加载机制和 js 沙箱机制
将子应用的js
注入主应用同域的iframe
中运行。
iframe
是一个原生的window
沙箱,内部有完整的history
和location
接口,子应用实例instance
运行在iframe
中,路由也彻底和主应用解耦,可以直接在业务组件里面启动应用。
收益
天然 js 沙箱,不会污染主应用环境
不用修改主应用window
任何属性,只在iframe
内部进行修改(注意点:无界的js沙箱是借助iframe
实现)应用切换没有清理成本
由于不污染主应用,子应用销毁也无需做任何清理工作
iframe 连接机制 和 css 沙箱机制
无界采用webcomponent来实现页面的样式隔离。
无界会创建一个wujie
自定义元素,然后将子应用的完整结构渲染在内部。子应用的实例instance
在iframe
内运行,dom
在主应用容器下的webcomponent
内,通过代理 iframe
的document
到webcomponent
,可以实现两者的互联。
将document
的查询类接口:getElementsByTagName、getElementsByClassName、getElementsByName、getElementById、querySelector、querySelectorAll、head、body
全部代理到webcomponent
,这样instance
和webcomponent
就精准的链接起来。
当子应用发生切换,iframe
保留下来,子应用的容器可能销毁,但webcomponent
依然可以选择保留,这样等应用切换回来将webcomponent
再挂载回容器上,子应用可以获得类似vue
的keep-alive
的能力.
收益
天然 css 沙箱
直接物理隔离,样式隔离子应用不用做任何修改(注意点:无界的css沙箱是借助webcomponent
实现)完整的 DOM 结构
webcomponent
保留了子应用完整的html
结构,样式和结构完全对应,子应用不用做任何修改天然适配弹窗问题
document.body
的appendChild
或者insertBefore
会代理直接插入到webcomponent
,子应用不用做任何改造(注意点:像子应用弹窗这类最外层的DOM,依然是处于容器之内)子应用保活
子应用保留iframe
和webcomponent
,应用内部的state
不会丢失(注意点:子应用的保活是由于保留了DOM)
路由同步机制
在iframe
内部进行history.pushState
,浏览器会自动的在joint session history中添加iframe
的session-history,浏览器的前进、后退在不做任何处理的情况就可以直接作用于子应用
劫持iframe
的history.pushState
和history.replaceState
,就可以将子应用的url
同步到主应用的query
参数上,当刷新浏览器初始化iframe
时,读回子应用的url
并使用iframe
的history.replaceState
进行同步
收益
- 浏览器刷新、前进、后退都可以作用到子应用
- 多应用同时激活时也能保持路由同步
- 实现成本低,无需复杂的监听来处理同步问题
小结
无界的优势
-
组件式的使用方式
无需注册,更无需路由适配,在组件内使用,跟随组件装载、卸载。 -
应用级别的 keep-alive
子应用开启保活模式
后,应用发生切换时整个子应用的状态可以保存下来不丢失,结合预执行模式
可以获得类似ssr
的打开体验 -
多应用同时激活在线
框架具备同时激活多应用,并保持这些应用路由同步的能力 -
纯净无污染
- 无界利用
iframe
和webcomponent
来搭建天然的js
隔离沙箱和css
隔离沙箱 - 利用
iframe
的history
和主应用的history
在同一个top-level browsing context来搭建天然的路由同步机制 - 副作用局限在沙箱内部,子应用切换无需任何清理工作,没有额外的切换成本
- 无界利用
-
性能和体积兼具
- 子应用执行性能和原生一致,子应用实例
instance
运行在iframe
的window
上下文中,避免with(proxyWindow){code}
这样指定代码执行上下文导致的性能下降,但是多了实例化iframe
的一次性的开销,可以通过preload
提前实例化 - 体积比较轻量,借助
iframe
和webcomponent
来实现沙箱,有效的减小了代码量
- 子应用执行性能和原生一致,子应用实例