北海(Kraken)构建大前端混合渲染技术体系 —— Web 与 Flutter Widget 混合渲染方案

简介: 组件(模块)封装与开发可以给前端业务开发的过程带来非常大的研发效能的提升,各个业务域的开发者会定制开发许多符合自己业务场景的基础组件(模块)沉淀一套快速复用的物料体系,以保证业务开发的研发效能。同样,在各个 Flutter 团队,也有大量的 Flutter Widget 的物料,以及各种基于 Flutter 场景做的性能优化。在大前端的视角下,我们期望在端内拥有 Web 开发的研发效能以及动态性的同时,也期望通过一些 Native 的优化手段让应用拥有媲美原生的体验与性能。

image.png



背景


组件(模块)封装与开发可以给前端业务开发的过程带来非常大的研发效能的提升,各个业务域的开发者会定制开发许多符合自己业务场景的基础组件(模块)沉淀一套快速复用的物料体系,以保证业务开发的研发效能。同样,在各个 Flutter 团队,也有大量的 Flutter Widget 的物料,以及各种基于 Flutter 场景做的性能优化。在大前端的视角下,我们期望在端内拥有 Web 开发的研发效能以及动态性的同时,也期望通过一些 Native 的优化手段让应用拥有媲美原生的体验与性能。

北海(Kraken) 作为一款高性能、易扩展的 Web 标准渲染引擎,通过上层实现 W3C 标准以提供前端开发者较低的学习成本以及快速复用已有的前端研发体系的能力。众所周知,除了根据前端框架做一些业务组件开发,在前端有一套为容器(浏览器)标准的扩展定制能力的技术 —— Web Components。Web Component 更多地提供开发者创建可重用的定制 element 的能力,而它本身基于 Web 技术栈还是存在一些限制,比如说无法直接管理 Render Object 达到在长列表下一个动态回收能力等。作为一个 UI 能力的复用以及对基础原子组件能力的封装,Web Components 是完全够用的,但是对于在端内在节点容器上提供一些默认的 Native 级别的优化,Web Components 显得并没有那么突出,这里面其实还可以有更大的想象空间。

那么,Kraken 本身是一款基于 Flutter 技术开发的 Web 渲染引擎,是否可以复用 Flutter 生态,将 Flutter Widget 能力融合进 Web 渲染能力中呢?这样可以复用大量 Widget 的业务组件,采用同一条渲染管线高效渲染的同时,也在端内提供了更多客户端(Widget)的性能优化手段,以形成一套“动态化”、“高性能”、“易扩展”的融合 Web 与 Flutter 生态的大前端技术体系。

image.png



用 Flutter Widget 实现一个 Custom Element



要把 Flutter 生态融合进 Kraken 中去,我们期望能让开发者把 Flutter Widget 作为一种 Custom Element 接入到 Kraken 体系中。

首先开发者需要继承 WidgetElement 实现一个  FlutterContainerWidgetElement ,并通过 defineCustomElement 注册到 Kraken 中去,使其成为一个 Custom Element。在 FlutterContainerWidgetElement 的 build 方法中,每当该节点的 attribute 或者子节点变化(比如 appendChild),该节点就会被标脏,依赖 Flutter 的生命周期,该节点最终会重新被 build,Kraken 在此时会把该节点最新的 properties 以及 children (对应到前端就是 setAttribute 以及 dom 节点)传递过来,Widget 根据这些参数完成一次 build,最终更新到界面上。

下面是一个将 Flutter 的 Column Widget 实现一个 ColumnWidgetElement 以提供给前端一个列布局容器的 Demo。

void main() {
  // 定义 tagname 以及 注册 Custom Element。
  Kraken.defineCustomElement('flutter-column', (context) {
    return ColumnWidgetElement(context);
  });
}
// 继承 WidgetElement 实现 Custom Element,在 build 方法中返回对应 Widiget。
class ColumnWidgetElement extends WidgetElement {
  ColumnWidgetElement(EventTargetContext? context) :
        super(context);
  // Build 方法会默认将前端设置给 Element 的 attribute(properties)以及该节点的 children(自动转换成一个 Widget 的 List)传递过来,这边只需要返回一个开发者自己实现的 Widget 即可。
  @override
  Widget build(BuildContext context, Map<String, dynamic> properties, List<Widget> children) {
    return Column(
      // 可以通过前端设置的 attribute 来触发 Widget 的逻辑与配置。
      textDirection: properties['direction'] ? TextDirection.ltr : TextDirection.ltr,
      // 前端 appendchild 的所有子节点会包装成 Widget List,直接使用即可。
      children: children,
    );
  }
}

