vue3+vite3+ts+elementplus搭建(进行中)

40 阅读7分钟

项目创建

在要安装的目录下执行以下命令

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 的最后
    })
]