Logo

Micro Frontends: 微前端 - 将微服务理念扩展到前端开发

Add by Jimmy Lv | Dec 21, 2017 16:12  744 |  26
Micro Frontends:  微前端 - 将微服务理念扩展到前端开发
Download

Map Outline

Micro Frontends: 微前端 - 将微服务理念扩展到前端开发
1 微前端的概念缘由
1.1 什么是微服务?
1.1.1 The Majestic Monolith
1.1.1.1 当今的软件开发环境中,大多数应用程序都是单一的,这种方法的缺点之一是企业所有者一年内需要做的决策数量非常有限(因为依赖关系,响应时间较慢)。
1.1.1.2 升级产品,在一系列相关服务中增加尺寸较大等新功能,需要所有相关方共同努力,以同步方式进行变更。
1.1.2 微服务架构的好处:
1.1.2.1 微服务是孤立的,独立的 “模块”,它们共同为更高的逻辑目的服务。他们通过商定的协议彼此沟通,每个服务都负责特定的功能。这使得每个服务都能够保持简单,简洁和可测试性。
1.1.2.2 微服务允许您更自发地采取更深远的业务决策,因为每个微服务都是独立运作的,而且一个正在管理的团队可以很好地控制变更。
1.1.2.3 微服务架构允许每个团队决定最适合他们的技术和基础架构
1.1.2.4 引入微服务体系结构的重大好处,它允许团队扩展独立部署和维护服务的交付。
1.1.2.5 假设服务边界已经被正确地定义为与可以独立运行的业务子域相对应,并确保在微服务设计中遵循许多最佳实践
1.1.2.5.1 复杂性:服务可以更好地执行问题分离。
1.1.2.5.2 可扩展性:服务可以独立扩展或缩减。
1.1.2.5.3 弹性:服务可以独立失败。
1.1.2.5.4 敏捷性:服务可以独立开发,测试和部署。
1.2 那么前端的现状呢? —— 臃肿的前端
1.2.1 构建一个功能丰富,功能强大的浏览器应用程序,也就是单页面应用程序
1.2.1.1 随着时间的推移,往往由一个单独的团队开发的前端层越来越难以维护。
1.2.1.2 如果做得对,它提供了优秀的用户体验。主要缺点是它不能很好地扩展。在一个大公司里,有许多开发团队,单前端团队可能成为一个发展瓶颈。
1.2.2 大型 SPA 却位于微服务架构之上。
1.2.2.1 one App with one BFF. Why split with Microservices together?
1.2.2.2 不幸的是,我们也看到许多团队在其后端服务之上创建了前端庞然大物 - 一个庞大而庞大的浏览器应用程序。
1.2.2.3 前端变得越来越大,后端变得越来越不重要。
1.2.2.3.1 90%的前端代码,具有非常薄的后端。
1.2.3 举实例:OSP 项目中 components library 的弊端 (NPM package)
1.2.3.1 What if you want to publish a change to the header, an already have fifty pages using that? You would have to ask every page to upgrade it's version of header, meanwhile, your users would get inconsistent headers across the website
1.2.3.2 You are now compiling another app as part of yours, what if it throws something unexpected, will your app break too?
1.2.3.3 You are forced to have the same technologies on both sides, what if header the header uses clojurescript and your page uses elm? Poor webpack, it now has to understand it all when compiling.
2 微前端的定义与解决方案
2.1 定义
2.1.1 微服务思想在前端的运用:微前端 - 将微服务理念扩展到前端开发
2.1.1.1 ThoughtWorks 的同志们最擅长举一反三,包装概念啦!
2.1.1.2 微前端(Micro Frontends)这个术语是微服务的衍生物。它代表了多个自包含的和松散耦合的 UI 组件(服务)的构建方法,其中每个组件负责特定的 UI 元素和 / 或功能。
2.1.1.3 如果我们看到微服务提供了后端的好处,如果我们能够将这些好处应用到前端,并不是向前迈出的一步,而且设计微服务不仅要完成后端逻辑,而且还要完成视觉部分
2.1.1.4 使各个前端团队按照自己的步调迭代,然后在准备就绪时释放; 风险隔离; 而且更容易尝试新技术。
2.1.1.5 对于微服务来说,微前端的许多要求是相同的:监控,健康检查,日志记录,仪器仪表,度量标准等等。
2.1.2 微观前端的想法是基于代表领域特定功能的屏幕将应用程序分解为更小的单元,而不是编写大型单片前端应用程序。前端是独立的,可以独立部署。
2.1.2.1 将网站或网络应用程序视为由独立团队拥有的功能组合。每个团队都有一个独特的业务或任务关注和专业的任务。一个团队是跨职能的,从数据库到用户界面端到端地发展其功能。
2.1.2.2 您可以拥有后端,前端,数据访问层和数据库,即一个服务中的子域所需的所有内容。每一项服务都应该由一个独立的团队来完成。
2.1.2.3 所有前端功能(身份验证,库存,购物车等)都是单个应用程序的一部分,并与后端(大部分时间通过 HTTP)进行通信,并将其分解为微服务。
2.1.3 Web 应用程序被分解成它的特征,并且每个特征都由不同的团队拥有,前端到后端。这确保了每个功能都是独立于其他功能开发,测试和部署的。
2.1.3.1 找到线上 bug,测试,理解代码,改变框架,甚至语言,隔离,责任和其他事情变得更容易处理。我们不得不付出的代价是部署,但是,容器(Docker 和 Rocket)以及不可变服务器的概念也得到了极大的改善。
2.1.3.2 通过微服务,DevOps 和持续交付是我们工程实践的核心,我们决定 AWS 是支持我们专注于基础架构自动化的正确环境,同时为我们提供冗余和可扩展性。
2.1.3.2.1 Docker 容器将给我们两个具体的好处:
2.1.3.2.2 跨越环境以及跨 JVM 和非 JVM 应用程序统一部署管道:目前,我们正在管理 Symfony / Angular 表示层的部署,与我们的 JVM / Agora 中间层不同,导致不必要的差异,复制工作和浪费。
2.1.3.2.3 在单独的主机上部署每个应用程序 / 服务实例的能力:容器调度将使我们能够整合我们的计​​算资源,同时保持服务实例之间的隔离,同时提高利用率。
2.1.4 存在多种技术来重新组合特征 - 有时作为页面,有时作为组件 - 变为有凝聚力的用户体验。
2.1.4.1 前端(不管是不是 SPA)将被缩减为只负责路由选择和决定要导入哪些服务的脚手架。
2.2 核心思想
2.2.1 是技术不可知的
2.2.1.1 每个团队都应该能够选择和升级他们的堆栈,而不必与其他团队协调。自定义元素是隐藏实现细节的好方法,同时为其他人提供中立的界面。
2.2.2 隔离团队代码
2.2.2.1 即使所有团队使用相同的框架,也不要共享运行时。构建独立的应用程序。不要依赖共享状态或全局变量。
2.2.3 建立团队前缀
2.2.3.1 同意命名不能隔离的约定。命名空间 CSS,事件,本地存储和 Cookies,以避免冲突和明确所有权。
2.2.4 通过定制 API 支持本地浏览器功能
2.2.4.1 使用浏览器事件进行通信,而不是构建全局的 PubSub 系统。如果您确实需要构建跨团队 API,请尽量保持简单。
2.2.4.2 组件事件总线
2.2.5 构建弹性网站
2.2.5.1 即使 JavaScript 失败或尚未执行,您的功能仍应有用。使用通用渲染和渐进增强来提高感知性能。
2.3 解决了什么问题?
2.3.1 跨团队沟通的问题
2.3.1.1 Spotify 在内部被分成小队(3-12 人)队称为小队。一个特点通常由一个小队拥有,而在正常情况下,小队拥有开发和维持其特征所需的一切。一个小队的 iOS,Android,网络和后端开发人员是很正常的。一般的想法是,每个小队都应该有自己的能力来完成自己的功能,最大限度地减少小组要求其他部门获得许可和 / 或帮助。
2.3.1.2 提供了大量引进 library 的好处是少数人讨论,而不是涉及约 100 人的决定和他们的各种需求。这样一场大讨论不仅会耗费时间和精力,而且会迫使我们采用最不起眼的方法来选择 library,而不是选择专门针对每个 team 的问题领域的方案。
3 微前端的优缺点
3.1 优点
3.1.1 可重用的代码
3.1.2 敏捷性 - 每项服务的独立开发和部署周期
3.1.2.1 可以独立部署
3.1.2.2 一旦完成其中一项就可以部署,而不必等待所有事情等。
3.1.3 降低错误和回归问题的风险
3.1.4 更简单快捷的测试
3.1.5 微前端的好处
3.1.5.1 个人开发团队可以选择自己的技术。
3.1.5.2 开发和部署非常快。
3.1.5.3 微服务的好处可以更好地利用。依赖性急剧下降。
3.1.5.4 有助于持续部署。
3.1.5.5 维护和支持非常简单,因为个人队伍拥有特定的区域。
3.1.5.6 测试变得简单以及每一个小的变化,你不必去触摸整个应用程序。
3.2 缺点
3.2.1 开发与部署环境分离
3.2.1.1 一个复杂的开发环境
3.2.1.2 有一个孤立的部署周期。
3.2.1.3 能够在一个孤立的环境中运行。
3.2.2 复杂的集成
3.2.2.1 隔离 js,避免 css 冲突,根据需要加载资源,在团队之间共享公共资源的机制,处理数据获取并考虑用户的良好加载状态。
3.2.2.2 but at the end, they all need to integrate somehow in the same front-end so the user can see it.
3.2.2.3 Contract Testing 也只不过是在微服务架构下的产物,很多都是。
3.2.3 第三方模块重叠 / 冗余组成复杂性
3.2.3.1 依赖管理
3.2.3.1.1 At first, we built applications as completely standalone applications that were loaded into iframes and communicated over postmessage. We're still doing that for some things, but it has some big drawbacks, especially when it comes to bundle size and browser support. Bundle size is pretty obvious, since you end up sending the same libraries multiple times, and since the applications are separated, you can't extract common dependencies at build time.
3.2.3.2 The problem of extracting libraries out is the synchronization between the page and the apps, you don't want to load any more or any less libraries that your apps need, so let's just focus on the main, heavier ones, such as React for now.
3.2.3.2.1 <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.4.2/react.min.js" crossorigin="anonymous"></script>
3.2.3.3 if you need two version you will still be loading a duplicated library, so the problem persists.
3.2.3.3.1 My best suggestion would be to ask the teams to upgrade their apps.
3.2.4 最终用户的体验
3.2.4.1 初始 Loading 时间可能会增加
3.2.4.2 HTML 应该是服务器端渲染。
4 参考资料
4.1 https://inbox.google.com/u/1/search/micro%20frontend
4.2 https://medium.com/@_rchaves_/building-microfrontends-part-ii-joining-apps-together-dfa1b6f17d3e
4.3 https://www.diigo.com/user/jimmylv?query=%23microfrontends
5 一些思考
5.1 技术选型
5.1.1 既然 React 经验不错,那为什么不推广到全公司?可能是跨 vendor 合作
5.1.2 前端 JavaScript 框架穷出不穷,最后又出来要取代 Webpack 和 Yarn 的工具,过几个月就要重写项目?重构压力、负担大,那不如直接支持多 framework?
5.2 在 Mobile/Mobile Web 上的悖论
5.2.1 已经分出了不同的子页面,那何不如直接 Router 即可?
5.3 合理划分:DDD
5.3.1 最大的挑战是搞清楚如何划分应用程序。糟糕的设计可能成为开发和维护的噩梦。主要原则是将应用程序分为不同的部分,子部分和组件。
5.4 所谓架构,其实是解决人的问题;所谓敏捷,其实是解决沟通的问题;所谓精益,其实是讨论如何榨干劳动力,美其名曰减少浪费;
5.5 Don't use any of this if you don't need it
5.5.1 do not use the ideas described here until it is needed, it will make things more complex.
5.5.2 If you are in a big company, those ideas could help you
6 Demo 时间
6.1 Single-SPA “meta framework” 可以在一个页面将多个不同的框架整合,甚至在切换的时候都不需要刷新页面 (这个是 demo,支持 React, Vue, Angular 1, Angular 2, etc)。
6.1.1 Build micro frontends that coexist and can each be written with their own framework.
6.1.2 Use multiple frameworks on the same page without refreshing the page (React, AngularJS, Angular, Ember, or whatever you're using)
6.1.3 Write code using a new framework, without rewriting your existing app
6.1.4 Lazy load code for improved initial load time.
6.1.5 Hot reload entire chunks of your overall application (instead of individual files).
6.2 比较好的方案、框架支持,https://single-spa.surge.sh/
6.3 import * as singleSpa from 'single-spa'; const appName = 'app1'; const loadingFunction = () => import('./app1/app1.js'); const activityFunction = location => location.hash.startsWith('#/app1'); singleSpa.declareChildApplication(appName, loadingFunction, activityFunction); singleSpa.start();
7 实战
7.1 Building Microfrontends
7.1.1 Creating small apps (rather than components)
7.1.1.1 # server.js const renderedApp = renderToString(React.createElement(App, null));
7.1.1.2 # src/App.js export default () => <header> <h1>Logo</h1> ...... </header>;
7.1.1.3 header, products-list, shopping-cart
7.1.2 Joining apps together
7.1.2.1 Option 1: template
7.1.2.1.1 # server.js Promise.all([ getContents('https://microfrontends-header.herokuapp.com/'), getContents('https://microfrontends-products-list.herokuapp.com/'), getContents('https://microfrontends-cart.herokuapp.com/') ]).then(responses => res.render('index', { header: responses[0], productsList: responses[1], cart: responses[2] }) ).catch(error => res.send(error.message) ) );
7.1.2.1.2 # views/index.ejs <head> <meta charset="utf-8"> <title>Microfrontends Homepage</title> </head> <body> <%- header %> <%- productsList %> <%- cart %> </body>
7.1.2.1.3 Problem: Some apps may take longer to load There are some cases where things take a while to load on the back-end, maybe your header loads much faster than the other parts, and you want to display that ASAP to your users, while the products list takes more time.
7.1.2.1.3.1 Option 1.1: Progressive loading from the back-end
7.1.2.2 Option 2: iframe
7.1.2.2.1 <body> <iframe frameBorder="0" width="100%" height="200" src="https://microfrontends-header.herokuapp.com/"></iframe> <iframe frameBorder="0" width="100%" height="200" src="https://microfrontends-products-list.herokuapp.com/"></iframe> <iframe frameBorder="0" width="100%" height="200" src="https://microfrontends-cart.herokuapp.com/"></iframe> </body>
7.1.2.3 Option 3: Client-Side JavaScript
7.1.2.3.1 This basically loads the apps through ajax and insert their content inside those divs. It also has to clone each script tag manually for them to work.
7.1.2.3.2 var script = document.createElement("script"); script.setAttribute("src", nonExecutableScript.src); script.setAttribute("type", "text/javascript"); element.appendChild(script);
7.1.2.3.3 一个缺点:to avoid problems with Javascript and CSS loading order, I suggest you to evolve this to a solution similar to facebook's bigpipe, returning a JSON like { html: ..., css: [...], js: [...] } so you can have full control of it.
7.1.3 Public Path Problem and Routing
7.1.3.1 coupling between the homepage and the apps, what if one team takes care of developing the homepage, and another one the header? What if we want to add the same header to another page?
7.1.3.2 a centralized URLs service. It should provide an API for the apps to register their own URLs, and this service would be in the front of your website, just pointing to the other apps. We will call it Router.
7.1.3.3 Option 4: WebComponents
7.1.3.3.1 # src/index.js class Header extends HTMLElement { attachedCallback() { ReactDOM.render(<App />, this.createShadowRoot()); } } document.registerElement('microfrontends-header', Header);
7.1.3.3.2 <body> <microfrontends-header></microfrontends-header> <microfrontends-products-list></microfrontends-products-list> <microfrontends-cart></microfrontends-cart> </body>
7.1.4 Communication Between Apps
7.1.4.1 because it should work with any other technologies and frameworks, you can send messages from React to Angular for example.
7.1.4.2 This is the same reason nowadays everybody uses JSON to communication on the back-end, even if nobody uses NodeJS!
7.1.4.3 how do we test this communication? How to write integration or contract tests here? I don't know. Also adding some ideas from Event Driven Architecture here might be good.
7.1.4.4 # angularComponent.ts const event = new CustomEvent('addToCart', { detail: item }); window.dispatchEvent(event); # reactComponent.js componentDidMount() { window.addEventListener('addToCart', (event) => { this.setState({ products: [...this.state.products, event.detail] }); }, false); }
7.2 iframe
7.2.1 每个应用程序都在自己的小 iframe 中,这使得小组能够使用任何他们需要的框架,而无需与其他小组协调工具和依赖关系。
7.2.2 将微服务包装到 IFrames 中,然后使用一些库和 Window.postMessageAPI 来交互。
7.2.3 缺点
7.2.3.1 Bundle 的大小非常明显,因为你最终会多次发送相同的库,并且由于应用程序是分开的,所以在构建时不能提取公共依赖关系。
7.2.3.2 至于浏览器的支持,你基本上不能嵌套两层以上的 iframes(parent - > iframe - > iframe),或者所有的地狱崩溃。如果任何嵌套的框架需要能够滚动或具有表单域,准备痛苦。
7.2.4 优点
7.2.4.1 Iframes - 最强大的实现还隔离了组件和应用程序部分的运行时环境,因此每个部分都可以独立开发,并且可以与其他部分的技术无关 - 也就是说我们可以在 React 中开发一些部分,在 Angular 中开发一些部分,在 vanilla Js 中开发更多或任何其他技术。只要 iframes 来自同一个来源,帧间消息传递就相当直接和强大。
7.3 Web Components 来作为一个整合层整合所哟模块
7.3.1 每个团队建立他们的组件使用他们所选择的网络技术,并把它包装自定义元素中(如<order-minicart></order-minicart>)。
7.3.1.1 允许创建可以导入到 Web 应用程序中的可重用组件。它们就像可以导入任何网页的小部件。
7.3.2 Web 组件 - Web 组件是应用程序中包含的组件的本地实现,如菜单,表单,日期选择器等。每个组件都是独立开发的,主应用程序项目利用它们并组成最终的应用程序。
7.3.3 这个特定元素(标签名称,属性和事件)的 DOM 规范充当其他团队的合同或公共 API。
7.3.4 优点
7.3.4.1 自定义元素是一个 Web 标准,所以像 Angular,React,Preact,Vue 或 Hyperapp 这样的主流 JavaScript 框架都支持它们。
7.3.4.1.1 自定义元素
7.3.4.1.1.1 可以创建自己的自定义 HTML 标签和元素。每个元素可以有自己的脚本和 CSS 样式。
7.3.4.1.1.2 还包括生命周期回调,它们允许我们定义特定于正在开发的组件的行为。
7.3.4.1.1.2.1 createdCallback 定义了组件注册时发生的行为。
7.3.4.1.1.2.2 attachedCallback 定义了将组件插入到 DOM 中时发生的行为。
7.3.4.1.1.2.3 detachedCallback 定义从 DOM 中删除元素时发生的行为。
7.3.4.1.1.2.4 attributeChangedCallback 定义添加,更改或删除元素的属性时发生的行为
7.3.4.1.2 Shadow DOM
7.3.4.1.2.1 允许我们在 Web 组件中封装 JavaScript,CSS 和 HTML。在组件内部时,这些东西与主文档的 DOM 分离。
7.3.4.1.3 HTML 导入
7.3.4.1.3.1 在微服务的上下文中,导入可以是包含我们要使用的组件的服务的远程位置。
7.3.4.1.3.2 <link rel="import" href="/components/tc-books/tc-books.html"> <link rel="import" href="/components/tc-books/tc-book-form.html">
7.3.4.1.4 HTML 模板元素
7.3.4.1.4.1 可以用来保存客户端内容,当页面加载时不会渲染。
7.3.4.2 他们可以使用组件及其功能,而不必知道实现。他们只需要能够与 DOM 进行交互。
7.3.4.3 使用 PubSub 机制,组件可以发布消息,其他组件可以订阅特定的主题。幸运的是浏览器内置了这个功能。
7.3.4.3.1 购物车现在可以订阅此事件window并在应该刷新其数据时得到通知。
7.3.4.3.2 window.addEventListener('blue:basket:changed', this.refresh);
7.3.5 缺点
7.3.5.1 可悲的是,Web 组件规范根本不谈论服务器渲染。没有 JavaScript,没有自定义元素:(
7.3.5.2 浏览器不全,支持不够,社区不够,框架支持不够。
7.3.5.2.1 WebComponents are still not fully supported in all browsers, with Mozilla holding back HTML imports, so you will need polyfills, more code for the user to load.
7.3.5.2.2 It haven't really gained popularity yet, maybe never will, I see blogposts from 2013 and still few people have tried it!
7.3.5.2.3 JavaScript bundle has to load first and register the components in order for the DOM to load, which means that to gain the advantages of server-side rendering you'll probably need to be more clever.
7.3.5.2.4 For this alternative we had to make changes not only on the homepage, but on the apps too, to convert them to WebComponents.
7.4 组件库 - 根据主应用程序的堆栈,不同的组件和应用程序部分可以开发为库和 “需要” 到主应用程序,所以主应用程序是由不同组件组成的。
7.4.1 将 “应用程序” 作为黑盒 React 组件分发给消费应用程序。应用程序的状态完全包含在组件中,API 只是通过 props 暴露出来。
7.4.2 它增加了应用程序之间的耦合,因为它迫使每个人都使用 React,甚至会使用相同版本的 React,但是对于我们来说,情况已经如此,所以这似乎是一个好的折衷。
7.5 SSI / ESI 方法的缺点是,最慢的片段决定了整个页面的响应时间。所以当一个片段的响应可以被缓存时是很好的。
7.5.1 Edge Side Includes(ESI) 和 Server Side Includes(SSI) 和功能类似. SSI 需要特殊的文件后缀 (shtml,inc).ESI(Edge Side Include)通过使用简单的标记语言来对那些可以加速和不能加速的网页中的内容片断进行描述,
7.5.2 每个网页都被划分成不同的小部分分别赋予不同的缓存控制 策略, 使 Cache 服务器可以根据这些策略在将完整的网页发送给用户之前将不同的小部分动态地组合在一起.
7.5.3 通过这种控制, 可以有效地减少从服务器抓取整个 页面的次数, 而只用从原服务器中提取少量的不能缓存的片断, 因此可以有效降低原服务器的负载, 同时提高用户访问的响应时间.
7.6 页面模块加载的问题
7.6.1 推荐区域最初是空白的。团队绿色 JavaScript 被加载和执行。用于获取个性化推荐的 API 调用已经完成。推荐标记被呈现并且请求关联的图像。现在片段需要更多的空间,并推动页面的布局。
7.6.2 团队红,控制页面,可以固定建议容器的高度。在响应式网站上,确定高度往往很难,因为不同的屏幕尺寸可能会有所不同。但更重要的问题是,这种队际协议在球队红绿之间产生了紧密的联系。
7.6.3 更好的方法是使用称为骨架屏幕的技术。Team red 将green-recosSSI Include 包含在标记中。此外,团队绿色会更改其片段的服务器端渲染方法,以便生成内容的原理图版本。该骷髅标记可以重用的实际内容的布局样式的部分。这样就保留了所需的空间,实际内容的填充不会导致跳跃。
7.6.3.1 A skeleton screen is essentially a blank version of a page into which information is gradually loaded.
7.7 优化
7.7.1 对于生产成本高且难以缓存的碎片,将其从初始渲染中排除是一个好主意。浏览器异步加载。 在我们的例子中green-recos,显示个性化推荐的片段就是这个候选人。
7.7.2 在前端设计中,必须向用户呈现外观和感觉一致的用户界面。有很多页面上都出现了 UI 元素。关注的分离并不总是像后端服务一样清晰。
7.7.3 UI 组件库
7.7.3.1 我们用微前端解决的下一个挑战是呈现一致的外观和感觉,同时也隔离风险。
7.7.3.2 我们建立了一个共享组件(CSS,Font 和 JavaScript)的库。我们将这些资源托管在每个微前端可以在其 HTML 输出中引用它们的位置。每个组件库的版本都正确地对资源进行版本控制,每个微前端都指定要使用的组件库的版本。因此,CSS 和 JavaScript 不会意外地改变; 每个微前端的开发者都必须显式更新依赖关系。
7.8 Micro Frontends + AEM/jQuery
7.8.1 AEM (of course), which contains web content only (no structured domain data).
7.8.2 React.js components that are hosted in AEM. AEM passes through different properties the components need e.g. ids, URLs of services.
7.8.3 Microservices that contain the structured domain data, and that are queried by the React.js components via Ajax.

More Maps From User