Eslint+Prettier+husky+lint-stage+commitizen构建前端工作流

494 阅读6分钟

前言

你是否遇到过以下的问题:

  • 维护老项目,格式化代码出现几千行变更?
  • 不能自动格式化,保存代码需要手动操作?
  • commit时不能自动格式化,代码CR时全是问题?
  • 不知道如何配置代码检验、格式化代码规范?
  • 团队commit信息不规范,经常看到 "fix" 这样的commit msg?
  • commit时不知道开头有哪些,只会"feat"、"fix"?

本篇文章带大家全部搞定~

不管是多人合作还是个人项目,代码规范是很重要的。在项目初始化时配置好规范,这样做不仅可以很大程度地避免基本语法错误、保证代码的可读性,也会为后续省去很多的麻烦。

前端工程流配置是前端工程师必须掌握的一项技能,本篇文章详细讲述了一个项目从0开始搭建,会涉及到哪些规范,如何使用、配置、以及可能遇到的问题。

本文使用到的技术:

create-react-app

eslint

prettier

stylelint

tslint

commitlint

husky

lint-stage

\

使用create-react-app创建一个项目:

create-react-app Lint-Demo --template typescript

代码检查工具

Eslint

介绍:一个插件化的javascript代码检测工具;

功能:每次保存,vscode就能标红不符合ESLint规则的地方,同时还会做一些简单的自我修正;

安装

  • 编辑器安装eslint插件
  • 安装eslint包(Node.js >=6.14, npm version 3+)
npm install eslint --save-dev
  • 紧接着你应该设置一个配置文件:
./node_modules/.bin/eslint --init
或
npx eslint --init

注:npx是什么?

npx附带npm,它允许你运行本地安装的包(不是全局),它能让项目内部安装的模块用起来更方便;

这是全部的配置项:

接下来,eslint会帮我们在项目根目录创建一个.eslintrc.js 文件,eslint会帮我们完成一些基础的配置:

注:.eslintrc.js 与.eslintrc.json功能一致,二取一即可;

module.exports = {
    "env": {
        "browser": true,
        "es2021": true
    },
    "extends": [
        "eslint:recommended",
        "plugin:react/recommended",
        "plugin:@typescript-eslint/recommended"
    ],
    "overrides": [
    ],
    "parser": "@typescript-eslint/parser",
    "parserOptions": {
        "ecmaVersion": "latest",
        "sourceType": "module"
    },
    "plugins": [
        "react",
        "@typescript-eslint"
    ],
    "rules": {
    }
}

如果需要自定义校验规则,可以参考eslint.bootcss.com/docs/user-g…

\

  • 之后,你可以在任何文件或目录上运行ESLint,如下:
./node_modules/.bin/eslint yourfile.js

\

  • 当然也可以放在package.json的script作为脚本来运行:
"scripts": {
  "lint": "eslint --ext .js,.ts,.tsx src/"
},

代码风格工具

prettier

介绍:一个代码格式化工具;

功能: 按下ctrl+s,代码自动格式化;规范团队代码风格;

安装:

  • 安装eslint包
npm install --save-dev --save-exact prettier
  • 然后,创建一个空配置文件,让编辑器和其他工具知道你正在使用 Prettier:
echo {}> .prettierrc.json
  • 接下来,创建一个.prettierignore文件,让 Prettier CLI 和编辑器知道哪些文件不能格式化。
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local
stats.html
  • 现在,可以使用命令格式化所有文件:
npx prettier --write .
  • 设置编辑器

每次都通过命令行格式化会很麻烦,可以通过在编辑器中运行 Prettier 来充分利用 Prettier,无论是通过键盘快捷键还是在保存文件时自动运行。

    • 安装vscode的prettier插件
    • 首选项-设置,设定编辑器默认代码格式化的插件为Prettier
    • 设定Prettier插件保存时自动格式化代码,搜索设置项贴入editor.formatOnSave,将搜索到的项目打钩即可
  • 处理Eslint与prettier的冲突

