【微前端】single-spa 到底是个什么鬼(上)

简介: 说起微前端框架,很多人第一反应就是 single-spa。但是再问深入一点:它是干嘛的,它有什么用,可能就回答不出来了。 一方面没多少人研究和使用微前端。可能还没来得及用微前端扩展项目,公司就已经倒闭了。 另一方面是中文博客对微前端的研究少之又少,很多文章只是简单翻译一下官方文档,读几个API,放个官方的 Demo 就完事了。很少有深入研究到底 single-spa 是怎么一回事的。这篇文章将不会聊怎么搭建一个 Demo,而是会从 “Why” 和 “How” 的角度来聊一下官方文档的都讲了哪些内容,相信看完这篇文章就能看懂 官方的 Demo 了。

image.png

前言


说起微前端框架,很多人第一反应就是 single-spa。但是再问深入一点:它是干嘛的,它有什么用,可能就回答不出来了。


一方面没多少人研究和使用微前端。可能还没来得及用微前端扩展项目,公司就已经倒闭了。


另一方面是中文博客对微前端的研究少之又少,很多文章只是简单翻译一下官方文档,读几个API,放个官方的 Demo 就完事了。很少有深入研究到底 single-spa 是怎么一回事的。


还有一方面是 single-spa 的文档非常难看懂,和 Redux 文档一样喜欢造概念。讲一个东西的时候,总是把别的库拉进来一起讲,把一个简单的东西变得非常复杂。最令人吐槽的一点就是官方的 sample code 都是只言片语,完全拼凑不出来一个 Demo,而 Github 的 Demo 还贼复杂,没解释,光看完都要 clone 好几个 repo。

最后,求人不如求己,刚完源码再刚一下文档。


这篇文章将不会聊怎么搭建一个 Demo,而是会从 “Why” 和 “How” 的角度来聊一下官方文档的都讲了哪些内容,相信看完这篇文章就能看懂 官方的 Demo 了。


一个需求


让我们从一个最小的需求开始说起。有一天产品经理突然说:我们要做一个 A 页面,我看到隔壁组已经做过这个 A 页面了,你把它放到我们项目里吧,应该不是很难吧?明天上线吧。


此时,产品经理想的是:应该就填一个 URL 就好吧?再不行,复制粘贴也很快吧。而程序员想的却是:又要看屎山了。又要重构了。又要联调了。测试数据有没有啊?等一下,联调的后端是谁啊?


**估计这是做大项目时经常遇到的需求了:搬运一个现有的页面。**我想大多数人都会选择在自己项目里复制粘贴别人的代码,然后稍微重构一下,再测试环境联调,最后上线。

但是,这样就又多了一份代码了,如果别人的页面改了,那么自己项目又要跟着同步修改,再联调,再上线,非常麻烦。


所以程序员就想能不能我填一个 url,然后这个页面就到项目里来了呢?所以,<iframe/> 就出场了。


iframe 的弊端


iframe 就相当于页面里再开个窗口加载别的页面,但是它有很多弊端:


  • 每次进来都要加载,状态不能保留
  • DOM 结构不共享。比如子应用里有一个 Modal,显示的时候只能在那一小块地方展示,不能全屏展示
  • 无法跟随浏览器前进后退
  • 天生的硬隔离,无法与主应用进行资源共享,交流也很困难


而 SPA 正好可以解决上面的问题:

  • 切换路由就是切换页面组件,组件的挂载和卸载非常快
  • 单页应用肯定共享 DOM
  • 前端控制路由,想前就前,想后就后
  • React 通信有 Redux,Vue 通信有 Vuex,可与 App 组件进行资源共享,交流很爽

这就给我们一个启发:能不能有这么一个巨型 SPA 框架,把现有的 SPA 当成 Page Component 来组装成一个新的 SPA 呢?这就是微前端的由来。


微前端是什么


微前端应该有如下特点:


  • 技术栈无关,主框架不限制接入应用的技术栈,微应用具备完全自主权
  • 独立开发、独立部署,微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
  • 增量升级,在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
  • 独立运行时,每个微应用之间状态隔离,运行时状态不共享


等一下等一下,说了一堆,到底啥是 single-spa 啊。


嘿嘿,single-spa 框架并没有实现上面任何特点,对的,一个都没有,Just Zero。


