Skip to content

10.2 代码规范落地

了解了什么是代码规范,以及代码规范包含哪些内容,想必此刻你已经对代码规范有了一个清晰的认识。接下来我们就要将规范在项目中实践落地,这需要思考三个问题:

  1. 如何制定规范?
  2. 如何检测规范?
  3. 如何统一团队的规范?

制定规范就是要设计一套科学合理的代码规范,以标准和高效为目的,这是实施代码规范的基本与核心。检测规范是要检测源代码是否符合制定的规范,发现不规范时及时提醒和警告。统一规范就是将团队中每个成员不同风格的代码强制统一。这三者环环相扣,缺一不可。

实践证明,检测和统一规范往往是落地过程中最困难的环节。不管制定了多么科学合理的规范,只有在团队中长期执行下去规范才有意义,这要从成本和易用性上下功夫。我们先从制定规范开始。

10.2.1 制定规范

前端近几年快速发展,从没有规范到已经基本成体系的规范,相关的实践并不缺乏。在制定规范这件事上,我们不需要从头开始研究,借鉴社区已经流行的规范,从中获取适合自己的即可。

为什么这么做?因为一个成熟的开发者往往都有自己的规范和习惯,这些规范和习惯也都是来源于社区。我们使用社区规范更容易和个人习惯重合,这比自己弄一套规范让大家强制适应要好得多。

1. 命名规范

命名规范是最基本的代码规范。经过社区的不断发展,形成了以下几种通用的命名规范:

  • 下划线命名:user_name。
  • 中划线命名:user-name。
  • 小驼峰命名:userName。
  • 大驼峰命名:UserName。

目前这四种命名规范已经被大多数开发者接纳,成为了普遍共识,命名时可以作为基本规范遵循。不过每个开发者往往都有各自的编码习惯,对于同一类命名,可能使用不同的规范。

还是拿变量举例:一个团队中,有的人习惯用下划线命名变量,如 “user_name”;有的人习惯用驼峰命名变量,如 “userName”。这两种命名方式都正确,都符合规范,但是在一个团队中却形成为了两种风格,这显然不合理。我们在制定团队规范时,就是要把不同类的命名规范统一起来。

如何决定使用哪种命名规范呢?笔者推荐一套适合大多数前端项目的命名规则:

  • 变量、属性、参数:下划线命名。
  • 函数:小驼峰命名。
  • 类名、类型:大驼峰命名。
  • 文件、文件夹:中划线命名。

将上述的规则应用在编码中,那么你看到的前端代码就是这样的:

js
// 文件命名
import "./my-style.css";

// 变量命名
var v_tag = "变量";
var v_info = {
  v_name: "xxx",
  v_label: "xxx",
};
// 函数命名
var getTag = (v_tag) => {
  console.log(v_tag);
};

// 类命名
class UserInfo {}
// 类型命名
interface UserType {
  id: number;
}

在上面代码中,我们看到下划线命名就知道它是变量、看到小驼峰命名就知道它是函数,根本不需要找到变量定义的地方去看它的赋值是什么,这在逻辑复杂的代码中效率极高。如果团队代码全都遵循这套规范,不管是可读性还是效率都会成倍提升。

命名规范是最基础也是最关键的规范。严格遵循以上的命名规范,代码质量和规范性就会上升一个台阶。

2. 格式规范

格式规范,就是指在代码中使用基本符号和组织代码的规范。在很多情况下,JavaScript 对于符号使用没有强制标准,你可以在同一段代码中使用不同的符号,运行结果却保持一致。

最经典的格式问题:一行代码的结尾处要不要加分号?字符串使用单引号还是双引号?答案是都可以,因为你无论怎么做都不会影响代码的执行结果。

虽然不影响执行,但在团队中却会造成代码风格差异,不利于规范统一,因此格式规范也要纳入规范制定中。前端中通常需要设定的格式规范包含以下内容:

  • 代码结尾是否使用分号。
  • 字符串使用单引号还是双引号。
  • 缩进使用几个空格。
  • 对象属性之间如何空格。
  • 代码何时自动换行。
  • ......

