前端知识总结

一.AI前端CSS规范

#分号

每个属性声明后面都要加分号。

#命名

  1. 不使用id选择器
  2. 适用有意义的名词命名
  3. 单词全部小写,名词超过1个时,使用-分隔符

#属性声明顺序

原则:整体到局部,外部到内部,重要属性优先

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.element {
display: block;
float: left;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 100;
margin: 0 100px;
padding: 50px; // padding习惯写到margin后面
width: 100px;
height: 100px;
border: 1px solid #e5e5e5; border-radius: 3px;
font: normal 13px "Helvetica Neue", sans-serif;
color: #333;
text-align: center;
line-height: 1.5;
background-color: #f5f5f5;
opacity: 1;
}

Copied!

#其他规范

使用prettier格式化工具约束,推荐配置如下:

  • 格式自动化
  • 4个缩进
  • 全部单引号
  • 属性:后有空格
  • 颜色全部小写
  • 小数点前面0自动添加
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
module.exports = {
printWidth: 100, // 设置prettier单行输出(不折行)的(最大)长度

tabWidth: 4, // 设置工具每一个水平缩进的空格数

useTabs: false, // 使用tab(制表位)缩进而非空格

semi: false, // 在语句末尾添加分号

singleQuote: true, // 使用单引号而非双引号

trailingComma: 'none', // 在任何可能的多行中输入尾逗号

bracketSpacing: true, // 在对象字面量声明所使用的的花括号后({)和前(})输出空格

arrowParens: 'avoid', // 为单行箭头函数的参数添加圆括号,参数个数为1时可以省略圆括号

// parser: 'babylon', // 指定使用哪一种解析器

jsxBracketSameLine: true, // 在多行JSX元素最后一行的末尾添加 > 而使 > 单独一行(不适用于自闭和元素)

rangeStart: 0, // 只格式化某个文件的一部分

rangeEnd: Infinity, // 只格式化某个文件的一部分

filepath: 'none', // 指定文件的输入路径,这将被用于解析器参照

requirePragma: false, // (v1.7.0+) Prettier可以严格按照按照文件顶部的一些特殊的注释格式化代码,这些注释称为“require pragma”(必须杂注)

insertPragma: false, // (v1.8.0+) Prettier可以在文件的顶部插入一个 @format的特殊注释,以表明改文件已经被Prettier格式化过了。

proseWrap: 'preserve' // (v1.8.2+)
}

Copied!

#参考连接

百度CSS规范指南(opens new window)

腾讯CSS规范指南(opens new window)

Google CSS规范指南

二、AI前端Git规范

#Git分支命名

  • master:主分支,负责记录上线版本的迭代,该分支代码与线上代码是完全一致的。
  • develop:开发分支,该分支记录相对稳定的版本,所有的feature分支和bugfix分支都从该分支创建。其它分支为短期分支,其完成功能开发之后需要删除
  • feature/*:特性(功能)分支,用于开发新的功能,不同的功能创建不同的功能分支,功能分支开发完成并自测通过之后,需要合并到 develop 分支,之后删除该分支。
  • bugfix/*:bug修复分支,用于修复不紧急的bug,普通bug均需要创建bugfix分支开发,开发完成自测没问题后合并到 develop 分支后,删除该分支。
  • release/*:发布分支,用于代码上线准备,该分支从develop分支创建,创建之后由测试同学发布到测试环境进行测试,测试过程中发现bug需要开发人员在该release分支上进行bug修复,所有bug修复完后,在上线之前,需要合并该release分支到master分支和develop分支。
  • hotfix/*:紧急bug修复分支,该分支只有在紧急情况下使用,从master分支创建,用于紧急修复线上bug,修复完成后,需要合并该分支到master分支以便上线,同时需要再合并到develop分支。

img

#Git Commit Message格式

type : subject

#type 提交类型:

  • feature: 新特性
  • fix: 修改问题
  • style: 代码格式修改
  • test: 测试用例修改
  • docs: 文档修改
  • refactor: 代码重构
  • misc: 其他修改, 比如构建流程, 依赖管理

#subject 提交描述

对应内容是commit 目的的简短描述,一般不超过50个字符

#参考链接

三、AI前端JS规范

#变量

命名方式:小驼峰

命名规范:前缀名词

1
2
3
4
5
6
7
8
// bad
let setCount = 10

// good
let maxCount = 10

Copied!

#常量

命名方式:全部大写

命名规范:多个单词时使用分隔符_

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// bad
const serverErrorCode = {
success: 200,
repeat: 444
}

// good
const SERVER_ERROR_CODE = {
SUCCESS: 200,
REPEAT: 444
}

Copied!

#函数

命名方式:小驼峰

命名规范:前缀动词

1
2
3
4
5
6
7
8
// bad
function wordClass() {}

// good
function saveWordClass() {}

Copied!

常用动词:can、has、is、load、get、set

#

命名方式:大驼峰

命名规范:前缀名词

1
2
3
4
5
6
7
8
// bad
class person {}

// good
class Person {}

Copied!

#注释

#单行

1
2
3
4
5
// 单行注释,注意前面的空格
let maxCount = 123

Copied!

#多行

1
2
3
4
5
6
7
/**
* 多行注释
* /


Copied!

#减少嵌套

确定条件不允许时,尽早返回。经典使用场景:校验数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// bad
if (condition1) {
if (condition2) {
...
}
}

// good
if (!condition1) return
if (!condition2) return
...

Copied!

#减少特定标记值

使用常量进行自解释

1
2
3
4
5
6
7
8
9
10
11
12
13
// bad
type: 1 // 1代表新增 2代表修改

// good
const MODIFY_TYPE = {
ADD: 1,
EDIT: 2
}

type: MODIFY_TYPE.ADD

Copied!

#表达式

尽可能简洁表达式

1
2
3
4
5
6
7
8
9
10
11
12
// bad
if (name === ''){}
if (collection.length > 0){}
if (notTrue === false){}

// good
if (!name) {}
if (collection.length){}
if (notTrue){}

Copied!

#分支较多处理

对于相同变量或表达式的多值条件,用switch代替if

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// bad
let type = typeof variable
if (type === 'object') {
// ......
}
else if (type === 'number' || type === 'boolean' || type === 'string') {
// ......
}

// good
switch (typeof variable) {
case 'object':
// ......
break
case 'number':
case 'boolean':
case 'string':
// ......
break
}

Copied!

#使用变量名自解释 V1.1

逻辑复杂时,建议使用变量名自解释,而不是晦涩难懂的简写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// bad
function(value) {
return !helpers.req(value) || this.entity.entVocabularyEntries.filter(item => item.vocabularyEntryName === value).length < 2
}

// good
function(value) {
let entVocabularyList = this.entity.entVocabularyEntries
let repeatCount = entVocabularyList.filter(item => item.vocabularyEntryName === value).length
return !helpers.req(value) || repeatCount < 2
}

Copied!

#使用函数名自解释 V1.1

遵循单一职责的基础上,可以把逻辑隐藏在函数中,同时使用准确的函数名自解释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// bad
if (modifyType === MODIFY_TYPE.ADD) {
batchVariableAPI(data).then(() => {
this.closeModal()
this.$toast.show('添加变量成功')
})
} else {
updateVariableAPI(data).then(() => {
this.closeModal()
this.$toast.show('修改变量成功')
})
}

// good
modifyType === MODIFY_TYPE.ADD ? this._insertVariable(data) : this._updateVariable(data)

_insertVariable() {
batchVariableAPI(data).then(() => this._successOperation('添加变量成功'))
}

_updateVariable() {
updateVariableAPI(data).then(() => this._successOperation('修改变量成功'))
}

_successOperation(toastMsg) {
this.closeModal()
this.$toast.show(toastMsg)
}

Copied!

#其他规范

使用prettier格式化工具以及eslint校验

  • 格式自动化
  • 4个缩进
  • 全部单引号
  • 方法if / else / for / while / function / switch / do / try / catch / finally 关键字后有一个空格
  • 自动省略分号

.prettierrc配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
module.exports = {
printWidth: 100, // 设置prettier单行输出(不折行)的(最大)长度

tabWidth: 4, // 设置工具每一个水平缩进的空格数

useTabs: false, // 使用tab(制表位)缩进而非空格

semi: false, // 在语句末尾添加分号

singleQuote: true, // 使用单引号而非双引号

trailingComma: 'none', // 在任何可能的多行中输入尾逗号

bracketSpacing: true, // 在对象字面量声明所使用的的花括号后({)和前(})输出空格

arrowParens: 'avoid', // 为单行箭头函数的参数添加圆括号,参数个数为1时可以省略圆括号

// parser: 'babylon', // 指定使用哪一种解析器

jsxBracketSameLine: true, // 在多行JSX元素最后一行的末尾添加 > 而使 > 单独一行(不适用于自闭和元素)

rangeStart: 0, // 只格式化某个文件的一部分

rangeEnd: Infinity, // 只格式化某个文件的一部分

filepath: 'none', // 指定文件的输入路径,这将被用于解析器参照

requirePragma: false, // (v1.7.0+) Prettier可以严格按照按照文件顶部的一些特殊的注释格式化代码,这些注释称为“require pragma”(必须杂注)

insertPragma: false, // (v1.8.0+) Prettier可以在文件的顶部插入一个 @format的特殊注释,以表明改文件已经被Prettier格式化过了。

proseWrap: 'preserve' // (v1.8.2+)
}

Copied!

.eslintrc.js规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
module.exports = {
root: true,
env: {
browser: true,
node: true
},
extends: ['plugin:vue/essential'],
parserOptions: {
parser: 'babel-eslint'
},
plugins: ['vue'],
// add your custom rules here
rules: {
'arrow-parens': 0, // allow paren-less arrow functions

'generator-star-spacing': 0, // allow async-await

'no-unused-vars': 'error', // disabled no ununsed var `V1.1`

'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', // no use debugger in production

'indent': [2, 4, { SwitchCase: 1 }], // 4 space for tab for perttier

'space-before-function-paren': ['error', 'never'], // no space in function name for perttier
}
}

Copied!

如果是vue-cli3项目,以上配置的eslint插件默认已安装;如果不是vue-cli3项目,需要npm安装对应包:npm install –save-dev babel-eslint eslint-plugin-vue

#参考链接

百度JS规范

四、AI前端Vue规范

#文件命名

统一小写,多个单词作为文件名使用分隔符-

1
2
3
4
5
6
7
8
9
// bad
EntityList.vue
entityList.vue

// good
entity-list.vue

Copied!

#紧密耦合的组件命名

和父组件紧密耦合的子组件应该以父组件名作为前缀命名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// bad
components/
|- todo-list.vue
|- todo-item.vue
└─ todo-button.vue

// good
components/
|- todo-list.vue
|- todo-list-item.vue
└─ todo-list-item-button.vue

Copied!

#自闭合组件

在单文件组件中没有内容的组件应该是自闭合的

1
2
3
4
5
6
7
8
<!-- bad -->
<u-input></u-input>

<!-- good -->
<u-input />

Copied!

#指令缩写

: 表示 v-bind: ,用@表示v-on

1
2
3
4
5
6
7
8
<!-- bad -->
<input v-bind:value="value" v-on:input="onInput">

<!-- good -->
<input :value="value" @input="onInput">

Copied!

#组件数据

组件的 data 必须是一个函数,并且建议在此不使用箭头函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// bad
export default {
data: () => ({
foo: 'bar'
})
}

// good
export default {
data () {
return {
foo: 'bar'
}
}
}

Copied!

#props命名

小驼峰命名。内容尽量详细,至少有默认值

1
2
3
4
5
6
7
8
// bad
greeting-text: String

// good
greetingText: { type: String, default: ''}

Copied!

#组件属性顺序和分行规则

顺序原则:重要属性放前面

顺序依据:依次指令->props属性-> 事件->dom属性(class有标记作用,除外)

分行规则:放在一行,重要内容较多时,可放置2~3行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- bad -->
<u-select
class="select"
size="s"
@select="searchEntity($event, row)"
@blur="searchEntity($event, row)"
v-model="row.variableId"
:list="variableList" />

<!-- good -->
<u-select v-model="row.variableId" :list="variableList" size="s"
@select="searchEntity($event, row)" @blur="searchEntity($event, row)" class="select" />

Copied!

#Vue API顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
export default {
name: '',
/*1. Vue扩展 */
extends: '', // extends和mixins都扩展逻辑,需要重点放前面
mixins: [],
components: {},
/* 2. Vue数据 */
props: {},
model: { prop: '', event: '' }, // model 会使用到 props
data () {
return {}
},
computed: {},
watch:{}, // watch 监控的是 props 和 data,有必要时监控computed
/* 3. Vue资源 */
filters: {},
directives: {},
/* 4. Vue生命周期 */
created () {},
mounted () {},
destroy () {},
/* 5. Vue方法 */
methods: {}, // all the methods should be put here in the last
}

