4

微前端框架single-spa初探

 3 years ago
source link: https://www.cnblogs.com/vvjiang/p/15240799.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

最近入职的一家公司采用single-spa这个微前端框架,所以自学了此框架。

single-spa这个微前端框架虽然有中文文档,但是有些零散和晦涩。

所以我想在学习之余,写篇博客拉平一下这个学习曲线。

什么是微前端?

微前端的灵感来源于服务端微服务的理念。

可以简单理解为,在开发一个复杂前端应用时,将其划分为一系列更小更简单的前端应用。

这些前端应用可以单独开发、测试、部署,松耦合,可维护性强,还可以让前端代码实现增量升级和使用不同的框架。

它的懒加载还能让整个复杂应用加载速度变快。

常用微前端玩法和single-spa

在我之前的公司是使用iframe来实现微前端的,但是各个子应用间的通信往往比较麻烦,而且很不灵活。

而最新的微前端理念,是由webpack5中模块联合特性实现的,这里就不多讲了。

single-spa是一个比较流行的微前端框架,它并不是使用iframe来实现微前端,也不是通过模块联合,而是通过路由路径来在dom上加载不同的子应用。

Import maps和SystemJS

在具体讲解single-spa前,我们得先了解一个东西:Import maps

这个功能是Chrome 89才支持的。

顾名思义,它是对import的一个映射处理,让你控制在js中使用import时,到底从哪个url获取这些库。

比如通常我们会在js中,以下面这种方式引入模块:

    import moment from "moment"

正常情况下肯定是node_modules中引入,但是现在我们在html中加入下面的代码:

    <script type="importmap">
    {
        "imports": {
            "moment": "/moment/src/moment.js"
        }
    }
    </script>

这里/moment/src/moment.js这个地址换成一个cdn资源也是可以的。最终达到的效果就是:

    import moment from "/moment/src/moment.js"

有了Import maps,import的语法就可以直接在浏览器中使用,而不再需要webpack来帮我们进行处理,不需要从node_modules中去加载库。

Import maps甚至还有一个兜底的玩法:

    "imports": {
        "jquery": [
            "https://某CDN/jquery.min.js",
            "/node_modules/jquery/dist/jquery.js"
        ]
    }

当cdn无效时,再从本地库中获取内容。

它的功能还有很多,我就不一一列举了,只需要对这个有一定的了解即可。

尽管Import maps非常强大,但是毕竟浏览器兼容性还并不是很好,所以就有了我们的polifill方案:SystemJS

SystemJS同样是一个模块加载器,可兼容到IE11,同样支持import映射,但是它的语法稍有不同:

    <script src="system.js"></script>
    <script type="systemjs-importmap">
    {
        "imports": {
            "lodash": "https://unpkg.com/[email protected]/lodash.js"
        }
    }
    </script>

在浏览器中引入system.js后,会去解析type为systemjs-importmap的script下的import映射。

它和Import maps最终达到的效果是一致的。

single-spa

之所以我们要先讲Import mapsSystemJS,就是因为single-spa的微前端往往需要结合SystemJS来实现。

single-spa框架中,基座会检测浏览器url的变化,在变化时往往通过SystemJS的import映射,来加载不同的子应用js。

但是需要注意,single-spa并不是必须依赖SystemJS

刚刚我们提到了一个概念:基座,现在讲下single-spa的两个概念:基座应用

你可以简单理解应用是一个个的单页面应用,而基座是一个应用管理器,用来根据路由加载不同的应用

一般在基座中我们需要像下面这样注册一个应用:

import { registerApplication, start } from 'single-spa';

// 注册应用1
registerApplication({
    name:'app1',
    app:() => import('./app1.js'),
    activeWhen: '/app1',
    customProps: { myTitle: "传递给应用的自定义参数的值" }
});

// 注册应用2
registerApplication({
    name:'app2',
    app:() => import('./app2.js'),
    activeWhen: '/app2'
});

start();

在上面的代码中,我们注册了app1和app2两个应用,分别匹配路由/app1和/app2。

