现代大前端是如何编码的?

前言

近几年,大前端越来越流行声明式UI+响应式编程的模式,如React、Vue、Flutter、Compose等,通过分析主流的语言框架的写法,提炼声明式UI+响应式编程的核心。

通过本篇文章,你将了解到:

  1. 命令式UI、声明式UI、响应式编程联系与差异
  2. 主流语言/框架的典型编码示例
  3. 声明式UI+响应编程的本质

1. 命令式UI、声明式UI、响应式编程联系与差异

命令式UI编程

以Android设置。 通常更新UI会固定执行以下操作:

kotlin 复制代码
class SecondActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_second)

        // 1. 获取控件(View)对象
        val textView = findViewById<TextView>(R.id.tv_welcome)
        
        // 2. 设置控件(View)属性
        textView.setTextColor(Color.BLACK)
    }
}

实际就分三步(前两步需要我们自己控制)。

命令式UI编程(Imperative UI Programming)是一种通过显式地操作视图对象及其状态来构建用户界面的方式。开发者需要手动控制UI组件的创建、更新和销毁过程

声明式UI编程

同样功能,我们稍微改造一下,使用Android里的Compose。

kotlin 复制代码
class SecondActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            // 使用Compose布局替代传统View
            WelcomeScreen()
        }
    }
}

// 声明式UI组件
@Composable
fun WelcomeScreen() {
    MaterialTheme {
        // 替代TextView,设置文本和颜色
        Text(
            text = "Welcome to My Compose App2",
            color = Color.Black
        )
    }
}

与命令式UI最大的不同点:声明式UI无需显式地持有UI对象,只需要关注UI状态结果即可。

声明式(Declarative)UI是一种通过描述UI应该是什么样子,而不是如何构建它的编程方式。开发者只需要声明当前界面的状态和结构,框架会自动处理状态变化后的更新与渲染。

Compose 声明式UI框架通过一个个被@Composable修饰的函数组合起来,最终描述UI的状态。

响应式编程

响应,顾名思义,需要弄清两个点,谁响应了谁? 很多时候我们代码的顺序都是按时间的先后顺序执行,A调用B,B执行结束后再调用C。 考虑另外的场景,A调用B,因为B比较耗时,A不相等,于是它告诉B:我先干别的(执行C),你弄完给我信号。等B完成后通知A,这个过程就是:

B 响应了 A

很显然,上述场景是典型的观察者模式,其本质是观察者注册了一个回调给被观察者,当被观察者满足条件后通过回调告诉观察者,最终被观察者响应了观察者。 用kotlin代码简单实现如下:

kotlin 复制代码
//观察者
class Observer {
    //注册回调
    fun register() {
        val subject = Subject()
        subject.register(object : Callback {
            //回调
            override fun notify() {
            }
        })
    }
}

interface Callback{
    fun notify()
}

//被观察者
class Subject {
    private val observers = mutableListOf<Callback>()
    fun register(callback: Callback) {
        observers.add(callback)
        thread { 
            //模拟异步通知
            Thread.sleep(3000)
            notifyObservers()
        }
    }
    fun unregister(callback: Callback) {
        observers.remove(callback)
    }
    private fun notifyObservers() {
        observers.forEach {
            it.notify()
        }
    }
}

你可能会说:难道这就是响应式编程?说对了一半,观察者模式是最简单的响应式编程形式,也是其它响应式的基础。 我们想要的效果是:

只需要一个对象,观测和通知都在这个对象上进行,也就是说将观察者和被观察者内置到这个对象的实现里,外部只需要关注怎么监听响应和怎么产生响应即可

因此,我们可以将响应式编程概括如下:

响应式编程(Reactive Programming)是一种基于异步数据流和变化传播的编程范式。它强调通过声明式的方式处理随时间变化的数据流,并自动将这些变化传播到所有依赖该数据的部分

声明式UI+响应式编程

声明式UI声明了UI结构,比如我们的Text需要动态变化,那么它绑定的数据一定是一个可变的数据。当数据发生变化的时候,UI要能感知到,如此一来当数据发生变化时,UI就会自动变化,我们只需要关注数据的变化和UI的声明,整个过程就简洁了许多。 还是以Compose为例:

kotlin 复制代码
// 声明式UI组件
@Composable
fun WelcomeScreen() {
    var name by remember { mutableStateOf("My Compose App2") }
    MaterialTheme {
        Column {
            // 替代TextView,设置文本和颜色
            Text(
                text = "Welcome to $name",
                color = Color.Black
            )
            // 新增按钮,点击后更改name的值
            Button(onClick = { name = "Updated Name" }) {
                Text(text = "Update Name")
            }
        }
    }
}