Copied!

#Vue组件顶级标签顺序

顺序保持一致,且标签之间留有空行。template第一层级下四个空格,script和style第一层级都不加空格

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div></div>
</template>

<script>
export default {}
</script>

<style>
.app {}
</style>

Copied!

#import引入顺序 V1.1

原则:同等类型的放一起,优先放mixins和components等UI资源。忌随意放置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// bad
import { getAllEntityList, getVariableGroup } from '@/server/api'
import { helpers } from 'vuelidate/lib/validators'
import { getRepeatLine } from '@/utils/common'
import { CloseModalMixin, InvalidCheckMixin } from '@/components/common/mixins'
import VSearchSelect from '@/components/variable/v-search-select'
import EModifyModal from '@/components/entity/e-modify-modal'
import { MODIFY_MODAL_TYPE } from '@/utils/config'
import { botIdLoc, custIdLoc } from '@/utils/locs'

// good
import { CloseModalMixin, InvalidCheckMixin } from '@/components/common/mixins'
import VSearchSelect from '@/components/variable/v-search-select'
import EModifyModal from '@/components/entity/e-modify-modal'
import { getAllEntityList, getVariableGroup } from '@/server/api'
import { helpers } from 'vuelidate/lib/validators'
import { MODIFY_MODAL_TYPE } from '@/utils/config'
import { getRepeatLine } from '@/utils/common'
import { botIdLoc, custIdLoc } from '@/utils/locs'

Copied!

#Vue 复杂data加注释/分组 V1.1

data数据是连接View和Modal的基础,当ViewModal复杂时,建议进行注释并分组。另外,当data过于复杂时应考虑优化重构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// bad
data() {
return {
isOpenModal: false,
list: [],
groupList: [],
searchParams: { groupId: '', searchParam: '', searchType: '' },
pageParam: { currentPage: 1, pageSize: 50 },
totalCount: 0,
groupId: '',
typeList: [],
defaultType: 'paramName'
}
}

// good
data() {
return {
variableList: [],
groupList: [],
typeList: [],

/*
* 查询参数
* 组与组之间通过空行区分
*/
searchParams: { groupId: '', searchParam: '', searchType: '', currentPage: 1, pageSize: 50 },
totalCount: 0,
defaultType: '',

isOpenModal: false
}
}

Copied!

五、AI前端工程工具链

为了提高整体开发效率,需要定制一些vue/electron/官网等脚手架工具。首先要将一些代码规范考虑在内,需要保持git仓库的代码就像是一个人写出来的。根据团队习惯,考虑后使用组合工具:

  1. 规范提交代码(必需)eslint + stylelint + prettier + husky + lint-staged
  2. 规范commit日志(必需)commitlint + commitizen + cz-conventional-changelog + conventional-changelog-cli
  3. 规范提交流程(github项目)release-it + gh-pages
  4. 辅助代码分析(可选)jsinspect + jscpd

#1. 规范提交代码

针对写出符合团队代码规范的js、css代码

  1. eslint: 对js做规则约束。强制校验
  2. stylelint: 对css做规则约束
  3. prettier: 代码格式化。强制格式化
  4. **husky + lint-staged (opens new window)**:本地git钩子工具

#1.1 eslint(opens new window)

#1.1.1 安装
1
2
3
4
npm install --save-dev eslint eslint-plugin-vue babel-eslint

Copied!

#1.1.2 .eslintrc.js配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
module.exports = {
root: true,
// 指定代码的运行环境。不同的运行环境,全局变量不一样
env: {
browser: true,
node: true
},
parserOptions: {
// ESLint 默认使用Espree作为其解析器,安装了 babel-eslint 用来代替默认的解析器
parser: 'babel-eslint'
},
// 使得不需要自行定义大量的规则
extends: [
// consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
'plugin:vue/essential'
],
// 插件
plugins: [
'vue'
],
// add your custom rules here
rules: {
// allow async-await
'generator-star-spacing': 'off',
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'indent': [2, 4, { 'SwitchCase': 1 }],
...
}
}

Copied!

#1.1.3 提交前强制校验

将约束命令放置在提交代码前检查,这就要使用husky (opens new window)+ lint-staged (opens new window)工具,工具能在提交代码precommit时调用钩子命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
npm install husky lint-staged -D

Copied!

"scripts": {
"lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
},
"gitHooks": {
"pre-commit": "lint-staged"
},
"lint-staged": {
"*.{js,vue,ts}": [
"npm run lint"
]
},

Copied!

TIP

如果是vue-cli3项目创建的项目,以上配置都自动生成了,直接执行vue-cli-service lint即可。

#1.2 prettier(opens new window)

#1.2.1 安装
1
2
3
4
npm install --save-dev prettier

Copied!

#1.2.2. .prettierrc.js配置

以下感谢团队伙伴@Birttany的配置说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
module.exports = {
printWidth: 100, // 设置prettier单行输出(不折行)的(最大)长度

tabWidth: 4, // 设置工具每一个水平缩进的空格数

useTabs: false, // 使用tab(制表位)缩进而非空格

semi: false, // 在语句末尾添加分号

singleQuote: true, // 使用单引号而非双引号

trailingComma: 'none', // 在任何可能的多行中输入尾逗号

bracketSpacing: true, // 在对象字面量声明所使用的的花括号后({)和前(})输出空格

arrowParens: 'avoid', // 为单行箭头函数的参数添加圆括号,参数个数为1时可以省略圆括号

parser: 'babylon', // 指定使用哪一种解析器

jsxBracketSameLine: true, // 在多行JSX元素最后一行的末尾添加 > 而使 > 单独一行(不适用于自闭和元素)

rangeStart: 0, // 只格式化某个文件的一部分

rangeEnd: Infinity, // 只格式化某个文件的一部分

filepath: 'none', // 指定文件的输入路径,这将被用于解析器参照

requirePragma: false, // (v1.7.0+) Prettier可以严格按照按照文件顶部的一些特殊的注释格式化代码,这些注释称为“require pragma”(必须杂注)

insertPragma: false, // (v1.8.0+) Prettier可以在文件的顶部插入一个 @format的特殊注释,以表明改文件已经被Prettier格式化过了。

proseWrap: 'preserve' // (v1.8.2+)
}

Copied!

#1.2.3 提交前强制格式化