也就是说当路由是/app1或者/app1/home时,会直接加载app1这个应用。

注册应用后,需要start()来开始挂载应用,否则只会下载应用,而不会挂载应用。

那么应用应该如何设置呢?

我们上面代码引用的./app1.js并没有导出一个真的单页面应用,而一般是如下:

console.info('第一步 下载应用阶段')
export function bootstrap(props) {
    const {
        name,        // 应用名称
        singleSpa,   // singleSpa实例
        mountParcel, // 手动挂载的函数
        myTitle  // 我们之前在注册应用时传递给customProps的属性
    } = props;     // Props 会传给每个生命周期函数

    // 这个生命周期函数会在应用第一次挂载前执行一次。
    return Promise.resolve().then(() => {
        console.info('第二步 初始化', myTitle)
    })
}
export function mount(props) {
    // 每当应用路由匹配成功,但该应用处于未挂载状态时,挂载的生命周期函数就会被调用。调用时,函数会根据URL来确定当前被激活的路由,创建DOM元素、监听DOM事件等以向用户呈现渲染的内容。任何子路由的改变(如hashchange或popstate等)不会再次触发mount,需要各应用自行处理。
    return Promise.resolve().then(() => {
        console.info('第三步 挂载应用', props.name)
        document.getElementById('root').innerHTML = "我是app1啊"
    })
}

export function unmount(props) {
    // 每当应用路由匹配不成功,但该应用已挂载时,卸载的生命周期函数就会被调用。卸载函数被调用时,会清理在挂载应用时被创建的DOM元素、事件监听、内存、全局变量和消息订阅等。
    return Promise.resolve().then(() => {
        console.info('第四步 卸载应用', props.name)
        document.getElementById('root').innerHTML = ""
    })
}

export function unload(props) {
    // 移除”生命周期函数的实现是可选的,它只有在unloadApplication被调用时才会触发。如果一个已注册的应用没有实现这个生命周期函数,则假设这个应用无需被移除。
    // 移除的目的是各应用在移除之前执行部分逻辑,一旦应用被移除,它的状态将会变成NOT_LOADED,下次激活时会被重新初始化。
    // 移除函数的设计动机是对所有注册的应用实现“热下载”,不过在其他场景中也非常有用,比如想要重新初始化一个应用,且在重新初始化之前执行一些逻辑操作时。
    return Promise.resolve().then(() => {
        console.info('第五步 移除应用', props.name)
    })
}

可以看到我们的app1.js这个应用中的代码,并没有导出一个单页面应用组件,而是导出了几个生命周期函数,然后通过这几个生命周期函数来控制组件的初始化,加载和卸载。

它这个操作是从现在我们的react或者vue这些框架的组件生命周期中获得灵感,将生命周期应用于整个应用程序。

single-spa 与 SystemJS实现微前端

看了上面的代码之后你可能有点疑惑,你这个东西也没什么用嘛,不就是个懒加载吗?

哪来的微前端?

我用个React.lazy(() => import('./app1.js'))来个懒加载怎么了,你不要说一大堆把我绕晕了。

上面这些实际上还真的没有实现微前端,但是,你可以结合我们之前讲的微前端,想象一下:

如果./app1.js不是基座这个项目内的代码,而是另一个项目呢?

我们将这个app1.js放在一个单独的项目中,它用react来写了一个单页面应用。

再将app2.js放在另一个单独的项目中,它用vue来写了一个单页面应用。

通过我们现在的这个基座项目再来处理这两个应用呢?

我们的基座项目是不是就可以写成下面这样:

import { registerApplication, start } from 'single-spa';

// 注册应用1
registerApplication({
    name:'app1',
    app:() => import('@晓组织/app1'),
    activeWhen: '/app1',
    customProps: { myTitle: "传递给应用的自定义参数的值" }
});

// 注册应用2
registerApplication({
    name:'app2',
    app:() => import('@晓组织/app2'),
    activeWhen: '/app2'
});