可以看出,我们现在只有一个name对象,Text依赖于name,当点击按钮更改name的值后,Text会自动刷新。 因此当声明式UI+响应式编程两者结合时:

  1. 是现代大前端开发的主流做法
  2. UI作为观察者,数据作为被观察者,中间的连接者即为响应式框架
  3. 通常称声明式UI+响应式编程为UI的状态管理

2. 主流语言/框架的典型编码示例

接下来我们简单分析主流语言/框架的声明式UI+响应式编程的典型实践。 涉及到: 三大原生平台:Android(Compose),iOS(SwiftUI),鸿蒙(ArkTS) 前端双子星:React,Vue 跨平台UI框架:Flutter

Android(Compose)

前面已经陆续以Compose为例分析过,此处就不再重复,需要注意的点是:

  1. Compose 声明式UI以函数为基础构建(函数是Kotlin里的一等公民,基础中的基础)
  2. 父布局通过{}包裹子布局
  3. 响应式是框架本身自带方法,也可以借助Flow来实现响应监听

iOS(SwiftUI)

以下是Swift代码。

swift 复制代码
import SwiftUI

struct ContentView: View {
    @State private var count = 0

    var body: some View {
        VStack {
            Text("你点击了 $count) 次")
                .font(.largeTitle)
                .padding()

            Button(action: {
                self.count += 1
            }) {
                Text("点击我")
                    .foregroundColor(.white)
                    .padding()
                    .background(Color.blue)
                    .cornerRadius(8)
            }
        }
        .padding()
    }
}

想要一个属性被观测,可以使用@State进行修饰,Text声明时绑定了count,当被@State修饰的count发生变化时,Text将自动刷新。 值得注意的点是:

  1. 响应式是框架本身自带的
  2. 使用注解(装饰器)修饰变量以期达到响应式的效果
  3. 父布局通过{}包裹子布局

鸿蒙(ArkTS)

以下是ArkTS代码。

swift 复制代码
@Entry
@Component
struct Index {
  @State message: string = 'Hello World';

  build() {
    RelativeContainer() {
      Text(this.message)
        .id('HelloWorld')
        .fontSize($r('app.float.page_text_font_size'))
        .fontWeight(FontWeight.Bold)
        .alignRules({
          center: { anchor: '__container__', align: VerticalAlign.Center },
          middle: { anchor: '__container__', align: HorizontalAlign.Center }
        })
        .onClick(() => {
          this.message = 'Welcome';
        })
    }
    .height('100%')
    .width('100%')
  }
}

也是通过注解(装饰器)来修饰可观测的变量。 可以看出,从编码习惯上来看,和SwiftUI比较类似。

值得注意的点是:

  1. 响应式是框架本身自带的
  2. 使用注解(装饰器)修饰变量以期达到响应式的效果
  3. 父布局通过{}包裹子布局

React

react 复制代码
import React, { useState } from 'react';

const App2: React.FC = () => {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(count + 1);
  };

  const decrement = () => {
    setCount(count - 1);
  };

  return (
      <>
          <div style={{
              display: 'flex',
              flexDirection: 'column',
              justifyContent: 'center',
              alignItems: 'center',
              height: '100vh',
              textAlign: 'center'
          }}>
              <h1>React 响应式编程和声明式UI演示</h1>
              <p>当前计数: {count}</p>
              <button onClick={increment}>增加</button>
              <button onClick={decrement}>减少</button>
          </div>
      </>
  );
};

export default App2;

前端的声明式UI更接近HTML语法,毕竟它们脱胎于HTM。 通过 {count}声明了一个UI段落,核心是通过useState(0)构造出一个可读可写的属性count。 声明式UI绑定观测count(可读),当按钮点击后修改count(setCount)的值(可写),UI自动刷新。

  1. 与之前Compose/SwiftUI/ArkTS 不同的是,React将可观测值的读写分离了出来
  2. 父布局通过<>包裹子布局

从此处也可以清晰感知到,前端和移动端在声明式UI的写法上还是有很大的差异。

Vue

同为前端双子星之一,Vue和React有不少想通的地方。

vue 复制代码
<script setup>
import { ref, onMounted } from 'vue'

// reactive state
const count = ref(0)

// functions that mutate state and trigger updates
function increment() {
  count.value++
}

