ESM Bundleless 在低代码场景的实践

简介: ESM Bundleless 在低代码场景的实践
编者按:本文是蚂蚁集团体验技术部前端工程师亦逊在第十六届 D2 前端技术论坛上的演讲内容。

主题介绍

今天分享的标题包含了三个关键词:ESM,即 ES Module,当前浏览器标准的模块规范;Bundleless,对应传统的 Bundle,即强调更少的打包;低代码,近两年火热的前端命题,相信大家都听说过、使用过、甚至开发过一个低代码平台,低代码的核心是借助可视化/配置的方式来提升应用的研发效率,这也是我所在的团队「云凤蝶」在努力做的方向。



在蚂蚁集团,我们有超过 36% 的中后台应用使用云凤蝶搭建,应用的平均复杂度在 20 个页面以上。云凤蝶内我们提供了丰富的 UI 资产,自研可视化画布让用户写更少的代码,对接 API 元数据一键生成表单表格,并且提供了官方的存储和模型服务。



一个搭建系统在发展中也会面临各种问题,今天主要结合在云凤蝶的实践经验,聊一聊对于一个企业级的中后台低代码平台,背后的资产加载体系是如何设计的。


接下来我会从背景、机会与挑战、设计思路和未来展望四部分来介绍资产 Bundleless 方案在云凤蝶的实践。首先是介绍云凤蝶「开放」的资产体系的背景。


云凤蝶「开放」的资产体系

在设计一个搭建系统之前,首要的问题就是组件从哪里来?一套组件库的研发成本是很高的,以 Ant Design 为例,单个组件的平均研发+测试成本至少在 2 周。而在体验技术部我们已经拥有了全球最好的一系列组件库,包括 Ant Design、AntV,要是云凤蝶能直接拿过来用我们就能节省非常多的时间和精力。



组件应该怎么拿?

常规的有两种方案,第一种平台集成这些组件库,直接管理组件的版本和打包构建。第二种方案是通过特定的组件规范能消费这些组件库,实现动态加载。



而事实上,无论再标准的中后台应用,业务中往往都会有自己的组件沉淀和能力需求,这些都无法全部内置到平台,所以低代码的资产体系必须要有开放的能力。「将资产与平台解耦,再通过规范建立连接,这样才能打造出富有生命力的资产体系」


NPM 导入

作为前端工程师我们都知道,NPM 社区是一个非常有活力的社区,所以我们确定了云凤蝶的资产体系的桥梁——NPM 导入。


接下来花大概不到1分钟的时间我们来演示一下 NPM 导入的过程。



在 NPM 导入面板中输入 NPM 包名和版本号后,云凤蝶就会自动解析出 NPM 包导出的 React 组件并加载,经过简单的配置就可以直接添加到云凤蝶的可视化画布中进行属性编辑。整个过程就像 npm install 一样丝滑。


刚刚到底发生哪些事呢?在 NPM 导入过程中我们会对 NPM 包进行依赖拆解和二次构建,将依赖关系和构建产物上传到 Assets CDN 上。


每次用户在云凤蝶编辑器操作中,通过感知依赖的变化,我们会处理最新的依赖关系并且根据版本进行依赖合并。


到了实际运行阶段,将合并好的依赖关系进行解析,所有模块的 js 产物通过 CDN Combo 的方式一次网络请求加载回来,再拆分给 SystemJS loader 进行模块实例化。



其中核心的部分就是依赖拆解。在依赖拆解和二次构建过程中,先从 npm registry 下载 npm 包,依据 package.json 浅层分析依赖信息,递归下载依赖,并通过 Webpack external 对每个依赖做了二次构建。


最终一个 npm 包就会得到一份依赖关系树和一系列 js 产物。



基于此云凤蝶算是完成了 Bundleless 资产体系1.0 的建设,再总结一下:


1、完全对接 NPM 生态,组件足够丰富,拒绝重复建设;

2、依赖复用,遵循 Semver 规则的版本合并;

3、包级别的按需加载,完全做到用到什么组件加载什么包;

4、实时预览,秒级部署上线,且支持 debug 调试;



下一代方案的设想

这套方案支撑了云凤蝶几百个中后台应用的搭建,但在云凤蝶发展过程中,随着搭建的中后台应用越来越多我们也发现了一些问题:


第一个是业务上的诉求,站点打开慢,尽管做了按需加载,但资产加载还成为搭建页面运行时的性能瓶颈。


对此我们也进行了分析,以官方资产 Ant Design 为例,官方提供了 62 个默认好用的组件,但实际的中后台应用中平均使用个数大约是 25 个,使用比例 40%