具体配置原因请看这里:github.com/prettier/es…

安装eslint-config-prettier以使 ESLint 和 Prettier 相互配合。它会关闭所有不必要或可能与 Prettier 冲突的 ESLint 规则。Stylelint 有一个类似的配置:stylelint-config-prettier

npm i eslint-config-prettier eslint-plugin-prettier -D

还要在.eslintrc 的 plugins中添加 "prettier" :

{
  "extends": ["prettier"],
  "plugins": ["prettier"],
  "rules": {
    "prettier/prettier": "error",
    "arrow-body-style": "off",
    "prefer-arrow-callback": "off"
  }
}

虽然我们已经要求项目使用eslint了,但是不能保证组员提交代码之前都将eslint中的问题解决掉了,所以我们需要在组员执行 git commit 命令的时候对其进行校验,如果不符合eslint规范,那么自动通过规范进行修复。我们可以借助husky的能力做到这一点。

Git检查工具

husky

介绍:husky是一个为 git 客户端增加 hook 的工具。作用就是在git执行一些操作的时候触发一些钩子,在钩子处执行一些自己需要的命令,比如代码的eslint校验、格式化代码等。最佳实践一般是校验两处,eslint(代码规范)及commit message规范

安装:

  • 安装依赖
npm i lint-staged husky -D
  • 添加script
"scripts": {
  "prepare": "husky install",
},

高版本的husky是需要你使用.husky下面的hooks脚本,不会在自己调用.git hooks脚本,生成.husky脚本很简单。执行命令husky install即可。但是由于husky是局部安装的,所以一般都是写一个script命令,然后执行npm run prepare如下:

  • 执行prepare命令 ,会在项目跟目录下创建 .husky/文件夹,用来存放所有的git hooks。
npm run prepare
  • 添加commit msg
 npx husky add .husky/commit-msg 'npm run commitlint --edit "$1"'
  • 自定义git提交规范
npm i @commitlint/cli @commitlint/config-conventional -D

项目根目录下创建 commitlint.config.js 在里面定义提交规则

配置案例:

// @see: https://cz-git.qbenben.com/zh/guide
/** @type {import('cz-git').UserConfig} */