格式规范是一类非常细节的规范,往详细了说规则特别多。但在制定格式规范的前期,我们不需要过于丰富的规范,先设置关键的几项,其余的根据项目进度慢慢补充。

首先制定以下 6 条格式规范,所有代码都要遵循:

  • 代码结尾不使用分号。
  • 字符串使用单引号。
  • 缩进使用 2 个空格。
  • 字符之间间隔 1 个空格。
  • 宽度超出 800px 自动换行。
  • 代码中连续空行最多 1 行。

依据这些规范,我们写一段与之匹配的代码:

js
var tag = "格式规范";

var getTag = () => {
  return tag;
};

var computed_value = new Array(100)
  .fill(0)
  .map((n, i) => i + 1)
  .filter((n) => n >= 50);

将以上的格式规范应用在全部代码中,当代码量到达一定数量级时,你会感受到格式统一带来的清爽。

3. 目录规范

目录规范就是项目目录结构的规范。在大型项目中有数量庞大的文件,我们需要有一个合理的目录结构,将所有文件按照功能和类别科学地组织起来,以便更好的分类和查找。

项目根目录下常见的文件夹,按照约定的目录名及对应的含义表示如下:

  • src:源代码目录。
  • config:构建配置目录。
  • public:静态资源目录。
  • dist:构建后的代码目录。
  • node_modules:第三方 npm 包目录。

按照这个规范,当我们需要增加构建配置文件时,要放到 config 目录下。引入某个不需要编译的图片或脚本,要放在 public 目录下。需要部署代码时,直接部署 dist 文件夹。这样每个文件夹都有自己的含义,统一使用该目录规范可以快速掌握项目结构。

这其中最重要的是源代码目录 src,我们所有的业务代码都要放到该目录下,因此 src 目录下还有二级目录规范。通常 src 目录下通用的目录规范如下:

  • assets:资源目录。
  • components:公共组件目录。
  • pages:页面目录。
  • stores:状态管理目录。
  • router:路由目录。
  • request:请求目录。
  • styles:全局样式目录。
  • utils:工具目录。

源代码目录下的所有文件都会经过构建工具编译,因此 assets 目录与 public 目录的区别就是资源是否会进行编译。通常在 index.html 中全局引入的资源会放在 public 目录下。

pages 目录和 components 目录都用于存放组件,区别是前者存放页面组件(有路由配置的组件),后者存放公共组件(在多个页面组件中通用的组件)。

下面用一个基本例子展示 src 文件夹下的目录结构:

sh
|-- src
    |-- index.ts # 入口文件
    |-- App.vue # 根组件
    |-- assets # 静态资源目录
    |   |-- logo.png
    |-- components # 公共组件目录
    |   |-- header
    |   |   |-- index.vue
    |   |   |-- index.less
    |-- stores # 状态管理目录,与 pages 结构对应
    |   |-- admins
    |   |   |-- index.ts # 状态文件
    |   |   |-- type.d.ts  # 状态类型
    |-- pages # 页面目录,与 stores 结构对应
    |   |-- admins
    |   |   |-- index.vue
    |   |   |-- index.less
    |-- request # 全局请求目录
    |   |-- index.ts
    |-- router # 路由配置目录
    |   |-- router.ts
    |   |-- index.ts
    |-- styles # 全局样式目录
    |   |-- common.less
    |   |-- index.less
    |-- utils # 工具目录
        |-- index.ts

对于组件的建议是:不要直接把组件文件放在 pages 或 components 目录下,最好用一个二级目录包裹(如上的 admin 目录),这样可以将组件分组,使结构更清晰。

4. 注释规范

技术圈有一个段子:“最讨厌自己写注释,最讨厌别人不写注释”。这反应了写注释虽然繁琐,但是对于合作者来说非常重要,它能快速让别人读懂你的代码并加入协作开发。

