微前端入门——Micro-App开发实践

5/25/2022 微前端Web-Component

# 一、业务背景

# 1.1 工作台系统

工作台

# 1.2 巨石应用

巨石应用

# 二、什么是微前端

微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将单页面前端应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用还可以独立开发、独立运行、独立部署,同时,它们也可以在共享组件的同时进行并行开发。

它主要解决了两个问题:

  • 1.随着项目迭代应用越来越庞大,难以维护。
  • 2.跨团队或跨部门协作开发项目导致效率低下的问题。

micro-app

# 三、快速开始

# 基座应用

1、安装依赖

npm i @micro-zoe/micro-app --save
1

2、在入口处引入

// main.js
import microApp from '@micro-zoe/micro-app';

microApp.start();
1
2
3
4

3、分配一个路由给子应用

// router.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import MyPage from './my-page.vue';

Vue.use(VueRouter);

const routes = [
  {
    // 👇 非严格匹配,/my-page/* 都指向 MyPage 页面
    path: '/my-page/*', // vue-router@4.x path的写法为:'/my-page/:page*'
    name: 'my-page',
    component: MyPage
  }
];

export default routes;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

4、在MyPage页面中嵌入子应用

<!-- my-page.vue -->
<template>
  <div>
    <h1>子应用</h1>
    <!--
      name(必传):应用名称
      url(必传):应用地址,会被自动补全为http://localhost:3000/index.html
      baseroute(可选):基座应用分配给子应用的基础路由,就是上面的 `/my-page`
     -->
    <micro-app name='app1' url='http://localhost:3000/' baseroute='/my-page'></micro-app>
  </div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12

# 子应用

1、设置基础路由 (如果基座应用是history路由,子应用是hash路由,这一步可以省略)

// main.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import routes from './router';

const router = new VueRouter({
  // 👇 设置基础路由,子应用可以通过window.__MICRO_APP_BASE_ROUTE__获取基座下发的baseroute,如果没有设置baseroute属性,则此值默认为空字符串
  base: window.__MICRO_APP_BASE_ROUTE__ || '/',
  routes
});

let app = new Vue({
  router,
  render: (h) => h(App)
}).$mount('#app');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

2、在 webpack-dev-server 的 headers 中设置跨域支持。
① 开发环境,修改配置文件:

devServer: {
  headers: {
    'Access-Control-Allow-Origin': '*',
  }
},
1
2
3
4
5

② 生产环境,修改 Nginx 配置文件:

location / {
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
    add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';

    if ($request_method = 'OPTIONS') {
        return 204;
    }
}
1
2
3
4
5
6
7
8
9

# 四、数据通信

# 4.1 子应用获取来自基座应用的数据

micro-app会向子应用注入名称为microApp的全局对象,子应用通过这个对象和基座应用进行数据交互。

有两种方式获取来自基座应用的数据:

# 方式 1:直接获取数据

const data = window.microApp.getData(); // 返回基座下发的data数据
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()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 4.2 子应用向基座应用发送数据

// dispatch只接受对象作为参数
window.microApp.dispatch({ type: '子应用发送的数据' });
1
2

# 4.3 基座应用向子应用发送数据

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

# 方式 1: 通过 data 属性发送数据

<template>
  <micro-app name='my-app' url='xx' :data='dataForChild' //
  data只接受对象类型,数据变化时会重新发送 />
</template>

<script>
  export default {
    data() {
      return {
        dataForChild: { type: '发送给子应用的数据' }
      };
    }
  };
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 方式 2: 手动发送数据

手动发送数据需要通过name指定接受数据的子应用,此值和<micro-app>元素中的name一致。

import microApp from '@micro-zoe/micro-app';

// 发送数据给子应用 my-app,setData第二个参数只接受对象类型
microApp.setData('my-app', { type: '新的数据' });
1
2
3
4

# 4.4 基座应用获取来自子应用的数据

基座应用获取来自子应用的数据有三种方式:

# 方式 1:直接获取数据

import microApp from '@micro-zoe/micro-app';

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

# 方式 2: 监听自定义事件 (datachange)

<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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 方式 3: 绑定监听函数

绑定监听函数需要通过name指定子应用,此值和<micro-app>元素中的name一致。

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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 4.5 全局数据通信

全局数据通信会向基座应用和所有子应用发送数据,在跨应用通信的场景中适用。

# 4.5.1 发送全局数据

# ① 基座应用

import microApp from '@micro-zoe/micro-app';

// setGlobalData只接受对象作为参数
microApp.setGlobalData({ type: '全局数据' });
1
2
3
4

# ② 子应用

// setGlobalData只接受对象作为参数
window.microApp.setGlobalData({ type: '全局数据' });
1
2

# 4.5.2 获取全局数据

import microApp from '@micro-zoe/micro-app'

// 直接获取数据
const globalData = microApp.getGlobalData() // 返回全局数据

// 子应用:
// const globalData = window.microApp.getGlobalData()

function dataListener (data) {
  console.log('全局数据', data)
}

/**
 * 绑定监听函数
 * dataListener: 绑定函数
 * autoTrigger: 在初次绑定监听函数时如果有缓存数据,是否需要主动触发一次,默认为false
 */
microApp.addGlobalDataListener(dataListener: Function, autoTrigger?: boolean)

// 解绑监听函数
microApp.removeGlobalDataListener(dataListener: Function)

// 清空基座应用绑定的所有全局数据监听函数
microApp.clearGlobalDataListener()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 4.6 数据通信数据流图

数据流

# 4.7 micro-app 应用架构设计图

