micro-app的简单学习

简述

本文承接上一篇手把手教你使用vue2搭建micro-app对micro-app进行简单的认识与学习。
因为上一篇只是对micro-app的搭建,并没有对具体的内容进行深入了解,所以本文是在上一篇文章代码的基础上对micro-app官网,的初步了解。

1、在基座中调用子应用

在了解完微前端之后,我们知道,微前端是由一个基座和多个子应用构成的,那么如何在基座中调用子应用呢?官网中如下介绍:

// 官网中看到是使用此标签在父子间中嵌入子组件的

<micro-app name='app1' url='http://localhost:3000/' baseroute='/my-page'>micro-app>

参照官网例子嵌入子应用,效果如下:
micro-app的简单学习_第1张图片
代码如下:


<micro-app name='app_first' url='http://localhost:3001/'>micro-app>
<micro-app name='app_second' url='http://localhost:3002/'>micro-app>

上述例子中是单个子应用的,下面也分析了一下搭建的实例中的子应用嵌入,这边是通过点击router-link时,动态嵌入对应的子应用。具体内容我都加了注释说明
micro-app的简单学习_第2张图片
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>

2、配置项

通过配置项,可以决定开启或关闭某些功能,配置项都是对标签的配置。
如果希望在某个应用中使用,可以如下配置:


如果想要做一个全局配置,可以如下配置:

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
  })

上方代码做了简单描述,具体还得看官网地址

3、生命周期

通过CustomEvent定义生命周期,在组件渲染过程中会触发相应的生命周期事件。

3-1、micro-app标签有如下五个生命周期

(1)created:标签初始化后,加载资源前触发
(2)beforemount:加载资源完成后,开始渲染之前触发
(3)muonted:子应用渲染结束后触发
(4)unmount:子应用卸载时触发
(5)error:子应用渲染出错时触发,只有会导致渲染终止的错误才会触发此生命周期

3-2、在单个组件中监听生命周期

首先看一下对生命周期使用的例子和效果,方便对代码的理解,正常情况下组件能使用到的生命周期就4个,在后期开发中可根据合适的场景调用对应的生命周期。
micro-app的简单学习_第3张图片

