微前端,前端这次词就不用多做解释了,这个概念的重点在于这个“微”字, 从字面意义上看,微是小的意思,小是相对于大的一个用于比较的形容词,所以通常是在项目庞大的情况下,才会考虑将它变小,去考虑将它拆分成若干个小项目。这就是做微前端所要达到的主要目标,将庞大的项目拆分成多个独立运行、独立部署和独立开发的小项目,使得项目利于维护和更新,然后在运行时,作为一个整体来呈现。

项目过于庞大的可能存在一系列问题,比如构建速度慢、应用加载慢、定位问题麻烦、项目可维护性差等等。

早期

过往的案例中,通常会使用iframe作为微前端的一种解决方案,但iframe有一些明显的缺点,比如浏览器的前进后退,由于iframe的url不会显示在浏览器的地址栏,就会使前进后退看上去有点奇怪,因为如果iframe存在历史记录,就会先对iframe的历史记录进行前进后退操作,但在地址栏上不会体现出来;并且我们也不能通过地址栏在主应用中直接打开子应用中某个页面,并且刷新也存在问题(子应用的状态会丢失);还有iframe是与外部窗口隔离开的,如果有弹窗需要遮罩层,就会使样式很奇怪,因为遮罩层只会遮住iframe的部分;然后iframe与外部窗口通信也不是很方便。当然iframe还是有优点的,比如资源隔离,样式之间不会互相影响,还可以加载外部页面,这在早期也算比较好用的一种方案。

<!-- index.html -->
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>主应用</title>
</head>
<body>
<style>
    body {
        font-size: 22px;
        color: #666;
        background-color: #f4f5ff;
    }
</style>
<p>我是主页面的内容</p>
<iframe src="./p1.html"></iframe>
</body>
</html>

<!-- p1.html -->
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>P1应用</title>
</head>
<body>
<style>
    body {
        font-size: 20px;
        color: orange;
        background-color: #e4e5ff;
    }
</style>
<p>
    我是p1的内容
    <button onclick="window.location.href='./p2.html'">点击跳转p2</button>
    <button onclick="window.location.href='https://126.com'">点击跳转126邮箱</button>
</p>
</body>
</html>

<!-- p2.html -->
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>P2应用</title>
</head>
<body>
<style>
    body {
        font-size: 20px;
        color: blue;
        background-color: #e4e5ff;
    }
</style>
<p>
    我是p2的内容
    <button onclick="window.location.href='./p1.html'">点击跳转p1</button>
</p>
</body>
</html>

虽然以前没有微前端的概念,但其实很多平台类的应用都存在对微前端的实践,他们会在主应用中提供入口,让用户可以进入其他应用去使用其他功能,而不是把所有的功能、所有的东西都放在一个应用里面。

single-spa

single-spa是近几年出来的微前端框架,带火了微前端的概念,我还没在项目中正式使用过,现在刚开始接触学习。它主要参考了近几年流行框架中路由的概念,来推出的一种方案。

借助single-spa脚手架,我们可以搭建三类项目:

  • 一个是包含root config的项目,可以说它是将所有项目包裹起来的主项目,可以将它看作是一个应用容器,在root config项目中包含一个名为xxx-root-config的js文件和一个index.ejs文件,这个ejs文件就是所有子应用所共用的html页面,root config项目通常配合使用single-spa-layout来帮助更方便地对应用布局、注册应用和定义路由
  • 第二类是普通的应用或者parcel组件,也就是普通的子应用,可以基于任意框架开发,parcel通常用于编写可以跨框架使用的组件,parcel和application的定义比较相似,只是使用上有所不同,application可以通过路由激活自动挂载,而parcel需要通过mountParcel或者mountRootParcel方法来手动挂载
  • 第三类是utility module,工具包,用于提供一些通用功能,比如样式、api等,通常没有组件需要渲染。

