关于微前端《2》

114 阅读11分钟

由于上个项目做的是关于微前端的子业务,以及近期项目开发也是微前端,今日无事,就微前端的自我了解和熟悉进行一个小小的总结吧,也算是自己的一个学习笔记。 前期的项目暂称其为项目A,近期项目称项目B吧,项目A使用的是微前端框架,子业务合成到基座都是同事完成,项目B需要我自己动手完成,配置好打包发布给隔壁主基座同事。

1-微前端介绍:

主应用和微应用的集成,也就是基座和子应用的分离开发,微前端这是一种架构,将巨石应用拆分成多个可独立开发,部署,上线,运行的小型(子)应用,对外暴露出一个主基座来统一管理各个子应用的运行,多个子应用在用户无关情况下进行随意切换,由于现在单纯的SPA应用无法满足这种逻辑强,复杂度高,功能多样化的项目了,所以微应用也是一种开发选择。

2-微前端各种框架

据悉知,微前端有著名的qiankun,iFrame,飞冰,icestark,Mooa, Web Components等多种框架,其中qiankun还是很常用的。那就对我所了解的几个框架进行详细介绍一下吧,

2.1 qiankun

其实对于iframe也是非常常见的一种微前端方案,那为什么不详细介绍呢,他会被challenge问题所牵绊,所以大家都不约而同的会放弃使用这个方案。不是为了显现自己的多么强大,只是需要考虑到用户体验度和项目,所以需要寻找更优秀的方案。 qiankun的官网地址:qiankun.umijs.org/zh

2.1.1 qiankun简介

2019 年 6 月,微前端框架 qiankun 正式发布了 1.0 版本,在这一年不到的时间内,我们收获了 4k+ star,收获了来自 single-spa 官方团队的问候,支撑了阿里 200+ 线上应用,也成为社区很多团队选用的微前端解决方案。 qiankun @2.0 带来了一些新能力的同时,只做了很小的 API 调整,1.x 的用户可以很轻松的迁移到 2.x 版本,详细信息见下方 升级指南 小节。 qiankun 是一个生产可用的微前端框架,它基于 single-spa,具备 js 沙箱、样式隔离、HTML Loader、预加载 等微前端系统所需的能力。qiankun 可以用于任意 js 框架,微应用接入像嵌入一个 iframe 系统一样简单。

具体可进入其官网查看qiankun.umijs.org/

qiankun主应用

1-安装

$ yarn add qiankun # 或者 npm i qiankun -S

2- 在主应用中注册微应用

import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: 'react app', // app name registered
    entry: '//localhost:7100',
    container: '#yourContainer',
    activeRule: '/yourActiveRule',
  },
  {
    name: 'vue app',
    entry: { scripts: ['//localhost:7100/main.js'] },
    container: '#yourContainer2',
    activeRule: '/yourActiveRule2',
  },
]);

start();

当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑,所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子。 如果微应用不是直接跟路由关联的时候,你也可以选择手动加载微应用的方式:

import { loadMicroApp } from 'qiankun';

loadMicroApp({
  name: 'app',
  entry: '//localhost:7100',
  container: '#yourContainer',
});

微应用

微应用不需要额外安装任何其他依赖即可接入 qiankun 主应用。

配置微应用的打包工具

除了代码中暴露出相应的生命周期钩子之外,为了让主应用能正确识别微应用暴露出来的一些信息,微应用的打包工具需要增加如下配置:

webpack:

const packageName = require('./package.json').name;


module.exports = {
  output: {
    library: `${packageName}-[name]`,
    libraryTarget: 'umd',
    jsonpFunction: `webpackJsonp_${packageName}`,
  },
};

相关配置介绍可以查看 webpack 相关文档

Vue微应用

  1. 在 src 目录新增 public-path.js
