微前端

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

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

早期

过往的案例中,通常会使用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. 端路由原理及react-router的常用组件

    在react中,通常都是使用单页面应用(SPA),即整个页面只有一个html,然后通过不同的url地址进行组件的匹配和切换. 我们看到的url地址可能会有两种形式,一种是 localhost:3000 ...

  2. Unity 编辑器资源导入处理函数 OnPreprocessTexture:深入解析与实用案例

    Unity 编辑器资源导入处理函数 OnPreprocessTexture 用法 点击封面跳转下载页面 简介 在Unity中,我们可以使用编辑器资源导入处理函数(OnPreprocessTexture ...

  3. mybatis-plus+nacos配置中心和服务发现保姆级教程

    默认你已经看了我的Mybatis-Plus+Mysql的教程,现在有了一个简单的项目如下(之前的教程:  https://www.cnblogs.com/leafstar/p/17638741.htm ...

  4. vue3探索——组件通信之事件总线

    Vue2.x使用EventBus进行组件通信,而Vue3.x推荐使用mitt.js. 比起Vue实例上的EventBus,mitt.js好在哪里呢?首先它足够小,仅有200bytes,其次支持全部事件 ...

  5. tailwindcss -原子化 CSS 框架

    原子化 CSS 框架 我记得很久之前有时候为了少写些css,我们通常会有如下的样板代码 .block { display: block; } .flex { display:flex } .flex- ...

  6. 【源码】Vue.js 官方脚手架 create-vue 是怎么实现的?

    Vue.js 官方脚手架 create-vue 是怎么实现的? 摘要 本文共分为四个部分,系统解析了vue.js 官方脚手架 create-vue 的实现细节. 第一部分主要是一些准备工作,如源码下载 ...

  7. 618京东到家APP-门详页反爬实战

    一.背景与系统安全需求分析 1. 系统的重要性 上图所示是接口所属位置.对电商平台或在线商店而言,分类查商品都是很重要的,通过为用户提供清晰的商品分类,帮助他们快速找到所需产品,节省浏览时间,提升购物 ...

  8. 谱图论:Laplacian二次型和Markov转移算子

    以下部分是我学习CMU 15-751: TCS Toolkit的课堂笔记.由于只是个人笔记,因此许多地方在推导上可能不那么严谨,还望理论大佬多多包涵. 1 问题定义 1.1 无向图\(G\) 在本文中 ...

  9. Python常用模块-20个常用模块总结

    目录 time模块 datetime模块 random 模块 os 模块 sys 模块 json 和 pickle 模块 hashlib和hmac 模块 logging 模块 numpy 模块 pan ...

  10. 轻量通讯协议 --- MQTT

    介绍 一.MQTT简介 MQTT(Message Queuing Telemetry Transport) 是一种轻量级的消息传输协议,通常用于在物联网(IoT)和传感器网络中进行通信.它设计用于在低 ...