相关的概念,比如:Root Config、Application、Parcel、Layout Engine,都可以在single-spa的官网上查阅。

知道了这些,就可以开始动手搭建简单的demo项目了。

1. 先来创建一个root config类型的项目,暂时先不使用single-spa-layout

# ying.ye @ xieyingdeMacBook-Pro in ~/CodeProjects/mfe-demo1 [10:38:13] 
$ npx create-single-spa
npx: 393 安装成功,用时 79.396 秒
? Directory for new project platform
? Select type to generate single-spa root config
? Which package manager do you want to use? yarn
? Will this project use Typescript? No
? Would you like to use single-spa Layout Engine No
? Organization name (can use letters, numbers, dash or underscore) becky
directory,就是项目创建在哪个目录下,默认是点,就是当前目录,为了方便管理所有微前端应用,我把这个项目创建在platform目录,这个目录它会自动创建,select type,应用的类型,选择single-spa root config,接下来是包管理器,选择yarn,当然选其他的也可以,是否使用ts,因为是简单的demo,就不用了,是否使用single-spa-layout,暂时选择不用,最后是organization name,就是组织名称,我用我的英文名becky。

项目初始化完成后,看一下项目的内容,整体结构的话和普通脚手架生成的项目并没有太大的不同,我们可以先关注src目录下的两个文件,index.ejs和becky-root-config.js。index.ejs就是之前说的子应用所共用的html页面,becky-root-config.js就是前面所说的root config文件,它的名字是由organization name和root config组合,使用横杆拼接起来的。

我们可以先运行一下这个项目,打开localhost:9000。页面上有一串welcome之类的文字,就是一个欢迎页面。但是我们刚刚看了,项目里就两个文件,那这些内容是哪里来的呢?我们可以再仔细看下两个文件的内容。

首先是index.ejs。

