微前端无界方案

微前端无界


无界

官方文档




主应用

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、设置子应用

【非必须】由于 preloadAppstartApp 的参数重复,为了避免重复输入,可以通过 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的一致。

不同之处在于startApphtmlel,没有widthheight

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 时进入保活模式,内部的数据和路由的状态不会随着页面切换而丢失。

在保活模式下,子应用只会进行一次渲染,页面发生切换时承载子应用 domwebcomponent 会保留在内存中,当子应用重新激活时无界会将内存中的 webcomponent 重新挂载到容器上。

注意:

  1. 保活模式下改变 url 子应用的路由不会发生变化,需要采用 通信 的方式对子应用路由进行跳转。
  2. 保活的子应用的实例不会销毁,子应用被切走了也可以响应 bus 事件,非保活的子应用切走了监听的事件也会全部销毁,需要等下次重新 mount 后重新监听。
单例模式

子应用的 alivefalse 且进行了生命周期改造时进入单例模式

子应用页面如果切走,会调用 window.__WUJIE_UNMOUNT 销毁子应用当前实例,子应用页面如果切换回来,会调用 window.__WUJIE_MOUNT 渲染子应用新的实例。

在单例式下,改变 url 子应用的路由会发生跳转到对应路由。

注意:
如果主应用上有多个菜单栏用到了子应用的不同页面,在每个页面启动该子应用的时候将 name 设置为同一个,这样可以共享一个 wujie 实例,承载子应用 jsiframe 也实现了共享,不同页面子应用的 url 不同,切换这个子应用的过程相当于:销毁当前应用实例 => 同步新路由 => 创建新应用实例


重建模式(一般不使用,非常消耗资源)

子应用既没有设置为保活模式,也没有进行生命周期的改造则进入了重建模式,每次页面切换不仅会销毁承载子应用 domwebcomponent,还会销毁承载子应用 jsiframe,相应的 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 通信

由于子应用运行的 iframesrc 和主应用是同域的,所以相互可以直接通信

主应用调用子应用的全局数据:

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

  1. 子应用 B 开启路由同步能力
  2. 子应用的点击跳转函数:
// 子应用 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 的指定路由
  1. 主应用 的 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>
  1. 子应用 B 开启路由同步能力

  2. 子应用的点击跳转函数:

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之前做:

  1. 在子应用运行一个src="http://xxxxx"的脚本
  2. 在子应用中运行一个内联的 js 脚本<script>content</script>
  3. 执行一个回调函数

那么这些工作可以放置在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之后做:

  1. 在子应用运行一个src="http://xxxxx"的脚本
  2. 在子应用中运行一个内联的 js 脚本<script>content</script>
  3. 执行一个回调函数

那么这些工作可以放置在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之前做:

  1. 插入一个src="http://xxxxx"的外联样式脚本
  2. 插入一个<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之后做:

  1. 插入一个src="http://xxxxx"的外联样式脚本
  2. 插入一个<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中,jsiframe中运行,往往子应用在外部的容器滚动,所以监听windowscroll事件是无效的,可以将处理windowscroll事件绑定在滚动容器中

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中,jsiframe中运行,往往子应用在外部的容器滚动,所以监听documentscroll事件是无效的,可以将处理documentscroll事件绑定在滚动容器中

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

子应用往bodyhead插入元素后执行的回调函数

  • 示例
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 )
    }
  },
];




降级处理

无界提供无感知的降级方案

在非降级场景下,子应用的domwebcomponent中,运行环境在iframe中,iframedom的操作通过proxy来代理到webcomponent上,而webcomponentproxy IE都无法支持,这里采用另一个的iframe替换webcomponent,用Object.defineProperty替换proxy来做代理的方案

注意

无界并没有对 es6 代码进行 polyfill,因为每个用户对浏览器的兼容程度是不一样的引入的 polyfill 也不一致,如果需要在较低版本的浏览器中运行,需要用户自行 通过 babel 来添加 polyfill。

优点:

  1. 降级的行为由框架判断,当浏览器不支持时自动降级
  2. 降级后,应用之间也保证了绝对的隔离度
  3. 代码无需做任何改动,之前的预加载、保活还有通信的代码都生效,用户不需要为了降级做额外的代码改动导致降级前后运行的代码不一致
  4. 用户也可以强制降级,比如说当前浏览器对 webcomponentproxy 是支持的,但是用户还是想将 dom 运行在 iframe 中,就可以将 degrade 设置为 true

