微前端

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

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

早期

过往的案例中,通常会使用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的话由于没有沙箱环境,可能会出现样式互相干扰的情况,需要去处理。由于我还没有具体的实践经验,那在实际使用中还可能出现一些问题,比如如何去拆分一个大型项目。

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

微前端、single-spa初探的更多相关文章

  1. 微前端框架single-spa初探

    前言 最近入职的一家公司采用single-spa这个微前端框架,所以自学了此框架. single-spa这个微前端框架虽然有中文文档,但是有些零散和晦涩. 所以我想在学习之余,写篇博客拉平一下这个学习 ...

  2. 微前端大赏二-singlespa实践

    微前端大赏二-singlespa实践 微前端大赏二-singlespa实践 序 介绍singleSpa singleSpa核心逻辑 搭建环境 vue main react child 生命周期 结论 ...

  3. 微前端框架single-spa子应用加载解析

    作者:京东物流 宁冲 1 前言 什么是微前端? 微前端是指存在于浏览器中的微服务. 本文主要通过对微前端框架single-spa的基座应用加载子应用的single-spa-vue函数库进行分析,通过代 ...

  4. piral 基于typescript 的微前端开发框架

    piral有一个微前端开发框架,功能强大,文档比较全,扩展能力也比较好 包含以下特性: 特性 高度模块化 多框架兼容 支持资源文件的拆分 全局状态管理 独立开发和部署 CLI工具 与同类框架的比较 参 ...

  5. podium服务器端的微前端开发框架

    podium 是一个比较全的微前端开发框架. 具有以下特性 自治开发 强大的组合能力 基于约定的开发模式 podium 包含的组件 podlets 页面片段,是一个独立的http 服务,独立运行的,实 ...

  6. 微前端框架 single-spa

    单体应用对比前端微服务化 普通的前端单体应用 微前端架构 1.基本概念 实现一套微前端架构,可以把其分成四部分(参考:https://alili.tech/archive/11052bf4/) 加载器 ...

  7. 《OneForAll框架搭建之旅》前端篇:微前端架构设计(Vue)

    心之所向,勇往直前!记录开发过程中的那些小事,给自己加点经验值. 前言 作为一个.Net后端开发,在竞争愈加激烈的当下,掌握点前端配菜好像已经是家常便饭了. 刚好在工作的第5个年头,辞去小主管职务的我 ...

  8. EMP-面向未来微前端方案正式开源了!

    原文团队掘金平台:https://juejin.im/post/6891532248269783054 EMP项目github链接: https://github.com/efoxTeam/emp E ...

  9. 「译」用 Blazor WebAssembly 实现微前端

    原文作者: Wael Kdouh 原文链接:https://medium.com/@waelkdouh/microfrontends-with-blazor-webassembly-b25e4ba3f ...

  10. 微前端 & 微前端实践 & 微前端教程

    微前端 & 微前端实践 & 微前端教程 微前端 micro frontends https://micro-frontends.org/ https://github.com/neul ...

随机推荐

  1. asp.net core之Startup

    Startup介绍 Startup是Asp.net Core的应用启动入口.在.NET5及之前一般会使用startup.cs类进行程序初始化构造.如下: public class Startup { ...

  2. Django: 'block' tag with name 'header' appears more than once

    错误原因 在同一文件中,重复引用标签多次 解决方案: 删掉重复的标签即可.

  3. VS Code 有哪些好用的插件呢?【持续更新】

    一.画图工具:vscode-drawio   功能:在 VSCode 中画流程图.数据流图等等.        使用方法:     创建一个后缀名为 .drawio 的文件,然后用 VSCode 打开 ...

  4. 快速入门OpenCv(python版)

    OpenCV是一个(开源)发行的跨平台计算机视觉库,可以运行在Linux.Windows和Mac OS操作系统上.它轻量级而且高效--由一系列 C 函数和少量 C++ 类构成,同时提供了Python. ...

  5. [minio]挂载minio到本地

    前言 将minio的bucket挂载到本地文件系统 环境 客户端系统版本:centos 7 MinIO节点IP:192.168.0.20 s3fs方式步骤 安装s3fs客户端(可能需要先安装epel- ...

  6. 基于Prometheus搭建监控平台

    目录 前言 配置server单节点 prometheus.service 配置node节点 配置mysql监控 在数据库中添加exporter账户 修改mysql_exporter的配置 添加serv ...

  7. go项目实现mysql接入以及web api

    本文为博主原创,转载请注明出处: 创建go项目,并在go项目中接入mysql,将mysql的配置项单独整理放到一个胚子和文件中,支持项目启动时,通过加载配置文件中的值,然后创建数据库连接. 之后使用n ...

  8. U盘目录穿越获取车机SHELL - 分析与复现

    github上破解日系车机的文章 - https://github.com/ea/bosch_headunit_root 其中有利用 U 盘获取车机 shell 的操作 主要根据下面这篇文章进行环境搭 ...

  9. 《SQL与数据库基础》08. 多表查询

    目录 多表查询 多表关系 一对多 多对多 一对一 多表查询概述 分类 内连接 外连接 自连接 联合查询 子查询 分类 标量子查询 列子查询 行子查询 表子查询 案例 本文以 MySQL 为例 多表查询 ...

  10. uni微信小程序隐私协议

    最近小程序又新增了个 隐私协议弹窗.需要用户去授权,官网的一些API才能使用.官网地址 功能展示 项目地址:https://ext.dcloud.net.cn/plugin?id=14358 1.ma ...