用 React 实现一个简易版 JetBrains Toolbox
前言
近日,我用 React 编写了一个简易版的 JetBrains Toolbox,你可以点击 此处 访问,文末会附上源代码。
坦白说,React 是难学的,在使用的初期,我也曾讨厌它。但不可否认,React 也是有趣的,需要使用者付出更多耐心去了解它。
在本篇文章中,我将通过构建 JetBrains Toolbox,来分享部分我使用 React 的心得。
项目分析
在开始之前,我希望你有 JetBrains Toolbox 的使用经历,因为这对理解项目很重要。
首页分析
- 已安装列表
- 可用列表
- 已安装的软件,同一软件可以安装不同版本
- 正在下载或安装中的软件
- 可以安装的软件
思考
3、4、5 应该封装成一个组件吗?
在日常编码中,我们很容易将有着相似的 UI 视图组合为一个组件,这种直觉通常是可靠的。但组件往往会变大以满足不同需求,直到它变得无法维护为止。根据经验,组件应该尽量小,要么只处理简易的逻辑,要么组合几个较小的组件。
详情页分析
通过版本列表,可以安装同一软件的不同版本。
数据结构
最开始,我把软件列表的数据模拟成以下结构:
// 文件位于: /src/assets/data.ts
interface SourceData {
name: string;
description: string;
logo: string;
versions: {
code: string;
name: string;
date: string;
}[];
}
const sourceData: SourceData[] = [
{
name: "Flee",
description: "由 JetBrains 打造的下一代 IDE",
logo: "https://resources.jetbrains.com/storage/products/company/brand/logos/Fleet_icon.svg",
versions: [
{
code: "1.10.189",
name: "1.10.189 Public Preview",
date: "2022/10/31",
},
{
code: "1.9.237",
name: "1.9.237 Public Preview",
date: "2022/10/15",
},
],
},
// 省略其余内容
]
老实讲,不假思索的冲动行为会引起灾难,这份数据在后续编码中给我带来不少困扰(如果各位有兴趣可以去看该项目的 GitHub 的提交记录,在此不做赘述)。
那就将错就错吧,假如后端返回的数据恰好如此呢?
我们需要将数据扁平化,结构为:
type Software = {
id: number;
name: string;
description: string;
logo: string;
versionCode: string;
versionName: string;
date: string;
children?: number[]; // 子辈 id
};
const flatData = [
{
id: 1,
name: "Flee",
description: "由 JetBrains 打造的下一代 IDE",
logo: "https://resources.jetbrains.com/storage/products/company/brand/logos/Fleet_icon.svg",
versionCode: "1.10.189",
versionName: "1.10.189 Public Preview",
date: "2022/10/31",
children: [2],
{
id: 2,
name: "Flee",
description: "由 JetBrains 打造的下一代 IDE",
logo: "https://resources.jetbrains.com/storage/products/company/brand/logos/Fleet_icon.svg",
versionCode: "1.9.237",
versionName: "1.9.237 Public Preview",
date: "2022/10/15",
},
]
扁平化数据的函数就不在此处粘贴了,相信难不倒聪明的各位。
思考
如何生成唯一的 id 呢?
UUID 是一个很好的选择,但不太可能自己去实现一个生成 UUID 的程序,但也不想引用第三方库,该怎么办?还记得 generator 函数 吗?我们可以利用它生成唯一 id:
function* generateID() {
let id = 1;
while (true) {
yield id++;
}
}
const generateId = generateID();
const id1 = generateId.next().value;
const id2 = generateId.next().value;
console.log(id1, id2); // 1, 2
状态管理
从经验来看,我们至少需要一个全局状态,用来储存安装列表,根据该列表的数据,很容易从原始数据中过滤出可用列表的数据,为了不在每次回到首页的时候重新过滤,把可用列表也存到全局状态吧:
installList
已安装列表availableList
可用列表
到此,首页两个列表已经解决。那安装状态呢?姑且将安装状态分为:下载中、安装中、已安装 三种,下载中 需要展示进度条,我们可以用两个字段来描述。
status
表示安装状态percent
表示下载进度百分比
请思考
我们应该将这两属性添加到每一个 installList 的元素上吗?
明确地说应该避免这么做。状态改变会触发 React 组件重新渲染,并且该组件会尝试重新渲染它的所有后代。
一起来看更好的做法吧。
新增一个状态 status
储存安装状态,它通过 installList
元素的 id 互相关联。同理,percent
也做同样的处理。
让我们回顾一下到目前为止需要的状态:
installList
,存储安装列表。availableList
,存储可用列表。installStatus
,installList 每一个元素的安装状态,通过 id 互相关联。downloadPercent
,installList 每一个元素的下载进度百分比,通过 id 互相关联。
调度中心与 Event bus
有了数据和状态,该让它们工作了。
当我们点击安装按钮时,会经过下面流程:
- 将当前软件添加到
installList
。 - 将当前软件从
availableList
中移除。 - 建立
installStatus
联系,状态为 下载中。 - 建立
downloadPercent
联系。 - 将
4
中的进度条递增。 - 当
5
中的值到达100
时,将3
中对应的关系改为 安装中 / 已安装。
那么,上述逻辑我们该在哪里编码适合呢?惯例上,我们通常会把命名为 App
的组件当作意义上的根组件,比如在 App
里注册路由,或是设置 Layout
,如此看来,该组件似乎是合适的选择。
还记得上面所说的吗?状态改变会触发 React 组件重新渲染,并且该组件会尝试重新渲染它的所有后代。倘若我们让 App
承载安装逻辑,每当进度条的状态发生改变,那我们几乎整个应用都得跟着重新渲染了,同时,这也不符合我们上文描述的组件拆分原则。
综上所述,我们需要创建一个新的组件,它至少应该与 App
互为兄弟关系,我把它命名为 调度中心,它将承载我们所有的安装逻辑,而安装按钮只需通知它:我要安装了!
层层传递的形式来发起通知是件琐事,我们可以利用 Event bus 的形式来传递通知。
// 文件路径为 /src/utils/emitter.ts
class Emitter {
#events: { type: string; cb: (evt: CustomEvent) => void }[] = [];
addEventListener(type: string, cb: (evt: CustomEvent) => void) {
this.#events.push({ type, cb });
}
removeEventListener(type: string, cb: (evt: CustomEvent) => void) {
this.#events = this.#events.filter((element) => {
return element.type !== type || element.cb !== cb;
});
}
emit(evt: CustomEvent) {
this.#events.forEach(({ type, cb }) => {
if (type === evt.type) {
cb(evt);
}
});
}
}
export const emitter = new Emitter();
// 订阅事件
emitter.addEventListener("xxx", handleFn);
// 发送通知
emitter.emit(new CustomEvent("xxx", { detail: data }));
// 取消订阅
emitter.removeEventListener("xxx", handleFn);
到此为止,我们的核心部分就完成了,如果你想了解整个项目的实现细节,包括组件的拆分方式,逻辑的抽象和复用,请查看 GitHub,该项目也包含 Vue 版本(截止 2022 年 12 月 4 日,Vue 版本代码尚未优化,请谅解)。也可欢迎访问我的 个人站点 了解更多信息。
另外:本人正在寻找一份前端开发的工作,地点可为:上海、广州,希望得到帮助,谢谢。联系邮箱:nocode@live.com
转载自:https://juejin.cn/post/7173234119697465358