缺点:

  1. 弹窗只能在子应用内部
  2. 由于无法使用 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设置子应用默认属性,非必须。startApppreloadApp 会从这里获取子应用默认属性,如果有相同的属性则会直接覆盖




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函数去销毁子应用,除非主应用再也不会打开这个子应用了,子应用被主动销毁会导致下次打开该子应用有白屏时间
namereplacefetchalivedegrade这五个参数在preloadAppstartApp中须保持严格一致,否则子应用的渲染可能出现异常

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

  • 详情:

    保活模式,子应用实例 instancewebcomponent 都不会销毁,子应用的状态和路由都不会丢失,切换子应用只是对 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
};
  • 详情: 预加载可以极大的提升子应用首次打开速度

警告

  • 资源的预加载会占用主应用的网络线程池
  • 资源的预执行会阻塞主应用的渲染线程
  • namereplacefetchalivedegrade 这五个参数在 preloadAppstartApp 中须保持严格一致,否则子应用的渲染可能出现异常
name
  • 类型: String

  • 详情: 子应用唯一标识符

    技巧

    如果主应用上有多个菜单栏用到了子应用的不同页面,在每个页面启动该子应用的时候建议将 name 设置为同一个,这样可以共享一个实例

url
  • 类型: String

  • 详情: 子应用的路径地址

html
  • 类型: String

  • 详情: 子应用的 html,设置后子应用将直接读取该值,没有设置则子应用通过 url 请求获取

props
  • 类型: { [key: string]: any }

  • 详情: 注入给子应用的数据

    警告

    exectrue此时子应用代码会预执行,如果子应用运行依赖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

  • 详情:
    保活模式,子应用实例 instancewebcomponent 都不会销毁,子应用的状态和路由都不会丢失,切换子应用只是对 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

  • 详情:

    主动降级设置,无界方案采用了 proxywebcomponent 等技术,在有些浏览器上可能出现不兼容的情况,此时无界会自动进行降级,采用一个的 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

主动销毁子应用,承载子应用的 iframeshadowRoot 都会被销毁,无界实例也会被销毁,相当于所有的缓存都被清空,除非后续不会再使用子应用,否则都不应该主动销毁。




子应用

全局变量

无界会在子应用的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 的标签 typemodule,所以无法采用闭包的方式将 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沙箱,内部有完整的historylocation接口,子应用实例instance运行在iframe中,路由也彻底和主应用解耦,可以直接在业务组件里面启动应用。

收益

  • 天然 js 沙箱,不会污染主应用环境
    不用修改主应用window任何属性,只在iframe内部进行修改(注意点:无界的js沙箱是借助iframe实现)

  • 应用切换没有清理成本
    由于不污染主应用,子应用销毁也无需做任何清理工作


iframe 连接机制 和 css 沙箱机制

无界采用webcomponent来实现页面的样式隔离。

无界会创建一个wujie自定义元素,然后将子应用的完整结构渲染在内部。子应用的实例instanceiframe内运行,dom在主应用容器下的webcomponent内,通过代理 iframedocumentwebcomponent,可以实现两者的互联。

document的查询类接口:getElementsByTagName、getElementsByClassName、getElementsByName、getElementById、querySelector、querySelectorAll、head、body全部代理到webcomponent,这样instancewebcomponent就精准的链接起来。

当子应用发生切换,iframe保留下来,子应用的容器可能销毁,但webcomponent依然可以选择保留,这样等应用切换回来将webcomponent再挂载回容器上,子应用可以获得类似vuekeep-alive的能力.

收益

  • 天然 css 沙箱
    直接物理隔离,样式隔离子应用不用做任何修改(注意点:无界的css沙箱是借助webcomponent实现)

  • 完整的 DOM 结构
    webcomponent保留了子应用完整的html结构,样式和结构完全对应,子应用不用做任何修改

  • 天然适配弹窗问题
    document.bodyappendChild或者insertBefore会代理直接插入到webcomponent,子应用不用做任何改造(注意点:像子应用弹窗这类最外层的DOM,依然是处于容器之内)

  • 子应用保活
    子应用保留iframewebcomponent,应用内部的state不会丢失(注意点:子应用的保活是由于保留了DOM)



路由同步机制

iframe内部进行history.pushState,浏览器会自动的在joint session history中添加iframe的session-history,浏览器的前进、后退在不做任何处理的情况就可以直接作用于子应用

劫持iframehistory.pushStatehistory.replaceState,就可以将子应用的url同步到主应用的query参数上,当刷新浏览器初始化iframe时,读回子应用的url并使用iframehistory.replaceState进行同步

收益

  • 浏览器刷新、前进、后退都可以作用到子应用
  • 多应用同时激活时也能保持路由同步
  • 实现成本低,无需复杂的监听来处理同步问题




小结

