React代码规范

by Teobler on 27 / 03 / 2022

views

这篇文章非常长,写这篇文章的初衷是为了沉淀这几年在项目上的一些代码实践,同时也希望能够听听外界的声音,毕竟闭门造车不可取。

这里我没有使用最佳实践这个词,也不太想用这个词,一是觉得这个词过于绝对,有以我为准的嫌疑,二是觉得时代一直都是在螺旋上升的,谁知道未来会发展成什么样呢。

每一个规范我都会尽量写清楚我们为什么产生了这个规范,背后的理由是什么,如果你有更棒的想法,欢迎与我交流。

你可以在评论区写下你觉得不合理的地方,以及为什么,或者你觉得有什么更好的实践,为什么更好等等。同时这些所有的文章我已经开源在了 github,欢迎所有人给我提 Issue 或 PR。

命名

命名其实是我不太想写的一部分,因为太多书太多人写过这方面的规范了,但我又想到其实这恰好是一个没有标准答案的东西,同时为了让整个规范更完整,还是决定以这个最基础的地方作为开篇。

在开始前我想阐述的一个原则是,我们期望命名的时候尽量不夹杂任何缩写(特别通用的专有名词例如 HTML 这类的除外),哪怕这个命名将会很长我们也接受。我们更希望任何人都能理解我们写的代码,毕竟代码是给人看的。

文件夹

文件夹的命名没有绝对的对错,在开源社区里既有串式命名法(kebab-case) 也有小驼峰命名法(lowerCamelCase)。

我们最后选择了串式命名法,原因是文件夹的名字其实不会被用作变量,所以不用在意是否有特殊符号,再者中间的横杠能清楚地分割每一个单词,能够让看代码的人快速看清楚文件夹的名字。

特别的,我们统一将存放测试的文件夹命名为 __tests__

文件

这里的文件其实分为好几类,我们一一说明。

组件

在 React 中组件通常以大驼峰(CamelCase)命名,于是将这个规范延展到了文件的命名中。

在此之上,我们额外添加了一个前缀原则。

如果一个组件最终需要在某个 path 下渲染为一整个页面,我们会在其前面加上 Page 前缀。这是为了让不熟悉代码的人能快速找到某一个页面,从而在这个页面中定位到其中的某一个子组件。

由于我们项目中存在大量的表单,为了快速找到这些散落在各个页面的表单,我们同样会在表单的组件文件前加上前缀 Form

一句话阐述,这个原则可能会根据不同项目的情况为需要的一类组件加上前缀方便查找。那么为什么是前缀而不是后缀呢?因为相比较于后缀来说,前缀可以让看到这个文件的人一眼就看出这是个什么类型的组件。

helper

由于 React 的局限性,大部分前端项目都不会有建模,这就意味着我们可能有大量的 helper 函数来处理业务逻辑,于是你能够看到大部分前端项目中充满了类似于 xxx-helperxxx-handlerxxx-utils 这样的文件或者是文件夹。

通常这样的文件中都会有不止一个处理业务数据或业务逻辑的方法,所以很难将文件内的函数名和文件名对应上,同时也为了更强的可读性,我们依然采用了与文件夹命名相同的串式命名法。

hooks

在项目中我们推荐一个文件中只存放一个独立的 hook,这样能够让每一个 hook 从创建之初就足够独立,减少各个 hook 之间的耦合性。基于这个前提,我们项目中 hooks 的文件名将与这个 hook 使用相同的以 'use' 开头的小驼峰命名法。

测试文件

这是一个特殊的类型,我们通常选择将测试文件放在生产文件同级目录下的 __test__ 文件夹下使用相同的命名,同时加入 .test 文件中缀。

那么如果不属于上面任何一种的文件我们将怎么命名呢?我们有一个原则是,如果你的文件内容足够独立,里面只放了一样东西,那么你的文件名应该与里面这个东西的名字一样。如果里面不止有一样东西,那么我们建议你使用串式命名法。

方法

方法的命名其实只有两个原则:

  1. 方法命名使用小驼峰命名法;
  2. 方法的名字应该是一个动词或动词短语,因为方法是可以被执行并伴随输入产生输出的,它应该是一个动作;

变量

与之对应变量的命名也遵循几个原则:

  1. 变量命名使用小驼峰命名法;
  2. 如果某个变量其实是一个常量,那么应该使用全大写的蛇形命名法(SNAKE_CASE);
  3. 变量应该是名词或名词短语;

类通常使用大驼峰命名法(UpperCamelCase)。这与上面方法和变量的命名一样更像是一个约定俗成的规范,我暂时没有找到为什么这么搞的原因,如果读者找到了这些规则的由来还请告诉我哈哈。

枚举

枚举的命名我们其实有过一段时间的讨论,大家各执一词,没有明显的优缺点,最后大家选择抄 Typescript 官网作业。使用大驼峰命名法(UpperCamelCase)来命名枚举的名字和枚举的键(key)。

