最近接触了一些DevOps、微服务等概念,想要找一篇文章大体了解一下,看到个微前端标题比较新颖因而选择翻译这篇文章。原文https://martinfowler.com/articles/micro-frontends.htmljavascript
近年来,微服务已经爆红,许多组织使用这种架构来避免大型、单一后端的限制。虽然已经有不少关于这种构建服务器端软件的风格的文章,可是许多公司仍然在与统一的前端代码库做斗争。html
也许您想要构建一个渐进的或响应性的web应用程序,可是找不到一个容易的地方将这些特性集成到现有代码中。也许您想开始使用新的JavaScript语言特性(或者能够编译成JavaScript的众多语言之一),可是没法将必要的构建工具放入现有的构建过程当中。或者,您可能只是想扩展您的开发,以便多个团队能够同时处理单个产品,可是现有总体中的耦合和复杂性意味着每一个人都在踩对方的脚。这些都是真实存在的问题,它们都会对你有效地为客户提供高质量体验的能力产生负面影响。 最近,咱们看到愈来愈多的人关注复杂的现代web开发所必需的整体架构和组织架构。特别是,咱们看到了将前端总体分解成更小、更简单的块模式,这些块能够独立开发、测试和部署,同时仍然做为一个单一的内聚产品出如今客户面前。咱们称这种技术为微前端。前端
An architectural style where independently deliverable frontend applications are composed into a greater wholejava
在2016年11月出版的ThoughtWorks technology radar上,咱们将微前沿技术列为组织应该评估的一项技术。咱们后来将其推广到试用版,并最终推广到Adopt版,这意味着咱们将其视为一种验证过的方法,您应该在有意义时使用它。nginx
想象一个客户点餐网站,表面上这是一个web
每一个页面都有足够的复杂性,咱们能够很容易地为每一个页面指定一个专门的团队,而且每一个团队都应该可以独立于全部其余团队在本身的页面上工做。他们应该可以开发、测试、部署和维护他们的代码,而不用担忧与其余团队的冲突或协调。然而,咱们的客户仍然应该看到一个单一的、无缝的网站。json
鉴于上述至关松散的定义,有许多方法能够合理地称为微前端。在本节中,咱们将展现一些例子并讨论它们之间的权衡。有一个至关天然的架构出如今全部的方法:一般有一个微前端的每一个页面应用程序,有一个单一的容器应用程序,其中:后端
咱们从一个明显不新颖的前端开发方法开始,在服务器上使用多个模版或片断呈现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使用独立的子页面构建页面变得很容易。它们还在样式和全局变量互不干扰方面提供了良好的隔离程度。
<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有点“使人讨厌”的直觉驱动的,但仍是有一些很好的理由让人们避免使用它们。上面提到的简单隔离确实使它们比其余选项更不灵活。在应用程序的不一样部分之间构建集成可能比较困难,所以它们会使用路由、历史记录和深度连接变得更加复杂,而且在使页面彻底响应方面还会带来一些额外的挑战。
接下来咱们将要描述一个最灵活而且使用最多的方法。每一个微前端都使用标记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包,或者在呈现微前端时传递数据。
这种方法的灵活性,加上独立的可部署性,使其成为咱们的默认选择,也是咱们在野外最多见的选择。当咱们看到完整的示例时,咱们将更详细地研究它。
前一种方法的一个变体是,每一个微前端都定义一个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组件规范,而且喜欢使用浏览器提供的功能,那么这是一个不错的选择。若是您喜欢在容器应用程序和微前端之间定义本身的接口,那么您可能更喜欢前面的示例。