文章目录
前言
对这个系列感兴趣的可以关注订阅专栏:从零开始打造一个低代码平台
数据是整个应用的核心,一切的功能都围绕着数据而设计。
在前一章,我们在Canvas
里定义了一个widgets
来存放组件数据,但这个数据只能在Canvas
里访问。随着我们的功能越来越多,我们会有更多的组件会用到这个数据,例如:组件树、组件的属性编辑等等。所以组件数据需要放在一个可以被共享的地方。
这一章开始,我们开始要引入全局状态管理。
一、组件层级
在定义组件数据类型前,我们先介绍一下组件的层级关系:
屏幕是个顶层组件,一个屏幕可以容纳若干个组件,包括容器组件和非容器组件。在组件树中,屏幕是根节点。
容器组件可以包含其它非屏幕组件,包含容器组件和非容器组件。在组件树中,容器组件是个父节点。
非容器组件只能被屏幕组件或容器组件包含。在组件树中,非容器组件是个叶节点。
二、数据类型定义
前面我们定义了一个组件类型Widget
:
export interface Widget {
type: string;
id: string;
left: number;
top: number;
width: number;
height: number;
}
这个只包含了公共的最基本的属性,实际上不同的组件还会有各自不同的属性,例如Button
会有Text
,RadioButton
会有Options
。所以,我们会基于Widget
进一步定义具体的组件。
这一章我们将定义三种比较有代表性的组件:Container
,Button
和Screen
,其它组件基本都可以归到其中一类。
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
显示了屏幕列表,和允许添加与删除屏幕,为后续多个屏幕切换做准备。