single-spa 到底是干嘛的


**single-spa 仅仅是一个子应用生命周期的调度者。**single-spa 为应用定义了 boostrap, load, mount, unmount 四个生命周期回调:


image.png


只要写过 SPA 的人都能理解,无非就是生、老、病、死。不过有几个点需要注意一下:


  • Register 不是生命周期,指的是调用 registerApplication 函数这一步
  • Load 是开始加载子应用,怎么加载由开发者自己实现(等会会说到)
  • Unload 钩子只能通过调用 unloadApplication 函数才会被调用

OK,上面 4 个生命周期的回调顺序是 single-spa 可以控制的,我能理解,那什么时候应该开始这一套生命周期呢?应该是有一个契机来开始整套流程的,或者某几个流程的。

契机就是当 window.location.href 匹配到 url 时,开始走对应子 App 的这一套生命周期嘛。所以,single-spa 还要监听 url 的变化,然后执行子 app 的生命周期流程。


到此,我们就有了 single-spa 的大致框架了,无非就两件事:

  • 实现一套生命周期,在 load 时加载子 app,由开发者自己玩,别的生命周期里要干嘛的,还是由开发者造的子应用自己玩
  • 监听 url 的变化,url 变化时,会使得某个子 app 变成 active 状态,然后走整套生命周期


画个草图如下:

image.png


是不是感觉 single-spa 很鸡贼?虽然 single-spa 说自己是微前端框架,但是一个微前端的特性都没有实现,都是需要开发者在加载自己子 App 的时候实现的,要不就是通过一些第三方工具实现。


注册子应用


有了上面的了解之后,我们再来看 single-spa 里最重要的 API:registerApplication,表示注册一个子应用。使用如下:

singleSpa.registerApplication({
    name: 'taobao', // 子应用名
    app: () => System.import('taobao'), // 如何加载你的子应用
    activeWhen: '/appName', // url 匹配规则,表示啥时候开始走这个子应用的生命周期
    customProps: { // 自定义 props,从子应用的 bootstrap, mount, unmount 回调可以拿到
        authToken: 'xc67f6as87f7s9d'
    }
})
singleSpa.start() // 启动主应用
复制代码

上面注册了一个子应用 'taobao'。我们自己实现了加载子应用的方法,通过 activeWhen 告诉 single-spa 什么时候要挂载子应用,好像就可以上手开撸代码喽。

可以个鬼!请告诉我 System.import 是个什么鬼。哦,是 SystemJS,诶,SystemJS 听说过,它是个啥?为啥要用 SystemJS?凭啥要用 SystemJS?


SystemJS


相信很多人看过一些微前端的博客,它们都会说 single-spa 是基于 SystemJS 的。错!single-spa 和 SystemJS 一点关系都没有!这里先放个主应用和子应用的关系图:

image.png

single-spa 的理念是希望主应用可以做到非常非常简单的和轻量,简单到只要一个 index.html + 一个 main.js 就可以完成微前端工程,连 Webpack 都不需要,直接在浏览器里执行 singleSpa.registerApplication 就收工了,这种执行方式也就是 in-browser 执行。


但是,浏览器里执行 JS,别说实现 import xxx from 'https://taobao.com' 了,我要是在浏览器里实现 ES6 的 import/export 都不行啊: import axios from 'axios'

image.png

其实,也不是不行,只需要在 <script> 标签加上 type="module",也是可以实现的,例如:

<script type="module" src="module.js"></script>
<script type="module">
  // or an inline script
  import {helperMethod} from './providesHelperMethod.js';
  helperMethod();
</script>
// providesHelperMethod.js
export function helperMethod() {
  console.info(`I'm helping!`);
}
复制代码


但是,遇到导入模块依赖的,像 import axios from 'axios' 这样的,就需要 importmap 了:

<script type="importmap">
    {
       "imports": {
          "vue": "https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.esm.browser.js"
       }
    }
</script>
<div id="container">我是:{{name}}</div>
<script type="module">
  import Vue from 'vue'
  new Vue({
    el: '#container',
    data: {
      name: 'Jack'
    }
  })
</script>
复制代码

importmap 的功能就是告诉 'vue' 这个玩意要从 "cdn.jsdelivr.net/npm/vue@2.6…" 这里来的。不过,importmap 现在只有 Chrome 是支持的。


