微前端无界方案

news2024/11/25 3:08:06

微前端无界


无界

官方文档




主应用

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来实现沙箱,有效的减小了代码量

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

想搭建AI知识库的企业看这篇就够了

企业要想在激烈的竞争中脱颖而出&#xff0c;有一套高效、智能的知识管理系统是非常重要的。搭建AI知识库能够帮助企业整合、分类、检索和应用知识&#xff0c;因此成为众多企业的第一选择。对于想要搭建AI知识库的企业来说&#xff0c;应该注意哪些方面呢&#xff1f;本文将从…

专业网站设计方案

当前互联网的快速发展和普及&#xff0c;使得网站设计成为了一个极其重要的环节。一个好的网站设计方案将能够吸引更多的访问者&#xff0c;提高用户体验&#xff0c;增强品牌形象。下面将为您介绍一个专业的网站设计方案。 首先&#xff0c;一个专业的网站设计方案应该具备清晰…

APP反抓包 - 客户端证书验证进阶(代码混淆)

1.关于混淆 在安卓开发中,对于第三方的包是可以进行混淆的,例如:OKHttp3.Http.Cert.check 被混淆后可以是a.f.c.b 形式。在安卓开发中,系统包是无法混淆的,例如:java.security.KeyStore不会被混淆。由于这种的情况的存在,再次审示我们之前的通用脚本,就会发现他是不通用…

2000-2022年上市公司供应链效率数据(含原始数据+结果)

2000-2022年上市公司供应链效率数据&#xff08;含原始数据结果&#xff09; 1、时间&#xff1a;2000-2022年 2、指标&#xff1a;年份、股票代码、省份、城市、区县、省份代码、城市代码、区县代码、首次上市年份、上市状态、股票简称、行业名称、行业代码、库存周转率、供…

单页源码加密屋zip文件加密API源码

简介&#xff1a; 单页源码加密屋zip文件加密API源码 api源码里面的参数已改好&#xff0c;往服务器或主机一丢就行&#xff0c;出现不能加密了就是加密次数达到上限了&#xff0c;告诉我在到后台修改加密次数 点击下载

解决宝塔Nginx和phpMyAdmin配置端口冲突问题

问题描述 在对基于宝塔面板的 Nginx 配置文件进行端口修改时&#xff0c;我注意到 phpMyAdmin 的端口配置似乎也随之发生了变化&#xff01; 解决方法 官方建议在处理 Nginx 配置时&#xff0c;应避免直接修改默认的配置文件&#xff0c;以确保系统的稳定性和简化后续的维护…

过拟合和欠拟合的学习

1.什么拟合 就是说这个曲线能不能很好地描述某些样本数据&#xff0c;并且拥有较好的泛化能力。 2.什么是过拟合 过拟合就是曲线太过于贴切训练数据的特征了&#xff0c;在训练集上表现得非常优秀&#xff0c;近乎完美的预测/区分了所有得数据&#xff0c;但是在新的测试集上…

Springboot整合 Spring Cloud Gateway

1.Gateway介绍 1.是spring cloud官方推出的响应式的API网关框架&#xff0c;旨在为微服务架构提供一种简单有效的API路由的管理方式&#xff0c;并基于Filter的方式提供网关的基本功能&#xff0c;例如&#xff1a;安全认证&#xff0c;监控&#xff0c;限流等等。 2.功能特征…

java图片水印字体乱码问题

问题描述&#xff1a;在linux Centos-7.5_64bit系统的其他服务器上不乱码&#xff0c;在部署项目的正式服务器乱码 水印字体设置是 微软雅黑 Font wordFont new Font("微软雅黑", Font.ITALIC,(srcImgHeightsrcImgWidth)/50); 一.Springboot项目&#xff0c;部署在…

SSH 免密登录,设置好仍然需要密码登录解决方法

说明&#xff1a; ssh秘钥登录设置好了&#xff0c;但是登录的时候依然需要提供密码 查看系统安全日志&#xff0c;定位问题 sudo cat /var/log/auth.log或者 sudo cat /var/log/secure找到下面的信息 Authentication refused: bad ownership or modes...&#xff08;网上的…

