在上一篇文章中,我们成功将原本独立部署的项目集成在一起,但是我们通过 iframe 可以实现同样的效果,这里我们需要通过探究 micro-app
提供的更多功能,从而探究二者之间的区别。
生命周期
生命周期列表
micro-app
提供了从初始化到应用卸载的完整生命周期,基座应用可以各生命周期执行自定义函数。具体如下:
- created
<micro-app>
标签初始化后,加载资源前触发。
- beforemount
加载资源完成后,开始渲染之前触发。
- mounted
子应用渲染结束后触发。
- unmount
子应用卸载时触发。
- error
子应用渲染出错时触发,只有会导致渲染终止的错误才会触发此生命周期。
具体应用如下所示:
<template>
<div>
<h1>子应用1</h1>
<micro-app
name='app1' url='http://localhost:8081/'
baseroute='/one'
@created='created'
@beforemount='beforemount'
@mounted='mounted'
@unmount='unmount'
@error='error'
/>
</div>
</template>
<script>
export default {
name: 'AppOne',
methods: {
created() {
console.log('micro-app 标签被创建');
},
beforemount() {
console.log('app1 即将被渲染');
},
mounted () {
console.log('app1 已经渲染完成');
},
unmount () {
console.log('app1 已经卸载');
},
error () {
console.log('app 1渲染出错');
},
}
}
</script>
<style lang="less">
</style>
切换应用时,触发卸载事件。
对于 ifreame 而言,并没有这么丰富的生命周期,只提供了 onload 事件,在子应用资源加载完成后进行回调。当然,我们可以通过MutationObserver 去监听 iframe 的挂载以及卸载事件,相对来说有一点麻烦。
子应用生命周期
对于子应用来说,只有两个生命周期,挂载以及卸载。
挂载:子应用的 js 被执行时,则为挂载,只用在入口文件中进行挂载相关操作。
// micro-app-1/main.js
console.log('app1 被基座挂载了');
// micro-app-2/main.js
console.log('app2 被基座挂载了');
注意:由于应用可能作为独立应用部署,也可能作为套件中的应用部署,所以我们上面的代码应该判断当前是否是套件,上述代码只用在套件环境才可能需要执行。
卸载:子应用被卸载时,会接受到一个名为 unmount 的事件,通过监听此事件进行卸载相关操作。
// micro-app-1/main.js
window.addEventListener('unmount', function () {
console.log('app1 被基座卸载了');
});
iframe 好像并没有直接的方法来告诉子应用被基座卸载,是否有其他方法,欢迎共同探究。
环境变量
micro-app
提供了一些环境变量,用于判断是否被基座应用以及基座给当前应用设置的其他属性,具体可查看如下链接。micro-app环境变量
micro-app 最常用的环境变量就是 __MICRO_APP_ENVIRONMENT__,对于子应用来说,如果我们被基座所引用,通常需要做一些特地操作。
// micro-app-1/main.js
if (window.__MICRO_APP_ENVIRONMENT__) {
console.log('app1 被基座所引用');
}
作为独立应用访问时,可以看到控制台并没有输出对应的语句。
应用有没有办法是否被 iframe 引用呢?答案是有的,具体可通过location.ancestorOrigins
JS 沙箱
micro-app使用 Proxy 拦截了用户全局操作,从而防止对 window 的修改引发全局环境变量污染。每个子应用都运行在沙箱环境,拥有相对纯净的运行空间。
首先我们在基座中定义变量,看子应用是否能够读取到。
// micro-app-base/main.js
window.a = 'a';
console.log('基座 a:', window.a);
// micro-app-1/main.js
console.log('app1 a:', window.a);
// micro-app-2/main.js
console.log('app2 a:', window.a);
可以看到在基座中定义的全局变量是可以被micro-app 子应用读取到的,但是被 iframe 引用的子应用是无法读取到的。
接下来我们在子应用中定义全局变量,看有没有办法被基座读取。
// micro-app-1/App.vue
<el-button @click="defineB">定义B</el-button><br/>
defineB() {
window.b = 1;
console.log('app1 b:', window.b);
}
// micro-app-base/AppOne.vue
<el-button @click="printB">打印b</el-button>
printB() {
console.log('基座 b:', window.b);
}
可以看到子应用中定义的全局变量是不会影响到基座中的全局变量的。当然如果想让基座读取到子应用中定义的全局变量,可以在沙箱中获取到真实的 window 对象即可。
更多细节可查阅JS沙箱
样式隔离
micro-app 默认会以<micro-app>
标签作为样式作用域,利用标签的name
属性为每个样式添加前缀,将子应用的样式影响禁锢在当前标签区域。
.test {
color: red;
}
/* 转换为 */
micro-app[name=xxx] .test {
color: red;
}
通过样式隔离,子应用的样式不会对基座产生影响,也不会对其他应用产生影响。
但基座应用的样式依然会对子应用产生影响,如果发生样式污染,推荐通过约定前缀或CSS Modules方式解决。
对于 iframe 而言,其既不会被基座样式影响,也不会影响基座样式。
关于样式隔离的更多内容,可参考此样式隔离.
元素隔离
micro-app模拟实现了类似ShadowDom的功能,元素不会逃离<micro-app>
元素边界,子应用只能对自身的元素进行增、删、改、查的操作。
基座应用和子应用都有一个元素<div id='root'></div>
,此时子应用通过document.querySelector('#root')
获取到的是自己内部的#root
元素,而不是基座应用的。
基座应用拥有统筹全局的作用,可以直接操作子应用元素。
const app = document.querySelectorAll('#app');
console.log(app);
如果想要获取 iframe 中的元素的话,由于子应用与基座应用基本都是非同源,浏览器会进行阻止。
var a = document.querySelector("iframe");
var b = a.contentWindow.document;
const app = b.getElementById("app");
数据通信
micro-app
提供了一套灵活的数据通信机制,基座应用和子应用之间可以非常便捷实现数据传输。
micro-app
会向子应用注入名称为microApp
的全局对象,子应用通过这个对象和基座应用进行数据交互。
基座发送数据给子应用
方式一
// micro-app-base
import microApp from '@micro-zoe/micro-app'
// 发送数据给子应用 app1,setData第二个参数只接受对象类型
microApp.setData('app1', {type: '数据'});
方式二
vue中和绑定普通属性方式一致。
// micro-app-base
<template>
<micro-app
name='app1'
url='xx'
:data='dataForChild' // data只接受对象类型,数据变化时会重新发送
/>
</template>
<script>
export default {
data () {
return {
dataForChild: {type: '发送给子应用的数据'}
}
}
}
</script>
子应用获取基座数据
方式一
const data = window.microApp.getData() // 返回基座下发的data数据
方式二
function dataListener (data) {
console.log('来自基座应用的数据', data)
}
/**
* 绑定监听函数,监听函数只有在数据变化时才会触发
* dataListener: 绑定函数
* autoTrigger: 在初次绑定监听函数时如果有缓存数据,是否需要主动触发一次,默认为false
* !!!重要说明: 因为子应用是异步渲染的,而基座发送数据是同步的,
* 如果在子应用渲染结束前基座应用发送数据,则在绑定监听函数前数据已经发送,在初始化后不会触发绑定函数,
* 但这个数据会放入缓存中,此时可以设置autoTrigger为true主动触发一次监听函数来获取数据。
*/
window.microApp.addDataListener(dataListener: Function, autoTrigger?: boolean)
// 解绑监听函数
window.microApp.removeDataListener(dataListener: Function)
// 清空当前子应用的所有绑定函数(全局数据函数除外)
window.microApp.clearDataListener()
子应用发送数据给基座
// dispatch只接受对象作为参数
window.microApp.dispatch({type: '子应用发送的数据'})
基座获取子应用数据
方式一
import microApp from '@micro-zoe/micro-app'
const childData = microApp.getData(appName) // 返回子应用的data数据
方式二
<template>
<micro-app
name='my-app'
url='xx'
// 数据在事件对象的detail.data字段中,子应用每次发送数据都会触发datachange
@datachange='handleDataChange'
/>
</template>
<script>
export default {
methods: {
handleDataChange (e) {
console.log('来自子应用的数据:', e.detail.data)
}
}
}
</script>
实战 demo1
每个应用都不可或缺需要进行登陆,如果作为独立应用而言,需要在该应用中定义一个独立的账号组件。
如果是作为子应用而言,所有的子应用都应该是公用一套账号体系的,所以需要在套件中定义一个独立的账号组件。
我们先在子应用中定义一个简单的账号组件,这里我们用一个下拉列表模拟登录组件,如下所示:
<template>
<div id="app">
<header class="header-wrapper">
<div>
<el-select v-model="user" placeholder="请选择登陆用户">
<el-option
v-for="user in userList"
:key="user.id"
:label="user.name"
:value="user"
/>
</el-select>
</div>
</header>
<main>
<h1>当前登陆的用户为: {{ user }}</h1>
</main>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
user: '',
userList: ['张三', '李四', '王五']
}
},
methods: {
}
}
</script>
<style scoped lang="less">
#app {
.header-wrapper {
height: 40px;
display: flex;
align-items: center;
border: 1px solid;
}
}
</style>
接着我们在基座应用中定义同样一个账号组件,如下所示:
<template>
<div id="app">
<section class="aside">
<p><router-link to="one">应用一</router-link></p>
<p><router-link to="two">应用二</router-link></p>
</section>
<section class="main">
<header class="header-wrapper">
<div>
<el-select v-model="user" placeholder="请选择登陆用户">
<el-option
v-for="user in userList"
:key="user.id"
:label="user.name"
:value="user"
/>
</el-select>
</div>
</header>
<router-view></router-view>
</section>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
user: '',
userList: ['张三', '李四', '王五']
}
},
}
</script>
<style lang="less" scoped>
#app {
display: flex;
width: 100vw;
height: 100vh;
.aside {
width: 200px;
height: 100%;
background: #262f3e;
a {
color: #fff;
}
}
.main {
flex: 1;
width: 0;
background: #fff;
}
}
</style>
这个时候,可以看到在基座应用中出现了两个账号组件,对于微前端来说,所有的子应用都是公用一套账号体系的,所以子应用中的账号不应该工作。此时,需要子应用判断其是否在微前端环境被使用,如果是的话,则不需要工作。
我们可以通过前文中介绍的环境变量完成上述判断。
// micro-app-one/App.vue
...
<div v-if="!isMicroApp">
<el-select v-model="user" placeholder="请选择登陆用户">
<el-option
v-for="user in userList"
:key="user.id"
:label="user.name"
:value="user"
/>
</el-select>
</div>
...
computed: {
isMicroApp() {
return window.__MICRO_APP_ENVIRONMENT__;
}
},
...
此时,可以看到只有基座中的账号组件在工作了,接下来需要做的则是将基座中用户选择的账号传递给子应用。
// micro-app-base/App.vue
import microApp from '@micro-zoe/micro-app'
methods: {
selectUser() {
microApp.setData('app1', {type: 'selectUser', data: this.user});
}
}
// micro-app-1/App.vue
mounted() {
if (this.isMicroApp) {
window.microApp.addDataListener((message) => {
const { type, data } = message;
if (type === 'selectUser') {
this.user = data;
}
});
}
},
实战 demo2
用户可能只使用套件中的一部分子应用,当选择其未使用过的账号时,子应用会检查该账号是否初始化过,如果没有的话,则需要告知套件并未初始化。
// micro-app-1/App.vue
mounted() {
if (this.isMicroApp) {
window.microApp.addDataListener((message) => {
const { type, data } = message;
if (type === 'selectUser') {
this.user = data;
window.microApp.dispatch({type: 'login', data: '账号并未初始化'});
}
});
}
},
// micro-app-base/AppOne.vue
<template>
<div>
<h1>{{ loginMessage }}</h1>
<micro-app
name='app1'
url='http://localhost:8081/'
@datachange='handleDataChange'
/>
</div>
</template>
<script>
export default {
name: 'AppOne',
data() {
return {
loginMessage: '',
}
},
methods: {
handleDataChange (e) {
const { type, data } = e.detail.data;
if (type === 'login') {
this.loginMessage = data;
}
}
}
}
</script>
<style lang="less">
</style>
通过上述 demo,我们可以非常轻松的实现父子应用之间的通信。
对于 iframe 而言,我们可以通过postMessage 实现父向子的单向通信,但是如果子向父而言,受同源策略限制,则是较难以实现。