微前端体验

微前端是什么

为了解决庞大的一整块后端服务带来的变更与扩展方面带来的限制。微前端是一种类似于微服务的架构,将微服务的理念应用于浏览器端。将 web 应用有单一的应用转变为多个小型前端应用聚合的一种应用。

这些单独的应用可以独立运行,独立开发,独立部署以及可以在共享组件的同时进行并行开发。 它的特点在于:

  • 单应用的代码库更小、内聚、可维护性更高。
  • 松耦合、自制的团队可扩展性更好。
  • 渐进地升级,更新甚至是重现部分前端功能成为可能。

为什么需要微前端

微前端主要是实现了解耦,在应用达到一定规模够,就可以考虑采用微前端,它支持组织分割为更多的团队,乃至创建更小的全栈团队。在某些场景中会发挥出更大的作用:

  • 增量升级
  • 多个团队贡献同一个前端项目
  • 一些独立的部分需要有特定的用户或团队激活、终止
  • 需要外部开发人员对 UI 进行扩展
  • UI 的特性会渐进地的丰富,且不会影响系统的其他部分
  • 支持不同团队使用不同的开发工具和前端技术栈

哪些方案可以实现微前端

微前端的实现方式有以下几种:

  • 使用 iframe 组合
  • 服务端模板渲染组合
  • 微前端框架 single-spa

iframe

iframe 天然具备微前端的基因。只需要经单个的前端应用,按照业务模块进行拆分,分别部署。然后通过 iframe 进行动态加载即可。

<html>
  <head>
    <title>微前端-iframe</title>
  </head>
  <body>
    <h1>我是容器</h1>
    <iframe id="microIframe"></iframe>
    <script type="text/javascript">
      const routes = {
        "/": "https://wekic.com",
        "/app1": "https://dev.wekic.com",
        "/app2": "....",
      };

      const iframe = document.getElementById("microIframe");
      iframe.src = routes[window.location.pathname];
    </script>
  </body>
</html>
<html>
  <head>
    <title>微前端-iframe</title>
  </head>
  <body>
    <h1>我是容器</h1>
    <iframe id="microIframe"></iframe>
    <script type="text/javascript">
      const routes = {
        "/": "https://wekic.com",
        "/app1": "https://dev.wekic.com",
        "/app2": "....",
      };

      const iframe = document.getElementById("microIframe");
      iframe.src = routes[window.location.pathname];
    </script>
  </body>
</html>

iframe 最大的特性就是提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决。但他的最大问题也在于他的隔离性无法被突破,导致应用间上下文无法被共享,随之带来的开发体验、产品体验的问题。

缺点在于:

  • url 不能同步,浏览器刷新 iframe url 状态丢失,后退前进按钮无法使用
  • UI 不同步,DOM 结构不共享。假如一个平面右下角 1/4 的 iframe 里有个带遮罩的弹窗,同时我们要求这个弹框居中显示,还要浏览器 resize 时自动居中。
  • 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等要求,主应用 cookie 要透传到根域名都不同的子应用中实现免登效果。
  • 慢。每次子应用进入都是一次浏览器上下文重建、资源重新再加的过程。

其中有的问题比较好解决,如第一点。有些问题我们可以稍微不管(问题 4),但是有些问题我们很难解决(问题三)甚至无法解决(问题二)。

服务端模板组合

服务端根据路由动态渲染特定页面的模板文件。服务器根据 url 路径动态设置需要加载的模板。

server {
    listen 8080;
    server_name localhost;

    root /usr/share/nginx/html;
    index index.html;
    ssi on;

    rewrite ^/$ http://localhost:8080/app redirect;

    location /app {
      set $PAGE 'app';
    }
    location /app1 {
      set $PAGE 'app1';
    }
    location /app2 {
      set $PAGE 'app2';
    }

    error_page 404 /index.html;
}
server {
    listen 8080;
    server_name localhost;

    root /usr/share/nginx/html;
    index index.html;
    ssi on;

    rewrite ^/$ http://localhost:8080/app redirect;

    location /app {
      set $PAGE 'app';
    }
    location /app1 {
      set $PAGE 'app1';
    }
    location /app2 {
      set $PAGE 'app2';
    }

    error_page 404 /index.html;
}