在提交git时需要对整个项目执行format格式化,使得代码强制统一。格式化之后再用eslint检查语法错误,无误后把格式化后的代码用git add .添加进入。如果有错误直接中断提交。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"scripts": {
"lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
},
"gitHooks": {
"pre-commit": "lint-staged"
},
"lint-staged": {
"*.{js,vue,ts}": [
"prettier --write",
"npm run lint"
]
},

Copied!

#1.3 stylelint

参见另一篇博客(opens new window)

#2. 规范commit日志(必需)

在多人协作的项目中,如果Git的提交说明精准,在后期协作以及Bug处理时会变得有据可查,项目的开发可以根据规范的提交说明快速生成开发日志,从而方便开发者或用户追踪项目的开发信息和功能特性。

规范的Git提交说明:

  • 提供更多的历史信息,方便快速浏览
  • 可以过滤某些commit,便于筛选代码review
  • 可以追踪commit生成更新日志
  • 可以关联issues

Git提交说明结构:

Git提交说明可分为三个部分:Header、Body和Footer。

1
2
3
4
5
6
7
8
<Header>

<Body>

<Footer>

Copied!

Header部分包括三个字段type(必需)、scope(可选)和subject(必需)。

1
2
3
4
<type>(<scope>): <subject>

Copied!

type用于说明 commit 的提交性质。(Angular规范)

  • feat 新增一个功能
  • fix 修复一个Bug
  • docs 文档变更
  • style 代码格式(不影响功能,例如空格、分号等格式修正)
  • refactor 代码重构
  • perf 改善性能
  • test 测试
  • build 变更项目构建或外部依赖(例如scopes: webpack、gulp、npm等)
  • ci 更改持续集成软件的配置文件和package中的scripts命令,例如scopes: Travis, Circle等
  • chore 变更构建流程或辅助工具
  • revert 代码回退

#2.1 commitlint(opens new window)

约定commit message提交的格式

#2.1.1 安装
1
2
3
4
npm install --save-dev @commitlint/config-conventional @commitlint/cli

Copied!

#2.1.2 配置

在根目录执行以下命令作为commitlint的配置

1
2
3
4
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js

Copied!

#2.1.2 提交前校验
1
2
3
4
5
6
7
8
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
}

Copied!

#2.2 commitizen cz-conventional-changelog

commitlint约定了提交的格式,但每次书写都需要记住那些约定,增加记忆负担。所以使用cz工具让提交commit过程更加可视化

commitizen/cz-cli是一个可以实现规范的提交说明的工具,提供选择的提交信息类别,快速生成提交说明。

#2.2.1 安装
1
2
3
4
npm i commitizen cz-conventional-changelog -D

Copied!

#2.2.2 配置

使用cz-conventional-changelog适配器(介于AngularJS规范),约定提交代码时的选项。

1
2
3
4
5
6
7
8
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
}

Copied!

#2.2.3 命令执行

以后git commit都使用npm run commit(即git-cz)代替

1
2
3
4
5
6
"scripts": {
"commit": "git add -A && git-cz"
},

Copied!

commitlint + commitizen cz-conventional-changelog + conventional-changelog-cli

#2.3 conventional-changelog-cli(opens new window)

自动根据commit生成changelog

良好的changelog可以让用户一眼知道组件库的版本迭代演进,也是长期维护项目的必备内容。

#2.3.1 安装
1
2
3
4
npm install conventional-changelog-cli -D

Copied!

#2.3.2 命令执行
1
2
3
4
5
6
"scripts": {
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0"
},

Copied!

TIP

每次发布组件包版本后,即可使用npm run changelog命令,自动根据git commit生成日志。

#3. 规范提交流程(可选)

针对github + npm项目,可以简化流程。内部非开源项目可跳过该内容。

#3.1 安装

1
2
3
4
npm i gh-pages release-it -D

Copied!

#3.2 命令执行

1
2
3
4
5
6
7
"scripts": {
"release": "release-it",
"release:docs": "npm run build:docs && gh-pages -d docs/.vuepress/dist
},

Copied!

#4. 辅助代码分析(可选)

敏捷开发过程中,代码复查是至关重要的一环,团队需要使用工具辅助代码分析。经比较和实践后,使用工具:jsinspect + jscpd

  1. jsinspect: 对js或jsx代码做重复检测。
  2. jscpd: 对代码重复率进行报告总结,辅助代码复查

#4.1 jsinspect(opens new window)

#4.1.1 安装
1
2
3
4
npm install jsinspect --save-dev

Copied!

#4.1.2 提交前强制校验
1
2
3
4
5
6
"scripts": {
"inspect": "jsinspect -t 50 ./src",
}

Copied!

#4.2 jscpd(opens new window)

#4.2.1 安装
1
2
3
4
npm install jscpd --save-dev

Copied!

#4.2.2 代码复查辅助命令
1
2
3
4
5
6
"scripts": {
"codereview": "jscpd ./src"
}

Copied!

#总结

  • eslint + husky + lint-staged + prettier作为基础工具。
  • commitlint约束提交规范,commitizen让工具通过select选项,进行git commit, conventional-changelog-cli自动根据git commit生成changelog文件。
  • release-it核心是可以把包自动发送到npm上,gh-pages把指定目录发布到github gh-pages分支上。

配置好以上工具,开发组件库时常用三个命令:

  • 平时开发提交commit: npm run commit “commit”: “git add -A && git-cz”,
  • 发布npm包: npm publish “prepublish”: “npm run build && release-it && npm run changelog”,
  • 发布docs: npm run docs “release:docs”: “npm run build:docs && gh-pages -d docs/.vuepress/dist”,