<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`)
}

3-3、全局监听

如果在开发中,有用到一些通用的监听方法,可在全局进行生命周期配置,配置如下:

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')
    }
  }})

4、环境变量

之前在搭建micro-app的时候,有配置一些环境变量,当时就是照着例子抄的,现在要大概了解一下大致的环境变量配置。

4-1、判断应用是否在微前端环境中

micro-app的简单学习_第4张图片

// 设置 webpack 的公共路径
if (window.__MICRO_APP_ENVIRONMENT__) { // 判断是否在微前端环境中
    // eslint-disable-next-line
    __webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__ || '/'
}

4-2、应用名称

micro-app的简单学习_第5张图片

/**
* 微前端环境下,注册mount和unmount方法
*/
if (window.__MICRO_APP_ENVIRONMENT__)
   window[`micro-app-${window.__MICRO_APP_NAME__}`] = { mount, unmount }
else
   mount()

4-3、子应用的静态资源前缀

micro-app的简单学习_第6张图片

// 设置 webpack 的公共路径
if (window.__MICRO_APP_ENVIRONMENT__) { // 判断是否在微前端环境中
    // eslint-disable-next-line
    __webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__ || '/'
}
/**引入 publicPath 设置 */
import './micro'

4-4、子应用的基础路由

micro-app的简单学习_第7张图片

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>

4-5、判断是否是基座应用

micro-app的简单学习_第8张图片

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')
}

5、JS沙箱

js沙箱,就是让你的程序跑在一个隔离的环境下,不对外界的其他程序造成影响,通过创建类似沙盒的独立作业环境,在其内部运行的程序并不能对硬盘产生永久性的影响。

micro-app使用Proxy拦截了用户全局操作的行为,防止对windows的访问和修改,避免全局变量污染。micro-app中的每个子应用都运行在沙箱环境,以获取相对纯净的运行空间。

沙箱默认是开启的,正常情况不建议关闭,因为关闭可能会出现一些不可预知的问题

目前有以下3种方式在子应用中获取外部真实window

1、new Function(“return window”)() 或Function(“return window”)()
2、(0, eval)(‘window’)
3、window.rawWindow
micro-app的简单学习_第9张图片
子应用中使用window.a,父应用也有window.a,两者会互相影响,使用如下方法解决:

(function(window, self) {
  with(window) {
    子应用的js代码
  }
}).call(代理对象, 代理对象, 代理对象)

6、样式隔离

micro-app的样式隔离默认是开启的,开启后会以标签作为样式作用域,利用标签的name属性为每个样式添加前缀,将子应用的样式影响禁锢在当前标签区域。 但基座应用的样式依然会对子应用产生影响,如果发生样式污染,推荐通过约定前缀或CSS Modules方式解决。

.test {
  color: red;}


/* 转换为 */micro-app[name=xxx] .test {
  color: red;
}

7、元素隔离

micro-app模拟实现了类似ShadowDom的功能,元素不会逃离元素边界,子应用只能对自身的元素进行增、删、改、查的操作。
如:基座应用和子应用都有一个元素

,此时子应用通过document.querySelector(‘#root’)获取到的是自己内部的#root元素,而不是基座应用的。
当然,基座应用是可以获取子应用元素的, 在微前端下基座拥有统筹全局的作用,没有对基座应用操作子应用元素的行为进行限制。
micro-app的简单学习_第10张图片

8、数据通信

micro-app提供了一套灵活的数据通信机制,方便基座应用和子应用之间的数据传输。
正常情况下,基座应用和子应用之间的通信是绑定的,基座应用只能向指定的子应用发送数据,子应用只能向基座发送数据,这种方式可以有效的避免数据污染,防止多个子应用之间相互影响。
同时还提供了全局通信,方便跨应用之间的数据通信。

8-1、子应用获取来自基座应用的数据

8-1-1、直接获取数据
// 官网是这样介绍的
const data = window.microApp.getData() // 返回基座下发的data数据

根据官网介绍,我直接在子应用打印了上述变量结果,但是只获取到子应用名称,效果如下:
micro-app的简单学习_第11张图片
接下来我好奇这getData()获取到到底是啥东西,于是去基座子应用调用添加了data,这样在子应用打印出来的内容就会多一点了,所以感觉如果想要在子应用中获取到基座数据,就需要将数据添加到data中
micro-app的简单学习_第12张图片

8-1-2、绑定监听函数
// 官网是如下介绍的

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()

同样,我在项目中试了一下,是可以拿到基座data数据的
micro-app的简单学习_第13张图片

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、子应用向基座应用发送数据

// 官网中如下介绍
// dispatch只接受对象作为参数
window.microApp.dispatch({type: '子应用发送的数据'})

由于这边只介绍了子应用向基座应用发送数据,但是没有介绍发送过去的数据在基座怎么接收,所以这个内容写完后,我也不知道到底有没有发送成功,就先去找了一个基座接收子应用数据的方法,来证明子应用有没有向基座发送成功。
micro-app的简单学习_第14张图片
micro-app的简单学习_第15张图片
子应用发送数据:

<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-3、基座应用向子应用发送数据

基座向子应用发送数据的方式有两种

8-3-1、通过data属性发送数据

我觉得和8-1中我子应用获取基座应用时,基座应该的数据写法是一致的,就是中要添加:data=‘数据’,具体可看8-1中我的示例代码,官方介绍如下:

<template>
  template>


<script>export default {
  data () {
    return {
      dataForChild: {type: '发送给子应用的数据'}
    }
  }}
script>
8-3-2、 手动发送数据

手动发送数据需要通过name指定接受数据的子应用,此值和元素中的name一致。官网介绍如下:

import microApp from '@micro-zoe/micro-app'
// 发送数据给子应用 my-app,setData第二个参数只接受对象类型
microApp.setData('my-app', {type: '新的数据'})

micro-app的简单学习_第16张图片
micro-app的简单学习_第17张图片
基座:

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-4、基座应用获取来自子应用的数据

关于在基座中获取子应用的数据,官方介绍了三种方法,其中第二种方式在8-2中已经尝试过了,这边就只尝试一三两种方法了。

8-4-1、直接获取数据
// 官方介绍
import microApp from '@micro-zoe/micro-app'

const childData = microApp.getData(appName) // 返回子应用的data数据

micro-app的简单学习_第18张图片
基座:

mounted () {
   const childData = microApp.getData('first-child')
   console.log('接收子应用向基座发送得数据', childData)
   // console.log(`子模块挂载(子应用渲染结束后触发)-mounted`)
},

子应用:

created () {
   // 向基项目发送数据
   window.microApp && window.microApp.dispatch({ name: '就将计就计',type: '子应用发送的数据', msg: '成功' })
},
8-4-2、监听自定义事件

监听自定义事件(datachange),8-2中已介绍,在此不多介绍,看8-2即可

8-4-3、绑定监听函数

绑定监听函数需要通过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)

micro-app的简单学习_第19张图片
micro-app的简单学习_第20张图片
基座:

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-5、全局数据通信

全局数据通信会向基座应用和所有子应用发送数据,在跨应用通信的场景中适用。全局通信在基座应用和子应用的用法跟上面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()

8-6、关闭沙箱后的通信方式

沙箱关闭后,子应用默认的通信功能失效,此时可以通过手动注册通信对象实现一致的功能。
注册方式:在基座应用中为子应用初始化通信对象

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: '子应用发送的数据'})

9、静态资源

9-1、资源路径自动补全

micro-app子应用默认提供资源路径自动补全功能。如: 图片/myapp/test.png,在最终渲染时会补全为http://localhost:8080/myapp/test.png
主要争对于link、script、img、background-imge、@font-face等

9-2、publicPath

如果自动补全失败,可以采用运行时publicPath方案解决。我们之前配置环境变量时已经有配置了,app_first为例
micro-app的简单学习_第21张图片
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'

9-3、资源共享

当多个子应用拥有相同的js或css资源,可以指定这些资源在多个子应用之间共享,在子应用加载时直接从缓存中提取数据,从而提高渲染效率和性能。

9-3-1、 globalAssets

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地址
  }})

我们搭建的项目已经做好了配置,在base基座下配置
micro-app的简单学习_第22张图片

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: []
}
9-3-2、global属性

在link、script设置global属性会将文件提取为公共文件,共享给其它应用。
设置global属性后文件第一次加载会放入公共缓存,其它子应用加载相同的资源时直接从缓存中读取内容,从而提升渲染速度。

<link rel="stylesheet" href="xx.css" exclude>
<script src="xx.js" exclude>script>
<style exclude>style>

10、预加载

预加载是指在应用尚未渲染时提前加载资源并缓存,从而提升首屏渲染速度。
预加载并不是同步执行的,它会在浏览器空闲时间,依照开发者传入的顺序,依次加载每个应用的静态资源,以确保不会影响基座应用的性能。

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' }
  // ],})

在基座代码中进行配置配置:
micro-app的简单学习_第23张图片

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/` }
]

