项目创建
在要安装的目录下执行以下命令
npm create vite @latest
Project name:
blog-admin
Package name:
blog-admin
$ npm create vite@latest
> npx
> create-vite
# 按提示要求填写项目名称
|
o Project name:
| myBlog-admin
|
o Package name:
| myblog-admin
|
o Select a framework:
| Vue
|
o Select a variant:
| TypeScript
cd myBlog-admin
npm install
npm run dev
代码规范
.editorconfig统一编码风格
用于在不同IDE编辑器上处理同一项目的多个开发人员维护一致的编码风格,-
安装 EditorConfig for VS Code 插件
- 配置
"editor.formatOnSave": true
实现保存时自动格式化
根目录下创建.editorconfig
文件
# 位于项目根目录的 .editorconfig 文件
root = true
[*]
charset = utf-8 # 统一使用 UTF-8 编码
end_of_line = lf # 统一使用 LF 换行符(Linux/macOS 风格)
insert_final_newline = true # 文件末尾自动添加空行
trim_trailing_whitespace = true # 自动删除行尾空格
indent_style = space # 统一使用空格缩进
indent_size = 2 # 2 空格缩进(与 Prettier 配置一致)
[*.{js,jsx,ts,tsx,vue,cjs,mjs}]
# 保持与 ESLint/Prettier 配置一致
indent_size = 2
max_line_length = 100 # 配合 Prettier 的 printWidth 设置
[*.{html,vue}]
# 针对 Vue 模板的特殊配置
indent_size = 2
[*.md]
trim_trailing_whitespace = false # 保留 Markdown 文件的行尾空格
[Makefile]
indent_style = tab # Makefile 需要制表符缩进
prettier+eslint代码规范
1. 安装依赖
npm install -D eslint eslint-plugin-vue @vue/eslint-config-prettier @vue/eslint-config-typescript prettier eslint-config-prettier eslint-plugin-prettier stylelint stylelint-config-standard stylelint-config-recommended-vue
2. ESLint 配置(eslint.config.js)
eslint9以上只支持eslint.config.js
初始化eslint.config.js 运行npx eslint --init 按提示要求选择后会自动生成
import globals from 'globals'
import js from '@eslint/js'
import tseslint from 'typescript-eslint'
import pluginVue from 'eslint-plugin-vue'
import VueEslintParser from 'vue-eslint-parser'
// 自 9.0 以来,eslint 不再有格式化规则,typescript 的主要维护者在他的文章 "You Probably Don't Need eslint-config-prettier or eslint-plugin-prettier" 中建议不要使用 `eslint-config-prettier`。
// import eslintConfigPrettier from 'eslint-config-prettier'
// import fs from 'fs'
// import path from 'path'
// import { fileURLToPath } from 'url'
import eslintPluginPrettier from 'eslint-plugin-prettier/recommended'
// 获取当前文件的目录路径
// const __filename = fileURLToPath(import.meta.url)
// const __dirname = path.dirname(__filename)
// 读取 .eslintrc-auto-import.json 文件
// 导入到全局,用于 ts 自动识别
// .eslintrc-auto-import.json 来自 vite 插件 unplugin-auto-import
// unplugin-auto-import 插件作用自动导入预设库的 API,在使用的地方不在需要手动 import 导入
// 具体配置在 vite.config.ts ,如果没有使用 unplugin-auto-import 这里配置可以忽略
// const autoImportPath = path.resolve(__dirname, 'src/types/.eslintrc-auto-import.json')
// const autoImportConfig = JSON.parse(fs.readFileSync(autoImportPath, 'utf8'))
export default [
{
files: ['src/**/*.{js,mjs,cjs,ts,mts,jsx,tsx,vue}'],
},
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
// ...autoImportConfig.globals, // 合并自动导入的 globals
},
parser: VueEslintParser,
parserOptions: {
parser: '@typescript-eslint/parser',
ecmaFeatures: {
jsx: true,
},
},
},
},
js.configs.recommended,
...tseslint.configs.recommended,
...pluginVue.configs['flat/essential'],
{
rules: {
// 允许 ESLint 直接运行 Prettier 并将结果作为 ESLint 规则来报告
// "prettier/prettier": "error"
// eslint(https://eslint.bootcss.com/docs/rules/)
'no-var': 'error', // 要求使用 let 或 const 而不是 var
'no-multiple-empty-lines': ['warn', { max: 1 }], // 不允许多个空行
'no-unexpected-multiline': 'error', // 禁止空余的多行
'no-useless-escape': 'off', // 禁止不必要的转义字符
// typeScript (https://typescript-eslint.io/rules)
'@typescript-eslint/no-unused-vars': 'warn', // 禁止定义未使用的变量
'@typescript-eslint/prefer-ts-expect-error': 'error', // 禁止使用 @ts-ignore
'@typescript-eslint/no-explicit-any': 'off', // 禁止使用 any 类型
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-namespace': 'off', // 禁止使用自定义 TypeScript 模块和命名空间。
'@typescript-eslint/semi': 'off',
'@typescript-eslint/no-unsafe-function-type': 'off', // 禁止使用 Function 作为 type。
// eslint-plugin-vue (https://eslint.vuejs.org/rules/)
'vue/multi-word-component-names': 'off', // 要求组件名称始终为 “-” 链接的单词
// 'vue/script-setup-uses-vars': 'error', // 防止<script setup>使用的变量<template>被标记为未使用
'vue/no-mutating-props': 'off', // 不允许组件 prop的改变
'vue/attribute-hyphenation': 'off', // 对模板中的自定义组件强制执行属性命名样式
indent: ['error', 2], // 缩进使用2个空格
semi: ['error', 'never'], //语句末尾不加分号
'no-unused-vars': 'off',
},
},
//eslintConfigPrettier,
eslintPluginPrettier,
{
ignores: ['node_modules/', 'dist/', '**/*.d.ts', 'public/', '.editor.config'],
},
]
3. Prettier 配置(prettier.config.js)
(json中不允许有注释,实际去掉注释)
export default {
semi: false, // 是否使用分号
singleQuote: true,
printWidth: 100, // 每行代码的长度
trailingComma: 'none', // 是否在参数列表中添加尾随逗号
arrowParens: 'avoid', // 箭头函数参数只有一个时是否带括号
htmlWhitespaceSensitivity: 'ignore', // html标签内是否忽略空格
vueIndentScriptAndStyle: true, // vue文件内是否自动换行
endOfLine: 'auto' // 换行符 lf | crlf | cr
// "bracketSameLine": true // 括号是否另起一行
}
4. Stylelint 配置(.stylelintrc.json)
(实际不允许有注释)
{
// 继承的标准配置文件,结合了Stylelint的标准配置和Vue推荐的配置
"extends": ["stylelint-config-standard", "stylelint-config-recommended-vue"],
// 自定义规则配置
"rules": {
// 禁用选择器类模式的校验,允许使用任意格式的类名
"selector-class-pattern": null,
// 设置值关键词的大小写规则为小写
// 忽略全部由大写字母开头的关键字
"value-keyword-case": [
"lower",
{
"ignoreKeywords": ["/^[A-Z]+$/"]
}
]
}
}
提交规范配置(Git Hooks)
1. 安装提交规范工具
- husky:Git 钩子管理工具
- Commitizen 交互式提交工具
- @commitlint/cli:提交信息校验引擎
- @commitlint/config-conventional:约定式提交规范配置
npm install -D husky commitlint @commitlint/cli @commitlint/config-conventional
2. 初始化 Husky
#npx husky install
npx husky-init #husky9
# 然后在 package.json 中添加 prepare 脚本
{
"scripts": {
"prepare": "husky install"
}
}
3.配置lint-staged
lint-staged
是一个在 Git 暂存区(staging area)对代码进行 lint 检查的工具,其核心作用是仅对本次提交中修改的文件运行代码检查,从而提高代码审查效率和开发体验。官方网站:github.com/okonet/lint…
安装依赖
npm i lint-staged -D
package.json 中添加不同文件在 git 提交执行的 lint 检测配置
"lint-staged": {
"*.{js,ts}": [
"eslint --fix",
"prettier --write"
],
"*.{cjs,json}": [
"prettier --write"
],
"*.{vue,html}": [
"eslint --fix",
"prettier --write",
"stylelint --fix"
],
"*.{scss,css}": [
"stylelint --fix",
"prettier --write"
],
"*.md": [
"prettier --write"
]
添加 lint-staged 指令
package.json 的 scripts 添加 lint-staged 指令
"scripts": {
"lint:lint-staged": "lint-staged"
}
修改提交前钩子命令
根目录 .husky 目录下 pre-commit 文件中的 npm test 修改为 npm run lint:lint-staged
#npm test
npm run lint:lint-staged
4. 配置 commitlint(commitlint.config.js)
配置文件
// commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
// 提交类型必须为小写
'type-case': [2, 'always', 'lower-case'],
// 自定义允许的提交类型
'type-enum': [
2,
'always',
[
'feat', // 新功能
'fix', // 修复 bug
'docs', // 文档变更
'style', // 代码格式(不影响代码运行)
'refactor', // 代码重构(非功能变更)
'perf', // 性能优化
'test', // 测试相关
'build', // 构建流程/外部依赖变更
'ci', // CI 配置变更
'chore', // 其他杂项(不修改 src 或测试文件)
'revert', // 回退提交
'wip' // 开发中提交(可选类型)
]
],
// 提交作用域最大长度限制
'scope-max-length': [2, 'always', 30],
// 提交信息主体最大长度(建议与 Prettier 的 printWidth 一致)
'subject-max-length': [2, 'always', 100]
}
}
初始化 Commitizen
运行以下命令选择适配器并更新 package.json
:
npx commitizen init cz-conventional-changelog --save-dev --save-exact
执行完会自动更新package.json,(如果没有安装cz-conventional-changelog,会自动安装)
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
}
在package.json添加命令
"scripts": {
"commit": "cz"
}
配置完成后运行npm run commit 会出现选择,按要求提交就行(如果需要中文,需要使用cz-customizable)
项目配置
1. 路径别名
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
}
})
在tsconfig.json中需要配置路径跳转
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
},
2.目录结构
├── public/ # 静态资源(直接复制到dist)
├── src/
│ ├── api/ # API接口管理
│ │ ├── modules/ # 按模块划分的接口
│ │ └── index.ts # 统一导出接口
│ ├── assets/ # 静态资源(需要处理的)
│ │ ├── images/ # 图片资源
│ │ ├── fonts/ # 字体文件
│ │ └── styles/ # 全局SCSS
│ │ ├── _variables.scss # 变量
│ │ ├── _mixins.scss # 混合器
│ │ └── index.scss # 全局样式入口
│ ├── components/ # 公共组件
│ │ ├── common/ # 全局通用组件(按钮/弹窗等)
│ │ └── business/ # 业务通用组件
│ ├── composables/ # 组合式函数
│ ├── directives/ # 自定义指令
│ ├── layouts/ # 布局组件
│ ├── plugins/ # 第三方插件配置
│ │ ├── element-plus.ts # Element Plus按需导入配置
│ │ └── global.ts # 全局组件/插件注册
│ ├── router/ # 路由配置
│ │ ├── modules/ # 路由模块拆分
│ │ └── index.ts
│ ├── stores/ # Pinia状态管理
│ │ ├── modules/ # 按模块划分的store
│ │ │ ├── user.ts # 用户相关store
│ │ │ └── app.ts # 应用全局store
│ │ └── index.ts
│ ├── types/ # TypeScript类型定义
│ │ ├── api.d.ts # API响应类型
│ │ └── global.d.ts # 全局类型声明
│ ├── utils/ # 工具函数
│ │ ├── auth.ts # 权限相关
│ │ ├── request.ts # Axios封装
│ │ └── validate.ts # 验证工具
│ ├── views/ # 页面组件
│ │ ├── home/ # 首页模块
│ │ │ ├── components/ # 页面私有组件
│ │ │ └── index.vue
│ │ └── login/ # 登录模块
│ ├── App.vue
│ └── main.ts
├── .env.development # 开发环境变量
├── .env.production # 生产环境变量
├── vite.config.ts # Vite配置
├── tsconfig.json # TypeScript配置
└── package.json
3.第三方库集成
(1)elementplus
安装依赖
npm install element-plus @element-plus/icons-vue
# 安装开发依赖 自动导入
npm install -D @types/node unplugin-auto-import unplugin-vue-components unplugin-icons
vite配置
在vite.config.ts配置elementplus的组件和图标自动导入
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import IconsResolver from 'unplugin-icons/resolver'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import Icons from 'unplugin-icons/vite'
export default defineConfig({
plugins: [
vue(),
AutoImport({
// 自动导入 API
resolvers: [
ElementPlusResolver(), // 自动导入 Element Plus API
IconsResolver({
prefix: 'Icon' // 自动导入图标组件
})
],
imports: ['vue', 'vue-router'],
dts: 'src/auto-imports.d.ts' // 生成 `auto-imports.d.ts` 全局声明
}),
Components({
// 自动导入组件
resolvers: [
ElementPlusResolver(), // 自动导入 Element Plus 组件
IconsResolver({
prefix: 'Icon', // 自定义图标组件前缀
enabledCollections: ['ep'] // 使用 Element Plus 图标集
})
]
}),
Icons({
autoInstall: true, // 自动安装图标组件
compiler: 'vue3' // 使用 Vue 3 编译器
})
],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
}
})
图标图方便可以再main.ts中全局导入,官网有说明。 后文会使用全局导入的方式。
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import pinia from './stores'
import '@/assets/styles/reset.scss' // 引入全局样式
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const app = createApp(App) // 创建 Vue 应用实例
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(pinia) // 挂载 Pinia 实例
app.use(router) // 挂载路由实例
app.mount('#app')
国际化
Element Plus 组件 默认 使用英语,如果需要使用其他语言需要配置国际化, 具体配置见官网
<template>
<el-config-provider :locale="zhCn">
<app />
</el-config-provider>
</template>
<script setup lang="ts">
import { ElConfigProvider } from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
</script>
(2) axios封装
安装依赖
npm install axios
封装请求
import axios, {
type AxiosInstance,
type InternalAxiosRequestConfig,
type AxiosResponse
} from 'axios'
import { ElMessage } from 'element-plus'
// 扩展 AxiosRequestConfig 类型
declare module 'axios' {
interface AxiosRequestConfig {
_requestKey?: string
_retryCount?: number
}
}
// 存储正在请求的 Promise
const pendingRequests = new Map<string, () => void>()
// 生成唯一请求key、
function generateRequestKey(config: InternalAxiosRequestConfig): string {
const { method, url, params, data } = config
return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&')
}
// 创建 Axios 实例
const service: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_BASE_API,
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
})
// 请求拦截器
service.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const requestKey = generateRequestKey(config)
if (pendingRequests.has(requestKey)) {
return Promise.reject(new Error('Duplicate request:'))
}
const controller = new AbortController()
config.signal = controller.signal
;(config as any)._requestKey = requestKey
pendingRequests.set(requestKey, () => controller.abort())
const token = localStorage.getItem('token')
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error: any) => {
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
(response: AxiosResponse) => {
const requestKey = (response.config as any)._requestKey
if (requestKey && pendingRequests.has(requestKey)) {
pendingRequests.delete(requestKey)
}
return response.data
},
(error: any) => {
// 可以统一处理错误
const config = error?.config as InternalAxiosRequestConfig
if (config && !axios.isCancel(error)) {
const requestKey = config._requestKey
if (requestKey && pendingRequests.has(requestKey)) {
pendingRequests.delete(requestKey)
}
}
// 错误提示统一处理
const showError = (message: string) => {
ElMessage.error(message)
}
if (!error.response) {
showError('网络异常,请检查您的网络连接')
return Promise.reject(error)
}
const { status, data } = error.response
const maxRetry = 3
if (status === 503) {
config._retryCount = config._retryCount || 0
if (config._retryCount < maxRetry) {
config._retryCount++
console.log(`Retrying request... Attempt ${config._retryCount}`)
return new Promise(resolve => setTimeout(resolve, 1000)).then(() => service(config))
}
}
switch (status) {
case 401:
localStorage.removeItem('token')
window.location.href = '/login'
break
case 403:
showError(data?.message ?? '权限不足')
break
case 404:
showError(data?.message ?? '资源不存在')
break
case 500:
showError(data?.message ?? '服务器内部错误')
break
case 503:
showError(data?.message ?? '服务暂时不可用')
break
default:
showError(data?.message ?? '未知错误')
}
return Promise.reject(error)
}
)
export default service
(3) pinia
安装
npm install pinia pinia-plugin-persistedstate
# pinia-plugin-persistedstate用于持久化(可选)
创建目录结构
src/
├── stores/
│ ├── index.ts # Pinia 实例初始化
│ └── userStore.ts # 示例用户状态模块
初始化 Pinia 实例 (src/stores/index.ts
)
import { createPinia } from 'pinia'
import persist from 'pinia-plugin-persistedstate' // 引入持久化插件
const pinia = createPinia()
// 引入持久化插件
pinia.use(persist)
export default pinia
定义用户 Store (src/stores/userStore.ts
)
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
token: '', // 初始值由持久化自动填充
hasRoutes: false
}),
actions: {
setHasRoutes(value: boolean) {
this.hasRoutes = value
},
resetToken() {
this.token = ''
localStorage.removeItem('token') // 手动清理 localStorage
}
},
// persist: true // 启用持久化(默认使用 localStorage)
persist: {
enabled: true,
strategies: [
{
key: 'user-store', // 显式命名持久化键名
storage: localStorage // 显式声明存储方式
}
]
}
})
3. 挂载 Pinia 到 Vue 应用
修改入口文件 (src/main.ts
)
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import pinia from './stores'
const app = createApp(App) // 创建 Vue 应用实例
app.use(pinia) // 挂载 Pinia 实例
app.use(router) // 挂载路由实例
app.mount('#app')
(4) vue-router
安装
npm install vue-router
创建路由文件
routes/index.ts 需要再main.ts引入,上面的main.ts文件中已经说明。当前路由示例为动态路由,从后端接口获取,也可以直接前端写死,或者前端根据角色控制。
import { getRoutes } from '@/services/apis/route'
import { useUserStore } from '@/stores/userStore'
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
// 基础路由(所有角色可见)
export const constantRoutes: RouteRecordRaw[] = [
{
path: '/login',
component: () => import('@/views/login/index.vue'),
meta: { hidden: true }
},
{
path: '/',
component: () => import('@/layouts/main-layout.vue'),
redirect: '/dashboard',
children: [
{
path: 'dashboard',
component: () => import('@/views/dashboard/index.vue'),
meta: { title: '控制台', icon: 'odometer' }
}
]
},
{
path: '/404',
component: () => import('@/views/404.vue'),
meta: { hidden: true }
},
{
path: '/:pathMatch(.*)*',
redirect: '/404',
meta: { hidden: true }
}
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: constantRoutes
})
// 动态添加路由
export function addRoutes(routes: RouteRecordRaw[]) {
routes.forEach(route => {
router.addRoute(route)
})
}
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore()
// 路由鉴权,这里可以进一步配置登陆了就不跳转登录页了等等,自行扩展
if (to.path === '/login') {
next()
} else {
if (!userStore.token) {
next(`/login?redirect=${to.path}`) // 未登录 跳转到登录页
} else {
if (!userStore.hasRoutes) {
try {
// 获取动态路由
const { data } = await getRoutes()
addRoutes(data)
userStore.setHasRoutes(true)
next({ ...to, replace: true })
} catch (error) {
console.error('路由加载失败:', error) // 添加错误日志
userStore.resetToken()
next(`/login?redirect=${to.path}`)
}
} else {
next()
}
}
}
})
export default router
配置路由进度条(可选)
安装nprogress插件
npm i nprogress
npm install --save-dev @types/nprogress
在路由文件中配置(路由文件内容多的话,建议单拎出来一个文件配置)
import nProgeress from 'nprogress'
// 这边可能ts类型飘红,可以在vite-env.d.ts中生命这个插件:declare module "nprogress";
import 'nprogress/nprogress.css' // 引入进度条样式
// ... 其他代码
router.beforeEach((to, _from, next) => {
nProgeress.start() // 开始进度
})
router.afterEach(() => {
nProgeress.done() // 完成进度条
})
(5)svg图标配置
安装SVG依赖插件
npm install vite-plugin-svg-icons -D
配置
在vite.config.ts中配置插件
plugins:[
createSvgIconsPlugin({
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
symbolId: 'icon-[dir]-[name]' // 配置 SVG 图标的 ID 格式
// inject: 'body-last', // 将 SVG 图标注入到 body 的最后
})
]