項(xiàng)目中一鍵添加husky實(shí)現(xiàn)詳解
關(guān)于husky
前置條件:項(xiàng)目已關(guān)聯(lián)了git。
husky有什么用?
當(dāng)我們commit message時(shí),可以進(jìn)行測(cè)試和lint操作,保證倉(cāng)庫(kù)里的代碼是優(yōu)雅的。 當(dāng)我們進(jìn)行commit操作時(shí),會(huì)觸發(fā)pre-commit,在此階段,可進(jìn)行test和lint。其后,會(huì)觸發(fā)commit-msg,對(duì)commit的message內(nèi)容進(jìn)行驗(yàn)證。
pre-commit
一般的lint會(huì)全局掃描,但是在此階段,我們僅需要對(duì)暫存區(qū)的代碼進(jìn)行l(wèi)int即可。所以使用lint-staged插件。
commit-msg
在此階段,可用 @commitlint/cli @commitlint/config-conventional 對(duì)提交信息進(jìn)行驗(yàn)證。但是記信息格式規(guī)范真的太太太太麻煩了,所以可用 commitizen cz-git 生成提交信息。
一鍵添加husky
從上述說(shuō)明中,可以得出husky配置的基本流程:
- 安裝husky;安裝lint-staged @commitlint/cli @commitlint/config-conventional commitizen cz-git
- 寫commitlint和lint-staged的配置文件
- 修改package.json中的scripts和config
- 添加pre-commit和commit-msg鉤子
看上去簡(jiǎn)簡(jiǎn)單單輕輕松松,那么,開干!
先把用到的import拿出來(lái)溜溜
import { red, cyan, green } from "kolorist"; // 打印顏色文字
import { copyFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { cwd } from "node:process";
import prompts from "prompts";// 命令行交互提示
import { fileURLToPath } from "node:url";
import { getLintStagedOption } from "./src/index.js";// 獲取lint-staged配置 ,后頭說(shuō)
import { createSpinner } from "nanospinner"; // 載入動(dòng)畫(用于安裝依賴的時(shí)候)
import { exec } from "node:child_process";
package驗(yàn)證
既然是為項(xiàng)目添加,那當(dāng)然得有package.json文件啦!
const projectDirectory = cwd();
const pakFile = resolve(projectDirectory, "package.json");
if (!existsSync(pakFile)) {
console.log(red("未在當(dāng)前目錄中找到package.json,請(qǐng)?jiān)陧?xiàng)目根目錄下運(yùn)行哦~"));
return;
}
既然需要lint,那當(dāng)然也要eslint/prettier/stylelint啦~
const pakContent = JSON.parse(readFileSync(pakFile));
const devs = {
...(pakContent?.devDependencies || {}),
...(pakContent?.dependencies || {}),
};
const pakHasLint = needDependencies.filter((item) => {
return item in devs;
});
但是考慮到有可能lint安裝在了全局,所以這邊就不直接return了,而是向questions中插入一些詢問(wèn)來(lái)確定到底安裝了哪些lint。
const noLintQuestions = [
{
type: "confirm",
name: "isContinue",
message: "未在package.json中找到eslint/prettier/stylelint,是否繼續(xù)?",
},
{
// 處理上一步的確認(rèn)值。如果用戶沒(méi)同意,拋出異常。同意了就繼續(xù)
type: (_, { isContinue } = {}) => {
if (isContinue === false) {
throw new Error(red("? 取消操作"));
}
return null;
},
name: "isContinueChecker",
},
{
type: "multiselect",
name: "selectLint",
message: "請(qǐng)選擇已安裝的依賴:",
choices: [
{
title: "eslint",
value: "eslint",
},
{
title: "prettier",
value: "prettier",
},
{
title: "stylelint",
value: "stylelint",
},
],
},
];
const questions = pakHasLint.length === 0 ? [...noLintQuestions, ...huskyQuestions] : huskyQuestions; // huskyQuestions的husky安裝的詢問(wèn)語(yǔ)句,下面會(huì)講
husky安裝詢問(wèn)
因?yàn)椴煌陌芾砥饔胁煌陌惭b命令,以及有些項(xiàng)目會(huì)不需要commit msg驗(yàn)證。所有就會(huì)有以下詢問(wèn)的出現(xiàn)啦
const huskyQuestions = [
{
type: "select",
name: "manager",
message: "請(qǐng)選擇包管理器:",
choices: [
{
title: "npm",
value: "npm",
},
{
title: "yarn1",
value: "yarn1",
},
{
title: "yarn2+",
value: "yarn2",
},
{
title: "pnpm",
value: "pnpm",
},
{
title: "pnpm 根工作區(qū)",
value: "pnpmw",
},
],
},
{
type: "confirm",
name: "commitlint",
message: "是否需要commit信息驗(yàn)證?",
},
];
使用prompts進(jìn)行交互提示
let result = {};
try {
result = await prompts(questions, {
onCancel: () => {
throw new Error(red("?Bye~"));
},
});
} catch (cancelled) {
console.log(cancelled.message);
return;
}
const { selectLint, manager, commitlint } = result;
這樣子,我們就獲取到了:
- manager 項(xiàng)目使用的包管理
- commitlint 是否需要commit msg驗(yàn)證
- selectLint 用戶自己選擇的已安裝的lint依賴
生成命令
通過(guò)manager和commitlint,可以生成要運(yùn)行的命令
const huskyCommandMap = {
npm: "npx husky-init && npm install && npm install --save-dev ",
yarn1: "npx husky-init && yarn && yarn add --dev ",
yarn2: "yarn dlx husky-init --yarn2 && yarn && yarn add --dev ",
pnpm: "pnpm dlx husky-init && pnpm install && pnpm install --save-dev ",
pnpmw: "pnpm dlx husky-init && pnpm install -w && pnpm install --save-dev -w ",
};
const preCommitPackages = "lint-staged";
const commitMsgPackages = "@commitlint/cli @commitlint/config-conventional commitizen cz-git";
// 需要安裝的包
const packages = commitlint ? `${preCommitPackages} ${commitMsgPackages}` : preCommitPackages;
// 需要安裝的包的安裝命令
const command = `${huskyCommandMap[manager]}${packages}`;
const createCommitHook = `npx husky set .husky/pre-commit "npm run lint:lint-staged"`;
const createMsgHook = `npx husky add .husky/commit-msg 'npx --no-install commitlint --edit "$1"'`;
// 需要?jiǎng)?chuàng)建鉤子的命令
const createHookCommand = commitlint ? `${createCommitHook} && ${createMsgHook}` : createCommitHook;
lint-staged 配置
一般的lint-staged.config.js長(zhǎng)這樣:
module.exports = {
"*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
"{!(package)*.json,*.code-snippets,.!(browserslist)*rc}": ["prettier --write--parser json"],
"package.json": ["prettier --write"],
"*.vue": ["eslint --fix", "prettier --write", "stylelint --fix"],
"*.{scss,less,styl,html}": ["stylelint --fix", "prettier --write"],
"*.md": ["prettier --write"],
};
所以呢,需要根據(jù)項(xiàng)目使用的lint來(lái)生成lint-staged.config.js:
// 簡(jiǎn)單粗暴的函數(shù)
export function getLintStagedOption(lint) {
const jsOp = [],
jsonOp = [],
pakOp = [],
vueOp = [],
styleOp = [],
mdOp = [];
if (lint.includes("eslint")) {
jsOp.push("eslint --fix");
vueOp.push("eslint --fix");
}
if (lint.includes("prettier")) {
jsOp.push("prettier --write");
vueOp.push("prettier --write");
mdOp.push("prettier --write");
jsonOp.push("prettier --write--parser json");
pakOp.push("prettier --write");
styleOp.push("prettier --write");
}
if (lint.includes("stylelint")) {
vueOp.push("stylelint --fix");
styleOp.push("stylelint --fix");
}
return {
"*.{js,jsx,ts,tsx}": jsOp,
"{!(package)*.json,*.code-snippets,.!(browserslist)*rc}": jsonOp,
"package.json": pakOp,
"*.vue": vueOp,
"*.{scss,less,styl,html}": styleOp,
"*.md": mdOp,
};
}
// lint-staged.config.js 內(nèi)容
const lintStagedContent = `module.exports =${JSON.stringify(getLintStagedOption(selectLint || pakHasLint))}`;
// lint-staged.config.js 文件
const lintStagedFile = resolve(projectDirectory, "lint-staged.config.js");
commitlint 配置
因?yàn)閏ommitlint.config.js中的配置過(guò)于復(fù)雜。所以,我選擇在安裝完依賴后直接copy文件!被copy的文件內(nèi)容:
// @see: https://cz-git.qbenben.com/zh/guide
/** @type {import('cz-git').UserConfig} */
module.exports = {
ignores: [(commit) => commit.includes("init")],
extends: ["@commitlint/config-conventional"],
// parserPreset: "conventional-changelog-conventionalcommits",
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"]],
},
prompt: {
alias: { fd: "docs: fix typos" },
messages: {
type: "選擇你要提交的類型 :",
scope: "選擇一個(gè)提交范圍(可選):",
customScope: "請(qǐng)輸入自定義的提交范圍 :",
subject: "填寫簡(jiǎn)短精煉的變更描述 :\n",
body: '填寫更加詳細(xì)的變更描述(可選)。使用 "|" 換行 :\n',
breaking: '列舉非兼容性重大的變更(可選)。使用 "|" 換行 :\n',
footerPrefixsSelect: "選擇關(guān)聯(lián)issue前綴(可選):",
customFooterPrefixs: "輸入自定義issue前綴 :",
footer: "列舉關(guān)聯(lián)issue (可選) 例如: #31, #I3244 :\n",
confirmCommit: "是否提交或修改commit ?",
},
types: [
{ value: "feat", name: "feat: ??新增功能 | A new feature", emoji: "??" },
{ value: "fix", name: "fix: ??修復(fù)缺陷 | 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: ??代碼重構(gòu) | 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: ??測(cè)試相關(guān) | Adding missing tests or correcting existing tests", emoji: "??" },
{ value: "build", name: "build: ??構(gòu)建相關(guān) | Changes that affect the build system or external dependencies", emoji: "??" },
{ value: "ci", name: "ci: ??持續(xù)集成 | Changes to our CI configuration files and scripts", emoji: "??" },
{ value: "revert", name: "revert: ??回退代碼 | Revert to a commit", emoji: "??" },
{ value: "chore", name: "chore: ??其他修改 | Other changes that do not modify src or test files", emoji: "??" },
],
useEmoji: true,
emojiAlign: "center",
themeColorCode: "",
scopes: [],
allowCustomScopes: true,
allowEmptyScopes: true,
customScopesAlign: "bottom",
customScopesAlias: "custom | 以上都不是?我要自定義",
emptyScopesAlias: "empty | 跳過(guò)",
upperCaseSubject: false,
markBreakingChangeMode: false,
allowBreakingChanges: ["feat", "fix"],
breaklineNumber: 100,
breaklineChar: "|",
skipQuestions: [],
issuePrefixs: [
// 如果使用 gitee 作為開發(fā)管理
{ value: "link", name: "link: 鏈接 ISSUES 進(jìn)行中" },
{ value: "closed", name: "closed: 標(biāo)記 ISSUES 已完成" },
],
customIssuePrefixsAlign: "top",
emptyIssuePrefixsAlias: "skip | 跳過(guò)",
customIssuePrefixsAlias: "custom | 自定義前綴",
allowCustomIssuePrefixs: true,
allowEmptyIssuePrefixs: true,
confirmColorize: true,
maxHeaderLength: Infinity,
maxSubjectLength: Infinity,
minSubjectLength: 0,
scopeOverrides: undefined,
defaultBody: "",
defaultIssues: "",
defaultScope: "",
defaultSubject: "",
},
};
被復(fù)制的路徑,和目標(biāo)路徑
const commitlintFile = resolve(projectDirectory, "commitlint.config.js"); const commitlintFileTemplateDir = resolve(fileURLToPath(import.meta.url), "../src/template", "commitlint.config.js");
準(zhǔn)備就緒,開始安裝!
- 執(zhí)行剛剛生成的安裝命令
- 更改package.json內(nèi)容
- 寫入配置文件
- 添加鉤子
const spinner = createSpinner("Installing packages...").start();
exec(`${command}`, { cwd: projectDirectory }, (error) => {
if (error) {
spinner.error({
text: red("Failed to install packages!"),
mark: "?",
});
console.error(error);
return;
}
/* 更改package.json內(nèi)容 開始 */
let newPakContent = JSON.parse(readFileSync(pakFile));// 獲取最新的包內(nèi)容
newPakContent.scripts = {
...newPakContent.scripts,
"lint:lint-staged": "lint-staged",
commit: "git add . && git-cz",
};
newPakContent.config = {
...(newPakContent?.config || {}),
commitizen: {
path: "node_modules/cz-git",
},
};
writeFileSync(pakFile, JSON.stringify(newPakContent));// 寫入
/* 更改package.json內(nèi)容 結(jié)束 */
writeFileSync(lintStagedFile, lintStagedContent);// 寫入lint-staged配置
copyFileSync(commitlintFileTemplateDir, commitlintFile);// 復(fù)制commitlint配置至項(xiàng)目中
spinner.success({ text: green("安裝成功~準(zhǔn)備添加鉤子! ??"), mark: "?" });// 包安裝成功啦~
const hookSpinner = createSpinner("添加husky鉤子中...").start();// 開始裝鉤子
exec(`${createHookCommand}`, { cwd: projectDirectory }, (error) => {
if (error) {
hookSpinner.error({
text: red(`添加鉤子失敗,請(qǐng)手動(dòng)執(zhí)行${createHookCommand}`),
mark: "?",
});
console.error(error);
return;
}
hookSpinner.success({ text: green("一切就緒! ??"), mark: "?" });// 鉤子安裝成功啦~一切ok~~
});
});
發(fā)包
最后,發(fā)下包,就可以在其他項(xiàng)目中使用啦

