当前位置:首页 > 科技数码

微前端 【208期】精致化的微前端开发

微前端是将微服务概念应用于前端技术后的一种相关实践,它使前端项目能够由多个团队独立开发和部署。

在本文中,我们将以此为目标,从零开始为微前端的开发构建一套最佳实践*,并使用它们来完成微前端网站中的示例应用:

*任何地方的最佳实践都是基于主观判断,所以不要盲目跟随实际情况。

微前端开发的目标

在微前端的实践中,其实所有的需求都是为“自主开发”和“自主部署”两个目标服务的,或者可以看作是两者的具体实施措施或者自然衍生结果。通常,我们所说的“微前端特性”包括但不限于:

技术无关:各个开发团队都可以自行选择技术栈,不受同一项目中其它团队影响;代码独立:各个交付产物都可以被独立使用,避免和其它交付产物耦合;样式隔离:各个交付产物中的样式不会污染到其它组件;原生支持:各个交付产物都可以自由使用浏览器原生 API,而非要求使用封装后的 API;

为了实现微前端的目标,整个项目需要拆分隔离。

结合微服务的微前端架构

早些时候,我们可能会想到

近年来,另一种Web原生分裂隔离方案逐渐进入人们的视野,那就是Web Components。但是这里也有对Web组件的负面态度。

为什么网络组件不是一个有效的解决方案

为了理解拒绝Web组件的原因,我们首先简单介绍一下什么是Web组件。

https://www.webcomponents.org/

Web Components是W3C维护的一组技术概念,目前包含*:

Shadow DOMCustom ElementsHTML TemplatesCSS changes

这四个部分的技术内容。

* HTML Imports一度被收录,但已经被删除,很多相关资料也没有更新。

需要注意的是,虽然Web Components的内容是由W3C维护的,但是技术本身不是。影子DOM、自定义元素、HTML模板是HTML生活标准和DOM生活标准的官方内容,由WHATWG维护。

因此,否定Web组件涉及两个方面:

作为交互中间层的场景下,Shadow DOM、Custom Elements 和 HTML Templates 解决的问题是完全独立的:Shadow DOM 用于辅助样式隔离,Custom Elements 用于简化启动代码,而 HTML Templates 用于内部实现对中间层封装毫无帮助。作为技术选型而言,每个问题的解决方案也是相互正交的,因此 Web Components 这个伪概念在此并不能发挥任何整体价值;Shadow DOM、Custom Elements 分别都不是相应问题的有效解决方案,以下会详细说明。

在这一点上,我们可以完全忘记Web组件的错误概念,只看具体的技术本身。

首先要注意的是Shadow DOM和Custom Elements只有IE11+的Polyfill支持,所以需要考虑IE9和IE10的应用可以直接排除。但是即使对于只需要考虑IE11+的应用,这两种技术也不是最优的解决方案。

定制元素的无形成本

自定义元素的使用

自定义元素用于扩展HTML元素的注册表,使HTML Parser能够识别用户定义的元素名称*,并能够自动触发相应的声明周期。

*准确地说,定制元素包括自主定制元素和定制内置元素。由于后者既没有统一的浏览器原生支持(Firefox宣布不支持),也没有可用的Polyfill,所以本文默认只参考前者。

有了自定义元素,启动代码就是HTML本身(或者对应的DOM API调用):

& ltbook-card title = " Custom Elements " author = " Web " price = " 42 " >;& lt/book-card>。

以及:

constbookCard=document。(“图书卡”)

bookCard.setAttribute('title ',' Custom Elements ')

bookCard.setAttribute('作者',' Web ')

bookCard.setAttribute('price ',' 42 ')

虽然看起来和一般的HTML模板代码一样,但实际上Custom Elements和框架模板的明显区别是没有作用域和验证。

如果元素名称偏离或者没有在运行时引入依赖组件,你只会得到一个HTMLUnknownElement,不会有错误,这样用户会得到意想不到的结果。

另外,从DOM操作可以看出一个主要问题:DOM API不支持批量更新。

对于原生DOM操作,这当然不会造成任何问题。但是在当前的应用中,Custom Elements的职责是生命周期代理,处理方法是通知其他框架在attributeChangedCallback生命周期中更新。类似于:

classbookcardextendshtmlelelement { AttributeChangedCallback(名称:string,旧值:string,新值:string){ somotherFrame . update({[name]:新值})}}

由于没有批量更新,上述修改标题、作者、价格的场景触发了三次数据更新操作。对于(大多数)视图同步框架来说,数据更新会有固定的附加成本(即使没有视图修改),这不仅会造成不必要的性能浪费,还可能会进入意外的中间状态。

当然也有一些变通办法,比如在封装层统一等待nextTick,或者使用接受对象的DOM属性进行更新,但是对于可维护性和易用性还是会有相当大的影响。

阴影DOM对服务器端渲染的破坏性很大

带阴影根的DOM树

阴影DOM V0引入了内容投影机制,由

众所周知,服务器端渲染可以直接生成静态的HTML供浏览器解析,但是

假设一个组件有以下阴影DOM:

& ltdivclass="foo">。& lth4>。Foo <。/h4>。& lt插槽>。& lt/slot>。

& lt/div>。

由以下代码使用:

& lt元素-详细信息>。& ltp>。Bar <。/p>。

& lt/element-details>。

那么实际的渲染结果是:

& lt元素-详细信息>。& ltdivclass="foo">。& lth4>。Foo <。/h4>。& ltp>。Bar <。/p>。& lt/div>。

& lt/element-details>。

问题是如果我们真的在服务器端渲染结果,从浏览器端开始,服务器端渲染的结果会被认为是原始内容,重新投影:

& lt元素-详细信息>。& ltdivclass="foo">。& lth4>。Foo <。/h4>。& ltdivclass="foo">。& lth4>。Foo <。/h4>。& ltp>。Bar <。/p>。& lt/div>。& lt/div>。

& lt/element-details>。

之所以在Web框架中没有出现这个问题,是因为投影过程是可以控制的(标记有额外的特殊属性),但是原生实现中没有相应的控制权,无法区分原始内容,导致多次渲染不正确。

*真正的原因是大多数Web框架根本不支持从外部HTML中提取子内容节点,只能在框架内部使用。

因此,如果你想使用Shadow DOM,你必须得到所有开发者的签名并承诺永远不使用

与一般的网络开发概念不同,阴影DOM使用嵌入式

一次

& lt元素-详细信息>。& ltstyle>。p {背景色:红色}<。/style>。& ltp>。Foo <。/p>。

& lt/element-details>。

& ltp>。Bar <。/p>。

因此,除了

交付品的设置

在所谓的“微前端”概念下,可交付成果可以大致分为几个不同的层次:

独立部署的网站:开发团队的构建产物为独立网站并自行完成网站的部署,通常对应的集成方式为在多个长得很像的网站之间跳转或者 <iframe> 标签引入外部页面;网站内容:开发团队的构建产物为独立网站但通过压缩包上传并部署到统一位置,通常对应的集成方式为服务器端的反向代理;页面脚本:开发团队的构建产物为 Java 代码文件*并被站点引入到站点中;

*目前有HTML模块的提案。虽然发布的内容是HTML文件,但是介绍方式是ES Module,也算是发布Java代码的方式。

那么在发布Java代码的大方向下,大致可以分为两类:

发布组件:启动位置和时机完全由父级内容控制,支持多实例,例如 Custom Elements;发布局部应用:启动过程由自身配置,只能单实例,例如 single-spa;

简单类比,前者发布一个“类”,后者发布一个“实例”。这里我们选择前者,所有发布的内容都是可重用的组件,由父组件决定何时何地使用。

由于之前已经排除了自定义元素的选项,所以有必要定义一个新的接口来满足组件化的要求,并避免自定义元素中的缺陷。

组件设计

自定义元素的生命周期

由于目标只是实现生命周期agent,不需要视图管理的支持,所以现有的框架都太重,不可控的风险极高。为此,我们需要自己定义一套API,有很多选择。例如,一个简单有效的设计是Svelte的获取/设置API:

//设置状态

component.set({value:42})

//获取状态

const{value}=component.get()

统一状态更新API可以有效解决批量更新的问题:

component . set({ title:' Sverte-style ',作者:' Sverte ',价格:42,})

至于事件通信API和生命周期API,可以更随意一些。

之后,我们还需要考虑组件之间的依赖关系。自定义元素解决方案中一个明显的问题是“组件使用”和“依赖创建”之间的脱节,这依赖于外部环境一次引入所有内容。