```
if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
```
  1. 入口文件 main.js 修改,为了避免根 id #app 与其他的 DOM 冲突,需要限制查找范围。

    import './public-path';
    import Vue from 'vue';
    import VueRouter from 'vue-router';
    import App from './App.vue';
    import routes from './router';
    import store from './store';
    
    
    Vue.config.productionTip = false;
    
    
    let router = null;
    let instance = null;
    function render(props = {}) {
      const { container } = props;
      router = new VueRouter({
        base: window.__POWERED_BY_QIANKUN__ ? '/app-vue/' : '/',
        mode: 'history',
        routes,
      });
    
    
      instance = new Vue({
        router,
        store,
        render: (h) => h(App),
      }).$mount(container ? container.querySelector('#app') : '#app');
    }
    
    
    // 独立运行时
    if (!window.__POWERED_BY_QIANKUN__) {
      render();
    }
    
    
    export async function bootstrap() {
      console.log('[vue] vue app bootstraped');
    }
    export async function mount(props) {
      console.log('[vue] props from main framework', props);
      render(props);
    }
    export async function unmount() {
      instance.$destroy();
      instance.$el.innerHTML = '';
      instance = null;
      router = null;
    }
    
  2. 打包配置修改(vue.config.js):

    const { name } = require('./package');
    module.exports = {
      devServer: {
        headers: {
          'Access-Control-Allow-Origin': '*',
        },
      },
      configureWebpack: {
        output: {
          library: `${name}-[name]`,
          libraryTarget: 'umd', // 把微应用打包成 umd 库格式
          jsonpFunction: `webpackJsonp_${name}`,
        },
      },
    };
    

2.1.2 关于Icestark

icestark同样也是一个面向大学系统的微前端解决方案,适用于以下场景:

  • 后台比较分散,体验差别大,因为要频繁跳转导致操作效率低,希望能统一收口的一个系统内
  • 单页面应用非常庞大,多人协作成本高,开发/构建时间长,依赖升级回归成本高
  • 系统有二方/三方接入的需求

icestark 在保证一个系统的操作体验基础上,实现各个微应用的独立开发和发版,主应用通过 icestark 管理微应用的注册和渲染,将整个系统彻底解耦。

项目构建在官网有具体介绍,感兴趣可自行学习:micro-frontends.ice.work/docs/guide

3-微应用的路由模式如何选择

对于react和其他技术方向我暂不做详细介绍,关于vue可具体说一下,react-routerangular-routervue-router 这三种路由,都支持 hash 和 history 模式,微应用使用不同的模式在 qiankun 中略有差别。

activeRule 使用 location.pathname 区分微应用

  1. 当微应用是 history 模式时,设置路由 base 即可
  2. 当微应用是 hash 模式时,三种路由的表现不一致 其中vue-router:可响应路由

activeRule 使用 location.hash 区分微应用

当微应用都是 hash 模式时可以使用 hash 区分微应用,主应用的路由模式不限。 vue-router 的 hash 模式下不支持设置路由的 base,需要额外新建一个空的路由页面,将其他所有路由都作为它的 children

const routes = [
  {
    path: '/app-vue-hash',
    name: 'Home',
    component: Home,
    children: [
      // 其他的路由都写到这里
    ],
  },
];

如何部署

建议:主应用和微应用都是独立开发和部署,即它们都属于不同的仓库和服务。

场景 1:主应用和微应用部署到同一个服务器(同一个 IP 和端口)

如果服务器数量有限,或不能跨域等原因需要把主应用和微应用部署到一起。

通常的做法是主应用部署在一级目录,微应用部署在二/三级目录。

微应用想部署在非根目录,在微应用打包之前需要做两件事:

  1. 必须配置 webpack 构建时的 publicPath 为目录名称,更多信息请看 webpack 官方说明 和 vue-cli3 的官方说明
  2. history 路由的微应用需要设置 base ,值为目录名称,用于独立访问时使用。

