flutter 专题三十六 Flutter动态化框架Thresh

一、前言

移动端技术栈自诞生以来,其双端开发成本和发布效率一直广受诟病。为了解决这些问题,前端跨端技术一直在不断尝试,希望能一次开发、多端运行并且能做到快速发布。期间经历了多个技术发展阶段。

第一阶段:以H5为代表,基于webview渲染

只需一次开发即可运行在双端,解决了开发效率低下的问题。但是webview存在严重的性能问题,用户的交互体验相比Native渲染有明显差距。

第二阶段:以RN和Weex为代表,前端技术栈开发,Native渲染

这些方案使用前端技术开发,最终映射到Native组件渲染,用户体验相比H5方案有了巨大的提升。但是这一阶段的方案同样存在不足。由于框架的渲染最终还是依赖双端Native组件,存在双端体验不一致性和平台兼容问题,极端情况下开发成本甚至超过双端Native开发。

第三阶段:Flutter,自绘引擎渲染

Google基于Skia渲染引擎,推出了Flutter跨平台框架,支持了Android/iOS/Web三个平台(尤其2.0的发布支持了全平台)。

基于自绘引擎,Flutter抹平了各个平台的差异,真正做到了一处开发,多端运行。业内对于Flutter彻底解决跨端开发的问题也寄予厚望。但是Flutter也并非完美,其动态能力不足,无法像H5、RN等技术一样快速发布。

为了解决动态能力不足的问题,满帮大前端团队从2019年开始对Flutter动态化能力进行探索,自研了动态化Flutter框架,在内部不断优化迭代,已上线20+页面,包括核心页面订单详情、货主货源详情、导航地图等等,并且于2020年底进行了开源。

二、Flutter动态化的思考

Thresh项目推出的初心是为了能提供一种基于Flutter的完全跨端动态化方案,性能能达到甚至优于React Native,再加上其多端渲染一致性以及即将推出的Google Fuchsia系统默认开发语言为Flutter,都表明Thresh未来将会充满想象力。

2.1、动态化常见方案

实现Flutter的动态化,通常需要考虑以下几点:

  • Flutter编译产物替换

Google原本打算在2019年推出Code Push方案,后来放弃了,主要两个原因:违反应用商店的规定和安全方面考虑;但目前android是可以通过产物替换来做到动态化,iOS端则无法做到。

  • 组件化搭建

通过Dart来定义部分核心通用组件,在平台下发已有的组件列表拼装的页面JSON,端上再通过解析渲染成页面。这种方案能满足轻交互场景,但只能支持有限动态性。

  • 自定义Dart转换+动态逻辑映射

通过自定义一套Dart规范以及通过转换器生成JSON来做到动态更新,性能损失小,但是逻辑动态性需要提前预埋,且前端开发同学需要一定的学习成本。

  • 自定义DSL+依赖JS引擎的动态执行

类似于RN/Weex,通过自定义动态化UI描述 + JS引擎的解释运行转换思路,最终构建成页面和执行动态逻辑。这个方案对于前端开发非常友好,零学习成本,但是由于在JS引擎运行,会有一些性能损耗。

2.2、Thresh的选择

满帮的实际使用场景,业务快速迭代,需要Android和iOS都要支持动态性,所以产物替换的思路不能完全解决问题。随后又考虑使用组件化思路,拼接多个业务组件虽然能搭建出页面,但是弊端也很明显,复杂交互逻辑时无法实现。另外自定义Dart描述UI方案虽然满足了动态更新的要求,但是逻辑动态性依旧不强,而且Dart开发对于前端开发同学有一定的学习成本。

最终,综合考虑了开发效率、学习成本、多端性能和一致性等因素,我们选择了自定义JS描述UI + JS引擎的解释运行转换思路,类React语法结构,开发语言使用JS/TS。

三、实现原理

3.1、构建Dart页面原理

在 Flutter 中描述视图组成的基本单位是 Widget,每一个 Widget 只包含当前部件的配置信息,它是一个轻量的、可被高效创建并销毁的数据结构。而许许多多的 Widgets 组合在一起,构建出了一个包含视图所有信息的 WidgetTree。之后 Flutter 会从 WidgetTree 中生成 ElementTree,再由 ElementTree 生成 RenderObjectTree。ElementTree 中的 Element 会同时持有其对应的 Widget 与 renderObject。


三棵树中,WidgetTree 会被频繁创建于销毁,但是 ElementTree 和 RenderObjectTree 只会在发生状态改变的时候才会改变,ElementTree 负责元素的更新与 diff,RenderObjectTree 则负责实际的布局与绘制。