start();

然后再在基座项目的模板页中来个引入映射:

<script type="systemjs-importmap">
    {
        "imports": {
            "@晓组织/app1": "//某网站/app1.js",
            "@晓组织/app2": "//另一个网站/app2.js"
        }
    }
</script>

以后我们要做app1模块的部分,只需要在app1这个项目中维护就可以了,不会干扰到其他的应用。

以后React20,React30出来,或者部分项目升级webpack,或者给一个项目大调整,我们可以一个个小应用尝试升级修改,不用所有项目同时调整。不仅风险变小了很多,也更加可控。

上面是结合SystemJS实现的微前端,其实还有使用npm包和单项目的玩法,但是不推荐,有兴趣的可以参考官网的这篇文章:拆分应用

single-spa-react

看到这里的朋友一定会想,这个东西好是好,怎么把它和react的单页面应用结合起来?

让我自己写加载和卸载react的单页面应用?这也太挫了吧。

当然不可能,single-spa的生态可是很好的。

single-spa-react就是一个辅助库,它可以帮助React应用程序实现single-spa 需要的生命周期函数(bootstrap、mount 和 unmount)。

import React from 'react';
import ReactDOM from 'react-dom';
import rootComponent from './path-to-root-component.js';

import singleSpaReact from 'single-spa-react';
const reactLifecycles = singleSpaReact({
    React,
    ReactDOM,
    rootComponent,
    errorBoundary(err, info, props) {
        // https://reactjs.org/docs/error-boundaries.html
        return (
            <div>This renders when a catastrophic error occurs</div>
        );
    },
});
export const bootstrap = reactLifecycles.bootstrap;
export const mount = reactLifecycles.mount;
export const unmount = reactLifecycles.unmount;

这是官网提供的示例代码,那个rootComponent就是我们的顶层React组件。

其它的就不用多说了,毕竟都很容易理解,想要了解详情可以看下:详情

其它的一些语言比如vue和Angular都有自己对应的辅助库,具体可以查阅:辅助库列表

single-spa的Parcels

Parcels是single-spa的一个高级特性,是一个与框架无关的组件,与应用的不同之处在于parcel组件需要手动挂载,而不是通过匹配路由的activity方法被激活。

官网说:只有在涉及到跨框架的应用之间进行组件调用时,我们才需要考虑parcel的使用。

一想到我们公司只用react,那么打扰了,再见。

CLI工具:create-single-spa和可定制化的webpack配置

上面很多都是在讲原理,现在到了实际应用的时候了。

single-spa的相关配置有些繁琐,所以我推荐依赖现有的CLI去新建项目,可以去改造,而不是自己从零开始去搭建。

npx create-single-spa

运行上面这行命令后,就会让你做一系列选择,那些选择就不多说,只说最关键的。

create-single-spa 可以让你创建三种项目,分别是:

  • single-spa application/parcel :应用和parcel。
  • single-spa root config:基座。
  • in-browser utility module: 通用组件,工具函数,样式指引。

你可以根据需要去创建不同的项目。

single-spa还提供了一些推荐的webpack配置库,不用自己操心去设置webpack配置。

不过我建议最后输出得到webpack配置你还是稍微打印出来看一下,做到心中有数,然后才可以再根据它的配置去做相应修改。

Demo分享

光看不做假把式,下面是我自己根据single-spa的CLI工具搭建的两个简易Demo:

同时运行起来即可,命令都是:

yarn start

如果确实想入门的话,对比一下我写的和官方CLI工具初始化时的一些差异,可以了解到更多的一些小细节。

总的来说,single-spa是一个非常优秀的微前端框架。

微前端领域最近的趋势是用webpack5中模块联合特性来实现,这与single-spa并不冲突,single-spa也有结合模块联合特性实现的例子。

不过这就不在本篇文章的涉及范围内了,也许以后会写下这块的内容。

本篇博客到此结束。

希望这篇文章能给您带来一些帮助,如有疏漏,也请不吝赐教。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK