【实战】从零开始打造一个低代码平台——7、全局状态管理


前言

对这个系列感兴趣的可以关注订阅专栏:从零开始打造一个低代码平台

数据是整个应用的核心,一切的功能都围绕着数据而设计。
在前一章,我们在Canvas里定义了一个widgets来存放组件数据,但这个数据只能在Canvas里访问。随着我们的功能越来越多,我们会有更多的组件会用到这个数据,例如:组件树、组件的属性编辑等等。所以组件数据需要放在一个可以被共享的地方。
这一章开始,我们开始要引入全局状态管理。


一、组件层级

在定义组件数据类型前,我们先介绍一下组件的层级关系:

屏幕是个顶层组件,一个屏幕可以容纳若干个组件,包括容器组件和非容器组件。在组件树中,屏幕是根节点。
容器组件可以包含其它非屏幕组件,包含容器组件和非容器组件。在组件树中,容器组件是个父节点。
非容器组件只能被屏幕组件或容器组件包含。在组件树中,非容器组件是个叶节点。

二、数据类型定义

前面我们定义了一个组件类型Widget

export interface Widget {
    type: string;
    id: string;
    left: number;
    top: number;
    width: number;
    height: number;
}

这个只包含了公共的最基本的属性,实际上不同的组件还会有各自不同的属性,例如Button会有TextRadioButton会有Options。所以,我们会基于Widget进一步定义具体的组件。

这一章我们将定义三种比较有代表性的组件:ContainerButtonScreen,其它组件基本都可以归到其中一类。

2.1 容器 (Container)

Container是一个容器类组件(本身的名字也叫“容器”),也就是说它可以包含其它的组件,甚至包含其它Container组件。

export interface Container extends Widget {
	bgColor: string;
	opacity: number;
	children: Widget[];
}

2.2 按键(Button)

Button是一个非容器类的组件,只能在其它的容器类组件中。

export interface Button extends Widget {
	color: string;
	opacity: number;
}

2.3 屏幕(Screen)

Screen也是一个容器类组件,但比较特殊,它只能包含其它组件,不能被包含,而且,每一个屏幕都有且只有一个Screen组件。

export interface Screen {
	bgImage: string;
	children: Widget[];
}

三、全局状态管理

做全局数据管理的方案很多,我们选用zustand/immer

关于Zustand

Zustand 是一个流行的状态管理库,它提供了一种简单且有效的方式来管理 React 应用程序的状态。以下是 zustand 的一些主要好处:
简单易用:zustand 的 API 设计得非常简洁,易于学习和使用。你可以快速上手并开始管理你的应用状态。
轻量级:zustand 是一个轻量级的库,它不会引入不必要的复杂性或开销。这使得它非常适合用于小型到中型的应用程序。
性能优化:zustand 提供了一些性能优化的特性,例如选择性地订阅状态的变化,从而避免不必要的重新渲染。
可预测性:zustand 的状态更新是同步的,这意味着你可以预测状态的变化,并且可以在状态更新后立即执行副作用。
易于测试:由于 zustand 的状态是集中管理的,并且更新是同步的,因此测试变得更加容易。你可以轻松地模拟状态并验证组件的行为。
支持 TypeScript:zustand 原生支持 TypeScript,这使得类型检查更加容易,并且可以减少类型错误。
可扩展性:zustand 可以很容易地与其他库或框架集成,例如 React Router、Redux 等。
社区和文档:zustand 拥有一个活跃的社区和完善的文档,这使得你可以很容易地找到问题的答案和解决方案。
总的来说,zustand 是一个强大且灵活的状态管理库,它可以帮助你更高效地管理 React 应用程序的状态。

关于immer

