本文承接上一篇手把手教你使用vue2搭建micro-app对micro-app进行简单的认识与学习。
因为上一篇只是对micro-app的搭建,并没有对具体的内容进行深入了解,所以本文是在上一篇文章代码的基础上对micro-app官网,的初步了解。
在了解完微前端之后,我们知道,微前端是由一个基座和多个子应用构成的,那么如何在基座中调用子应用呢?官网中如下介绍:
// 官网中看到是使用此标签在父子间中嵌入子组件的
<micro-app name='app1' url='http://localhost:3000/' baseroute='/my-page'>micro-app>
<micro-app name='app_first' url='http://localhost:3001/'>micro-app>
<micro-app name='app_second' url='http://localhost:3002/'>micro-app>
上述例子中是单个子应用的,下面也分析了一下搭建的实例中的子应用嵌入,这边是通过点击router-link时,动态嵌入对应的子应用。具体内容我都加了注释说明
base/src/App.vue
<template>
<div id="app">
<div id="nav">
<router-link to="/">Homerouter-link> |
<router-link to="/about">Aboutrouter-link>|
<router-link :to="`/${prefix}/first-child/home`">FirstChildHomerouter-link>|
<router-link :to="`/${prefix}/first-child/about`">FirstChildAboutrouter-link>|
<router-link :to="`/${prefix}/second-child/home`">SecondChildHomerouter-link>|
<router-link :to="`/${prefix}/second-child/about`">SecondChildAboutrouter-link>
div>
<div style="background-color: pink">
<micro-app
v-if="isChild"
v-bind="micro"
destory
>micro-app>
<router-view v-else>router-view>
div>
div>
template>
<script>
import { MICRO_APPS, CHILD_PREFIX } from './micro/config.js'
export default {
name: 'App',
data () {
return {
// 是否为子模块
isChild: false,
micro: {
url: '' , /**子模块地址 */
key: '' , /**vue 标签的 key 值,用于不同子模块间的切换时,组件重新渲染 */
name: '', /**子模块名称,唯一 */
data: {}, /**子模块数据 */
baseroute: '' /**子模块数据 */
},
prefix: CHILD_PREFIX /**子模块链接前缀 */
}
},
watch: {
$route (val) { /**监听路由变化修改视图显示 */
this.changeChild(val)
}
},
created () {
this.changeChild(this.$route)
},
methods: {
/**
* 获取子模块 url 和 name
* */
getAppUrl (name) {
console.log('获取子模块 url 和 name', MICRO_APPS.find(app => app.name === name) || {})
return MICRO_APPS.find(app => app.name === name) || {}
},
/**
* 修改子视图显示
* @param route 在watch中监听到的路由变化
* */
changeChild (route) {
let path = route.path.toLowerCase() // 路由路径,将路径都转成小写字母,如:/app/first-child/home
let paths = path.split('/') // 将上述路径已'/'截取,获得如:['', 'app', 'first-child', 'home']
console.log('path', path)
console.log('paths', paths)
// 判断是否为子模块,子模块有固定的前缀,在 micro/config 设置
this.isChild = paths.length > 2 && paths[1] === CHILD_PREFIX
console.log('CHILD_PREFIX', CHILD_PREFIX)
if (this.isChild) { // 子模块
// 获取子模块的name和url
let app = this.getAppUrl(paths[2])
console.log('app', app)
this.micro = {
...app
, data: { name: route.name }
, key: `${app.name}`
, baseroute: `/${CHILD_PREFIX}/${paths[2]}`
}
console.log('micro', this.micro)
}
}
}
}
script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
#nav {
padding: 30px;
}
#nav a {
font-weight: bold;
color: #2c3e50;
}
#nav a.router-link-exact-active {
color: #42b983;
}
style>
通过配置项,可以决定开启或关闭某些功能,配置项都是对标签的配置。
如果希望在某个应用中使用,可以如下配置:
如果想要做一个全局配置,可以如下配置:
import microApp from '@micro-zoe/micro-app'
microApp.start({
inline: true, // 默认值false
destroy: true, // 默认值false
disableScopecss: true, // 默认值false
disableSandbox: true, // 默认值false
shadowDOM: true, // 默认值false
ssr: true, // 默认值false
})
上方代码做了简单描述,具体还得看官网地址
通过CustomEvent定义生命周期,在组件渲染过程中会触发相应的生命周期事件。
(1)created:标签初始化后,加载资源前触发
(2)beforemount:加载资源完成后,开始渲染之前触发
(3)muonted:子应用渲染结束后触发
(4)unmount:子应用卸载时触发
(5)error:子应用渲染出错时触发,只有会导致渲染终止的错误才会触发此生命周期
首先看一下对生命周期使用的例子和效果,方便对代码的理解,正常情况下组件能使用到的生命周期就4个,在后期开发中可根据合适的场景调用对应的生命周期。
<micro-app
v-if="isChild"
v-bind="micro"
destory
@created='created'
@beforemount='beforemount'
@mounted='mounted'
@unmount='unmount'
@error='error'
>micro-app>
/**
* 子模块创建(标签初始化后,加载资源前触发)
*/
created () {
console.log(`子模块创建(标签初始化后,加载资源前触发)-created`)
},
/**
* 子模块挂载之前(加载资源完成后,开始渲染之前触发)
*/
beforemount () {
console.log(`子模块挂载之前(加载资源完成后,开始渲染之前触发)-beforemount`)
},
/**
* 子模块挂载(子应用渲染结束后触发)
*/
mounted () {
console.log(`子模块挂载(子应用渲染结束后触发)-mounted`)
},
/**
* 子模块卸载(子应用卸载时触发)
*/
unmount () {
console.log(`子模块卸载(子应用卸载时触发)-unmount`)
},
/**
* 子模块异常(子应用渲染出错时触发,只有会导致渲染终止的错误才会触发此生命周期)
*/
error () {
console.log(`子模块异常-error`)
}
如果在开发中,有用到一些通用的监听方法,可在全局进行生命周期配置,配置如下:
import microApp from '@micro-zoe/micro-app'
microApp.start({
lifeCycles: {
created (e) {
console.log('created')
},
beforemount (e) {
console.log('beforemount')
},
mounted (e) {
console.log('mounted')
},
unmount (e) {
console.log('unmount')
},
error (e) {
console.log('error')
}
}})
之前在搭建micro-app的时候,有配置一些环境变量,当时就是照着例子抄的,现在要大概了解一下大致的环境变量配置。
// 设置 webpack 的公共路径
if (window.__MICRO_APP_ENVIRONMENT__) { // 判断是否在微前端环境中
// eslint-disable-next-line
__webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__ || '/'
}
/**
* 微前端环境下,注册mount和unmount方法
*/
if (window.__MICRO_APP_ENVIRONMENT__)
window[`micro-app-${window.__MICRO_APP_NAME__}`] = { mount, unmount }
else
mount()
// 设置 webpack 的公共路径
if (window.__MICRO_APP_ENVIRONMENT__) { // 判断是否在微前端环境中
// eslint-disable-next-line
__webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__ || '/'
}
/**引入 publicPath 设置 */
import './micro'
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
{
path: window.__MICRO_APP_BASE_ROUTE__ || '/' /**根据项目运行的不同环境,设置路径的前缀 */
, name: 'Home'
, redirect: { name: 'FirstHome' }
, component: () => import('../views/Empty.vue')
, children: [
{
path: 'home'
, name: 'FirstHome'
, component: () => import('../views/Home.vue')
}
, {
path: 'about'
, name: 'FirstAbout'
, component: () => import('../views/About.vue')
}
]
}
]
const router = new VueRouter({
mode: 'history',
routes
})
export default router
<template>
<div id="app">
<div id="nav">
<router-link :to="`${prefix}/home`">子应用1Homerouter-link> |
<router-link :to="`${prefix}/about`">子应用1Aboutrouter-link> |
<button @click="goto('SecondHome')">SecondHomebutton> |
<button @click="goto('SecondAbout')">SecondAboutbutton>
div>
<router-view />
div>
template>
<script>
export default {
name: 'App'
, data () {
return {
prefix: window.__MICRO_APP_BASE_ROUTE__ || ''
}
}
, methods: {
dataListener (data) {
console.log('data11111111111111', data)
console.log('this.$route.name111111111111', this.$route.name)
if (data.name !== this.$route.name) /** 不判断时会报一个“冗余导航【NavigationDuplicated】”的异常 */
this.$router.push({ name: data.name })
}
, goto (name) {
// 向基项目发送数据
window.microApp && window.microApp.dispatch({ route: { name } })
}
},
created () {
/** 绑定数据【data属性】监听事件 */
window.microApp && window.microApp.addDataListener(this.dataListener)
}
, destroyed () {
/** 移除数据【data属性】监听事件 */
window.microApp && window.microApp.removeDataListener(this.dataListener)
}
}
script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
#nav {
padding: 30px;
}
#nav a {
font-weight: bold;
color: #2c3e50;
}
#nav a.router-link-exact-active {
color: #42b983;
}
style>
import microApp from '@micro-zoe/micro-app'
import * as config from './config'
/**启用 micro */
microApp.start({
preFetchApps: config.MICRO_APPS
, globalAssets: config.GLOBAL_ASSETS
})
if (window.__MICRO_APP_BASE_APPLICATION__) {
console.log('我是基座应用11111111111111111111111111111111111111111111111')
}
js沙箱,就是让你的程序跑在一个隔离的环境下,不对外界的其他程序造成影响,通过创建类似沙盒的独立作业环境,在其内部运行的程序并不能对硬盘产生永久性的影响。
micro-app使用Proxy拦截了用户全局操作的行为,防止对windows的访问和修改,避免全局变量污染。micro-app中的每个子应用都运行在沙箱环境,以获取相对纯净的运行空间。
沙箱默认是开启的,正常情况不建议关闭,因为关闭可能会出现一些不可预知的问题
目前有以下3种方式在子应用中获取外部真实window
1、new Function(“return window”)() 或Function(“return window”)()
2、(0, eval)(‘window’)
3、window.rawWindow
子应用中使用window.a,父应用也有window.a,两者会互相影响,使用如下方法解决:
(function(window, self) {
with(window) {
子应用的js代码
}
}).call(代理对象, 代理对象, 代理对象)
micro-app的样式隔离默认是开启的,开启后会以标签作为样式作用域,利用标签的name属性为每个样式添加前缀,将子应用的样式影响禁锢在当前标签区域。 但基座应用的样式依然会对子应用产生影响,如果发生样式污染,推荐通过约定前缀或CSS Modules方式解决。
.test {
color: red;}
/* 转换为 */micro-app[name=xxx] .test {
color: red;
}
micro-app模拟实现了类似ShadowDom的功能,元素不会逃离元素边界,子应用只能对自身的元素进行增、删、改、查的操作。
如:基座应用和子应用都有一个元素,此时子应用通过document.querySelector(‘#root’)获取到的是自己内部的#root元素,而不是基座应用的。
当然,基座应用是可以获取子应用元素的, 在微前端下基座拥有统筹全局的作用,没有对基座应用操作子应用元素的行为进行限制。
micro-app提供了一套灵活的数据通信机制,方便基座应用和子应用之间的数据传输。
正常情况下,基座应用和子应用之间的通信是绑定的,基座应用只能向指定的子应用发送数据,子应用只能向基座发送数据,这种方式可以有效的避免数据污染,防止多个子应用之间相互影响。
同时还提供了全局通信,方便跨应用之间的数据通信。
// 官网是这样介绍的
const data = window.microApp.getData() // 返回基座下发的data数据
根据官网介绍,我直接在子应用打印了上述变量结果,但是只获取到子应用名称,效果如下:
接下来我好奇这getData()获取到到底是啥东西,于是去基座子应用调用添加了data,这样在子应用打印出来的内容就会多一点了,所以感觉如果想要在子应用中获取到基座数据,就需要将数据添加到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()
methods: {
dataListener (data) {
console.log('来自基座应用的数据', data)
}
},
created () {
// const data = window.microApp.getData() // 返回基座下发的data数据
// console.log('子应用获取父应用数据', data)
/** 绑定数据【data属性】监听事件 */
window.microApp && window.microApp.addDataListener(this.dataListener, true)
},
destroyed () {
/** 移除数据【data属性】监听事件 */
window.microApp && window.microApp.removeDataListener(this.dataListener)
}
// 官网中如下介绍
// dispatch只接受对象作为参数
window.microApp.dispatch({type: '子应用发送的数据'})
由于这边只介绍了子应用向基座应用发送数据,但是没有介绍发送过去的数据在基座怎么接收,所以这个内容写完后,我也不知道到底有没有发送成功,就先去找了一个基座接收子应用数据的方法,来证明子应用有没有向基座发送成功。
子应用发送数据:
<button @click="goto('SecondHome')">向基座发送数据Homebutton> |
<button @click="goto('SecondAbout')">向基座发送数据Aboutbutton>
methods: {
goto (name) {
// 向基项目发送数据
window.microApp && window.microApp.dispatch({ name: name,type: '子应用发送的数据', msg: '成功' })
}
},
基座接收数据:
<micro-app
v-if="isChild"
v-bind="micro"
:data="micro"
destory
@created='created'
@beforemount='beforemount'
@mounted='mounted'
@unmount='unmount'
@error='error'
@datachange='handleDataChange'
>micro-app>
methods: {
handleDataChange (event) {
console.log('接收子应用向基座发送得数据', event)
}
}
基座向子应用发送数据的方式有两种
我觉得和8-1中我子应用获取基座应用时,基座应该的数据写法是一致的,就是中要添加:data=‘数据’,具体可看8-1中我的示例代码,官方介绍如下:
<template>
template>
<script>export default {
data () {
return {
dataForChild: {type: '发送给子应用的数据'}
}
}}
script>
手动发送数据需要通过name指定接受数据的子应用,此值和元素中的name一致。官网介绍如下:
import microApp from '@micro-zoe/micro-app'
// 发送数据给子应用 my-app,setData第二个参数只接受对象类型
microApp.setData('my-app', {type: '新的数据'})
import microApp from "@micro-zoe/micro-app";
/**
* 子模块创建(标签初始化后,加载资源前触发)
*/
created () {
microApp.setData('first-child', {type: '新的数据'})
},
子应用:
methods: {
dataListener (data) {
console.log('来自基座应用的数据', data)
}
},
created () {
// const data = window.microApp.getData() // 返回基座下发的data数据
// console.log('子应用获取父应用数据', data)
/** 绑定数据【data属性】监听事件 */
window.microApp && window.microApp.addDataListener(this.dataListener, true)
},
destroyed () {
/** 移除数据【data属性】监听事件 */
window.microApp && window.microApp.removeDataListener(this.dataListener)
}
关于在基座中获取子应用的数据,官方介绍了三种方法,其中第二种方式在8-2中已经尝试过了,这边就只尝试一三两种方法了。
// 官方介绍
import microApp from '@micro-zoe/micro-app'
const childData = microApp.getData(appName) // 返回子应用的data数据
mounted () {
const childData = microApp.getData('first-child')
console.log('接收子应用向基座发送得数据', childData)
// console.log(`子模块挂载(子应用渲染结束后触发)-mounted`)
},
子应用:
created () {
// 向基项目发送数据
window.microApp && window.microApp.dispatch({ name: '就将计就计',type: '子应用发送的数据', msg: '成功' })
},
监听自定义事件(datachange),8-2中已介绍,在此不多介绍,看8-2即可
绑定监听函数需要通过name指定子应用,此值和元素中的name一致。看官方介绍和8-1很像但不完全一样,官方介绍如下:
import microApp from '@micro-zoe/micro-app'
function dataListener (data) {
console.log('来自子应用my-app的数据', data)}
/**
* 绑定监听函数
* appName: 应用名称
* dataListener: 绑定函数
* autoTrigger: 在初次绑定监听函数时如果有缓存数据,是否需要主动触发一次,默认为false
*/
microApp.addDataListener(appName: string, dataListener: Function, autoTrigger?: boolean)
// 解绑监听my-app子应用的函数
microApp.removeDataListener(appName: string, dataListener: Function)
// 清空所有监听appName子应用的函数
microApp.clearDataListener(appName: string)
dataListener (data) {
console.log('接收子应用向基座发送得数据--方法三', data)
},
/**
* 子模块创建(标签初始化后,加载资源前触发)
*/
created () {
/** 绑定数据【data属性】监听事件 */
microApp.addDataListener('first-child', this.dataListener, true)
},
destroyed () {
/** 移除数据【data属性】监听事件 */
window.microApp && window.microApp.removeDataListener('first-child', this.dataListener)
},
子应用:
created () {
// 向基项目发送数据
window.microApp && window.microApp.dispatch({ name: '就将计就计',type: '子应用发送的数据', msg: '成功' })
},
全局数据通信会向基座应用和所有子应用发送数据,在跨应用通信的场景中适用。全局通信在基座应用和子应用的用法跟上面8-3第三种方法以及8-1很类似,只是属性名变成了全局而已,具体可参考8-3和8-1中的方法。
官网介绍如下:
基座发送全局数据:
import microApp from '@micro-zoe/micro-app'
// setGlobalData只接受对象作为参数
microApp.setGlobalData({type: '全局数据'})
基座获取全局数据:
import microApp from '@micro-zoe/micro-app'
// 直接获取数据const globalData = microApp.getGlobalData() // 返回全局数据
function dataListener (data) {
console.log('全局数据', data)}
/**
* 绑定监听函数
* dataListener: 绑定函数
* autoTrigger: 在初次绑定监听函数时如果有缓存数据,是否需要主动触发一次,默认为false
*/
microApp.addGlobalDataListener(dataListener: Function, autoTrigger?: boolean)
// 解绑监听函数
microApp.removeGlobalDataListener(dataListener: Function)
// 清空基座应用绑定的所有全局数据监听函数
microApp.clearGlobalDataListener()
子应用发送全局数据:
// setGlobalData只接受对象作为参数
window.microApp.setGlobalData({type: '全局数据'})
子应用获取全局数据:
// 直接获取数据const globalData = window.microApp.getGlobalData() // 返回全局数据
function dataListener (data) {
console.log('全局数据', data)}
/**
* 绑定监听函数
* dataListener: 绑定函数
* autoTrigger: 在初次绑定监听函数时如果有缓存数据,是否需要主动触发一次,默认为false
*/
window.microApp.addGlobalDataListener(dataListener: Function, autoTrigger?: boolean)
// 解绑监听函数
window.microApp.removeGlobalDataListener(dataListener: Function)
// 清空当前子应用绑定的所有全局数据监听函数
window.microApp.clearGlobalDataListener()
沙箱关闭后,子应用默认的通信功能失效,此时可以通过手动注册通信对象实现一致的功能。
注册方式:在基座应用中为子应用初始化通信对象
import { EventCenterForMicroApp } from '@micro-zoe/micro-app'
// 注意:每个子应用根据appName单独分配一个通信对象
window.eventCenterForAppxx = new EventCenterForMicroApp(appName)
子应用就可以通过注册的eventCenterForAppxx对象进行通信,其api和window.microApp一致,基座通信方式没有任何变化。
子应用通信方式:
// 直接获取数据const data = window.eventCenterForAppxx.getData() // 返回data数据
function dataListener (data) {
console.log('来自基座应用的数据', data)}
/**
* 绑定监听函数
* dataListener: 绑定函数
* autoTrigger: 在初次绑定监听函数时如果有缓存数据,是否需要主动触发一次,默认为false
*/
window.eventCenterForAppxx.addDataListener(dataListener: Function, autoTrigger?: boolean)
// 解绑监听函数
window.eventCenterForAppxx.removeDataListener(dataListener: Function)
// 清空当前子应用的所有绑定函数(全局数据函数除外)
window.eventCenterForAppxx.clearDataListener()
// 子应用向基座应用发送数据,只接受对象作为参数
window.eventCenterForAppxx.dispatch({type: '子应用发送的数据'})
micro-app子应用默认提供资源路径自动补全功能。如: 图片/myapp/test.png,在最终渲染时会补全为http://localhost:8080/myapp/test.png
主要争对于link、script、img、background-imge、@font-face等
如果自动补全失败,可以采用运行时publicPath方案解决。我们之前配置环境变量时已经有配置了,app_first为例
app_first/src/micro/index.js
// 设置 webpack 的公共路径
if (window.__MICRO_APP_ENVIRONMENT__) { // 判断是否在微前端环境中
// eslint-disable-next-line
__webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__ || '/'
}
app_first/src/main.js
/**引入 publicPath 设置 */
import './micro'
当多个子应用拥有相同的js或css资源,可以指定这些资源在多个子应用之间共享,在子应用加载时直接从缓存中提取数据,从而提高渲染效率和性能。
globalAssets用于设置全局共享资源,它和预加载的思路相同,在浏览器空闲时加载资源并放入缓存。
当子应用加载相同地址的js或css资源时,会直接从缓存中提取数据,从而提升渲染速度。
// index.js--官方介绍
import microApp from '@micro-zoe/micro-app'
microApp.start({
globalAssets: {
js: ['js地址1', 'js地址2', ...], // js地址
css: ['css地址1', 'css地址2', ...], // css地址
}})
import microApp from '@micro-zoe/micro-app'
import * as config from './config'
/**启用 micro */
microApp.start({
preFetchApps: config.MICRO_APPS
, globalAssets: config.GLOBAL_ASSETS
})
/**
* 全局资源
*/
export const GLOBAL_ASSETS = {
js: [],
css: []
}
在link、script设置global属性会将文件提取为公共文件,共享给其它应用。
设置global属性后文件第一次加载会放入公共缓存,其它子应用加载相同的资源时直接从缓存中读取内容,从而提升渲染速度。
<link rel="stylesheet" href="xx.css" exclude>
<script src="xx.js" exclude>script>
<style exclude>style>
预加载是指在应用尚未渲染时提前加载资源并缓存,从而提升首屏渲染速度。
预加载并不是同步执行的,它会在浏览器空闲时间,依照开发者传入的顺序,依次加载每个应用的静态资源,以确保不会影响基座应用的性能。
import microApp from '@micro-zoe/micro-app'
// 方式一
microApp.preFetch([
{ name: 'my-app', url: 'xxx' }])
// 方式二
microApp.preFetch(() => [
{ name: 'my-app', url: 'xxx' }])
// 方式三
microApp.start({
preFetchApps: [
{ name: 'my-app', url: 'xxx' }
],
// 函数类型
// preFetchApps: () => [
// { name: 'my-app', url: 'xxx' }
// ],})
import microApp from '@micro-zoe/micro-app'
import * as config from './config'
/**启用 micro */
microApp.start({
preFetchApps: config.MICRO_APPS
, globalAssets: config.GLOBAL_ASSETS
})
/**
* 子应用地址
*/
export const MICRO_APPS = [
{ name: 'first-child', url: `http://localhost:3001/` },
{ name: 'second-child', url: `http://localhost:3002/` }
]
插件系统可以赋予开发者灵活处理静态资源的能力,对有问题的资源文件进行修改
插件系统的主要作用是对js的修改,每一个js文件都会经过插件系统,可以对js进行拦截和处理,通常用于修复js中的错误或向子应用注入一些全局变量。
适用场景:无法控制的js
使用方式:
import microApp from '@micro-zoe/micro-app'
microApp.start({
plugins: {
// 全局插件,作用于所有子应用的js文件
global?: Array<{
// 可选,强隔离的全局变量(默认情况下子应用无法找到的全局变量会兜底到基座应用中,scopeProperties可以禁止这种情况)
scopeProperties?: string[],
// 可选,可以逃逸到外部的全局变量(escapeProperties中的变量会同时赋值到子应用和外部真实的window上)
escapeProperties?: string[],
// 可选,传递给loader的配置项
options?: any,
// 必填,js处理函数,必须返回code值
loader?: (code: string, url: string, options: any) => code
}>
// 子应用插件
modules?: {
// appName为应用的名称,这些插件只会作用于指定的应用
[appName: string]: Array<{
// 可选,强隔离的全局变量(默认情况下子应用无法找到的全局变量会兜底到基座应用中,scopeProperties可以禁止这种情况)
scopeProperties?: string[],
// 可选,可以逃逸到外部的全局变量(escapeProperties中的变量会同时赋值到子应用和外部真实的window上)
escapeProperties?: string[],
// 可选,传递给loader的配置项
options?: any,
// 必填,js处理函数,必须返回code值
loader?: (code: string, url: string, options: any) => code
}>
}
}})
案例:
import microApp from '@micro-zoe/micro-app'
microApp.start({
plugins: {
global: [
{
scopeProperties: ['key', 'key', ...], // 可选
escapeProperties: ['key', 'key', ...], // 可选
options: 配置项, // 可选
loader(code, url, options) { // 必填
console.log('全局插件')
return code
}
}
],
modules: {
'appName1': [{
loader(code, url, options) {
if (url === 'xxx.js') {
code = code.replace('var abc =', 'window.abc =')
}
return code
}
}],
'appName2': [{
scopeProperties: ['key', 'key', ...], // 可选
escapeProperties: ['key', 'key', ...], // 可选
options: 配置项, // 可选
loader(code, url, options) { // 必填
console.log('只适用于appName2的插件')
return code
}
}]
}
}})
micro-app支持多层嵌套,即子应用可以嵌入其它子应用,但为了防止标签名冲突,子应用中需要做一些修改。
在子应用中设置tagName:
microApp.start({
tagName: 'micro-app-xxx', // 标签名称必须以 `micro-app-` 开头
})
在子应用中使用新定义的标签进行渲染,如:
<micro-app-xxx name='xx' url='xx'>micro-app-xxx>
注: 无论嵌套多少层,name都要保证全局唯一。
安装micro-app插件,安装在app_first目录下,因为我这边想把first嵌套到second下面
npm install @micro-zoe/micro-app --save
import microApp from '@micro-zoe/micro-app'
// 设置 webpack 的公共路径
if (window.__MICRO_APP_ENVIRONMENT__) { // 判断是否在微前端环境中
// eslint-disable-next-line
__webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__ || '/'
}
/**启用 micro */
microApp.start({
tagName: 'micro-app-first', // 标签名称必须以 `micro-app-` 开头
})
然后在app_second/src/App.vue中使用新定义的标签:
<div style="background: pink">
<micro-app-first name='first-child' url='http://localhost:3001/'>micro-app-first>
div>
通过自定义fetch替换框架自带的fetch,可以修改fetch配置(添加cookie或header信息等等),或拦截HTML、JS、CSS等静态资源。
自定义的fetch必须是一个返回string类型的Promise。
import microApp from '@micro-zoe/micro-app'
microApp.start({
/**
* 自定义fetch
* @param {string} url 静态资源地址
* @param {object} options fetch请求配置项
* @param {string|null} appName 应用名称
* @returns Promise
*/
fetch (url, options, appName) {
if (url === 'http://localhost:3001/error.js') {
// 删除 http://localhost:3001/error.js 的内容
return Promise.resolve('')
}
const config = {
// fetch 默认不带cookie,如果需要添加cookie需要配置credentials
credentials: 'include', // 请求时带上cookie
}
return window.fetch(url, Object.assign(options, config)).then((res) => {
return res.text()
})
}})
注: 如果跨域请求带cookie,那么Access-Control-Allow-Origin不能设置为*
micro-app支持两种渲染微前端的模式,默认模式和umd模式。
默认模式:子应用在初次渲染和后续渲染时会顺序执行所有js,以保证多次渲染的一致性。(可以满足大多数项目)
umd模式:子应用暴露出mount、unmount方法,此时只在初次渲染时执行所有js,后续渲染时只会执行这两个方法。(多次渲染时性能会更好)
我的项目是否需要切换为umd模式?
如果子应用渲染和卸载不频繁,那么使用默认模式即可,如果子应用渲染和卸载非常频繁建议使用umd模式。
切换为umd模式:子应用在window上注册mount和unmount方法
src/app_first/src/main.js
/**引入 publicPath 设置 */
import './micro'
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
// new Vue({
// router,
// render: function (h) { return h(App) }
// }).$mount('#app')
let app
/**
* 挂载函数
*/
function mount () {
app = new Vue({
el: '#app',
router,
render: function (h) { return h(App) }
})
}
/**
* 卸载函数
*/
function unmount () {
app.$destroy()
app.$el.innerHTML = ''
app = null
}
/**
* 微前端环境下,注册mount和unmount方法
*/
if (window.__MICRO_APP_ENVIRONMENT__)
window[`micro-app-${window.__MICRO_APP_NAME__}`] = { mount, unmount }
else
mount()
路由类型约束:
1、 基座是hash路由,子应用也必须是hash路由
2、 基座是history路由,子应用可以是hash或history路由
基础路由:
通常基座应用和子应用各有一套路由系统,为了防止冲突,基座需要分配一个路由给子应用,称之为基础路由,子应用可以在这个路由下渲染,但不能超出这个路由的范围,这就是基础路由的作用。
使用方式:
基座应用中通过设置 的baseroute属性下发,子应用通过window.__MICRO_APP_BASE_ROUTE__获取此值并设置基础路由。
注:
1、 如果基座是history路由,子应用是hash路由,不需要设置基础路由baseroute
2、 如果子应用只有一个页面,没有使用react-router,vue-router之类,也不需要设置基础路由baseroute
页面代码配置:基座和子应用都使用的是history路由
路由配置:
base/src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import { CHILD_PREFIX } from '@/micro/config.js'
Vue.use(VueRouter)
const routes = [
{
path: '/'
, name: 'Home'
, component: () => import('../views/Home.vue')
}
, {
path: '/about'
, name: 'About'
, component: () => import('../views/About.vue')
}
, {
path: `/${CHILD_PREFIX}/first-child`
, name: 'FirstChild'
, children: [
{
path: 'home'
, name: 'FirstHome'
}
, {
path: 'about'
, name: 'FirstAbout'
}
]
}
, {
path: `/${CHILD_PREFIX}/second-child`
, name: 'SecondChild'
, children: [
{
path: 'home'
, name: 'SecondHome'
}
, {
path: 'about'
, name: 'SecondAbout'
}
]
}
]
const router = new VueRouter({
mode: 'history'
, routes
})
export default router
app_first/src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
{
path: window.__MICRO_APP_BASE_ROUTE__ || '/' /**根据项目运行的不同环境,设置路径的前缀 */
, name: 'Home'
, redirect: { name: 'FirstHome' }
, component: () => import('../views/Empty.vue')
, children: [
{
path: 'home'
, name: 'FirstHome'
, component: () => import('../views/Home.vue')
}
, {
path: 'about'
, name: 'FirstAbout'
, component: () => import('../views/About.vue')
}
]
}
]
const router = new VueRouter({
mode: 'history',
routes
})
export default router
问题: 开发者想通过基座应用的侧边栏跳转,从而控制子应用的页面,这其实是做不到的,只有子应用的路由实例可以控制自身的页面。
要解决以上问题,有三种方法:
// 官网如下介绍
window.history.pushState(null, '', 'page2')
// 主动触发一次popstate事件
window.dispatchEvent(new PopStateEvent('popstate', { state: null }))
这个我在代码中试了一下,不管是基座跳转子应用,还在子应用跳转基座,还是子应用直接互相跳转都可以,主要是url写对就好了
<button @click="jump()">基座跳转到子应用1homebutton>
jump() {
console.log('window.history', window.history)
window.history.pushState(null, '', 'http://localhost:3000/app/first-child/home')
// 主动触发一次popstate事件
window.dispatchEvent(new PopStateEvent('popstate', { state: null }))
},
适用场景:基座控制子应用跳转(类似于之前讲的数据通信,基座app中传递路径,子应用监听数据变化并执行跳转)
base/src/App.vue
import microApp from '@micro-zoe/micro-app'
microApp.setData('子应用name', { path: '/new-path/' })
app_first/src/App.vue
// 监听基座下发的数据变化
window.microApp.addDataListener((data) => {
// 当基座下发跳转指令时进行跳转
if (data.path) {
router.push(data.path)
}})
适用场景:子应用控制基座跳转
base/src/App.vue
<template>
<micro-app
name='子应用名称'
url='url'
:data='microAppData'
></micro-app></template>
<script>
export default {
data () {
return {
microAppData: {
pushState: (path) => {
this.$router.push(path)
}
}
}
},
}
</script>
app_first/src/App.vue
window.microApp.getData().pushState(path)
关于打包配置,文档中说的很简单,介绍了如下配置,我在这边也是浪费了很长时间才打出来正确的包
// vue.config.js
module.exports = {
outputDir: 'my-app',
publicPath: process.env.NODE_ENV === 'production' ? '/my-app/' : '', // bad ❌
publicPath: '/my-app/', // good
}
我这边以app_first为例,进行了配置
打开js文件看了一下,发现有生成map文件,如下:
查找发现需要添加配置:
productionSourceMap: false,
相关配置可参考:
配置完后重新在打包,还是报错
解决方法
主要配置代码:(记得去注释router中的mode: ‘history’,)
注:
(1)后面我在部署的时候打了好多包,最后发现其实router中的mode: ‘history’,不需要注释也可以打包成功,但是我就不懂开始看打包的时候,为啥就一直报错呢
(2)部署的时候遇到基座刷新报错500的问题,打包时基座的publicPath要改成绝对路径,即:‘/‘而不是’./’。要去掉点(.)。当然下面代码中我已经改掉了。 publicPath: ‘/’
app_first/vue.config.js
module.exports = {
productionSourceMap: false,
outputDir: 'dist',
publicPath: './',
devServer: {
host: 'localhost',
port: 3001,
headers: { // 设置本地运行的跨域权限
'Access-Control-Allow-Origin': '*',
}
}
}
app_second/vue.config.js
module.exports = {
productionSourceMap: false,
outputDir: 'dist',
publicPath: './',
devServer: {
open: true,
host: 'localhost',
port: 3002,
headers: { // 设置本地运行的跨域权限
'Access-Control-Allow-Origin': '*'
}
}
}
base/vue.config.js
module.exports = {
productionSourceMap: false,
outputDir: 'dist',
publicPath: '/',
devServer: {
host: 'localhost',
port: 3000
}
}
到此打包过程就结束了,按之前对微前端的了解,每个子应用都是独立打包独立部署。那接下来就要开始部署了。
官网这边介绍的是用nginx部署,作为前端我也没玩过这东西,也不知道怎么搞,但是微前端部署需要用到。百度大概了一下,先去官网下载一个nginx
然后对于配置,还是一脑壳迷雾浆糊,因为来没搞过,一点操作意识都没有,不知道从哪开始下手,找后台大哥咨询了一下才大概懂得怎么配置
下载好的nginx安装包解压后效果图如下,一般需要配置的地方如下图所示:
下面看看部署过程,我这边打包好的文件没有放在html中,放哪都行,只要配置路径写对即可,部署时也遇到很多问题,下面直接展示最终成功结果。
1、部署文件目录修改,我直接把打包的基座和子应用都并列放在了文件夹下
2、配置nginx.conf,主要配置如下:
这边需要注意图中标注的部分,可以看出配置了三个,分别是基座和子应用
配置代码:
server {
listen 3000;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root base/dist;
add_header Access-Control-Allow-Origin *;
if ( $request_uri ~* ^.+.(js|css|jpg|png|gif|tif|dpg|jpeg|eot|svg|ttf|woff|json|mp4|rmvb|rm|wmv|avi|3gp)$ ){
add_header Cache-Control max-age=7776000;
add_header Access-Control-Allow-Origin *;
}
try_files $uri $uri/ /index.html;
}
}
server {
listen 3001;
server_name localhost;
location / {
root first-child/dist;
add_header Access-Control-Allow-Origin *;
if ( $request_uri ~* ^.+.(js|css|jpg|png|gif|tif|dpg|jpeg|eot|svg|ttf|woff|json|mp4|rmvb|rm|wmv|avi|3gp)$ ){
add_header Cache-Control max-age=7776000;
add_header Access-Control-Allow-Origin *;
}
try_files $uri $uri/ /index.html;
}
}
server {
listen 3002;
server_name localhost;
location / {
root second-child/dist;
add_header Access-Control-Allow-Origin *;
if ( $request_uri ~* ^.+.(js|css|jpg|png|gif|tif|dpg|jpeg|eot|svg|ttf|woff|json|mp4|rmvb|rm|wmv|avi|3gp)$ ){
add_header Cache-Control max-age=7776000;
add_header Access-Control-Allow-Origin *;
}
try_files $uri $uri/ /index.html;
}
}
整个文件代码:(方便对比,但是nginx版本不一样多多少少肯定会有差距的,所以以下配置文件仅供参考,真正用到的只有上面的配置代码)
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
server {
listen 3000;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root base/dist;
add_header Access-Control-Allow-Origin *;
if ( $request_uri ~* ^.+.(js|css|jpg|png|gif|tif|dpg|jpeg|eot|svg|ttf|woff|json|mp4|rmvb|rm|wmv|avi|3gp)$ ){
add_header Cache-Control max-age=7776000;
add_header Access-Control-Allow-Origin *;
}
try_files $uri $uri/ /index.html;
}
}
server {
listen 3001;
server_name localhost;
location / {
root first-child/dist;
add_header Access-Control-Allow-Origin *;
if ( $request_uri ~* ^.+.(js|css|jpg|png|gif|tif|dpg|jpeg|eot|svg|ttf|woff|json|mp4|rmvb|rm|wmv|avi|3gp)$ ){
add_header Cache-Control max-age=7776000;
add_header Access-Control-Allow-Origin *;
}
try_files $uri $uri/ /index.html;
}
}
server {
listen 3002;
server_name localhost;
location / {
root second-child/dist;
add_header Access-Control-Allow-Origin *;
if ( $request_uri ~* ^.+.(js|css|jpg|png|gif|tif|dpg|jpeg|eot|svg|ttf|woff|json|mp4|rmvb|rm|wmv|avi|3gp)$ ){
add_header Cache-Control max-age=7776000;
add_header Access-Control-Allow-Origin *;
}
try_files $uri $uri/ /index.html;
}
}
# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
# listen 8000;
# listen somename:8080;
# server_name somename alias another.alias;
# location / {
# root html;
# index index.html index.htm;
# }
#}
# HTTPS server
#
#server {
# listen 443 ssl;
# server_name localhost;
# ssl_certificate cert.pem;
# ssl_certificate_key cert.key;
# ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 5m;
# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;
# location / {
# root html;
# index index.html index.htm;
# }
#}
}
启动之后打开浏览器,在地址栏输入以下地址,可以看到对应的基座和子应用页面
(1)基座
127.0.0.1:3000
127.0.0.1:3001
127.0.0.1:3002
到此对官网的学习就差不多结束了,对于micro-app,本人纯新手,以上都是对官网的学习,如果文章中有什么错误,欢迎下方评论纠正。