阅读 3647

微前端那些事

微前端出现在我们的视线的次数越来越多,因为to B 的发展越来越迅猛,导致中后台应用需求激增,如何将多项目集合成一个web主体就成为一个问题,当然也有不少童鞋们还会有疑惑🤔,究竟微前端是什么东西呢?

1.什么是微前端

微前端本质是是一种项目架构方案,是为了解决前端项目太过庞大,导致项目管理维护难、团队协作乱、升级迭代困难、技术栈不统一等等问题,有点类似微服务的概念,是将微服务理念扩展到前端开发的一种应用,讲到这里你可能还是一脸懵逼~,我们接着讲

举个例子:七某云平台

本质上应该就是一个微前端应用,左侧的菜单就是各个子应用的入口,切换菜单的同时就是在切换子应用,而整个主容器就是一个portal门户(可能包含用户登录机制 、菜单权限获取 、 全局异常处理等)

2.微前端的落地方式

微前端它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用拆分为为多个小型前端应用聚集为一体的应用。

2.1 iFrame

iFrame 是微前端集成的最简单方式之一。可以说iFrame 里的页面是完全独立的,而且iFrame 页面中的静态资源(js、css)都是相互隔离的,互相不干扰,相当于一个独立的环境,具备沙箱隔离,可以让前端应用之间可以相互独立运行

//通过切换url来切换不同业务项目

 <iframe v-show="url" frameborder="0" id="contentIframe"></iframe>
 createFrame(url) {
      const iframe = document.getElementById('contentIframe');
      const deviceWidth = document.body.clientWidth;
      // const deviceHeight = document.body.clientHeight;
      iframe.style.width = `${Number(deviceWidth) - 10}px`;
      iframe.style.height = `${800}px`;
      iframe.src = url;
}

复制代码

当然iFrame 也有局限性👇:

  • 子项目需调整,需要隐藏自身页面中的导航(公共区域)
  • iFrame嵌入的视图控制难,有局限性
  • 刷新无法保存记录,也就意味着当浏览器刷新状态将消失,后退返回无效
  • iframe 阻塞主页面加载

2.2 路由分发方式

路由分发是指通过路由将不同业务拆分的子项目,结合反向代理的方式实现

路由分发方式也是比较简单的一种实现微前端的方式,将多个子项目聚合成一体,可以通过ngxin来配置不同路由的转发代理,如下

http {
  server {
    listen       80;
    server_name  192.168.0.1
    location /web/monitor {
      proxy_pass http://192.168.0.2/web/monitor;
    }
    location /web/admin {
      proxy_pass http://192.168.0.3/web/admin;
    }
    location / {
      proxy_pass /;
    }
  }
}
复制代码

通过不同的路由请求,转发到不同的项目域名服务器下,这种方式好处在于团队协作方便、框架无关、项目独立部署维护

当然路由分发也有局限性:

  • web应用之间的复用性差
  • 每个独立的项目之间切换,需要重新加载,容易出现白屏影响用户体验

2.3 Single-SPA

官方号称“一个用于前端微服务化的JavaScript前端解决方案”,single-spa 听起来很高大上,它能兼容各种技术栈,并且在同一个页面中可以使用多种技术框架(React, Vue, Angular等任意技术框架),不用考虑因新的技术框架而去重构旧项目的代码,官方文档🚀

大概的原理是,首先需要一个主应用(容器应用),需要先注册子应用,然后当url匹配到相应的子应用路由后,将会先请求子应用的资源,然后挂载子应用,同理,当url切换出该子应用路由时,将卸载该应用,以此达到切换子应用的效果,通过子应用生命周期boostrap(获取输出的资源文件) 、 mount、unmount的交替

聊聊Single-SPA 的优点:

  • 各项目独立开发、部署、迭代,互不影响效率高
  • 开发团队可以选择自己的技术并及时更新技术栈。
  • 相互之间的依赖性大大降低
  • 有利于CI/CD,更快的交付产品

由于要把时间留给终极大boss(乾坤qiankun-蚂蚁金服微前端框架),Single-SPA的实践在这里不做大篇幅介绍,有兴趣的童鞋可以看下面几篇文章

前端微服务化解决方案2 - Single-SPA

2.4 qiankun (蚂蚁金服微前端框架)

qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。官方文档🚀

qiankun这名字起得溜啊,本质上就是在上一节提到得Single-SPA上做一些封装,让我们前端开发者用得更上手

  • 主应用下安装 qiankun
yarn add qiankun
复制代码
  • 如何在主应用中注册子应用

官方文档介绍得是react的方式,而树酱是基于vue开发的,所以这里介绍下vue的方式,本质上就是注册子项目应用(按需加载子项目编译好的静态资源),当子应用加载完之后,浏览器的 url 发生变化时,便会自动触发 qiankun 的路由匹配逻辑,去执行子应用的生命周期函数,以下是具体实现👇,有点长小心头顶

// main.js 入口文件修改
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import axios from 'axios';
import api from "./service";
import ViewUI from 'view-design';
import cacheKeys from '@/const/cacheKey';
import globalMixins from '@/mixin/global';
import Bus from '@/utils/bus';
import 'view-design/dist/styles/iview.css';
import './theme/customTheme.less';
import '@/icons';

Vue.config.productionTip = false;
// 导入乾坤函数
import {
  registerMicroApps,
  runAfterFirstMounted,
  setDefaultMountApp,
  start
} from "qiankun";