组件

在开始聊组件的代码规范前,我想先聊聊 React 本身。首先我们需要接受 React 只是一个视图库,而不是一个大包大揽的框架。

如果回过头来审视React,并接受它在整个应用中只是负责视图部分这个事实,很多问题则会迎刃而解。无论是我们依照传统GUI的MVC/MVP架构,或者其变种MVVM,如果React只是其实的 V,那么显然,在前端我们需要一部分代码来充当M或者VM的角色。

以此为基础一个组件的本职工作应该是渲染视图,为了避免业务逻辑散落到每一个本应只负责渲染视图的组件中,我们应该将所有业务逻辑集中起来。React 给我们提供了 hooks 这个选项,前端开发人员们加入了 helper / utils 这个选项,而我推荐你业务建模这个选项。

业务逻辑在前端主要分为两类:

一是我们在日常开发中没办法保证后端的数据能够直接用于前端视图的渲染,需要根据页面的情况做一定的处理/转换/拼接。

二是一些逻辑判断/校验等需要放到客户端来做,一方面为了提升用户体验,另一方面也能够减轻服务器的压力。

而如果我们能够将这些数据按照业务进行建模,那么这就意味着这些业务模型将承担 M 的职责,将业务逻辑内聚,组件直接调用模型的getter 方法即可拿到需要渲染的数据。

这样做的好处使得我们拥有了职责分离的代码,将业务内聚于模型,将渲染内聚于组件。使我们专注于每一部分的同时获得了代码更好地可测性,即低耦合的代码。

综上所述,组件应该只负责渲染视图,业务逻辑应该内聚于业务模型。如果你的团队做不到这一点,那么我们可以后退一步,将你的所有业务逻辑放入 hooks 力求达到类似的效果。

目录结构

一个好的目录结构能够让你的开发效率事半功倍,也能够让新人快速熟悉项目,在合适的地方找到想要找到的东西。

为了达到这个目的,我认为一个好的目录结构应该遵循两个原则 - 分层与分类

首先分层与分类不是对立的,它们在目录组织中应该相辅相成。接下来我会用一个简单的例子来讲述它们之间的关系以及如何使用它们让你的目录结构更清晰。

pages

分类

首先是我们项目中的主体 pages 文件夹。在这个文件夹下我们的原则是每一个单独的 route path 作为一个单独的文件夹,文件夹的名字与 route path 保持一致,以 route path 来做页面的分类,并且在当前文件夹下以一个 PageXxx 的页面文件作为这个 route path 的入口。

这样做的好处是不论谁想找到哪一个页面,都可以轻易通过页面和 url 的对应关系快速找到这个 url 下的入口页面。

然后在当前文件下下,这个页面可能被拆分成不同的组件,于是有一个 component 文件夹来存放这些被拆分出来的组件。同理页面中用到的一个或多个组件将被存放在同级目录的 hooks 文件夹下。之前提到的业务建模将被存放于 models 目录下... 而每一个文件的测试将存放于该文件同级目录下的 __tests__ 目录下。

分层

同时我们不提倡页面间的相互引用,页面间如果出现了相互引用,那么也就意味着这个组件/方法/模型是一个多个页面可以共同使用的公共组件/方法/模型。我们会将它向上级目录“提取”,也就是分层。

这样做是为了让修改代码的人认识到,你正在修改的是一个不仅仅你在使用的东西,你需要保证自己的修改不破坏其他页面的功能。

pages 这个文件夹下还有可能产生一些其他的文件夹,比如 typesconstants 等。在不同的项目会有不同的叫法,只不过大原则不变 - route path 的页面作为分类,每一个类别中的东西大体相同,然后以引用者进行分层,自己的东西自己管理,大家的东西由上层管理

ui-components

聊完了 pages 目录,我们来看看大部分项目都会有的一些东西,首先是 src/ui-components 目录。如果已经理解了我们当前目录结构的思想,那么很显然这里的组件将会是全局通用的组件,这里的组件不应该包含任何业务逻辑。

那么为什么这些组件不遵循分类原则,由各个页面自己管理呢?

理论上每个项目都会有自己的 UI 设计,好的 UX team 还应该有自己的 design system。而 UI 组件就是这些设计的最终呈现,将它们维护在顶层符合“这是整个系统层面的设计实现”这一理念。既方便了管理和改动,又能够快速查看我们的组件哪些是已经有了的,哪些是还没有实现的。

更进一步,如果项目 B 也想使用同样的设计语言,这样的管理方式能够快速将其作为一个通用组件库剥离项目 A 将其应用的其它地方。

modules