module.exports = {
	ignores: [(commit) => commit.includes("init")],
	extends: ["@commitlint/config-conventional", "cz"],
	rules: {
		// @see: https://commitlint.js.org/#/reference-rules
		"body-leading-blank": [2, "always"],
		"footer-leading-blank": [1, "always"],
		"header-max-length": [2, "always", 108],
		"subject-empty": [2, "never"],
		"type-empty": [2, "never"],
		"subject-case": [0],
		"type-enum": [
			2,
			"always",
			[
				"feat",
				"fix",
				"docs",
				"style",
				"refactor",
				"perf",
				"test",
				"build",
				"ci",
				"chore",
				"revert",
				"wip",
				"workflow",
				"types",
				"release"
			]
		]
	},
	prompt: {
		messages: {
			type: "Select the type of change that you're committing:",
			scope: "Denote the SCOPE of this change (optional):",
			customScope: "Denote the SCOPE of this change:",
			subject: "Write a SHORT, IMPERATIVE tense description of the change:\n",
			body: 'Provide a LONGER description of the change (optional). Use "|" to break new line:\n',
			breaking: 'List any BREAKING CHANGES (optional). Use "|" to break new line:\n',
			footerPrefixsSelect: "Select the ISSUES type of changeList by this change (optional):",
			customFooterPrefixs: "Input ISSUES prefix:",
			footer: "List any ISSUES by this change. E.g.: #31, #34:\n",
			confirmCommit: "Are you sure you want to proceed with the commit above?"
			// 中文版
			// type: "选择你要提交的类型 :",
			// scope: "选择一个提交范围(可选):",
			// customScope: "请输入自定义的提交范围 :",
			// subject: "填写简短精炼的变更描述 :\n",
			// body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n',
			// breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n',
			// footerPrefixsSelect: "选择关联issue前缀(可选):",
			// customFooterPrefixs: "输入自定义issue前缀 :",
			// footer: "列举关联issue (可选) 例如: #31, #I3244 :\n",
			// confirmCommit: "是否提交或修改commit ?"
		},
		types: [
			{
				value: "feat",
				name: "feat:     🚀  A new feature",
				emoji: "🚀"
			},
			{
				value: "fix",
				name: "fix:      🧩  A bug fix",
				emoji: "🧩"
			},
			{
				value: "docs",
				name: "docs:     📚  Documentation only changes",
				emoji: "📚"
			},
			{
				value: "style",
				name: "style:    🎨  Changes that do not affect the meaning of the code",
				emoji: "🎨"
			},
			{
				value: "refactor",
				name: "refactor: ♻️   A code change that neither fixes a bug nor adds a feature",
				emoji: "♻️"
			},
			{
				value: "perf",
				name: "perf:     ⚡️  A code change that improves performance",
				emoji: "⚡️"
			},
			{
				value: "test",
				name: "test:     ✅  Adding missing tests or correcting existing tests",
				emoji: "✅"
			},
			{
				value: "build",
				name: "build:    📦️   Changes that affect the build system or external dependencies",
				emoji: "📦️"
			},
			{
				value: "ci",
				name: "ci:       🎡  Changes to our CI configuration files and scripts",
				emoji: "🎡"
			},
			{
				value: "chore",
				name: "chore:    🔨  Other changes that don't modify src or test files",
				emoji: "🔨"
			},
			{
				value: "revert",
				name: "revert:   ⏪️  Reverts a previous commit",
				emoji: "⏪️"
			}
			// 中文版
			// { value: "特性", name: "特性:   🚀  新增功能", emoji: "🚀" },
			// { value: "修复", name: "修复:   🧩  修复缺陷", emoji: "🧩" },
			// { value: "文档", name: "文档:   📚  文档变更", emoji: "📚" },
			// { value: "格式", name: "格式:   🎨  代码格式(不影响功能,例如空格、分号等格式修正)", emoji: "🎨" },
			// { value: "重构", name: "重构:   ♻️  代码重构(不包括 bug 修复、功能新增)", emoji: "♻️" },
			// { value: "性能", name: "性能:   ⚡️  性能优化", emoji: "⚡️" },
			// { value: "测试", name: "测试:   ✅  添加疏漏测试或已有测试改动", emoji: "✅" },
			// { value: "构建", name: "构建:   📦️  构建流程、外部依赖变更(如升级 npm 包、修改 webpack 配置等)", emoji: "📦️" },
			// { value: "集成", name: "集成:   🎡  修改 CI 配置、脚本", emoji: "🎡" },
			// { value: "回退", name: "回退:   ⏪️  回滚 commit", emoji: "⏪️" },
			// { value: "其他", name: "其他:   🔨  对构建过程或辅助工具和库的更改(不影响源文件、测试用例)", emoji: "🔨" }
		],
		useEmoji: true,
		themeColorCode: "",
		scopes: [],
		allowCustomScopes: true,
		allowEmptyScopes: true,
		customScopesAlign: "bottom",
		customScopesAlias: "custom",
		emptyScopesAlias: "empty",
		upperCaseSubject: false,
		allowBreakingChanges: ["feat", "fix"],
		breaklineNumber: 100,
		breaklineChar: "|",
		skipQuestions: [],
		issuePrefixs: [{ value: "closed", name: "closed:   ISSUES has been processed" }],
		customIssuePrefixsAlign: "top",
		emptyIssuePrefixsAlias: "skip",
		customIssuePrefixsAlias: "custom",
		allowCustomIssuePrefixs: true,
		allowEmptyIssuePrefixs: true,
		confirmColorize: true,
		maxHeaderLength: Infinity,
		maxSubjectLength: Infinity,
		minSubjectLength: 0,
		scopeOverrides: undefined,
		defaultBody: "",
		defaultIssues: "",
		defaultScope: "",
		defaultSubject: ""
	}
};