// 导入路由监听函数
import { genActiveRule } from "./utils";
// 导入主应用工具类库
import LibraryJs from "./library/js";
// 导入主应用需要下发的emit函数
import * as childEmit from "./utils/childEmit"
// 定义传入子应用的数据
Vue.mixin(globalMixins);
Vue.use(ViewUI);
Vue.use(api);
Vue.use(Bus);
Vue.prototype.$axios = axios;
Vue.prototype.$cacheKeys = cacheKeys;
Vue.config.productionTip = false;

// 定义传入子应用的数据
let msg = {
  data: store,         // 从主应用仓库读出的数据
  // components: LibraryUi,       // 从主应用读出的组件库
  utils: LibraryJs,            // 从主应用读出的工具类库
  emitFnc: childEmit,           // 从主应用下发emit函数来收集子应用反馈
  prototype: [
    {name: '$axios', value: axios },
    {name: 'isQiankun', value: true },//是否qiankun启用
  ]
};

// 主应用渲染函数
let app = null;
function render({ appContent, loading } = {}) {
  if (!app) {
    app = new Vue({
      el: "#container",
      router,
      store,
      data() {
        return {
          content: appContent,
          loading
        };
      },
      render(h) {
        return h(App, {
          props: {
            content: this.content,
            loading: this.loading
          }
        });
      }
    });
  } else {
    app.content = appContent;
    app.loading = loading;
  }
  window.vue = app;
};
render();

//注册子应用
registerMicroApps(
  [
    {
      name: "monitor",
      entry: "http://10.0.0.110:8081/",
      render,
      activeRule: genActiveRule("/monitor"),
      props: msg
    },
    {
      name: "portalAdmin",
      entry: "http://183.62.46.202:8082/",
      render,
      activeRule: genActiveRule("/admin"),
      props: msg
    }
  ],
  {
    beforeLoad: [
      app => {
        console.log("before load", app);
      }
    ],
    beforeMount: [
      app => {
        console.log("before mount", app);
      }
    ],
    afterUnmount: [
      app => {
        console.log("after unload", app);
      }
    ]
  },
);

// 设置默认子应用
setDefaultMountApp("/portal");
// 第一个子应用加载完毕回调
runAfterFirstMounted(() => {
  // console.log(app)
});
// 启动微服务
start({ prefetch: true });

/* new Vue({
  router,
  store,
  render: h => h(App)
}).$mount("#app"); */

复制代码
  • 子应用适配

子应用不需要额外安装任何其他依赖即可接入 qiankun 主应用,只需向主应用暴露相应的生命周期钩子

import Vue from 'vue';
import VueRouter from 'vue-router';
import routes from './router';
import './public-path';


// 声明变量管理vue及路由实例
let router = null;
let instance = null;

// 导出子应用生命周期 挂载前
export async function bootstrap(props = {}) {
  Vue.prototype.isQiankun = props.isQiankun;
}

// 导出子应用生命周期 挂载前 挂载后
export async function mount({data = {}} = {}) {
  router = new VueRouter({
    base: window.__POWERED_BY_QIANKUN__ ? '/portal' : '/',
    mode: "history",
    routes
  });
  window.vue = instance = new Vue({
    router,
    store,
    render: h => h(App, {props: data})
  }).$mount("#app");
};

// 导出子应用生命周期 挂载前 卸载后
export async function unmount() {
  instance.$destroy();
  instance = null;
  router = null;
}

// 单独开发环境
window.__POWERED_BY_QIANKUN__ || mount();

复制代码

子应用挂载到主应用,通过props

还需main.js 中引入 public-path.js 文件

// public-path.js 
if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
复制代码

配置好生命周期钩子函数后,为了让主应用能获取子应用暴露的资源文件,子应用的打包工具需要增加如下配置:

// vue.config.js

module.exports = {
  resolve: {
            alias: {
                '@': resolve('src'),
            },
    },
   configureWebpack: {
    output: {
      library: `${name}-[name]`,
      filename: '[name].js',
      libraryTarget: 'umd',
      globalObject: 'this',
    },
  },
}
复制代码

以上即完成了子父应用的微前端适配,过程中会有可能会遇到一些奇奇怪怪的问题,可以查看官方的见问题 以及 github 上面的 issue

完成以上步骤,当你想部署到测试环境或者生产环境的时候,还需要配置nginx。

首先先聊聊子应用的nginx配置

events {
    worker_connections  1024;
}
http{
    server {
        listen       80;
        add_header Access-Control-Allow-Origin *;
        add_header Access-Control-Allow-Headers X-Requested-With;
        add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
        location / {
            try_files $uri $uri/ /index.html;
            root   /usr/share/nginx/html;
            index  index.html index.htm;
        }
    }
}
复制代码
  • vue路由模式 :子项目的路由模式为history,因此需要再nginx配置try_files $uri $uri/ /index.html; 否则当你刷新路由时会报404 vue 官方文档
  • 资源跨域问题:需要设置允许访问的来源,如果没有配置,主应用将因为跨域无法正常获取子应用的资源(js、css)

配置完子应用,主应用的ngnix也不能漏

http{
    server {
        listen       80;
        location / {
            try_files $uri $uri/ /index.html;
            root   /usr/share/nginx/html;
            index  index.html index.htm;
        }
        location /monitor {
           try_files $uri $uri/ /index.html;
           proxy_pass http://10.0.0.110:8081;
        }
        location /admin {
            try_files $uri $uri/ /index.html;
            proxy_pass http://183.62.46.202:8082;
         }
    }
}

复制代码

大功告成

3 总结

3.1 一些优质的微前端文章分享

可能是你见过最完善的微前端解决方案

微前端架构选型指南

每日优鲜供应链前端团队微前端改造

alili.tech 微前端

Micro-frontend Architecture in Action-微前端的那些事儿

欢迎指出问题🧒