建立Java文件之间依赖关系最直观的方法是ES Module,类似于:

从“@ domain/MF-helper”导入{render,Component }

从“@domain/lib1”导入{ MyComp1 }

class mycomp 2 extends component { some method(){ render(my comp 1,myElement)}}

此外,动态导入(第3阶段建议)可用于处理动态内容的依赖性:

从“@ domain/MF-helper”导入{render,Component }

class mycomp 2 extends component { async some method(){ const { my comp 1 } = awaitiimport(' @ domain/lib 1 ')render(my comp 1,myElement)}}

基于专家系统模块的参考关系可以使组件的可用性绝对可靠。如果依赖模块加载失败,则不执行当前模块,从而进入统一错误处理页面。当然,在动态导入模式下,调用者可以自己定义本地错误处理,并提供回退支持*。

*除非有明确的应用程序边界(例如,小程序级别的独立子程序),否则回退行为是不可靠的。如果依赖关系对整个应用程序不可用,应该立即报告错误,而不是尽可能最好地运行。

这样在源代码层面极大地保证了可读性和可维护性,有效提高了开发效率。

当然,我们知道浏览器的模块支持在实际应用中不会直接用在生产环境中,而是会提前打包搭建。为了维护独立部署的目标,需要在构建过程中排除其他组件的依赖关系,将依赖关系的确定推迟到运行时处理。

至于风格隔离,可以由各个团队直接处理,主流框架基本都提供组件风格的解决方案。另外,样式冲突是常规前缀无法避免的。虽然不会污染父组件,但还是会污染子组件。(除非仅使用父子选择器而不是后代选择器)

一般在整个应用中,只有一定级别的特定组件会使用路由(每个组件都需要知道是否使用)。因此,在组件设计中,当前的基本路径和导航将逐层通过上下文传递。如果一个组件需要实现子路由,它可以被封装,并自己暴露给下层。

构建和部署战略

如前所述,为了保证开发效率,源代码只使用ES Module引入外部组件,并没有封装到当前组件中,而是推迟到运行时。

这里可能出现的问题是,组件是否需要发布到注册中心?

答案是需要*,为了实现两个目标:

Library as a Constraint:模块导出、类型签名能够静态确定;Registry as a CDN:不同版本同时处于可访问状态;

*技术上还是不需要的,只需要发布后的URL。

这里的注册表不一定是NPM注册表,而是任何可以稳定追溯的机制,比如Git Tags。

虽然在构建工具层面不需要安装依赖,但是安装为依赖可以有效改善开发体验,比如编辑器的代码提示,版本发布要满足语义版本号的要求(并且可以在运行时验证)。

同样,虽然运行时只需要组件发布的URL,但是通过Registry发布可以得到很多好处:

全版本实时可用:发布新版本并不会替换掉原有版本,所有版本同时处于可用状态,能够通过在 URL 中指明版本进行访问,例如 https://unpkg.com/@angular/core@6.0.4/bundles/core.umd.js,以至于在切换版本(更新或者回退)时,并不需要重新部署相应代码,而仅仅修改版本配置文件即可;Alias 自动更新:为了方便自动同步,测试环境中可以并不使用具体版本号,而使用 Alias,例如在 Staging 环境中使用 @stable,就能无需配置自动同步最新版本,快速验证兼容性问题。

此外,有必要在运行时引入依赖管理方案。

所有主流浏览器都支持ES Module,但大部分应用还是需要照顾一些旧版本。同时ES Module的模块引入过程无法截取,所以无法使用原生实现。

在目前主流的模块格式中,AMD和System.register是唯一可以在浏览器运行时使用的。相比之下,后者功能强大得多,几乎为ES Module提供了完整的语义支持(比如循环依赖和顶层异步等待)。同时后者也有SystemJS这样优秀的工具,有自己的扩展支持,不需要从头实现。

例如,专家系统模块的代码:

从“@ domain/MF-helper”导入{render,Component }

从“@domain/lib1”导入{ MyComp1 }

exportclassmycamp 2 extends component { some method(){ render(my camp 1,myElement)}}

以System.registry格式打包,输出为:

System.register('@domain/lib2 ',['@domain/mf-helpers ',' @domain/lib1'],function(exports,module){ ' use strict ';varrender,Component,MyComp1返回{ setters:【function(module){ render = module . render;组件=模块。组件;},函数(module){MyComp1=module。MyComp1}],execute:function(){ ClassMyomp 2 extends component { some method(){ render(Myomp 1,MyElement);} }导出(' MyComp2 ',my comp 2);}};});

这里虽然没有外部依赖,但是需要注意的是@domain以外的其他组件的内部依赖不需要实时更新,可以直接打包成文件。

为了验证版本,输出内容需要部分扩展:

system.pkginfo({name:'@domain/lib2',version:'1.0.1',dependencies:{'@domain/mf-helpers':{version:'^1.0.0'},'@domain/lib1':{version:'^1.2.3'},}});System.register(/*...*/);

因此,它可以验证lib1和lib2是否符合版本要求(并且可以用于其他后续扩展)。

虽然mf-helpers在这里被认为是一个外部依赖,但是应该完全允许内联,所以它的内部实现必须是完全结构化的,从而避免副本*之间的冲突。

*准确的说,不是回避,而是通知相关人员解决。

所以运行时需要扩展钩子System.register()和System.instantiate(),增加System.pkgInfo()的实现。

从属库的重用

虽然微前端的目标是代码隔离,但它仍然可以允许开发人员主动声明可重用的内容。

在当前的发布模式下,区分可重用依赖和不可重用依赖非常简单:不可重用依赖封装在组件内部,而可重用依赖保留模块声明。但是,还有一个问题,如何实现可重用依赖的重用?

为了能够重用依赖项,您需要能够独立打包依赖项并记录它们的网址:

system.pkginfo({name:'@domain/lib3',version:'1.0.1',dependencies:{'some-lib':{version:'^1.0.0',path:'./some-lib.js'},} });

所以在运行时加载lib3的时候,会判断当前环境下是否有兼容1.0.0版的some-lib包,如果没有,会请求对应的URL。

但是,需要注意的是,同一个依赖关系的单个副本和多个副本之间的差异会影响运行结果。在声明可重用的依赖关系时,需要确保它们遵循语义版本号,没有内部状态,也希望它们没有其他外部依赖关系。当然,在现实中,很容易找到典型的可重用依赖关系,比如常见的工具库:lodash或moment。

在线示例

在这里,我们改写了Angular、React和Vue在https://micro-frontends.org/,实施的三个团队项目,完美对应了色彩关系。

示例地址为:https://trotyl.github.io/mif-demo-entry/,,相应的组件源代码位于:

trotyl/mif-demo-angulartrotyl/mif-demo-reacttrotyl/mif-demo-vue

基础设施位于:

trotyl/mif-coretrotyl/mif-runtime

因为文章是关于Micro-Frontend的,所以mif在样例代码中被广泛用作前缀。

该示例以最外面的红色团队组件作为根组件开始:

const{render,ROOT _ CONTENT } = MIF . CoreSystem . import(' https://trotyl . github . io/MIF-demo-angular/demo-angular . MIF . js ')。然后(m = & gtm.RedApp)。然后(RedApp = & gtrender(RedApp,{ CONTENT:ROOT _ CONTENT },document.body))

可以看到,本文从三个不同的GitHub Repo加载组件脚本,它们共同构成了这个完整但前端的应用程序:

页面脚本请求

同时,虽然moment在所有三个组件中都被用作依赖项,但是由于重用的设置,它在整个应用程序中只被加载一次,然后被所有组件共享。

例子中使用的临时支撑库还是有很多问题的,不建议在真实的微前端项目中直接使用。

写在最后

微前端是一个巨大的话题。由于时间关系,本文没有涵盖所有可能的方面。在目前的很多团队中,微前端实践往往意味着巨大的牺牲,比如为了优化所谓的部署时间而放弃应用性能、代码质量、开发经验等等。但本文的目标是探索一种尽可能保持当前前端生态的优秀实践,符合微前端发展理念的细化方案。

1.《微前端 【208期】精致化的微前端开发》援引自互联网,旨在传递更多网络信息知识,仅代表作者本人观点,与本网站无关,侵删请联系页脚下方联系方式。

2.《微前端 【208期】精致化的微前端开发》仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证。