优点在于实现简单,技术栈独立。 缺点在于需要额外配置 Nginx,分离不彻底。

微前端框架 single-spa

single-spa 是一个用于前端微服务化的 JavaScript 前端解决方案 (本身没有处理样式隔离、js 执行隔离) 实现了路由劫持和应用加载。

借助 single-spa,开发者可以为不同的子应用使用不同的技术栈,比如子应用 A 使用 vue 开发,子应用 B 使用 react 开发,完全没有历史债务。

import * as singleSpa from "single-spa";
const app1Url = "http://app1.com/app1.js";
const app2Url = "http://app1.com/app2.js";

singleSpa.registerApplication(
  "app1",
  () => loadJS(app1Url),
  (location) => location.pathname.startsWith("/app1")
);

singleSpa.registerApplication(
  "app2",
  () => loadJS(app1Url),
  (location) => location.pathname.startsWith("/app2")
);

singleSpa.start();
import * as singleSpa from "single-spa";
const app1Url = "http://app1.com/app1.js";
const app2Url = "http://app1.com/app2.js";

singleSpa.registerApplication(
  "app1",
  () => loadJS(app1Url),
  (location) => location.pathname.startsWith("/app1")
);

singleSpa.registerApplication(
  "app2",
  () => loadJS(app1Url),
  (location) => location.pathname.startsWith("/app2")
);

singleSpa.start();

子应用需要暴露 3 个 api

let domEl;
export function bootstrap(props) {
  return Promise.resolve().then(() => {
    domEl = document.createElement("div");
    domEl.id = "app1";
    document.body.appendChild(domEl);
  });
}
export function mount(props) {
  return Promise.resolve().then(() => {
    domEl.textContent = "App 1 is mounted!";
  });
}
export function unmount(props) {
  return Promise.resolve().then(() => {
    domEl.textContent = "";
  });
}
let domEl;
export function bootstrap(props) {
  return Promise.resolve().then(() => {
    domEl = document.createElement("div");
    domEl.id = "app1";
    document.body.appendChild(domEl);
  });
}
export function mount(props) {
  return Promise.resolve().then(() => {
    domEl.textContent = "App 1 is mounted!";
  });
}
export function unmount(props) {
  return Promise.resolve().then(() => {
    domEl.textContent = "";
  });
}

single-spa优点:

  • 纯前端坚决方案
  • 可以使用多中技术栈
  • 完善的生态系统

缺点:

  • 上手成本稍高
  • 需要改造现有应用
  • 跨应用变得复杂

qiankun

qiankun 孵化自蚂蚁金融科技基于微前端架构的云产品统一接入平台,在经过一批线上应用的充分检验及打磨后,我们将其微前端内核抽取出来并开源,希望能同时帮助社区有类似需求的系统更方便的构建自己的微前端系统,同时也希望通过社区的帮助将 qiankun 打磨的更加成熟完善。

qiankun 基于 Single-SPA, 提供了更加开箱即用的 API (single-spa + sandbox + import-html-entry),它做到了技术栈无关,并且接入简单。

总结

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

  • 与技术栈无关,主框架不限制接入应用的技术栈,子应用具备完全自主权。
  • 子应用独立开发、独立部署、独立仓库。前后端可以独立开发,部署完成之后主框架会自动完成同步更新。
  • 独立运行,每个应用之间的状态隔离,运行状态不能共享。

微前端架构旨在解决单体应用在一个相对长时间跨度下,由于参与人员、团队的增多、变迁、从一个普通应用变成相对复杂应用。随之而来的应用不可维护的问题。这些问题在企业级web应用中比较常见。