如果你的代码规范还好,如果代码不规范且没有注释、或者注释不够清晰,别人就要花大量时间理解这些代码逻辑。特别是对于一些“独特的逻辑和设计”,除了作者本人可能无人知晓,此时没有注释标记就会给合作者埋下隐患。

所以注释很重要,能让人快速看明白的注释更重要,因此我们需要制定注释规范。

  1. JavaScript 注释

在 JavaScript 代码中有三种注释方式:

  • 单行注释。
  • 多行注释。
  • 函数注释。

单行注释顾名思义只有一行,用于简单的标记信息;多行注释通常是对一段代码块注释,让其暂时不可用,以后需要是再放开;函数注释则是一种更规范的多行注释,举例如下:

js
// 单行注释
var name = "史泰龙";

/* 
name = '威尔史密斯'
console.log('多行注释') 
*/

/**
 * 函数注释
 * @params {Number} id
 */

上面代码中,单行注释最简单,只要在目标行行首加 “//” 符号,多行注释则是将一段代码包裹在符号 “/* */” 中间。函数注释看起来稍微复杂一些,是以 “/**” 开头,可以在注释中使用注释标签(如:@params)。

函数注释中的注释标签表示函数的某个部分,在没有 TypeScript 的时候,这种注释方式很清晰地标记了函数的构造,可以快速了解函数。常用的注释标签如下:

  • @desc:表示函数描述。
  • @params:表示函数参数。
  • @callback:表示回调函数。
  • @return:表示函数返回值。

注释标签后的花括号表示数据类型,后面跟着名称和描述信息(可选)。假设函数有一个名为 user_id 的参数,类型是数值,表示用户 ID,函数执行后返回查询到的用户数据,那么注释如下:

js
/**
 * @desc 获取用户信息函数
 * @params {Number} user_id 用户ID
 * @return {Object} 查询的用户数据
 */
  1. HTML 与 Vue 注释

除 JavaScript 之外,HTML 也有自己的注释方式,且与 JavaScript 不同。但 HTML 的注释方式只有一种,就是将注释内容包裹在 “” 符号之间,单行和多行通用。如下:

html
<!-- <h2>单行注释</h2> -->

<!-- <h3>多行注释</h3>
<h4>多行注释</h4> -->

Vue 注释的模版部分遵循 HTML 规则。对于多层嵌套较为复杂的模版,为了更清楚地查阅代码结构,我们推荐将模版按照以下注释规范划分为多个模块,表现得更清晰:

html
<!--【start】用户模块 -->
<div>
  <span>文本信息</span>
  <p>内容信息</p>
</div>
<!--【end】用户模块 -->

Vue 注释的脚本部分遵循 JavaScript 规则,直接使用上面介绍的 JavaScript 注释规范即可。

5. 类型规范

类型规范指如何设置 TypeScript 类型。定义类型与定义变量很相似,在 TypeScript 项目中会有数量庞大的类型,因此也会存在类型冲突的风险。类型规范与变量规范类似,也包括了命名规范、分组规范等规则。

  1. 命名规范

类型通常在代码中与变量、函数等混合使用,因此类型一定要遵守命名规范,将其与变量、函数等区分开来。前面我们介绍过,类型与类名使用大驼峰命名方式,遵循该规则就不会与变量、函数混淆。

那么如何区分类型与类名呢?实际上我们不需要关心这个问题。在 TypeScript 中,当我们创建一个类时,会同时创建一个同名的实例接口类型,所以你可以把类当成一个接口类型来使用。假设定义一个 Person 类,代码如下:

ts
class Person {
  constructor(name: string) {
    this.name = name;
  }
  name: string;
  getName() {
    return this.name;
  }
}

Person 类定义后相当于创建了下面的 Person 接口类型,因此直接可以当作类型使用:

js
interface Person {
  name: string
  getName(): string
}