部署之后注意三点:

  1. activeRule 不能和微应用的真实访问路径一样,否则在主应用页面刷新会直接变成微应用页面。
  2. 微应用的真实访问路径就是微应用的 entryentry 可以为相对路径。
  3. 微应用的 entry 路径最后面的 / 不可省略,否则 publicPath 会设置错误,例如子项的访问路径是 http://localhost:8080/app1,那么 entry 就是 http://localhost:8080/app1/

具体的部署有以下两种方式,选择其一即可。

方案 1:微应用都放在在一个特殊名称(不会和微应用重名)的文件夹下(建议使用

假设我们有一个主应用和 6 个微应用(分别为 vue-hashvue-historyreact-hashreact-historyangular-hashangular-history ),打包后如下放置:

└── html/                     # 根文件夹
    |
    ├── child/                # 存放所有微应用的文件夹
    |   ├── vue-hash/         # 存放微应用 vue-hash 的文件夹
    |   ├── vue-history/      # 存放微应用 vue-history 的文件夹
    |   ├── react-hash/       # 存放微应用 react-hash 的文件夹
    |   ├── react-history/    # 存放微应用 react-history 的文件夹
    |   ├── angular-hash/     # 存放微应用 angular-hash 的文件夹
    |   ├── angular-history/  # 存放微应用 angular-history 的文件夹
    ├── index.html            # 主应用的index.html
    ├── css/                  # 主应用的css文件夹
    ├── js/                   # 主应用的js文件夹
  • vue-history 微应用

    路由设置:

    base: window.__POWERED_BY_QIANKUN__ ? '/app-vue-history/' : '/child/vue-history/',
    

    webpack 打包 publicPath 配置(vue.config.js):

    module.exports = {
      publicPath: '/child/vue-history/',
    };
    

至此主应用已经和微应用都能跑起来了,但是主应用和 vue-historyreact-historyangular-history 微应用是 history 路由,需要解决刷新 404 的问题,nginx 还需要配置一下:

server {
  listen       8080;
  server_name  localhost;


  location / {
    root   html;
    index  index.html index.htm;
    try_files $uri $uri/ /index.html;
  }


  location /child/vue-history {
    root   html;
    index  index.html index.htm;
    try_files $uri $uri/ /child/vue-history/index.html;
  }
  # angular 和 react 的history 配置同上
}

方案 2:微应用直接放在二级目录,但是设置特殊的 activeRule

└── html/                     # 根文件夹
    |
    ├── vue-hash/             # 存放微应用 vue-hash 的文件夹
    ├── vue-history/          # 存放微应用 vue-history 的文件夹
    ├── react-hash/           # 存放微应用 react-hash 的文件夹
    ├── react-history/        # 存放微应用 react-history 的文件夹
    ├── angular-hash/         # 存放微应用 angular-hash 的文件夹
    ├── angular-history/      # 存放微应用 angular-history 的文件夹
    ├── index.html            # 主应用的index.html
    ├── css/                  # 主应用的css文件夹
    ├── js/                   # 主应用的js文件夹

基本操作和上面是一样的,只要保证 activeRule 和微应用的存放路径名不一样即可。

场景 2:主应用和微应用部署在不同的服务器,使用 Nginx 代理访问

一般这么做是因为不允许主应用跨域访问微应用,做法就是将主应用服务器上一个特殊路径的请求全部转发到微应用的服务器上,即通过代理实现“微应用部署在主应用服务器上”的效果。

例如,主应用在 A 服务器,微应用在 B 服务器,使用路径 /app1 来区分微应用,即 A 服务器上所有 /app1 开头的请求都转发到 B 服务器上。

此时主应用的 Nginx 代理配置为:

/app1/ {
  proxy_pass http://www.b.com/app1/;
  proxy_set_header Host $host:$server_port;
}

主应用注册微应用时,entry 可以为相对路径,activeRule 不可以和 entry 一样(否则主应用页面刷新就变成微应用):

registerMicroApps([
  {
    name: 'app1',
    entry: '/app1/', // http://localhost:8080/app1/
    container: '#container',
    activeRule: '/child-app1',
  },
],

对于 webpack 构建的微应用,微应用的 webpack 打包的 publicPath 需要配置成 /app1/,否则微应用的 index.html 能正确请求,但是微应用 index.html 里面的 js/css 路径不会带上 /app1/

module.exports = {
  output: {
    publicPath: `/app1/`,
  },
};

微应用打包的 publicPath 加上 /app1/ 之后,必须部署在 /app1 目录,否则无法独立访问。

另外,如果不想微应用通过代理路径被独立访问,可以根据请求的一些信息判断下,主应用中请求微应用是用 fetch 请求的,可以带参数和 cookie。例如通过请求头参数判断:

if ($http_custom_referer != "main") {
  rewrite /index /404.html;
}

4-微前端优势:

1.技术栈无关:主框架不限制接入应用的技术栈,微应用完全具备自主权 2.独立开发,独立部署:微应用的git仓库独立,微应用部署完成之后,父应用打开的页面同步更新 3.独立运行:每个项目都可以作为一个完整的单独项目去运行,他可能只是一个大后台项目中的某个功能模块,然后所有的微应用组合到一起就是PM想要的完成功能 4.增量升级:在面对各种复杂的场景时,我们通常很难对一个已存的系统做全量的技术栈升级或者重构(深有体会)而微前端可以让我们做到很好的渐近式重构 对于现代应用的不足和痛点: 项目中的组件和功能模块会越来越多,导致整个项目的打包速度变慢; 因为文件夹的数量会随着功能模块的增多而增多,查找代码会变得越来越慢; 如果只改动其中一个模块的情况,需要把整个项目重新打包上线; 目录层级和模块层级过深而且文件过多,定位文件会越来越慢; 所有的项目都只能用同一种技术(对于中小型项目还是很方便的)

5-思考什么时候需要微前端,什么时候不需要

5.1 - 需要微前端:

1--系统本身是需要集成和被集成的,一般有两种情况:

  a.旧系统不能下,新的需求还在来(深有体会)
   没有一个商业公司会同意技术工程师以单纯的技术升级的理由来直接下线一个有着一定用户存量的系统。而你又不能简单的通过iframe这种手段完成新功能的接入
   b.你的系统需要有一套支持动态插拔的机制。
   这个机制可以是一套精心设计的插件体系,但是一旦出现接入应用或被接入应用年代久远,改造成本高的场景,可能后面就会过渡到各种微前端的情形

2--系统中的部件具备足够清晰的服务边界

通过微前端手段划分服务边界,将复杂度隔离在不同的系统单元中,从而避免带来代码腐化和代码臃肿冗余,以及研发节奏差异带来的工程协同上的问题

5.2 - 不需要微前端

1-你的团队/你具备系统内所有架构组件的话语权 简单来说就是,系统里的所有组件都是一个小团队开发的 2-你/你的团队。后期有足够的动力去治理改造这个系统中的问题和所有组件 直接改造村里系统的收益大于新老系统混杂带来的问题。 3-系统及组织架构上,各部件之间本身就是强耦合,低内聚,自洽,不可分离的。 系统本身就是一个最小单元的架构量子,拆分的成本高于治理的成本 4-极高的产品体验要求,对任何产品交互上的不一致零容忍 不允许交互上不一致的情况出现,这基本上从产品上否决了渐进式升级的技术策略

6--瞎总结一下,借鉴参考,不喜勿喷,嘴下留情

微前端核心价值: 1-技术栈无关:主框架不限制接入应用的技术栈,微应用具备完全自主权

2-独立开发,独立部署:微应用仓库独立,前后端可独立开发,部署完成之后,主框架自动完成同步更新

3-增量升级:在面对复杂的场景时,很难重构和全部技术栈升级,微前端是一种很好的实现 4-独立运行时,每个微应用之间状态隔离,运行时状态不共享