无界的优势

  • 组件式的使用方式
    无需注册,更无需路由适配,在组件内使用,跟随组件装载、卸载。

  • 应用级别的 keep-alive
    子应用开启保活模式后,应用发生切换时整个子应用的状态可以保存下来不丢失,结合预执行模式可以获得类似ssr的打开体验

  • 多应用同时激活在线
    框架具备同时激活多应用,并保持这些应用路由同步的能力

  • 纯净无污染

    • 无界利用iframewebcomponent来搭建天然的js隔离沙箱和css隔离沙箱
    • 利用iframehistory和主应用的history在同一个top-level browsing context来搭建天然的路由同步机制
    • 副作用局限在沙箱内部,子应用切换无需任何清理工作,没有额外的切换成本
  • 性能和体积兼具

    • 子应用执行性能和原生一致,子应用实例instance运行在iframewindow上下文中,避免with(proxyWindow){code}这样指定代码执行上下文导致的性能下降,但是多了实例化iframe的一次性的开销,可以通过 preload 提前实例化
    • 体积比较轻量,借助iframewebcomponent来实现沙箱,有效的减小了代码量

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/622210.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

软件需求工程习题

1.&#xff08;面谈&#xff09;是需求获取活动中发生的需求工程师和用户间面对面的会见。 2.使用原型法进行需求获取&#xff0c;&#xff08;演化式&#xff09;原型必须具有健壮性&#xff0c;代码质量要从一开始就能达到最终系统的要求 3.利用面谈进行需求获取时&#xf…

提升文本到图像模型的空间一致性:SPRIGHT数据集与训练技术的新进展

当前的T2I模型&#xff0c;如Stable Diffusion和DALL-E&#xff0c;虽然在生成高分辨率、逼真图像方面取得了成功&#xff0c;但在空间一致性方面存在不足。这些模型往往无法精确地按照文本提示中描述的空间关系来生成图像。为了解决这一问题&#xff0c;研究人员进行了深入分析…

长难句打卡 5.13

And in Europe, some are up in arms over a proposal to drop a specific funding category for social-science research and to integrate it within cross-cutting topics of sustainable development. 在欧洲&#xff0c;有些人正竭力反对一项“终止专用于社会科学研究的…

rac asm新增磁盘报0RA-15333或ORA-15075

虚拟化做的rac&#xff0c;发现原来加盘直接把sdb、sdc、sdd、sde加到asm里了&#xff0c;后面通过udev绑定的盘&#xff0c;增加到asm里就报错&#xff1a; [DBT-30007]Addition of disks to disk group DATA failed ORA-15032:not all alterations performed 0RA-15333: d…

05、 java 的三种注释及 javadoc 命令解析文档注释(即:java 特有注释方式)的过程

java的三种注释 1、单行注释&#xff1a;其一、代码展示&#xff1a;其二、特点&#xff1a; 2、多行注释&#xff1a;其一、代码展示&#xff1a;其二、特点&#xff1a; 3、文档注释(java特有)&#xff1a;其一、代码展示&#xff1a;其二、注释文档的使用&#xff1a;其三、…

flink cdc,读取datetime类型

:flink cdc&#xff0c;读取datetime类型&#xff0c;全都变成了时间戳 Flink CDC读取MySQL的datetime类型时会转换为时间戳的问题&#xff0c;可以通过在Flink CDC任务中添加相应的转换器来解决。具体来说&#xff0c;可以在MySQL数据源的debezium.source.converter配置项中指…

电商平台接口自动化框架实践||电商API数据采集接口

电商数据采集接口 语言&#xff1a;python 接口自动化实现流程 红色为可实现/尚未完成 绿色为需要人工干预部分 自动生成测试用例模板&#xff08;俩种方式二选一&#xff09;&#xff1a; mimproxy&#xff0c;通过浏览器代理抓包方式&#xff0c;访问 H5 或者 web 页面&a…

电脑快速搜索文件及文件夹软件——Everything

一、前言 Everything是一款由voidtools开发的文件搜索工具&#xff0c;主要运行于Windows操作系统上。它的主要功能是快速、高效地搜索电脑上的文件和文件夹名称。Everything通过利用NTFS文件系统的MFT&#xff08;主文件表&#xff09;来索引文件&#xff0c;从而实现几乎实时…

大型医疗挂号微服务“马上好医”医疗项目(4)设计一个医院方接口

如何构建一个医院方接口 一、如何进行数据库建模 数据库建模一般需要使用工具PowerDesign&#xff0c;但是其实在navicat中是有类似的功能的 二、分析医院接口会有什么字段 其实很多的同学在入行的时候会有一个问题&#xff0c;没有设计思维。 表字段的设计方案 状态字段…