src/modules 下面则是全局通用的一些功能性方法,比如 request 的一些 client、config,比如用户的鉴权,比如项目中需要的一些功能开关等等。

 到这里我们的例子就结束了,我并没有事无巨细地列出每一个文件夹,只是拿出一些典型的文件夹来解释我的目录结构的思想,如果大家需要的话,我可以在日后的更新中慢慢完善这个目录结构。

测试

在开始聊测试规范之前,如果你还在写测试这件事情上苦苦挣扎,可以瞅瞅我的 另一个仓库 我会在这个仓库中慢慢整理我在项目中遇到的,大家都很苦恼的一个测试场景,目的是为了让大家有作业可以抄。

技术栈

这里的测试规范我想先聚焦于单元测试,如果后面机会再扩展到诸如 UI、E2E 之类的更高层级的测试规范。而技术栈我会默认是常规的 Jest + React Testing Library + React Testing Library/React Hooks。但是理论上这些规范都可以扩展到不同的技术栈当中去,重要的是思想。

测试文件位置

首先也是我们之前有提到过的,测试文件应该位于被测文件同级目录下的 __tests__ 目录下,以便于快速定位两个相联文件的位置。

测试层级

在测试时你的所有测试应该被一个最外层的 describe 表达式包裹,这个 describe 的第一个字符串参数应该是 '#' 加你将要测试的文件名。用以告知读代码的人这是这个测试的最外层。

如果你的文件中有多个独立的方法需要测试(这种情况一般出现在 utils/helpers 文件中)那么你应该使用第二层 describe 将每一个待测方法的测试用例包裹起来,这时当前这个 describe 的字符串参数应该是 '##' 加你将要测试的方法名,下面是一个没什么意义的简单的例子。

describe("# utils", () => {
  describe("## isSomeFiledsRequired", () => {
    it("should return false when input is false", () => {
        expect(isSomeFiledsRequired(false)).toBeFalsy();
      },
    );
    
    it("should return true when input is true", () => {
        expect(isSomeFiledsRequired(true)).toBeTruthy();
      },
    );
  });
  
  describe("## isSomeFiledsOptional", () => {
    it("should return false when input is false", () => {
        expect(isSomeFiledsOptional(false)).toBeFalsy();
      },
    );
    
    it("should return true when input is true", () => {
        expect(isSomeFiledsOptional(true)).toBeTruthy();
      },
    );
  });
});
 

这样分级的意义在于如果你的文件中有多个方法时,你的测试将更加可读,能够一眼看出你的每一个方法的测试在哪里,便于修改和管理。

测试用例描述

每一个测试用例应该拥有有可读性强的用例描述,同时为了统一用例描述,我们推荐你采用如下的用例描述格式:

should xxxxx given xxxxxx when xxxxxx

根据不同的场景 given 的部分可以省略,这样的句式能够让看测试的人快速明白这个测试用例的目的是什么,条件是什么。可以结合用例的代码快速理解整个测试用例的目的。

测试用例模式

每一个测试用例在编写的时候不应该包含任何逻辑,只应该调用目标函数/组件,然后对它们的输出进行断言。

其次每一个测试用例都应该遵循 given when then 的原则,并且在某一个步骤超过三行的情况下添加注释

  1. 准备好被测函数/组件需要的输入/mock
  2. 调用目标函数/组件
  3. 对它们的输出进行断言
it("should return xxx given yyy is zzz when is some business case", () => {
  // given
  const param1 = { a: 1, b: 2 };
  const param2 = jest.fn();
  const param3 = jest.fn();
  const param4 = "dididi";
  
  // when
  const result = Foo({ param1, param2, param3, param4 });
  
  // then
  expect(result).toBe("lalala");
  expect(param2).toBeCalledWith("biubiubiu");
  expect(param3).toBeCalledTimes(2);
});

lint

lint 规范其实在我看来是最不重要的规范。

在 lint 工具这么完善的今天,团队就不应该浪费时间在lint规则上。但是我依然见到不少团队由于组内一些不同的声音导致浪费大量不必要的声音在制定 lint 规则上。

这些声音大多是这样的:

  • 我上个项目不是这样的balabala,是这样的balabala
  • 我个人不习惯这么写,我习惯这么写balabala
  • 我不加分号,加分号好丑
  • ......

其实这个问题很好解决,首先使用一个大家认可度比较高的 lint 工具,比如 eslint。然后选择一套使用度比较广的预设规则。

遇到大家有质疑的声音,提出你的理由,为什么这么做是好的,为什么现有的规则是不好的。

然后很显然这没有绝对的对错,团队里也一定会有两个声音,最后投票就好了,少数服从多数,这是团队项目,不是你一个人玩的个人项目。

团队里有且只应该有一个统一大多数人认可的 lint 规则。

最后

最后,这当然不是一个项目上所有的规范,还有许许多多的规范我可能没有覆盖到,我会在后面慢慢完善,欢迎关注我的 repo 获取后面的更新。