工欲善其事必先利其器(前端代码规范)

语言: CN / TW / HK

theme: channing-cyan highlight: atelier-cave-dark


前言

工欲善其事,必先利其器。 —— 《论语》

具体到写代码则是需要一套趁手的工具和完整的工作流程,简单讲就是代码产出规范以及自动帮我们进行规范约束的工具

准备

新建项目文件夹,新增index.js写入以下内容 javascript alert('test') console.log('test') eval()

进入文件夹终端,执行以下命令 shell npm i -g pnpm pnpm init

统一包管理器

package.jsonscripts 项增加 preinstall 钩子限制项目使用的包管理器,如 npx only-allow pnpm -y 限制使用 pnpm

关于 scripts 钩子可以参考官网文档,对 only-allow 感兴趣的可以看这篇分析:only-allow 源码学习 ```json "scripts": { "preinstall": "npx only-allow pnpm -y" }

```

ESlint

安装 eslint shell pnpm i -D eslint 新增 .eslintrc 文件,写入以下 rules json { // rules 取值 0、1、2,分别是不处理、警告、禁止 "rules": { "no-eval": 2, "no-alert": 1, // 禁止使用alert confirm prompt "no-console": 0 } } 执行 pnpm eslint "**/*.js",得到以下校验结果 ``` 1:1 warning Unexpected alert no-alert 3:1 error eval can be harmful no-eval

✖ 2 problems (1 error, 1 warning) ```

Typescript 支持

shell pnpm i -D typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin 新增 main.ts文件,输入以下内容 typescript const toString = Object.prototype.toString export function is(val: unknown, type: any): boolean { return toString.call(val) === `[object ${type}]` } alert('test') console.log('s') eval('') .eslintrc修改如下 json { "parser": "@typescript-eslint/parser", "plugins": [ "@typescript-eslint" ], "rules": { "@typescript-eslint/no-explicit-any": 2,// 禁用any "no-eval": 2, "no-alert": 1, // 禁止使用alert confirm prompt "no-console": 0 } } 执行 pnpm eslint "**/*.ts" 得到以下校验结果 ``` 2:40 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 5:1 warning Unexpected alert no-alert 7:1 error eval can be harmful no-eval

✖ 3 problems (2 errors, 1 warning) `` 从上面的例子可以看到ESLint` 还是挺容易配置的,为了方便后面配置,先来了解一些常用配置项

常用配置详解

详细说明请参考 ESLint 配置

parser 和 parserOptions 解析器及其配置

parser作为主解析器使用,parserOptions则是针对解析器的一些补充配置,如parserOptions.parser则可以配置子解析器,也可以针对不同类型文件配置不同的解析器

.vue 文件来举例,使用 vue-eslint-parser 进行解析,文件内不同的内容再使用不同解析器 JSON { "parser": "vue-eslint-parser", "parserOptions": { "parser": { "js": "espree", "ts": "@typescript-eslint/parser", "<template>": "espree", } } } 有些库因其内部做了处理只需要配置 parser 即可,比如parser 指定为 @typescript-eslint/parser 也可以处理 js,比如前面的例子在配置 Typescript 支持后执行 pnpm eslint "**/*.js" 命令同样可以得到检测结果

parserOptions.project

由于项目中可能会使用别名(resolve.alias)或者有其特殊配置,所以需要指定对应配置才能正确解析

比如下面的配置 @typescript-eslint/parser 解析器会根据 project 指定的 tsconfig.json 的配置对 ts 进行解析,这样对于 ts 文件内路径别名 @ 才能正确解析其路径 ```json // .eslintrc "plugins": ["@typescript-eslint"], "parserOptions": { "parser": "@typescript-eslint/parser", "project": "./tsconfig.json" },

// tsconfig.json { "compilerOptions": { "baseUrl": "./", "paths": { "@/": ["src/"] } }, "include": ["src"], "exclude": ["node_modules"] } 还有一些常用的 `parserOptions` 配置如 `ecmaVersion`指定按照哪个 `ecma` 版本规范对`js`解析, `sourceType` 用来指定按照哪种模块规范解析`js`模块 "parserOptions": { "ecmaVersion": "latest", "sourceType": "module" } ```

extends 和 plugins

不想自己配置繁琐的规则可以使用 extends 去继承对应插件的规则,extends可以是字符串也可以是字符串数组,关于ESLint是如何解析和应用extends字段的可以参看源码applyExtends

eslint: 开头的如 eslint:recommended 表示使用 eslint 推荐规则,也有无前缀的如 extends: "airbnb",这是继承 eslint-config-airbnb规则的意思

plugin: 开头的则表示使用插件拓展规则,如 plugin:jsdoc/recommended 表示使用 jsdoc 插件的推荐规则

需要自定义规则或者extends对应规则时一般来讲需要配置对应 plugins 以拓展规则,插件名称可以省略 eslint-plugin- 前缀

如果 rules或者extends 配置了插件的规则而 plugins 没指定对应插件,触发 lint 时可能会因读取不到对应规则而提示失败。

以 eslint-plugin-jsdoc 为例

shell pnpm i -D eslint-plugin-jsdoc .eslintrc 修改如下 json { "parser": "@typescript-eslint/parser", "plugins": [ "@typescript-eslint" ], "extends": [ "plugin:jsdoc/recommended" ], "rules": { "no-eval": 2, "no-alert": 1, // 禁止使用alert confirm prompt "no-console": 0, "@typescript-eslint/no-explicit-any": 2 } } 执行 pnpm eslint "**/*.ts" 得到校验结果可以看到看到 jsdoc的规则提示

``` 2:8 warning Missing JSDoc comment jsdoc/require-jsdoc 2:40 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 5:1 warning Unexpected alert no-alert 7:1 error eval can be harmful no-eval

✖ 4 problems (2 errors, 2 warnings) 0 errors and 1 warning potentially fixable with the --fix option. `` 不过这里有个小细节就是plugins配置项没有配置jsdoc对应的插件只配置了extends,这是plugins机制,自动加载以eslint-plugin-` 为前缀的插件

overrides

overrides通过 files 可以指定不同文件或文件夹进行针对性配置,且其拥有和基础配置项几乎一样的配置,如parserOptionsextendsplugins

举个例子,比如只针对utils.ts设置禁用 anymain.ts不限制,utils.ts内容和main.ts一样

.eslintrc 修改如下 json { "parser": "@typescript-eslint/parser", "overrides": [ { "files": [ "utils.ts" ], "extends": [ "plugin:jsdoc/recommended" ], "rules": { "@typescript-eslint/no-explicit-any": 2, "no-alert": 1 } } ], "plugins": [ "@typescript-eslint" ], "rules": { "no-eval": 2, "no-alert": 1 } } 执行 pnpm eslint "**/*.ts" 结果如下

``` // main.ts 5:1 warning Unexpected alert no-alert 7:1 error eval can be harmful no-eval

// utils.ts 2:8 warning Missing JSDoc comment jsdoc/require-jsdoc 2:40 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 5:1 warning Unexpected alert no-alert 7:1 error eval can be harmful no-eval

✖ 6 problems (3 errors, 3 warnings) 0 errors and 1 warning potentially fixable with the --fix option. 在 `utils.ts` 正确书写才能通过校验,[jsdoc 文档](https://www.jsdoc.com.cn/)javascript /* * 类型检测 * * @param {any} val 检测对象 * @param {string} type 类型 * @returns {boolean} 是否属于 type 类型 / export function is(val: unknown, type: string): boolean { return toString.call(val) === [object ${type}] } ```

rules

rules有多种写法 - 单取数字值: 0、1、2,分别是关闭、警告、禁止 - 字符串:"off"、"warn"、"error"和上面0、1、2对应 - 数组,第一项可取前两种取值,第二项为该规则的具体配置,不同规则可能支持不同拓展写法,更新详细说明请参照ESlint rules 文档以及对应规则说明 ```JSON "rules": { "no-alert": 1, "quotes": ["error", "double"], "no-console": [ "error", { "allow": ["log", "warn", "error", "info"] } ], }

```

globals

有时可能需要一些全局变量的设置以处理 JSON "globals": { "__DEV__": false, "__dirname": false, "define": true, "history": true, "location": true, "wxjs": true, "$": true, "WeixinJSBridge": true, "wx": true, "process": true, "qq": true }, 另外 ESLint 还提供 .eslintignore 文件用来屏蔽不需要校验的文件

Stylelint

Stylelint的配置和 ESLint 实际上是一个思路,这里就不详细介绍了,详细的请参照Stylelint 文档

css

shell pnpm i -D stylelint stylelint-config-standard 新增index.css,新增 .stylelintrc 文件,写入以下配置 ```json { "extends": [ "stylelint-config-standard" ], "rules": { "no-empty-first-line": true } }

```

执行 pnpm stylelint "**/*.css" 得到校验结果 ``` index.css 1:1 ✖ Unexpected empty source no-empty-source

1 problem (1 error, 0 warnings) ```

Scss & Less

shell pnpm i -D stylelint-scss stylelint-config-recommended-scss pnpm i -D stylelint-less stylelint-config-recommended-less

.stylelintrc 文件增加 overrides 配置 ```json { "extends": ["stylelint-config-standard"], "overrides": [ { "extends": "stylelint-config-recommended-scss", "files": ["/*.scss"] }, { "extends": "stylelint-config-recommended-less", "files": ["/*.less"] } ], "rules": { "no-empty-first-line": true }, }

分别建立对应文件执行`pnpm stylelint "**/*.{css,scss,less}"`得到以下结果 When linting something other than CSS, you should install an appropriate syntax, e.g. "postcss-less", and use the "customSyntax" option

index.css 1:1 ✖ Unexpected empty source no-empty-source

index.less 1:1 ✖ Unexpected empty source no-empty-source

index.scss 1:1 ✖ Unexpected empty source no-empty-source

3 problems (3 errors, 0 warnings) `` 提示less需要在配置项customSyntax配置postcss-less`转换器

安装postcss-less并更改配置如下

json { "extends": [ "stylelint-config-standard" ], "overrides": [ { "extends": "stylelint-config-recommended-scss", "files": [ "**/*.scss" ] }, { "extends": "stylelint-config-recommended-less", "customSyntax": "postcss-less", "files": [ "**/*.less" ] } ], "rules": { "no-empty-first-line": true } } 再次执行 pnpm stylelint "**/*.{css,scss,less}" 关于customSyntax的配置提示消失

customSyntax是自定义css语法转换插件的配置,比如 scss也有 postcss-scss,详细说明参照customSyntax 文档

stylelint-config-recess-order

stylelint的规范推荐加上自动排序规则stylelint-config-recess-order shell pnpm i -D stylelint-config-recess-order .stylelintrc 更改如下 ```json { "extends": ["stylelint-config-standard", "stylelint-config-recess-order"], "overrides": [ { "extends": ["stylelint-config-recommended-scss", "stylelint-config-recess-order"], "files": ["/*.scss"] }, { "extends": ["stylelint-config-recommended-less", "stylelint-config-recess-order"], "customSyntax": "postcss-less", "files": ["/*.less"] } ], "rules": { "no-empty-first-line": true } }

`` 从这里的配置可以看到overrides里每一项的extends` 需要单独配置完整的继承关系

ESLint 一样,Stylelint 提供 .stylelintignore 文件用来屏蔽不需要校验的文件

prettier

ESLintStylelint 是对jscss进行语法规范,代码风格则可以交给prettier来处理 shell pnpm i -D prettier eslint-config-prettier eslint-plugin-prettier 新增 .prettierrc 文件 json { "printWidth": 140, "singleQuote": true, "semi": false, "trailingComma": "none", "bracketSameLine": true, "arrowParens": "avoid", "htmlWhitespaceSensitivity": "ignore", "overrides": [ { // rc 文件按照 json进行格式化 "files": [".prettierrc", ".eslintrc", ".stylelintrc", ".lintstagedrc"], "options": { "parser": "json" } } ] } prettier 同样提供 .prettierignore 文件用来屏蔽不需要格式化的文件

VScode 配置

当然以上各种 Lint手动校验实在太累,为了方便可以配合编辑器设置自动格式化,以 VScode 为例

安装对应插件 - Prettier - Code formatter - ESLint - Stylelint - Vetur(vue2) - Vue Language Features (Volar) - TypeScript Vue Plugin (Volar)(如果使用 Vue3 + TypeScript开发 )

新增 .vscode 文件夹,创建 settings.json 配置文件,写入如下简单配置

注意: eslint.workingDirectories 配置项最好指定当前项目的eslint配置文件,这样编辑器的eslint才会根据项目配置进行校验提示 ```json { "extensions.ignoreRecommendations": false, "editor.tabSize": 2, "typescript.updateImportsOnFileMove.enabled": "always", "javascript.updateImportsOnFileMove.enabled": "always", "javascript.format.insertSpaceBeforeFunctionParenthesis": true, "editor.fontSize": 14, "editor.formatOnType": true, "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll.eslint": true, "source.fixAll.stylelint": true }, "editor.defaultFormatter": "esbenp.prettier-vscode", "[vue]": { "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode" }, "prettier.semi": false, //结尾分号 "prettier.trailingComma": "none", //结尾逗号 "prettier.singleQuote": true, "stylelint.validate": ["css", "less", "postcss", "scss", "vue", "sass"], "eslint.workingDirectories": [".eslintrc", { "mode": "location" }],// 这里需要指定下 .eslintrc ,让编辑器的eslint根据配置去执行校验提示 "eslint.validate": ["vue", "typescript", "javascript", "javascriptreact"]

} ```

Vue

vue 生态链中提供了eslint-plugin-vue库,eslint-plugin-vue 官网 也有详细配置说明,根据说明这里直接选择 vue-eslint-parser shell pnpm i -D vue-eslint-parser eslint-plugin-vue .eslintrcoverrides 配置中添加对应 vue的配置

```JSON { "parser": "vue-eslint-parser", "parserOptions": { "ecmaVersion": "latest", "sourceType": "module", "ecmaFeatures": {"jsx": true } }, "plugins": ["@typescript-eslint"], "extends": ["prettier"], "overrides": [ { "files": ["/*.{ts,tsx}"], "extends": ["plugin:jsdoc/recommended"], "parserOptions": { "sourceType": "module", "parser": "@typescript-eslint/parser" }, "rules": { "@typescript-eslint/no-explicit-any": 2, "no-alert": 1 } }, { "files": "/*.vue", "extends": ["plugin:vue/vue3-recommended", "prettier"], "rules": { "@typescript-eslint/no-explicit-any": 2, "no-alert": 2 }, "parserOptions": { "sourceType": "module", "parser": "@typescript-eslint/parser" } } ], "rules": { "no-eval": 2, "no-alert": 1 } }

`` *注意*:执行pnpm eslint /*.vue`进行测试,配置没错的情况下如果有 Failed to load plugin xxx ... Class extends** 之类的报错大概率是版本兼容问题

目前 stylelint 是 14.x 版本,在 vue3.vue 文件中有 Unknown word (CssSyntaxError)错误,解决方案 - stylelint降级到 13 版本以下 ,但是相关的插件都需要处理,相对麻烦 - 添加 stylelint-config-html插件来处理

第二种方案比较简单,安装相应插件 shell pnpm i -D postcss-html stylelint-config-html

.stylelintrc 新增 overrides 配置 ```json { "extends": [ "stylelint-config-standard", "stylelint-config-recommended-less", "stylelint-config-recommended-scss", "stylelint-config-html/vue", "stylelint-config-recess-order" ], "customSyntax": "postcss-html", "files": [ "*/.vue" ] }

`` 这里直接配置lessscss支持,如不需要都支持只需把extends`中对应的规则删除即可

vue有许多社区模板可以参考

React

既然 Vue的配置搞懂了 React 的自然不在话下,所以就不再介绍了,参照React ESLint 插件文档eslint-plugin-react 配置即可

css-in-js相关的,如styled-component对应的 lint 有 - stylelint-processor-styled-components(已弃用) - eslint-plugin-styled-components-a11y - eslint-plugin-styled-components-css

规范代码提交

代码提交也是很重要的一环,这个环节主要是保证提交上来的代码是符合规范的,包括前文配置的lint规范、提交日志规范、测试用例等,为此需要定制git hooks在代码提交上来之前处理好

lint-staged

lint-staged可以来处理不同文件类型需要执行的lint或其他命令 shell pnpm i -D lint-staged package.json 中添加lint-staged内容,比如下面针对 less等项目文件的格式化和规范处理 ```json "lint-staged": { "/*.less": "stylelint --syntax less", "(|!test)/*.{jsx,js,tsx,ts,less,md,json,vue}": [ "prettier --write", "git add" ] },

也可以使用 `.lintstagedrc` 文件来配置,如json { "/src//*.{js,jsx,ts,tsx,vue}": ["eslint --cache --fix", "prettier --write"], "/src//.{css,scss,less}": ["stylelint --cache --fix", "prettier --write"], "/.md": "prettier --write" }

```

commitlint

规范提交日志对问题定位和代码回滚有较大的意义

首先当前项目未初始化 git 的先执行 git init 初始化 git

commitizen

使用commitizencz-conventional-changelog来规范日志格式 ```shell

全局安装 commitizen,可能需要根据错误提示执行 pnpm setup ,需重新加载终端或编辑器

pnpm i -g commitizen

初始化 cz,会在 package.json 生成commitizen的配置

commitizen init cz-conventional-changelog --pnpm --save-dev --save-exact

需要中文的添加 cz-conventional-changelog-zh

pnpm i -D cz-conventional-changelog-zh `commitizen init cz-conventional-changelog --pnpm --save-dev --save-exact` 会在 `package.json` 生成如下配置json "config": { "commitizen": { "path": "./node_modules/cz-conventional-changelog" } } `` 使用cz-conventional-changelog-zh的将path改为./node_modules/cz-conventional-changelog-zh`即可

执行 git cz 会有如下提示(这里使用了中文包),根据提示选择、输入内容即可 ``` ? 选择您要提交的更改类型: (Use arrow keys)

feat: 一个新功能 fix: 一个bug docs: 文档增删改 style: 样式修改(空白、格式、缺少分号等) refactor: 既不修复bug也不添加新功能的更改 perf: 性能优化 test: 增加测试 (Move up and down to reveal more choices) ```

自定义提交规范

新增 commitizen 配置文件 .cz-config.js

这里讲讲自实现方式,新增 commit-rules.js 写入自定义规则如下

```javascript

var configLoader = require('commitizen').configLoader;

const types = [ { "key": "new-type", "description": "新类型", "title": "new-type" }, { "key": "feat", "description": "一个新功能", "title": "Features" }, { "key": "fix", "description": "一个bug", "title": "Bug Fixes" }, ] var config = configLoader.load() || {}; /* * 交互收集 commit 信息插件 * * @param cz 提供基础的交互能力 * @param commit 相当于 git commit / function prompter (cz, commit) { var length = types.length var defaultType = process.env.CZ_TYPE || config.defaultType var defaultScope = process.env.CZ_SCOPE || config.defaultScope var choices = types.map(function ({ key, description }) { return { name: (key + ':').padEnd(length) + ' ' + description, value: key }; }); cz.prompt([ { type: 'list', name: 'type', message: '选择您要提交的更改类型:', choices: choices, default: defaultType }, { type: 'input', name: 'scope', message: '这个变化的范围是什么(例如组件或文件名):(按回车键跳过)', default: defaultScope, filter: function (value) { return value.trim(); } }, { type: 'maxlength-input', name: 'subject', message: '写一个简短的修改描述(最多20个字符):\n', maxLength: 20, validate: function (subject, answers) {

    return subject.length == 0
      ? '缺少修改描述'
      : subject.length <= 20
        ? true
        : '描述内容的长度必须小于或等于20'
  },
}

]).then(answers => { const { type, scope, subject } = answers var messageScope = scope ? '(' + scope + ')' : ''; const message = ${type}${messageScope}: ${subject} time: ${new Date().getTime()}

commit(message)

})

} module.exports = { prompter } ```

.cz-config.js 写入如下内容

```json { "path": "./commit-rules" }

另外项目可能要求只需要本地安装即可,这样 `git cz` 命令就没法使用,可以换成当前项目内安装`commitizen`, `package.json` 新增 `scripts`json "scripts": { "am": "git add . & git-cz", "cm": "git-cz" },

执行 `pnpm am` 得到自定义规则交互提示

git add . & git-cz

[email protected], [email protected]

? 选择您要提交的更改类型: (Use arrow keys)

new-type: 新类型 feat: 一个新功能 fix: 一个bug `` 使用git czpnpm git-cz代替git commit` 用来生成符合规范的 Commit message

husky

然后就是定制git hooks流程,在 .git/hooks 目录下已经有许多钩子的例子,只需去掉 .sample 后缀钩子便可生效

当然使用 husky 则方便很多

```shell

安装 husky

pnpm i -D husky

未初始化git的需要先初始化git,否则husky会初始化失败

git init

初始化husky

pnpm husky install

新增钩子文件

npx husky add .husky/pre-commit npx husky add .husky/commit-msg `commit-msg` 文件写入以下内容

!/bin/sh

. "$(dirname "$0")/_/husky.sh"

校验通过交互收集到的 commit 是否符合规范

pnpm commitlint --edit $1 新增 `commitlint.config.js` 写入如下内容,因为前面自定义规范的时候格式改动规则需要相应变动,否则会不通过javscript module.exports = { "extends": ['@commitlint/config-conventional'], "rules": { 'subject-empty': [0, 'never'], 'type-empty': [0, 'never'], "type-enum": [ 2, "always", [ "new-type" ] ] } } `` 此时执行pnpm am` 根据交互提示最终通过自定义规则可以提交成功

pre-commit可以参考如下内容 ```

!/bin/sh

. "$(dirname "$0")/_/husky.sh"

执行 postinstall 钩子

pnpm run postinstall

校验lint

npx lint-staged

执行测试用例

pnpm test

`` 最后是测试lint-staged`

先把pre-commit内暂时不存在的命令或钩子移除,执行 pnpm am,会看到触发了 lint-staged 脚本对文件进行了校验,有些不需要检验的文件可以在.eslintignore.stylelintignore.prettierignore增加屏蔽配置或者完善lint-staged匹配规则即可

到此基本过完了所有规范制定和工具集成配置,还是相当繁琐的,尤其是各种插件的组装可能会有各种莫名其妙的报错(大概率是版本问题)

严格还是宽松,如何选择?

有了整个架子就可以按照规范需要去设置对应规则了,至于一个项目要配置多严格的规范,我觉得这要看团队的共识,总的来说有以下两个方向

20230119-154715.jpg

R-C.png

最终选择感觉主要还是看leader的态度。

还有些更严格的会配置进webpack等开发工具中时时影响开发过程,个人认为那是在添堵

附录

json "devDependencies": { "@typescript-eslint/eslint-plugin": "^5.49.0", "@typescript-eslint/parser": "^5.49.0", "cz-conventional-changelog": "^3.3.0", "cz-conventional-changelog-zh": "^0.0.2", "eslint": "^8.32.0", "eslint-config-prettier": "^8.6.0", "eslint-plugin-jsdoc": "^39.6.8", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-vue": "^9.9.0", "husky": "^8.0.3", "lint-staged": "^13.1.0", "postcss-html": "^1.5.0", "postcss-less": "^6.0.0", "prettier": "^2.8.3", "stylelint": "^14.16.1", "stylelint-config-html": "^1.1.0", "stylelint-config-recess-order": "^3.1.0", "stylelint-config-recommended-less": "^1.0.4", "stylelint-config-recommended-scss": "^8.0.0", "stylelint-config-standard": "^29.0.0", "stylelint-less": "^1.0.6", "stylelint-scss": "^4.3.0", "typescript": "^4.9.4", "vue-eslint-parser": "^9.1.0" }, "dependencies": { "vue": "^3.2.45" }, "config": { "commitizen": { "path": "./node_modules/cz-conventional-changelog-zh" } }

下一篇:工欲善其事必先利其器(配置Vue3 + ts项目模板)