基于 micro-app 的特点,我设计了以下应用架构图:
1、把基座应用和子应用的公用组件封装成 npm 包引用, 一些第三方的 js 和 css 等资源,可以使用资源共享使用,减少资源的加载。
2、在基座应用通过路由系统控制子应用的跳转切换,子应用并不能互跳, 需要通过数据通信,集中交给基座应用来管理路由。
3、在基座应用和子应用的全局数据通信层,加上 store 进行状态管理,以此触发全局的视图更新。

架构设计图

# 五、应用优化

# 5.1 预加载

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

microApp.preFetch(Array<app> | Function => Array<app>)
1

preFetch 接受 app 数组或一个返回 app 数组的函数,app 的值如下:

app: {
  name: string, // 应用名称,必传
  url: string, // 应用地址,必传
  disableScopecss?: boolean // 是否关闭样式隔离,非必传
  disableSandbox?: boolean // 是否关闭沙盒,非必传
}
1
2
3
4
5
6

# 5.2 资源共享

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

设置资源共享的方式有两种:

# 5.2.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地址
  }
})
1
2
3
4
5
6
7
8
9

# 5.2.2 方式二、global 属性

在 link、script 设置global属性会将文件提取为公共文件,共享给其它应用。

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

# 使用方式

<link rel="stylesheet" href="xx.css" global />
<script src="xx.js" global></script>
1
2

# 5.3 keep-alive

在应用之间切换时,你有时会想保留这些应用的状态,以便恢复用户的操作行为和提升重复渲染的性能,此时开启 keep-alive 模式可以达到这样的效果。

开启 keep-alive 后,应用卸载时不会销毁,而是推入后台运行。

# 5.3.1 使用方式

<micro-app name="xx" url="xx" keep-alive></micro-app>
1

micro-app 的 keep-alive 是应用级别的,它只会保留当前正在活动的页面状态,以保证应用被卸载和重新渲染时的状态保留,如果想要缓存具体的页面或组件,需要使用子应用框架的能力,如:vue 的 keep-alive。

# 六、实现原理

借鉴了 WebComponent 的思想,通过 CustomElement 结合自定义的 ShadowDom,将微前端封装成一个类 WebComponent 组件,从而实现微前端的组件化渲染。并且由于自定义 ShadowDom 的隔离特性,micro-app 不需要像 single-spa 和 qiankun 一样要求子应用修改渲染逻辑并暴露出方法,也不需要修改 webpack 配置。

# 6.1 子应用 DOM 结构

子应用

# 6.2 Web Component - 脱离框架的组件化解决方案

Web Components是一系列加入 w3c 的 HTML 和 DOM 的特性,使得开发者可以创建可复用的组件。由于 web components 是由 w3c 组织去推动的,因此它很有可能在不久的将来成为浏览器的一个标配。 抛开官方为了推进这套方案的宣传文案不说,单从 原生、定制化标签 这些字眼就已经足够让开发者忍不住去了解它。 使用 Web Component编写的组件是脱离框架的,换言之,也就是说使用 Web Component 开发的组件库,是适配所有框架的,不会像 Antd 这样需要对 Vue、React 等框架出不同的版本。 编写 Web Component组件后,你可以这样使用它:

//引入自定义的组件
<script src="/build/myCom.js"></script>

//组件的使用
<my-com></my-com>
1
2
3
4
5

# 6.2.1 核心技术

  • Custom elements(自定义元素):一组 JavaScript API,允许您定义 custom elements 及其行为,然后可以在您的用户界面中按照需要使用它们。
  • Shadow DOM(影子 DOM):一组 JavaScript API,用于将封装的 "影子" DOM 树 附加到元素(与主文档 DOM 分开呈现)并控制其关联的功能。通过这种方式,您可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
  • HTML templates(HTML 模板):< template > 和 < slot > 元素使您可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用。
  • HTML Imports(HTML 导入):一旦定义了自定义组件,最简单的重用它的方法就是使其定义细节保存在一个单独的文件中,然后使用导入机制将其导入到想要实际使用它的页面中。

# 6.2.2 生命周期方法

  • connectedCallback:
    当 web component 被添加到 DOM 时,会调用这个回调函数,这个函数只会被执行一次。可以在这个回调函数中完成一些初始化操作,比如更加参数设置组件的样式。
  • disconnectedCallback:
    当 web component 从文档 DOM 中删除时执行。
  • adoptedCallback:
    当 web component 被移动到新文档时执行。
  • attributeChangedCallback:
    被监听的属性发生变化时执行。

# 6.2.3 完成一个简单的组件

# 1、定义组件

class MyButton extends HTMLElement {
  constructor() {
    super();
    const template = document.getElementById('mybutton');
    const content = template.content.cloneNode(true);
    this.appendChild(content);
  }
}
1
2
3
4
5
6
7
8

# 2、定义组件模板

<template id="mybutton">
  <button>Add</button>
</template>
1
2
3

# 3、注册组件

window.customElements.define('my-button', MyButton);
1

# 4、使用组件

<body>
  <my-button></my-button>
</body>
1
2
3

这样, 一个简单的 Web Component 就完成了。

# 6.3 micro-app 实现原理(简易核心原理代码演示)

https://github.com/micro-zoe/micro-app/issues/17 (opens new window)

# 七、总结

1、微前端是工作台系统、巨石应用系统中,提高开发效率,渐进式升级的解决方案。
2、通过 micro-app 标签,定义子应用的 name 属性、url 属性,在基座应用分配路由的形式集成子应用。
3、通过 micro-app 的 setData 方法、dispatch 方法、监听事件,完成基座应用与子应用的数据通信,我们可以结合应用的数据管理模块 store,在数据通信时,触发全局视图的数据更新。
4、使用预加载、资源共享、keep-alive、按需引入和懒加载,可以提升子应用首屏加载的体验,以及
在基座应用和子应用切换时的交互体验。
5、我们了解了 micro-app 使用 webcomponent 渲染的基本实现原理。