相应的在 JavaScript 中,前端开发者可以直接调用生成 flutter-column 即可调用该 Widget 能力。

const column = document.createElement('flutter-column');
document.body.appendChild(column);
for (let i = 0; i < 10; i++) {
    column.appendChild(document.createTextNode(i));
}


技术原理



那么,要实现这样一套混合渲染的方案的前提,得搞明白 Kraken 以及 Flutter Widget 的渲染流程。

image.png

注:Render Object Tree 中,K 代表 Kraken 生成的 Render Object,F 则代表 Flutter Widget 生成的 Render Object(下同)。

Kraken 渲染流程跟 Web 非常类似,经典的三棵树——CSSOM Tree、DOM Tree 以及 Render Object Tree,与之相对应的 Flutter 渲染流程也有经典的三棵树—— Widget Tree、Element Tree 以及 RenderObject Tree。当最终生成 RenderObject Tree 以后,后续的合成光栅化上屏操作是一致的。对于 Kraken 技术原理不熟悉的小伙伴,可以通过笔者的另一篇文章——《深入解析基于 Flutter 的 Web 渲染引擎「北海 Kraken 」技术原理》(https://zhuanlan.zhihu.com/p/401698292) 来了解 Kraken 的实现思路以及更多技术原理细节。

不难发现,在上述的渲染流程中最终生成的 Render Object Tree 里 Kraken 生成的 Render Object 与 Flutter Widget 生成的 Render Object 是完全独立的两棵子树,无法混合成如下所述的互相嵌套:

image.png

我们知道,最终影响渲染的只是 Render Object Tree,而上层的 DOM(或 Flutter Element)在各自的生命周期以及 API 中承担了不同的角色,它们提供的是对不同的开发者生态(Web 生态以及 Flutter 生态)的支持。如果强行把 Widget 或者 Flutter Element 侵入到 Web 标准的 DOM 中去,会导致各种差异难以抹平,长久看是无法持续维护与迭代的。

所以我们期望通过 Adapter 来适配两套标准,最终通过各自的生命周期的桥接来组装出最终的 Render Object Tree,最终达到混合渲染的目标。基于这个设想,我们可以得到如下的渲染流程:

image.png

可以看到,这里面核心就是需要处理四棵树(DOM、Widget、Flutter Element 以及 Render Object),其中 CSS 是依赖与 DOM 上的,不需要做额外的处理。上图中通过 Adapter 完成 DOM 与 Widget 以及 Flutter Element 之间的互相绑定与转换,最终通过各自生命周期的桥接将当前节点产生的 Render Object(无论是 DOM 产生的还是 Flutter Element 产生的)直接挂在到父亲节点的 Render Object 上,形成最终混合在一起的 Render Object Tree。

实际上这四棵树就会产生如下的一个对应关系:

image.png

乍一看四棵树在不同情况下排列组合成了一堆非常复杂的树形结构,实际上确实非常复杂。由于需要支持 Flutter Widget 作为一个 Custom Element 存在 Web 的渲染管线中,那么无论是互相嵌套还是插入或者被插入到普通 DOM 节点上,都需要对应的支持,排列组合一下就是下面这些情况:

  • Element 作为 Container 下 append 一层 Flutter Widget。
  • Flutter Widget 作为 Container 下 append Element(Text Node)。
  • Flutter Widget 作为 Container 下 append  Flutter Widget。
  • Flutter Widget 作为 Container 下 append  Element 作为子 Container,  Element 继续 append Flutter Widget 。
  • Element 作为 Container 下 append Element(Text Node)。

上图的四棵树的互相对应关系其实就包含了上述的几种情况,我分别从头开始梳理一下。

image.png

Kraken 本身是一个 Widget ,它是可以被嵌套在其他 Flutter Widget 的内部的,所以在 Kraken 的顶层会有一个对应的 rootFlutterElement ,在 Kraken Widget 的生命周期 mount 中,会将该 Flutter Element 的指针存储起来,以供后续使用(后面的 Flutter Element 需要挂载在 Kraken 的根节点上)。此外,初始化过程会创建出大家熟悉的 Window、Document、Body 以及 Head 等节点,同时创建出对应的 Render Object。

image.png

接着是中间一部分,这里展示了一个 Flutter Widget 作为容器(下面继续插入 DOM 子节点)以及普通 DOM 在 Kraken 内部的一个渲染流程。首先是普通的 DOM,以最右侧黄色树的 DIV Element 以及 TextNode 为例,它们插入到 BODY 节点上就是默认的 WEB 渲染流程,会生成对应的 RenderFlowLayout 以及 RenderTextBox、RenderParagraph 作为对应的 Render Object 插入到 Render Object  Tree 中。当一个 Flutter Widget (WidgetElement)作为节点插入到 DOM 树中时,会有一个 WidgetElement(继承自 dom.Element)作为 DOM 结构插入到 DOM 树中去。那么这个以 Flutter Widget 作为实现的 DOM 节点,是如何驱动 Flutter Widget 产生 Flutter Element 以及对应的 Render Object 并挂在到对应的父节点的 Render Object 上的呢?

首先,WidgetElement 类在初始化时会创建一个 Stateful 的 Widget——_KrakenAdapterWidget,当一个 DOM 节点的 attribute 或者 通过 appendChild 等操作插入(或删除)一个子节点时候,会需要通过触发 build 将 Widget 标脏,使该 Widget 可以通过 Flutter 的生命周期重新构建输出。同时,该 Adapter 也会对所有子节点进行处理,如果子节点是一个 Flutter Widget,则直接通过 build 方法构建出对应的 Widget,如果子节点是一个普通的 DOM,则会通过 KrakenElementToWidgetAdaptor 来包装一下该节点,已桥接对应的生命周期以及 Render Object(下面会讲到普通 DOM 如何转换成一个 Flutter Widget)。

image.png

我们知道, DOM 最终被被插入 DOM Tree 后会将产生的 Render Object 也插入到 Render Object Tree,如果 DOM 节点是一个 WidgetElement(Flutter Widget),那么就需要使用 Flutter Widget 最终生成的 Render Object。所以 override 了 didAttachRendere 生命周期,内部调用了 _attachWidget,通过它将 Flutter Widget 最终产生的 Render Obejct 给 attach 到 Render Object Tree 上。

image.png

最后来看看上文已经提到过的,一个普通 DOM 元素是如何融入到 Flutter Element 与 Widget 的渲染流程中去的。以 DIV Element 为例,在上文提到的 convertNodeListToWidgetList 中,会将一个普通 DOM 元素转化成 KrakenElementToWidgetAdaptor 这个 Widget,该 Widget 的 createElement 会生成对应的 Flutter Element, 所以会被 override 掉,并生成 KrakenElementToFlutterElementAdaptor 这个 Flutter Element。同样的,该 Widget 我们并不希望它会按照 Widget 流程产生一个 Render Object,而是直接用上述的 DIV Element 产生的 Render Object, 所以会通过 createRenderObject 方法来直接返回 DIV Element 产生的 Render Object。

同样,KrakenElementToFlutterElementAdaptor 这个 Flutter Element,会在 mount 以及 umount 生命周期中触发 createRenderer 等方法,用 Flutter 的生命周期钩子去保证 Kraken DOM 的一些流程的调用或者资源的释放。

基于以上原理,Kraken 实现了 Flutter Widget 作为一个 Custom Element 嵌入到 Kraken 中的任何地方。以 Demo 的 JS 代码为例渲染 已注册到 Kraken 内部的 Flutter Widget,Flutter Widget 无论作为容器还是一个子节点,都可以呈现在 Kraken 中,用 JS 来动态修改。


进阶版:更多复杂的场景


用 Flutter Widget 优化瀑布流场景性能

笔者以业务中常见的一种形态——瀑布流场景为例,找了一个社区的瀑布流 Widget 组件——waterfall_flow 以及一个下拉触底刷新的 Widget 组件—— flutter_easyrefresh,我们期望把它作为一个瀑布流容器集成到 Kraken 的渲染流程中,让它能够在 JavaScript 中被当作一个 Element 调用。同时,也期望它内部提供的一些动态 Render Object 的回收能力,保证在长列表下有一个滚动流畅以稳定的及内存的表现,而这些只需要开发这个 Flutter Widget 的同学去实现,前端同学对于这部分优化能力是无开发以及理解成本的,对于前端同学来说,就像使用一个 npm 包一样简单,并且能够让性能做到容器级别的优化。

void main() {
  Kraken.defineCustomElement('flutter-container', (context) {
    return EasyRefreshWidgetElement(context);
  });
}
class EasyRefreshWidgetElement extends WidgetElement {
  EasyRefreshWidgetElement(EventTargetContext? context) :
        super(context, defaultStyle: { 'height': '100vh', 'display': 'block' });
  @override
  Widget build(BuildContext context, Map<String, dynamic> properties, List<Widget> children) {
    return EasyRefresh(
      child: WaterfallFlow.builder(
        // 所有 Web 标准的节点可以传入到 Widget 中。
        itemCount: children.length,
        itemBuilder: (BuildContext context, int index) => children[index],
        padding: EdgeInsets.all(5.0),
        gridDelegate: SliverWaterfallFlowDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          crossAxisSpacing: 5.0,
          mainAxisSpacing: 5.0,
        ),
      ),
      // 通过 dispatchEvent 可以像 element 节点抛出符合 Web 标准的事件,前端通过 addEventListener 监听。
      onRefresh: () async => dispatchEvent(Event('refresh')),
      onLoad: () async => dispatchEvent(Event('load')),
    );
  }
}

