10.2 代码规范落地
了解了什么是代码规范,以及代码规范包含哪些内容,想必此刻你已经对代码规范有了一个清晰的认识。接下来我们就要将规范在项目中实践落地,这需要思考三个问题:
- 如何制定规范?
- 如何检测规范?
- 如何统一团队的规范?
制定规范就是要设计一套科学合理的代码规范,以标准和高效为目的,这是实施代码规范的基本与核心。检测规范是要检测源代码是否符合制定的规范,发现不规范时及时提醒和警告。统一规范就是将团队中每个成员不同风格的代码强制统一。这三者环环相扣,缺一不可。
实践证明,检测和统一规范往往是落地过程中最困难的环节。不管制定了多么科学合理的规范,只有在团队中长期执行下去规范才有意义,这要从成本和易用性上下功夫。我们先从制定规范开始。
10.2.1 制定规范
前端近几年快速发展,从没有规范到已经基本成体系的规范,相关的实践并不缺乏。在制定规范这件事上,我们不需要从头开始研究,借鉴社区已经流行的规范,从中获取适合自己的即可。
为什么这么做?因为一个成熟的开发者往往都有自己的规范和习惯,这些规范和习惯也都是来源于社区。我们使用社区规范更容易和个人习惯重合,这比自己弄一套规范让大家强制适应要好得多。
1. 命名规范
命名规范是最基本的代码规范。经过社区的不断发展,形成了以下几种通用的命名规范:
- 下划线命名:user_name。
- 中划线命名:user-name。
- 小驼峰命名:userName。
- 大驼峰命名:UserName。
目前这四种命名规范已经被大多数开发者接纳,成为了普遍共识,命名时可以作为基本规范遵循。不过每个开发者往往都有各自的编码习惯,对于同一类命名,可能使用不同的规范。
还是拿变量举例:一个团队中,有的人习惯用下划线命名变量,如 “user_name”;有的人习惯用驼峰命名变量,如 “userName”。这两种命名方式都正确,都符合规范,但是在一个团队中却形成为了两种风格,这显然不合理。我们在制定团队规范时,就是要把不同类的命名规范统一起来。
如何决定使用哪种命名规范呢?笔者推荐一套适合大多数前端项目的命名规则:
- 变量、属性、参数:下划线命名。
- 函数:小驼峰命名。
- 类名、类型:大驼峰命名。
- 文件、文件夹:中划线命名。
将上述的规则应用在编码中,那么你看到的前端代码就是这样的:
// 文件命名
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 行。
依据这些规范,我们写一段与之匹配的代码:
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 文件夹下的目录结构:
|-- 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. 注释规范
技术圈有一个段子:“最讨厌自己写注释,最讨厌别人不写注释”。这反应了写注释虽然繁琐,但是对于合作者来说非常重要,它能快速让别人读懂你的代码并加入协作开发。
如果你的代码规范还好,如果代码不规范且没有注释、或者注释不够清晰,别人就要花大量时间理解这些代码逻辑。特别是对于一些“独特的逻辑和设计”,除了作者本人可能无人知晓,此时没有注释标记就会给合作者埋下隐患。
所以注释很重要,能让人快速看明白的注释更重要,因此我们需要制定注释规范。
- JavaScript 注释
在 JavaScript 代码中有三种注释方式:
- 单行注释。
- 多行注释。
- 函数注释。
单行注释顾名思义只有一行,用于简单的标记信息;多行注释通常是对一段代码块注释,让其暂时不可用,以后需要是再放开;函数注释则是一种更规范的多行注释,举例如下:
// 单行注释
var name = "史泰龙";
/*
name = '威尔史密斯'
console.log('多行注释')
*/
/**
* 函数注释
* @params {Number} id
*/
上面代码中,单行注释最简单,只要在目标行行首加 “//” 符号,多行注释则是将一段代码包裹在符号 “/* */” 中间。函数注释看起来稍微复杂一些,是以 “/**” 开头,可以在注释中使用注释标签(如:@params)。
函数注释中的注释标签表示函数的某个部分,在没有 TypeScript 的时候,这种注释方式很清晰地标记了函数的构造,可以快速了解函数。常用的注释标签如下:
- @desc:表示函数描述。
- @params:表示函数参数。
- @callback:表示回调函数。
- @return:表示函数返回值。
注释标签后的花括号表示数据类型,后面跟着名称和描述信息(可选)。假设函数有一个名为 user_id 的参数,类型是数值,表示用户 ID,函数执行后返回查询到的用户数据,那么注释如下:
/**
* @desc 获取用户信息函数
* @params {Number} user_id 用户ID
* @return {Object} 查询的用户数据
*/
- HTML 与 Vue 注释
除 JavaScript 之外,HTML 也有自己的注释方式,且与 JavaScript 不同。但 HTML 的注释方式只有一种,就是将注释内容包裹在 “” 符号之间,单行和多行通用。如下:
<!-- <h2>单行注释</h2> -->
<!-- <h3>多行注释</h3>
<h4>多行注释</h4> -->
Vue 注释的模版部分遵循 HTML 规则。对于多层嵌套较为复杂的模版,为了更清楚地查阅代码结构,我们推荐将模版按照以下注释规范划分为多个模块,表现得更清晰:
<!--【start】用户模块 -->
<div>
<span>文本信息</span>
<p>内容信息</p>
</div>
<!--【end】用户模块 -->
Vue 注释的脚本部分遵循 JavaScript 规则,直接使用上面介绍的 JavaScript 注释规范即可。
5. 类型规范
类型规范指如何设置 TypeScript 类型。定义类型与定义变量很相似,在 TypeScript 项目中会有数量庞大的类型,因此也会存在类型冲突的风险。类型规范与变量规范类似,也包括了命名规范、分组规范等规则。
- 命名规范
类型通常在代码中与变量、函数等混合使用,因此类型一定要遵守命名规范,将其与变量、函数等区分开来。前面我们介绍过,类型与类名使用大驼峰命名方式,遵循该规则就不会与变量、函数混淆。
那么如何区分类型与类名呢?实际上我们不需要关心这个问题。在 TypeScript 中,当我们创建一个类时,会同时创建一个同名的实例接口类型,所以你可以把类当成一个接口类型来使用。假设定义一个 Person 类,代码如下:
class Person {
constructor(name: string) {
this.name = name;
}
name: string;
getName() {
return this.name;
}
}
Person 类定义后相当于创建了下面的 Person 接口类型,因此直接可以当作类型使用:
interface Person {
name: string
getName(): string
}
var person: Person = {
name: '猪猪侠',
getName() {
return '蜘蛛侠'
},
}
因此,类型的命名规范可以遵守下面两个规则:
- 不存在声明类:大驼峰命名就是类型,直接就能区分。
- 存在声明类:将类名称当作一个接口类型使用,类名与类型一致。
- 分组规范
在 JavaScript 中我们不会把所有变量都定义在全局对象中,这会造成全局变量污染,定义类型也是一个道理。在定义类型时要格外注意避免定义全局类型,要适当地为其进行分组。
类型分组,在 TypeScript 通过命名空间和模块两种方案实现。
命名空间,简单来说就是直接将一批类型划分到一个组下,一个命名空间就是一个组,组内定义的类型不会与其他组的类型冲突。命名空间是最简单直观的避免全局类型冲突的方式,举例如下:
// test.d.ts
declare namespace Person {
interface Action {
play(): void;
}
}
declare namespace Animal {
interface Action {
eat(): void;
}
}
上面例子中将两个 Action 类型分别定义到两个命名空间下,这样就不会有类型冲突。命名空间是最简单直观的分组,可以防止全局类型污染。使用命名空间下的类型方法如下:
var action1: Person.Action;
var action2: Animal.Action;
模块则是另一种分组方式,它与 ES6 的模块机制类似,将类型划分为不同的模块,并通过导入(import)导出(export)来使用对应的模块。模块是更现代化的分组方式:
// 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% 的规范统一。如果我们要高效、快速、可靠地统一代码规范,必须要借助工具来实现,工具可以帮助我们自动化处理这些逻辑性的任务。
- 检测规范的工具
统一规范的前提是检测规范,因此需要一个实时检测代码的工具,对不规范的代码给出提示,这样我们可以快速定位到不规范的位置并对其进行修复。
前端圈中最流行的代码检测工具名为 ESLint。它支持自定义丰富的代码规则,然后根据这些规则检测源代码是否符合规范。ESLint 很强大,它能最大程度地保证代码质量,在团队协作的项目中不可或缺。
TypeScript 也有检查代码规范的功能。不同的是:TypeScript 只会检查类型错误,而 ESLint 会检查风格错误。严格的代码检查应该同时包含 TypeScript 和 ESLint,它们并不冲突。
- 统一规范的工具
统一规范的工具一般特指代码格式化工具 ——— 格式化是指将已有代码用规范的格式重置,不会改变代码逻辑,只是将不规范的代码变成规范的代码。
这样的话,我们甚至不需要关心代码规范,只要制定好一些统一的规则,然后在某个时机自动格式化代码,每个人产出的代码格式就是一样的。这是最高效且优雅的统一规范的方式。
那么有没有这样的工具呢?当然有,前端圈中最流行的代码格式化工具是 “Prettier”,它支持通过编辑器插件和命令行两种方式格式化代码,让我们轻松地实现团队代码规范的统一。
接下来详细介绍 ESLint 和 Prettier 这两个工具,并把它们集成到项目中,实现代码规范的检测和统一。