以下示例个实际github项目配置: lq782655835/yi-ui(opens new window)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
"scripts": {
"commit": "git add -A && git-cz",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
"release": "release-it",
"lint": "vue-cli-service lint"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"lint-staged": {
"packages/**/*.{vue,ts,tsx,js,jsx,json}": [
"prettier --write",
"npm run lint"
]
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
}

Copied!

六、AI JavaScript 风格指南

Original Repository: ryanmcdermott/clean-code-javascript(opens new window)

笔者注:本文原文地址 (opens new window),作者根据 Robert C. Martin 《代码整洁之道》总结了适用于 JavaScript 的软件工程原则。该风格指南全面而实用,非常适合作为前端团队代码质量参考标准,也为新融入团队的同学提供一份代码质量指南。

1.#目录

  1. 介绍
  2. 变量
  3. 函数
  4. 对象和数据结构
  5. 测试
  6. 并发
  7. 错误处理
  8. 格式化
  9. 注释

2.#介绍

作者 (opens new window)根据 Robert C. Martin 《代码整洁之道》 (opens new window)总结了适用于 JavaScript 的软件工程原则《Clean Code JavaScript》 (opens new window)

本文是对其的翻译。

不必严格遵守本文的所有原则,有时少遵守一些效果可能会更好,具体应根据实际情况决定。这是根据《代码整洁之道》作者多年经验整理的代码优化建议,但也仅仅只是一份建议。

软件工程已经发展了 50 多年,至今仍在不断前进。现在,把这些原则当作试金石,尝试将他们作为团队代码质量考核的标准之一吧。

最后你需要知道的是,这些东西不会让你立刻变成一个优秀的工程师,长期奉行他们也并不意味着你能够高枕无忧不再犯错。千里之行,始于足下。我们需要时常和同行们进行代码评审,不断优化自己的代码。不要惧怕改善代码质量所需付出的努力,加油。

3.#变量

#使用有意义,可读性好的变量名

反例:

1
2
3
4
var yyyymmdstr = moment().format('YYYY/MM/DD');

Copied!

正例:

1
2
3
4
var yearMonthDay = moment().format('YYYY/MM/DD');

Copied!

回到目录

#使用 ES6 的 const 定义常量

反例中使用”var”定义的”常量”是可变的。

在声明一个常量时,该常量在整个程序中都应该是不可变的。

反例:

1
2
3
4
var FIRST_US_PRESIDENT = "George Washington";

Copied!

正例:

1
2
3
4
const FIRST_US_PRESIDENT = "George Washington";

Copied!

回到目录

#对功能类似的变量名采用统一的命名风格

反例:

1
2
3
4
5
6
getUserInfo();
getClientData();
getCustomerRecord();

Copied!

正例:

1
2
3
4
getUser();

Copied!

回到目录

#使用易于检索名称

我们需要阅读的代码远比自己写的要多,使代码拥有良好的可读性且易于检索非常重要。阅读变量名晦涩难懂的代码对读者来说是一种相当糟糕的体验。 让你的变量名易于检索。

反例:

1
2
3
4
5
6
7
// 525600 是什么?
for (var i = 0; i < 525600; i++) {
runCronJob();
}

Copied!

正例:

1
2
3
4
5
6
7
8
// Declare them as capitalized `var` globals.
var MINUTES_IN_A_YEAR = 525600;
for (var i = 0; i < MINUTES_IN_A_YEAR; i++) {
runCronJob();
}

Copied!

回到目录

#使用说明变量(即有意义的变量名)

反例:

1
2
3
4
5
const cityStateRegex = /^(.+)[,\\s]+(.+?)\s*(\d{5})?$/;
saveCityState(cityStateRegex.match(cityStateRegex)[1], cityStateRegex.match(cityStateRegex)[2]);

Copied!

正例:

1
2
3
4
5
6
7
8
9
const ADDRESS = 'One Infinite Loop, Cupertino 95014';
var cityStateRegex = /^(.+)[,\\s]+(.+?)\s*(\d{5})?$/;
var match = ADDRESS.match(cityStateRegex)
var city = match[1];
var state = match[2];
saveCityState(city, state);

Copied!

回到目录

#不要绕太多的弯子

显式优于隐式。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
var locations = ['Austin', 'New York', 'San Francisco'];
locations.forEach((l) => {
doStuff();
doSomeOtherStuff();
...
...
...
// l是什么?
dispatch(l);
});

Copied!

正例:

1
2
3
4
5
6
7
8
9
10
11
12
var locations = ['Austin', 'New York', 'San Francisco'];
locations.forEach((location) => {
doStuff();
doSomeOtherStuff();
...
...
...
dispatch(location);
});

Copied!

回到目录

#避免重复的描述

当类/对象名已经有意义时,对其变量进行命名不需要再次重复。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
var Car = {
carMake: 'Honda',
carModel: 'Accord',
carColor: 'Blue'
};

function paintCar(car) {
car.carColor = 'Red';
}

Copied!

正例:

1
2
3
4
5
6
7
8
9
10
11
12
var Car = {
make: 'Honda',
model: 'Accord',
color: 'Blue'
};

function paintCar(car) {
car.color = 'Red';
}

Copied!

回到目录

#避免无意义的条件判断

反例:

1
2
3
4
5
6
7
8
9
10
11
function createMicrobrewery(name) {
var breweryName;
if (name) {
breweryName = name;
} else {
breweryName = 'Hipster Brew Co.';
}
}

Copied!

正例:

1
2
3
4
5
6
function createMicrobrewery(name) {
var breweryName = name || 'Hipster Brew Co.'
}

Copied!

回到目录

4.#函数

#函数参数 (理想情况下应不超过 2 个)

限制函数参数数量很有必要,这么做使得在测试函数时更加轻松。过多的参数将导致难以采用有效的测试用例对函数的各个参数进行测试。

应避免三个以上参数的函数。通常情况下,参数超过两个意味着函数功能过于复杂,这时需要重新优化你的函数。当确实需要多个参数时,大多情况下可以考虑这些参数封装成一个对象。

JS 定义对象非常方便,当需要多个参数时,可以使用一个对象进行替代。

反例:

1
2
3
4
5
6
function createMenu(title, body, buttonText, cancellable) {
...
}

Copied!

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var menuConfig = {
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
}

function createMenu(menuConfig) {
...
}


Copied!

回到目录

#函数功能的单一性

这是软件功能中最重要的原则之一。

功能不单一的函数将导致难以重构、测试和理解。功能单一的函数易于重构,并使代码更加干净。

反例:

1
2
3
4
5
6
7
8
9
10
11
function emailClients(clients) {
clients.forEach(client => {
let clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}

Copied!

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function emailClients(clients) {
clients.forEach(client => {
emailClientIfNeeded(client);
});
}

function emailClientIfNeeded(client) {
if (isClientActive(client)) {
email(client);
}
}

function isClientActive(client) {
let clientRecord = database.lookup(client);
return clientRecord.isActive();
}

Copied!

回到目录

#函数名应明确表明其功能

反例:

1
2
3
4
5
6
7
8
9
10
function dateAdd(date, month) {
// ...
}

let date = new Date();

// 很难理解dateAdd(date, 1)是什么意思

Copied!

正例:

1
2
3
4
5
6
7
8
9
function dateAddMonth(date, month) {
// ...
}

let date = new Date();
dateAddMonth(date, 1);

Copied!

回到目录

#函数应该只做一层抽象

当函数的需要的抽象多于一层时通常意味着函数功能过于复杂,需将其进行分解以提高其可重用性和可测试性。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function parseBetterJSAlternative(code) {
let REGEXES = [
// ...
];

let statements = code.split(' ');
let tokens;
REGEXES.forEach((REGEX) => {
statements.forEach((statement) => {
// ...
})
});

let ast;
tokens.forEach((token) => {
// lex...
});

ast.forEach((node) => {
// parse...
})
}

Copied!

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function tokenize(code) {
let REGEXES = [
// ...
];

let statements = code.split(' ');
let tokens;
REGEXES.forEach((REGEX) => {
statements.forEach((statement) => {
// ...
})
});

return tokens;
}

function lexer(tokens) {
let ast;
tokens.forEach((token) => {
// lex...
});

return ast;
}

function parseBetterJSAlternative(code) {
let tokens = tokenize(code);
let ast = lexer(tokens);
ast.forEach((node) => {
// parse...
})
}

Copied!

回到目录

#移除重复的代码

永远、永远、永远不要在任何循环下有重复的代码。

这种做法毫无意义且潜在危险极大。重复的代码意味着逻辑变化时需要对不止一处进行修改。JS 弱类型的特点使得函数拥有更强的普适性。好好利用这一优点吧。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function showDeveloperList(developers) {
developers.forEach(developer => {
var expectedSalary = developer.calculateExpectedSalary();
var experience = developer.getExperience();
var githubLink = developer.getGithubLink();
var data = {
expectedSalary: expectedSalary,
experience: experience,
githubLink: githubLink
};

render(data);
});
}

function showManagerList(managers) {
managers.forEach(manager => {
var expectedSalary = manager.calculateExpectedSalary();
var experience = manager.getExperience();
var portfolio = manager.getMBAProjects();
var data = {
expectedSalary: expectedSalary,
experience: experience,
portfolio: portfolio
};

render(data);
});
}

Copied!

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function showList(employees) {
employees.forEach(employee => {
var expectedSalary = employee.calculateExpectedSalary();
var experience = employee.getExperience();
var portfolio;

if (employee.type === 'manager') {
portfolio = employee.getMBAProjects();
} else {
portfolio = employee.getGithubLink();
}

var data = {
expectedSalary: expectedSalary,
experience: experience,
portfolio: portfolio
};

render(data);
});
}

Copied!

回到目录

#采用默认参数精简代码

反例:

1
2
3
4
5
6
7
8
function writeForumComment(subject, body) {
subject = subject || 'No Subject';
body = body || 'No text';
}


Copied!

正例:

1
2
3
4
5
6
7
function writeForumComment(subject = 'No subject', body = 'No text') {
...
}


Copied!

回到目录

#使用 Object.assign 设置默认对象

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var menuConfig = {
title: null,
body: 'Bar',
buttonText: null,
cancellable: true
}

function createMenu(config) {
config.title = config.title || 'Foo'
config.body = config.body || 'Bar'
config.buttonText = config.buttonText || 'Baz'
config.cancellable = config.cancellable === undefined ? config.cancellable : true;

}

createMenu(menuConfig);

Copied!

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var menuConfig = {
title: 'Order',
// User did not include 'body' key
buttonText: 'Send',
cancellable: true
}

function createMenu(config) {
config = Object.assign({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
}, config);

// config now equals: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}
// ...
}

createMenu(menuConfig);

Copied!

回到目录

#不要使用标记(Flag)作为函数参数

这通常意味着函数的功能的单一性已经被破坏。此时应考虑对函数进行再次划分。

反例:

1
2
3
4
5
6
7
8
9
10
function createFile(name, temp) {
if (temp) {
fs.create('./temp/' + name);
} else {
fs.create(name);
}
}

Copied!

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
function createTempFile(name) {
fs.create('./temp/' + name);
}

----------


function createFile(name) {
fs.create(name);
}

Copied!

回到目录

#避免副作用

当函数产生了除了“接受一个值并返回一个结果”之外的行为时,称该函数产生了副作用。比如写文件、修改全局变量或将你的钱全转给了一个陌生人等。

程序在某些情况下确实需要副作用这一行为,如先前例子中的写文件。这时应该将这些功能集中在一起,不要用多个函数/类修改某个文件。用且只用一个 service 完成这一需求。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Global variable referenced by following function.
// If we had another function that used this name, now it'd be an array and it could break it.
var name = 'Ryan McDermott';

function splitIntoFirstAndLastName() {
name = name.split(' ');
}

splitIntoFirstAndLastName();

console.log(name); // ['Ryan', 'McDermott'];

Copied!

正例:

1
2
3
4
5
6
7
8
9
10
11
12
function splitIntoFirstAndLastName(name) {
return name.split(' ');
}

var name = 'Ryan McDermott'
var newName = splitIntoFirstAndLastName(name);

console.log(name); // 'Ryan McDermott';
console.log(newName); // ['Ryan', 'McDermott'];

Copied!

回到目录

#不要写全局函数

在 JS 中污染全局是一个非常不好的实践,这么做可能和其他库起冲突,且调用你的 API 的用户在实际环境中得到一个 exception 前对这一情况是一无所知的。

想象以下例子:如果你想扩展 JS 中的 Array,为其添加一个 diff 函数显示两个数组间的差异,此时应如何去做?你可以将 diff 写入 Array.prototype,但这么做会和其他有类似需求的库造成冲突。如果另一个库对 diff 的需求为比较一个数组中首尾元素间的差异呢?

使用 ES6 中的 class 对全局的 Array 做简单的扩展显然是一个更棒的选择。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Array.prototype.diff = function(comparisonArray) {
var values = [];
var hash = {};

for (var i of comparisonArray) {
hash[i] = true;
}

for (var i of this) {
if (!hash[i]) {
values.push(i);
}
}

return values;
}

Copied!

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class SuperArray extends Array {
constructor(...args) {
super(...args);
}

diff(comparisonArray) {
var values = [];
var hash = {};

for (var i of comparisonArray) {
hash[i] = true;
}

for (var i of this) {
if (!hash[i]) {
values.push(i);
}
}

return values;
}
}

Copied!

回到目录

#采用函数式编程

函数式的编程具有更干净且便于测试的特点。尽可能的使用这种风格吧。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const programmerOutput = [
{
name: 'Uncle Bobby',
linesOfCode: 500
}, {
name: 'Suzie Q',
linesOfCode: 1500
}, {
name: 'Jimmy Gosling',
linesOfCode: 150
}, {
name: 'Gracie Hopper',
linesOfCode: 1000
}
];

var totalOutput = 0;

for (var i = 0; i < programmerOutput.length; i++) {
totalOutput += programmerOutput[i].linesOfCode;
}

Copied!

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const programmerOutput = [
{
name: 'Uncle Bobby',
linesOfCode: 500
}, {
name: 'Suzie Q',
linesOfCode: 1500
}, {
name: 'Jimmy Gosling',
linesOfCode: 150
}, {
name: 'Gracie Hopper',
linesOfCode: 1000
}
];

var totalOutput = programmerOutput
.map((programmer) => programmer.linesOfCode)
.reduce((acc, linesOfCode) => acc + linesOfCode, 0);

Copied!

回到目录

#封装判断条件

反例:

1
2
3
4
5
6
if (fsm.state === 'fetching' && isEmpty(listNode)) {
/// ...
}

Copied!

正例:

1
2
3
4
5
6
7
8
9
10
function shouldShowSpinner(fsm, listNode) {
return fsm.state === 'fetching' && isEmpty(listNode);
}

if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
// ...
}

Copied!

回到目录

#避免“否定情况”的判断

反例:

1
2
3
4
5
6
7
8
9
10
function isDOMNodeNotPresent(node) {
// ...
}

if (!isDOMNodeNotPresent(node)) {
// ...
}

Copied!

正例:

1
2
3
4
5
6
7
8
9
10
function isDOMNodePresent(node) {
// ...
}

if (isDOMNodePresent(node)) {
// ...
}

Copied!

回到目录

#避免条件判断

这看起来似乎不太可能。

大多人听到这的第一反应是:“怎么可能不用 if 完成其他功能呢?”许多情况下通过使用多态(polymorphism)可以达到同样的目的。

第二个问题在于采用这种方式的原因是什么。答案是我们之前提到过的:保持函数功能的单一性。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Airplane {
//...
getCruisingAltitude() {
switch (this.type) {
case '777':
return getMaxAltitude() - getPassengerCount();
case 'Air Force One':
return getMaxAltitude();
case 'Cessna':
return getMaxAltitude() - getFuelExpenditure();
}
}
}

Copied!

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Airplane {
//...
}

class Boeing777 extends Airplane {
//...
getCruisingAltitude() {
return getMaxAltitude() - getPassengerCount();
}
}

class AirForceOne extends Airplane {
//...
getCruisingAltitude() {
return getMaxAltitude();
}
}

class Cessna extends Airplane {
//...
getCruisingAltitude() {
return getMaxAltitude() - getFuelExpenditure();
}
}

Copied!

回到目录

#避免类型判断(part 1)

JS 是弱类型语言,这意味着函数可接受任意类型的参数。

有时这会对你带来麻烦,你会对参数做一些类型判断。有许多方法可以避免这些情况。

反例:

1
2
3
4
5
6
7
8
9
10
function travelToTexas(vehicle) {
if (vehicle instanceof Bicycle) {
vehicle.peddle(this.currentLocation, new Location('texas'));
} else if (vehicle instanceof Car) {
vehicle.drive(this.currentLocation, new Location('texas'));
}
}

Copied!

正例:

1
2
3
4
5
6
function travelToTexas(vehicle) {
vehicle.move(this.currentLocation, new Location('texas'));
}

Copied!

回到目录

#避免类型判断(part 2)

如果需处理的数据为字符串,整型,数组等类型,无法使用多态并仍有必要对其进行类型检测时,可以考虑使用 TypeScript。

反例:

1
2
3
4
5
6
7
8
9
10
11
function combine(val1, val2) {
if (typeof val1 == "number" && typeof val2 == "number" ||
typeof val1 == "string" && typeof val2 == "string") {
return val1 + val2;
} else {
throw new Error('Must be of type String or Number');
}
}

Copied!

正例:

1
2
3
4
5
6
function combine(val1, val2) {
return val1 + val2;
}

Copied!

回到目录

#避免过度优化

现代的浏览器在运行时会对代码自动进行优化。有时人为对代码进行优化可能是在浪费时间。

这里可以找到许多真正需要优化的地方(opens new window)

反例:

1
2
3
4
5
6
7
8
9
// 这里使用变量len是因为在老式浏览器中,
// 直接使用正例中的方式会导致每次循环均重复计算list.length的值,
// 而在现代浏览器中会自动完成优化,这一行为是没有必要的
for (var i = 0, len = list.length; i < len; i++) {
// ...
}

Copied!

正例:

1
2
3
4
5
6
for (var i = 0; i < list.length; i++) {
// ...
}

Copied!

回到目录

#删除无效的代码

不再被调用的代码应及时删除。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function oldRequestModule(url) {
// ...
}

function newRequestModule(url) {
// ...
}

var req = newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');


Copied!

正例:

1
2
3
4
5
6
7
8
9
function newRequestModule(url) {
// ...
}

var req = newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');

Copied!

回到目录

5.#对象和数据结构

#使用 getters 和 setters

JS 没有接口或类型,因此实现这一模式是很困难的,因为我们并没有类似 publicprivate 的关键词。

然而,使用 getters 和 setters 获取对象的数据远比直接使用点操作符具有优势。为什么呢?

  1. 当需要对获取的对象属性执行额外操作时。
  2. 执行 set 时可以增加规则对要变量的合法性进行判断。
  3. 封装了内部逻辑。
  4. 在存取时可以方便的增加日志和错误处理。
  5. 继承该类时可以重载默认行为。
  6. 从服务器获取数据时可以进行懒加载。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
class BankAccount {
constructor() {
this.balance = 1000;
}
}

let bankAccount = new BankAccount();

// Buy shoes...
bankAccount.balance = bankAccount.balance - 100;

Copied!

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class BankAccount {
constructor() {
this.balance = 1000;
}

// It doesn't have to be prefixed with `get` or `set` to be a getter/setter
withdraw(amount) {
if (verifyAmountCanBeDeducted(amount)) {
this.balance -= amount;
}
}
}

let bankAccount = new BankAccount();

// Buy shoes...
bankAccount.withdraw(100);

Copied!

回到目录

#让对象拥有私有成员

可以通过闭包完成

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var Employee = function(name) {
this.name = name;
}

Employee.prototype.getName = function() {
return this.name;
}

var employee = new Employee('John Doe');
console.log('Employee name: ' + employee.getName()); // Employee name: John Doe
delete employee.name;
console.log('Employee name: ' + employee.getName()); // Employee name: undefined

Copied!

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var Employee = (function() {
function Employee(name) {
this.getName = function() {
return name;
};
}

return Employee;
}());

var employee = new Employee('John Doe');
console.log('Employee name: ' + employee.getName()); // Employee name: John Doe
delete employee.name;
console.log('Employee name: ' + employee.getName()); // Employee name: John Doe

Copied!

回到目录

6.#

#单一职责原则 (SRP)

如《代码整洁之道》一书中所述,“修改一个类的理由不应该超过一个”。

将多个功能塞进一个类的想法很诱人,但这将导致你的类无法达到概念上的内聚,并经常不得不进行修改。

最小化对一个类需要修改的次数是非常有必要的。如果一个类具有太多太杂的功能,当你对其中一小部分进行修改时,将很难想象到这一修够对代码库中依赖该类的其他模块会带来什么样的影响。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class UserSettings {
constructor(user) {
this.user = user;
}

changeSettings(settings) {
if (this.verifyCredentials(user)) {
// ...
}
}

verifyCredentials(user) {
// ...
}
}

Copied!

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class UserAuth {
constructor(user) {
this.user = user;
}

verifyCredentials() {
// ...
}
}


class UserSettings {
constructor(user) {
this.user = user;
this.auth = new UserAuth(user)
}

changeSettings(settings) {
if (this.auth.verifyCredentials()) {
// ...
}
}
}

Copied!

回到目录

#开/闭原则 (OCP)

“代码实体(类,模块,函数等)应该易于扩展,难于修改。”

这一原则指的是我们应允许用户方便的扩展我们代码模块的功能,而不需要打开 js 文件源码手动对其进行修改。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class AjaxRequester {
constructor() {
// What if we wanted another HTTP Method, like DELETE? We would have to
// open this file up and modify this and put it in manually.
this.HTTP_METHODS = ['POST', 'PUT', 'GET'];
}

get(url) {
// ...
}

}

Copied!

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class AjaxRequester {
constructor() {
this.HTTP_METHODS = ['POST', 'PUT', 'GET'];
}

get(url) {
// ...
}

addHTTPMethod(method) {
this.HTTP_METHODS.push(method);
}
}

Copied!

回到目录

#利斯科夫替代原则 (LSP)

“子类对象应该能够替换其超类对象被使用”。

也就是说,如果有一个父类和一个子类,当采用子类替换父类时不应该产生错误的结果。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class Rectangle {
constructor() {
this.width = 0;
this.height = 0;
}

setColor(color) {
// ...
}

render(area) {
// ...
}

setWidth(width) {
this.width = width;
}

setHeight(height) {
this.height = height;
}

getArea() {
return this.width * this.height;
}
}

class Square extends Rectangle {
constructor() {
super();
}

setWidth(width) {
this.width = width;
this.height = width;
}

setHeight(height) {
this.width = height;
this.height = height;
}
}

function renderLargeRectangles(rectangles) {
rectangles.forEach((rectangle) => {
rectangle.setWidth(4);
rectangle.setHeight(5);
let area = rectangle.getArea(); // BAD: Will return 25 for Square. Should be 20.
rectangle.render(area);
})
}

let rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);

Copied!

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
class Shape {
constructor() {}

setColor(color) {
// ...
}

render(area) {
// ...
}
}

class Rectangle extends Shape {
constructor() {
super();
this.width = 0;
this.height = 0;
}

setWidth(width) {
this.width = width;
}

setHeight(height) {
this.height = height;
}

getArea() {
return this.width * this.height;
}
}

class Square extends Shape {
constructor() {
super();
this.length = 0;
}

setLength(length) {
this.length = length;
}

getArea() {
return this.length * this.length;
}
}

function renderLargeShapes(shapes) {
shapes.forEach((shape) => {
switch (shape.constructor.name) {
case 'Square':
shape.setLength(5);
case 'Rectangle':
shape.setWidth(4);
shape.setHeight(5);
}

let area = shape.getArea();
shape.render(area);
})
}

let shapes = [new Rectangle(), new Rectangle(), new Square()];
renderLargeShapes(shapes);

Copied!

回到目录

#接口隔离原则 (ISP)

“客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。”

在 JS 中,当一个类需要许多参数设置才能生成一个对象时,或许大多时候不需要设置这么多的参数。此时减少对配置参数数量的需求是有益的。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.setup();
}