所以,SystemJS 就将这一块补齐了。当然,除了 importmap,它还有很多的功能,比如获取当前加载的所有模块、当前模块的 URL、可以 import html, import css,import wasm。


等等,这在 Webpack 不也可以做到么?Webpack 还能 import less, import scss 呢?这不比 SystemJS 牛逼?对的,如果不是因为要在浏览器使用 import/export,没人会用 SystemJS。SystemJS 的好处和优势有且仅有一点:那就是在浏览器里使用 ES6 的 import/export。


而正因为 SystemJS 可以在浏览器里可以使用 ES6 的 import/export 并支持动态引入,正好符合 single-spa 所提倡的 in-browser 执行思路,所以 single-spa 文档里才反复出现 SystemJS 的身影,而且 Github Demo 里依然是使用 SystemJS 的 importmap 机制来引入不同模块:

<script type="systemjs-importmap">
    {
      "imports": {
        "@react-mf/root-config": "//localhost:9000/react-mf-root-config.js"
      }
    }
</script>
<script>
  singleSpa.registerApplication({
    name: 'taobao', // 子应用名
    app: () => System.import('@react-mf/root-config'), // 如何加载你的子应用
    activeWhen: '/appName', // url 匹配规则,表示啥时候开始走这个子应用的生命周期
    customProps: { // 自定义 props,从子应用的 bootstrap, mount, unmount 回调可以拿到
        authToken: 'xc67f6as87f7s9d'
    }
  })
</script>
复制代码


公共依赖


SystemJS 另一个好处就是可以通过 importmap 引入公共依赖。

假如,我们有三个子应用,它们都有公共依赖项 antd,那每个子应用打包出来都会有一份 antd 的代码,就显示很冗余。


image.png

一个解决方法就是在主应用里,通过 importmap 直接把 antd 代码引入进来,子应用在 Webpack 设置 external 把 antd 打包时排除掉。子应用打包就不会把 antd 打包进去了,体积也变小了。

image.png

有人会说了:我用 CDN 引入不行嘛?不行啊,因为子应用的代码都是 import {Button} from 'antd' 的,浏览器要怎么直接识别 ES6 的 import/export 呢?那还不得 SystemJS 嘛。


难道 Webpack 就没有办法可以实现 importmap 的效果了么?Webpack 5 提出的 Module Federation 模块联邦就可以很好地做的 importmap 的效果。这是 Webpack 5 的新特性,使用的效果和 importmap 差不多。关于模块联邦是个啥,可以参考 这篇文章


至于用 importmap 还是 Webpack 的 Module Federation,singles-spa 是推荐使用 importmap 的,但是,文档也没有反对使用 Webpack 的 Module Federation 的理由。能用就OK。


SystemJS vs Webpack ES


有人可能会想:都 1202 年了,怎么还要在浏览器环境写 JS 呢?不上个 Webpack 都不好意思说自己是前端开发了。


没错,Webpack 是非常强大的,而且可以利用 Webpack 很多能力,让主应用变得更加灵活。比如,写 less,scss,Webpack 的 prefetch 等等等等。然后在注册子应用时,完全可以利用 Webpack 的动态引入:

singleSpa.registerApplication({
    name: 'taobao', // 子应用名
    app: () => import('taobao'), // 如何加载你的子应用
    activeWhen: '/appName', // url 匹配规则,表示啥时候开始走这个子应用的生命周期
    customProps: { // 自定义 props,从子应用的 bootstrap, mount, unmount 回调可以拿到
        authToken: 'xc67f6as87f7s9d'
    }
})
复制代码

那为什么 single-spa 还要推荐 SystemJS 呢?个人猜测是因为 single-spa 希望主应用应该就一个空壳子,只需要管内容要放在哪个地方,所有的功能、交互都应该交由 index.html 来统一管理


当然,这仅仅是一种理念,可以完全不遵循它。像我个人还是喜欢用 Webpack 多一点,SystemJS 还是有点多余,而且觉得有点奥特曼了。不过,为了跟着文档的节奏来,这里假设就用 SystemJS 来实现主应用。


Root Config


