哈喽,大家好,最近做一个saas项目,自己之前也搭过多个vue3的项目、nuxt3的项目、 h5低代码平台。但也过了很长一段时间了,没有整理,利用这个机会采用最新的技术栈。整理一下,分享给大家。 最新vue3+vite5+pinia+unocss+antdesign vue+husky+eslint+lint-staged+prettier+commitlint搭建的后台管理系统 git源码仓库地址
新增npm插件地址vue3-antd-icons-picker
技术栈选型:
打包编译:vite
包管理:pnpm
ui框架: Ant Design Vue
状态管理:pinia
css: UnoCss
icones:icones
代码规范:eslint husky lint-staged prettier+commitlint
ui框架在项目中用过很多,element
,iview
,naive-ui
,antd
, 就ui风格上antd符合我的审美,功能也比较齐全,其次naive-ui
也是很好用的一个,这次我选择antd
css为什么没选择Tailwind CSS
,而采用Unocss
,我们建议您阅读UnoCSS 创建者Anthony Fu 撰写的博客文章《Reimagine Atomic CSS》,以便更好地理解 UnoCSS 背后的动机。
安装环境
node版本18+或者20。我的版本是v18.18.0。
mac系统用homebrew安装环境
node版本管理工具:mac用 n
,windows是用nvm
,镜像源使用淘宝的。
- 创建项目my-vue-app修改成自己的项目名字
pnpm create vite
按照提示选择vue
选择Customize with create-vue
自己选择是否需要TypeScript,我这选择否,其他的就自行选择即可
按照命令提示进入目录初始化安装
安装husky
pnpm add husky -D
执行npx husky init
初始化,它会在 .husky/
中创建 pre-commit
脚本,并更新 package.json
中的 prepare
脚本。
npx husky init
在package.json
中加入以下代码
{
...
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
}
}
安装lint-staged
pnpm add lint-staged -D
在项目根目录下创建lint.staged.config.cjs
文件
// lint.staged.config.cjs
module.exports = {
'*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'],
'package.json': ['prettier --write'],
'*.vue': ['eslint --fix', 'prettier --write'],
'*.{scss,less,styl,html}': ['prettier --write'],
'*.md': ['prettier --write']
}
在package.json
中加入以下代码
{
...
"lint-staged": {
"*.{js,jsx,ts,tsx,vue}": [
"prettier --write",
"eslint --cache --fix",
"git add"
],
"*.{vue,css,less,scss}": [
"prettier --write"
],
"*.{json,html,yml,md}": [
"prettier --write"
]
}
}
安装commitlint
pnpm add @commitlint/cli @commitlint/config-conventional -D
在根目录下新建commitlint.config.cjs
文件
module.exports = {
extends: ['@commitlint/config-conventional', 'cz'],
prompt: {
questions: [
{
type: 'input',
name: 'subject',
message: '请输入commit信息',
validate: function (value) {
if (value.length) {
return true
} else {
return '请输入commit信息'
}
}
}
],
types: [
{ value: 'feat', name: '✨ feat: 新功能', emoji: ':sparkles:' },
{ value: 'fix', name: '🐛 fix: 修复bug', emoji: ':bug:' },
{ value: 'docs', name: '✏️ docs: 文档变更', emoji: ':memo:' },
{ value: 'style', name: '💄 style: 代码的样式美化', emoji: ':lipstick:' },
{ value: 'refactor', name: '♻️ refactor: 重构', emoji: ':recycle:' },
{ value: 'perf', name: '⚡️ perf: 性能优化', emoji: ':zap:' },
{ value: 'release', name: '🎉 release: 发布正式版', emoji: ':tada:' },
{ value: 'test', name: '✅ test: 测试', emoji: ':white_check_mark:' },
{ value: 'build', name: '📦️ build: 打包', emoji: ':package:' },
{ value: 'ci', name: '👷 ci: CI相关更改', emoji: ':ferris_wheel:' },
{ value: 'chore', name: '🚀 chore: 构建/工程依赖/工具', emoji: ':hammer:' },
{ value: 'revert', name: '⏪️ revert: 回退', emoji: ':rewind:' }
],
useEmoji: true
}
}
修改pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
echo "🚀 pre-commit"
echo "npx --no-install lint-staged"
npx --no-install lint-staged
执行下面命令
npx husky add .husky/commit-msg 'npx --no-install commitlint -e "$HUSKY_GIT_PARAMS"'
echo "npx --no -- commitlint --edit $1" > .husky/commit-msg
在package.json
中husky的配置,增加commit-msg
配置
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint --config commitlint.config.cjs -E HUSKY_GIT_PARAMS"
}
}
在.husky目录下会创建commit-msg
并写入以下代码
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
echo "🚀 pre-commit"
echo "npx --no-install lint-staged"
npx --no-install lint-staged
安装 cz 建议全局安装下
pnpm install -g commitizen
pnpm install -g cz-conventional-changelog
mac执行
echo '{ "path": "cz-conventional-changelog" }' > ~/.czrc
windows执行
echo { "path": "cz-conventional-changelog" } > C:\Users\12105.czrc
commitizen init cz-conventional-changelog --save --save-exact
项目下安装 commitizen
pnpm install commitizen cz-conventional-changelog -D
在package.json
中增加命令
"scripts": {
...
"lint:lint-staged": "lint-staged",
"prepare": "husky install",
"prettier": "prettier --write \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"",
"lint:prettier": "prettier --write \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"",
"commit": "git add . && git cz",
"commitlint": "commitlint --edit"
}
安装 cz-customizable
自动化选择提交commit
pnpm add cz-customizable commitlint-config-cz -D
在根目录下创建.cz-config.cjs
module.exports = {
types: [
{
value: 'feat',
name: '✨ feat: 新功能'
},
{
value: 'fix',
name: '🐛 fix: 修复bug'
},
{
value: 'build',
name: '📦️ build: 打包'
},
{
value: 'perf',
name: '⚡️ perf: 性能优化'
},
{
value: 'release',
name: '🎉 release: 发布正式版'
},
{
value: 'style',
name: '💄 style: 代码的样式美化'
},
{
value: 'refactor',
name: '♻️ refactor: 重构'
},
{
value: 'docs',
name: '✏️ docs: 文档变更'
},
{
value: 'test',
name: '✅ test: 测试'
},
{
value: 'revert',
name: '⏪️ revert: 回退'
},
{
value: 'chore',
name: '🚀 chore: 构建/工程依赖/工具'
},
{
value: 'ci',
name: '👷 ci: CI相关更改'
}
],
messages: {
type: '请选择提交类型(必填)',
customScope: '请输入文件修改范围(可选)',
subject: '请简要描述提交(必填)',
body: '请输入详细描述(可选)',
breaking: '列出任何BREAKING CHANGES(可选)',
footer: '请输入要关闭的issue(可选)',
confirmCommit: '确定提交此说明吗?'
},
allowCustomScopes: true,
// 跳过问题
skipQuestions: ['body', 'footer'],
subjectLimit: 72
}
在package.json
新增
{
...
"config": {
"commitizen": {
"path": "./node_modules/cz-customizable"
},
"cz-customizable": {
"config": ".cz-config.cjs"
}
}
}
安装 eslint-plugin-simple-import-sort 解决依赖引入排序
修改.eslintrc.cjs文件
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
plugins: ['simple-import-sort'],
rules: {
'simple-import-sort/imports': 'error',
'simple-import-sort/exports': 'error'
},
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-prettier/skip-formatting'
],
parserOptions: {
sourceType: 'module',
ecmaVersion: 'latest'
},
globals: {
process: true,
module: true
}
}
自此代码规范就完成了。让我们测试下提交效果,执行
pnpm commit
这样看起来就舒服多了。选择修改的类型,输入msg提交就可以了,不按这规范是提交不成功的,eslint格式校验没通过也提交不了代码。这样对代码质量控制就有保障了。
必须按规范提交才会成功
这里的警告是说提交的时候不要加命令 git add,最好是手动add。可以去掉。
安装 ant-design-vue 我选择按需引入
pnpm add ant-design-vue@4.x unplugin-vue-components dayjs -D
修改vite.config.js
import { fileURLToPath, URL } from 'node:url'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
import Components from 'unplugin-vue-components/vite'
import { defineConfig } from 'vite'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
vueJsx(),
vueDevTools(),
Components({
resolvers: [
AntDesignVueResolver({
importStyle: false // css in js
})
]
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
安装UnoCss
pnpm add unocss -D
在main.js(main.ts)中增加
// main.js
import 'virtual:uno.css'
安装 自定义svg图标和preset-icons预设
pnpm add @unocss/preset-uno @unocss/preset-icons @iconify/iconify @iconify/utils vite-plugin-purge-icons vite-plugin-svg-icons -D
这里@iconify/iconify可以不安装,可以只安装指定的图标库如:@iconify-json/mdi看下面的uno.config.js
的方法
安装(可选) preset-rem-to-px
pnpm add @unocss/preset-rem-to-px -D
修改vite.config.js
import { fileURLToPath, URL } from 'node:url'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import { resolve } from 'path'
import UnoCSS from 'unocss/vite'
import IconsResolver from 'unplugin-icons/resolver'
import Icons from 'unplugin-icons/vite'
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
import Components from 'unplugin-vue-components/vite'
import { defineConfig } from 'vite'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
vueJsx(),
vueDevTools(),
UnoCSS(),
Components({
resolvers: [
AntDesignVueResolver({
importStyle: false // css in js
}),
IconsResolver({
// prefix: 'icon', // 自动引入的Icon组件统一前缀,默认为 i,设置false为不需要前缀
// {prefix}-{collection}-{icon} 使用组件解析器时,您必须遵循名称转换才能正确推断图标。
// alias: { park: 'icon-park' } 集合的别名
enabledCollections: ['ep'] // 这是可选的,默认启用 Iconify 支持的所有集合['mdi']
})
]
}),
Icons({ autoInstall: true, compiler: 'vue3' }),
createSvgIconsPlugin({
iconDirs: [resolve(process.cwd(), 'src/assets/svg')]
})
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
assets: fileURLToPath(new URL('./src/assets', import.meta.url)),
components: fileURLToPath(new URL('./src/components', import.meta.url)),
config: fileURLToPath(new URL('./src/config', import.meta.url)),
utils: fileURLToPath(new URL('./src/utils', import.meta.url)),
service: fileURLToPath(new URL('./src/service', import.meta.url)),
views: fileURLToPath(new URL('./src/views', import.meta.url)),
store: fileURLToPath(new URL('./src/stores', import.meta.url)),
constants: fileURLToPath(new URL('./src/constants', import.meta.url))
},
extensions: ['.js', '.vue', '.json', '.scss', '*']
}
})
修改uno.config.js
,这collections
就可以配置需要的图标集,使用的话会自动下载对应的依赖。
customizations
这个处理svg相关配置。颜色,大小等属性。可以配置props,如{width:2em,height:2em}等配置。详细文档建议大家看下官网的,官网很详细了。
import presetRemToPx from '@unocss/preset-rem-to-px'
import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders'
import { defineConfig, presetAttributify, presetIcons, presetTypography, presetUno } from 'unocss'
export default defineConfig({
shortcuts: {
'bg-base': 'bg-[#f0f0f0] dark:bg-[#20202a]',
'card-base': 'bg-[#ffffff] dark:bg-[#1a1a1a]',
// 这里定义你自己的一些组合变量
},
presets: [
presetUno(),
presetTypography(),
presetAttributify(),
presetRemToPx({
baseFontSize: 4 // 默认16
}),
presetIcons({
extraProperties: {
display: 'inline-block',
'vertical-align': 'middle'
},
customizations: {
transform(svg, collection, icon) {
// do not apply fill to this icons on this collection
if (collection === 'custom' && icon === 'my-icon') return svg
return svg.replace(/#fff/, 'currentColor')
},
custom: FileSystemIconLoader('./src/assets/svg', (svg) =>
svg.replace(/#fff/, 'currentColor')
)
},
collections: {
carbon: () => import('@iconify-json/carbon/icons.json').then((i) => i.default),
mdi: () => import('@iconify-json/mdi/icons.json').then((i) => i.default),
antd: () => import('@iconify-json/ant-design/icons.json').then((i) => i.default)
}
})
]
})
在src/assets
目录下创建一个svg
的目录,存放自己的svg图标
修改
main.js
增加配置
...
import 'virtual:svg-icons-register'
使用方式
<div class="i-custom:logo color-primary"></div>
or
# presetAttributify安装后可以这么使用
<div i-custom:logo color-primary></div>
安装pinia持久化pinia-plugin-persistedstate
pnpm add pinia-plugin-persistedstate -D
pinia支持option和setup的语法可以看官方文档,选择自己喜欢的就行 如option写法
setup写法
单独设置pinia持久化,可以定义一个getStoreKey获取不同环境存储的变量。
paths
是可以指定缓存的字段。所有需要缓存的话,则可以直接persist:true
即可。
{
persist: {
// 修改存储中使用的键名称,默认为当前 Store的 id
key: getStoreKey('user'),
// 修改为 sessionStorage,默认为 localStorage
// storage: window.sessionStorage,
// 部分持久化状态的点符号路径数组,[]意味着没有状态被持久化(默认为undefined,持久化整个状态)
paths: ['openRouteList', 'userInfo', 'currentRouteName', 'token', 'permissions']
}
}
还可以支持插件比如做undo,redo这样的插件统一处理,这里就不展开讲了。 可以统一导出到index中
使用如下
<script setup>
import useStore from 'store' // 这是在vite.config.js设置别名了哦
import { storeToRefs } from 'pinia'
const { common, user } = useStore()
// 解构且保持响应式
const { locale, i18nLocal, theme } = storeToRefs(common)
// 这可以使用local这些了
// 调用方法可以
user.login()
// 当然也可以这样访问store
user.token
</script>
修改main.js
增加配置
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'
import router from './router'
const app = createApp(App)
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
app.use(router)
app.mount('#app')
安装Vueuse 强烈推荐
pnpm add @vueuse/core -D
配置 axios
pnpm add axios -D
在src下创建service
目录,新建index.js
统一处理错误
/**
* 请求失败后的错误统一处理
* @param {Number} status 请求失败的状态码
*/
const errorHandle = (status, msg) => {
// 状态码判断
switch (status) {
case 302:
message.error(msg || '接口重定向了!')
break
case 400:
message.error(msg || '发出的请求有错误,服务器没有进行新建或修改数据的操作')
break
// 401: 未登录
// 未登录则跳转登录页面,并携带当前页面的路径
// 在登录成功后返回当前页面,这一步需要在登录页操作。
case 401: //重定向
message.error(msg || '登录失效')
router.replace({
path: '/login'
})
break
// 403 token过期
// 清除token并跳转登录页
case 403:
message.error(msg || '登录过期,用户得到授权,但是访问是被禁止的')
// store.commit('token', null);
setTimeout(() => {
router.replace({
path: '/login'
})
}, 1000)
break
case 404:
message.error(msg || '网络请求不存在')
break
case 406:
message.error(msg || '请求的格式不可得')
break
case 408:
message.error(msg || ' 请求超时!')
break
case 410:
message.error(msg || '请求的资源被永久删除,且不会再得到的')
break
case 422:
message.error(msg || '当创建一个对象时,发生一个验证错误')
break
case 500:
message.error(msg || '服务器发生错误,请检查服务器')
break
case 502:
message.error(msg || '网关错误')
break
case 503:
message.error(msg || '服务不可用,服务器暂时过载或维护')
break
case 504:
message.error(msg || '网关超时')
break
default:
message.error(msg || '其他错误错误')
}
}
移除重复请求
// 移除重复请求
const removePending = (config) => {
for (const key in pending) {
const item = +key
const list = pending[key]
// 当前请求在数组中存在时执行函数体
if (
list.url === config.url &&
list.method === config.method &&
JSON.stringify(list.params) === JSON.stringify(config.params) &&
JSON.stringify(list.data) === JSON.stringify(config.data)
) {
// 执行取消操作
list.cancel('操作太频繁,请稍后再试')
// 从数组中移除记录
pending.splice(item, 1)
}
}
}
新建request.js
导出get,post,del,put请求
新建api.js
,各个业务的请求放constants
目录导出
安装vue-i18n
pnpm add vue-i18n -D
在项目src
下创建languages
目录
下面新建对应的语言json和导出js文件
例如:获取菜单请求如图
修改languages/index.js
import { createI18n } from 'vue-i18n'
//引入同级目录下文件
const modules = import.meta.glob('./*', { eager: true })
//假设你还有其他目录下的语言文件 它的路径是 src/views/home/locales/en-US.json
//那么你就可以 使用 :lower:(小写) :upper:(大写) 来引入文件
// const viewModules = import.meta.globEager(
// "../views/**/locales/[[:lower:]][[:lower:]]-[[:upper:]][[:upper:]].json"
// )
function getLangAll() {
let message = {}
getLangFiles(modules, message)
// getLangFiles(viewModules, message)
return message
}
/**
* 获取所有语言文件
* @param {Object} mList
*/
function getLangFiles(mList, msg) {
for (let path in mList) {
if (mList[path].default) {
// 获取文件名
let pathName = path.substr(path.lastIndexOf('/') + 1, 5)
if (msg[pathName]) {
msg[pathName] = {
...mList[pathName],
...mList[path].default
}
} else {
msg[pathName] = mList[path].default
}
}
}
}
//注册i8n实例并引入语言文件
const i18n = createI18n({
legacy: false,
locale: 'zh-CN',
messages: getLangAll()
})
export default i18n //将i18n暴露出去,在main.js中引入挂载
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import { routers } from '@/router/modules/default
...
const { common, user } = useStore()
const { locale, i18nLocal, theme } = storeToRefs(common)
const { userInfo } = storeToRefs(user)
const router = useRouter()
const route = useRoute()
const i18n = useI18n({ useScope: 'global' })
const defaultRoute = route.name ? route.name : 'analysis'
dayjs.locale(locale)
i18n.locale.value = i18nLocal.value
需要多语言的就在语言文件中配置对应的变量,多语言就完成了。
theme跟随系统主题变化
这里用到了matchMedia,监听则可以跟随系统主题自动切换。逻辑就是更改root上的var变量和prefers-color-scheme,更改对应主题的变量
菜单 icon配置
antd的icon是可支持自定义的svg,我们处理这个菜单icon的时候,只需要保存@iconify/iconify上对应的class即可。再写一个对应的@iconify/iconify的svg解析组件。可以定义自己的可配置图标集数据。
如使用antd的icon的话,如图所示。
这里我们需要用到vite的vite-plugin-svg-icons
import svgIcons from 'virtual:svg-icons-names'
...
function getSvgIcons() {
return svgIcons.map((icon) => icon.replace('icon-', ''))
}
function getIcons() {
const prefix = iconsData.prefix
return iconsData.icons.map((icon) => `${prefix}:${icon}`)
}
// 如果菜单直接使用的svg如antd的Icon则使用getSvgIcons,如果使用的iconify的css图标。那则直接使用
新建一个Icon的组件,这里我们可以是使用iconify的渲染方法renderSVG
import { renderSVG } from '@iconify/iconify'
效果图