結(jié)尾
這個(gè)是本萌新因?yàn)閼杏窒氚裧it提交規(guī)范下又不想每次創(chuàng)項(xiàng)目都要翻文檔安裝的產(chǎn)物,沒(méi)有經(jīng)過(guò)測(cè)試,中間部分代碼會(huì)有更好的解決方案~
本代碼倉(cāng)庫(kù)
以上就是項(xiàng)目中一鍵添加husky實(shí)現(xiàn)詳解的詳細(xì)內(nèi)容,更多關(guān)于項(xiàng)目一鍵添加husky的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
一次前端Vue項(xiàng)目國(guó)際化解決方案的實(shí)戰(zhàn)記錄
這篇文章主要給大家介紹了關(guān)于前端Vue項(xiàng)目國(guó)際化解決方案的實(shí)戰(zhàn)記錄,以上只是一部分Vue項(xiàng)目開發(fā)中遇到的典型問(wèn)題和解決方案,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-07-07
深入淺析Vue不同場(chǎng)景下組件間的數(shù)據(jù)交流
探通過(guò)本篇文章給大家探討不同場(chǎng)景下組件間的數(shù)據(jù)“交流”的Vue實(shí)現(xiàn)方法,感興趣的朋友一起看看吧2017-08-08
Vue項(xiàng)目開發(fā)實(shí)現(xiàn)父組件與子組件數(shù)據(jù)間的雙向綁定原理及適用場(chǎng)景
在 Vue.js 中,實(shí)現(xiàn)父組件與子組件數(shù)據(jù)之間的雙向綁定,可以通過(guò)以下幾種方式,下面我將介紹幾種常見(jiàn)的方法,并解釋它們的實(shí)現(xiàn)原理和適用場(chǎng)景,感興趣的朋友跟隨小編一起看看吧2024-12-12
vue拖拽組件 vuedraggable API options實(shí)現(xiàn)盒子之間相互拖拽排序
這篇文章主要介紹了vue拖拽組件 vuedraggable API options實(shí)現(xiàn)盒子之間相互拖拽排序克隆clone,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-07-07
教你如何開發(fā)Vite3插件構(gòu)建Electron開發(fā)環(huán)境
這篇文章主要介紹了如何開發(fā)Vite3插件構(gòu)建Electron開發(fā)環(huán)境,文中給大家提到了如何讓 Vite 加載 Electron 的內(nèi)置模塊和 Node.js 的內(nèi)置模塊,需要的朋友可以參考下2022-11-11
vue watch監(jiān)聽(tīng)變量值的實(shí)時(shí)變動(dòng)示例詳解
這篇文章主要介紹了vue 監(jiān)聽(tīng)變量值的實(shí)時(shí)變動(dòng) watch,使用字符串形式的偵聽(tīng)器函數(shù),還有一種是使用函數(shù)形式的偵聽(tīng)器函數(shù),本文通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-08-08
vue 解決mintui彈窗彈起來(lái),底部頁(yè)面滾動(dòng)bug問(wèn)題
這篇文章主要介紹了vue 解決mintui彈窗彈起來(lái),底部頁(yè)面滾動(dòng)bug問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-11-11

