作者|张翰(门柳)
出品|阿里巴巴新零售淘系技术部
我在之前一篇文章《打破重重阻碍,Flutter 和 Web 生态如何对接?》提到了这么一句话:“CSS 和 Widget 的对接也是很繁琐的过程,而且存在完备性问题”。我直接给了结论,没有给出原因。
现在填上这个坑,这篇文章专门对比 Flutter Widget 的布局原理和 CSS 布局原理的差异,分享在对接过程中会遇到的问题和解决方案,帮大家理一理思路,内容可以分为这几部分:
- CSS 和 Widget 参赛选手介绍
- Let's Battle! 从五个角度正面硬刚
- Love & Peace 讨论取长补短的可行性
- Happy Ending 最重要的环节
CSS
CSS 是 Cascading Style Sheets(层叠样式表)的简写,是一种用来描述样式的标记语言,最初的想法诞生自 1994 年,1996 年落成第一版规范(参考 20 Years of CSS)。HTML 描述了页面的结构,CSS 描述页面呈现出来的样子,这对 CP 已经配合工作了二三十年,依然是布局圈里最高效的组合。
▐ CSS is Awesome
CSS 描述布局很高效,这点无需质疑。后续出现的各种布局方案,前端框架 CSS in JS、XML描述文件、Flutter 等等,即使没有直接照搬 CSS 的功能,也深受 CSS 设计的影响。
CSS 很容易上手,经典的盒模型一学就会,文本相关的属性看名字就知道怎么回事,Flexbox 也是很好用的,但是 CSS 逐渐出现了一些很难驾驭的功能,多列布局就稍微难了一点,CSS Grid … 这代码也太难写了吧,我觉得这是给 AI 设计的,不是让人手写的,再加上 clip-path, filter, css houdini 等等,功能越来越强大,可以做滤镜、画皮卡丘、画油画、甚至还可以做游戏。功能很强大,但是不实用,上述这些功能在生产环境基本上都是用 JS 实现的,没时间去磨 CSS。
▐ 渲染布局流程
浏览器里 CSS 的渲染过程,简化一下可以总结为 加载、解析、查询并作用到 DOM 节点、计算布局 这四个过程。浏览器首先加载 HTML 文件,遇到
然后浏览器根据带有 ComputedStyle 的 DOM 树生成 LayoutTree,节点的 display 特性不同,生成的 LayoutObject 的类型也不同,然后布局算法会多次遍历这课树,计算出每个节点的 Rect。这个过程是很复杂的,并不是一个 DOM 节点就对应一个 Layout 节点,要考虑 display:none、伪元素、文本节点、shadow dom 等等,而且整个过程是同步的,在解析出的 DOM 节点已经布局完成后,如果浏览器又解析到一个
布局完成之后是执行 Paint,把布局信息一层一层的提交到 compositor 线程,然后再划分成一块一块的交给 GPU 线程去绘制。我觉得最后这两步是比较快的,在主线程里的 Layout 和 Paint 才是最耗时间的,而且和 JS 代码的执行搅在了一起。
Flutter Widget
和 CSS 不同,Flutter Widget 设计得很细致,分类清晰功能明确,不像 CSS 那样各种属性耦合在一起,互相影响。Widget 的设计是比较原子化的,基本不会互相影响,为了保持布局算法的高效,对 Widget 的嵌套方式有要求。
Flutter 设计得比较合理,有一个原因不容忽视,Flutter 在 Google 的开发团队和 Chrome 有很大渊源,有些人已经参与制定 CSS 规范很多年,都是在这个领域里很有经验的人。整体上没有 CSS 那么多的冗余和历史包袱,新框架都可以吸取以前的教训,站在巨人的肩膀上设计得越来越好用。换句话说,如果让 CSS 的设计者们,抛开历史包袱重新设计,不考虑向下兼容,很有可能就设计成了现在 Flutter Widget 的样子。
▐ 渲染布局流程
关于 Flutter 的渲染流程,官方文档Flutter 工作原理就是很好的学习资料,最大的亮点是次线性的布局,有接近 O(n) 的性能,比 CSS 高效得多。
具体过程大家去官网学习吧,这里就放一张图,本文的重点是 Battle!
Let's Battle!
▐ Round 1: 背后的大佬
在真正开始比较之前,先看一下他们背后是什么样的组织在支撑和运营着他们。
CSS 背后是 W3C,这是业界认可的标准化组织,而且被各大浏览器实现,浏览器厂商也在积极的推到标准的发展。CSS 是个开放的技术,它背后的大佬是 W3C、Chrome、Safai、Firefox 等等一系列盈利或者非盈利组织,大家互惠互利共同发展。
但是 Flutter 背后是是只有 Google,虽然也是开源的,但是设计与实现都是由 Google 的团队来主导,其他人都是在使用,真正有能力有机会参与开发的很少,PR 都是小修小补。框架和标准的一个差别,就是会不会向下兼容,框架有可能明天宣布推出 2.0,带上牛逼的优化和 breaking change,不向下兼容,是司空见惯的事。
从这个角度看,CSS 虽然臃肿,但毕竟是个标准化的技术,生命力顽强。你现在写的 CSS 代码,五年之后依然可以运行,现在写的 Flutter 代码到五年后就不好说了。假如 Google 宣布不维护 Flutter 了,很可能社区里就瞬间丧失了信心,那 Flutter 就死了;假如 Chrome 宣布不支持 CSS 了,CSS 依然活得好好的,Chrome 很可能会死(参考 IE),FireFox 要笑醒了。
▐ Round 2: 学习成本
要说学习成本,当然是 CSS 高效,先不讨论原理,先从几个侧面的案例说明一下。
市面上能出现“零基础,三个月成为前端高手!”的培训班,也侧面说明了前端学习成本低,其实他们的口号是错的,三个月肯定学不会前端,能学会的只有用 CSS 切页面。但是没有培训班开“三个月掌握Flutter”的课,一个熟练的前端开发者三个月学会是可能的,客户端上手更快一点,没有编程基础的人学完肯定一脸懵逼的要求退钱了。
另外微信可以举办“青少年微信小程序编程创意营” ,面向中小学生,小程序的 UI 就是受限的 HTML + CSS 来写的,但是 Flutter 要搞这种比赛的话,就得面向有熟练编程经验的人了。
如果从语法角度考虑的话,CSS 容易学是因为它只是一种描述性语言,不含复杂的编程逻辑,设计目标就是用来描述“我想要的 UI 是什么样子”的,是面向结果的描述,而 Widget 是要通过写 Dart 代码来实现的,UI 和代码逻辑写一起,通过代码一行行描述“我怎么把 UI 组合出来”的,是面向过程的描述,所以 CSS 更直观一些,写出来的代码更容易让人理解。还有个小原因,就是像 Fluter Widget 这样层层嵌套的代码,写起来和改起来都很麻烦,太依赖编辑器,复制粘贴不太方便(差不多的话,是可以抄一下代码的嘛)。
另外 CSS 诞生了这么多年,学习资料简直多到爆!本身规范事无巨细,还有 MDN 、CSS Tricks 、CodePen 等网站即授之以渔又授之于鱼,各种线上培训也把知识框架和学习路线都给你安排的明明白白的。相比之下,Flutter Widget 就只有官网,学习资料和社区生态都还差很多。
▐ Round 3: 开发效率
CSS 上手虽然简单,但是很难掌握,没有个几年的开发经验,没被它虐过千百遍,是根本驾驭不住它的。
假如要画出来 CSS 的学习曲线的话,初期肯定是快速上升,到了一定高度后就变慢了,甚至还迷之下降。但是 Flutter 刚开始学习时要了解很多概念,要转变思维,上手稍微慢了一点,但是越学越快。说个不恰当的比喻,写 CSS 可以看做是操作提线木偶,写 Widget 就相当于是搭乐高积木。
因为 CSS 各种属性之间是可以互相影响的,输入和输出不是简单的对应关系,你以为你写了 width: 100px 它的宽度就一定是 100px 了?就像是绑了几百根线的木偶,让你拉动其中的五根来做一个 OK 的手势,你牵了其中一条线,动的可能不只是一个部位,而是整个上半身。
Flutter Widget 就更加原子化,而且对于谁可以嵌套谁是有要求的,就像是拼乐高积木,每块积木都很小,但是有明确型号的卡口,要先符合它的设计,然后再发挥创意拼装成各种造型。迫使你按照理想的方式去组合 Widget,避免写出性能太差的代码,CSS 就是任意属性都可以和任意属性在写在一起,而且标准里总能给出你一个合理的解释,所以写出来的代码很混乱,也增大了布局的难度。
所以 CSS 的开发效率初期较快,但是积累沉淀较难,大规模协作很难管理(耦合度高,全局作用域等),而 Flutter 的封装性更好,有利于协作,开发效率会越来越快的。
▐ Round 4: 天下武功,唯快不破!
要问 Flutter 的 Widget 和 CSS 谁更快?大家的共识也是 Flutter 更快吧。我觉得 Flutter 和 CSS 布局相比有两大性能优势:一个是次线性的布局算法,一个是更合理的线程模型。
Widget 的布局原理比 CSS 高效,这是牺牲了一部分灵活性换来的,以后扩展 Widget 时,都要遵循这些设计才能继续保持高效。CSS 简单的属性背后可以深挖出特别复杂的细节,使得布局模型越来越复杂,渲染管线越来越长,光 display 就有十几二十个值,每一条都对布局影响巨大,方便了开发者,但是给布局性能带来很大挑战。
关于线程模型,也是浏览器一直被诟病的性能瓶颈,主线程太忙了,JS 的执行、HTML/CSS 的解析、DOM 的构建,布局的运算,全都在主线程。相比之下 Flutter 划分的四个线程就比较均衡,GPU 线程做的工作和浏览器差不多,但是宿主平台(Android/iOS)的代码跑在 Platform 线程里,Flutter Framework 主要运行在 UI 线程里,另外还有 IO 线程实现网络和图片、字体等文件的加载。
▐ Round 5: 未来的发展
分享几个关于 CSS 现状的数据,W3C 官方定义的 CSS 样式有 520 条,Chrome 平台上统计的样式有 703 条,包括了一些带前缀的样式,有大量样式的使用率很低。支付宝小程序的同学总结过 Top 100 小程序里用到的不同 CSS 样式,有 184 条。
总结一下就是:W3C 标准里定义了 520 条样式,Chrome 还额外支持 180+ 条私货,但是常用的样式不超过 200 条。
CSS 有大量历史包袱,我自己也感觉大部分样式我都用不到,有些甚至是最佳实践里禁止使用的,学会 50 条 CSS 后就可以写 80% 种情况的布局了,对于难写的样式,我多加几层标签再写点 JS 也能实现。但是这些样式不能废弃,必须继续支持,新增一条好用的属性时,要解释清楚和现在所有属性的适配情况。
CSS 是负重前行,注定要越变越复杂。Widget 则是轻装上阵,而且解耦比较好,是可插拔可组合的,如果未来想废弃一些 Widget,把这部分 Widget 从主包里拆出来放到独立插件里就行了,想用的话自行引入。整个迭代过程受历史包袱的影响很小。
Love & Peace: 可否实现对接?
Battle 已经结束,友谊第一比赛第二,不讨论胜负,下面进入 Love & Peace 环节。
Flutter 的压线性布局让人眼馋, CSS 的灵活度又深受大家喜爱,可不可以取其精华去其糟粕,让开发者写 CSS 但是底层用 Flutter 来渲染?有这种想法的不是一个人,所以有很多方案把前端框架或小程序对接到 Flutter 上,在实现的时候,就会遇到如何把 CSS 转换为 Widget 的问题。
▐ 技术可行性
技术上当然是可行的,我在前一篇文章《打破重重阻碍,Flutter 和 Web 生态如何对接?》里介绍了各种实现方案,自己也写代码做过对接(用的C++魔改方案),跑通了整个渲染链路。
我介绍一下我的实现方式,可以简单分成三步:
1. 解析 CSS 语法
我写了个精简版的 CSSOM,是标准 CSSOM 的子集。用来实现样式表和样式属性和增删改查、样式值的解析与计算、选择器的匹配和查询等功能。(功能是独立的,有需要的话自取)
CSSOM 主要是为了处理 CSS 的上层语法,转成一致的数据格式,为下一步的转换做准备,有一部分 CSS 的语法是在这个过程实现的,例如 CSS 选择器,包括伪类选择器和选择器关系等,还有 @media 和 @keyframes 等功能,也可以实现 CSS variable 以及 calc()。无论上层是直接写 CSS 还是 CSS in JS,都保证下一步转换时输入的数据格式一致,可以简化后续的实现。
2. 实现 CSS 属性和 Widget 数据格式的映射
这部分要把 CSS 的基础数据格式转成构建 Flutter Widget 所依赖的数据格式。例如 CSS 的 color 属性会转成 Flutter 的 Color 类,margin 和 padding 会转成 EdgeInsets 类,flex-direction 将被转成 Axis 枚举,把文本相关的属性转换成 TextStyle 类。
这部分也是做原子性的转换,技术上看起来比较简单,只是转换数据格式而已,但是需要对 CSS 和 Widget 的设计都比较了解,知道同一个概念在双方语义中的对应关系,得搞清楚里边的技术细节。这个对应关系如果转换错了,后续的布局怎么调都调不对(过来人的经验…)
3. 构建 Widget 树
拿到 CSSOM 传来的数据,掌握了 CSS 和 Widget 数据结构的语义转换,然后在结合 HTML 定义的结构,就可以生成真正的 Widget 树了。
构建 Widget 树就不能只考虑 CSS 了, HTML + CSS 才和 Widget 对等。这里还要处理不同类型 HTML 节点和 Widegt 的对应关系,节点上带的布局样式不同,生成的节点也不同,Widget 的层次深度比 HTML 的要深。例如普通的 div 标签,如果仅包含普通盒模型样式,就转换成 Container;如果包含了 flex 相关的属性,根据具体配置的不同,转成 Flex/Center/Row/Column 等;如果包含了绝对定位就要转成 Positioned/Stack。这个过程也是很繁琐,包含了大量细节,需要理解默认 HTML 标签的语义、CSS 的层模型以及 BFC 等等,还需要理解 Flutter Widget 之间的嵌套限制,组合成 CSS 想要的效果。
▐ 使用限制
对于开头提到的问题,前面的技术可行性分析回应了「繁琐」,下面讨论一下「完备性」。
CSS 是灵活的,Widget 是受限的,把一个灵活的语法转换成受限的实现,注定是不完备的。
在 CSS 里,任意属性可以和任意属性写在一起,W3C 标准里总有一个明确的解释方式,HTML 和 CSS 是没有错误的,只有不符合预期。但是在 Flutter 里,某个 Widget 里可以放哪些 Widget 是明确的限制的,例如 Positioned 外层必须有个 Stack、Center 只能有一个子 Widget,不符合预期的嵌套是会报错的,写出的代码不会出现匪夷所思的混用,所以布局算法可以很快。从这个角度讲,CSS 是 O(n!) 的复杂度,而 Widget 是多项式复杂度,用 Widget 去实现 CSS 注定是不完备的。(这难道是个 P 和 NP 问题……?)
在限制 CSS 写法的情况下,能不能对接到 Widget 的实现?这个是可以的。
想要 Widget 次线性布局的性能,就必须牺牲 CSS 的一部分灵活性。想要用技术突破这个限制?那就要改这套次线性的布局算法了,改完之后就不再是 Flutter 了,性能优势也没有了。
以我的实践经验来看,Widget 只能支持一定范围内的 CSS 样式。它对 CSS 的使用限制不在于样式条数,并不是说某个样式实现不了,而是在于样式的混用,即使支持了 500 条 CSS,但是某些属性依然不能同时使用,外层用了样式 A 内层再用样式 B 就是无效的,C 和 D 写一起就只有 C 有效。我评估 Widget 对 CSS 支持的范围上限会比现在的 ReactNative/Weex 还要大一些,能够满足大部分业务和小程序的需求,然而业务需求是会增长的,达到支持范围上限以后就很难再扩大了,就需要教育开发者了,对开发体验有影响。
We are hiring
好了,PK 完毕,下面是大家最喜闻乐见的招聘环节。
欢迎大家加入淘系技术部基础平台部的小程序与跨平台技术团队!是支撑淘系小程序、小游戏、Flutter 等跨平台技术的核心团队,有技术广度和也有技术深度,我们需要 iOS、Android、C++、Flutter、Canvas、WebAssembly、WebGL 等各方面的人才。如果你善于学习,这是一个很好的接触跨领域知识的机会!欢迎对技术有追求的同学加入!
简历请发送至邮箱:hanks.zh@alibaba-inc.com