最近接触了一些DevOps、微服务等概念,想要找一篇文章大致了解一下,看到个微前端标题比较新颖于是选择翻译这篇文章。原文https://martinfowler.com/articles/micro-frontends.html
近年来,微服务已经爆红,许多组织使用这种架构来避免大型、单一后端的限制。虽然已经有很多关于这种构建服务器端软件的风格的文章,但是许多公司仍然在与统一的前端代码库作斗争。
也许您想要构建一个渐进的或响应性的web应用程序,但是找不到一个容易的地方将这些特性集成到现有代码中。也许您想开始使用新的JavaScript语言特性(或者可以编译成JavaScript的众多语言之一),但是无法将必要的构建工具放入现有的构建过程中。或者,您可能只是想扩展您的开发,以便多个团队可以同时处理单个产品,但是现有整体中的耦合和复杂性意味着每个人都在踩对方的脚。这些都是真实存在的问题,它们都会对你有效地为客户提供高质量体验的能力产生负面影响。 最近,我们看到越来越多的人关注复杂的现代web开发所必需的总体架构和组织架构。特别是,我们看到了将前端整体分解成更小、更简单的块模式,这些块可以独立开发、测试和部署,同时仍然作为一个单一的内聚产品出现在客户面前。我们称这种技术为微前端。
An architectural style where independently deliverable frontend applications are composed into a greater whole
在2016年11月出版的ThoughtWorks technology radar上,我们将微前沿技术列为组织应该评估的一项技术。我们后来将其推广到试用版,并最终推广到Adopt版,这意味着我们将其视为一种验证过的方法,您应该在有意义时使用它。
例子
想象一个客户点餐网站,表面上这是一个
- 需要一个登陆界面,用户可以浏览和搜索餐厅。并且可以筛选和查询对应餐厅的价格、菜品以及哪些是之前有人点过的。
- 每一个餐厅需要有自己的页面,能展示菜单,允许用户选择他们想要的菜以及价格、数量和特殊需求。
- 客户个人页面展示个人历史点菜记录、订单详情和支付选项
每个页面都有足够的复杂性,我们可以很容易地为每个页面指定一个专门的团队,并且每个团队都应该能够独立于所有其他团队在自己的页面上工作。他们应该能够开发、测试、部署和维护他们的代码,而不用担心与其他团队的冲突或协调。然而,我们的客户仍然应该看到一个单一的、无缝的网站。
集成方法
鉴于上述相当松散的定义,有许多方法可以合理地称为微前端。在本节中,我们将展示一些例子并讨论它们之间的权衡。有一个相当自然的架构出现在所有的方法:通常有一个微前端的每个页面应用程序,有一个单一的容器应用程序,其中:
- 渲染通用的页面元素例如header和footer
- 解决诸如身份验证和导航之类的横切关注点
- 将各种微前端整合到页面上,并告诉每个微前端何时何地呈现自己
服务器端模版组合
我们从一个明显不新颖的前端开发方法开始,在服务器上使用多个模版或片段呈现HTML。我们有一个index.html,它爆红任何常见的页面元素,然后使用服务器端包含从HTML文件中插入特定到页面内容:
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Feed me</title>
</head>
<body>
<h1>🍽 Feed me</h1>
<!--# include file="$PAGE.html" -->
</body>
</html>
我们使用Nginx提供这个文件,配置$PAGE变量通过匹配被请求的URL:
server {
listen 8080;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
ssi on;
# Redirect / to /browse
rewrite ^/$ http://localhost:8080/browse redirect;
# Decide which HTML fragment to insert based on the URL
location /browse {
set $PAGE 'browse';
}
location /order {
set $PAGE 'order';
}
location /profile {
set $PAGE 'profile'
}
# All locations should render through index.html
error_page 404 /index.html;
}
这是相当标准的服务器端组合。我们可以合理地将此微前端称为微前端的原因是,我们将代码分割成这样的一种方式,即每个部分都表示一个独立团队可以支付的自包含领域概念。这里没有显示的是这些不同的HTML文件是如何在web服务器上结束的,但是假设它们都有自己的部署管道,这允许我们将更改部署到一个页面,而不影响或考虑任何其他页面。 为了获得更大的独立性,可以有一个单独的服务器负责呈现和服务于每个微前端,其中一个服务器位于前端,向其他服务器发出请求。通过仔细缓存响应,这个可以在不影响延迟的情况下完成。
这个例子说明了微前端不一定是一种新技术,也不一定是复杂的。只要我们注意我们的设计决策如何影响我们的代码库。构建时集成
我们有时看到的一种方法是将每个微前端发布为一个包,并让容器应用程序将它们都包含为库依赖项。这是我们应用中容器的package.json可能的样子:
{
"name": '@feed-me/container',
"version": '1.0.0',
"description": 'A food delivery web app',
"dependencies": {
"@feed-me/browse-restaurants": "^1.2.3",
"@feed-me/order-food": "^4.5.6",
"@feed-me/user-profile": "^7.8.9"
}
}
起初,这似乎是有道理的。它像往常一样生成一个可部署的JavaScript包,允许我们从各种应用程序中删除重复的公共依赖项。然而,这种方法意味着我们必需重新编译并发布每个单独的微前端,以便发布对产品任何单独部分的更改。正如微服务一样,我们已经看到了这种同步发布过程所带来的痛苦,因此我们强烈建议不要使用这种方法来处理微前端。 在经历了将应用程序划分为可以独立开发和测试的离散代码库的所有麻烦之后,让我们不要在发布阶段重新引入所有耦合。我们应该找到一种在运行时集成微前端的方法,而不是在构建时。
运行时通过iframe集成
在浏览器中组合应用程序最简单的方法之一是用iframe。从本质上讲,iframe使用独立的子页面构建页面变得很容易。它们还在样式和全局变量互不干扰方面提供了良好的隔离程度。
<html>
<head>
<title>Feed me!</title>
</head>
<body>
<h1>Welcome to Feed me!</h1>
<iframe id="micro-frontend-container"></iframe>
<script type="text/javascript">
const microFrontendsByRoute = {
'/': "https://browse.example.come/index.html",
'/order-food': 'https://order.example.com.index.html',
'/user-profile': 'https://profile.example.com/index.html',
}
const iframe = document.getElementById('micro-frontend-container');
iframe.src = microFrontendsByRoute[window.location.pathname];
</script>
</body>
</html>
就像服务端集成选项一样,使用iframes构建页面并不是新技术,而且看起来也不那么令人兴奋。但是,如果我们回顾一下前面列出的微前端主要优点,iframe基本上符合要求,只要我们仔细考虑如何分割应用程序和构建团队。 我们经常看到很多人不愿意选择iframe。虽然有些不情愿似乎是由一种iframe有点“令人讨厌”的直觉驱动的,但还是有一些很好的理由让人们避免使用它们。上面提到的简单隔离确实使它们比其他选项更不灵活。在应用程序的不同部分之间构建集成可能比较困难,因此它们会使用路由、历史记录和深度链接变得更加复杂,并且在使页面完全响应方面还会带来一些额外的挑战。
通过Javascript运行时集成
接下来我们将要描述一个最灵活并且使用最多的方法。每个微前端都使用标记script包含在页面中,并且在加载时公开一个全局函数作为其入口点。然后容器应用程序确定应该挂载哪个微前端,并调用相关函数来告诉微前端何时何地呈现自己。
<html>
<head>
<title>Feed me!</title>
</head>
<body>
<h1>Welcome to Feed me!</h1>
<!-- These scripts don't render anything immediately -->
<!-- Instead they attach entry-point functions to `window` -->
<script src="https://browse.example.com/bundle.js"></script>
<script src="https://order.example.com/bundle.js"></script>
<script src="https://profile.example.com/bundle.js"></script>
<div id="micro-frontend-root"></div>
<script type="text/javascript">
// These global functions are attached to window by the above scripts
const microFrontendsByRoute = {
'/': window.renderBrowseRestaurants,
'/order-food': window.renderOrderFood,
'/user-profile': window.renderUserProfile,
};
const renderFunction = microFrontendsByRoute[window.location.pathname];
// Having determined the entry-point function, we now call it,
// giving it the ID of the element where it should render itself
renderFunction('micro-frontend-root');
</script>
</body>
</html>
上面的例子显然是一个原始的例子,但是它演示了基本的技术。与构建时集成不同,我们可以独立部署每个bundle.js文件。与iframe不同的是,我们拥有充分的灵活性,可以在我们喜欢的任何微前端之间构建集成。我们可以通过多种方式扩展上面的代码,例如只根据需要下载每个JavaScript包,或者在呈现微前端时传递数据。
这种方法的灵活性,加上独立的可部署性,使其成为我们的默认选择,也是我们在野外最常见的选择。当我们看到完整的示例时,我们将更详细地研究它。
通过WebComponents运行时集成
前一种方法的一个变体是,每个微前端都定义一个HTML自定义元素来实例化容器,而不是定义一个全局函数来调用容器。
<html>
<head>
<title>Feed me!</title>
</head>
<body>
<h1>Welcome to Feed me!</h1>
<!-- These scripts don't render anything immediately -->
<!-- Instead they each define a custom element type -->
<script src="https://browse.example.com/bundle.js"></script>
<script src="https://order.example.com/bundle.js"></script>
<script src="https://profile.example.com/bundle.js"></script>
<div id="micro-frontend-root"></div>
<script type="text/javascript">
// These element types are defined by the above scripts
const webComponentsByRoute = {
'/': 'micro-frontend-browse-restaurants',
'/order-food': 'micro-frontend-order-food',
'/user-profile': 'micro-frontend-user-profile',
};
const webComponentType = webComponentsByRoute[window.location.pathname];
// Having determined the right web component custom element type,
// we now create an instance of it and attach it to the document
const root = document.getElementById('micro-frontend-root');
const webComponent = document.createElement(webComponentType);
root.appendChild(webComponent);
</script>
</body>
</html>
这里的最终结果与前面的示例非常相似,主要的区别在于您选择以“web组件方式”进行操作。如果您喜欢web组件规范,并且喜欢使用浏览器提供的功能,那么这是一个不错的选择。如果您喜欢在容器应用程序和微前端之间定义自己的接口,那么您可能更喜欢前面的示例。