乐趣区

关于前端:微前端singlespa初探

微前端

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

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

晚期

过往的案例中,通常会应用 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/[email protected]/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/[email protected]/lib/system/single-spa.min.js"     
      }
    }
  </script>
  <link rel="preload" href="https://cdn.jsdelivr.net/npm/[email protected]/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/[email protected]/dist/zone.min.js"></script> -->

  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/import-map-overrides.js"></script>
  <% if (isLocal) { %>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/system.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/extras/amd.js"></script>
  <% } else { %>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/system.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/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/[email protected]",
                "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/[email protected]/umd/react.production.min.js",
                "react-dom": "https://cdn.jsdelivr.net/npm/[email protected]/umd/react-dom.production.min.js",
                "moment": "https://cdn.jsdelivr.net/npm/[email protected]/moment.min.js"
            }
        }
    </script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/system.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/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/[email protected]/lib/system/single-spa.min.js",
        "react": "https://cdn.jsdelivr.net/npm/[email protected]/umd/react.production.min.js",
        "react-dom": "https://cdn.jsdelivr.net/npm/[email protected]/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 的话因为没有沙箱环境,可能会呈现款式相互烦扰的状况,须要去解决。因为我还没有具体的实践经验,那在理论应用中还可能呈现一些问题,比方如何去拆分一个大型项目。

对于是否应用微前端,通过何种形式、何种框架来实际微前端,能够借鉴别人教训,依据理论状况来整体考量。

退出移动版