使用案例: git commit -m 'feat: 增加 xxx 功能' 错误type将无法提交。

\

正常情况下如果这么设置"pre-commit: eslint src/*.js",检查的是全部文件,有点浪费性能。所以出现了lint-staged:

lint-staged

介绍: 仅仅过滤出 Git 代码暂存区文件(被 git add 的文件)的工具;他只会检查我们这一次提交的文件,大大提高了性能,

项目集成:

  • 安装依赖
npm i lint-staged -D
  • 在package中添加命令
{
  "script": {
    "lint:lint-staged": "lint-staged"
  }
}
  • 添加lint-stage配置

方法一:在package.json中添加

方法二:创建.lintstagedrc 文件

方法三:创建lint-staged.config.js 文件并进行配置

这里以.lintstagedrc 为例:

{
	"*.{js,jsx,ts,tsx}": ["npx prettier --write .", "eslint  --fix"],
	"*.md": ["prettier --write"]
}

\

  • 修改husky的配置脚本
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npm run lint:lint-staged

这样,在 commit 之前,会将暂存区的内容做一次 代码检查 和 代码美化,然后再添加到暂存区;然后再 commit。

通常我们的git commit会按照统一的风格来提交,这样可以快速定位每次提交的内容,方便之后对版本进行控制,我们可以使用commitizen辅助我们:

辅助提交工具

commitizen

介绍:

集成:

  • 安装依赖
npm i commitizen cz-conventional-changelog -D
  • 安装指令和命令行的展示信息

在package.json 中添加 commit 指令, 执行 git-cz 指令

npm set-script commit "git-cz" 
  • 编写commit指令,初始化命令行的选项信息,不然没有选项
npx commitizen init cz-conventional-changelog --save-dev --save-exact
  • 增加 .cz-config.js
  'use strict'
  module.exports = {
    types: [
      { value: '✨新增', name: '新增:    新的内容' },
      { value: '🐛修复', name: '修复:    修复一个Bug' },
      { value: '📝文档', name: '文档:    变更的只有文档' },
      { value: '💄格式', name: '格式:    空格, 分号等格式修复' },
      { value: '♻️重构', name: '重构:    代码重构,注意和特性、修复区分开' },
      { value: '⚡️性能', name: '性能:    提升性能' },
      { value: '✅测试', name: '测试:    添加一个测试' },
      { value: '🔧工具', name: '工具:    开发工具变动(构建、脚手架工具等)' },
      { value: '⏪回滚', name: '回滚:    代码回退' }
    ],
    scopes: [
      { name: 'leetcode' },
      { name: 'javascript' },
      { name: 'typescript' },
      { name: 'Vue' },
      { name: 'node' }
    ],
    // it needs to match the value for field type. Eg.: 'fix'
    /*  scopeOverrides: {
      fix: [
        {name: 'merge'},
        {name: 'style'},
        {name: 'e2eTest'},
        {name: 'unitTest'}
      ]
    },  */
    // override the messages, defaults are as follows
    messages: {
      type: '选择一种你的提交类型:',
      scope: '选择一个scope (可选):',
      // used if allowCustomScopes is true
      customScope: 'Denote the SCOPE of this change:',
      subject: '短说明:\n',
      body: '长说明,使用"|"换行(可选):\n',
      breaking: '非兼容性说明 (可选):\n',
      footer: '关联关闭的issue,例如:#31, #34(可选):\n',
      confirmCommit: '确定提交说明?(yes/no)'
    },
    allowCustomScopes: true,
    allowBreakingChanges: ['特性', '修复'],
    // limit subject length
    subjectLimit: 100
  }
  • package.json 中,将原来commit配置,变更为自定义配置
"config": {
  "commitizen": {
    "path": "./node_modules/cz-customizable"
  }
}
  • 然后提交会变成这样

\

附录

仓库地址:

\

\