视频号小店怎么选品?给大家分享三个选品思维,让你快速脱颖而出

哈喽&#xff0c;大家好&#xff0c;我是电商花花&#xff0c;专注做电商的花花。 为什么我会说视频号小店是我们今年翻身&#xff0c;赚钱的最佳选择&#xff1f; 因为现在视频号小店不管是在流量上还是市场上&#xff0c;视频号小店都有着属于自己的优势&#xff0c;只要我…

Spring MVC(五) 文件上传

1 单文件上传 在程序开发中&#xff0c;有时候需要上传一些文件。我们在学习Servlet的时候&#xff0c;也做过文件上传的操作&#xff0c;只不过基于Servlet的文件上传操作起来过于复杂&#xff0c;因此所有的MVC框架都提供了自己的文件上传操作&#xff0c;基本上都是基于File…

SpringCloud微服务01-MybatisPlus-Docker

https://b11et3un53m.feishu.cn/wiki/MWQIw4Zvhil0I5ktPHwcoqZdnec 一、微服务介绍 单体架构所有功能集群在一个架构中&#xff0c;难以维护复杂需求 微服务之间是不同的TomCat要跨服务查询&#xff0c; 学习是如何拆分单体架构为微服务 二、MybatisPlus 1.快速入门 ①入门…

Win10弹出这个:https://logincdn.msauth.ne

问题描述&#xff1a; Win10脚本错误 Windows10家庭版操作系统开机后弹出这个 https://logincdn.msauth.net/shared/1.0/content/js/ConvergedLogin_PCore_vi321_9jVworKN8EONYo0A2.js 解决方法&#xff1a; 重启计算机后手动关闭第三方安全优化软件&#xff0c;然后在任务管理…

强化学习——马尔可夫过程的理解

目录 一、马尔可夫过程1.随机过程2.马尔可夫性质3.马尔可夫过程4.马尔可夫过程示例 参考文献 一、马尔可夫过程 1.随机过程 随机过程是概率论的“动态”版本。普通概率论研究的是固定不变的随机现象&#xff0c;而随机过程则专注于那些随时间不断变化的情况&#xff0c;比如天…

OpenHamrony 实战开发——LiteOS-M内核的中断管理

在程序运行过程中&#xff0c;当出现需要由CPU立即处理的事务时&#xff0c;CPU暂时中止当前程序的执行转而处理这个事务&#xff0c;这个过程叫做中断。当硬件产生中断时&#xff0c;通过中断号查找到其对应的中断处理程序&#xff0c;执行中断处理程序完成中断处理。 通过中…

Ubuntu安装VScode

Ubuntu安装VScode 前言&#xff1a; 1、Ubuntu安装VScode比较方便 2、我更喜欢source insight 1、获取到linux版本的VScode安装包 VSCode 下载地址是&#xff1a;https://code.visualstudio.com/ 2、得到安装包 3、复制到ubuntu中&#xff0c;使用命令安装 sudo dpkg -i cod…

jmeter分布式集群压测

目的&#xff1a;通过多台机器同时运行 性能压测 脚本&#xff0c;模拟更好的并发压力 简单点&#xff1a;就是一个人&#xff08;控制机controler/调度机 master&#xff09;做一个项目的时候&#xff0c;压力有点大&#xff0c;会导致结果不理想&#xff0c;这时候找几个人&a…

国际护士节庆祝活动向媒体投稿有方法很轻松

作为一名医院职工,我肩负着医院对外信息宣传的重任。在国际护士节这个特殊的日子里,我们医院举办了一系列庆祝活动,以表彰护士们的辛勤付出和无私奉献。然而,在将这些活动信息投稿至媒体的过程中,我最初却遭遇了诸多挑战。 起初,我采用传统的邮箱投稿方式,将精心撰写的稿件发送…

Docker运行出现iptables: No chain/target/match by that name报错如何解决?

在尝试重启 Docker 容器时遇到的错误信息表明有关 iptables 的配置出了问题。这通常是因为 Docker 需要配置网络&#xff0c;而 iptables 规则没有正确设置或被意外删除。具体到你的错误信息中&#xff0c;报错 iptables: No chain/target/match by that name 表示 Docker 尝试…