由于 single-spa 非常强调 in-browser 的方式来实现主应用,所以 index.html 就充当了静态资源、子应用的路径声明的角色。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Polyglot Microfrontends</title>
  <meta name="importmap-type" content="systemjs-importmap" />
  <script type="systemjs-importmap" src="https://storage.googleapis.com/polyglot.microfrontends.app/importmap.json"></script>
  <% if (isLocal) { %>
  <script type="systemjs-importmap">
    {
      "imports": {
        "@polyglot-mf/root-config": "//localhost:9000/polyglot-mf-root-config.js"
      }
    }
  </script>
  <% } %>
  <script src="https://cdn.jsdelivr.net/npm/import-map-overrides@2.2.0/dist/import-map-overrides.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/system.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/extras/amd.min.js"></script>
</head>
<body>
  <script>
    System.import('@polyglot-mf/root-config');
    System.import('@polyglot-mf/styleguide');
  </script>
  <import-map-overrides-full show-when-local-storage="devtools" dev-libs></import-map-overrides-full>
</body>
</html>
复制代码


而 main.js 则实现子应用注册、主应用启动。

import { registerApplication, start } from "single-spa";
registerApplication({
  name: "@polyglot-mf/navbar",
  app: () => System.import("@polyglot-mf/navbar"),
  activeWhen: "/",
});
registerApplication({
  name: "@polyglot-mf/clients",
  app: () => System.import("@polyglot-mf/clients"),
  activeWhen: "/clients",
});
registerApplication({
  name: "@polyglot-mf/account-settings",
  app: () => loadWithoutAmd("@polyglot-mf/account-settings"),
  activeWhen: "/settings",
});
start();
// A lot of angularjs libs are compiled to UMD, and if you don't process them with webpack
// the UMD calls to window.define() can be problematic.
function loadWithoutAmd(name) {
  return Promise.resolve().then(() => {
    let globalDefine = window.define;
    delete window.define;
    return System.import(name).then((module) => {
      window.define = globalDefine;
      return module;
    });
  });
}
复制代码

像这样的资源声明 + 主子应用加载的组件,single-spa 称之为 Root Config。 它不是什么新概念,就只有两个东西:


  • 一个主应用的 index.html
  • 一个执行 registerApplication 函数的 JS 文件


single-spa-layout


虽然一个 index.html 是完美的轻量微前端主应用,但是就算再压缩主应用的交互,那总得告诉子应用放置的位置吧,那不还得 DOM API 一把梭?一样麻烦?


为了解决这个问题,single-spa 说:没事,我帮你搞,然后造了 single-spa-layout。具体使用请看代码:

<html>
  <head>
    <template id="single-spa-layout">
      <single-spa-router>
        <nav class="topnav">
          <application name="@organization/nav"></application>
        </nav>
        <div class="main-content">
          <route path="settings">
            <application name="@organization/settings"></application>
          </route>
          <route path="clients">
            <application name="@organization/clients"></application>
          </route>
        </div>
        <footer>
          <application name="@organization/footer"></application>
        </footer>
      </single-spa-router>
    </template>
  </head>
</html>
复制代码


不能说和 Vue Router  很像,只能说一模一样吧。当然上面这么写很直观,但是浏览器并不认识这些元素,所以 single-spa-layout 把识别这些元素的逻辑都封装成了函数,并暴露给开发者,开发者只要调用一下就能识别出 appName 等信息了:

import { registerApplication, start } from 'single-spa';
import {
  constructApplications,
  constructRoutes,
  constructLayoutEngine,
} from 'single-spa-layout';
// 获取 routes
const routes = constructRoutes(document.querySelector('#single-spa-layout'));
// 获取所有的子应用
const applications = constructApplications({
  routes,
  loadApp({ name }) {
    return System.import(name); // SystemJS 引入入口 JS
  },
});
// 生成 layoutEngine
const layoutEngine = constructLayoutEngine({ routes, applications });
// 批量注册子应用
applications.forEach(registerApplication);
// 启动主应用
start();
复制代码

没什么好说的,constrcutRoutes, constructApplicationconstructLayoutEngine 本质上就是识别 single-spa-layout 定义的元素标签,然后获取里面的属性,再通过 registerApplication 函数一个个注册就完事了。

