用 React 实现一个简易版 JetBrains Toolbox


theme: orange
highlight: atelier-cave-light

前言

近日,我用 React 编写了一个简易版的 JetBrains Toolbox,你可以点击 此处 访问,文末会附上源代码。

坦白说,React 是难学的,在使用的初期,我也曾讨厌它。但不可否认,React 也是有趣的,需要使用者付出更多耐心去了解它。

在本篇文章中,我将通过构建 JetBrains Toolbox,来分享部分我使用 React 的心得。

项目分析

在开始之前,我希望你有 JetBrains Toolbox 的使用经历,因为这对理解项目很重要。

首页分析

image.png

  1. 已安装列表
  2. 可用列表
  3. 已安装的软件,同一软件可以安装不同版本
  4. 正在下载或安装中的软件
  5. 可以安装的软件

思考

3、4、5 应该封装成一个组件吗?

在日常编码中,我们很容易将有着相似的 UI 视图组合为一个组件,这种直觉通常是可靠的。但组件往往会变大以满足不同需求,直到它变得无法维护为止。根据经验,组件应该尽量小,要么只处理简易的逻辑,要么组合几个较小的组件。

详情页分析

image.png

通过版本列表,可以安装同一软件的不同版本。

数据结构

最开始,我把软件列表的数据模拟成以下结构:

// 文件位于: /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

状态管理

从经验来看,我们至少需要一个全局状态,用来储存安装列表,根据该列表的数据,很容易从原始数据中过滤出可用列表的数据,为了不在每次回到首页的时候重新过滤,把可用列表也存到全局状态吧:

  1. installList 已安装列表
  2. availableList 可用列表

到此,首页两个列表已经解决。那安装状态呢?姑且将安装状态分为:下载中、安装中、已安装 三种,下载中 需要展示进度条,我们可以用两个字段来描述。

  1. status 表示安装状态
  2. percent 表示下载进度百分比

请思考

我们应该将这两属性添加到每一个 installList 的元素上吗?

明确地说应该避免这么做。状态改变会触发 React 组件重新渲染,并且该组件会尝试重新渲染它的所有后代。

一起来看更好的做法吧。

新增一个状态 status 储存安装状态,它通过 installList 元素的 id 互相关联。同理,percent 也做同样的处理。

让我们回顾一下到目前为止需要的状态:

  1. installList,存储安装列表。
  2. availableList,存储可用列表。
  3. installStatus,installList 每一个元素的安装状态,通过 id 互相关联。
  4. downloadPercent,installList 每一个元素的下载进度百分比,通过 id 互相关联。

调度中心与 Event bus

有了数据和状态,该让它们工作了。

当我们点击安装按钮时,会经过下面流程:

  1. 将当前软件添加到 installList
  2. 将当前软件从 availableList 中移除。
  3. 建立 installStatus 联系,状态为 下载中
  4. 建立 downloadPercent 联系。
  5. 4 中的进度条递增。
  6. 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

© 版权声明
THE END
喜欢就支持一下吧
点赞8 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容