设置resolve.alias别名
在引用文件时,可以使用相对路径的方式,但是这样嵌套的页面非常复杂,有可能会造成多个层级../../../这种引用情况,所以有时候可以通过配置resolve.alias别名来进行缓解。
在vite.config.ts文件中,引入path中的resolve,然后在defineConfig写入配置的相对路径:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: { alias: { '@': resolve(__dirname, './src') }
}
})
unplugin-auto-import:自动按需引入 vue\vue-router\pinia 等的api
1、安装插件·
pnpm i unplugin-auto-import -D --save
2、在 vite.config.js 中的配置
// vue.config.js
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite' //自动按需引入 vue\vue-router\pinia\vueuse 等的api
module.exports = defineConfig({
// ...
configureWebpack: {
plugins: [
vue(),
//
AutoImport({
dts: './src/auto-imports.d.js',
imports: ['vue', 'pinia', 'vue-router'],
vueTemplate: true, // 是否在 vue 模板中自动导入
}),
],
},
})
3、使用前后对比
// 没有使用unplugin-auto-import
<script setup>
import { computed, ref } from 'vue'
const count = ref(0)
const doubled = computed(() => count.value * 2)
</script>
// 使用unplugin-auto-import
<script setup>
const count = ref(0)
const doubled = computed(() => count.value * 2)
</script>
unplugin-vue-components:自动按需引入 第三方的组件库组件 和 我们自定义的组件
1、安装插件·
pnpm i unplugin-vue-components -D --save
2、在 vite.config.js 中的配置
// vue.config.js
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite' //自动按需引入 第三方的组件库组件 和 我们自定义的组件
module.exports = defineConfig({
// ...
configureWebpack: {
plugins: [
vue(),
//
Components({
dts: './src/components.d.js',
// imports 指定组件所在位置,默认为 src/components
dirs: ['src/components/']
}),
],
},
})
3、使用前后对比
// 没有使用unplugin-vue-import
<script setup>
import HelloWorld from "@/components/HelloWorld.vue"
</script>
<template>
<HelloWorld>
</template>
// 使用unplugin-vue-import
<script setup>
</script>
<template>
<HelloWorld>
</template>
VueUse Hooks合集
VueUse是一个基于 Composition API 实现的基本 Vue 组合实用函数的集合,可以看做是vue版的hook。 1、安装vueUse
pnpm i @vueuse/core
2、使用案例1 (在vue文件中直接引用)
<script setup>
import {useMouse} from '@vueuse/core'
const { x , y } = useMouse()
</script>
3、使用案例2 (在vite或者webpack中配置搭配unplugin-auto-import和 unplugin-vue-components插件自动按需引用)
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { VueUseComponentsResolver } from 'unplugin-vue-components/resolvers';
export default defineConfig({
plugins: [
vue(),
AutoImport({
imports: [
'vue',
'@vueuse/core' // 自动导入 vueuse 中的 API
],
dts: './src/auto-imports.d.js', // 生成声明文件
}),
Components({
dts: './src/components.d.js',
dirs: ['src/components/'], // imports 指定组件所在位置,默认为 src/components
resolvers: [ VueUseComponentsResolver()], // 自动按需引入 vueuse 的组件
}),
],
})
<script setup>
// `useMouse` 从 vueuse 中自动导入
const { x, y } = useMouse()
</script>
Element-Plus按需导入
1、安装element-plus
pnpm install element-plus
2、按需自动导入(首先你需要安装unplugin-vue-components 和 unplugin-auto-import这两款插件)、
// vite.config.ts
import { defineConfig } from 'vite';
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
import Icons from "unplugin-icons/vite";
import IconsResolver from "unplugin-icons/resolver";
export default defineConfig({
// ...
plugins: [
// ...
AutoImport({
//通过 `resolvers` 选项指定使用 ElementPlusResolver 来处理 Element Plus api的自动导入
resolvers: [
// 自动导入 Element Plus 相关函数,如:ElMessage, ElMessageBox... (带样式)
ElementPlusResolver(),
// 自动导入图标组件
IconsResolver({}),
],
vueTemplate: true, // 是否在 vue 模板中自动导入
}),
Components({
//通过 `resolvers` 选项指定使用 `ElementPlusResolver` 来处理 `Element Plus` 组件的自动引入
resolvers: [
// 自动导入 Element Plus 组件
ElementPlusResolver(),
// 自动注册图标组件,element-plus图标库,其他图标库 https://icon-sets.iconify.design/
IconsResolver({ enabledCollections: ["ep"] }),
],
}),
Icons({
// 自动安装图标库
autoInstall: true,
}),
],
})
3、安装自动导入 Icon 依赖
npm i -D unplugin-icons
4、vite.config.js 配置
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path';
// 自动导入
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
// element-plus
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
import Icons from "unplugin-icons/vite";
import IconsResolver from "unplugin-icons/resolver";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
AutoImport({
// 自动导入 Vue 相关函数,如:ref, reactive, toRef 等
imports: ['vue', 'pinia', 'vue-router'],
//通过 `resolvers` 选项指定使用 ElementPlusResolver 来处理 Element Plus api的自动导入
resolvers: [
// 自动导入 Element Plus 相关函数,如:ElMessage, ElMessageBox... (带样式)
ElementPlusResolver(),
// 自动导入图标组件
IconsResolver({}),
],
vueTemplate: true, // 是否在 vue 模板中自动导入
dts: './src/auto-imports.d.js', // 指定自动导入函数类型声明文件路径
}),
Components({
//通过 `resolvers` 选项指定使用 `ElementPlusResolver` 来处理 `Element Plus` 组件的自动引入
resolvers: [
// 自动导入 Element Plus 组件
ElementPlusResolver(),
// 自动注册图标组件,element-plus图标库,其他图标库 https://icon-sets.iconify.design/
IconsResolver({ enabledCollections: ["ep"] }),
],
// 指定自定义组件位置(默认:src/components)
dirs: ["src/components", "src/**/components"],
dts: './src/components.d.js', // 指定自动导入组件类型声明文件路径
}),
Icons({
// 自动安装图标库
autoInstall: true,
}),
],
resolve: {
alias: { '@': resolve(__dirname, './src') }
},
})
5、自动引入Element Plus后,当我们想要使用命令的方式创建element组件时,样式会无法自动引入,我们以ElMessage为例
import { ElMessage } from 'element-plus'
ElMessage.warning('warning')
这时候message组件的样式会无法引入,需要安装unplugin-element-plus进行弥补
pnpm i unplugin-element-plus -D
import ElementPlus from 'unplugin-element-plus/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
plugins: [
ElementPlus({
importStyle: 'sass',
useSource: true
}),
Components({
resolvers: [
ElementPlusResolver()
]
})
]
})
安装SCSS
pnpm i sass -D --save
安装unocss
1、
npm install -D unocss
2、在 vite.config.js 或 vite.config.ts 文件中添加 UnoCSS 配置
// vite.config.js
import { defineConfig } from 'vite';
import UnoCSS from 'unocss/vite';
export default defineConfig({
plugins: [
UnoCSS(),
],
});
3、在入口文件main.js中引入
import 'uno.css'
安装Pinia
Pinia 是 Vue 的专属状态管理库,它允许你跨组件或页面共享状态。
1、安装依赖
npm install pinia
2、main.ts 引入 pinia
// src/main.ts
import { createPinia } from "pinia";
import App from "./App.vue";
createApp(App).use(createPinia()).mount("#app");
3、定义Store,新建文件 src/store/counter.ts
// src/store/counter.ts
import { defineStore } from "pinia";
export const useCounterStore = defineStore("counter", () => {
// ref变量 → state 属性
const count = ref(0);
// computed计算属性 → getters
const double = computed(() => {
return count.value * 2;
});
// function函数 → actions
function increment() {
count.value++;
}
return { count, double, increment };
});
父组件
<!-- src/App.vue -->
<script setup lang="ts">
import HelloWorld from "@/components/HelloWorld.vue";
import { useCounterStore } from "@/store/counter";
const counterStore = useCounterStore();
</script>
<template>
<h1 class="text-3xl">vue3-element-admin-父组件</h1>
<el-button type="primary" @click="counterStore.increment">count++</el-button>
<HelloWorld />
</template>
子组件
<!-- src/components/HelloWorld.vue -->
<script setup lang="ts">
import { useCounterStore } from "@/store/counter";
const counterStore = useCounterStore();
</script>
<template>
<el-card class="text-left text-white border-white border-1 border-solid mt-10 bg-[#242424]" >
<template #header> 子组件 HelloWorld.vue</template>
<el-form>
<el-form-item label="数字:"> {{ counterStore.count }}</el-form-item>
<el-form-item label="加倍:"> {{ counterStore.double }}</el-form-item>
</el-form>
</el-card>
</template>
效果预览
环境变量
Vite 环境变量主要是为了区分开发、测试、生产等环境的变量
参考: Vite 环境变量配置官方文档
env配置文件
项目根目录新建 .env.development 、.env.production
-
开发环境变量配置:.env.development
properties 代码解读 复制代码 # 变量必须以 VITE_ 为前缀才能暴露给外部读取 VITE_APP_TITLE = 'vue3-element-admin' VITE_APP_PORT = 3000 VITE_APP_BASE_API = '/dev-api' -
生产环境变量配置:.env.production
properties 代码解读 复制代码 VITE_APP_TITLE = 'vue3-element-admin' VITE_APP_PORT = 3000 VITE_APP_BASE_API = '/prod-api'反向代理解决跨域
跨域原理
浏览器同源策略: 协议、域名和端口都相同是同源,浏览器会限制非同源请求读取响应结果。
本地开发环境通过 Vite 配置反向代理解决浏览器跨域问题,生产环境则是通过 nginx 配置反向代理 。
vite.config.ts 配置代理
表面肉眼看到的请求地址: http://localhost:3000/dev-api/api/v1/users/me
真实访问的代理目标地址: http://vapi.youlai.tech/api/v1/users/me
整合 Axios
Axios 基于promise可以用于浏览器和node.js的网络请求库
参考: Axios 官方文档
安装依赖
bash
代码解读
复制代码
npm install axios
Axios 工具类封装
typescript
代码解读
复制代码
// src/utils/request.ts
import axios, { InternalAxiosRequestConfig, AxiosResponse } from 'axios';
import { useUserStoreHook } from '@/store/modules/user';
// 创建 axios 实例
const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 50000,
headers: { 'Content-Type': 'application/json;charset=utf-8' }
});
// 请求拦截器
service.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const userStore = useUserStoreHook();
if (userStore.token) {
config.headers.Authorization = userStore.token;
}
return config;
},
(error: any) => {
return Promise.reject(error);
}
);
// 响应拦截器
service.interceptors.response.use(
(response: AxiosResponse) => {
const { code, msg } = response.data;
// 登录成功
if (code === '00000') {
return response.data;
}
ElMessage.error(msg || '系统出错');
return Promise.reject(new Error(msg || 'Error'));
},
(error: any) => {
if (error.response.data) {
const { code, msg } = error.response.data;
// token 过期,跳转登录页
if (code === 'A0230') {
ElMessageBox.confirm('当前页面已失效,请重新登录', '提示', {
confirmButtonText: '确定',
type: 'warning'
}).then(() => {
localStorage.clear(); // @vueuse/core 自动导入
window.location.href = '/';
});
}else{
ElMessage.error(msg || '系统出错');
}
}
return Promise.reject(error.message);
}
);
// 导出 axios 实例
export default service;
vue-router 动态路由
安装 vue-router
bash
代码解读
复制代码
npm install vue-router@next
路由实例
创建路由实例,顺带初始化静态路由,而动态路由需要用户登录,根据用户拥有的角色进行权限校验后进行初始化
typescript
代码解读
复制代码
// src/router/index.ts
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
export const Layout = () => import('@/layout/index.vue');
// 静态路由
export const constantRoutes: RouteRecordRaw[] = [
{
path: '/redirect',
component: Layout,
meta: { hidden: true },
children: [
{
path: '/redirect/:path(.*)',
component: () => import('@/views/redirect/index.vue')
}
]
},
{
path: '/login',
component: () => import('@/views/login/index.vue'),
meta: { hidden: true }
},
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: [
{
path: 'dashboard',
component: () => import('@/views/dashboard/index.vue'),
name: 'Dashboard',
meta: { title: 'dashboard', icon: 'homepage', affix: true }
}
]
}
];
/**
* 创建路由
*/
const router = createRouter({
history: createWebHashHistory(),
routes: constantRoutes as RouteRecordRaw[],
// 刷新时,滚动条位置还原
scrollBehavior: () => ({ left: 0, top: 0 })
});
/**
* 重置路由
*/
export function resetRouter() {
router.replace({ path: '/login' });
location.reload();
}
export default router;
全局注册路由实例
typescript
代码解读
复制代码
// main.ts
import router from "@/router";
app.use(router).mount('#app')
动态权限路由
路由守卫 src/permission.ts ,获取当前登录用户的角色信息进行动态路由的初始化
最终调用 permissionStore.generateRoutes(roles) 方法生成动态路由
typescript
代码解读
复制代码
// src/store/modules/permission.ts
import { listRoutes } from '@/api/menu';
export const usePermissionStore = defineStore('permission', () => {
const routes = ref<RouteRecordRaw[]>([]);
function setRoutes(newRoutes: RouteRecordRaw[]) {
routes.value = constantRoutes.concat(newRoutes);
}
/**
* 生成动态路由
*
* @param roles 用户角色集合
* @returns
*/
function generateRoutes(roles: string[]) {
return new Promise<RouteRecordRaw[]>((resolve, reject) => {
// 接口获取所有路由
listRoutes()
.then(({ data: asyncRoutes }) => {
// 根据角色获取有访问权限的路由
const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles);
setRoutes(accessedRoutes);
resolve(accessedRoutes);
})
.catch(error => {
reject(error);
});
});
}
// 导出 store 的动态路由数据 routes
return { routes, setRoutes, generateRoutes };
});
接口获取得到的路由数据
根据路由数据 (routes)生成菜单的关键代码
| src/layout/componets/Sidebar/index.vue | src/layout/componets/Sidebar/SidebarItem.vue |
|---|---|
按钮权限
除了 Vue 内置的一系列指令 (比如 v-model 或 v-show) 之外,Vue 还允许你注册自定义的指令 (Custom Directives),以下就通过自定义指令的方式实现按钮权限控制。
自定义指令
typescript
代码解读
复制代码
// src/directive/permission/index.ts
import { useUserStoreHook } from '@/store/modules/user';
import { Directive, DirectiveBinding } from 'vue';
/**
* 按钮权限
*/
export const hasPerm: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
// 「超级管理员」拥有所有的按钮权限
const { roles, perms } = useUserStoreHook();
if (roles.includes('ROOT')) {
return true;
}
// 「其他角色」按钮权限校验
const { value } = binding;
if (value) {
const requiredPerms = value; // DOM绑定需要的按钮权限标识
const hasPerm = perms?.some(perm => {
return requiredPerms.includes(perm);
});
if (!hasPerm) {
el.parentNode && el.parentNode.removeChild(el);
}
} else {
throw new Error(
"need perms! Like v-has-perm="['sys:user:add','sys:user:edit']""
);
}
}
};
全局注册自定义指令
typescript
代码解读
复制代码
// src/directive/index.ts
import type { App } from 'vue';
import { hasPerm } from './permission';
// 全局注册 directive 方法
export function setupDirective(app: App<Element>) {
// 使 v-hasPerm 在所有组件中都可用
app.directive('hasPerm', hasPerm);
}
typescript
代码解读
复制代码
// src/main.ts
import { setupDirective } from '@/directive';
const app = createApp(App);
// 全局注册 自定义指令(directive)
setupDirective(app);
组件使用自定义指令
html
代码解读
复制代码
// src/views/system/user/index.vue
<el-button v-hasPerm="['sys:user:add']">新增</el-button>
<el-button v-hasPerm="['sys:user:delete']">删除</el-button>
px 转 vw
这是一个将像素单位转换为视口单位(vw、vh、vmin、vmax)的PostCSS插件 安装 要使用这个插件,你需要在你的项目中设置好PostCSS。如果你还没有设置PostCSS,你可以通过运行以下命令来安装:
npm install postcss --save
接下来,安装postcss-px-conversion插件:
npm install postcss-px-conversion --save
使用
要在你的PostCSS配置中使用这个插件,将其添加到PostCSS插件列表中,同时加上所需的配置选项。
以下是在postcss.config.js中的示例配置:
// postcss.config.js
module.exports = {
plugins: {
"postcss-px-conversion": {
unitType: "px", // 要从哪种单位转换(默认为'px')
viewportWidth: 375,
enablePerFileConfig: true, // 启用per-file配置
viewportWidthComment: "viewport-width", // 用于指定视口宽度的注释
// 其他配置选项...
},
},
};
Nuxt3 集成
nuxt.config.ts
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
postcss: {
plugins: {
"postcss-px-conversion": {
unitType: "px", // 要从哪种单位转换(默认为'px')
viewportWidth: 375,
unitPrecision: 10,
viewportUnit: "vw",
minPixelValue: 1,
includeFiles: [//pages//],
excludeFiles: [/node_modules/, /pages/active/index.vue/]
}
}
}
});
目录结构
- package.json
- pages
- index
- index.vue
- active
-index.vue
index 页面会被自动转换单位,active页面不会
你可以使用各种选项来配置这个插件:
unitType:要从哪种单位转换(默认为'px')。viewportWidth:视口的宽度。unitPrecision:vw单位的小数位数。allowedProperties:要转换为vw的CSS属性列表。excludedProperties:要排除在转换之外的CSS属性列表。viewportUnit:期望的视口单位(vw、vh、vmin、vmax)。fontViewportUnit:期望的字体视口单位。selectorBlacklist:要忽略的选择器(字符串或正则表达式)。minPixelValue:要替换的最小像素值。allowMediaQuery:在媒体查询中允许px到vw的转换。replaceRules:替换包含vw的规则而不是添加回退规则。excludeFiles:要忽略的文件(作为正则表达式数组)。includeFiles:只转换匹配的文件(作为正则表达式数组)。enableLandscape:为横向模式添加@media (orientation: landscape)。landscapeUnit:横向模式的期望单位。landscapeViewportWidth:横向方向的视口宽度。enablePerFileConfig:启用per-file配置(默认为true)。viewportWidthComment:用于指定视口宽度的注释(默认为"viewport-width")。
请根据你的项目需求调整这些选项。
Per-File 配置
此插件现在支持per-file配置,允许你为每个CSS或SCSS文件指定不同的视口宽度。要使用这个功能,只需在文件的开头添加一个特殊的注释:
/* viewport-width: 1920 */
插件会读取这个注释并使用指定的宽度来进行单位转换。这对于在同一个项目中为不同的设备(如PC、平板、手机)创建不同的CSS文件特别有用。
示例
以下是一个示例配置,将像素值转换为vw单位,默认视口宽度为750像素,并启用per-file配置:
// postcss.config.js
module.exports = {
plugins: {
"postcss-px-conversion": {
unitType: "px",
viewportWidth: 750,
unitPrecision: 5,
allowedProperties: ["*"],
excludedProperties: [],
viewportUnit: "vw",
fontViewportUnit: "vw",
selectorBlacklist: [],
minPixelValue: 1,
allowMediaQuery: false,
replaceRules: true,
excludeFiles: [],
includeFiles: [],
enableLandscape: false,
landscapeUnit: "vw",
landscapeViewportWidth: 568,
enablePerFileConfig: true,
viewportWidthComment: "viewport-width",
},
},
};
使用这个配置,你的CSS中的像素值将在PostCSS处理期间自动转换为视口单位。同时,你可以在每个文件中使用注释来指定该文件的特定视口宽度。
例如,在一个针对桌面设备的CSS文件中:
/* viewport-width: 1920 */
.header {
width: 1600px; /* 将被转换为 83.33333vw */
}
而在一个针对移动设备的CSS文件中:
/* viewport-width: 375 */
.header {
width: 350px; /* 将被转换为 93.33333vw */
}
这样,你就可以在一次构建中生成适配多种设备的CSS,同时保持了代码的灵活性和可维护性。