相关实践学习
Serverless极速搭建Hexo博客
本场景介绍如何使用阿里云函数计算服务命令行工具快速搭建一个Hexo博客。
相关文章
|
2月前
|
前端开发 JavaScript
轻松上手:基于single-spa构建qiankun微前端项目完整教程
轻松上手:基于single-spa构建qiankun微前端项目完整教程
52 0
|
7月前
|
资源调度 前端开发 JavaScript
第十章(应用场景篇) Single-SPA微前端架构深度解析与实践教程
第十章(应用场景篇) Single-SPA微前端架构深度解析与实践教程
246 0
|
前端开发 JavaScript 持续交付
Single-Spa构建第一个微前端项目
微前端是前端web开发的趋势,微服务允许你将后端分解成更小的部分,受此启发,微前端允许你独立构建、测试和部署前端应用。根据你选择的微前端框架,你甚至可以让多个微前端应用——用React、Angular、Vue或其他工具编写的——在同一个大应用中无扰共存!之前在这里《认识微前端:一种用于前端 Web 开发的微服务》大概介绍了一下。
443 0
Single-Spa构建第一个微前端项目
|
前端开发 JavaScript API
【微前端】single-spa 到底是个什么鬼(下)
说起微前端框架,很多人第一反应就是 single-spa。但是再问深入一点:它是干嘛的,它有什么用,可能就回答不出来了。 一方面没多少人研究和使用微前端。可能还没来得及用微前端扩展项目,公司就已经倒闭了。 另一方面是中文博客对微前端的研究少之又少,很多文章只是简单翻译一下官方文档,读几个API,放个官方的 Demo 就完事了。很少有深入研究到底 single-spa 是怎么一回事的。这篇文章将不会聊怎么搭建一个 Demo,而是会从 “Why” 和 “How” 的角度来聊一下官方文档的都讲了哪些内容,相信看完这篇文章就能看懂 官方的 Demo 了。
【微前端】single-spa 到底是个什么鬼(下)
|
Web App开发 JSON JavaScript
一个经常被忽略的 single-spa 微前端实践
大家好,我是海怪。了解过微前端的同学应该对 single-spa 这个框架都不陌生,但是我翻看了中文整个社区,发现太少文章是讲 single-spa Demo 实践的。 还有一些文章讲了,但是都是以晒代码为主,只讲是什么,不讲为什么。这对读者来说并不是一个很好的体验。那今天就跟大家深入分析一下 single-spa 的 React 版 Demo 吧。让读者知其然,也能知其所然。
一个经常被忽略的 single-spa 微前端实践
|
2月前
|
存储 人工智能 前端开发
前端大模型应用笔记(三):Vue3+Antdv+transformers+本地模型实现浏览器端侧增强搜索
本文介绍了一个纯前端实现的增强列表搜索应用,通过使用Transformer模型,实现了更智能的搜索功能,如使用“番茄”可以搜索到“西红柿”。项目基于Vue3和Ant Design Vue,使用了Xenova的bge-base-zh-v1.5模型。文章详细介绍了从环境搭建、数据准备到具体实现的全过程,并展示了实际效果和待改进点。
160 2
|
2月前
|
JavaScript 前端开发 程序员
前端学习笔记——node.js
前端学习笔记——node.js
45 0
|
2月前
|
人工智能 自然语言处理 运维
前端大模型应用笔记(一):两个指令反过来说大模型就理解不了啦?或许该让第三者插足啦 -通过引入中间LLM预处理用户输入以提高多任务处理能力
本文探讨了在多任务处理场景下,自然语言指令解析的困境及解决方案。通过增加一个LLM解析层,将复杂的指令拆解为多个明确的步骤,明确操作类型与对象识别,处理任务依赖关系,并将自然语言转化为具体的工具命令,从而提高指令解析的准确性和执行效率。
|
2月前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。
|
2月前
|
机器学习/深度学习 弹性计算 自然语言处理
前端大模型应用笔记(二):最新llama3.2小参数版本1B的古董机测试 - 支持128K上下文,表现优异,和移动端更配
llama3.1支持128K上下文,6万字+输入,适用于多种场景。模型能力超出预期,但处理中文时需加中英翻译。测试显示,其英文支持较好,中文则需改进。llama3.2 1B参数量小,适合移动端和资源受限环境,可在阿里云2vCPU和4G ECS上运行。
112 1