而我们选取了使用频率最高的 25 个组件进行了 bundle 测试,是 640 Kb,而 antd 整体的 bundleSize 是 1.2 M(UNZIPPED),也就是说差不多有一半的冗余体积。这还只是官方资产的情况,像图表、业务线资产包等冗余体积只多不少。


所以在搭建场景中,包级别的按需加载还远远不够,细粒度到组件级别的按需加载才更适合搭建的场景。此外方案中的 SystemJS 的性能也会影响模块实例化的耗时,我们需要寻找更快的模块加载方案。



第二个是技术上的追求,我们回顾过去20年的前端发展时间线:


视图相关从 jQuery 发展出了 React、Vue 等一系列优秀的框架。构建工具也从 Webpack、Rollup 到这两年火热的 Vite、esbuild。这一切的背后还伴随着网络能力和浏览器标准规范的升级,从 HTTP/1.1 到 HTTP/2.0,从 2G、3G 到如今的 5G,从 CommonJS、AMD、UMD 等模块规范到标准的 ES Module。


而云凤蝶的资产体系的技术却依旧停留在差不多 5-6 年前,这都马上 2022 年了,显然不够 modern。面向未来的方向,我们需要向前再迈一步



基于此,对于下一代的资产加载方案我们期望的是能达到体积上的下降和性能上的提升,核心是设计一套支持组件级别的按需加载,兼顾当前生产同时面向未来的高性能模块加载方案。最终我们将目光看向了 ES Module。



机会与挑战

于是我们开始对 ES Module 进行了调研分析,看看它的机会和挑战有哪些?


作为企业级中后台应用的搭建平台,首先我们要掌握的就是 ES Module 能够上生产环境了么?浏览器兼容性如何?加载性能相较于原先 SystemJS 怎么样?


从 Caniuse 的数据上来看,除了 IE11,Chrome、Firefox、Safari 的支持度都比较友好,满足了中后台应用浏览器的兼容性要求。同时在单模块的加载性能上对比 SystemJS,也有 1.3x -1.5x 的提升。



有了兼容性和性能上的保障后我们再来看看 ES Module 究竟是如何工作的。


浏览器从 ES Module 入口文件开始解析,找到导入语句的模块说明符,然后下载文件,继续解析,全部解析之后才会通过深度优先的后序遍历进行模块实例化


显然,在这个过程中递归的模块解析和下载会带来请求瀑布的问题,并且由于 import/export 语句的顺序要求,相比于传统的 SystemJS / UMD 等方案,无法直接做 CDN Combo。如果依赖关系层级非常深,那么不管 ES Module 单模块解析有多快,这个速度都不太理想。



面对请求瀑布的问题,我们开始调研有哪些解决方案,首先是 HTTP/2.0。


我们知道,在 HTTP/1.1 协议中浏览器在同一时间,针对同一域名下的请求有一定数量限制,超过限制数目的请求会被阻塞。以 Chrome 为例,最大并发限制是 6 个。而 HTTP/2 多路复用则允许同时通过单个连接发起多重的请求响应消息。HTTP/2 把通信的基本单位缩小为一个一个的帧,并行地在同一个 TCP 连接上进行双向交换。


下面有两个简短的视频可以演示一下 HTTP/1.1 对比 HTTP/2.0 的直观差异。看得出来,HTTP/2.0 的对并行加载的提升确实非常明显,但 js 模块依赖还是非常复杂的,大家都知道的 node_modules 黑洞,所以我们认为 HTTP/2.0 或许只能说是 ES Module 上生产必要条件但并不是标准答案。它可以解决依赖树广度的问题,但我们依然面临依赖树深度的问题



看起来 ES Module 上生产确实还是有一些阻力,回过头来我们还是找到了官方的说明,这是 V8 在 2018 年对使用 ES Module 的性能建议。


大概的意思是可以在以下两个场景中直接使用 ES Module,而不借助 Webpack、Rollup 等打包工具进行 Bundle。


1、本地研发,大家都比较好理解,像近两年火热的 Vite、Snowpack 等本地开发工具,通过对依赖的提前预构建 ESM 和本地单文件 ESM,启动和热更新都非常快,极大的提升了开发者体验;

2、生产环境上推荐小型 Web 应用,V8 在这里也对小型 Web 应用进行了解释:模块数量 < 100,依赖树层级 < 5。比如说偏展示型的静态站点,CodeSandBox 等在线 Demo 编辑场景。



那么距离 2018 年过去 3 年了,中后台低代码搭建或者说是云凤蝶应用可以么?


严格意义上来说任何中后台应用都算不上小型 Web 应用,但如果我们单看云凤蝶的资产加载,通过对云凤蝶资产的历史数据分析,我们认为通过特定的优化策略收敛复杂度,达到 V8 的建议,把资产加载体系搬到 ES Module 上是具备可行性的。