setup() {
this.rootNode = this.settings.rootNode;
this.animationModule.setup();
}

traverse() {
// ...
}
}

let $ = new DOMTraverser({
rootNode: document.getElementsByTagName('body'),
animationModule: function() {} // Most of the time, we won't need to animate when traversing.
// ...
});


Copied!

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.options = settings.options;
this.setup();
}

setup() {
this.rootNode = this.settings.rootNode;
this.setupOptions();
}

setupOptions() {
if (this.options.animationModule) {
// ...
}
}

traverse() {
// ...
}
}

let $ = new DOMTraverser({
rootNode: document.getElementsByTagName('body'),
options: {
animationModule: function() {}
}
});

Copied!

回到目录

#依赖反转原则 (DIP)

该原则有两个核心点:

  1. 高层模块不应该依赖于低层模块。他们都应该依赖于抽象接口。
  2. 抽象接口应该脱离具体实现,具体实现应该依赖于抽象接口。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class InventoryTracker {
constructor(items) {
this.items = items;

// BAD: We have created a dependency on a specific request implementation.
// We should just have requestItems depend on a request method: `request`
this.requester = new InventoryRequester();
}

requestItems() {
this.items.forEach((item) => {
this.requester.requestItem(item);
});
}
}

class InventoryRequester {
constructor() {
this.REQ_METHODS = ['HTTP'];
}

requestItem(item) {
// ...
}
}

