- 原文地址:Micro Frontends
- 原文做者:Cam Jackson
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:DEARPORK
- 校对者:lgh757079506, solerji
作好前端开发不是件容易的事情,而比这更难的是扩展前端开发规模以便于多个团队能够同时开发一个大型且复杂的产品。本系列文章将描述一种趋势,能够将大型的前端项目分解成许多个小而易于管理的部分,也将讨论这种体系结构如何提升前端代码团队工做的有效性和效率。除了讨论各类好处和代价以外,咱们还将介绍一些可用的实现方案和深刻探讨一个应用该技术的完整示例应用程序。javascript
建议按照顺序阅读本系列文章:html
咱们从咱们一直引用的全局渲染函数继续。咱们应用的主页是个可筛选的餐馆列表,入口代码以下所示:前端
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
window.renderBrowse = (containerId, history) => {
ReactDOM.render(<App history={history} />, document.getElementById(containerId));
registerServiceWorker();
};
window.unmountBrowse = containerId => {
ReactDOM.unmountComponentAtNode(document.getElementById(containerId));
};
复制代码
一般在 React.js 应用中,ReactDOM.render
的调用会在最顶层做用域,这意味着一旦脚本文件加载完成,它就会当即在一个硬编码的 DOM 元素中开始渲染。对于这个应用,咱们须要可以同时控制渲染发生的时间和位置,因此咱们将它包裹在一个接收 DOM 元素 ID 做为参数的函数里,并把这个函数添加到全局的 window
对象。相应的,咱们还能够看到用于清理的 un-mounting 函数。java
虽然咱们已经看到当微前端集成到整个容器应用时这个函数将被如何调用,但这里成功的最大标准之一是咱们是否可以独立地开发和运行微前端。因此每一个微前端也有本身的 index.html
,它带有内联的脚本,以便于在容器外以“独立”模式渲染应用。react
<html lang="en">
<head>
<title>Restaurant order</title>
</head>
<body>
<main id="container"></main>
<script type="text/javascript">
window.onload = () => {
window.renderRestaurant('container');
};
</script>
</body>
</html>
复制代码
图 9:每一个微前端均可以在容器外独立运行。android
在此以前,微前端大部分只是普通的旧 React 应用。browse 应用从后端拉取餐厅列表,提供 input
标签用于搜索和筛选,并渲染 React Router <Link>
元素导航到特定餐馆。这时咱们要切换到 order
微前端,它负责渲染单独的餐厅及其菜单。webpack
图 10:这些微前端仅经过路由变换相互做用,并不直接交互ios
有关咱们的微前端,最后一件值得一提的事是他们都使用了 styled-component
来完成它们全部的样式。这个 CSS-in-JS 库能够很容易地将样式和特定组件关联,这样咱们能够保证微前端的样式不会泄露以影响容器或其余微前端的样式。git
咱们较早前提到跨应用通讯应该尽可能避免。在这个例子中,咱们惟一(须要跨应用通讯)的需求是浏览页须要告诉详情页加载哪一个餐馆。这里咱们将看到怎样用客户端路由去解决这个问题。github
这里涉及的三个 React 应用都使用了 React Router 进行声明式路由,可是经过两种略有差异的方式进行初始化。容器应用中,咱们建立了一个 <BrowserRouter>
,它在内部会实例化一个 history
对象。这是咱们以前一直在“掩饰”的 history
对象。咱们用这个对象操做客户端历史记录,同时咱们也用它将不一样的 React Router 链接起来。在咱们的微前端里,咱们像这样初始化 Router:
<Router history={this.props.history}>
复制代码
在这种状况下,咱们使用的是容器应用的 history 实例,而并不会让路由组件本身实例化一个新的 history 对象。全部的 <Router>
实例如今都已经链接在一块儿,所以任何实例触发的路由变化都会反映在全部实例中。这为咱们提供了一种经过 URL 将“参数”从一个微前端传递到另外一个微前端的简单方法。例如在 browse 微前端,咱们有一个这样的连接:
<Link to={`/restaurant/${restaurant.id}`}>
复制代码
当这个连接被点击时,容器中的路由会被更新,该容器会看到新的 URL 而后决定挂载以及渲染 restaurant 微前端。该微前端本身的路由逻辑将从 URL 提取 restaurant ID 并渲染正确的信息。
但愿这个示例流程展现了简洁 URL 的灵活性及强大功能。除了便于分享与作书签之外,在这个特定的架构里,它还能够是微前端互相交流意图的有用方式。基于这个目的使用 URL 有如下许多优点:
当咱们使用路由做为微前端通讯的模式时,咱们选择的路由构成了一个合约。在这种状况下,咱们规定能够在 /restaurant/:restaurantId
查看一个餐厅的详细信息,而且咱们没法在不更新全部引用该路由的应用的状况下更新路由。鉴于此合约的重要性,咱们应该进行自动化测试以检查合约是否被遵照。
虽然咱们但愿咱们的团队以及咱们的微前端尽量独立,但有些事情仍应该是共有的。咱们以前写过关于公用组件库如何帮助实现微前端的一致性的文章,但对于这些小 demo 来讲公用组件库有点杀鸡用牛刀。因此做为替代,咱们有一个小的公用内容仓库,包含图像,JSON 数据和 CSS,它们经过网络提供给全部微前端。
还有同样东西咱们能够选择在微前端之间共享:依赖库。正如咱们将简要描述,依赖的重复是微前端的常见缺点。即使在应用之间共享依赖也有自身的一些困难,但对于这个 demo 来讲,谈论一下如何完成它是值得的。
第一步是选择要共享的依赖。编译后代码的快速分析显示咱们 50% 的 bundle 体积是由 react
和 react-dom
贡献的。抛开他们的体积不谈,这两个库是咱们最“核心”的依赖,所以咱们知道全部微前端均可以从提取它们成为公共依赖中受益。最后,这些都是稳定、成熟的库,一般只在两个主版本间引入重大更改,所以跨应用更新不会太困难。
至于实际提取操做,咱们要作的就是在 webpack config 中将库标记为 externals,咱们能够抄抄前面。
module.exports = (config, env) => {
config.externals = {
react: 'React',
'react-dom': 'ReactDOM'
}
return config;
};
复制代码
而后咱们在每一个 index.html
中加几个 script
标签,从咱们的共享内容服务器中获取这两个库。
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<script src="%REACT_APP_CONTENT_HOST%/react.prod-16.8.6.min.js"></script>
<script src="%REACT_APP_CONTENT_HOST%/react-dom.prod-16.8.6.min.js"></script>
</body>
复制代码
在团队中共享代码一直是个很难作好的事情。咱们要确保咱们只共享咱们真正想公用且一次须要修改多处地方的东西。若是咱们对咱们共享与不共享的事情保持谨慎,咱们将从中真正受益。
该应用托管在 AWS,拥有核心基础设施(S3 存储桶,CloudFront 分配,域名,证书等),使用 Terraform 的集中仓库一次性配置。每一个微前端都有本身的源码仓库,在 Travis CI 上有本身的连续部署管道,用于构建、测试以及部署静态资源到 S3 存储桶中。这平衡了集中式基础架构管理的便利性与独立部署的灵活性。
请注意,每一个微前端(以及容器)都有本身的存储桶。这意味着它们能够自由控制里面的内容,咱们不须要担忧来自其余团队或应用的对象名称冲突或冲突的访问规则。
在本文开头,咱们提到过微前端技术的取舍,就像任何架构同样。咱们提到过的好处确实有成本,咱们将在此介绍。
独立构建的 Javascript bundle 会形成公共依赖的重复,从而增长了咱们必须经过网络发送给最终用户的字节数。例如,若是每一个微前端都包含了本身的 React 副本,那么咱们将迫使用户下载 React n 次。页面性能与用户参与/转换有直接关系,而且世界上大部分地区运行在比发达城市慢得多的网络设施上,因此咱们有不少理由关心下载体积。
这个问题不容易解决。一方面咱们但愿团队独立编译他们的应用以便自主工做,另外一方面又咱们但愿他们能够共享公共依赖,这二者之间存在内在的紧张关系。一种解决办法是像咱们前面 demo 描述的,将咱们编译后代码的常见依赖外置。一旦咱们沿着这条路走下去,咱们将从新引入一些微前端之间构建过程的耦合。如今它们之间有着一个隐含的合约:“咱们都必须使用这些依赖的明确版本”。若是其中一个依赖产生重大改动,咱们可能最终须要一个大的协调升级工做以及一次性的同步发版。这是咱们使用微前端最初想要避免的一切!
内在的紧张关系是个困难的问题,但并不全是坏消息。首先,即使咱们对于重复的依赖不采起任何措施,每一个单独页面仍可能比咱们构建整个前端更快地加载。缘由是经过独立编译每一个页面,咱们有效地以咱们本身的形式实现了代码分割。在传统的前端中,应用中的任何页面加载完成时,咱们一般会一次性下载全部页面的源码和依赖。经过独立构建,任何单独的页面加载将只会下载那个页面的源码和依赖。这可能致使更快的首页加载,但随后的导航速度会变慢,由于用户必须在每一个页面上从新下载相同的依赖。若是咱们严格地不用没必要要的依赖使咱们的微前端膨胀,或者咱们知道用户在应用中一般访问的一两个页面,即使有重复依赖,咱们也极可能在性能方面达到净增益。
在前一段有不少“可能”和“也许”,代表了每一个应用一般都有它们本身独特的性能特征。若是你想确切地知道特定的变化会形成什么性能影响,只能靠实际测量,并且最好是在生产环境中。咱们见过不少团队仅仅为了下载数兆大小的高清图像或者对一个运行很是慢的数据库进行昂贵的查询额外多写几千字节的 JavaScript 代码。所以,尽管考虑每一个架构决策的性能影响很重要,但请确保你知道真正的瓶颈在哪里。
咱们应该可以开发一个单一的微前端,而无需考虑其余团队正在开发的全部其它微前端。咱们可能甚至应该在“独立”模式下,在空白页面上运行咱们的微前端,而不是运行在将在生产环境中承载微前端的容器应用内部。这可使开发变得更加简单,特别是当真正的容器是一个复杂的遗留代码库的时候,而一般状况下咱们使用微前端来逐步从旧世界迁移到新世界。可是,在与生产环境彻底不一样的环境中开发存在风险。若是咱们的开发时容器与生产容器的行为不一样,那么咱们可能会发现咱们的微前端被破坏,或者在咱们部署到生产环境时表现不一样。特别值得关注的是可能由容器或其余微前端带来的全局样式。
这里的解决方案与咱们不得不担忧环境差别的任何其余状况没有什么不一样。若是咱们在一个与生产环境不一样的本地环境开发,咱们须要确保按期将咱们的微前端集成和部署到像生产环境的环境中,而且咱们应该在这些环境中进行测试(手动以及自动化)以尽早发现集成问题。这不会彻底解决问题,但最终这是一个取舍:简化开发环境的生产力提高是否值得冒集成出问题的风险?答案取决于项目!
最后的缺点是与微服务直接平行的缺点。做为一个更加分散的架构,微前端将不可避免地致使须要管理更多的东西 —— 更多的存储库,更多的工具,更多的构建/部署管道,更多的服务器,更多的域等等。所以,在采用这样的架构以前,你应该考虑几个问题:
咱们可能会另写一篇文章讨论这些主题。咱们但愿提出的主要观点是,当你选择微前端时,根据定义,你选择建立许多小东西而不是一个总体。你应该考虑你是否有采用这种方法所需的技术和组织成熟度,从而不形成混乱。
随着前端代码库在过去几年中变得愈来愈复杂,咱们看到对更具可扩展性的架构的需求不断增加。咱们须要可以划清界限,在技术和域实体之间创建正确的耦合和内聚层级。咱们应该可以跨独立开发团队可扩展地进行软件交付。
虽然远非惟一方案,但咱们已经看到许多微前端提供了这些好处的真实案例,而且咱们已经可以逐渐将该技术应用于遗留代码库以及新代码库。不管微前端是否适合你和你的组织,咱们只能但愿这将成为持续趋势的一部分,在这个趋势中,前端工程化和前端架构以咱们应有的严肃性被对待。
若是您发现此文章有用,请分享它。我很感激反馈与鼓励。
很是感谢 Charles Korn,Andy Marks,和 Willem Van Ketwich 的全面审校和反馈。
同时感谢 Bill Codding,Michael Strasser,和 Shirish Padalkar 在 ThoughtWorks 内部邮件列表提供的意见。
感谢 Martin Fowler 的反馈意见,并发布在本身的网站上给了文章一个家。
最后,感谢 Evan Bottcher 和 Liauw Fendy 的鼓励和支持。
建议按照顺序阅读本系列文章:
若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。