2022年了,再来谈谈 App UI开发
前端快速发展了这么多年,诞生了非常多有意思的 State 框架,用法上都有自己的一套,但是归根结底还是围绕着 Mutable 还是 Immutable 的选择。本人深受 React 的颠覆式影响,近几年都是跟随 Immutable 的单项数据流模式,所以在架构上选择时,除了基础的 MVVM + Reactive,有时还会把 Flux (opens in a new window) 也引入其中。

说到 Flux,那就不得不提 Redux 了。Redux 并不是完全照搬了 Flux 的思想,而是提倡全局 Store,整个应用使用同一个 Store,所有的公共状态都从 Store 获取。这当然是有待商榷的,因为应用大了,公共状态必然很多,怎么分别管理子状态是要慎重考虑的。本人更倾向于尽量使用类似 React 的 useState,先把状态分散化管理,有需要的复杂界面采用useReducer, 全局 Store 只存储一些登录状态、主题、语言等相关的设置。
State 设计好之后,就是 UI 开发的事情了。上古时代,jQuery 将 State 和 CSS 一起拼成 HTML 字符串给某个标签赋值,对应到前几年写的 iOS UIKit 代码也是类似情况,那种命令式刷新 UI 的方式难看至极,已经过时了。
所以 React Native (opens in a new window) 和 Flutter (opens in a new window) 诞生了,这两者出生的时候都获得了很大关注。JSX 和 Widget 代码类似,都是把样式和布局写在一起,虽然被 Web 开发人员唾弃,说它们应该像网页一样分成 CSS 和 HTML,不然就会地狱嵌套了。我只能说这种吐槽是图样图森破,证明开发人员没学到位。
先复习一下 React Native 的写法:
import React from 'react';import { View, Text, Image, ScrollView, TextInput } from 'react-native';
const App = () => { return ( <ScrollView> <Text>Some text</Text> <View> <Text>Some more text</Text> <Image source={{ uri: 'https://reactnative.dev/docs/assets/p_cat2.png', }} style={{ width: 200, height: 200 }} /> </View> <TextInput style={{ height: 40, borderColor: 'gray', borderWidth: 1, }} defaultValue='You can type in me' /> </ScrollView> );};再对比一下 Flutter 的 demo 例子:
Widget titleSection = Container( padding: const EdgeInsets.all(32), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.only(bottom: 8), child: const Text( 'Oeschinen Lake Campground', style: TextStyle( fontWeight: FontWeight.bold, ), ), ), Text( 'Kandersteg, Switzerland', style: TextStyle( color: Colors.grey[500], ), ), ], ), ), Icon( Icons.star, color: Colors.red[500], ), const Text('41'), ], ),);我是接受并喜欢这种方式的,因为这就是我想要的开发模式,即声明式(Declarative)编程 (opens in a new window)。
声明式编程是一个大的概念,除了 SQL、Make 编译、正则表达式、HTML 等一些特定的领域专属语言之外,一些有名的编程范型也被归类为其子范型,比如函数式编程,尤其是纯函数式编程。与其相对的是命令式编程,包括 C/C++/Objective-C,Java 等。当然还有一些语言是两者都支持的,因为语言标准还在改进,年龄大的比如 C# 3.0+、Java 8+、JavaScript ES2015+,年轻的 Swift、Kotlin、Dart 等。
函数式编程,可以说是我这几年发现的宝藏,虽然它已经出现几十年了,但近 10 年才得到比较蓬勃的发展,应该归功于 JavaScript 的新标准快速发布。为什么这么流行肯定是有原因的,我们以 Javascript 举例来说明函数式编程的好处:
// 1var array = [1, 2, 3];var output = [];for (var i = 0; i < array.length; i++) { var tmp = array[i] * 2; output.push(tmp);}
console.log(output);
// 2output = array.map(function (n) { return n * 2;});
console.log(output);
// 3let doubleTimes = (n) => n * 2;output = array.map(doubleTimes);
console.log(output);上面三个 console 打印都是一样的结果,第一个是命令式的编程格式,第二个是直接在map里面写匿名逻辑函数, 第三个是使用纯函数当参数。
纯函数是没有副作用的,比如 doubleTimes 函数。这里的副作用指当调用函数时,除了返回函数值之外,还对主调用函数产生附加的影响,例如修改全局变量(函数外的变量)或修改参数(C++没有加 const 的引用类型参数)。函数副作用会给程序设计带来不必要的麻烦,给程序带来十分难以查找的错误,并且降低程序的可读性。这个概念对刚接触函数式编程的开发人员有点难以理解,其实看多了就懂。
通过将每一个小功能都拆分成一个个的纯函数,我们就可以通过链式调用的方式(类似 Promise)组合起来,达成一个复杂的功能,比如:
Maybe.of(null).chain(safeProp('address')).chain(safeProp('street'));正因为函数式没有副作用,我们也可以使用函数式编程的方式来开发 UI。当参数不变时,即状态不变时,UI 是没有变化的,可以用公式 UI = f(State) 来表示。
不同的函数可以代表不同的 UI 组件,复杂的界面通过组合不同的函数来绘制,即 SwiftUI 这种。当然不同的平台会有不一样的实现方式,但是这种组合(Composable)的概念是相通的,如 Web Components 可以 自定义 elements 和 template。
我们把这类 UI 开发范式取个名字,即声明式 UI 开发,这种编程方式采用更接近自然语义,让开发者可以直观地描述 UI 界面,不必关心框架如何实现 UI 绘制和渲染,从而实现极简高效的开发。
现代或将来的开发 App 的 UI 框架必定是走这个方向的,不要 xml 或 storyboard 布局,纯代码写 UI,并且能够 Hot Reload。以 iOS 的 SwiftUI 和 Android 的 Jetpack Compose 为例,Apple 和 Google 都是不余遗力的各自推广自家的新框架。接下来我们来比较一下 SwiftUI、Jetpack Compose 的写法:
SwiftUI 的例子 (opens in a new window):
import SwiftUI
struct ContentView: View { var body: some View { VStack(alignment: .leading) { Text("Turtle Rock") .font(.title)
HStack { Text("Joshua Tree National Park") .font(.subheadline) Spacer() Text("California") .font(.subheadline) } } .padding() }}Android Jetpack Compose 例子 (opens in a new window):
@Composablefun MessageCard(msg: Message) { // Add padding around our message Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = "Contact profile picture", modifier = Modifier // Set image size to 40 dp .size(40.dp) // Clip image to be shaped as a circle .clip(CircleShape) )
// Add a horizontal space between the image and the column Spacer(modifier = Modifier.width(8.dp))
Column { Text(text = msg.author) // Add a vertical space between the author and message texts Spacer(modifier = Modifier.height(4.dp)) Text(text = msg.body) } }}可以看得出来,这两者基本上没有差别吧,熟悉其中一种,立马可以上手另一种。
另外,国内的华为 HarmonyOS 也支持多语言开发 UI,有 Java UI、JS UI 两种。其中 Java UI 没有创新, 跟 Java Android 是一样的开发模式,JS 开发也就是类似 Web 和小程序开发一样,分成 HTML、CSS、JavaScript 三块。但是 HarmonyOS 在 2021 年 10 月发布 3.0 Beta 版本,JS UI 有了大动作,首先将名字正式命名为 ArkUI,JS 新增 5800+的 API 接口,重点是扩展了另一套极简声明式 UI 范式 eTS,这是基于 Typescript 的声明式开发范式。在最新的 DevEco Studio 创建新工程时,就有三个选项了。
eTS 是我们关注的重点,下面我们来看看官方 Demo 的效果:
import { TopTabs } from '../common/topTabs'import { SubscribeSwiper } from '../common/subscribe'import { HomeListItem } from '../common/homeListItem'
@Componentexport struct HomeTabComponent { private recommends: Resource[] = [$r('app.string.today_recommend'), $r('app.string.featured_recommend'), $r('app.string.september_recommend'), $r('app.string.august_recommend')] @Link showSettings: boolean
build() { Column() { TopTabs({ showSettings: $showSettings }) List() { ListItem() { SubscribeSwiper() }
ForEach(this.recommends, item => { // ForEach语句来显示推荐列表 ListItem() { HomeListItem({ titleIndex: this.recommends.indexOf(item) }) // 推荐列表Item,使用自定义组件HomeListItem } }, item => this.recommends.indexOf(item).toString()) } .listDirection(Axis.Vertical) .width('100%') .layoutWeight(1) } }}我在官网看完这个 Demo (opens in a new window) 之后,顿时感觉 App UI 开发范式有大一统的感觉了。
最后我想说一下,声明式开发是整个大前端的趋势,尤其是在 UI 这一块,但是这个趋势不代表命令式开发就没有存在的必要,毕竟还有这么多老项目以及这么多不愿意尝试的老人。这里说的老人不一定是年龄大的程序员,还有些思想顽固的意思。如果你有所追求,就打开 IDE 开干吧!