核心思路是把 Flutter 的页面渲染逻辑中的三棵树中的第一棵树Widget,通过JS 来构造。这其中要完成JS与 Flutter 层完成基础组件映射,再通过JS引擎来生成UI描述,并传递给Dart层的 UIEngine,UIEngine 把UI描述转换为 Flutter 控件,最终渲染成页面。

Thresh框架完成了常用基础组件的定义与开发,能支撑95%以上业务场景的接入,语法定义规则支持React,对前端开发人员零成本接入。现支持的组件列表以及其部分属性如下 

3.1.1、Flutter初始化

Flutter 是由 main() 函数开始程序执行的,主要完成以下几个工作:

  • 建立与 Native 之间的通信渠道 MethodChannel 以保证所有的通信都能够被接收和发送;
  • 建立接收到消息时的所有处理方法的分发渠道,以保证所有合法的通信都能够在 Flutter 中被正确处理,同时通过 MethodChannel 向 JS 发送当前设备的媒介数据;
  • 注册拦截函数,以便在接收到渲染 JSON 数据后将 JSON 转换为 Widget;
  • 最后建立 Flutter App 的初始承载页面,该页面在接收到 JS 发送显示页面的消息之前将会一直处于等待状态;同时向 JS 发送 ready 消息,表示 Flutter 环境已准备完成,可以显示页面。
3.1.2、生成WidgetTree

依据 Flutter 中对 Widget 注册的所有拦截函数,JS 中会提供一套与之相对应的原子组件,以便在两种不同的 DSL 之间进行组件的互相转换。在 JS 中 UI 的构建通过 JSX 实现,借鉴了 React 的写法。

通过在 JS 中构建 UI 的描述层,再将 UI 描述转换为 JSON 格式字符串,经由 Native 发送到 Flutter ,由 Flutter 对 JSON 字符串进行解析后创建对应的 WidgetTree 并执行后续渲染操作。

3.1.3、JS与Flutter通信

在 JS 代码执行之前,Native 会向 JS 代码的执行环境中注册两个通信方法,一个为 JS 向 Flutter 传递消息的通道,另一个则是 Flutter 向 JS 传递消息的通道。通过这两个通道,就可以实现所有数据在 JS 与 Flutter 之间的流转(后面3.2章节会详细介绍)。

3.1.4、构建Flutter页面

对于当完成所有链路的数据转换后就会拿到ModelTree & WidgetTree,ModelTree会持有并缓存WidgetTree,最终构建一个Widget页面并渲染显示。页面构建渲染流程主要是:

Flutter 接收到渲染 JSON 数据后,会通过递归遍历的方式从最底层开始,将每一个独立的渲染数据节点解析为 Model 对象。Model 将会持有所有的渲染数据,同时会关联自己的父节点;同时 Model 会携带所有的渲染数据,通过 Widget 拦截函数生成其对应的 Widget 实例,并持有该 Widget 实例。

比如, JS 中的 <Container /> 组件在 Flutter 中经过拦截函数将会被创建为一个名叫 DFContainer 的 widget 实例。DFContainer 等 widgets 是使用 Flutter 提供的原子组件封装的一套自定义组件。

当通过 model 创建 Widget 时,如果发现其 isStateful = true ,则会在该 Widget 实例外层包裹一个 StatefulWidget,同时让 model 持有该 StatefulWidget 及其 state,以便之后进行更新操作。也就是说,如果一个 model 具有 isStateful = true ,则其会同时拥有 Widget & statefulWidget & state的特性。

在遍历过程中,原先的 JSON 数据会被转换为两个树 —— ModelTree & WidgetTree。其中 WidgetTree 中的每个节点都会被 ModelTree 中对应的节点所持有。

对于首次显示的页面来说,会使用被创建的 WidgetTree 直接替换初始化时创建的承载页面的内容;而非首页则会直接通过 Navigator.push(),使用 WidgetTree 创建并显示一个新页面。整个流程如下图:

3.2、通信机制

JS 与 Flutter 是依赖于 Native 又完全独立的两端:JS 中的数据运算与流转不会直接影响到 Flutter 页面的渲染;Flutter 的渲染过程也不会阻塞 JS 的代码执行。

为了让完全独立的两者产生联系,我们找到了一个既能与 JS 产生联系,又能与 Flutter 传递消息的媒介 —— Native. 通过将一个消息从一端传递给 Native,再由 Native 完整传递给另一端,就实现了 JS 与 Flutter 之间的通信。

