likes
comments
collection

用 React 实现一个简易版 JetBrains Toolbox

作者站长头像
站长
· 阅读数 17

前言

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

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

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

项目分析

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

首页分析

用 React 实现一个简易版 JetBrains Toolbox

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

思考

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

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

详情页分析

用 React 实现一个简易版 JetBrains Toolbox

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

数据结构

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

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