方案设计

接下来介绍我们在 ESM Bundleless 方案上是如何克服上述这些上生产环境的困难的。

Bundleless 粒度

首先是 Bundleless 的粒度,Bundleless 不是 Bundle 也不是 UnBundle,拆分的粒度决定了模块的数量级、依赖树复杂度和依赖复用程度


拆的粒度越细,模块数量越多,但复用程度越高,整体体积会越小。


拆的粒度越粗,模块数量越少,但复用程度就越低,整体体积也会越大。


所以说,拆分粒度的选择是一个体积和数量的权衡,要么大要么多。经过我们的测试,相较于文件级别粒度的海量请求,包级别的粒度会更适合生产环境。



导出级别的依赖分析

有了包维度的拆分依据,就要分析出依赖关系,按照之前我们的做法是也同样按包维度做了分析依赖,但这次我们进一步做到了导出级别,因为之前的经验告诉我们搭建系统需要组件级别的按需加载,也就是 Treeshaking。


同时 TreeShaking 在 Bundleless 里带来的收益不仅是减少了整体 Bundle 的体积,还因为摇掉了一些不必要的依赖从而显著降低依赖关系的复杂度。


通过 AST 分析 npm 导出后,对每个导出进行独立的依赖分析,我们得到了导出级别的依赖关系结构。在这个分析过程中还会遇到复杂的一些问题,包括:CommonJS 导出、babel-plugin-import 转回 namedImport 等。



以左侧的代码为例,就可以分析得到右侧结构化的依赖关系。




依赖合并

有了所有 npm 包的导出级别的依赖关系,我们就可以根据实际的组件消费情况进行依赖关系的合并,得到一颗 TreeShaking 后的依赖关系树可以看到,经过 TreeShaking 过后,依赖关系的复杂度已经显著降低。



当然在实际过程中还会遇到更多复杂的依赖结构,所以我们还需要设计一套可扩展的依赖合并优化策略。这里举2个例子:


1、当一个依赖节点的子节点嵌套非常深,并且所有的子孙节点都没有参与过整颗依赖树的依赖合并时,这个依赖节点就被定义为「孤立节点」,可以不拆分子依赖直接 bundle 在一起;


2、对于一个依赖的所有导出,如果都依赖了2个以上的公共子依赖,那这些公共依赖也可以 bundle 在一起;



解决了模块之间复杂的依赖合并,经过 TreeShaking 和特定策略的适当 Bundle,我们可以做到将依赖数量收敛到100,层级收敛到 5 级以内。


importmap 最后一公里

最后是构建后的模块引用路径的问题,浏览器识别 ESM 引用的时候是只支持相对路径,而不支持裸导入说明符的,也就是我们平时常写的 import React from 'react'import { Button } from 'antd'


有两种方案,一种是将带有构建 hash 的文件路径直接替换到 ES Module 的引入文件中,就像 Vite 的本地做法一样,但带来的问题是无法精准的更新有变更的依赖。如果 antd 更新了,那所有依赖 antd 的 js file 都需要更新。


第二种方案就是浏览器的标准:importmap,通过提前的声明,浏览器就能正确识别出 react、antd,一旦模块有版本更新只需要更新 importmap 就行了。


 

预加载优化

额外的,我们还借助 modulepreload 提前预加载最深层的子依赖,从而大幅改善加载时间,大约是 1.3x - 1.5x 的提升。



最终我们通过 npm 导出级别的依赖计算、ES Module 的 Treeshaking 、特定的依赖合并策略以及运行时的预加载优化和 importmap 真正将云凤蝶资产加载体系搬到了 ES Module 的生产环境。


未来设想

畅想未来的模块方案和前端构建方式。由于 ES Module 作为标准和 Treeshaking 的优势,未来社区上的 npm 包都会优先使用  ES Module,同时 Bundleless 甚至 NoBundle 的构建方式会有更多的场景,我们可以更好的去享受网络、浏览器标准升级带来优势。LowCode 和 ProCode 之前由于平台的天然隔离,我们只能借助 DSL 互转和微前端的方式去做混合研发,但无论这两种方式,对于依赖复用还是没有一个通用的方案,之后借助统一的 ESM 构建方式我们可以天然地解决依赖复用的问题。



以上就是我今天分享的所有内容,如果你也对文中提到的这些感兴趣,欢迎加入我们,一起打造更好的云凤蝶。