Immer 是一个用于 JavaScript 应用程序的不可变状态管理库。它的主要好处包括:
简化不可变更新:Immer 允许你使用看起来可变的代码来更新状态,但实际上它会在幕后创建新的不可变副本。这使得处理复杂的状态更新变得更加直观和容易。
减少样板代码:使用 Immer,你不需要编写大量的代码来创建状态的副本和更新它们。Immer 会自动处理这些事情,从而减少了样板代码,使你的代码更加简洁和易于维护。
易于调试:由于 Immer 确保状态是不可变的,这使得调试变得更加容易。你可以轻松地跟踪状态的变化,并且不会因为意外的副作用而感到困惑。
性能优化:Immer 会优化状态更新,只更新实际发生变化的部分,从而提高性能。
易于集成:Immer 可以很容易地集成到现有的项目中,无论是使用 Redux、MobX 还是其他状态管理库。
支持 TypeScript:Immer 原生支持 TypeScript,这使得类型检查更加容易,并且可以减少类型错误。
社区和文档:Immer 拥有一个活跃的社区和完善的文档,这使得你可以很容易地找到问题的答案和解决方案。
总的来说,Immer 是一个强大且灵活的状态管理库,它可以帮助你更高效地管理 JavaScript 应用程序的状态。

3.1 创建数据仓库

3.1.1 安装相关依赖:

yarn add zustand immer uuid

3.1.2 创建数据仓库和相关操作:

import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
import { Scr } from '../types/widget/scr';

export interface WidgetState {
	// 容纳所有的屏幕
    scrs: Scr[];
}

export interface WidgetActions {
	// 增加一个屏幕的操作
    addScr: (scr: Scr) => void;

	// 删除一个屏幕的操作
    removeScr: (scrId: string) => void;
}

export const useWidgetStore = create(
    immer<WidgetState & WidgetActions>((set) => ({
        scrs: [],
        addScr: (scr: Scr) => {
            set((state) => {
                state.scrs = [...state.scrs, scr];
            });
        },
        removeScr: (scrId: string) => {
            set((state) => {
                state.scrs = state.scrs.filter((s) => s.id !== scrId);
            });
        },
    }))
)

3.2 扩展SideBar

扩展SideBar显示屏幕列表。SideBar里从数据仓库读取屏幕数据,然后定义了增加``删除屏幕操作,另外高亮选中的屏幕。

import { useState } from "react";
import { useWidgetStore } from "../stores/widget.store";
import { createScr } from "../utils/widget";

interface SideBarProps {
  className?: string;
}

export const SideBar: React.FC<SideBarProps> = ({ className }) => {
  const scrs = useWidgetStore((s) => s.scrs);
  const addScr = useWidgetStore((s) => s.addScr);
  const removeScr = useWidgetStore((s) => s.removeScr);

  const [activeIndex, setActiveIndex] = useState<number>(-1);

  return (
    <div className={className ?? ""}>
      <div className="flex items-center justify-end p-2 space-x-2">
        <i
          className="iconfont icon-plus hover:text-orange-500"
          onClick={() => {
            addScr(createScr());
          }}
        />
        <i
          className="iconfont icon-minus hover:text-red-500"
          onClick={() => {
            if (activeIndex >= 0) {
              removeScr(scrs[activeIndex].id);
              if (activeIndex >= 0) {
                setActiveIndex(activeIndex - 1);
              }
            }
          }}
        />
      </div>
      <ul className="p-2">
        {scrs.map((scr, index) => (
          <li
            key={index}
            className={`${
              activeIndex === index ? "bg-orange-500" : ""
            } flex items-center cursor-pointer space-x-2 p-2 rounded-md`}
            onClick={() => {
              setActiveIndex(index);
            }}
          >
            <i className="iconfont icon-screen" />
            <span>{scr.name}</span>
          </li>
        ))}
      </ul>
    </div>
  );
};

3.3 组件唯一性

我们给每个组件赋予一个唯一的ID,确保组件的唯一性。

import { v4 as uuidv4 } from 'uuid';

export function getUuid() {
    return uuidv4();
}

3.4 创建屏幕

另外,提供一个utils函数用于创建屏幕:

import { Scr } from '../types/widget/scr'
import { getUuid } from './uuid'

export const createScr = (name?: string): Scr => {
    return {
        type:'scr',
        name: name ?? 'scr',
        id: getUuid(),
        children: [],

        left: 0,
        top: 0,
        width: 0,
        height: 0,
    }
}

我们将以上这些拼凑到一块,就实现了添加和删除屏幕的功能。

现在我们的应用运行起来是这样的:

在这里插入图片描述


总结

这一章我们开始了最核心的数据设计,从屏幕开始,因为屏幕是容纳组件的最上层容器。
然后扩展了SideBar,让SideBar显示了屏幕列表,和允许添加与删除屏幕,为后续多个屏幕切换做准备。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值