Vue 3 + TypeScript + Vite 项目模板,集成 ESLint、Stylelint、Prettier、Commitlint、Husky。
cover: 附件/Halo封面/design.jpg
快速开始
pnpm create vite # vue ts
cd <your project>
git init
pnpm config set registry https://registry.npmmirror.com
cover: 附件/Halo封面/design.jpg
1. 安装运行时依赖
pnpm add pinia pinia-plugin-persistedstate vue-router
pnpm add -D vite-plugin-vue-devtools
编辑 src/main.ts
import { createPinia } from "pinia";
import "@/style.css";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
import { createApp } from "vue";
import App from "@/App.vue";
import router from "@/router";
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
createApp(App).use(pinia).use(router).mount("#app");
创建 src/router/index.ts
import { createRouter, createWebHistory } from "vue-router";
import HelloWorld from "@/components/HelloWorld.vue";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
component: HelloWorld,
name: "home",
path: "/",
},
],
});
export default router;
编辑 vite.config.ts
import vue from "@vitejs/plugin-vue";
import { defineConfig } from "vite";
import VueDevTools from "vite-plugin-vue-devtools";
export default defineConfig({
plugins: [VueDevTools(), vue()],
resolve: {
tsconfigPaths: true,
},
});
编辑 tsconfig.app.json
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"types": ["vite/client"],
"paths": { "@/*": ["./src/*"] },
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}
cover: 附件/Halo封面/design.jpg
2. 安装 Git 提交规范工具
pnpm add -D husky lint-staged @commitlint/cli @commitlint/config-conventional czg
pnpm exec husky init
编辑 .husky/pre-commit
pnpm lint-staged
编辑 .husky/commit-msg
commitlint --edit "$1"
注意:确保文件编码为 UTF-8 无 BOM,否则 shell 无法识别命令。
创建 commitlint.config.js
// @see https://commitlint.js.org/#/reference-rules
export default {
extends: ["@commitlint/config-conventional"],
rules: {
"type-enum": [
2,
"always",
[
"feat", // ✨ 新增功能
"fix", // 🐛 修复缺陷
"docs", // 📝 文档变更
"style", // 🌈 代码格式
"refactor", // 🔄 代码重构
"perf", // 🚀 性能优化
"test", // 🧪 测试改动
"build", // 📦️ 构建/依赖变更
"ci", // ⚙️ CI 配置
"revert", // ↩️ 回滚
"chore", // 🛠️ 辅助工具
"wip", // 🚧 开发中
],
],
"subject-case": [0],
},
prompt: {
messages: {
type: "选择你要提交的类型:",
scope: "选择一个提交范围(可选):",
customScope: "请输入自定义的提交范围:",
subject: "填写简短精炼的变更描述:\n",
body: '填写更加详细的变更描述(可选)。使用 "|" 换行:\n',
breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行:\n',
footerPrefixesSelect: "选择关联 issue 前缀(可选):",
customFooterPrefix: "输入自定义 issue 前缀:",
footer: "列举关联 issue(可选)例如: #31, #I3244:\n",
generatingByAI: "正在通过 AI 生成你的提交简短描述…",
generatedSelectByAI: "选择一个 AI 生成的简短描述:",
confirmCommit: "是否提交或修改commit?",
},
types: [
{ value: "feat", name: "特性: ✨ 新增功能", emoji: ":sparkles:" },
{ value: "fix", name: "修复: 🐛 修复缺陷", emoji: ":bug:" },
{ value: "docs", name: "文档: 📝 文档变更", emoji: ":memo:" },
{ value: "style", name: "格式: 🌈 代码格式", emoji: ":lipstick:" },
{ value: "refactor", name: "重构: 🔄 代码重构", emoji: ":recycle:" },
{ value: "perf", name: "性能: 🚀 性能优化", emoji: ":zap:" },
{ value: "test", name: "测试: 🧪 测试改动", emoji: ":white_check_mark:" },
{ value: "build", name: "构建: 📦️ 构建/依赖变更", emoji: ":package:" },
{ value: "ci", name: "集成: ⚙️ CI 配置", emoji: ":ferris_wheel:" },
{ value: "revert", name: "回退: ↩️ 回滚 commit", emoji: ":rewind:" },
{ value: "chore", name: "其他: 🛠️ 辅助工具", emoji: ":hammer:" },
{ value: "wip", name: "开发中: 🚧 临时提交", emoji: ":construction:" },
],
issuePrefixes: [{ value: "closed", name: "closed: ISSUES has been processed" }],
},
};
编辑 package.json 添加 scripts
{
"scripts": {
"prepare": "husky",
"commit": "czg",
"preinstall": "npx only-allow pnpm"
}
}
使用
czg而非git-cz:czg是 cz-git 的原生 CLI,启动速度远快于 commitizen 适配器模式,且无需额外安装commitizen和cz-git。
cover: 附件/Halo封面/design.jpg
3. 安装 Lint 工具
pnpm add -D eslint @eslint/js typescript-eslint eslint-plugin-vue prettier \
eslint-config-prettier stylelint stylelint-config-standard-scss postcss-html \
stylelint-config-recess-order eslint-plugin-perfectionist \
eslint-plugin-vuejs-accessibility sass
创建 eslint.config.js
import js from "@eslint/js";
import eslintConfigPrettier from "eslint-config-prettier/flat";
import perfectionist from "eslint-plugin-perfectionist";
import pluginVue from "eslint-plugin-vue";
import vuejsAccessibility from "eslint-plugin-vuejs-accessibility";
import tseslint from "typescript-eslint";
export default tseslint.config(
js.configs.recommended,
...tseslint.configs.recommended,
...pluginVue.configs["flat/recommended"],
perfectionist.configs["recommended-natural"],
...vuejsAccessibility.configs["flat/recommended"],
{
files: ["**/*.ts", "**/*.vue"],
languageOptions: {
parserOptions: {
parser: tseslint.parser,
project: ["./tsconfig.app.json", "./tsconfig.node.json"],
extraFileExtensions: [".vue"],
},
},
rules: {
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/prefer-optional-chain": "error",
"@typescript-eslint/prefer-nullish-coalescing": "error",
// 代码质量
eqeqeq: "error",
curly: "error",
"no-var": "error",
"prefer-const": "error",
"no-implicit-coercion": "error",
// TypeScript 一致性
"@typescript-eslint/array-type": ["error", { default: "array-simple" }],
"@typescript-eslint/prefer-for-of": "error",
"@typescript-eslint/prefer-string-starts-ends-with": "error",
"@typescript-eslint/prefer-includes": "error",
"@typescript-eslint/no-unnecessary-boolean-literal-compare": "error",
"@typescript-eslint/switch-exhaustiveness-check": "error",
},
},
{ ignores: ["dist/", "node_modules/", "*.d.ts"] },
{
rules: {
"vue/multi-word-component-names": "off",
"perfectionist/sort-objects": "off",
// Vue 一致性
"vue/block-order": ["error", { order: ["script", "template", "style"] }],
"vue/html-button-has-type": "error",
"vue/require-default-prop": "error",
"vue/require-explicit-emits": "error",
"vue/html-self-closing": [
"error",
{
html: { void: "always", normal: "always", component: "always" },
},
],
},
},
eslintConfigPrettier,
);
创建 stylelint.config.js
/** @type {import("stylelint").Config} */
export default {
extends: ["stylelint-config-standard-scss", "stylelint-config-recess-order"],
overrides: [
{
files: ["**/*.vue"],
customSyntax: "postcss-html",
},
],
rules: {
"scss/operator-no-unspaced": null,
// Vue SFC :deep() pseudo-class
"selector-pseudo-class-no-unknown": [
true,
{
ignorePseudoClasses: ["deep", "global", "slotted"],
},
],
// 代码约定
"declaration-no-important": true,
"max-nesting-depth": 3,
"color-named": "never",
"function-url-no-scheme-relative": true,
// 禁止 BEM __ 双下划线命名
"selector-class-pattern": "^(?!.*__)[a-z][a-z0-9-_]*$",
"property-no-vendor-prefix": true,
"value-no-vendor-prefix": true,
},
};
创建 .prettierrc.json
{
"semi": true,
"singleQuote": false,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2,
"endOfLine": "lf",
"arrowParens": "always",
"bracketSpacing": true,
"singleAttributePerLine": true,
"vueIndentScriptAndStyle": false
}
创建 .prettierignore
dist/
node_modules/
pnpm-lock.yaml
.husky/
public/
*.stylelintcache
src/assets
编辑 package.json 添加 lint scripts
{
"scripts": {
"lint": "eslint . && stylelint \"src/**/*.{css,scss,vue}\"",
"lint:fix": "eslint . --fix && stylelint \"src/**/*.{css,scss,vue}\" --fix",
"format": "prettier --write --ignore-unknown .",
"format:check": "prettier --check --ignore-unknown ."
},
"lint-staged": {
"**/*.{ts,vue}": ["eslint --fix"],
"**/*.{css,scss,vue}": ["stylelint --fix"],
"**/*": ["prettier --write --ignore-unknown"]
}
}
编辑 tsconfig.node.json
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023"],
"types": ["node"],
"skipLibCheck": true,
"module": "nodenext",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}
4. 编辑器配置
编辑 .gitignore 追加
*.stylelintcache
创建 .vscode/extensions.json
{
"recommendations": [
"Vue.volar",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"stylelint.vscode-stylelint"
]
}
创建 .vscode/settings.json
{
"editor.cursorSmoothCaretAnimation": "on",
"editor.cursorBlinking": "smooth",
"editor.cursorStyle": "line",
"editor.cursorWidth": 2,
"editor.renderWhitespace": "boundary",
"editor.renderLineHighlight": "all",
"editor.bracketPairColorization.enabled": true,
"editor.guides.bracketPairs": true,
"editor.guides.indentation": true,
"editor.smoothScrolling": true,
"editor.wordWrap": "off",
"editor.fontFamily": "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', 'Courier New', monospace",
"editor.fontLigatures": true,
"editor.fontSize": 14,
"editor.lineHeight": 24,
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true,
"editor.tabSize": 2,
"editor.detectIndentation": false,
"editor.stickyScroll.enabled": true
}