动态化Flutter 框架主要由这三部分构成,每一部分都处理不同的逻辑和绑定事件通信来更新渲染页面、事件响应,其核心渲染通信流程:Flutter ⇋ Native ⇋ JS 。

3.2.1、搭建三端通信链路

Flutter 初始化时,Flutter会与Native通过 methodChannel 建立通信关系,methodChannel 是一条双向通信的链路,既可以在 Flutter 中接收到 Native 的消息,也可以主动向 Native 发出消息。

同时,Native 在执行 JS 代码之前会向 JS 的 context 中注入一个方法,我们将这个方法命名为 methodChannel_js_call_flutter,用来使 JS 能够向 Flutter 传递消息。因此,在 Flutter 动态化中的通信链路如下图。

从上面两个链路中会发现,JS到Native的消息是可以顺利到达 Flutter;但是Flutter到JS没有直接的的通讯链路,在 Native 中断掉了。为了解决这个问题,JS 会在 context 中暴露一个名为 methodChannel_flutter_call_js 的方法,该方法的参数即为消息内容,这样 Native 就能够直接调用该方法将消息传递到 JS。

3.2.2、“半双工”通信过程

在Thresh中,几乎所有的三端通信需求都是“半双工”的。此处的“半双工”指的是,当一方作为消息传递方时,无法通过当前传递消息的通道获得消息接受方的反馈。这就表示当传递方发送出一条消息后就会结束自己的通信行为,它们不需要去关心自己是否会得到反馈,而实际上也不会有任何反馈。

基于以上情况,Thresh中的所有通信链路都会使用这种模式进行通信:消息传递方只需要传递数据而不需要关心回调,消息接收方只需要处理数据而不需要返回处理结果。这种模式对于跨越三端的通信来说更便于管理和约束,也使得 Native 成为了一个完全的数据中转站,否则 Native 除了需要传送数据外,还需要处理结果的反馈工作。即【数据传递方】 -> 【数据中转方】 -> 【数据接收方】是单向的。


但是并不是所有的通信都不需要反馈,例如与 Native 通信的双端通信链路 bridge,在向 Native 发出通信消息后需要获得 Native 的处理结果。对于这种情况,简单粗暴的单向通信将无法直接满足需求。但如果换成携带回调的“全双工”通信,从而能够在同一个通信通道上实现结果的接收,将会破坏原有的通信模式,也为通信的管理增加了难度。

为了解决在“半双工”通信模式上的通信反馈问题,我们通过在传递方为每一个需要反馈的通信加上标识符,再将反馈处理方法通过标识符缓存;当接收方处理完成后,携带标识符通过另一个通信通道将处理结果作为一个新的消息传递给原本的传递方后(在这个新的通道中,原本的数据传递和接收方将会互换身份),传递方会根据标识符在缓存中查找到处理方法并执行处理逻辑。

3.2.3、建立可靠的消息通道

JS 与 Flutter 的通信是 Flutter 动态化的基石,而首次通信的成功与否又是通信能否成功建立的首要条件。

由于所有的跨三端通信都是“半双工”的,而 JS 与 Flutter 的环境准备又各自完全独立,这也就导致如果任一方环境准备完成前,另一方就发送了消息,这就会出现环境未完成的一方无法接收到消息的情况,从而影响后面所有的通信,导致通信中断或错乱。

为了解决这种情况,JS 与 Flutter 中采取了以下策略来保证首次通信的顺利执行(以下以 A / B 代指 JS 与 Flutter 中的任一方):

  • A 环境准备完成后会立即向 B 发送通知;
  • 如果 B 已准备好则会立即回复一条通知,A 收到回复通知后标记双方环境已建立,可进行后续的通信;
  • 如果 B 未准备好,则 A 将不会收到任何回复,直到 B 准备好,此时A / B 身份互换,会重新回到步骤 1。

3.3、组件更新与事件传递

3.3.1、JS事件触发与传递

在将 JS 中的事件函数转换为 id 后,这个 id 也会与节点所属页面名称、节点 id 一起被携带到 Flutter 中,最终这三个信息会被包装为一个 Flutter 中的事件函数。

当在 Flutter 中触发事件时,首先会触发这个函数,该函数会向 JS 发送一条携带了页面名称、节点 id、事件 id 以及事件参数的消息。JS 接收到该消息后,首先会根据页面名称与节点 id 查找到触发了事件的节点,接着通过事件 id 在节点事件池中查找到对应的事件,传入参数并执行该事件。