如何写好网评文章?写好了怎么去投稿呢,教程来了

如何写好网评文章&#xff0c;可谓仁者见仁、智者见智。俗话说&#xff1a;“冰冻三尺非一日之寒。”写好网评文章决不是一朝一夕能够练成的&#xff0c;是一个漫长的修炼的过程&#xff0c;需要我们耐得住寂寞、静得下心神。从事网评写作六年多&#xff0c;我有一些心得体会和…

51cto已购买的视频怎么下载到本地

你是否曾在学习51CTO的精品课程时&#xff0c;希望可以随时随地无网络干扰地进行学习&#xff0c;或是想要将这些已购买的课程永久珍藏&#xff1f;今天&#xff0c;你的愿望将要实现。我们将向你揭示如何轻松地将已购买的51CTO视频下载到本地&#xff0c;让学习的路上再也没有…

【Linux线程(一)】线程初理解

前言&#xff1a; &#xff08;一&#xff09;线程的概念 &#xff08;二&#xff09;线程的理解 &#xff08;三&#xff09;示例 &#xff08;四&#xff09;线程优缺点 线程的优点 线程的缺点 &#xff08;五&#xff09;线程和进程的切换 1.线程的切换 2.进程的切换…

感染了后缀为.360勒索病毒如何应对?数据能够恢复吗?

导言&#xff1a; 在数字化时代的浪潮中&#xff0c;网络安全问题如同暗流涌动&#xff0c;威胁着每一个互联网用户的安宁。而近年来&#xff0c;一种名为.360勒索病毒的新型网络威胁逐渐浮出水面&#xff0c;以其独特的加密方式和狡猾的传播策略&#xff0c;给全球网络安全带…

数据库——SQL SERVER(先学删库跑路)

目录 一&#xff1a;什么是数据库 二&#xff1a;为什么需要数据库 三&#xff1a;数据库的安装 四&#xff1a;学前必备知识 1. 数据库原理 2. 数据库与编程语言 3. 数据库与数据结构的区别 4. 连接 5. 有了编程语言为啥还要数据库 6. 初学者学习数据库的三个方面 …

出租车计价器设计与实现(论文 + 源码)

关于java出租车计价器设计与实现.zip资源-CSDN文库https://download.csdn.net/download/JW_559/89304164 出租车计价器设计与实现 摘 要 在我国&#xff0c;出租车行业是八十年代初兴起的一项新兴行业&#xff0c;随着出租车的产生&#xff0c;计价器也就应运而生。但当时在全…

树状数组(解决单点更新的QSQ问题)

解决单点更新的区间前缀和 #include <iostream> #include <cmath>#define int long longusing namespace std; const int N5e510; int n,T,tree[N]; int lowbit(int i){return i&(-i); } //单点更新 找后继 void add(int id,int val){for(int iid;i<n;iilow…

28.6k Star!Dify:完善生态、支持Ollama与本地知识库、企业级拖放式UI构建AI Agent、API集成进业务!

原文链接&#xff08;更好排版、视频播放、社群交流&#xff09; 28.6k Star&#xff01;Dify&#xff1a;完善生态、支持Ollama与本地知识库、企业级拖放式UI构建AI Agent、API集成进业务&#xff01; 原创 Aitrainee [ AI进修生 ](javascript:void(0)&#x1f609; AI进修…

【C++杂货铺】红黑树

目录 &#x1f308;前言&#x1f308; &#x1f4c1; 红黑树的概念 &#x1f4c1; 红黑树的性质 &#x1f4c1; 红黑树节点的定义 &#x1f4c1; 红黑树的插入操作 &#x1f4c1; 红黑树和AVL树的比较 &#x1f4c1; 全代码展示 &#x1f4c1; 总结 &#x1f308;前言…

mybatis-plus(2)

上文我们介绍完mybatis-plus的常用注解&#xff0c;现在介绍 mp的基础的yaml配置 mybatis-plus:type-aliases-package: #该位置写 数据库对应实体类的全路径global-config:db-config:id-type: auto # 全局id类型为自增长 mp同时也是支持手写sql&#xff0c;而且mapper的读取地…

OpenMVS学习笔记(一):WSL编译安装测试

1.CUDA和CUDNN安装 [1] WSL版本cuda安装&#xff1a; >> wget https://developer.download.nvidia.com/compute/cuda/repos/wsl-ubuntu/x86_64/cuda-wsl-ubuntu.pin >> sudo mv cuda-wsl-ubuntu.pin /etc/apt/preferences.d/cuda-repository-pin-600 >> wg…