<!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>Root Config</title>
  <!--
    Remove this if you only support browsers that support async/await.
    This is needed by babel to share largeish helper code for compiling async/await in older
    browsers. More information at https://github.com/single-spa/create-single-spa/issues/112
  -->
  <script src="https://cdn.jsdelivr.net/npm/regenerator-runtime@0.13.7/runtime.min.js"></script>
  <!--
    This CSP allows any SSL-enabled host and for arbitrary eval(), but you should limit these directives further to increase your app's security.
    Learn more about CSP policies at https://content-security-policy.com/#directive
  -->
  <meta http-equiv="Content-Security-Policy" content="default-src 'self' https: localhost:*; script-src 'unsafe-inline' 'unsafe-eval' https: localhost:*; connect-src https: localhost:* ws://localhost:*; style-src 'unsafe-inline' https:; object-src 'none';">
  <meta name="importmap-type" content="systemjs-importmap" />
  <!-- If you wish to turn off import-map-overrides for specific environments (prod), uncomment the line below -->
  <!-- More info at https://github.com/joeldenning/import-map-overrides/blob/master/docs/configuration.md#domain-list -->
  <!-- <meta name="import-map-overrides-domains" content="denylist:prod.example.com" /> -->

  <!-- Shared dependencies go into this import map. Your shared dependencies must be of one of the following formats:

    1. System.register (preferred when possible) - https://github.com/systemjs/systemjs/blob/master/docs/system-register.md
    2. UMD - https://github.com/umdjs/umd
    3. Global variable

    More information about shared dependencies can be found at https://single-spa.js.org/docs/recommended-setup#sharing-with-import-maps.
  -->
  <script type="systemjs-importmap">
    {
      "imports": {
        "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js"     
      }
    }
  </script>
  <link rel="preload" href="https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js" as="script">

  <!-- Add your organization's prod import map URL to this script's src  -->
  <!-- <script type="systemjs-importmap" src="/importmap.json"></script> -->

  <% if (isLocal) { %>
  <script type="systemjs-importmap">
    {
      "imports": {
        "@becky/root-config": "//localhost:9000/becky-root-config.js"
      }
    }
  </script>
  <% } %>

  <!--
    If you need to support Angular applications, uncomment the script tag below to ensure only one instance of ZoneJS is loaded
    Learn more about why at https://single-spa.js.org/docs/ecosystem-angular/#zonejs
  -->
  <!-- <script src="https://cdn.jsdelivr.net/npm/zone.js@0.11.3/dist/zone.min.js"></script> -->

  <script src="https://cdn.jsdelivr.net/npm/import-map-overrides@2.2.0/dist/import-map-overrides.js"></script>
  <% if (isLocal) { %>
  <script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/system.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/extras/amd.js"></script>
  <% } else { %>
  <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>
  <noscript>
    You need to enable JavaScript to run this app.
  </noscript>
  <main></main>
  <script>
    System.import('@becky/root-config');
  </script>
  <import-map-overrides-full show-when-local-storage="devtools" dev-libs></import-map-overrides-full>
</body>
</html>

可以看到,这里使用了一些在普通项目里很少有看到使用的东西,比如类型为systemjs-importmap的script标签,它里面的内容是json,然后在其他script标签中,还使用了System.import方法。这涉及到两块内容:importmap和systemjs。

importmap

importmap直译过来是导入映射,与模块的使用有关,一般我们在项目中导入模块,会调用require方法,或者使用import语句或方法,引入的模块通常需要使用npm之类的包管理器进行管理。但是import map提供了一种支持,让我们可以直接在页面上管理模块,不需要通过打包构建。不过由于这个特性比较新,很多浏览器不支持,可以看一个小的示例。

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>import maps demo</title>
    <script type="importmap">
        {
            "imports": {
                "react": "https://cdn.skypack.dev/react@17.0.1",
                "react-dom": "https://cdn.skypack.dev/react-dom",
                "moment": "https://cdn.skypack.dev/moment"
            }
        }
    </script>
</head>
<body>
    <div id="root"></div>
    <script type="module">
        import React from 'react';
        import ReactDOM from 'react-dom';
        import moment from 'moment';

        ReactDOM.render(`Hello World: ${moment().format('YYYY-MM-DD HH:mm:ss')}`, document.getElementById('root'));
    </script>
</body>
</html>

单独运行这个页面,可以看到不会报错,是正常运行显示的,说明我们完全不需要构建就可以使用import语句导入模块。

Import maps 本质上是一个配置文件,可以让开发者将模块标识符映射到一到多个文件,描述了依赖的解析方式,某种程度上,Import maps 给浏览器端带来了包管理,但是目前支持 Import Maps 的浏览器还很少。

简单来说importmap的作用就是使浏览器端支持模块的解析,而不需要应用构建步骤,这使得前端开发更便捷了,但是import maps现在来使用的话存在一个缺点,就是需要所有模块都导出成 ESModule,当前社区当中的很多模块都没有导出成 ESModule,有些模块甚至没有经过编译,所以目前使用仍然有一定困难。

systemjs

systemjs可以说是import maps的一种兼容方案,同样有模块管理的功能,在浏览器端实现了对 CommonJS、AMD、UMD 等各种模块的加载;它提供了一套自己的加载方法,我们可以对刚才的demo做一些改动,来看看效果。

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>import maps demo</title>
    <script type="systemjs-importmap">
        {
            "imports": {
                "react": "https://cdn.jsdelivr.net/npm/react@16.13.1/umd/react.production.min.js",
                "react-dom": "https://cdn.jsdelivr.net/npm/react-dom@16.13.1/umd/react-dom.production.min.js",
                "moment": "https://cdn.jsdelivr.net/npm/moment@2.29.4/moment.min.js"
            }
        }
    </script>
    <script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/system.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/extras/use-default.js"></script>
</head>
<body>
<div id="root"></div>
<script type="module">
    const React = await System.import('react');
    const ReactDOM = await System.import('react-dom');
    const moment = await System.import('moment');

    // console.log(moment);
    ReactDOM.render(`Hello World: ${moment().format('YYYY-MM-DD HH:mm:ss')}`, document.getElementById('root'));
</script>
</body>
</html>
首先将script的type改为systemjs-importmap,然后我们改为使用umd的模块,并在页面上使用script标签引入systemjs,在浏览器中引入system.js后,会去解析类型为systemjs-importmap的script标签里的import映射。最后对这个类型为module的script标签中的内容进行修改,将import语句都改为使用System.import方法。因为调用System.import得到的是一个promise,我们直接使用await来获取最终的内容。
log一下变量,可以看到调用System.import方法直接获取的是整个模块,为了使用更方面,我们可以在页面上引入systemjs的use-default脚本,将模块的default部分提取出来。

再单独运行一下这个页面,可以看到,呈现的效果是一样的。

关于importmap和systemjs就先到这里不细讲了,因为具体的我也还没仔细看。

回到index.ejs,我们看到,使用System.import导入了一个@becky/root-config,再看上面的importmap,我们可以看到,这个标识符对应的就是becky-root-config.js文件。也就是说页面上引用了root config打包构建后的文件。

index.ejs文件的最后是一个import-map-overrides-full的标签,它是single-spa提供的一个开发工具,show-when-local-storage="devtools",说明只要将localstorage中的devtools设置为true,就可以看到页面的右下角有一个图标。

点击这个图标可以打开一个面板,上面展示了浏览器管理的两个模块,就是我们在importmap中定义的,通过这个工具,我们可以对应用中定义的importmap映射地址进行修改替换,方便本地调试。当然我们也可以通过安装浏览器插件的方式来进行调试,对于chrome可以安装一个single-spa-inspector的插件来管理所有主应用下的子应用。

index.ejs看完了,再来看下root-config文件,这个文件很关键,我们通过这个文件来注册应用。可以看到顶部导入了两个方法,一个是registerApplication,见名知意,就是注册应用的方法,一个是start,就是启动项目的方法。

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

registerApplication({
  name: "@single-spa/welcome",
  app: () =>
    System.import(
      "https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js"
    ),
  activeWhen: ['/'],
});

//
// registerApplication({
//   name: '@becky/navbar',
//   app: () => System.import("@becky/navbar"),
//   activeWhen: "/"
// })

start({
  urlRerouteOnly: true,
});
可以看到,这里注册了一个子应用的示例,参数是一个对象,包含了三个属性,name、app和activeWhen,name就是名称,@single-spa/welcome ,app就是对应的应用,可以看到是导入了一个single-spa官方的用作示例的欢迎页,最后是activeWhen,就是这个应用在什么条件下激活,默认是“/”,所以根路由被激活时就会显示,所以我们刚刚访问localhost:9000页面看到的内容就是它。
activeWhen可以是一个字符串、或者函数,也可以是一个数组,包含函数或字符串类型的元素,本质上是一个函数,如果是字符串或者数组中的字符串元素,实际会被处理成用location进行判断,判断location.pathname是否是这个字符串开头的,如果是,路由就被激活,如果activeWhen是一个函数或者数组中包含的函数元素,则这个函数有一个默认传参是location,根据函数返回值判断路由对应的应用是否被激活。
所以activeWhen: ['/']相当于activeWhen: [(location) => location.pathname.startWith('/')]

可以看出single-spa是通过js文件接入子应用的。

检查元素看一下,可以看到页面上有一个div元素,id为single-spa-application:后边再跟一个应用名@single-spa/welcome,div里边就是欢迎页的内容。

现在我们在root-config文件中注册一个简单的应用,比如写一个app1,使用registerApplication注册。因为是项目中的文件,我们直接调用require,或者()=>import,activeWhen我们就用“/app1”。运行一下,访问路由/app1,可以看到app1被挂载了,我们写的内容也显示在页面上了。

// src/app1.js
export const bootstrap = (props) => {
    return Promise
        .resolve()
        .then(() => {
            console.log('App1 bootstrapped!');
        })
}

export const mount = (props) => {
    console.log(props);
    return Promise
        .resolve()
        .then(() => {
            const ele = document.createElement('div');
            ele.id="becky-app1";
            ele.innerText='App1 mounted!!';
            document.body.append(ele);
            console.log('App1 mounted!');
        })
}

export const unmount = (props) => {
    return Promise
        .resolve()
        .then(() => {
            console.log('App1 unmounted!');
        })
}

// src/becky-root-config.js
registerApplication({
  name: '@becky/app11',
  app: require('./app1'), // ()=>import('./app1)
  activeWhen: '/app1'
});

回到app1.js的内容,可以看到导出了几个方法,bootstrap、mount和unmount,通过single-spa官网对应用的定义,我们了解到,导出应用必须要定义这三个生命周期方法,来对应用的启动、挂载和卸载的过程做一些处理。当然如果需要的话,我们还可以定义unload生命周期方法。这些生命周期方法可以接收到一些属性,我们可以通过log来打印查看。

2.子应用

当然实际项目肯定没这么简单,我们再来创建几个项目,来作为子应用。同样使用single-spa的脚手架来创建。

# ying.ye @ xieyingdeMacBook-Pro in ~/CodeProjects/mfe-demo1 [10:38:13] 
$ npx create-single-spa
npx: 393 安装成功,用时 44.124 秒
? Directory for new project navbar
? Select type to generate single-spa application / parcel
? Which framework do you want to use? react
? Which package manager do you want to use? yarn
? Will this project use Typescript? No
? Organization name (can use letters, numbers, dash or underscore) becky
? Project name (can use letters, numbers, dash or underscore) navbar
同样的,需要选择directory,就是项目创建在什么目录下,默认是点,我把这个项目创建在navbar目录,作为整个应用的导航栏,select type,应用的类型,选择application or parcel,使用的ui框架,选择react,接下来是包管理器,选择yarn,是否使用ts,简单的demo就不用了,暂时选择不用,organization name,还是使用我的英文名becky,最后是project name,在root config项目创建的时候脚手架给设置了默认项目名root config,普通的子应用我们可以自己设置项目名,还是设置navbar。

同样的步骤我们再创建几个项目。

在子项目中执行yarn:start --port 8081命令,默认start用的端口是8080,因为要跑多个子项目,我们通过--port来指定端口,通过8081我们并不能直接访问目标页面,打开localhost:8081,我们可以看到一个页面,上面的文字提示我们需要在一个root config应用下来预览这个子应用页面,或者执行yarn start:standalone命令来预览,前面已经跑起来一个root config项目了,我们可以尝试直接把这个子应用注册到root config中。

先复制localhost:8081页面上提示我们复制的URL地址。

这次注册我们直接使用System.import,因为这个地址包含的内容是打包后的模块。

// src/becky-root-config.js
registerApplication({
  name: '@becky/navbar',
  app: () => System.import("http://localhost:8081/becky-navbar.js"),
  activeWhen: "/"
})

此时刷新页面后,会发现页面是空白的,控制台报了一个错误,提示无法解析‘react’这个标识符,这是因为我们的子应用使用了react框架,但是子应用使用webpack构建时是使用externals把react设置为外部模块,所以无法解析,此时我们可以选择在root config项目下的index.ejs文件中,使用importmap来引入react。

<!-- index.ejs -->
<script type="systemjs-importmap">
    {
      "imports": {
        "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js",
        "react": "https://cdn.jsdelivr.net/npm/react@16.13.1/umd/react.production.min.js",
        "react-dom": "https://cdn.jsdelivr.net/npm/react-dom@16.13.1/umd/react-dom.production.min.js"      
       }
    }
</script>

再次刷新页面,我们就可以看到页面有@becky/navbar is mounted!的字样,代表子应用挂载成功了。

为了管理方便,我们把navbar的模块也放到importmap中。

<!-- src/index.ejs -->
<% if (isLocal) { %>
  <script type="systemjs-importmap">
    {
      "imports": {
        "@becky/root-config": "//localhost:9000/becky-root-config.js",
        "@becky/navbar": "http://localhost:8081/becky-navbar.js"
      }
    }
  </script>
<% } %>
// src/becky-root-config.js
registerApplication({
  name: '@becky/navbar',
  app: () => System.import("@becky/navbar"),
  activeWhen: "/"
})

3. 使用single-spa-layout

刚刚我们创建的子应用可以都这样直接调用registerApplication来注册,但是这样一个个注册不太方便,页面布局也不太直观,而且刚刚直接访问根路径时,我们看到navbar是在顶部,但是访问app1的时候,navbar又在app1的下面,为了方便布局和注册应用,我们可以使用single-spa-layout这个库。通过使用这个库,我们还可以定义应用加载时的过渡效果。

# 在root config项目下安装依赖
yarn add single-spa-layout

然后对我们的index.ejs做一些修改。我们可以把官网的demo复制过来,然后做一点修改,把刚刚创建的几个子应用运行一下,并且把对应模块放到importmap中。

<!-- index.ejs -->
<% if (isLocal) { %>
  <script type="systemjs-importmap">
    {
      "imports": {
        "@becky/root-config": "//localhost:9000/becky-root-config.js",
        "@becky/navbar": "http://localhost:8081/becky-navbar.js",
        "@becky/settings": "http://localhost:8082/becky-settings.js",
        "@becky/students": "http://localhost:8083/becky-students.js"
      }
    }
  </script>  
<% } %>
<!-- ... -->
<template id="single-spa-layout">
    <single-spa-router>
      <nav class="topnav">
        <application name="@becky/navbar"></application>
      </nav>
      <div class="main-content">
        <route path="settings">
          <application name="@becky/settings"></application>
        </route>
        <route path="students">
          <application name="@becky/students"></application>
        </route>
      </div>
    </single-spa-router>
</template>

然后修改我们的root config文件。同样的把官网中的demo代码复制过来。

// src/becky-root-config.js
import {
  constructApplications,
  constructRoutes,
  constructLayoutEngine,
} from 'single-spa-layout';

const routes = constructRoutes(document.querySelector('#single-spa-layout'));
const applications = constructApplications({
  routes,
  loadApp({ name }) {
    return System.import(name);
  },
});
const layoutEngine = constructLayoutEngine({ routes, applications });

applications.forEach(registerApplication);

刷新页面,通过访问我们使用route标签定义的路由,可以激活不同的子应用。检查元素时,可以看到子应用挂载的布局与我们定义的single-spa-router中的内容是保持一致的。

此时一个简单的demo就完成了。

4. 增加简单的路由跳转

接下来我们可以修改一下navbar,就不用手动去改地址栏了。

打开navbar项目,直接查看src下面的文件,这个test文件是用于测试的,可以先不管,becky-navbar.js就是定义应用的主文件,可以看到使用了single-spa-react这个库,来创建single-spa应用,要修改navbar的展示内容,我们主要看rootComponent这个参数,它指定了应用的根组件,也就是说子应用的内容都是在这个组件里面,我们看到根组件就是这个root.component.js。

打开这个文件,我们做一点修改。首先我们看这个根组件,它接收了一个props参数,里面包含了跟应用相关的一些信息,比如props.name,就是应用名称,刚刚显示在页面上了。

export default function Root(props) {
  const onClick = (path) => {
    window.singleSpaNavigate(path);
  }

  return (
      <section>
        <li>
          <a href="" onClick={ () => onClick('/settings')}>Settings</a>
        </li>
        <li>
          <a href="" onClick={ () => onClick('/students')}>Students</a>
        </li>
      </section>
  );
}

完成修改后就可以看到效果了,点击不同的链接可以跳转不同的路由。

我们可以继续尝试在子应用中配置子应用自己的路由。比如students项目,我们给他添加路由功能。

首先添加react-router-dom依赖,再对root.component进行改造。再添加几个react组件,并配置一张路由表。由于子应用的路由是基于主应用的路由,所以给BrowserRouter配置一个basename属性,/students。
// src/root.component.js
import { BrowserRouter } from "react-router-dom";
import App from './components/App';

export default function Root(props) {
  return (
      <BrowserRouter basename={"/students"}>
        <App />
      </BrowserRouter>
  );
}

// src/components/App.js
import React from 'react';
import {NavLink, useRoutes, useInRouterContext} from 'react-router-dom';
import routes from "../routes";

function App(props) {
    console.log('xxx', useInRouterContext())
    // 根据路由表生成对应的路由规则
    const element = useRoutes(routes);

    return (
        <div>
            <div className="row">
                <div className="col-xs-2 col-xs-offset-2">
                    <div className="list-group">
                        {/* 路由链接 */}
                        <NavLink className="list-group-item" to="/list">List</NavLink>
                        <NavLink className="list-group-item" end to="/detail">Detail</NavLink>
                    </div>
                </div>
                <div className="col-xs-6">
                    <div className="panel">
                        <div className="panel-body">
                            {/*  在展示路由组件的位置注册路由  */}
                            {element}
                        </div>
                    </div>
                </div>
            </div>
        </div>
    );
}

export default App;

// src/components/Detail.js
import React from 'react';

function Detail(props) {
    return (
        <h3>我是Detail的内容</h3>
    );
}

export default Detail;

// src/components/List.js
import React from 'react';

function List(props) {
    return (
        <h3>我是List的内容</h3>
    );
}

export default List;

// src/routes/index.js
import List from "../components/List";
import Detail from "../components/Detail";

export default [
    {
        path: '/list',
        element: <List/>
    },
    {
        path: '/detail',
        element: <Detail/>,
    }
]

改造完成后,重启students项目。再继续看,可以正常运行并完成路由跳转。

简单的demo就到此为止了。

5. webpack的配置

最后我们再看一下webpack的配置。文件很简单,因为single-spa这个框架做了一层封装,我们可以在students项目把配置打印出来看一下。

主要看一下output和externals,externals配置的是引用外部的模块,可以看到single-spa、react、react-dom这些三方库都是引用的外部模块,也就是利用了我们在importmap中配置的模块映射,还有@becky开头的模块标识符也是引用了外部的模块,这些也是在importmap中配置了。

然后是output,看到输出的文件是becky-students.js,也就是importmap映射的子应用的文件。libraryTarget是system,他指定了使用system的模式处理模块,我们可以从network再看一下页面加载的becky-student.js的内容,可以看到文件的开头就调用了System.register注册了模块,这又涉及到了systemjs的内容。

最后

简单说了下微前端、以及single-spa的基础使用。有兴趣同学的可以继续查阅single-spa的官网文档、以及其他优秀的文章。

最后再说些微前端的缺点,比如子应用如果维护在不同的代码库里,这可能会造成代码分散,不利于整体的管理,对管理者要求更高,如果有新需求迭代,需要盘点确认涉及哪些子应用,还可能出现三方库版本不同所造成的维护问题。使用single-spa的话由于没有沙箱环境,可能会出现样式互相干扰的情况,需要去处理。由于我还没有具体的实践经验,那在实际使用中还可能出现一些问题,比如如何去拆分一个大型项目。

关于是否使用微前端,通过何种方式、何种框架来实践微前端,可以借鉴他人经验,根据实际情况来整体考量。


beckyyyy
550 声望414 粉丝

工作多年的一只前端菜鸟