3.3.2、JS组件更新

触发事件的目的大部分都是为了更新页面上的内容,在 JS 中,组件更新的基本单位是自定义组件。

当一个自定义组件触发 setState() 后,会将该组件推入更新队列中等待更新。在节点进入队列之前会进行去重,从队列中进入第一个组件开始后的 16ms,队列将执行更新操作。在这 16ms 内进入队列中的其他待更新组件将会一同触发更新。

在实际进行更新操作前,会先对队列中的元素进行父节点的去重,即:依次获取所有待更新节点,同时向上获取该节点的父节点,如果其父节点存在于当前队列中,则从队列中移除该待更新节点,不存在则保留。这样做是因为只要队列中存在了父组件,则子组件就一定会被更新;其目的是为了执行最少次数操作,但实现尽可能多组件的更新。

组件的更新借鉴了 React 的组件更新 diff 算法,但是由于引入了 Flutter StatefulWidget 和 StatelessWidget 的概念,因此相比 React 的 diff 算法,thresh.js 的 diff 算法是粗粒度的。

两者相同的地方在于:都会对每一个节点进行对比,以保证每一个节点的状态都正确,最终被正确更新。

不同点在于:React 除了会对同类型节点进行属性和状态的合并外,也会将新创建或被删除的节点在旧节点数组中进行插入或删除操作,操作和更新的基本单位是原子组件;而 thresh.js 只会关注那些更新前后依然保留的同类型节点,在完成属性与状态的合并后,会直接抛弃旧节点,保留新节点,最终新节点将替换待更新自定义组件中的旧节点,并使用更新后的自定义组件的数据向 Flutter 发出更新消息——更新的基本单位是自定义组件。

3.3.3、Flutter组件更新

JS 发送的更新消息有两部分组成:需要被更新的页面名称、更新节点 id 以及更新节点的 JSON 数据。当 Flutter 收到 JS 发送的更新消息后,首先会重复 json 转换为 Model 步骤,创建出 ModelTree & WidgetTree. 之后通过更新的页面名称和节点 id 在缓存中查找到需要被更新的 Model。

由于更新以 JS 中的自定义组件为最小单位,而每个自定义组件在 Flutter 中都会被创建为 StatefulWidget,因此在获取到新旧两个 Model 后会进行如下操作:

  1. 将 newModel 的渲染数据、子节点 models 及其所持有的 newWidget 合并到 oldModel;
  2. 通过 oldModel 所持有的 state 将 statefulWidget 中所包裹的 oldWidget 更新为 newWidget;
  3. 通过 state 完成组件更新操作后,Flutter 会对被更新的组件进行 diff 与重新渲染,以保证页面能够显示新的内容。

四、工程化

4.1、Thresh架构

Thresh的整体工程化架构如下图:

如上图所示,自下而上,CI/CD + 基础服务 + 监控上报等支撑了Thresh业务,最上面为架构图。

  • X-RAY为公司自研的生产发布平台,支持Bundle包的构建下发以及运维。
  • 顶部是整体Thresh的架构流程图,包含 页面开发、DSL 的转换、通信等等,用于构建页面与逻辑。

Thresh动态化跨平台方案虽然在设计上有高性能渲染、一致性、开发效率、前端同学零成本接入等优势,但是考虑未来多业务方接入以及提升开发调试效率,推进了Thresh周边基础设施建设,下面简单介绍开发期、调试期、发布期。

  • 开发期
    支持plugin方式接入,业务方接入提供一套模板工程,能快速进入业务开发;另外Thresh兼容TS,能较低成本的让前端开发融入。

  • 调试期
    通过支持HotReload模式,秒级编译,极大的提升开发和调试效率,另外提供调试面板 + 动态调试能力也能极大地辅助提高调试效率。

  • 发布期
    依赖满帮自研的X-RAY灰度发布系统,具备分钟级别动态发版能力,能快速支撑业务和问题修复。

4.3、Thresh开发集成

Thresh的开发集成形成了一整套流程,涵盖三方集成、多业务模块接入、开发调试等等,其中涉及细节比较多,这个在开源仓库里面有详细介绍。

至此,Thresh的架构设计和开发集成能力都基本完成,相比于其他动态化跨平台开发框架,Thresh有如下优势:

  • 基于JS的自定义DSL,扩展性强,学习成本低
  • 多端一致性,拥有统一的自渲染引擎skia,较好的跨端兼容性适配
  • 支持Hot Reload,便于开发调试,秒级编译
  • 支持组件级别UI刷新,极佳的体验性
  • 提供开发期调试面板,方便开发