11、插件系统

插件系统可以赋予开发者灵活处理静态资源的能力,对有问题的资源文件进行修改
插件系统的主要作用是对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
        }
      }]
    }
  }})

12、多层嵌套

micro-app支持多层嵌套,即子应用可以嵌入其它子应用,但为了防止标签名冲突,子应用中需要做一些修改。
在子应用中设置tagName:

microApp.start({
  tagName: 'micro-app-xxx', // 标签名称必须以 `micro-app-` 开头
})

在子应用中使用新定义的标签进行渲染,如:

<micro-app-xxx name='xx' url='xx'>micro-app-xxx>

注: 无论嵌套多少层,name都要保证全局唯一。
micro-app的简单学习_第24张图片
安装micro-app插件,安装在app_first目录下,因为我这边想把first嵌套到second下面

npm install @micro-zoe/micro-app --save

micro-app的简单学习_第25张图片
app_first/src/micro/index.js

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>

micro-app的简单学习_第26张图片

13、高级功能

13-1、自定义fetch

通过自定义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不能设置为*

13-2、性能内存优化

micro-app支持两种渲染微前端的模式,默认模式和umd模式。
默认模式:子应用在初次渲染和后续渲染时会顺序执行所有js,以保证多次渲染的一致性。(可以满足大多数项目)
umd模式:子应用暴露出mount、unmount方法,此时只在初次渲染时执行所有js,后续渲染时只会执行这两个方法。(多次渲染时性能会更好)
我的项目是否需要切换为umd模式?
如果子应用渲染和卸载不频繁,那么使用默认模式即可,如果子应用渲染和卸载非常频繁建议使用umd模式。
切换为umd模式:子应用在window上注册mount和unmount方法
micro-app的简单学习_第27张图片
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()

14、路由