var person: Person = {
  name: '猪猪侠',
  getName() {
    return '蜘蛛侠'
  },
}

因此,类型的命名规范可以遵守下面两个规则:

  • 不存在声明类:大驼峰命名就是类型,直接就能区分。
  • 存在声明类:将类名称当作一个接口类型使用,类名与类型一致。
  1. 分组规范

在 JavaScript 中我们不会把所有变量都定义在全局对象中,这会造成全局变量污染,定义类型也是一个道理。在定义类型时要格外注意避免定义全局类型,要适当地为其进行分组。

类型分组,在 TypeScript 通过命名空间和模块两种方案实现。

命名空间,简单来说就是直接将一批类型划分到一个组下,一个命名空间就是一个组,组内定义的类型不会与其他组的类型冲突。命名空间是最简单直观的避免全局类型冲突的方式,举例如下:

ts
// test.d.ts
declare namespace Person {
  interface Action {
    play(): void;
  }
}

declare namespace Animal {
  interface Action {
    eat(): void;
  }
}

上面例子中将两个 Action 类型分别定义到两个命名空间下,这样就不会有类型冲突。命名空间是最简单直观的分组,可以防止全局类型污染。使用命名空间下的类型方法如下:

ts
var action1: Person.Action;
var action2: Animal.Action;

模块则是另一种分组方式,它与 ES6 的模块机制类似,将类型划分为不同的模块,并通过导入(import)导出(export)来使用对应的模块。模块是更现代化的分组方式:

ts
// type.ts
export namespace Person {
  interface Action {
    play(): void;
  }
}
export namespace Animal {
  interface Action {
    eat(): void;
  }
}

// 使用时
import type { Person, Animal } from "./type.ts";

10.2.2 检测和统一规范

我们从五个方面制定了详细的代码规范。完成这些后,需要将这套规范在团队中推广下去,让团队的每个成员严格遵守,形成统一。那么如何保证团队成员会遵守规范呢?

首先作为团队成员我们要自觉遵守规范,强调规范意识。同时可以安排组员互相 Review 对方的代码,检查和监督对方的代码规范性。但这种方式的成本太高,必须要花费大量的时间在代码规范和检查上。尽管如此,我们也不可能对规范细节面面俱到,做到完全统一规范。

总之一句话:靠人的自觉和监督不可能做到 100% 的规范统一。如果我们要高效、快速、可靠地统一代码规范,必须要借助工具来实现,工具可以帮助我们自动化处理这些逻辑性的任务。

  1. 检测规范的工具

统一规范的前提是检测规范,因此需要一个实时检测代码的工具,对不规范的代码给出提示,这样我们可以快速定位到不规范的位置并对其进行修复。

前端圈中最流行的代码检测工具名为 ESLint。它支持自定义丰富的代码规则,然后根据这些规则检测源代码是否符合规范。ESLint 很强大,它能最大程度地保证代码质量,在团队协作的项目中不可或缺。

TypeScript 也有检查代码规范的功能。不同的是:TypeScript 只会检查类型错误,而 ESLint 会检查风格错误。严格的代码检查应该同时包含 TypeScript 和 ESLint,它们并不冲突。

  1. 统一规范的工具

统一规范的工具一般特指代码格式化工具 ——— 格式化是指将已有代码用规范的格式重置,不会改变代码逻辑,只是将不规范的代码变成规范的代码。

这样的话,我们甚至不需要关心代码规范,只要制定好一些统一的规则,然后在某个时机自动格式化代码,每个人产出的代码格式就是一样的。这是最高效且优雅的统一规范的方式。

那么有没有这样的工具呢?当然有,前端圈中最流行的代码格式化工具是 “Prettier”,它支持通过编辑器插件和命令行两种方式格式化代码,让我们轻松地实现团队代码规范的统一。

接下来详细介绍 ESLint 和 Prettier 这两个工具,并把它们集成到项目中,实现代码规范的检测和统一。