五、结束语

通过 JS 构建 Flutter 应用程序的基本原理并不复杂,主要是 JS 中的数据处理、Flutter 中的数据转换,以及实现数据在 JS 和 Flutter 中的流转通道。这类方案大提都类似,比如MXFlutter、美团外卖 MTFlutter。不过,这种方案目前看来还是比较鸡肋,偏离了Flutter跨平台涉及的初衷。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/65370.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

NodeJs 箭头函数:`()=>{}` 和 `()=>()` 的区别与使用场景

在 JavaScript 中&#xff0c;箭头函数&#xff08;Arrow Function&#xff09;是一种简洁的函数写法&#xff0c;它不仅可以减少代码量&#xff0c;还能避免 this 绑定的问题。然而&#xff0c;箭头函数有两种常见的写法&#xff1a;()>{} 和 ()>()。这两种写法虽然看起…

缓存-Redis-缓存更新策略-主动更新策略-Cache Aside Pattern(全面 易理解)

**Cache-Aside Pattern&#xff08;旁路缓存模式&#xff09;**是一种广泛应用于缓存管理的设计模式&#xff0c;尤其在使用 Redis 作为缓存层时尤为常见。该模式通过在应用程序与缓存之间引入一个旁路&#xff0c;确保数据的一致性和高效性。本文将在之前讨论的 Redis 主动更新…

python制作翻译软件

本文复刻此教程&#xff1a;制作属于自己的翻译软件-很简单【Python】_哔哩哔哩_bilibili 一、明确需求&#xff08;以搜狗翻译为例&#xff09; &#xff08;1&#xff09;网址&#xff1a;https://fanyi.sogou.com/text &#xff08;2&#xff09; 数据&#xff1a;翻译内容…

【C++】20.二叉搜索树

文章目录 1. 二叉搜索树的概念2. 二叉搜索树的性能分析3. 二叉搜索树的插入4. 二叉搜索树的查找5. 二叉搜索树的删除6. 二叉搜索树的实现代码7. 二叉搜索树key和key/value使用场景7.1 key搜索场景&#xff1a;7.2 key/value搜索场景&#xff1a;7.3 主要区别&#xff1a;7.4 ke…

vue3探索——使用ref与$parent实现父子组件间通信

在vue3中&#xff0c;可以使用vue3的API defineExpose()函数结合ref或者$parent&#xff0c;实现父子组件数据的传递。 子组件向父组件传递数据defineExpose()和ref 子组件&#xff1a;通过defineExpose() 函数&#xff0c;向外暴露响应式数据或者方法 // src/components/son…

Opencv图片的旋转和图片的模板匹配

图片的旋转和图片的模板匹配 目录 图片的旋转和图片的模板匹配1 图片的旋转1.1 numpy旋转1.1.1 函数1.1.2 测试 1.2 opencv旋转1.2.1 函数1.2.2 测试 2 图片的模板匹配2.1 函数2.2 实际测试 1 图片的旋转 1.1 numpy旋转 1.1.1 函数 np.rot90(kl,k1)&#xff0c;k1逆时针旋转9…

重温设计模式--13、策略模式

策略模式介绍 文章目录 策略模式介绍C 代码示例 策略模式是一种行为设计模式&#xff0c;它允许在运行时选择算法的行为。该模式将算法的定义和使用分离开来&#xff0c;使得算法可以独立于使用它的客户端而变化&#xff0c;提高了代码的灵活性和可维护性。 其主要包含以下几个…

计算机基础知识复习1.5

标记-清除算法&#xff1a;标记-清除分为标记 和清除 两个阶段&#xff0c;首先通过可达性分析&#xff0c;标记出所有需要回收的对象&#xff0c;然后统一回收所有被标记的对象。 复制算法&#xff1a;为了解决碎片空间的问题&#xff0c;出现了复制算法 将内存分成两块&…

SQL Server 中的覆盖索引

1. 覆盖索引的工作原理 当查询只涉及索引中已经包含的列时&#xff0c;SQL Server 可以直接使用索引来返回查询结果&#xff0c;而不需要回表到数据页去检索实际的数据行。覆盖索引因此能够显著减少 I/O 操作&#xff0c;提高查询效率。 例如&#xff0c;假设有一个表 Employ…