此时,由于该 Custom Element 已经被注册到 Kraken 中去,在前端只需要通过 web 标准的 createElement 即可创建该节点。

const flutterContainer = document.createElement('flutter-container');

以及该节点可以通过 addEventListener 来监听 Widget 组件抛出的 Custom Event。

flutterContainer.addEventListener('refresh', () => {});

最终渲染出来的内容如下,可以看到内部子节点的 Web 节点可以直接使用瀑布流 Widget 的布局能力。同时,由于该 Widget 自带的动态 Render Object 回收能力,可以使得子节点在滚动时候动态回收,保证流畅地滚动,并且内存不会有明显的增量。而这一切对于前端开发者是没有任何额外的接入以及理解成本的,Flutter Widget 接入 Web 体系中会完全按照 Web 标准呈现给前端。同样,对于 Flutter 开发者,依旧可以控制熟悉的三棵树——Widget、Flutter Element 以及 Render Object,以提供一些端上的增强能力。

image.png


最后


Kraken 在 follow 了 W3C 标准的同时,也将 Flutter 的渲染能力融合进整个体系,让 Flutter 生态与 Web 生态融合渲染,成为一个融合渲染的大前端技术体系,互相取之所长,补其所短。让 Web 前端生态提供的表现力、动态性以及 Web 生态使业务可以快速开发迭代,满足大部分变化的业务。同时也让 Native 的同学可以提供给前端开发者多样化的 UI 渲染能力,以及将更多的性能优化手段运用到整个体系中,使优化的上限更高,体验更好。