3.文章转载时请保留本站内容来源地址,https://www.lu-xu.com/keji/1202896.html

上一篇

厄齐尔遭抢劫 厄齐尔遭抢劫有无受伤情况怎样

下一篇

范伟赵本山同框是怎么回事《刘老根》系列重启是真的吗

张伦硕律师声明是什么内容张伦硕律师声明什么情况

张伦硕律师声明是什么内容张伦硕律师声明什么情况

7月25日,张伦硕在社交平台上发表律师声明,斥责部分自媒体账号谣言:“鹿为马,黑白颠倒,信口开河,挑拨离间。。。。。。还这么自信?文人遇匪,有理说不清?流氓懂武功,谁也拦不住。一直以为一切恶意都会过去,结果却是越来越多没有底线的污蔑和造谣。世界...

联邦快递回应说了什么内容?联邦快递怎么回应的

近日,国家有关部门对联邦快递(中国)有限公司未能依法按照其名称和地址投递快件一案进行调查,发现联邦快递称将华为相关快件转往美国是“错误操作”与事实不符。联邦快递回应说了什么?联邦快递如何回应?26日,联邦快递做出了最新回应,称:“我们已经注意到...

尤长靖工作室声明内容是什么坤音老板恶意用词引热议

尤长靖工作室声明内容是什么坤音老板恶意用词引热议

日前,尹坤娱乐的老板秦周易在一段聊天记录中被曝光。在录音中,秦周易对尤长靖的恶意言论引起了热烈的讨论。27日凌晨,秦周易发表道歉,称无辜者被误解,感到非常抱歉。她还说别有用心的人不要伤害刚起步的年轻艺人。随后,尤长靖工作室发表声明称,音频中的言...

坤音老板正式声明内容是什么坤音老板发为什么道歉尤长靖

坤音老板正式声明内容是什么坤音老板发为什么道歉尤长靖

尹坤娱乐公司老板秦周易的一段聊天记录在网上曝光,在网上引起很大争议。在录音中,秦周易表示,尤长靖的“绿茶泼妇”、“作为女人看”等言论令尤长靖粉丝极为不满。很多粉丝都看了留言:“你一定要积极道歉!”“这是人身攻击,非常不满意”,“希望尹坤老板能有...

黄毅清被拘留 黄毅清为何被拘黄毅清道歉内容

黄毅清被拘留 黄毅清为何被拘黄毅清道歉内容

7月28日,上海市虹口区公安局发布警告通知,纪检监察部门联系海报黄谋清(男,34岁)进行约谈,并对其反映进行核实。黄一清被双规,然后发表道歉。我们来看看具体情况。黄一清发表了一份手写的道歉声明,称:“7月26日至27日,我在自己的微博账户上发布...

300203 成员风采|首届主任委员单位—聚光科技(杭州)股份有限公司.绿色科技引领者(股票代码:300203)

  • 300203 成员风采|首届主任委员单位—聚光科技(杭州)股份有限公司.绿色科技引领者(股票代码:300203)
  • 300203 成员风采|首届主任委员单位—聚光科技(杭州)股份有限公司.绿色科技引领者(股票代码:300203)
  • 300203 成员风采|首届主任委员单位—聚光科技(杭州)股份有限公司.绿色科技引领者(股票代码:300203)

聚光科技股票 成员风采|首届主任委员单位—聚光科技(杭州)股份有限公司.绿色科技引领者(股票代码:300203)

  • 聚光科技股票 成员风采|首届主任委员单位—聚光科技(杭州)股份有限公司.绿色科技引领者(股票代码:300203)
  • 聚光科技股票 成员风采|首届主任委员单位—聚光科技(杭州)股份有限公司.绿色科技引领者(股票代码:300203)
  • 聚光科技股票 成员风采|首届主任委员单位—聚光科技(杭州)股份有限公司.绿色科技引领者(股票代码:300203)
8月新规 8月有哪些新规具体什么内容

8月新规 8月有哪些新规具体什么内容

8月份,不同城市出台了不同的新规。让我们和边肖一起看看他们。(1)再次提高军队离退休人员养老金补贴标准;②中央单位出差人员餐费自行结算;3 .办理纳税服务投诉时限减半;(4)《绿色建筑评价标准》更新为5项指标;⑤实施12项出入境便利化政策;⑥北...