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

前言

近几年,大前端越来越流行声明式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+响应式编程,了解了其核心,我们在切入其它领域时也能事半功倍。

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

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

相关推荐
粉末的沉淀1 分钟前
css:制作带边框的气泡框
前端·javascript·css
N***73851 小时前
Vue网络编程详解
前端·javascript·vue.js
e***71671 小时前
Spring Boot项目接收前端参数的11种方式
前端·spring boot·后端
程序猿小蒜2 小时前
基于springboot的的学生干部管理系统开发与设计
java·前端·spring boot·后端·spring
银空飞羽2 小时前
让Trae CN SOLO自主发挥,看看能做出一个什么样的项目
前端·人工智能·trae
Eshine、2 小时前
解决前端项目中,浏览器无法正常加载带.gz名称的文件
前端·vue3·.gz·.gz名称的js文件无法被加载
alexhilton3 小时前
深入理解withContext和launch的真正区别
android·kotlin·android jetpack
用户47949283569153 小时前
别再当 AI 的"人肉定位器"了:一个工具让 React 组件秒定位
前端·aigc·ai编程
WYiQIU4 小时前
面了一次字节前端岗,我才知道何为“造火箭”的极致!
前端·javascript·vue.js·react.js·面试
qq_316837754 小时前
uniapp 观察列表每个元素的曝光时间
前端·javascript·uni-app