let inventoryTracker = new InventoryTracker(['apples', 'bananas']);
inventoryTracker.requestItems();

Copied!

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class InventoryTracker {
constructor(items, requester) {
this.items = items;
this.requester = requester;
}

requestItems() {
this.items.forEach((item) => {
this.requester.requestItem(item);
});
}
}

class InventoryRequesterV1 {
constructor() {
this.REQ_METHODS = ['HTTP'];
}

requestItem(item) {
// ...
}
}

class InventoryRequesterV2 {
constructor() {
this.REQ_METHODS = ['WS'];
}

requestItem(item) {
// ...
}
}

// By constructing our dependencies externally and injecting them, we can easily
// substitute our request module for a fancy new one that uses WebSockets.
let inventoryTracker = new InventoryTracker(['apples', 'bananas'], new InventoryRequesterV2());
inventoryTracker.requestItems();

Copied!

回到目录

#使用 ES6 的 classes 而不是 ES5 的 Function

典型的 ES5 的类(function)在继承、构造和方法定义方面可读性较差。

当需要继承时,优先选用 classes。

但是,当在需要更大更复杂的对象时,最好优先选择更小的 function 而非 classes。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
var Animal = function(age) {
if (!(this instanceof Animal)) {
throw new Error("Instantiate Animal with `new`");
}

this.age = age;
};

Animal.prototype.move = function() {};

var Mammal = function(age, furColor) {
if (!(this instanceof Mammal)) {
throw new Error("Instantiate Mammal with `new`");
}

Animal.call(this, age);
this.furColor = furColor;
};

Mammal.prototype = Object.create(Animal.prototype);
Mammal.prototype.constructor = Mammal;
Mammal.prototype.liveBirth = function() {};

var Human = function(age, furColor, languageSpoken) {
if (!(this instanceof Human)) {
throw new Error("Instantiate Human with `new`");
}

Mammal.call(this, age, furColor);
this.languageSpoken = languageSpoken;
};

Human.prototype = Object.create(Mammal.prototype);
Human.prototype.constructor = Human;
Human.prototype.speak = function() {};

Copied!

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Animal {
constructor(age) {
this.age = age;
}

move() {}
}

class Mammal extends Animal {
constructor(age, furColor) {
super(age);
this.furColor = furColor;
}

liveBirth() {}
}

class Human extends Mammal {
constructor(age, furColor, languageSpoken) {
super(age, furColor);
this.languageSpoken = languageSpoken;
}

speak() {}
}

Copied!

回到目录

#使用方法链

这里我们的理解与《代码整洁之道》的建议有些不同。

有争论说方法链不够干净且违反了德米特法则 (opens new window),也许这是对的,但这种方法在 JS 及许多库(如 JQuery)中显得非常实用。

因此,我认为在 JS 中使用方法链是非常合适的。在 class 的函数中返回 this,能够方便的将类需要执行的多个方法链接起来。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Car {
constructor() {
this.make = 'Honda';
this.model = 'Accord';
this.color = 'white';
}

setMake(make) {
this.name = name;
}

setModel(model) {
this.model = model;
}

setColor(color) {
this.color = color;
}

save() {
console.log(this.make, this.model, this.color);
}
}

let car = new Car();
car.setColor('pink');
car.setMake('Ford');
car.setModel('F-150')
car.save();

Copied!

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Car {
constructor() {
this.make = 'Honda';
this.model = 'Accord';
this.color = 'white';
}

setMake(make) {
this.name = name;
// NOTE: Returning this for chaining
return this;
}

setModel(model) {
this.model = model;
// NOTE: Returning this for chaining
return this;
}

setColor(color) {
this.color = color;
// NOTE: Returning this for chaining
return this;
}

save() {
console.log(this.make, this.model, this.color);
}
}

let car = new Car()
.setColor('pink')
.setMake('Ford')
.setModel('F-150')
.save();

Copied!

回到目录

#优先使用组合模式而非继承

在著名的设计模式 (opens new window)一书中提到,应多使用组合模式而非继承。

这么做有许多优点,在想要使用继承前,多想想能否通过组合模式满足需求吧。

那么,在什么时候继承具有更大的优势呢?这取决于你的具体需求,但大多情况下,可以遵守以下三点:

  1. 继承关系表现为”是一个”而非”有一个”(如动物->人 和 用户->用户细节)
  2. 可以复用基类的代码(“Human”可以看成是”All animal”的一种)
  3. 希望当基类改变时所有派生类都受到影响(如修改”all animals”移动时的卡路里消耗量)

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Employee {
constructor(name, email) {
this.name = name;
this.email = email;
}

// ...
}

// Bad because Employees "have" tax data. EmployeeTaxData is not a type of Employee
class EmployeeTaxData extends Employee {
constructor(ssn, salary) {
super();
this.ssn = ssn;
this.salary = salary;
}

// ...
}

Copied!

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Employee {
constructor(name, email) {
this.name = name;
this.email = email;

}

setTaxData(ssn, salary) {
this.taxData = new EmployeeTaxData(ssn, salary);
}
// ...
}

class EmployeeTaxData {
constructor(ssn, salary) {
this.ssn = ssn;
this.salary = salary;
}

// ...
}

Copied!

回到目录

7.#测试

一些好的覆盖工具 (opens new window)

一些好的 JS 测试框架 (opens new window)

#单一的测试每个概念

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const assert = require('assert');

describe('MakeMomentJSGreatAgain', function() {
it('handles date boundaries', function() {
let date;

date = new MakeMomentJSGreatAgain('1/1/2015');
date.addDays(30);
date.shouldEqual('1/31/2015');

date = new MakeMomentJSGreatAgain('2/1/2016');
date.addDays(28);
assert.equal('02/29/2016', date);

date = new MakeMomentJSGreatAgain('2/1/2015');
date.addDays(28);
assert.equal('03/01/2015', date);
});
});

Copied!

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const assert = require('assert');

describe('MakeMomentJSGreatAgain', function() {
it('handles 30-day months', function() {
let date = new MakeMomentJSGreatAgain('1/1/2015');
date.addDays(30);
date.shouldEqual('1/31/2015');
});

it('handles leap year', function() {
let date = new MakeMomentJSGreatAgain('2/1/2016');
date.addDays(28);
assert.equal('02/29/2016', date);
});

it('handles non-leap year', function() {
let date = new MakeMomentJSGreatAgain('2/1/2015');
date.addDays(28);
assert.equal('03/01/2015', date);
});
});

Copied!

回到目录

8.#并发

#用 Promises 替代回调

回调不够整洁并会造成大量的嵌套。ES6 内嵌了 Promises,使用它吧。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
require('request').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', function(err, response) {
if (err) {
console.error(err);
}
else {
require('fs').writeFile('article.html', response.body, function(err) {
if (err) {
console.error(err);
} else {
console.log('File written');
}
})
}
})


Copied!

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
require('request-promise').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin')
.then(function(response) {
return require('fs-promise').writeFile('article.html', response);
})
.then(function() {
console.log('File written');
})
.catch(function(err) {
console.error(err);
})


