single-spa
Micro-frontends are the future of front end web development.
微前端是前端Web开发的未来。
Inspired by microservices, which allow you to break up your backend into smaller pieces, micro-frontends allow you to build, test, and deploy pieces of your frontend app independently of each other.
受微服务的启发,微服务使您可以将后端分解成较小的部分,微前端使您可以彼此独立地构建,测试和部署前端应用程序的各个部分。
Depending on the micro-frontend framework you choose, you can even have multiple micro-frontend apps — written in React, Angular, Vue, or anything else — coexisting peacefully together in the same larger app.
根据您选择的微前端框架,您甚至可以拥有多个用React,Angular,Vue或其他任何语言编写的微前端应用程序,在同一个较大的应用程序中和平共处。
In this article, we’re going to develop an app composed of micro-frontends using single-spa and deploy it to Heroku.
在本文中,我们将使用Single-SPA开发一个由微前端组成的应用程序,并将其部署到Heroku 。
We’ll set up continuous integration using Travis CI. Each CI pipeline will bundle the JavaScript for a micro-frontend app and then upload the resulting build artifacts to AWS S3.
我们将使用Travis CI设置持续集成。 每个CI管道都将为一个微前端应用程序捆绑JavaScript,然后将生成的构建工件上载到AWS S3 。
Finally, we’ll make an update to one of the micro-frontend apps and see how it can be deployed to production independently of the other micro-frontend apps.
最后,我们将对其中一个微前端应用程序进行更新,并了解如何将其独立于其他微前端应用程序部署到生产中。
演示应用程序概述 (Overview of the Demo App)
Before we discuss the step-by-step instructions, let’s get a quick overview of what makes up the demo app. This app is composed of four sub-apps:
在讨论逐步说明之前,让我们快速概述一下演示应用程序的组成。 该应用程序由四个子应用程序组成:
A container app that serves as the main page container and coordinates the mounting and unmounting of the micro-frontend apps
一个容器应用程序 ,用作主页容器,并协调微型前端应用程序的安装和卸载
A micro-frontend navbar app that’s always present on the page
始终显示在页面上的微型前端导航栏应用
A micro-frontend “page 1” app that only shows when active
一个微型前端“页面1”应用程序 ,仅在激活时显示
A micro-frontend “page 2” app that also only shows when active
微型前端“第2页”应用程序 ,仅在活动状态时显示
These four apps all live in separate repos, available on GitHub, which I’ve linked to above.
这四个应用程序都位于单独的存储库中,可以在GitHub上找到,我已经在上面链接到了。
The end result is fairly simple in terms of the user interface, but, to be clear, the user interface isn’t the point here.
就用户界面而言,最终结果相当简单,但要明确的是,用户界面并不是这里的重点。
If you’re following along on your own machine, by the end of this article you too will have all the underlying infrastructure necessary to get started with your own micro-frontend app.
如果您在自己的机器上进行跟踪,那么到本文结尾,您还将拥有开始使用自己的微型前端应用程序所需的所有基础基础结构。
Alright, grab your scuba gear, because it’s time to dive in!
好吧,抓住您的潜水用具,因为是时候潜水了!
创建容器应用 (Creating the Container App)
To generate the apps for this demo, we’re going to use a command-line interface (CLI) tool called create-single-spa. The version of create-single-spa at the time of writing is 1.10.0, and the version of single-spa installed via the CLI is 4.4.2.
为了生成此演示的应用程序,我们将使用称为create-single-spa的命令行界面(CLI)工具。 在撰写本文时,create-single-spa的版本为1.10.0,通过CLI安装的single-spa的版本为4.4.2。
We’ll follow these steps to create the container app (also sometimes called the root config):
我们将按照以下步骤创建容器应用(有时也称为root配置):
mkdir single-spa-demo
cd single-spa-demo
mkdir single-spa-demo-root-config
cd single-spa-demo-root-config
npx create-single-spa
We’ll then follow the CLI prompts:
然后,我们将遵循CLI提示:
- Select “single spa root config” 选择“单个spa根配置”
- Select “yarn” or “npm” (I chose “yarn”) 选择“纱线”或“ npm”(我选择“纱线”)
- Enter an organization name (I used “thawkin3,” but it can be whatever you want) 输入组织名称(我使用了“ thawkin3”,但可以是您想要的任何名称)
Great! Now, if you check out the single-spa-demo-root-config
directory, you should see a skeleton root config app. We'll customize this in a bit, but first let's also use the CLI tool to create our other three micro-frontend apps.
大! 现在,如果您检出single-spa-demo-root-config
目录,您应该会看到一个骨架根配置应用程序。 我们将对此进行一些自定义,但首先我们还要使用CLI工具来创建其他三个微型前端应用程序。
创建微前端应用 (Creating the Micro-Frontend Apps)
To generate our first micro-frontend app, the navbar, we’ll follow these steps:
要生成第一个微型前端应用程序navbar,我们将遵循以下步骤:
cd ..
mkdir single-spa-demo-nav
cd single-spa-demo-nav
npx create-single-spa
We’ll then follow the CLI prompts:
然后,我们将遵循CLI提示:
- Select “single-spa application / parcel” 选择“单个spa应用程序/包裹”
- Select “react” 选择“React”
- Select “yarn” or “npm” (I chose “yarn”) 选择“纱线”或“ npm”(我选择“纱线”)
- Enter an organization name, the same one you used when creating the root config app (“thawkin3” in my case) 输入组织名称,该名称与您在创建根配置应用程序时使用的名称相同(在我的情况下为“ thawkin3”)
- Enter a project name (I used “single-spa-demo-nav”) 输入项目名称(我使用“ single-spa-demo-nav”)
Now that we’ve created the navbar app, we can follow these same steps to create our two page apps. But, we’ll replace each place we see “single-spa-demo-nav” with “single-spa-demo-page-1” the first time through and then with “single-spa-demo-page-2” the second time through.
现在,我们已经创建了导航栏应用程序,我们可以按照以下相同的步骤来创建两个页面的应用程序。 但是,我们将在第一次使用“ single-spa-demo-page-1”替换每个我们看到“ single-spa-demo-nav”的地方,然后将“ single-spa-demo-page-2”替换为第二次通过。
At this point we’ve generated all four apps that we need: one container app and three micro-frontend apps. Now it’s time to hook our apps together.
至此,我们已经生成了我们需要的所有四个应用程序:一个容器应用程序和三个微前端应用程序。 现在是时候将我们的应用程序连接在一起了。
在容器应用程序中注册Micro-Frontend应用程序 (Registering the Micro-Frontend Apps with the Container App)
As stated before, one of the container app’s primary responsibilities is to coordinate when each app is “active” or not. In other words, it handles when each app should be shown or hidden.
如前所述,容器应用程序的主要职责之一是协调每个应用程序是否处于“活动”状态。 换句话说,它处理何时应显示或隐藏每个应用程序。
To help the container app understand when each app should be shown, we provide it with what are called “activity functions.” Each app has an activity function that simply returns a boolean, true or false, for whether or not the app is currently active.
为了帮助容器应用了解何时应显示每个应用,我们为其提供了所谓的“活动功能”。 每个应用程序都有一个活动函数,该函数简单地返回一个布尔值,true或false,以判断该应用程序当前是否处于活动状态。
Inside the single-spa-demo-root-config
directory, in the activity-functions.js
file, we'll write the following activity functions for our three micro-frontend apps.
在single-spa-demo-root-config
目录中的activity-functions.js
文件中,我们将为三个微型前端应用程序编写以下活动函数。
export function prefix(location, ...prefixes) {
return prefixes.some(
prefix => location.href.indexOf(`${location.origin}/${prefix}`) !== -1
);
}
export function nav() {
// The nav is always active
return true;
}
export function page1(location) {
return prefix(location, 'page1');
}
export function page2(location) {
return prefix(location, 'page2');
}
Next, we need to register our three micro-frontend apps with single-spa. To do that, we use the registerApplication
function. This function accepts a minimum of three arguments: the app name, a method to load the app, and an activity function to determine when the app is active.
接下来,我们需要向单个spa注册三个微前端应用程序。 为此,我们使用registerApplication
函数。 此函数至少接受三个参数:应用程序名称,加载应用程序的方法以及确定应用程序何时处于活动状态的活动函数。
Inside the single-spa-demo-root-config
directory, in the root-config.js
file, we'll add the following code to register our apps:
在single-spa-demo-root-config
目录中的root-config.js
文件中,我们将添加以下代码来注册我们的应用程序:
import { registerApplication, start } from "single-spa";
import * as isActive from "./activity-functions";
registerApplication(
"@thawkin3/single-spa-demo-nav",
() => System.import("@thawkin3/single-spa-demo-nav"),
isActive.nav
);
registerApplication(
"@thawkin3/single-spa-demo-page-1",
() => System.import("@thawkin3/single-spa-demo-page-1"),
isActive.page1
);
registerApplication(
"@thawkin3/single-spa-demo-page-2",
() => System.import("@thawkin3/single-spa-demo-page-2"),
isActive.page2
);
start();
Now that we’ve set up the activity functions and registered our apps, the last step before we can get this running locally is to update the local import map inside the index.ejs
file in the same directory.
现在我们已经设置了活动功能并注册了我们的应用程序,要index.ejs
在本地运行,最后一步是在同一目录中的index.ejs
文件内更新本地导入映射。
We'll add the following code inside the head
tag to specify where each app can be found when running locally:
我们将在head
标签内添加以下代码,以指定在本地运行时可以在哪里找到每个应用程序:
<% if (isLocal) { %>
<script type="systemjs-importmap">
{
"imports": {
"@thawkin3/root-config": "http://localhost:9000/root-config.js",
"@thawkin3/single-spa-demo-nav": "http://localhost:9001/thawkin3-single-spa-demo-nav.js",
"@thawkin3/single-spa-demo-page-1": "http://localhost:9002/thawkin3-single-spa-demo-page-1.js",
"@thawkin3/single-spa-demo-page-2": "http://localhost:9003/thawkin3-single-spa-demo-page-2.js"
}
}
</script>
<% } %>
Each app contains its own startup script, which means that each app will be running locally on its own development server during local development. As you can see, our navbar app is on port 9001, our page 1 app is on port 9002, and our page 2 app is on port 9003.
每个应用程序都包含自己的启动脚本,这意味着每个应用程序在本地开发期间都将在其自己的开发服务器上本地运行。 如您所见,我们的导航栏应用程序位于端口9001上,第1页应用程序位于端口9002上,而第2页应用程序位于端口9003上。
With those three steps taken care of, let’s try out our app.
完成这三个步骤后,让我们尝试一下我们的应用程序。
测试运行以在本地运行 (Test Run for Running Locally)
To get our app running locally, we can follow these steps:
要使我们的应用在本地运行,我们可以按照以下步骤操作:
- Open four terminal tabs, one for each app 打开四个终端选项卡,每个应用程序一个
For the root config, in the
single-spa-demo-root-config
directory:yarn start
(runs on port 9000 by default)对于根配置,请在
single-spa-demo-root-config
目录中:yarn start
(默认在端口9000上运行)For the nav app, in the
single-spa-demo-nav
directory:yarn start --port 9001
对于nav应用程序,在
single-spa-demo-nav
目录中:yarn start --port 9001
For the page 1 app, in the
single-spa-demo-page-1
directory:yarn start --port 9002
对于第1页应用,在
single-spa-demo-page-1
目录中:yarn start --port 9002
For the page 2 app, in the
single-spa-demo-page-2
directory:yarn start --port 9003
对于第2页应用,在
single-spa-demo-page-2
目录中:yarn start --port 9003
Now, we’ll navigate in the browser to http://localhost:9000 to view our app.
现在,我们将在浏览器中导航到http:// localhost:9000以查看我们的应用程序。
We should see… some text! Super exciting.
我们应该看到……一些文字! 超级刺激。
On our main page, the navbar is showing because the navbar app is always active.
在我们的主页上,显示导航栏是因为导航栏应用始终处于活动状态。
Now, let’s navigate to http://localhost:9000/page1. As shown in our activity functions above, we’ve specified that the page 1 app should be active (shown) when the URL path begins with “page1.” So, this activates the page 1 app, and we should see the text for both the navbar and the page 1 app now.
现在,让我们导航到http:// localhost:9000 / page1。 如上述活动功能所示,我们已指定URL路径以“ page1”开头时,第1页应用应该处于活动状态(如图所示)。 因此,这将激活页面1应用程序,我们现在应该同时看到导航栏和页面1应用程序的文本。
One more time, let’s now navigate to http://localhost:9000/page2. As expected, this activates the page 2 app, so we should see the text for the navbar and the page 2 app now.
再过一次,现在让我们导航到http:// localhost:9000 / page2。 如预期的那样,这将激活页面2应用程序,因此我们现在应该看到导航栏和页面2应用程序的文本。
对应用程序进行细微调整 (Making Minor Tweaks to the Apps)
So far our app isn’t very exciting to look at, but we do have a working micro-frontend setup running locally. If you aren’t cheering in your seat right now, you should be!
到目前为止,我们的应用程序看起来并不十分令人兴奋,但我们确实在本地运行了一个有效的微前端设置。 如果您现在不为自己的座位加油,那应该是!
Let’s make some minor improvements to our apps so they look and behave a little nicer.
让我们对我们的应用程序进行一些小的改进,使它们的外观和行为更好一些。
指定安装容器 (Specifying the Mount Containers)
First, if you refresh your page over and over when viewing the app, you may notice that sometimes the apps load out of order, with the page app appearing above the navbar app.
首先,如果您在查看应用程序时一遍又一遍地刷新页面,则可能会发现有时应用程序加载失败,页面应用程序显示在导航栏应用程序上方。
This is because we haven’t actually specified where each app should be mounted. The apps are simply loaded by SystemJS, and then whichever app finishes loading fastest gets appended to the page first.
这是因为我们还没有实际规定的每个应用程序应该安装。 只需通过SystemJS加载应用程序,然后以最快的速度完成加载的任何应用程序都会首先添加到页面中。
We can fix this by specifying a mount container for each app when we register them.
我们可以通过在注册每个应用时为它们指定安装容器来解决此问题。
In our index.ejs
file that we worked in previously, let's add some HTML to serve as the main content containers for the page:
在我们之前使用的index.ejs
文件中,让我们添加一些HTML作为页面的主要内容容器:
<div id="nav-container"></div>
<main>
<div id="page-1-container"></div>
<div id="page-2-container"></div>
</main>
Then, in our root-config.js
file where we've registered our apps, let's provide a fourth argument to each function call that includes the DOM element where we'd like to mount each app:
然后,在我们注册了应用程序的root-config.js
文件中,让我们为每个函数调用提供第四个参数,其中包括我们要在其中安装每个应用程序的DOM元素:
import { registerApplication, start } from "single-spa";
import * as isActive from "./activity-functions";
registerApplication(
"@thawkin3/single-spa-demo-nav",
() => System.import("@thawkin3/single-spa-demo-nav"),
isActive.nav,
{ domElement: document.getElementById('nav-container') }
);
registerApplication(
"@thawkin3/single-spa-demo-page-1",
() => System.import("@thawkin3/single-spa-demo-page-1"),
isActive.page1,
{ domElement: document.getElementById('page-1-container') }
);
registerApplication(
"@thawkin3/single-spa-demo-page-2",
() => System.import("@thawkin3/single-spa-demo-page-2"),
isActive.page2,
{ domElement: document.getElementById('page-2-container') }
);
start();
Now, the apps will always be mounted to a specific and predictable location. Nice!
现在,应用程序将始终安装在特定且可预测的位置。 真好!
造型应用 (Styling the App)
Next, let’s style up our app a bit. Plain black text on a white background isn’t very interesting to look at.
接下来,让我们对应用程序进行一些样式设置。 白色背景上的纯黑色文本看起来不是很有趣。
In the single-spa-demo-root-config
directory, in the index.ejs
file again, we can add some basic styles for the whole app by pasting the following CSS at the bottom of the head
tag:
在single-spa-demo-root-config
目录中,再次在index.ejs
文件中,我们可以通过将以下CSS粘贴在head
标签底部来为整个应用程序添加一些基本样式:
<style>
body, html { margin: 0; padding: 0; font-size: 16px; font-family: Arial, Helvetica, sans-serif; height: 100%; }
body { display: flex; flex-direction: column; }
* { box-sizing: border-box; }
</style>
Next, we can style our navbar app by finding the single-spa-demo-nav
directory, creating a root.component.css
file, and adding the following CSS:
接下来,我们可以通过找到single-spa-demo-nav
目录,创建root.component.css
文件并添加以下CSS来设置navbar应用程序的样式:
.nav {
display: flex;
flex-direction: row;
padding: 20px;
background: #000;
color: #fff;
}
.link {
margin-right: 20px;
color: #fff;
text-decoration: none;
}
.link:hover,
.link:focus {
color: #1098f7;
}
We can then update the root.component.js
file in the same directory to import the CSS file and apply those classes and styles to our HTML. We'll also change the navbar content to actually contain two links so we can navigate around the app by clicking the links instead of entering a new URL in the browser's address bar.
然后,我们可以更新同一目录中的root.component.js
文件,以导入CSS文件,并将这些类和样式应用于HTML。 我们还将导航栏内容更改为实际上包含两个链接,因此我们可以通过单击链接而不是在浏览器的地址栏中输入新的URL来浏览应用程序。
import React from "react";
import "./root.component.css";
export default function Root() {
return (
<nav className="nav">
<a href="/page1" className="link">
Page 1
</a>
<a href="/page2" className="link">
Page 2
</a>
</nav>
);
}
We’ll follow a similar process for the page 1 and page 2 apps as well. We’ll create a root.component.css
file for each app in their respective project directories and update the root.component.js
files for both apps too.
对于第1页和第2页应用程序,我们也会遵循类似的过程。 我们将在其各自的项目目录中为每个应用程序创建一个root.component.css
文件,并同时为两个应用程序更新root.component.js
文件。
For the page 1 app, the changes look like this:
对于第1页的应用程序,更改如下所示:
.container1 {
background: #1098f7;
color: white;
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
flex: 1;
font-size: 3rem;
}
import React from "react";
import "./root.component.css";
export default function Root() {
return (
<div className="container1">
<p>Page 1 App</p>
</div>
);
}
And for the page 2 app, the changes look like this:
对于第2页应用,更改如下所示:
.container2 {
background: #9e4770;
color: white;
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
flex: 1;
font-size: 3rem;
}
import React from "react";
import "./root.component.css";
export default function Root() {
return (
<div className="container2">
<p>Page 2 App</p>
</div>
);
}
添加React路由器 (Adding React Router)
The last small change we’ll make is to add React Router to our app. Right now the two links we’ve placed in the navbar are just normal anchor tags, so navigating from page to page causes a page refresh. Our app will feel much smoother if the navigation is handled client-side with React Router.
我们要做的最后一个小更改是将React Router添加到我们的应用程序中。 现在,我们在导航栏中放置的两个链接只是普通的锚标记,因此在页面之间导航会刷新页面。 如果使用React Router在客户端处理导航,我们的应用程序会感觉更加流畅。
To use React Router, we’ll first need to install it. From the terminal, in the single-spa-demo-nav
directory, we'll install React Router using yarn by entering yarn add react-router-dom
. (Or if you're using npm, you can enter npm install react-router-dom
.)
要使用React Router,我们首先需要安装它。 在终端的single-spa-demo-nav
目录中,我们将通过输入yarn add react-router-dom
使用yarn安装React Router。 (或者,如果您使用的是npm,则可以输入npm install react-router-dom
。)
Then, in the single-spa-demo-nav
directory in the root.component.js
file, we'll replace our anchor tags with React Router's Link
components like so:
然后,在root.component.js
文件的single-spa-demo-nav
目录中,我们将锚标记替换为React Router的Link
组件,如下所示:
import React from "react";
import { BrowserRouter, Link } from "react-router-dom";
import "./root.component.css";
export default function Root() {
return (
<BrowserRouter>
<nav className="nav">
<Link to="/page1" className="link">
Page 1
</Link>
<Link to="/page2" className="link">
Page 2
</Link>
</nav>
</BrowserRouter>
);
}
Cool. That looks and works much better!
凉。 这样看起来更好用!
准备生产 (Getting Ready for Production)
At this point we have everything we need to continue working on the app while running it locally. But how do we get it hosted somewhere publicly available?
至此,我们拥有了在本地运行该应用程序时继续使用该应用程序所需的一切。 但是,我们如何将其托管在可公开访问的某个地方?
There are several possible approaches we can take using our tools of choice, but the main tasks are:
我们可以使用选择的工具采取几种可能的方法,但是主要任务是:
- to have somewhere we can upload our build artifacts, like a CDN, and 在某个地方,我们可以上传我们的构建工件,例如CDN,以及
- to automate this process of uploading artifacts each time we merge new code into the master branch. 为了使每次我们将新代码合并到master分支中时都自动执行上载工件的过程。
For this article, we’re going to use AWS S3 to store our assets, and we’re going to use Travis CI to run a build job and an upload job as part of a continuous integration pipeline.
在本文中,我们将使用AWS S3存储资产,并使用Travis CI来运行构建作业和上传作业,作为持续集成管道的一部分。
Let’s get the S3 bucket set up first.
首先让我们设置S3存储桶。
设置AWS S3存储桶 (Setting up the AWS S3 Bucket)
It should go without saying, but you’ll need an AWS account if you’re following along here.
不用说,但是如果您按照这里进行操作,则需要一个AWS账户。
If we are the root user on our AWS account, we can create a new IAM user that has programmatic access only. This means we’ll be given an access key ID and a secret access key from AWS when we create the new user. We’ll want to store these in a safe place since we’ll need them later.
如果我们是AWS账户的root用户,则可以创建一个仅具有编程访问权限的新IAM用户。 这意味着在创建新用户时,我们将获得来自AWS的访问密钥ID和秘密访问密钥。 我们希望将它们存储在安全的地方,因为以后需要它们。
Finally, this user should be given permissions to work with S3 only, so that the level of access is limited if our keys were to fall into the wrong hands.
最后,应授予该用户仅使用S3的权限,这样,如果我们的密钥落入错误的手,则访问级别受到限制。
AWS has some great resources for best practices with access keys and managing access keys for IAM users that would be well worth checking out if you’re unfamiliar with how to do this.
AWS提供了一些很棒的资源,这些资源可用于使用访问密钥和管理IAM用户 访问密钥的最佳实践,如果您不熟悉如何执行此操作 ,那么值得一试。
Next we need to create an S3 bucket. S3 stands for Simple Storage Service and is essentially a place to upload and store files hosted on Amazon’s servers. A bucket is simply a directory.
接下来,我们需要创建一个S3存储桶。 S3代表简单存储服务,本质上是一个用于上传和存储托管在Amazon服务器上的文件的地方。 存储桶只是一个目录。
I’ve named my bucket “single-spa-demo,” but you can name yours whatever you’d like. You can follow the AWS guides for how to create a new bucket for more info.
我已将我的存储桶命名为“ single-spa-demo”,但您可以随便命名。 您可以按照AWS指南了解如何创建新存储桶以获取更多信息。
Once we have our bucket created, it’s also important to make sure the bucket is public and that CORS (cross-origin resource sharing) is enabled for our bucket so that we can access and use our uploaded assets in our app.
创建存储桶后,确保存储桶是公共的,并为存储桶启用CORS(跨域资源共享)也很重要,以便我们可以在应用程序中访问和使用上传的资产。
In the permissions for our bucket, we can add the following CORS configuration rules:
在存储桶的权限中,我们可以添加以下CORS配置规则:
<CORSConfiguration>
<CORSRule>
<AllowedOrigin>*</AllowedOrigin>
<AllowedMethod>GET</AllowedMethod>
</CORSRule>
</CORSConfiguration>
In the AWS console, it ends up looking like this after we hit Save:
在AWS控制台中,单击“保存”后,它最终看起来像这样:
创建Travis CI作业以将工件上传到AWS S3 (Creating a Travis CI Job to Upload Artifacts to AWS S3)
Now that we have somewhere to upload files, let’s set up an automated process that will take care of uploading new JavaScript bundles each time we merge new code into the master branch for any of our repos.
现在我们已经可以上传文件了,让我们建立一个自动化的过程,该过程将在每次将任何新代码合并到我们的任何存储库的master分支中时,都上传新JavaScript捆绑包。
To do this, we’re going to use Travis CI. As mentioned earlier, each app lives in its own repo on GitHub, so we have four GitHub repos to work with. We can integrate Travis CI with each of our repos and set up continuous integration pipelines for each one.
为此,我们将使用Travis CI 。 如前所述,每个应用程序都位于GitHub上的自己的存储库中,因此我们可以使用四个GitHub存储库。 我们可以将Travis CI与我们的每个存储库集成,并为每个存储库建立连续的集成管道。
To configure Travis CI for any given project, we create a .travis.yml
file in the project's root directory. Let's create that file in the single-spa-demo-root-config
directory and insert the following code:
要为任何给定项目配置Travis CI,我们在项目的根目录中创建一个.travis.yml
文件。 让我们在single-spa-demo-root-config
目录中创建该文件,并插入以下代码:
language: node_js
node_js:
- node
script:
- yarn build
- echo "Commit sha - $TRAVIS_COMMIT"
- mkdir -p dist/@thawkin3/root-config/$TRAVIS_COMMIT
- mv dist/*.* dist/@thawkin3/root-config/$TRAVIS_COMMIT/
deploy:
provider: s3
access_key_id: "$AWS_ACCESS_KEY_ID"
secret_access_key: "$AWS_SECRET_ACCESS_KEY"
bucket: "single-spa-demo"
region: "us-west-2"
cache-control: "max-age=31536000"
acl: "public_read"
local_dir: dist
skip_cleanup: true
on:
branch: master
This implementation is what I came up with after reviewing the Travis CI docs for AWS S3 uploads and a single-spa Travis CI example config.
这是我在查看Travis CI文档以获取AWS S3上传和单spa Travis CI示例配置后想到的。
Because we don’t want our AWS secrets exposed in our GitHub repo, we can store those as environment variables. You can place environment variables and their secret values within the Travis CI web console for anything that you want to keep private, so that’s where the .travis.yml
file gets those values from.
因为我们不希望我们的AWS秘密在GitHub存储库中公开,所以我们可以将它们存储为环境变量。 您可以将环境变量及其秘密值放置在Travis CI Web控制台中,以保留任何您希望保留的私有内容,因此.travis.yml
文件是从中获取这些值的地方。
Now, when we commit and push new code to the master branch, the Travis CI job will run, which will build the JavaScript bundle for the app and then upload those assets to S3. To verify, we can check out the AWS console to see our newly uploaded files:
现在,当我们提交新代码并将其推送到master分支时,Travis CI作业将运行,这将为应用程序构建JavaScript捆绑包,然后将这些资产上传到S3。 为了进行验证,我们可以检出AWS控制台以查看我们新上传的文件:
Neat! So far so good. Now we need to implement the same Travis CI configuration for our other three micro-frontend apps, but swapping out the directory names in the .travis.yml
file as needed. After following the same steps and merging our code, we now have four directories created in our S3 bucket, one for each repo.
整齐! 到目前为止,一切都很好。 现在,我们需要为其他三个微型前端应用程序实现相同的Travis CI配置,但是需要根据需要交换.travis.yml
文件中的目录名称。 完成相同的步骤并合并我们的代码之后,我们现在在S3存储桶中创建了四个目录,每个存储库一个。
创建生产导入图 (Creating an Import Map for Production)
Let’s recap what we’ve done so far. We have four apps, all living in separate GitHub repos. Each repo is set up with Travis CI to run a job when code is merged into the master branch, and that job handles uploading the build artifacts into an S3 bucket.
让我们回顾一下到目前为止我们已经完成的工作。 我们有四个应用程序,它们都位于单独的GitHub存储库中。 每个存储库都使用Travis CI设置,以在代码合并到master分支中时运行一个作业,该作业负责将构建工件上载到S3存储桶中。
With all that in one place, there’s still one thing missing: How do these new build artifacts get referenced in our container app? In other words, even though we’re pushing up new JavaScript bundles for our micro-frontends with each new update, the new code isn’t actually used in our container app yet!
将所有这些都放在一个地方,仍然缺少一件事:如何在我们的容器应用程序中引用这些新的构建工件? 换句话说,即使我们在每次新更新时都为微前端增加了新JavaScript捆绑包,新的代码实际上仍未在容器应用中使用!
If we think back to how we got our app running locally, we used an import map. This import map is simply JSON that tells the container app where each JavaScript bundle can be found.
如果回想一下如何使应用程序在本地运行,我们使用了导入地图。 此导入映射只是JSON,它告诉容器应用可以在哪里找到每个JavaScript包。
But, our import map from earlier was specifically used for running the app locally. Now we need to create an import map that will be used in the production environment.
但是,我们之前的导入地图专门用于在本地运行应用程序。 现在,我们需要创建一个将在生产环境中使用的导入地图。
If we look in the single-spa-demo-root-config
directory, in the index.ejs
file, we see this line:
如果我们在single-spa-demo-root-config
目录中的index.ejs
文件中查找, index.ejs
看到以下行:
<script type="systemjs-importmap" src="https://storage.googleapis.com/react.microfrontends.app/importmap.json"></script>
Opening up that URL in the browser reveals an import map that looks like this:
在浏览器中打开该URL将显示如下所示的导入映射:
{
"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",
"single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.5.3/lib/system/single-spa.min.js",
"@react-mf/root-config": "https://react.microfrontends.app/root-config/e129469347bb89b7ff74bcbebb53cc0bb4f5e27f/react-mf-root-config.js",
"@react-mf/navbar": "https://react.microfrontends.app/navbar/631442f229de2401a1e7c7835dc7a56f7db606ea/react-mf-navbar.js",
"@react-mf/styleguide": "https://react.microfrontends.app/styleguide/f965d7d74e99f032c27ba464e55051ae519b05dd/react-mf-styleguide.js",
"@react-mf/people": "https://react.microfrontends.app/people/dd205282fbd60b09bb3a937180291f56e300d9db/react-mf-people.js",
"@react-mf/api": "https://react.microfrontends.app/api/2966a1ca7799753466b7f4834ed6b4f2283123c5/react-mf-api.js",
"@react-mf/planets": "https://react.microfrontends.app/planets/5f7fc62b71baeb7a0724d4d214565faedffd8f61/react-mf-planets.js",
"@react-mf/things": "https://react.microfrontends.app/things/7f209a1ed9ac9690835c57a3a8eb59c17114bb1d/react-mf-things.js",
"rxjs": "https://cdn.jsdelivr.net/npm/@esm-bundle/rxjs@6.5.5/system/rxjs.min.js",
"rxjs/operators": "https://cdn.jsdelivr.net/npm/@esm-bundle/rxjs@6.5.5/system/rxjs-operators.min.js"
}
}
That import map was the default one provided as an example when we used the CLI to generate our container app. What we need to do now is replace this example import map with an import map that actually references the bundles we’re using.
作为示例,当我们使用CLI生成容器应用程序时,该导入映射是默认提供的。 我们现在需要做的是将这个示例导入图替换为一个实际引用我们正在使用的包的导入图。
So, using the original import map as a template, we can create a new file called importmap.json
, place it outside of our repos and add JSON that looks like this:
因此,使用原始的导入地图作为模板,我们可以创建一个名为importmap.json
的新文件,将其放置在我们的存储库之外,并添加如下所示的JSON:
{
"imports": {
"react": "https://cdn.jsdelivr.net/npm/react@16.13.0/umd/react.production.min.js",
"react-dom": "https://cdn.jsdelivr.net/npm/react-dom@16.13.0/umd/react-dom.production.min.js",
"single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.5.1/lib/system/single-spa.min.js",
"@thawkin3/root-config": "https://single-spa-demo.s3-us-west-2.amazonaws.com/%40thawkin3/root-config/179ba4f2ce4d517bf461bee986d1026c34967141/root-config.js",
"@thawkin3/single-spa-demo-nav": "https://single-spa-demo.s3-us-west-2.amazonaws.com/%40thawkin3/single-spa-demo-nav/f0e9d35392ea0da8385f6cd490d6c06577809f16/thawkin3-single-spa-demo-nav.js",
"@thawkin3/single-spa-demo-page-1": "https://single-spa-demo.s3-us-west-2.amazonaws.com/%40thawkin3/single-spa-demo-page-1/4fd417ee3faf575fcc29d17d874e52c15e6f0780/thawkin3-single-spa-demo-page-1.js",
"@thawkin3/single-spa-demo-page-2": "https://single-spa-demo.s3-us-west-2.amazonaws.com/%40thawkin3/single-spa-demo-page-2/8c58a825c1552aab823bcbd5bdd13faf2bd4f9dc/thawkin3-single-spa-demo-page-2.js"
}
}
You’ll note that the first three imports are for shared dependencies: react, react-dom, and single-spa. That way we don’t have four copies of React in our app causing bloat and longer download times. Next, we have imports for each of our four apps. The URL is simply the URL for each uploaded file in S3 (called an “object” in AWS terminology).
您会注意到,前三个导入是针对共享依赖项的:react,react-dom和single-spa。 这样,我们的应用程序中就不会有四个React副本,从而导致膨胀和更长的下载时间。 接下来,我们为四个应用程序都导入了文件。 该URL只是S3中每个上载文件的URL(在AWS术语中称为“对象”)。
Now that we have this file created, we can manually upload it to our bucket in S3 through the AWS console.
现在我们已经创建了该文件,我们可以通过AWS控制台将其手动上传到S3中的存储桶中。
Note: This is a pretty important and interesting caveat when using single-spa: The import map doesn’t actually live anywhere in source control or in any of the git repos. That way, the import map can be updated on the fly without requiring checked-in changes in a repo. We’ll come back to this concept in a little bit.
注意 :使用单spa时,这是一个非常重要且有趣的警告:导入映射实际上不在源代码管理或任何git repos中都存在。 这样,导入地图可以即时更新,而无需在存储库中签入更改。 我们将稍作讨论。
Finally, we can now reference this new file in our index.ejs
file instead of referencing the original import map.
最后,我们现在可以在index.ejs
文件中引用此新文件,而不是引用原始导入映射。
<script type="systemjs-importmap" src="//single-spa-demo.s3-us-west-2.amazonaws.com/%40thawkin3/importmap.json"></script>
创建生产服务器 (Creating a Production Server)
We are getting closer to having something up and running in production! We’re going to host this demo on Heroku, so in order to do that, we’ll need to create a simple Node.js and Express server to serve our file.
我们越来越接近在生产中投入运行! 我们将在Heroku上托管此演示,因此,为了做到这一点,我们需要创建一个简单的Node.js和Express服务器来提供文件。
First, in the single-spa-demo-root-config
directory, we'll install express by running yarn add express
(or npm install express
). Next, we'll add a file called server.js
that contains a small amount of code for starting up an express server and serving our main index.html
file.
首先,在single-spa-demo-root-config
目录中,我们将通过运行yarn add express
(或npm install express
)来npm install express
。 接下来,我们将添加一个名为server.js
的文件,其中包含少量代码,用于启动快速服务器并为我们的主要index.html
文件提供服务。
const express = require("express");
const path = require("path");
const PORT = process.env.PORT || 5000;
express()
.use(express.static(path.join(__dirname, "dist")))
.get("*", (req, res) => {
res.sendFile("index.html", { root: "dist" });
})
.listen(PORT, () => console.log(`Listening on ${PORT}`));
Finally, we’ll update the NPM scripts in our package.json
file to differentiate between running the server in development mode and running the server in production mode.
最后,我们将更新package.json
文件中的NPM脚本,以区分在开发模式下运行服务器和在生产模式下运行服务器。
"scripts": {
"build": "webpack --mode=production",
"lint": "eslint src",
"prettier": "prettier --write './**'",
"start:dev": "webpack-dev-server --mode=development --port 9000 --env.isLocal=true",
"start": "node server.js",
"test": "jest"
}
部署到Heroku (Deploying to Heroku)
Now that we have a production server ready, let’s get this thing deployed to Heroku! In order to do so, you’ll need to have a Heroku account created, the Heroku CLI installed, and be logged in. Deploying to Heroku is as easy as 1–2–3:
现在我们已经准备好生产服务器,让我们将其部署到Heroku! 为此,您需要创建一个Heroku帐户 ,安装Heroku CLI并登录。部署到Heroku的过程很简单,只需1–2–3:
In the
single-spa-demo-root-config
directory:heroku create thawkin3-single-spa-demo
(changing that last argument to a unique name to be used for your Heroku app)在
single-spa-demo-root-config
目录中:heroku create thawkin3-single-spa-demo
(将最后一个参数更改为用于Heroku应用程序的唯一名称)git push heroku master
git push heroku master
heroku open
heroku open
And with that, we are up and running in production. Upon running the heroku open
command, you should see your app open in your browser. Try navigating between pages using the nav links to see the different micro-frontend apps mount and unmount.
这样,我们就可以投入生产了。 运行heroku open
命令后,您应该会在浏览器中看到您的应用程序已打开。 尝试使用导航链接在页面之间导航,以查看不同的微型前端应用程序的安装和卸载。
进行更新 (Making Updates)
At this point, you may be asking yourself, “All that work for this? Why?” And you’d be right. Sort of. This is a lot of work, and we don’t have much to show for it, at least not visually. But, we’ve laid the groundwork for whatever app improvements we’d like.
在这一点上,您可能会问自己:“这一切有用吗? 为什么?” 而且你会是对的。 有点。 这是一项繁重的工作,我们没有太多可以展示的内容,至少在视觉上没有。 但是,我们为我们想要的任何应用程序改进奠定了基础。
The setup cost for any microservice or micro-frontend is often a lot higher than the setup cost for a monolith; it’s not until later that you start to reap the rewards.
任何微服务或微前端的设置成本通常都比整体组件的设置成本高得多。 直到后来,您才开始获得奖励。
So let’s start thinking about future modifications. Let’s say that it’s now five or ten years later, and your app has grown. A lot. And, in that time, a hot new framework has been released, and you’re dying to re-write your entire app using that new framework.
因此,让我们开始考虑将来的修改。 假设现在已经是五到十年了,您的应用程序已经发展壮大。 很多。 而且,在那个时候,一个热门的新框架已经发布,并且您渴望使用该新框架重新编写整个应用程序。
When working with a monolith, this would likely be a years-long effort and may be nearly impossible to accomplish. But, with micro-frontends, you could swap out technologies one piece of the app at a time, allowing you to slowly and smoothly transition to a new tech stack. Magic!
当使用整体结构时,这可能需要花费数年的时间,并且几乎不可能完成。 但是,借助微型前端,您可以一次将技术替换为应用程序的一部分,从而使您可以缓慢而平稳地过渡到新的技术堆栈。 魔法!
Or, you may have one piece of your app that changes frequently and another piece of your app that is rarely touched. While making updates to the volatile app, wouldn’t it be nice if you could just leave the legacy code alone?
或者,您的应用程序中有一部分经常更改,而应用程序中的另一部分很少被更改。 在对易失性应用程序进行更新时,如果您仅将遗留代码放在一边会不会很好?
With a monolith, it’s possible that changes you make in one place of your app may affect other sections of your app. What if you modified some stylesheets that multiple sections of the monolith were using? Or what if you updated a dependency that was used in many different places?
使用整体程序,您在应用程序的一个位置所做的更改可能会影响应用程序的其他部分。 如果您修改了整体中多个部分正在使用的某些样式表,该怎么办? 或者,如果您更新了在许多不同地方使用的依赖项,该怎么办?
With a micro-frontend approach, you can leave those worries behind, refactoring and updating one app where needed while leaving legacy apps alone.
使用微前端方法,您可以将那些烦恼抛在身后,在需要时重构和更新一个应用程序,而无需再使用旧版应用程序。
But, how do you make these kinds of updates? Or updates of any sort, really?
但是,您如何进行此类更新? 还是真的有任何更新?
Right now we have our production import map in our index.ejs
file, but it's just pointing to the file we manually uploaded to our S3 bucket. If we wanted to release some new changes right now, we'd need to push new code for one of the micro-frontends, get a new build artifact, and then manually update the import map with a reference to the new JavaScript bundle.
现在,我们在index.ejs
文件中有我们的生产导入映射,但这只是指向我们手动上传到S3存储桶的文件。 如果我们想立即发布一些新更改,则需要为其中一个微前端推送新代码,获取新的构建工件,然后使用对新JavaScript包的引用手动更新导入映射。
Is there a way we could automate this? Yes!
有没有办法可以使它自动化? 是!
更新其中一个应用 (Updating One of the Apps)
Let’s say we want to update our page 1 app to have different text showing. In order to automate the deployment of this change, we can update our CI pipeline to not only build an artifact and upload it to our S3 bucket, but to also update the import map to reference the new URL for the latest JavaScript bundle.
假设我们要更新页面1应用以显示不同的文本。 为了自动化此更改的部署,我们可以更新CI管道以不仅构建构件并将其上传到S3存储桶,还可以更新导入映射以引用最新JavaScript包的新URL。
Let’s start by updating our .travis.yml
file like so:
让我们从更新我们的.travis.yml
文件开始,如下所示:
language: node_js
node_js:
- node
env:
global:
# include $HOME/.local/bin for `aws`
- PATH=$HOME/.local/bin:$PATH
before_install:
- pyenv global 3.7.1
- pip install -U pip
- pip install awscli
script:
- yarn build
- echo "Commit sha - $TRAVIS_COMMIT"
- mkdir -p dist/@thawkin3/root-config/$TRAVIS_COMMIT
- mv dist/*.* dist/@thawkin3/root-config/$TRAVIS_COMMIT/
deploy:
provider: s3
access_key_id: "$AWS_ACCESS_KEY_ID"
secret_access_key: "$AWS_SECRET_ACCESS_KEY"
bucket: "single-spa-demo"
region: "us-west-2"
cache-control: "max-age=31536000"
acl: "public_read"
local_dir: dist
skip_cleanup: true
on:
branch: master
after_deploy:
- chmod +x after_deploy.sh
- "./after_deploy.sh"
The main changes here are adding a global environment variable, installing the AWS CLI, and adding an after_deploy
script as part of the pipeline. This references an after_deploy.sh
file that we need to create. The contents will be:
此处的主要更改是添加全局环境变量,安装AWS CLI以及在管道中添加after_deploy
脚本。 这引用了我们需要创建的after_deploy.sh
文件。 内容将是:
echo "Downloading import map from S3"
aws s3 cp s3://single-spa-demo/@thawkin3/importmap.json importmap.json
echo "Updating import map to point to new version of @thawkin3/root-config"
node update-importmap.mjs
echo "Uploading new import map to S3"
aws s3 cp importmap.json s3://single-spa-demo/@thawkin3/importmap.json --cache-control 'public, must-revalidate, max-age=0' --acl 'public-read'
echo "Deployment successful"
This file downloads the existing import map from S3, modifies it to reference the new build artifact, and then re-uploads the updated import map to S3. To handle the actual updating of the import map file’s contents, we use a custom script that we’ll add in a file called update-importmap.mjs.
该文件从S3下载现有的导入映射,对其进行修改以引用新的构建工件,然后将更新的导入映射重新上传到S3。 为了处理导入地图文件内容的实际更新,我们使用一个自定义脚本,该脚本将添加到名为update-importmap.mjs.
的文件中update-importmap.mjs.
// Note that this file requires node@13.2.0 or higher (or the --experimental-modules flag)
import fs from "fs";
import path from "path";
import https from "https";
const importMapFilePath = path.resolve(process.cwd(), "importmap.json");
const importMap = JSON.parse(fs.readFileSync(importMapFilePath));
const url = `https://single-spa-demo.s3-us-west-2.amazonaws.com/%40thawkin3/root-config/${process.env.TRAVIS_COMMIT}/root-config.js`;
https
.get(url, res => {
// HTTP redirects (301, 302, etc) not currently supported, but could be added
if (res.statusCode >= 200 && res.statusCode < 300) {
if (
res.headers["content-type"] &&
res.headers["content-type"].toLowerCase().trim() ===
"application/javascript"
) {
const moduleName = `@thawkin3/root-config`;
importMap.imports[moduleName] = url;
fs.writeFileSync(importMapFilePath, JSON.stringify(importMap, null, 2));
console.log(
`Updated import map for module ${moduleName}. New url is ${url}.`
);
} else {
urlNotDownloadable(
url,
Error(`Content-Type response header must be application/javascript`)
);
}
} else {
urlNotDownloadable(
url,
Error(`HTTP response status was ${res.statusCode}`)
);
}
})
.on("error", err => {
urlNotDownloadable(url, err);
});
function urlNotDownloadable(url, err) {
throw Error(
`Refusing to update import map - could not download javascript file at url ${url}. Error was '${err.message}'`
);
}
Note that we need to make these changes for these three files in all of our GitHub repos so that each one is able to update the import map after creating a new build artifact.
请注意,我们需要在所有GitHub存储库中对这三个文件进行这些更改,以使每个文件都能够在创建新的构建构件后更新导入地图。
The file contents will be nearly identical for each repo, but we’ll need to change the app names or URL paths to the appropriate values for each one.
每个存储库的文件内容几乎相同,但是我们需要将应用程序名称或URL路径更改为每个适当的值。
导入地图上的附注 (A Side Note on the Import Map)
Earlier I mentioned that the import map file we manually uploaded to S3 doesn’t actually live anywhere in any of our GitHub repos or in any of our checked-in code. If you’re like me, this probably seems really odd! Shouldn’t everything be in source control?
早先我提到过,我们手动上传到S3的导入地图文件实际上并没有存在于我们的GitHub存储库或任何签入代码中的任何地方。 如果您像我,这似乎真的很奇怪! 难道不是一切都在源代码控制中吗?
The reason it’s not in source control is so that our CI pipeline can handle updating the import map with each new micro-frontend app release.
之所以不在源代码控制中,是因为我们的CI管道可以处理每个新的微型前端应用程序发行版的更新导入映射。
If the import map were in source control, making an update to one micro-frontend app would require changes in two repos: the micro-frontend app repo where the change is made, and the root config repo where the import map would be checked in. This sort of setup would invalidate one of micro-frontend architecture’s main benefits, which is that each app can be deployed completely independent of the other apps.
如果导入映射在源代码管理中,则对一个微前端应用程序进行更新将需要更改两个存储库:进行更改的微前端应用程序存储库和将在其中检入导入映射图的根配置存储库这种设置将使微前端架构的主要优势之一失效,即每个应用程序可以完全独立于其他应用程序进行部署。
In order to achieve some level of source control on the import map, we can always use S3’s versioning feature for our bucket.
为了在导入地图上实现某种程度的源代码控制,我们可以始终对存储桶使用S3的版本控制功能。
关键时刻 (Moment of Truth)
With those modifications to our CI pipelines in place, it’s time for the final moment of truth: Can we update one of our micro-frontend apps, deploy it independently, and then see those changes take effect in production without having to touch any of our other apps?
在对我们的CI管道进行了这些修改之后,就该到最后的关键时刻了:我们可以更新我们的一个微前端应用程序,独立部署它,然后看看这些更改在生产中生效而无需动用任何我们的其他应用?
In the single-spa-demo-page-1
directory, in the root.component.js
file, let's change the text from "Page 1 App" to "Page 1 App - UPDATED!" Next, let's commit that change and push and merge it to master.
在root.component.js
文件的single-spa-demo-page-1
目录中,让我们将文本从“ Page 1 App”更改为“ Page 1 App-UPDATED!”。 接下来,让我们提交更改并将其推送并合并到master。
This will kick off the Travis CI pipeline to build the new page 1 app artifact and then update the import map to reference that new file URL.
这将启动Travis CI管道以构建新的第1页应用程序工件,然后更新导入映射以引用该新文件URL。
If we then navigate in our browser to https://thawkin3-single-spa-demo.herokuapp.com/page1, we’ll now see… drum roll please… our updated app!
然后,如果我们在浏览器中导航到https://thawkin3-single-spa-demo.herokuapp.com/page1 ,我们现在将看到...鼓声滚滚...我们更新的应用程序!
结论 (Conclusion)
I said it before, and I’ll say it again: Micro-frontends are the future of frontend web development.
我之前说过,我会再说一遍: 微前端是前端Web开发的未来。
The benefits are massive, including independent deployments, independent areas of ownership, faster build and test times, and the ability to mix and match various frameworks if needed.
好处是巨大的,包括独立部署,独立所有权区域,更快的构建和测试时间,以及在需要时可以混合和匹配各种框架的能力。
There are some drawbacks, such as the initial set up cost and the complexity of maintaining a distributed architecture, but I strongly believe the benefits outweigh the costs.
有一些缺点,例如初始设置成本和维护分布式体系结构的复杂性,但是我坚信,收益大于成本。
Single-spa makes micro-frontend architecture easy. Now you, too, can go break up the monolith!
单一SPA可简化微前端架构。 现在,您也可以分解大块!
翻译自: https://www.freecodecamp.org/news/developing-and-deploying-micro-frontends-with-single-spa/
single-spa