微前端是什么?

802 阅读4分钟

微前端

###是什么

“微前端”一词 2016 年首次出现,它是将微服务的概念扩展到前端领域。 将前端整体分解为更小、更简单的块的模式出现,这些块可以独立开发、测试和部署,同时在客户面前仍然被视为单一的内聚产品。我们称此技术为微前端。其定义为:一种架构风格,其中独立交付的前端应用程序组成一个更大的整体。

目前前端主流趋势是 SPA 应用(单页面应用程序),但随着需求的变更,功能的增加,项目会越来越大,也难以维护,升级到最主流的技术栈的成本会越来越高,最终成为一个“巨石应用”。 微前端的理念是将网站或应用程序视为独立团队的功能组合。

微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用(Frontend Monolith)后,随之而来的应用不可维护的问题。面对开发者:项目拆分,面对用户:项目整合

###微前端架构具备以下几个核心价值

  • 技术栈无关

    主框架不限制接入应用的技术栈,微应用具备完全自主权

  • 独立开发、独立部署

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

  • 增量升级

    在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略

  • 独立运行时

    每个微应用之间状态隔离,运行时状态不共享

###项目膨胀出现的问题

  • 编译时间长
    • 开发体验差(热加载慢)
    • 打包成本高(打包时间长,牵一发动全身)
  • 团队协作问题
    • 代码冲突
    • 版本控制

新的问题

  • 域名问题
    • 模块拆分的过多,需要独立部署,运维成本,域名管理

优点和缺点及性能

  • 优点
    • 入口统一
    • 增量升级
    • 简单、分离的代码库
    • 独立部署
  • 缺点
    • 架构复杂
    • 依赖项重复
  • 性能
    • 如:主应用是vue,子应用也是vue,vue|vue-router|vuex会重复加载,解决方法生产环境对重复依赖项不进行webpack打包,通过cdn和是否在qiankun环境中动态插入script标签, 整合公共资源 externals 只加载一遍

###架构图 图片替换文本

常见的应用场景

  • 模块和模块之间交互不多,如:几个管理系统的合并权限菜单交给主应用渲染,统一系统入口、客服面板、工单面板等一些插件面板不需要和其他应用做大量的交互

####JS客服面板 图片替换文本

####JS工单面板 图片替换文本

####结构简单清楚 图片替换文本

实现方案

qiankun阿里开源的框架

single-spa

qiankun

怎样用

主应用

import {
  start,
  initGlobalState,
  registerMicroApps,
  runAfterFirstMounted,
  // setDefaultMountApp,
  addGlobalUncaughtErrorHandler,
} from "qiankun";
import render from "./render";
import { MICRO_APP_LIST } from "@/config";

// Step1 初始化应用
render({ loading: false });

const loader = (loading) => render({ loading });

// 微应用配置
const apps = MICRO_APP_LIST.map((app) => {
  const { name, entry, activeRule } = app;
  const container = "#subapp-viewport";
  return {
    name,
    entry,
    activeRule,
    loader,
    container,
    props: { activeRule },
  };
});

// Step2 注册子应用
registerMicroApps(apps, {
  beforeLoad: [
    (app) => {
      console.log("[LifeCycle] before load %c%s", "color: green;", app.name);
    },
  ],
  beforeMount: [
    (app) => {
      console.log("[LifeCycle] before mount %c%s", "color: green;", app.name);
    },
  ],
  afterUnmount: [
    (app) => {
      console.log("[LifeCycle] after unmount %c%s", "color: green;", app.name);
    },
  ],
});

const { onGlobalStateChange, setGlobalState } = initGlobalState();

onGlobalStateChange((value, prev) => {
  console.log("[onGlobalStateChange - master]:", value, prev);
});

// 设置全局状态数据
setGlobalState();

// Step3 设置默认进入的子应用
// setDefaultMountApp('/cs');