Copied!

回到目录

#Async/Await 是较 Promises 更好的选择

Promises 是较回调而言更好的一种选择,但 ES7 中的 async 和 await 更胜过 Promises。

在能使用 ES7 特性的情况下可以尽量使用他们替代 Promises。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
require('request-promise').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin')
.then(function(response) {
return require('fs-promise').writeFile('article.html', response);
})
.then(function() {
console.log('File written');
})
.catch(function(err) {
console.error(err);
})


Copied!

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function getCleanCodeArticle() {
try {
var request = await require('request-promise')
var response = await request.get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin');
var fileHandle = await require('fs-promise');

await fileHandle.writeFile('article.html', response);
console.log('File written');
} catch(err) {
console.log(err);
}
}

Copied!

回到目录

9.#错误处理

错误抛出是个好东西!这使得你能够成功定位运行状态中的程序产生错误的位置。

#别忘了捕获错误

对捕获的错误不做任何处理是没有意义的。

代码中 try/catch 的意味着你认为这里可能出现一些错误,你应该对这些可能的错误存在相应的处理方案。

反例:

1
2
3
4
5
6
7
8
try {
functionThatMightThrow();
} catch (error) {
console.log(error);
}

Copied!

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try {
functionThatMightThrow();
} catch (error) {
// One option (more noisy than console.log):
console.error(error);
// Another option:
notifyUserOfError(error);
// Another option:
reportErrorToService(error);
// OR do all three!
}

Copied!

#不要忽略被拒绝的 promises

理由同 try/catch

反例:

1
2
3
4
5
6
7
8
9
10
getdata()
.then(data => {
functionThatMightThrow(data);
})
.catch(error => {
console.log(error);
});

Copied!

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
getdata()
.then(data => {
functionThatMightThrow(data);
})
.catch(error => {
// One option (more noisy than console.log):
console.error(error);
// Another option:
notifyUserOfError(error);
// Another option:
reportErrorToService(error);
// OR do all three!
});

Copied!

回到目录

10.#格式化

格式化是一件主观的事。如同这里的许多规则一样,这里并没有一定/立刻需要遵守的规则。可以在这里 (opens new window)完成格式的自动化。

#大小写一致

JS 是弱类型语言,合理的采用大小写可以告诉你关于变量/函数等的许多消息。

这些规则是主观定义的,团队可以根据喜欢进行选择。重点在于无论选择何种风格,都需要注意保持一致性。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var DAYS_IN_WEEK = 7;
var daysInMonth = 30;

var songs = ['Back In Black', 'Stairway to Heaven', 'Hey Jude'];
var Artists = ['ACDC', 'Led Zeppelin', 'The Beatles'];

function eraseDatabase() {}
function restore_database() {}

class animal {}
class Alpaca {}

Copied!

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var DAYS_IN_WEEK = 7;
var DAYS_IN_MONTH = 30;

var songs = ['Back In Black', 'Stairway to Heaven', 'Hey Jude'];
var artists = ['ACDC', 'Led Zeppelin', 'The Beatles'];

function eraseDatabase() {}
function restoreDatabase() {}

class Animal {}
class Alpaca {}

Copied!

回到目录

#调用函数的函数和被调函数应放在较近的位置

当函数间存在相互调用的情况时,应将两者置于较近的位置。

理想情况下,应将调用其他函数的函数写在被调用函数的上方。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class PerformanceReview {
constructor(employee) {
this.employee = employee;
}

lookupPeers() {
return db.lookup(this.employee, 'peers');
}

lookupMananger() {
return db.lookup(this.employee, 'manager');
}

getPeerReviews() {
let peers = this.lookupPeers();
// ...
}

perfReview() {
getPeerReviews();
getManagerReview();
getSelfReview();
}

getManagerReview() {
let manager = this.lookupManager();
}

getSelfReview() {
// ...
}
}

let review = new PerformanceReview(user);
review.perfReview();

Copied!

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class PerformanceReview {
constructor(employee) {
this.employee = employee;
}

perfReview() {
getPeerReviews();
getManagerReview();
getSelfReview();
}

getPeerReviews() {
let peers = this.lookupPeers();
// ...
}

lookupPeers() {
return db.lookup(this.employee, 'peers');
}

getManagerReview() {
let manager = this.lookupManager();
}

lookupMananger() {
return db.lookup(this.employee, 'manager');
}

getSelfReview() {
// ...
}
}

let review = new PerformanceReview(employee);
review.perfReview();

Copied!

回到目录

11.#注释

#只对存在一定业务逻辑复杂性的代码进行注释

注释并不是必须的,好的代码是能够让人一目了然,不用过多无谓的注释。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function hashIt(data) {
// The hash
var hash = 0;

// Length of string
var length = data.length;

// Loop through every character in data
for (var i = 0; i < length; i++) {
// Get character code.
var char = data.charCodeAt(i);
// Make the hash
hash = ((hash << 5) - hash) + char;
// Convert to 32-bit integer
hash = hash & hash;
}
}

Copied!

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function hashIt(data) {
var hash = 0;
var length = data.length;

for (var i = 0; i < length; i++) {
var char = data.charCodeAt(i);
hash = ((hash << 5) - hash) + char;

// Convert to 32-bit integer
hash = hash & hash;
}
}


Copied!

回到目录

#不要在代码库中遗留被注释掉的代码

版本控制的存在是有原因的。让旧代码存在于你的 history 里吧。

反例:

1
2
3
4
5
6
7
doStuff();
// doOtherStuff();
// doSomeMoreStuff();
// doSoMuchStuff();

Copied!

正例:

1
2
3
4
doStuff();

Copied!

回到目录

#不需要版本更新类型注释

记住,我们可以使用版本控制。废代码、被注释的代码及用注释记录代码中的版本更新说明都是没有必要的。

需要时可以使用 git log 获取历史版本。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 2016-12-20: Removed monads, didn't understand them (RM)
* 2016-10-01: Improved using special monads (JP)
* 2016-02-03: Removed type-checking (LI)
* 2015-03-14: Added combine with type-checking (JR)
*/
function combine(a, b) {
return a + b;
}

Copied!

正例:

1
2
3
4
5
6
function combine(a, b) {
return a + b;
}

Copied!

回到目录

#避免位置标记

这些东西通常只能代码麻烦,采用适当的缩进就可以了。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
////////////////////////////////////////////////////////////////////////////////
// Scope Model Instantiation
////////////////////////////////////////////////////////////////////////////////
let $scope.model = {
menu: 'foo',
nav: 'bar'
};

////////////////////////////////////////////////////////////////////////////////
// Action setup
////////////////////////////////////////////////////////////////////////////////
let actions = function() {
// ...
}

Copied!

正例:

1
2
3
4
5
6
7
8
9
10
11
let $scope.model = {
menu: 'foo',
nav: 'bar'
};

let actions = function() {
// ...
}

Copied!

回到目录

#避免在源文件中写入法律评论

将你的 LICENSE 文件置于源码目录树的根目录。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
The MIT License (MIT)

Copyright (c) 2016 Ryan McDermott

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
*/

function calculateBill() {
// ...
}

Copied!

正例:

1
2
3
4
5
6
function calculateBill() {
// ...
}

Copied!

回到目录

12.#补充

  • 大概率坏代码判断
    • 重复代码
    • 过长函数
    • 过大的类
    • 过长参数列表
  • 好的命名
  • 函数单一职责
  • 通过引入解释性变量或函数,使得表达更清晰
  • 更少的嵌套,尽早 return
  • 以多态取代条件表达式
  • 倾向于遍历对象而不是 Switch 语句
  • 使用默认参数和解构
  • 善于利用ES6 Array处理数组
    • Array.find()
    • Array.forEach(),Array.filter(),Array.map(),Array.reduce()
    • 数组rest运算符
    • 多重判断时使用 Array.includes
    • 对 所有/部分 判断使用 Array.every & Array.some
  • 善于利用ES6 Object处理对象
    • 对象解构
    • rest(ES9开始支持)
    • Object.assign()
    • Object.keys(),Object.values()

13.#参考文章

七、推荐-200错误统一处理

对于接口层来说,后端经常定义的结构如下:

1
2
3
4
5
6
7
8
{
code: [Number], // 状态码
desc: [String], // 详细描述
detail: [Array, Object] // 前端需要的数据
}

Copied!

#bad

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
batchAddVariable({ globalParamList: validList })
.then(res => {
if (res === SERVER_ERROR_CODE.SUCCESS) { // !!!Bad: how many interface, how many judge 200
this.close(true)
this.$toast.show(res.detail.colletion) // !!!Bad: always get detail data
} else { // !!!Bad: too much nest,reading difficulty
this.$toast.show(res.desc)
if (res === SERVER_ERROR_CODE.REPEAT) {
...
}
}
})

Copied!

#good

1
2
3
4
5
6
7
8
9
10
11
12
13
batchAddVariable({ globalParamList: validList })
.then(data => {
this.close(true)
this.$toast.show(data.colletion)
})
.catch(res => {
if (res === SERVER_ERROR_CODE.REPEAT) {
...
}
})

Copied!

#解决方案

http层axios拿到数据后进行统一处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import Vue from 'vue'
import axios from 'axios'

const service = axios.create({
baseURL: rootURL, // api的base_url
timeout: 15000, // 请求超时时间
})

// request拦截器
service.interceptors.request.use(
config => {
if (config.method === 'post' || config.method === 'put' || config.method === 'delete') {
config.headers['Content-Type'] = 'application/json'
if (config.type === 'form') {
config.headers['Content-Type'] = 'multipart/form-data'
} else {
// 序列化
config.data = JSON.stringify(config.data)
}
}

return config
},
error => {
Promise.reject(error)
}
)