Golang开发-案例整理汇总

前言 CSDN的文章缺少一个索引所有文章分类的地方,所以手动创建这么一个文章汇总的地方,方便查找。Golang开发经典案例汇总 GoangWeb开发 GolangWeb开发- net/http模块 GolangWeb开发-好用的HTTP客户端httplib(beego) GolangWeb开发- Gin不使用Nginx部署Vue项目 Golang并发开…

交叉编译的核心原理与核心概念

什么是交叉编译&#xff1f; 交叉编译&#xff08;Cross Compilation&#xff09;是一种在一种计算机体系结构或操作系统&#xff08;主机&#xff0c;Host&#xff09;上生成另一种计算机体系结构或操作系统&#xff08;目标&#xff0c;Target&#xff09;上的可执行文件的过…

vue-codemirror定位光标位置并在光标处插入信息

业务场景:在代码编辑器外点击按钮,向代码编辑器内的光标处新增一条拼接好的信息。 getCursor方法: 官方文档: doc.getCursor(?start: string) → {line, ch} Retrieve one end of the primary selection. start is an optional string indicating which end of the select…

【GOOD】A Survey of Deep Graph Learning under Distribution Shifts

深度图学习在分布偏移下的综述&#xff1a;从图的分布外泛化到自适应 Northwestern University, USA Repository Abstract 图上的分布变化——训练和使用图机器学习模型之间的数据分布差异——在现实世界中普遍存在&#xff0c;并且通常不可避免。这些变化可能会严重恶化模…

『SQLite』解释执行(Explain)

摘要&#xff1a;本节主要讲解SQL的解释执行&#xff1a;Explain。 在 sqlite 语句之前&#xff0c;可以使用 “EXPLAIN” 关键字或 “EXPLAIN QUERY PLAN” 短语&#xff0c;用于描述表查询的细节。 基本语法 EXPLAIN 语法&#xff1a; EXPLAIN [SQLite Query]EXPLAIN QUER…

(一)使用 WebGL 绘制一个简单的点和原理解析

使用 WebGL 绘制一个简单的点&#xff0c;我们需要通过 WebGL 的管线来进行一系列的步骤。以下是实现的详细步骤和原理解析&#xff1a; WebGL 绘制点的基本步骤 初始化 WebGL 上下文 首先&#xff0c;我们需要获取 WebGL 上下文&#xff0c;这样才能进行所有的绘图操作。通常…

Vue路由跳转报错

说明&#xff1a;使用 Vue 的router.replace/push&#xff0c;若跳转到当前路由&#xff0c;控制台会报错如下&#xff1a;NavigationDuplicated: Avoided redundant navigation to current location 原因&#xff1a;Vue-router在3.1之后把$router.push()方法改为了Promise。所…

【Axure高保真原型】环形进度条(开始暂停效果)

今天和大家分享环形进度条&#xff08;开始暂停效果&#xff09;的原型模版&#xff0c;效果包括&#xff1a; 点击开始按钮&#xff0c;可以环形进度条开始读取&#xff0c;中部百分比显示环形的读取进度&#xff1b; 在读取过程中&#xff0c;点击暂停按钮&#xff0c;可以随…

Euler 21.10(华为欧拉)安装oracle19c-RAC

1. Euler 21.10安装oracle19c-RAC 1.1. 环境规划 1.1.1. 主机规划 hostname IP 实例名 hfdb90 192.168.40.90 hfdb1 hfdb91 192.168.40.90 hfdb2 系统版本 BigCloud Enterprise Linux For Euler 21.10 (GNU/Linux 4.19.90-2107.6.0.0100.oe1.bclinux.x86_64 x86_6…

【python】matplotlib(radar chart)

文章目录 1、功能描述和原理介绍2、代码实现3、效果展示4、完整代码5、多个雷达图绘制在一张图上6、参考 1、功能描述和原理介绍 基于 matplotlib 实现雷达图的绘制 一、雷达图的基本概念 雷达图&#xff08;Radar Chart&#xff09;&#xff0c;也被称为蛛网图或星型图&…

(三)通过WebGL绘制一个简单的三角形来理解渲染管线

理解 WebGL 绘图原理的关键是了解它的渲染管线。WebGL 渲染管线实际上是由多个阶段组成的&#xff0c;每个阶段都有特定的任务&#xff0c;最终输出的是屏幕上的图像。为了让你能轻松理解这些原理&#xff0c;我将通过一个简单的例子来详细解释。 绘制一个简单的三角形 我们将…