同时,基于 Kraken “易扩展”这个点,开发者可以用非常低的定制成本根据自己的业务域开发一款深度定制的 Web 渲染引擎。复用已有的基建以及生态,给端内的体验带来更多的体验升级以及可能性。

最后,Kraken 的所有代码都已经开源,Kraken 提供了开放的 TSC 机制期望所有开发者可以平等地交流以及决策,使 Kraken 可以更好地发展,也欢迎更多的开发者一起来共建 Kraken。

Kraken Github:https://github.com/openkraken/kraken

Kraken 官网:https://openkraken.com/

相关文章
|
2月前
|
前端开发 JavaScript 定位技术
一、前端高德地图注册、项目中引入、渲染标记(Marker)and覆盖物(Circle)
文章介绍了如何在前端项目中注册并使用高德地图API,包括注册高德开放平台账号、引入高德地图到项目、以及如何在地图上渲染标记(Marker)和覆盖物(Circle)。
84 1
|
16天前
|
前端开发 数据可视化 搜索推荐
深入剖析极态云优雅的前端框架设计方案(上)
最近在体验极态云,这款低代码软件开发产品,发现其前端框架设计方案很优雅很强大! 在接下来的学习过程中,我将持续输出自己对极态云前端框架设计方案的深入理解,包括具体的使用技巧、优势分析以及可能的应用场景等方面的内容,希望能为大家提供有价值的参考。
|
24天前
|
JavaScript 前端开发 算法
前端优化之超大数组更新:深入分析Vue/React/Svelte的更新渲染策略
本文对比了 Vue、React 和 Svelte 在数组渲染方面的实现方式和优缺点,探讨了它们与直接操作 DOM 的差异及 Web Components 的实现方式。Vue 通过响应式系统自动管理数据变化,React 利用虚拟 DOM 和 `diffing` 算法优化更新,Svelte 通过编译时优化提升性能。文章还介绍了数组更新的优化策略,如使用 `key`、分片渲染、虚拟滚动等,帮助开发者在处理大型数组时提升性能。总结指出,选择合适的框架应根据项目复杂度和性能需求来决定。
|
1月前
|
前端开发 JavaScript API
深度剖析:前端如何驾驭海量数据,实现流畅渲染的多种途径
深度剖析:前端如何驾驭海量数据,实现流畅渲染的多种途径
70 3
|
19天前
|
Dart Android开发 开发者
Flutter跨平台开发实战:构建高性能移动应用
【10月更文挑战第25天】随着移动设备种类的增加,开发者面临跨平台应用开发的挑战。Flutter作为Google推出的开源UI工具包,凭借其强大的跨平台能力和高效的开发效率,成为解决这一问题的新方案。本文将介绍Flutter的核心优势、实战技巧及性能优化方法,通过一个简单的待办事项列表应用示例,帮助读者快速上手Flutter,构建高性能的移动应用。
31 0
|
1月前
|
缓存 前端开发 UED
前端 8 种图片加载优化方案梳理
本文首发于微信公众号“前端徐徐”,详细探讨了现代网页设计中图片加载速度优化的重要性及方法。内容涵盖图片格式选择(如JPEG、PNG、WebP等)、图片压缩技术、响应式图片、延迟加载、CDN使用、缓存控制、图像裁剪与缩放、Base64编码等前端图片优化策略,旨在帮助开发者提升网页性能和用户体验。
184 0
|
2月前
|
前端开发 定位技术 API
二、前端高德地图、渲染标记(Marker)引入自定义icon,手动设置zoom
文章介绍了如何在前端使用高德地图API渲染标记(Marker),并引入自定义图标,同时展示了如何手动设置地图的缩放级别。
237 1
|
2月前
|
Web App开发 前端开发 JavaScript
Web前端项目的跨平台桌面客户端打包方案之——CEF框架
Chromium Embedded Framework (CEF) 是一个基于 Google Chromium 项目的开源 Web 浏览器控件,旨在为第三方应用提供嵌入式浏览器支持。CEF 隔离了底层 Chromium 和 Blink 的复杂性,提供了稳定的产品级 API。它支持 Windows、Linux 和 Mac 平台,不仅限于 C/C++ 接口,还支持多种语言。CEF 功能强大,性能优异,广泛应用于桌面端开发,如 QQ、微信、网易云音乐等。CEF 开源且采用 BSD 授权,商业友好,装机量已超 1 亿。此外,GitHub 项目 CefDetector 可帮助检测电脑中使用 CEF
326 3
|
2月前
|
编解码 前端开发 JavaScript
前端移动端适配方案
【9月更文挑战第8天】前端移动端适配方案
93 0
|
3月前
|
API Java 数据库连接
从平凡到卓越:Hibernate Criteria API 让你的数据库查询瞬间高大上,彻底告别复杂SQL!
【8月更文挑战第31天】构建复杂查询是数据库应用开发中的常见需求。Hibernate 的 Criteria API 以其强大和灵活的特点,允许开发者以面向对象的方式构建查询逻辑,同时具备 SQL 的表达力。本文将介绍 Criteria API 的基本用法并通过示例展示其实际应用。此 API 通过 API 构建查询条件而非直接编写查询语句,提高了代码的可读性和安全性。无论是简单的条件过滤还是复杂的分页和连接查询,Criteria API 均能胜任,有助于提升开发效率和应用的健壮性。
121 0