相关文章
|
弹性计算 数据可视化 关系型数据库
【最佳实践】Filebeat实现MySQL日志轻量化发送至Elasticsearch
在今天的文章中,我们来详细地描述如果使用Filebeat把MySQL的日志信息传输到Elasticsearch中。
4691 0
【最佳实践】Filebeat实现MySQL日志轻量化发送至Elasticsearch
|
11月前
|
人工智能 数据可视化 数据库
低代码平台:技术复杂性的系统简化
低代码平台通过模块化和自动化技术重新定义开发流程,简化应用构建。它支持“一键编程”和“快速迭代”,降低开发复杂度,提供敏捷开发能力。可视化开发、分布式协作、无缝部署等特性提高了整体协作效率。平台优化了五大核心引擎(SQL、功能、模板、图表、切面),提升开发灵活性与性能。此外,低代码平台还融合AI技术,提供智能代码生成、自动优化和故障排查等功能,进一步提高开发效率。插件生态覆盖多行业场景,支持实时数据处理、AI模型训练、图像处理等。开放架构结合微服务和开源框架,确保高性能与可扩展性。低代码平台正逐步成为企业技术创新的实用助手,助力快速响应市场需求。
280 22
|
11月前
|
人工智能 数据可视化 数据库
低代码平台:技术复杂性的系统简化
低代码平台通过模块化和自动化技术,简化了传统开发流程中的需求分析、代码开发、测试部署等环节,显著提高了开发效率和协作能力。其核心特性如“一键编程”、“快速迭代”降低了开发复杂度,提供了敏捷开发的能力,使企业能更快响应市场需求和技术变革。可视化开发、实时渲染、分布式协作支持及无缝部署等功能进一步优化了开发体验。平台内置的五大核心引擎(SQL、功能、模板、图表、切面)进行了系统性优化,提升了数据处理能力和开发灵活性。此外,低代码平台还融合了AI技术,提供了智能代码助手、自动优化和故障排查等功能,增强了开发效率和精度。
|
存储 关系型数据库 MySQL
MySQL周内训参照1、ER实体关系图与数据库模型图绘制
MySQL周内训参照1、ER实体关系图与数据库模型图绘制
656 1
|
人工智能 搜索推荐 大数据
智能食品生产:自动化与定制化的食品制造
【10月更文挑战第26天】本文探讨了智能食品生产中的自动化与定制化趋势。自动化技术在原料处理、加工制造、包装和质检等环节的应用,显著提高了生产效率和产品质量。智能化技术则通过物联网、大数据、云计算和人工智能等手段,实现了更高效、精准和灵活的生产,并能满足消费者的个性化需求。虽然面临高成本、技术维护和数据安全等挑战,但政府和企业共同努力,将推动食品行业的健康和可持续发展。
|
开发工具 git
GIT:如何合并已commit的信息并进行push操作
通过上述步骤,您可以有效地合并已提交的信息,并保持项目的提交历史整洁。记得在执行这些操作之前备份当前工作状态,以防万一。这样的做法不仅有助于项目维护,也能提升团队协作的效率。
828 5
|
存储 JavaScript 前端开发
js中的遍历方法比较:map、for...in、for...of、reduce和forEach的特点与适用场景
js中的遍历方法比较:map、for...in、for...of、reduce和forEach的特点与适用场景
732 0
|
前端开发 JavaScript UED
【专栏:HTML与CSS移动端开发篇】移动端触摸事件与手势识别
【4月更文挑战第30天】本文探讨了移动端触摸事件和手势识别在网页开发中的重要性。介绍了基础触摸事件如`touchstart`, `touchmove`, `touchend`, `touchcancel`及相关属性。文章列举了处理触摸事件的方法,包括单点触摸、多点触摸、滑动、长按、捏合缩放、旋转检测和事件代理。建议使用第三方库如Hammer.js简化手势处理,并分享了最佳实践,如避免意外触摸、提供视觉反馈、考虑性能和跨设备测试。理解并有效利用这些技术能提升用户交互体验。
626 7
|
开发框架 前端开发 Android开发
【Flutter 前端技术开发专栏】Flutter 与原生模块通信机制
【4月更文挑战第30天】本文探讨了Flutter作为跨平台开发框架与原生Android和iOS交互的必要性,主要通过方法调用和事件传递实现。文中详细介绍了Flutter与Android/iOS的通信方式,数据传输(包括基本和复杂类型),性能优化,错误处理以及实际应用案例。理解并掌握这一通信机制对开发高质量移动应用至关重要,未来有望随着技术发展得到进一步优化。
413 0
【Flutter 前端技术开发专栏】Flutter 与原生模块通信机制
|
JavaScript
vue 页面刷新、重置、更新页面所有数据
vue 页面刷新、重置、更新页面所有数据