起步
参考学习文章
参考教学视频
https://www.bilibili.com/video/BV1tg4y1x75Q?p=1&vd_source=ddf1ca4c71c5b908ebcce09b36ea0f49
Shadow DOM
Shadow DOM 是指,浏览器可以渲染一系列DOM 元素,而不必把它们插入到主文档的DOM 树结构中。 基于Shadow DOM, 可以实现基于组件的应用。
vue用法
1.主应用下载依赖
npm i -s wujie-vue3
npm i -s wujie-vue3
2.主应用注册依赖(main.js)
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
import WujieVue from 'wujie-vue3'
app.use(WujieVue)
app.use(createPinia())
app.use(router)
app.mount('#app')
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
import WujieVue from 'wujie-vue3'
app.use(WujieVue)
app.use(createPinia())
app.use(router)
app.mount('#app')
3.主应用使用wujie组件
<template>
<div style="height: 100vh; width: 100vw">
<WujieVue class="item" name="vue3" :url="vue3Url" :sync="true"></WujieVue>
</div>
</template>
<script>
export default {
data() {
return {
vue3Url: 'http://127.0.0.1:5173/'
}
},
methods: {}
}
</script>
<style scoped>
.item {
display: inline-block;
border: 1px dashed #ccc;
border-radius: 8px;
width: 100%;
height: 100%;
}
</style>
<template>
<div style="height: 100vh; width: 100vw">
<WujieVue class="item" name="vue3" :url="vue3Url" :sync="true"></WujieVue>
</div>
</template>
<script>
export default {
data() {
return {
vue3Url: 'http://127.0.0.1:5173/'
}
},
methods: {}
}
</script>
<style scoped>
.item {
display: inline-block;
border: 1px dashed #ccc;
border-radius: 8px;
width: 100%;
height: 100%;
}
</style>
4.子应用修改跨域(webpack.dev.conf.js)
同qiankun。如果没有产生跨域,子应用甚至无需修改。
devServer: {
headers: {
"Access-Control-Allow-Origin": "*" // 开启应用间的跨域访问
},
headers: {
"Access-Control-Allow-Origin": "*" // 开启应用间的跨域访问
},
wujie-vue3组件源码
组件在mounted
生命周期里做了startApp
所以入口文件中就不用再startApp了
startAppQueue的作用
Promise.resolve()
用于创建一个已解决(fulfilled)状态的 Promise 对象。这个 Promise 对象实际上是一个立即可完成的异步任务,它不会等待任何操作,立即进入已解决状态。
在 data
部分的 startAppQueue
属性中,将 Promise.resolve()
分配给 startAppQueue
是为了初始化一个已解决状态的 Promise 对象。然后,通过 .then()
方法将 this.startApp
函数添加到 startAppQueue
的执行队列中。
这种方法的常见用途是确保在组件的生命周期内只执行一次 startApp
函数。由于 startAppQueue
本身是一个 Promise 对象,它允许您将多个异步任务(即 this.startApp
函数)排队执行,并确保它们按照添加的顺序依次执行。
每次组件需要执行 startApp
函数时,它都会将新的 this.startApp
函数添加到 startAppQueue
中,并等待当前队列中的所有任务完成后才会执行新添加的任务。这可以确保在某一时刻只有一个 startApp
函数在执行,以避免并发问题或不必要的重复执行。这对于确保代码的可靠性和可维护性非常有用。
import { bus, preloadApp, startApp as rawStartApp, destroyApp, setupApp } from "wujie";
import { h, defineComponent } from "vue";
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: undefined },
prefix: { type: Object, default: undefined },
alive: { type: Boolean, default: undefined },
props: { type: Object, default: undefined },
attrs: { type: Object, default: undefined },
replace: { type: Function, default: undefined },
fetch: { type: Function, default: undefined },
fiber: { type: Boolean, default: undefined },
degrade: { type: Boolean, default: undefined },
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 },
},
data() {
return {
startAppQueue: Promise.resolve(),
};
},
mounted() {
bus.$onAll(this.handleEmit);
this.execStartApp();
this.$watch(
() => this.name + this.url,
() => this.execStartApp()
);
},
methods: {
handleEmit(event, ...args) {
this.$emit(event, ...args);
},
async startApp() {
try {
await rawStartApp({
name: this.name,
url: this.url,
el: this.$refs.wujie,
loading: this.loading,
alive: this.alive,
fetch: this.fetch,
props: this.props,
attrs: this.attrs,
replace: this.replace,
sync: this.sync,
prefix: this.prefix,
fiber: this.fiber,
degrade: this.degrade,
plugins: this.plugins,
beforeLoad: this.beforeLoad,
beforeMount: this.beforeMount,
afterMount: this.afterMount,
beforeUnmount: this.beforeUnmount,
afterUnmount: this.afterUnmount,
activated: this.activated,
deactivated: this.deactivated,
loadError: this.loadError,
});
} catch (error) {
console.log(error);
}
},
execStartApp() {
this.startAppQueue = this.startAppQueue.then(this.startApp);
},
destroy() {
destroyApp(this.name);
},
},
beforeDestroy() {
bus.$offAll(this.handleEmit);
},
render() {
return h("div", {
style: {
width: this.width,
height: this.height,
},
ref: "wujie",
});
},
};
const WujieVue = defineComponent(wujieVueOptions);
WujieVue.setupApp = setupApp;
WujieVue.preloadApp = preloadApp;
WujieVue.bus = bus;
WujieVue.destroyApp = destroyApp;
WujieVue.install = function (app) {
app.component("WujieVue", WujieVue);
};
export default WujieVue;
import { bus, preloadApp, startApp as rawStartApp, destroyApp, setupApp } from "wujie";
import { h, defineComponent } from "vue";
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: undefined },
prefix: { type: Object, default: undefined },
alive: { type: Boolean, default: undefined },
props: { type: Object, default: undefined },
attrs: { type: Object, default: undefined },
replace: { type: Function, default: undefined },
fetch: { type: Function, default: undefined },
fiber: { type: Boolean, default: undefined },
degrade: { type: Boolean, default: undefined },
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 },
},
data() {
return {
startAppQueue: Promise.resolve(),
};
},
mounted() {
bus.$onAll(this.handleEmit);
this.execStartApp();
this.$watch(
() => this.name + this.url,
() => this.execStartApp()
);
},
methods: {
handleEmit(event, ...args) {
this.$emit(event, ...args);
},
async startApp() {
try {
await rawStartApp({
name: this.name,
url: this.url,
el: this.$refs.wujie,
loading: this.loading,
alive: this.alive,
fetch: this.fetch,
props: this.props,
attrs: this.attrs,
replace: this.replace,
sync: this.sync,
prefix: this.prefix,
fiber: this.fiber,
degrade: this.degrade,
plugins: this.plugins,
beforeLoad: this.beforeLoad,
beforeMount: this.beforeMount,
afterMount: this.afterMount,
beforeUnmount: this.beforeUnmount,
afterUnmount: this.afterUnmount,
activated: this.activated,
deactivated: this.deactivated,
loadError: this.loadError,
});
} catch (error) {
console.log(error);
}
},
execStartApp() {
this.startAppQueue = this.startAppQueue.then(this.startApp);
},
destroy() {
destroyApp(this.name);
},
},
beforeDestroy() {
bus.$offAll(this.handleEmit);
},
render() {
return h("div", {
style: {
width: this.width,
height: this.height,
},
ref: "wujie",
});
},
};
const WujieVue = defineComponent(wujieVueOptions);
WujieVue.setupApp = setupApp;
WujieVue.preloadApp = preloadApp;
WujieVue.bus = bus;
WujieVue.destroyApp = destroyApp;
WujieVue.install = function (app) {
app.component("WujieVue", WujieVue);
};
export default WujieVue;
Lerna
Lerna是一个用于管理JavaScript项目中多个包(packages)的工具。它可以帮助开发人员更轻松地管理具有多个包的大型项目,尤其是在使用Monorepo(单一仓库)结构的情况下。
Lerna提供了一些功能,包括:
- 包管理: Lerna可以帮助你创建、发布和管理项目中的不同包。这些包可以是独立的npm包,也可以是项目中的内部模块。
- 版本管理: Lerna可以协助你在项目的多个包之间保持版本一致性。当你更新一个包时,Lerna可以帮助你升级相关依赖的版本,并确保项目的其他部分不会受到不兼容的更改的影响。
- 交叉包依赖: Lerna允许你在项目中的不同包之间建立依赖关系,使得一个包可以依赖于另一个包,而不必将它发布到npm。
- 自动化任务: Lerna提供了一些命令和脚本,可以简化包的创建、测试、构建和发布过程。
Lerna通常与其他工具,如Yarn或npm一起使用,以便更有效地管理项目中的包。它特别适用于大型前端或Node.js项目,其中需要管理多个相互依赖的包。使用Lerna可以提高项目的可维护性和可扩展性。
路由同步(sync)
路由同步会将子应用路径的path+query+hash
通过window.encodeURIComponent
编码后挂载在主应用url
的查询参数上,其中key
值为子应用的 name。
比如官方demo中切换到react17路径会变成:
https://wujie-micro.github.io/demo-main-vue/react17?react17=%2Fdemo-react17%2Flocation
开启路由同步后,刷新浏览器或者将url
分享出去子应用的路由状态都不会丢失,当一个页面存在多个子应用时无界支持所有子应用路由同步,浏览器刷新、前进、后退子应用路由状态也都不会丢失
需要开启参数 sync
只有无界实例在初次实例化的时候才会从url
上读回路由信息,一旦实例化完成后续只会单向的将子应用路由同步到主应用url
上
如果不开启路由同步,会出现什么问题?
点击菜单栏到子应用,这个时候子应用假如在页面 a,然后在子应用里面跳转路由到页面 b
如果 sync = false,此时刷新浏览器或者将 url 分享给别人,会发现子应用停留在页面 a 而不是页面 b
如果 sync = true
1、刷新浏览器依然可以停留在路由 b(从浏览器参数读回) 2、将浏览器 url 分享给别人,别人打开后子应用的路由也会在 b
个人思考
目前未阅读源码,在单例模式和重建模式下,更改传给wujie的url属性,子应用是会
保活模式需要手动路由同步
子应用的 alive 设置为true
时进入保活模式,内部的数据和路由的状态不会随着页面切换而丢失。
在保活模式下,子应用只会进行一次渲染,页面发生切换时承载子应用dom
的webcomponent
会保留在内存中,当子应用重新激活时无界会将内存中的webcomponent
重新挂载到容器上
保活模式下改变 url 子应用的路由不会发生变化,需要采用 通信 的方式对子应用路由进行跳转
注意
保活的子应用的实例不会销毁,子应用被切走了也可以响应 bus 事件,非保活的子应用切走了监听的事件也会全部销毁,需要等下次重新 mount 后重新监听。
主应用
<template>
<!--保活模式,name相同则复用一个子应用实例,改变url无效,必须采用通信的方式告知路由变化 -->
<WujieVue width="100%" height="100%" name="vue3" :url="vue3Url"></WujieVue>
</template>
<script>
import hostMap from "../hostMap";
import wujieVue from "wujie-vue2";
export default {
data() {
return {
vue3Url: hostMap("//localhost:7300/") + this.$route.params.path,
};
},
watch: {
"$route.params.path": {
handler: function () {
wujieVue.bus.$emit("vue3-router-change", `/${this.$route.params.path}`);
},
immediate: true,
},
},
};
</script>
<style lang="scss" scoped></style>
<template>
<!--保活模式,name相同则复用一个子应用实例,改变url无效,必须采用通信的方式告知路由变化 -->
<WujieVue width="100%" height="100%" name="vue3" :url="vue3Url"></WujieVue>
</template>
<script>
import hostMap from "../hostMap";
import wujieVue from "wujie-vue2";
export default {
data() {
return {
vue3Url: hostMap("//localhost:7300/") + this.$route.params.path,
};
},
watch: {
"$route.params.path": {
handler: function () {
wujieVue.bus.$emit("vue3-router-change", `/${this.$route.params.path}`);
},
immediate: true,
},
},
};
</script>
<style lang="scss" scoped></style>
子应用
mounted() {
window.$wujie?.bus.$on("vue3-router-change", (path) => this.$router.push(path));
},
mounted() {
window.$wujie?.bus.$on("vue3-router-change", (path) => this.$router.push(path));
},
单例模式(主子应用都是vite4+vue3)
单例模式需要满足两个条件:
- 子应用的
alive
为false
,也就是不开启保活 - 子应用进行了生命周期改造
如何给子应用进行生命周期改造
入口文件如main.js
中改造为:
if (window.__POWERED_BY_WUJIE__) {
let instance
window.__WUJIE_MOUNT = () => {
instance = createApp(App)
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes
})
instance.use(router).use(createPinia()).mount('#app')
}
window.__WUJIE_UNMOUNT = () => {
instance.unmount()
}
} else {
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes
})
createApp(App).use(router).use(createPinia()).mount('#app')
}
if (window.__POWERED_BY_WUJIE__) {
let instance
window.__WUJIE_MOUNT = () => {
instance = createApp(App)
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes
})
instance.use(router).use(createPinia()).mount('#app')
}
window.__WUJIE_UNMOUNT = () => {
instance.unmount()
}
} else {
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes
})
createApp(App).use(router).use(createPinia()).mount('#app')
}
子应用页面如果切走,会调用window.__WUJIE_UNMOUNT
销毁子应用当前实例,子应用页面如果切换回来,会调用window.__WUJIE_MOUNT
渲染子应用新的子应用实例
实现在单例模式下改变 url 子应用的路由跳转到对应路由
1.主应用路由使用路径参数
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/vue3-sub1/:path',
name: 'vue3-sub1',
component: () => import('../views/vue3-sub1.vue')
},
]
})
export default router
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/vue3-sub1/:path',
name: 'vue3-sub1',
component: () => import('../views/vue3-sub1.vue')
},
]
})
export default router
2.主应用中使用router-link
切换路径
<template>
<div>
<router-link to="/vue3-sub1/home">home</router-link>
<router-link to="/vue3-sub1/about">about</router-link>
</div>
<router-view></router-view>
</template>
<template>
<div>
<router-link to="/vue3-sub1/home">home</router-link>
<router-link to="/vue3-sub1/about">about</router-link>
</div>
<router-view></router-view>
</template>
3.主应用中改变wujie-vue3组件中的url,子应用的路由会跳转到对应路由
传递给wujie-vue3组件的url属性值,实际上就是子应用相应页面的url路径,wujie做了处理,控制单向跳转
<template>
<div style="height: 100vh; width: 100vw">
<WujieVue class="item" name="vue3" :url="vue3Url" :plugins="plugins"></WujieVue>
</div>
</template>
<script>
export default {
data() {
return {
plugins: [
{
// vite4子应用样式切换丢失
patchElementHook(element, iframeWindow) {
if (element.nodeName === 'STYLE') {
element.insertAdjacentElement = function (_position, ele) {
iframeWindow.document.head.appendChild(ele)
}
}
}
}
]
}
},
computed: {
vue3Url() {
return 'http://127.0.0.1:5174/' + `#/${this.$route.params.path}`
}
}
}
</script>
<style scoped>
.item {
display: inline-block;
border: 1px dashed #ccc;
border-radius: 8px;
width: 100%;
height: 100%;
}
</style>
<template>
<div style="height: 100vh; width: 100vw">
<WujieVue class="item" name="vue3" :url="vue3Url" :plugins="plugins"></WujieVue>
</div>
</template>
<script>
export default {
data() {
return {
plugins: [
{
// vite4子应用样式切换丢失
patchElementHook(element, iframeWindow) {
if (element.nodeName === 'STYLE') {
element.insertAdjacentElement = function (_position, ele) {
iframeWindow.document.head.appendChild(ele)
}
}
}
}
]
}
},
computed: {
vue3Url() {
return 'http://127.0.0.1:5174/' + `#/${this.$route.params.path}`
}
}
}
</script>
<style scoped>
.item {
display: inline-block;
border: 1px dashed #ccc;
border-radius: 8px;
width: 100%;
height: 100%;
}
</style>
如果主应用上有多个菜单栏用到了子应用的不同页面,在每个页面启动该子应用的时候将name
设置为同一个,这样可以共享一个wujie
实例,承载子应用js
的iframe
也实现了共享,不同页面子应用的url
不同,切换这个子应用的过程相当于:销毁当前应用实例 => 同步新路由 => 创建新应用实例
vite4子应用样式切换丢失
无界子应用如果是单例模式,js只会执行一遍,动态加载进来的样式,无界需要收集起来,等子应用下次切换回来,再将这些样式恢复,对于 document.body.appendChild,和 document.head.appendChild 这样的方法 无界内部已经劫持进行收集,但是vite4上面的代码可以看到采用了 style.InsertAdjacentElement 这样的方法导致无界没有收集到,所以采用下面的插件修改一下 style.InsertAdjacentElement 成 document.head.appendChild可以了,当然也可以不修改InsertAdjacentElement,将InsertAdjacentElement进来的样式放进 iframeWindow.__WUJIE.styleSheetElements里面,下次渲染就可以将样式还原了
<template>
<div style="height: 100vh; width: 100vw">
{{ this.$route.params.path }} {{ vue3Url }}
<WujieVue class="item" name="vue3" :url="vue3Url" :plugins="plugins"></WujieVue>
</div>
</template>
<script>
export default {
data() {
return {
plugins: [
{
patchElementHook(element, iframeWindow) {
if (element.nodeName === 'STYLE') {
element.insertAdjacentElement = function (_position, ele) {
iframeWindow.document.head.appendChild(ele)
}
}
}
}
]
}
},
computed: {
vue3Url() {
return 'http://127.0.0.1:5174/' + `#/${this.$route.params.path}`
}
}
}
</script>
<style scoped>
.item {
display: inline-block;
border: 1px dashed #ccc;
border-radius: 8px;
width: 100%;
height: 100%;
}
</style>
<template>
<div style="height: 100vh; width: 100vw">
{{ this.$route.params.path }} {{ vue3Url }}
<WujieVue class="item" name="vue3" :url="vue3Url" :plugins="plugins"></WujieVue>
</div>
</template>
<script>
export default {
data() {
return {
plugins: [
{
patchElementHook(element, iframeWindow) {
if (element.nodeName === 'STYLE') {
element.insertAdjacentElement = function (_position, ele) {
iframeWindow.document.head.appendChild(ele)
}
}
}
}
]
}
},
computed: {
vue3Url() {
return 'http://127.0.0.1:5174/' + `#/${this.$route.params.path}`
}
}
}
</script>
<style scoped>
.item {
display: inline-block;
border: 1px dashed #ccc;
border-radius: 8px;
width: 100%;
height: 100%;
}
</style>
重建模式
子应用既没有设置为保活模式,也没有进行生命周期的改造则进入了重建模式
每次页面切换不仅会销毁承载子应用dom
的webcomponent
,还会销毁承载子应用js
的iframe
,相应的wujie
实例和子应用实例都会被销毁
重建模式下改变 url 子应用的路由会跳转对应路由,但是在 路由同步 场景并且子应用的路由同步参数已经同步到主应用url
上时则无法生效,因为改变url
后会导致子应用销毁重新渲染,此时如果有同步参数则同步参数的优先级最高
代码部分
参考单例模式章节,但去除子应用生命周期的改造代码,也就是说重建模式只做主应用的代码实现即可
element-plus错位
wujie组件添加插件
plugins: [
{
// 在子应用所有的css之前
cssBeforeLoaders: [
// 强制使子应用body定位是relative
{ content: 'body{position: relative !important}' },
],
},
{
jsLoader: (code) => {
// 替换popper.js内计算偏左侧偏移量
var codes = code.replace(
'left: elementRect.left - parentRect.left',
'left: fixed ? elementRect.left : elementRect.left - parentRect.left'
);
// 替换popper.js内右侧偏移量
return codes.replace(
'popper.right > data.boundaries.right',
'false'
);
},
},
],
plugins: [
{
// 在子应用所有的css之前
cssBeforeLoaders: [
// 强制使子应用body定位是relative
{ content: 'body{position: relative !important}' },
],
},
{
jsLoader: (code) => {
// 替换popper.js内计算偏左侧偏移量
var codes = code.replace(
'left: elementRect.left - parentRect.left',
'left: fixed ? elementRect.left : elementRect.left - parentRect.left'
);
// 替换popper.js内右侧偏移量
return codes.replace(
'popper.right > data.boundaries.right',
'false'
);
},
},
],
wujie 目前 localStorage 没有做隔离
子应用中使用 localStorage.clear() 时,会连带清除主应用中的 localStorage