// respone拦截器
service.interceptors.response.use(
response => {
const res = response.data
if (res.code === SERVER_ERROR_CODE.SUCCESS) { // 统一处理
return res.detail // 直接返回数据
} else {
Vue.prototype.$toast.show(res.desc) // 错误统一报出
return Promise.reject(res)
}
},
error => {
return Promise.reject(error)
}
)

export default service

Copied!

到此基本就可以很优雅的写逻辑代码了。不过还有个点可以继续优化。通常情况,后台返回非200错误,只需要$toast提示结果就行,catch代码部分可以省略。类似如下:

1
2
3
4
5
6
batchAddVariable({ globalParamList: validList })
.then(data => this.close(true))
// .catch(() => {}) // 业务通常这里不需要写

Copied!

多么简洁的代码,但Promise执行reject代码,浏览器会报Uncaught (in promise)错误。这是因为中断了Promise操作但又没有对其进行处理,故由此错误。只需要使用unhandledrejection全局处理即可。

1
2
3
// Promise Catch不报错
window.addEventListener('unhandledrejection', event => event.preventDefault())

八、推荐-优雅引用字体

#编程式字体方法的好处

  1. 学习视觉同学对于具体字体的考量,也许还能发现视觉同学的bug
  2. 全局控制,避免样式散乱
  3. 书写字体样式成为一门艺术

#bad

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.form-title {
font: 'PingFang-SC-medium';
font-size: 18px;
font-color: #22222;
}

.form-text {
font: 'PingFang-SC-regular';
font-size: 14px;
font-color: #333333;
}

Copied!

#good

variables.scss文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$font-normal-color = #222222; // 字体颜色
$font-light-color = #333333;

@mixin font-class($fontFamily, $fontSize, $fontColor) {
font-family: $fontFamily;
font-size: $fontSize;
color: $fontColor;
}

@mixin font-large($size: 14px, $color: $font-normal-color) {
@include font-class($font-family-medium, $size, $color);
}

@mixin font-normal($size: 14px, $color: $font-light-color) {
@include font-class($font-family-regular, $size, $color);
}

Copied!

应用:

1
2
3
4
5
6
7
8
9
10
.form-title {
@include font-large(18px, #22222);
}

.form-text {
@include font-large(14px, #333333);
}

Copied!

font-large/font-normal等公用mixins建议放在统一的variables.scss文件中,再通过sass-resource自动引入到所有组件中

#最佳字体顺序参考

#PC端

1
2
3
4
5
$font-family-medium = 'PingFang-SC-medium', Helveti   ca, Tahoma, Arial, 'Microsoft YaHei', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', sans-serif;
$font-family-regular = 'PingFang-SC-regular', Helvetica, Tahoma, Arial, 'Microsoft YaHei', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', sans-serif;

Copied!

#移动端

1
2
3
4
5
6
7
8
9
$font-family-ultralight = 'PingFangSC-Ultralight', 'Source Han Sans CN', "Helvetica Neue";
$font-family-regular = 'PingFangSC-Regular', 'Source Han Sans CN', "Helvetica Neue";
$font-family-medium = 'PingFangSC-Medium', 'Source Han Sans CN Medium', "Helvetica Neue";
$font-family-thin = 'PingFangSC-Thin', 'Source Han Sans CN Thin', "Helvetica Neue";
$font-family-light = 'PingFangSC-Light', 'Source Han Sans CN Light', "Helvetica Neue";
$font-family-semibold = 'PingFangSC-Semibold', 'Source Han Sans CN Light', "Helvetica Neue";

Copied!

九、推荐-Vue实例选项顺序

在Vue中,export default对象中有很多约定的API Key。每个人的顺序排放都可能不一致,但保持统一的代码风格有助于团队成员多人协作。

Vue官网文档中也有推荐顺序 (opens new window),文档中对选项顺序做了许多分类。但从工程项目角度思考,需要更加精简以及合理的排序。推荐如下规则进行排序:

  1. Vue扩展: extends, mixins, components
  2. Vue数据: props, model, data, computed, watch
  3. Vue资源: filters, directives
  4. Vue生命周期: created, mounted, destroy…
  5. Vue方法: methods

以下推荐顺序,基于团队小伙伴@akimoto整理的顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
export default {
name: '',
/*1. Vue扩展 */
extends: '', // extends和mixins都扩展逻辑,需要重点放前面
mixins: [],
components: {},
/* 2. Vue数据 */
props: {},
model: { prop: '', event: '' }, // model 会使用到 props
data () {
return {}
},
computed: {},
watch:{}, // watch 监控的是 props 和 data,有必要时监控computed
/* 3. Vue资源 */
filters: {},
directives: {},
/* 4. Vue生命周期 */
created () {},
mounted () {},
destroy () {},
/* 5. Vue方法 */
methods: {}, // all the methods should be put here in the last
}

十、组件设计风格

#要求 Element 元素统一使用El后缀

1
2
3
4
5
6
7
8
9
10
11
12
// ✗ bad
const elem = this.$el;
const element = e.target;
const input = this.$refs.input

// ✓ good
const el = this.$el;
const el = e.target;
const inputEl = this.$refs.input;

Copied!

#要求 Vue 实例统一使用VM后缀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ✗ bad
const instance = this;
const form = this.$refs.form;
this.$emit('select', {
item,
});

// ✓ good
const vm = this;
const formVM = this.$refs.form;
this.$emit('select', {
item,
itemVM: selectedVM,
});

Copied!

#被动接收事件方法使用on前缀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ✗ bad
{
methods: {
input() {
// ...
},
handleValidate() {
// ...
},
},
}

// ✓ good
{
methods: {
onInput() {
// ...
},
onValidate() {
// ...
},
},
}

Copied!

#slot 只在对应名称的 class 内设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ✗ bad
<slot name="head">
<div :class="$style.head">
<slot name="title">
<div :class="$style.title">
{{ title }}
</div>
</slot>
<div :class="$style.close"></div>
</div>
</slot>

// ✓ good
<div :class="$style.head">
<slot name="head">
<div :class="$style.title">
<slot name="title">{{ title }}</slot>
</div>
<div :class="$style.close"></div>
</slot>
</div>

Copied!

#变量命名

  • 常见状态:default, primary, info, success, warning, error, disabled, muted, …
  • 大小分级:mini, small, base, large, huge, …
  • 颜色分级:darkest, darker, dark, base, light, lighter, lightest, …

十一、推荐-Vue项目目录结构

目录结构保持一致,使得多人合作容易理解与管理,提高工作效率。Vue标准项目(opens new window)

#简要说明

  • main.js主入口,router.js路由划分
  • plugins 自己或第三方插件,包括但不限于components、directives、filters、third lib
  • pages 所有路由页面。原则:轻page,重component
  • components 所有组件。包括原子组件、业务公用组件、页面独有组件
  • server api引入入口
  • assets sass、图片资源入口,不常修改数据
  • utils 工具文件夹
  • store 标准vuex格式,非必须

#详细说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
project
└───src
│ │ app.vue // 主页面
│ │ main.js // 主入口
| | router.js // 所有路由
│ │
│ |____assets // css、image、svg等资源
│ | |____css // 所有sass资源
| | | | reset.scss // 兼容各浏览器
| | | | global.scss // 全局css
| | | | variable.scss // sass变量和function等
│ | |____img // image图标库
| | |____svg // svg图标库
| |
| |____components // 组件
│ | |____common // common自注册组件
│ | |____base // 原子组件(如果是引入第三方,该文件夹可省略)
│ | | ... // 业务公用组件
│ | |____entity // entity页面组件
│ | |____about // about页面组件
| |
| |____pages // UI层(原则:轻page,重component)
| | |____entity
| | | | list.vue // 列表页
| | | | create.vue // 新增页
| | | | edit.vue // 修改页
| | | main.vue
| |
| |____plugins // 自己或第三方插件
| | | index.js // 插件入口文件
| | | directives.js // 所有Vue指令
| | | filters.js // 所有Vue过滤
| |
| |____server // 接口层
| | | index.js // 所有接口
| | | http.js // axios二次封装
| |
| |____store // vuex数据
| | | index.js
| |
| |____utils // 工具层
| | | config.js// 配置文件,包括常量配置
|
└───public // 公用文件,不经过webpack处理
│ │ favicon.ico
│ │ index.html
│ vue.config.js // vue-cli3主配置
│ babel.config.js// babel配置
│ .eslintrc.js // eslint配置
│ .prettierrc.js // perttier配置
│ package.json // npm配置
│ README.md // 项目说明

Copied!

十二、推荐-Vue-Router写法

使用路由懒加载,实现方式是结合Vue异步组件和Webpack代码分割功能。

优点:

  • 减小包体积,提高加载速度
  • 当页面>20个时,组件定义需要拉到编辑器顶部才知道具体路径

#bad

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import IntentionList from '@/pages/intention/list'
import Variable from '@/pages/variable'
...

{
path: '/intention/list',
name: 'ilist',
component: IntentionList
},
{
path: '/variable',
name: 'variable',
component: Variable
}

Copied!

#good

1
2
3
4
5
6
7
8
9
10
11
12
13
{
path: '/intention/list',
name: 'ilist',
component: () => import('@/pages/intention/list')
},
{
path: '/variable',
name: 'variable',
component: () => import('@/pages/variable')
}

Copied!

import语法需要Babel添加syntax-dynamic-import插件。最新当vue-cli 3.0中默认添加该特性,不需要额外引用。另外,合理控制异步模块的数量。