路由类型约束:
1、 基座是hash路由,子应用也必须是hash路由
2、 基座是history路由,子应用可以是hash或history路由
基础路由:
通常基座应用和子应用各有一套路由系统,为了防止冲突,基座需要分配一个路由给子应用,称之为基础路由,子应用可以在这个路由下渲染,但不能超出这个路由的范围,这就是基础路由的作用。
使用方式:
基座应用中通过设置 的baseroute属性下发,子应用通过window.__MICRO_APP_BASE_ROUTE__获取此值并设置基础路由。
注:
1、 如果基座是history路由,子应用是hash路由,不需要设置基础路由baseroute
2、 如果子应用只有一个页面,没有使用react-router,vue-router之类,也不需要设置基础路由baseroute
页面代码配置:基座和子应用都使用的是history路由
在这里插入图片描述
路由配置:
micro-app的简单学习_第28张图片
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

15、应用之间跳转

问题: 开发者想通过基座应用的侧边栏跳转,从而控制子应用的页面,这其实是做不到的,只有子应用的路由实例可以控制自身的页面。
要解决以上问题,有三种方法:

15-1、window.history

// 官网如下介绍

window.history.pushState(null, '', 'page2')

// 主动触发一次popstate事件
window.dispatchEvent(new PopStateEvent('popstate', { state: null }))

这个我在代码中试了一下,不管是基座跳转子应用,还在子应用跳转基座,还是子应用直接互相跳转都可以,主要是url写对就好了

15-1-1、基座跳转到子应用

micro-app的简单学习_第29张图片

<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 }))
},
15-1-2、子应用2跳转到基座

micro-app的简单学习_第30张图片

15-1-3、子应用1跳转到子应用2

micro-app的简单学习_第31张图片

15-2、通过数据通信控制跳转

适用场景:基座控制子应用跳转(类似于之前讲的数据通信,基座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)
  }})

15-3、传递路由实例

适用场景:子应用控制基座跳转
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)

16、打包

关于打包配置,文档中说的很简单,介绍了如下配置,我在这边也是浪费了很长时间才打出来正确的包

// vue.config.js
module.exports = {
  outputDir: 'my-app',
  publicPath: process.env.NODE_ENV === 'production' ? '/my-app/' : '', // bad ❌
  publicPath: '/my-app/', // good 
}

我这边以app_first为例,进行了配置
micro-app的简单学习_第32张图片
micro-app的简单学习_第33张图片
打开js文件看了一下,发现有生成map文件,如下:
micro-app的简单学习_第34张图片
查找发现需要添加配置:

productionSourceMap: false,

micro-app的简单学习_第35张图片
相关配置可参考:
配置完后重新在打包,还是报错
micro-app的简单学习_第36张图片
解决方法
micro-app的简单学习_第37张图片
主要配置代码:(记得去注释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
    }
}

到此打包过程就结束了,按之前对微前端的了解,每个子应用都是独立打包独立部署。那接下来就要开始部署了。

17、部署

官网这边介绍的是用nginx部署,作为前端我也没玩过这东西,也不知道怎么搞,但是微前端部署需要用到。百度大概了一下,先去官网下载一个nginx
micro-app的简单学习_第38张图片
然后对于配置,还是一脑壳迷雾浆糊,因为来没搞过,一点操作意识都没有,不知道从哪开始下手,找后台大哥咨询了一下才大概懂得怎么配置
下载好的nginx安装包解压后效果图如下,一般需要配置的地方如下图所示:
micro-app的简单学习_第39张图片
下面看看部署过程,我这边打包好的文件没有放在html中,放哪都行,只要配置路径写对即可,部署时也遇到很多问题,下面直接展示最终成功结果。
1、部署文件目录修改,我直接把打包的基座和子应用都并列放在了文件夹下

micro-app的简单学习_第40张图片
micro-app的简单学习_第41张图片
2、配置nginx.conf,主要配置如下:
这边需要注意图中标注的部分,可以看出配置了三个,分别是基座和子应用
micro-app的简单学习_第42张图片

配置代码:

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;
    #    }
    #}


}

3、配置完成后启动nginx
micro-app的简单学习_第43张图片
micro-app的简单学习_第44张图片

启动之后打开浏览器,在地址栏输入以下地址,可以看到对应的基座和子应用页面

(1)基座

127.0.0.1:3000

micro-app的简单学习_第45张图片
(2)子应用1

127.0.0.1:3001

micro-app的简单学习_第46张图片
(3)子应用2

127.0.0.1:3002

micro-app的简单学习_第47张图片

总结

到此对官网的学习就差不多结束了,对于micro-app,本人纯新手,以上都是对官网的学习,如果文章中有什么错误,欢迎下方评论纠正。

你可能感兴趣的:(微前端micro-app,micro-app,通信方式,mocro-app部署,micro-app打包)