// lifecycle hooks
onMounted(() => {
  console.log(`The initial count is ${count.value}.`)
})
</script>

<template>
  <button @click="increment">Count is: {{ count }}</button>
</template>

Vue是将JS和UI分离,分别为Script与template。 使用ref修饰待观测的变量,当点击button时更改count的值,而该button在声明时绑定了count,因此此时会自动刷新UI。

  1. 与之前Compose/SwiftUI/ArkTS 有点类似,都是包装变量
  2. 父布局通过<>包裹子布局

Flutter

dart 复制代码
class Test5 extends StatefulWidget {
  @override
  _Test5State createState() => _Test5State();
}

class _Test5State extends State<Test5> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Stateful Widget Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

同样的是声明式UI,与之前其它框架不同的是:Flutter并没有对counter进行单独的包装,Text绑定counter是读取counter的过程,_incrementCounter()是写counter。 当修改了变量的值后,必须要显示的调用setState()方法触发build的执行,最后UI才会刷新,并且刷新的是整个页面,它不像其它框架一样能够针对某个变化的属性刷新某个UI组件。 因此,Flutter官方自带的状态管理并不是完整的响应式编程

当然,为了解决此问题,Flutter社区涌现了许多优秀的状态管理库,如Provider、Riverpod、Bloc、GetX、Redux等,我们以GetX为例,改造上面的代码:

dart 复制代码
class _Test5State extends State<Test5> {
  var _counter = 0.obs;

  void _incrementCounter() {
    _counter.value += 1;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Stateful Widget Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Obx(()=>
                Text(
                  '$_counter',
                  style: Theme.of(context).textTheme.headline4,
                ),
            )
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

此时,将counter变量使用.obs包装起来,在声明式UI那使用Obx包装绑定的Text和counter,而后直接修改counter的值,UI就会自动刷新。 值得注意的是:

  1. Flutter 需要借助三方库实现完整的响应式状态管理
  2. Flutter 更多的使用具名参数声明UI,使用children字段包裹子布局,因此代码看着比较多,当UI层次复杂时,嵌套也比较深

3. 声明式UI+响应编程的本质

声明式UI总结:

  1. 专注于初始构造UI,弱化开发者对UI对象的管理,侧重于配置。
  2. 对需要动态更改的UI状态,需要绑定可观测的变量/属性。

响应编程总结:

  1. 基础是观察者模式。
  2. 通过将观察者模式封装起来,UI组件作为观察者,绑定的可变变量/属性作为被观察者,当变量/属性变化时自动刷新UI,完成响应式更新UI过程。

当然,虽然各家都有自己的状态管理,但社区也不乏存在许多优秀的开源框架,他们或多或少地满足了工程化实践里的现实需求,优势和劣势并存、大家可以根据自己的实际场景选择不同的状态管理库。 以下为一些常用的状态管理库:

Jetpack Compose:Mobius、Koin SwiftUI:ReduxSwift、ReSwift React:Redux、MobX Vue:MobX Flutter:Provid、Bloc、Riverpod、GetX

好了,以上就是现代大前端最简单的画UI过程,当然,这只是每个领域的冰山一角,更多内容还需要深入实践。 不过万变不离其宗,即使再出现其它的语言和框架,都绕不开声明式UI+响应式编程,了解了其核心,我们在切入其它领域时也能事半功倍。

那么问题来了,你觉得以上语言/框架,谁借鉴了谁,你觉得哪个写起来更爽?

如果觉得本文有那么一点点帮助,请一键三连哦~

相关推荐
OpenTiny社区1 分钟前
HDC2025即将拉开序幕!OpenTiny重新定义前端智能化解决方案~
前端·vue.js·github
newki11 分钟前
【NDK】项目演示-Android串口的封装工具库以及集成的几种思路
android·c++·app
Oriel11 分钟前
Strapi对接OSS:私有链接导致富文本图片过期问题的解决方案
前端
noodb软件工作室20 分钟前
支持中文搜索的markdown轻量级笔记flatnotes来了
前端·后端
Catfood_Eason39 分钟前
HTML5 盒子模型
前端·html
小李小李不讲道理1 小时前
「Ant Design 组件库探索」二:Tag组件
前端·react.js·ant design
vvilkim1 小时前
Flutter布局系统全面解析:从基础组件到复杂界面构建
flutter
用户2018792831671 小时前
Android 虚拟机的奇妙工厂之旅:从 Dalvik 到 ART 的技术童话
android
1024小神1 小时前
在rust中执行命令行输出中文乱码解决办法
前端·javascript