美文网首页
【微前端】single-spa 到底是个什么鬼

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

作者: 懂会悟 | 来源:发表于2022-07-02 15:34 被阅读0次

    一个需求

    让我们从一个最小的需求开始说起。有一天产品经理突然说:我们要做一个 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 到底是干嘛的

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

    只要写过 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 状态,然后走整套生命周期

      是不是感觉 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 一点关系都没有!这里先放个主应用和子应用的关系图:



    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'。

    其实,也不是不行,只需要在 <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/vu"这里来的。不过,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 的代码,就显示很冗余。



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



    有人会说了:我用 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

    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, constructApplication 和 constructLayoutEngine 本质上就是识别 single-spa-layout 定义的元素标签,然后获取里面的属性,再通过 registerApplication 函数一个个注册就完事了。

    改造子应用

    上面说的都是主应用的事情,现在我们来关心一下子应用。
    子应用最关键的一步就是导出 bootstrap, mount, unmount 三个生命周期钩子。

    import SubApp from './index.tsx'
    
    export const bootstrap = () => {}
    export const mount = () => {
      // 使用 React 来渲染子应用的根组件
      ReactDOM.render(<SubApp/>, document.getElementById('root'));
    }
    export const unmount = () => {}
    

    single-spa-react, single-spa-vue, single-spa-angular, single-spa-xxx, ...
    emmmm,怎么说的呢,上面三个 export 不太好看,能不能有一种更直接的方法就实现 3 个生命周期的导出呢?
    single-spa 说:可以啊,搞!所以有了 single-spa-react:

    import React from 'react';
    import ReactDOM from 'react-dom';
    import SubApp from './index.tsx';
    import singleSpaReact, {SingleSpaContext} from 'single-spa-react';
    
    const reactLifecycles = singleSpaReact({
      React,
      ReactDOM,
      rootComponent: SubApp,
      errorBoundary(err, info, props) {
        return (
          <div>出错啦!</div>
        );
      },
    });
    
    export const bootstrap = reactLifecycles.bootstrap;
    export const mount = reactLifecycles.mount;
    export const unmount = reactLifecycles.unmount;
    

    single-spa 说:我不能单给 react 搞啊,别的框架也要给它们整上一个,一碗水端平,所以有这了这些牛鬼蛇神:


    导入子应用的 CSS

    不知道你有没有注意到,在刚刚的子应用注册里我们仅仅用 System.import 导入了一个 JS 文件,那 CSS 样式文件怎么搞呢?可能可以 System.import('xxx.css') 来导入。
    但是,这又有问题了:在切换了应用时,unmount 的时候要怎么把已有的 CSS 给删掉呢?官方说可以这样:

    const style = document.createElement('style');
    style.textContent = `.settings {color: blue;}`;
    
    export const mount = [
      async () => {
        document.head.appendChild(styleElement);
      },
      reactLifecycles.mount,
    ]
    
    export const unmount = [
      reactLifecycles.unmount,
      async () => {
        styleElement.remove();
      }
    ]
    

    我:single-spa,求求你做个人吧,搭个 Demo,还要我来处理 CSS?single-spa 说:好,等我再去造一个轮子。于是,就有了 single-spa-css。用法如下:

    import singleSpaCss from 'single-spa-css';
    
    const cssLifecycles = singleSpaCss({
      // 这里放你导出的 CSS,如果 webpackExtractedCss 为 true,可以不指定
      cssUrls: ['https://example.com/main.css'],
    
      // 是否要使用从 Webpack 导出的 CSS,默认为 false
      webpackExtractedCss: false,
    
      // 是否 unmount 后被移除,默认为 true
      shouldUnmount: true,
    
      // 超时,不废话了,都懂的
      timeout: 5000
    })
    
    const reactLifecycles = singleSpaReact({...})
    
    // 加入到子应用的 bootstrap 里
    export const bootstrap = [
      cssLifecycles.bootstrap,
      reactLifecycles.bootstrap
    ]
    
    export const mount = [
      // 加入到子应用的 mount 里,一定要在前面,不然 mount 后会有样式闪一下的问题
      cssLifecycles.mount,
      reactLifecycles.mount
    ]
    
    export const unmount = [
      // 和 mount 同理
      reactLifecycles.unmount,
      cssLifecycles.unmount
    ]
    

    这里要注意一下,上面的 https://example.com/main.css 并没有看起来那么简单易用。

    假如你用了 Webpack 来打包,很有可能会用分包或者 content hash 来给 CSS 文件命名,比如 filename: "[name].[contenthash].css"。那请问 cssUrls 要怎么写呀,每次都要改 cssUrls 参数么?太麻烦了吧。

    single-spa-css 说:我可以通过 Webpack 导出的 webpack_require.cssAssetFileName 获取导出之后的真实 CSS 文件名。ExposeRuntimeCssAssetsPlugin 这个插件正好可以解决这个问题。这么一来 cssUrls 就可以不用指定了,直接把 Webpack 导出的真实 CSS 名放到 cssUrls 里了。

    const MiniCssExtractPlugin = require("mini-css-extract-plugin");
    const ExposeRuntimeCssAssetsPlugin = require("single-spa-css/ExposeRuntimeCssAssetsPlugin.cjs");
    
    module.exports = {
      plugins: [
        new MiniCssExtractPlugin({
          filename: "[name].css",
        }),
        new ExposeRuntimeCssAssetsPlugin({
          // The filename here must match the filename for the MiniCssExtractPlugin
          filename: "[name].css",
        }),
      ],
    };
    

    子应用 CSS 样式隔离

    虽然 single-spa-css 解决了子应用的 CSS 引入和移除问题,但是又带来了另一个问题:怎么保证各个子应用的样式不互相干扰呢?官方给出的建议是:
    第一种方法:使用 Scoped CSS,也即在子应用的 CSS 选择器上加前缀就好了嘛,像这样:

    .app1__settings-67f89dd87sf89ds {
      color: blue;
    }
    

    要是嫌麻烦,可以在 Webpack 使用 PostCSS Prefix Selector 给样式自动加前缀:

    const prefixer = require('postcss-prefix-selector');
    
    module.exports = {
      plugins: [
        prefixer({
          prefix: "#single-spa-application\\:\\@org-name\\/project-name"
        })
      ]
    }
    

    另一种方法是在加载子应用的函数里,将子应用挂载到 Shadow DOM 上,可以实现完美的样式隔离。Shadow DOM 是什么,怎么玩可见 MDN这里

    公共 CSS 样式怎么处理

    上面说的都是子应用自己的 CSS 样式,那如果子应用之间要共享 CSS 怎么办呢?比如有两个子应用都用了 antd,那都要 import 两次 antd.min.css 了。
    这个问题和上面提到的处理“公共依赖”的问题是差不多的。官方给出两个建议:

    • 将公共的 CSS 放到 importmap 里,也可以理解为在 index.html 里直接加个 link 获取 antd 的 CSS 完事
    • 将所有的公共的 UI 库都 import 到 utility 里,将 antd 所有内容都 export,再把 utility 包放到 importmap 里,然后 import { Button } from '@your-org-name/utility'; 去引入里面的组件

    其实上面两个方法都大同小异,思路都是在主应用一波引入,只是一个统一引入CSS,另一个统一引入 UI 库。

    子应用的 JS 隔离

    我们来想想应用的 JS 隔离本质是什么,本质其实就是在 B 子应用里使用 window 全局对象里的变量时,不要被 A 子应用给污染了。

    一个简单的解决思路就是:在 mount A 子应用时,正常添加全局变量,比如 jQuery 的 $, lodash 的 _。在 unmount A 子应用时,用一个对象记录之前给 window 添加的全局变量,并把 A 应用里添加 window 的变量都删掉。下一次再 mount A 应用时,把记录的全局变量重新加回来就好了。
    single-spa 再次站出来:这个不用你自己手动记录 window 的变更了。single-spa-leaked-globals 已经实现好了,直接用就好了:

    import singleSpaLeakedGlobals from 'single-spa-leaked-globals';
    
    // 其它 single-spa-xxx 提供的生命周期函数
    const frameworkLifecycles = ...
    
    const leakedGlobalsLifecycles = singleSpaLeakedGlobals({
      globalVariableNames: ['$', 'jQuery', '_'], // 新添加的全局变量
    })
    
    export const bootstrap = [
      leakedGlobalsLifecycles.bootstrap, // 放在第一位
      frameworkLifecycles.bootstrap,
    ]
    
    export const mount = [
      leakedGlobalsLifecycles.mount, // mount 时添加全局变量,如果之前有记录在案的,直接恢复
      frameworkLifecycles.mount,
    ]
    
    export const unmount = [
      leakedGlobalsLifecycles.unmount, // 删掉新添加的全局变量
      frameworkLifecycles.unmount,
    ]
    

    但是,这个库的局限性在于:每个 url 只能加一个子 app,如果多个子 app 之间还是会访问同一个 window 对象,也因此会互相干扰,并不能做到完美的 JS 沙箱。

    比如:一个页面里,导航栏用 3.0 的 jQuery,而页面主体用 5.0 的 jQuery,那就会有冲突了。

    所以这个库的场景也仅限于:首页用 3.0 的 jQuery,订单详情页使用 5.0 的 jQuery 这样的场景。

    子应用的分类

    上面我们说到了,当 url 匹配 activeWhen 参数时,就会执行对应子应用的生命周期。那这样就相当于子应用和 url 绑定在了一起了。

    我们再来看 single-spa-leaked-globals,single-spa-css 这些库,虽然它们也导出了生命周期,但这些生命周期与页面渲染、url 变化没有多大关系。

    它们与普通的 application 唯一不同的地方就是:普通 application 的生命周期是通过 single-spa 来自动调度的,而这些库是要通过手动调度的。只不过我们一般选择在子应用里的生命周期里手动调用它们而已。

    这种与 url 无关的 “app” 在微前端也有着非常重要的作用,一般是在子应用的生命周期里提供一些功能,像 single-spa-css 就是在 mount 时添加 <link/> 标签。single-spa 将这样的 “类子 app” 称为 Parcel。

    同时,single-spa 还分出另一个类:Utility Modules。很多子应用都用 antd, dayjs, axios 的,那么就可以搞一个 utility 集合这些公共库,然后统一做 export,然后在 importmap 里统一导入。子应用就可以不需要在自己的 package.json 里添加 antd, dayjs, axios 的依赖了。

    总结一下,single-spa 将微前端分为三大类:


    create-single-spa

    上面介绍了一堆的与子应用相关的库,如果自己要从 0 开始慢慢地配置子应用就比较麻烦。所以,single-spa 说:不麻烦,有脚手架工具,一行命令生成子应用,都给您配好了。

    npm install --global create-single-spa
    
    # 或者
    yarn global add create-single-spa
    

    然后

    create-single-spa
    

    注意!这里的 create-single-spa 指的是创建子应用!

    总结

    以上就是 singles-spa 文档里的所有内容了(除了 SSR 和 Dev Tools,前者用的不多,后者自己看一下就会了,不多废话)。由于本文是通过发现问题到解决问题来讲述文档内容的,所以从头看到尾还是有点乱,这里就做一下总结:

    微前端概念

    特点: 技术栈无关 独立开发、独立部署 增量升级 独立运行时

    single-spa

    只做两件事: 提供生命周期概念,并负责调度子应用的生命周期 挟持 url 变化事件和函数,url 变化时匹配对应子应用,并执行生命周期流程

    三大分类: Application:子应用,和 url 强相关,交由 single-spa 调用生命周期 Parcel:组件,和 url 无关,手动调用生命周期 * Utility Module:统一将公共资源导出的模块

    “重要”概念 Root Config:指主应用的 index.html + main.js。HTML 负责声明资源路径,JS 负责注册子应用和启动主应用 Application:要暴露 bootstrap, mount, umount 三个生命周期,一般在 mount 开始渲染子 SPA 应用 * Parcel:也要暴露 bootstrap, mount, unmount 三个生命周期,可以再暴露 update 生命周期。Parcel 可大到一个 Application,也可以小到一个功能组件。与 Application 不同的是 Parcel 需要开发都手动调用生命周期

    SystemJS

    可以在浏览器使用 ES6 的 import/export 语法,通过 importmap 指定依赖库的地址。

    和 single-spa 没有关系,只是 in-browser import/export 和 single-spa 倡导的 in-browser run time 相符合,所以 single-spa 将其作为主要的导入导出工具。

    用 Webpack 动态引入可不可以,可以,甚至可能比 SystemJS 好用,并无好坏之分

    single-spa-layout

    和 Vue Router 差不多,主要功能是可以在 index.html 指定在哪里渲染哪个子应用。

    single-spa-react, single-spa-xxx....

    给子应用快速生成 bootstrap, mount, unmount 的生命周期函数的工具库。

    single-spa-css

    隔离前后两个子应用的 CSS 样式。

    在子应用 mount 时添加子应用的 CSS,在 unmount 时删除子应用的 CSS。子应用使用 Webpack 导出 CSS 文件时,要配合 ExposeRuntimeCssAssetsPlugin 插件来获取最终导出的 CSS 文件名。

    算实现了一半的 CSS 沙箱。

    如果要在多个子应用进行样式隔离,可以有两种方法:

    • Shadow DOM,样式隔离比较好的方法,但是穿透比较麻烦
    • Scoped CSS,在子应用的 CSS 选择器上添加前缀做区分,可以使用 postcss-prefix-selector 这个包来快速添加前缀
    single-spa-leaked-globals

    在子应用 mount 时给 window 对象恢复/添加一些全局变量,如 jQuery 的 $ 或者 lodash 的 _,在 unmount 时把 window 对象的变量删掉。

    实现了“如果主应用一个url只有一个页面”情况下的 JS 沙箱。

    公共依赖

    有两种方法处理:

    • 造一个 Utility Module 包,在这个包导出所有公共资源内容,并用 SystemJS 的 importmap 在主应用的 index.html 里声明
    • 使用 Webpack 5 Module Federation 特性实现公共依赖的导入

    最后

    single-spa 文档就这些了嘛?没错,就这些了。文档好像给了很多“最佳实践”,但真正将所有“最佳实践”结合起来并落地的又没多少。

    比如文档说用 Shadow CSS 来做子应用之间的样式隔离,但是 single-spa-leaked-globals 又不让别人在一个 url 上挂载多个子应用。感觉很不靠谱:这里行了,那里又不行了。

    再说回 Shadow CSS 来做样式隔离,但是也没有详细说明要具体要怎么做。像这样的例子还有很多:文档往往只告诉了一条路,怎么走还要看开发者自己。这就你给人一种 “把问题只解决一半” 的感觉。

    如果真的想用 single-spa 来玩小 Demo 的,用上面提到的小库来搭建微前端是可以的,但是要用到生产环境真的没那么容易。

    所以,为了填平 single-spa 遗留下来的坑,阿里基于 single-spa 造出了 qiankun 微前端框架,真正实现了微前端的所有特性,不过这又是另外一个故事了。
    转载【微前端】single-spa 到底是个什么鬼
    qiankun
    single-spa

    相关文章

      网友评论

          本文标题:【微前端】single-spa 到底是个什么鬼

          本文链接:https://www.haomeiwen.com/subject/yoglbrtx.html