一、pre
在阅读这篇文章之前,必须要同步一个观念就是:重视语义化是一个顶尖程序员的必备素养,因此很多时候你会听到别人吐槽命名太难,其实是因为他的习惯非常好,更希望找到完全契合场景的命名,这本身就是一件费脑子的事情。如果这个观点我们无法达成一致,那么本文的所有说法可能对你来说就显得比较可笑。
二、痛点
一直以来,使用 HTML + CSS 来表达 UI 结构,都有一个若隐若现的痛点。痛点来源主要体现在 DOM 结构的语义表现力不足。
例如这样一段代码,我们能够很清晰的知道 DOM 结构是怎么样的,但是其具体的布局结构方式和特性就不知道了。
1
2
3
4
尽管 HTML 也新增了许多语义化标签来弥补这种语义表现力缺失的问题,但是由于大家的应用场景是在是很难和这些语义化标签非常契合的对应上,从而导致了语义化标签的使用并不是很广泛,到目前为止,语义化标签都处于一个很尴尬的境地。
导致语义化标签被广大开发者冷淡的很大一部分原因,是因为 HTML 设计的这几个语义化标签确实是不太匹配日常开发的大多数具体场景。
好在我们在使用 React/Vue/Angular 开发项目时,可以通过自定义组件来增强某一个部分的语义表达。
例如在 React 中,我们编写一个分页列表,你一眼就能看出来我的页面结构长什么样。
这已经是 UI 表述的最佳实践。但是我们也不是只看最外面这一层,当你深入到更底层的逻辑时,最后看到的还是 div,语义表现力不足的事实总是存在的。
因此为了解决这个问题,在 antd 等优秀的开源框架中,为了增强组件的语义表现力,会提供 Row Cloumn Flex Grid 等容器组件供开发者使用。
这其实是 UI 表达的最佳实践。但是 antd 等框架对于这种思路的践行并不是非常彻底。他没有非常明确的建议说,我们不要使用 div 了,所以许多开发者就算使用了这种方式,也不是把它当成最佳实践来使用,很多时候只是为了少些两行 css 代码才使用他们。
很多时候也源自于 UI 组件库本身也没有想要去做一个大而全的布局思维重构,从而导致 antd 虽然提出了这个方向和构思,但是普通开发者也不一定能领会到。
HTML + CSS 语义表现力缺失还体现在结构和样式分离。有很大一部分开发者并不喜欢写完结构之后,还要重新去另外的文件给他补充样式。并且这样方式在维护的时候也并不友好,隐性的规则让样式的最终结果变得扑朔迷离。
最明显的一点就是默认的样式继承规则和样式优先级,这东西让许多人在调样式的时候非常难受。
所以很多后端来写前端,可能他什么都能搞定,就是搞不定 css 样式,这可太难了,哈哈哈哈。
许多开发者都意识到了这种痛点。因此 css in js 的方案层出不穷。原子 css 又再次重新火了起来。不过在根源上由于 HTML 文档流的设计不够简洁,视觉格式化模型中涉及到的概念太多,因此最终在使用上依然会有不小的理解成本与麻烦。
例如大家在使用原子化 css 的时候,很容易会出现一个元素上套用了太多 class 的情况,反而导致可读性极大的降低。
Button
因此我们需要思考新的方案来解决这种场景,例如使用一个变量名来复用这些样式。
const btn = 'blue-400 hover:blue-500 dark:blue-500 dark:hover:blue-600 text-sm text-white font-mono font-light py-2 px-4 border-2 rounded border-blue-200'
Button
这些理念都非常好。但是原子化 CSS 毕竟不是直接编写样式,他的适用场景会受到很大的限制。
并且当我们在思考如何复用原子化 CSS 的时候,其实就表示,开发者确实在期待一套完整的,全新的 UI 布局表述方式。
这就是鸿蒙应用开发中, ArkUI 的布局思路。
在设计上,arkUI 充分吸收了 HTML 等客户端方案发展这么多年的经验教训,在设计上完全摒弃了文档流的概念,转而强调容器的概念。为了应对不同的场景,arkUI 目前已经支持了 26 种容器组件。因为其明确的语义化,学习成本也非常低,例如如下容器组件,我们一看就能知道这是用来干嘛的。
Row
Column
Flex
Grid
List
Scroll
Swiper
Tabs
Refresh
...
并且布局方式到底是什么情况,由容器组件说了算,而不是子元素的类型说了算。
// 表示从左到右布局
Row() {
Text('hello world')
Text('hello world')
Text('hello world')
}
// 表示从上到下布局
Column() {
Text('hello world')
Text('hello world')
Text('hello world')
}
除了在语义化上非常重视之外,arkUI 并不支持结构与样式分离,而是把设置样式的行为当成一个 set
方法,支持一种链式调用的方式来做到样式与结构合并的最终结果。
Text('width: 10px')
.fontSize('12fp')
.color('#333')
.border({
width: '10px',
color: Color.Red,
style: BorderStyle.Dotted,
radius: 15
})
这其实是原子化 css 的进化版。如果你觉得原子化 CSS 真香,那么这种方式的好处你也一定能够快速理解到。
而且他比原子化 CSS 学习和记忆成本更低,更灵活,可以说是原子化 CSS 的理想化实现。
ArkUI 在设计上,还引入了一个风险较大的设定:样式后置。
这个最开始是在 Swift UI 中出现,可能许多前端开发都没见过。
Column() {
Text('hello text')
.fontSize('12fp')
.fontColor(Color.Black)
.fontStyle(FontStyle.Italic)
}
.width(20)
.height(20)
.border({
width: 10,
color: Color.Blue
})
之所以说他风大,是因为这种书写方式大家都没见过,可能会容易给人的第一感觉就是:什么玩意儿 ...
哪怕是在 Flutter 的设计中,也是可前置可后置,然后不管是文档案例,还是大家在开发中,其实也是让样式前置。
// 伪代码
Widget build(BuildContext context) {
return new Container(
width: 20,
height: 20,
...
child: new Text()
)
}
我刚开始在学习 Swift 使用的时候也会担心这种样式后置的方式会让样式堆在一起比较难受,但是用了一段时间之后发现,真香!
我们来看一下这样一段代码。
Column() {
Text(`最新值:${this.counter}`)
Column().block()
Column().block()
Column().block()
.onClick(() => {
this.counter ++
})
}
.margin(10)
.border({width: 4})
.width('50%')
.height('400lpx')
.justifyContent(FlexAlign.SpaceEvenly)
.block 是样式的复用。
@Extend(Column) function block() {
.width(40)
.height(40)
.backgroundColor(Color.Orange)
.border({
width: 2,
color: Color.Red
})
}
之所以我觉得真香的原因是因为我们在开发过程中,其实子元素的样式重复非常多,因此我们会考虑将子元素的样式封装起来,用一些方式来复用它。
这样,当我们将样式后置之后,虽然我们依然对父元素添加了一串样式,但是前面一部分的代码结构就依然非常简洁。
以前在刚开始接触学习 Flutter 的时候,也觉得 Flutter 的 UI 表现形式太糟糕太复杂了,为什么不学着 JSX 那样搞简单一点,并且其他人的这个类似想法还在 github 上有非常激烈的探讨。
直到后来我才理解到,这种注重语义化和容器的 UI 表达方式,可能比 JSX 更好,这才是最佳实践。
除此之外,这种声明式语法的编译速度会比 JSX 更快,性能上会更好。
三、总结
鸿蒙应用开发的 ArkUI,和基于 HTML + CSS 的 React 相比,能够更方便的使用语义化,提倡样式与结构合并,并在 UI 设计上,简化了视觉格式化模型,注重容器特性,学习理解成本得到了极大的降低,并且基于 set 的思维方式链式调用样式,大胆的将样式后置,在我个人的主观感受里,这是一种比 React,比 Flutter 更舒适的开发体验。
大多数前端开发多半都有一个坏习惯,写点代码就想看看布局现在已经长什么样了,这样其实挺影响开发效率的。你可能需要频繁的切换或者必须要一个外接显示器随时预览,反正我就是这样。也不知道大家有没有... 想想这是什么原因造成的吧。