// Step4 启动应用
start({
  prefetch: "all",
  // 开启严格的样式隔离模式,但是也会造成一些问题。如一些ui框架Modal插入到的是body节点
  // sandbox: { strictStyleIsolation: true }
});

runAfterFirstMounted(() => {
  console.log("[MainApp] first app mounted");
});

####子应用

import Vue from "vue";
import App from "./App.vue";
import router, { routes } from "./router";
import store from "./store";
import "./style.scss";
import "@/plugins";

// 解决微应用加载资源404问题
// eslint-disable-next-line
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;

Vue.config.productionTip = false;

let instance = null;

function render(props = {}) {
  const { container } = props;

  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() {
  //
}

export async function mount(props) {
  render(props);
}

export async function unmount() {
  instance.$destroy();
  instance.$el.innerHTML = "";
  instance = null;
}

// 导出微应用的全部路由,主应用使用
export const routes = allRoutes;

####vue.config.js

const {
  name: MICRO_APP_NAME,
  port: MICRO_APP_PORT
} = require("./package.json");
// TODO: 开发环境,配置 chainWebpack 、 configureWebpack 导致vue浏览器插件无法使用
module.exports = {
  publicPath: "./",
  productionSourceMap: false,
  runtimeCompiler: true,
  parallel: false,
  css: {
    sourceMap: false
  },
  configureWebpack: config => {
    config.output = {
      ...config.output,
      // 把子应用打包成 umd 库格式
      library: `${MICRO_APP_NAME}-[name]`,
      libraryTarget: "umd",
      jsonpFunction: `webpackJsonp_${MICRO_APP_NAME}`
    };
  },
  devServer: {
    host: "0.0.0.0",
    open: true,
    port: MICRO_APP_PORT, // 端口需要统一
    clientLogLevel: "warning",
    compress: true,
    inline: true,
    hotOnly: true,
    quiet: true,
    https: false,
    progress: true,
    disableHostCheck: true,
    headers: {// 开启资源跨域
      "Access-Control-Allow-Origin": "*"
    },
    overlay: false
  },
  lintOnSave: true
};

通信方案

微前端场景下,我们认为最合理的通信方案是通过 URL 及 CustomEvent 来处理。但在一些简单场景下,基于 props 的方案会更直接便捷,因此我们为 qiankun 用户提供这样一组 API 来完成应用间的通信:

全局变量隔离

class SnapshotSandbox {
    constructor() {
        this.proxy = window; // window属性
        this.modifyPropsMap = {}; // 记录在window上的修改
        this.active(); // 激活
    }
    active() {
        this.windowSnapshot = {}; // 拍照
        for (const prop in window) {
            if (window.hasOwnProperty(prop)) {
                this.windowSnapshot[prop] = window[prop];
            }
        }
        Object.keys(this.modifyPropsMap).forEach((p) => {
            window[p] = this.modifyPropsMap[p];
        });
    }
    inactive() {
        // 失活
        for (const prop in window) {
            if (window.hasOwnProperty(prop)) {
                if (window[prop] !== this.windowSnapshot[prop]) {
                    this.modifyPropsMap[prop] = window[prop];
                    window[prop] = this.windowSnapshot[prop];
                }
            }
        }
    }
}

let sandbox = new SnapshotSandbox();
// 立即执行
((window) => {
    window.a = 1;
    window.b = 2;
    console.log(window.a, window.b);
    sandbox.inactive();
    console.log(window.a, window.b);
    sandbox.active();
    console.log(window.a, window.b);
})(sandbox.proxy);

####项目结构 图片替换文本

###注意

  • 配置运行时 publicPath,解决微应用加载资源 404 的问题webpack_public_path
    • webpack_public_path = window.INJECTED_PUBLIC_PATH_BY_QIANKUN;
  • 端口占用导致微应用无法启动
    • 子应用 vue.config.js 中配置的端口,应该和主应用注册子应用时配置的端口保持一致。

###vue实例问题

图片替换文本

参考资料