一、基础项目搭建:
- 技术栈
技术栈 | 描述 | 官网 |
---|---|---|
Vue3 | 渐进式 JavaScript 框架 | https://cn.vuejs.org/ |
Element Plus | 基于 Vue 3,面向设计师和开发者的组件库 | https://element-plus.gitee.io/zh-CN/ |
Vite | 前端开发与构建工具 | https://cn.vitejs.dev/guide/ |
TypeScript | 开发语言,是 JavaScript 的超集 | |
Vue Router | Vue.js 的官方路由 | https://router.vuejs.org/zh/ |
wangEditor | Typescript 开发的 Web 富文本编辑器 | www.wangeditor.com/ |
Echarts | 开源可视化图表库 | https://echarts.apache.org/zh/ |
vue-i18n | Vue 国际化多语言插件 | https://vue-i18n.intlify.dev/ |
VueUse | 基于Vue组合式API的实用工具集(类比HuTool工具) | http://www.vueusejs.com/guide/ |
- 环境准备
环境 | 名称 |
---|---|
运行环境 | Node 16+ |
开发工具 | VSCode |
VSCode插件 | Vue Language Features (Volar) TypeScript Vue Plugin (Volar) ,禁用Vetur |
推荐安装nvm控制node版本
1. 项目初始化
按照 vite 官网搭建第一个 Vite 项目,执行以下命令完成 vue 、typescirpt 模板项目的初始化
npm init vite@latest vue3-element-admin --template vue-ts
2. 使用Vscode启动项目
npm install
npm run dev
3. src 路径别名配置
相对路径别名配置,使用 @ 代替 src
配置 vite.config.ts
npm install @types/node
import {
defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import path from "path"; //这个path用到了上面安装的@types/node
const pathSrc = path.resolve(__dirname, "src");
// https://vitejs.dev/config/
export default defineConfig({
// 路径别名
resolve: {
alias: {
"@": pathSrc, // @代替src
},
},
plugins: [vue()],
});
// src/App.vue
import HelloWorld from '../src/components/HelloWorld.vue'
import HelloWorld from '@/components/HelloWorld.vue'
修改tsconfig.json
"compilerOptions": {
...
"baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录
"paths": {
// 路径映射,相对于baseUrl
"@/*": ["src/*"]
}
}
二、使用Element Plus组件库
1. 安装
# 选择一个你喜欢的包管理器
# NPM
$ npm install element-plus --save
# Yarn
$ yarn add element-plus
# pnpm
$ pnpm install element-plus
2. 用法
2.1 完整引入
如果你对打包后的文件大小不是很在乎,那么使用完整导入会更方便。
// main.ts
import {
createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
2.2 按需导入
您需要使用额外的插件来导入要使用的组件。
2.3 自动导入(推荐)
首先你需要安装unplugin-vue-components 和 unplugin-auto-import这两款插件
npm install -D unplugin-vue-components unplugin-auto-import
vite.config.ts - 自动导入配置
src
目录下,新建 types
目录,用于存放自动导入函数和组件的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 path from "path"; //这个path用到了上面安装的@types/node
const pathSrc = path.resolve(__dirname, "src");
// https://vitejs.dev/config/
export default defineConfig({
// 路径别名
resolve: {
alias: {
"@": pathSrc, // @代替src
},
},
plugins: [
vue(),
AutoImport({
// 自动导入 Vue 相关函数,如:ref, reactive, toRef 等
imports: ["vue"],
eslintrc: {
enabled: true, // 是否自动生成 eslint 规则,建议生成之后设置 false
filepath: "./.eslintrc-auto-import.json", // 指定自动导入函数 eslint 规则的文件
},
dts: path.resolve(pathSrc, "types", "auto-imports.d.ts"), // 指定自动导入函数TS类型声明文件路径
}),
Components({
dts: path.resolve(pathSrc, "types", "components.d.ts"), // 指定自动导入组件TS类型声明文件路径
}),
],
});
.eslintrc.cjs - 自动导入函数 eslint 规则引入
"extends": [
"./.eslintrc-auto-import.json"
],
2.4 手动导入
Element Plus 提供了基于 ES Module 的开箱即用的 Tree Shaking 功能。
但你需要安装 unplugin-element-plus 来导入样式。 配置文档参考 docs
App.vue
<template>
<el-button>我是 ElButton</el-button>
</template>
<script>
import {
ElButton } from 'element-plus'
export default {
components: {
ElButton },
}
</script>
// vite.config.ts
import {
defineConfig } from 'vite'
import ElementPlus from 'unplugin-element-plus/vite'
export default defineConfig({
// ...
plugins: [ElementPlus()],
})
三. 使用 Icon
1. 安装导入 Icon 依赖
npm i -D unplugin-icons
2. 配置 vite.config.ts
// vite.config.ts
import {
ElementPlusResolver } from "unplugin-vue-components/resolvers";
import Icons from "unplugin-icons/vite";
import IconsResolver from "unplugin-icons/resolver";
export default ({
mode }: ConfigEnv): UserConfig => {
return {
plugins: [
// ...
AutoImport({
// ...
resolvers: [
// 自动导入 Element Plus 相关函数,如:ElMessage, ElMessageBox... (带样式)
ElementPlusResolver(),
// 自动导入图标组件
IconsResolver({
}),
]
vueTemplate: true, // 是否在 vue 模板中自动导入
dts: path.resolve(pathSrc, 'types', 'auto-imports.d.ts') // 自动导入组件类型声明文件位置,默认根目录
}),
Components({
resolvers: [
// 自动导入 Element Plus 组件
ElementPlusResolver(),
// 自动注册图标组件
IconsResolver({
enabledCollections: ["ep"] // element-plus图标库,其他图标库 https://icon-sets.iconify.design/
}),
],
dts: path.resolve(pathSrc, "types", "components.d.ts"), // 自动导入组件类型声明文件位置,默认根目录
}),
Icons({
// 自动安装图标库
autoInstall: true,
}),
],
};
};
参考配置:element-plus-best-practices - vite.config.ts
<template>
<h1>{
{
msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {
{
count }}</button>
</div>
<div class="card">
<el-button type="success"><i-ep-SuccessFilled />Success</el-button>
<el-button type="info"><i-ep-InfoFilled />Info</el-button>
<el-button type="warning"><i-ep-WarningFilled />Warning</el-button>
<el-button type="danger"><i-ep-WarnTriangleFilled />Danger</el-button>
</div>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>
四、使用 SVG 图标
通过 vite-plugin-svg-icons 插件,Iconfont
第三方图标库实现本地图标。
1. 安装依赖
npm install -D fast-glob@3.2.11
npm install -D vite-plugin-svg-icons@2.0.1
2. 创建 src/assets/icons 目录 , 把svg 图标放入。
3. main.ts 引入注册脚本
// src/main.ts
import 'virtual:svg-icons-register';
4. 配置 vite.config.ts
// vite.config.ts
import {
createSvgIconsPlugin } from 'vite-plugin-svg-icons';
export default ({
command, mode}: ConfigEnv): UserConfig => {
return (
{
plugins: [
createSvgIconsPlugin({
// 指定需要缓存的图标文件夹
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
// 指定symbolId格式
symbolId: 'icon-[dir]-[name]',
})
]
}
)
}
5. SVG 组件封装以及使用
- 组件封装,新建 src/components/SvgIcon/index.vue
<script setup lang="ts">
const props = defineProps({
prefix: {
type: String,
default: "icon",
},
name: {
type: String,
required: false,
},
color: {
type: String,
},
size: {
type: String,
default: "1em",
},
});
const symbolId = computed(() => `#${
props.prefix}-${
props.name}`);
</script>
<template>
<svg
aria-hidden="true"
class="svg-icon"
:style="'width:' + size + ';height:' + size"
>
<use :xlink:href="symbolId" :fill="color" />
</svg>
</template>
<style scoped>
.svg-icon {
display: inline-block;
outline: none;
width: 1em;
height: 1em;
vertical-align: -0.15em; /* 因icon大小被设置为和字体大小一致,而span等标签的下边缘会和字体的基线对齐,故需设置一个往下的偏移比例,来纠正视觉上的未对齐效果 */
fill: currentColor; /* 定义元素的颜色,currentColor是一个变量,这个变量的值就表示当前元素的color值,如果当前元素未设置color值,则从父元素继承 */
overflow: hidden;
}
</style>
- 组件使用
<template>
<el-button type="info"><svg-icon name="bl"/>本地svg</el-button>
</template>
参考:vite-plugin-svg-icons 文档
五、使用全局状态管理工具 pinia
1. pinia介绍
Pinia 是 Vue 的专属状态管理库,它允许你跨组件或页面共享状态。
pinia优点:
- 支持Vue2和Vue3,也就是老项目也可以使用Pinia。
- 足够轻量,压缩后的体积只有1kb左右。
- 完整的TypeScript支持,Vue3版本的一大优势就是对TypeScript的支持,所以Pinia也做到了完整的支持。如果你对Vuex很熟悉的化,一定知道Vuex对TS的语法支持不是完整的。
- 代码更加简洁,可以实现很好的代码自动分割。Vue2的时代,写代码需要来回翻滚屏幕屏幕找变量,非常的麻烦,Vue3的Composition api完美了解决这个问题。 可以实现代码自动分割,pinia也同样继承了这个优点。
- 去除 mutations,只有 state,getters,actions;actions 支持同步和异步。
- 不需要嵌套模块,让代码更加扁平化,只有 store 的概念,store 之间可以自由使用,每一个store都是独立的,符合Vue3的Composition api 。
- 无需手动添加 store,store 一旦创建便会自动添加。
1.1 安装
yarn add pinia
# 或者使用 npm
npm install pinia
1.2 引入
创建一个 pinia 实例 (根 store) 并将其传递给应用:
import {
createApp } from 'vue'
import {
createPinia } from 'pinia'
import App from './App.vue'
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
app.mount('#app')
1.3 使用
在 src
文件夹下创建 store
文件夹,并添加 counter.js
文件。
2. Pinia中的Store
2.1 定义store
Store
(如 Pinia)是一个保存状态和业务逻辑的实体,它并不与你的组件树绑定。换句话说,它承载着全局状态。它有点像一个永远存在的组件,每个组件都可以读取和写入它。它有三个概念,state
、getter
和action
,我们可以假设这些概念相当于组件中的data
、computed
和methods
。store
是用defineStore(name, function | options)
定义的,建议其函数返回的值命名为use…Store
方便理解
- 参数
name
:必填值且唯一,简单点说就可以理解成是一个命名空间。 - 参数
function | options
:可以是对象或函数形式。 -
- 对象形式【选项模式】,其中配置
state
、getters
和actions
选项。
- 对象形式【选项模式】,其中配置
-
- 函数形式【组合模式,类似组件组合式 API 的书写方式】,定义响应式变量和方法,并且 return 对应的变量和方法;ref() 相当于 state,computed() 相当于 getters,function() 相当于 actions。
2.2 使用store读取和写入 state
下面案例以选项模式为例:
在 store
文件夹下创建 counter.js
文件,这个文件就是存有关 counter 的一些相关的数据。
import {
defineStore} from 'pinia'
/*defineStore 是需要传参数的,其中第一个参数是id,就是一个唯一的值,
简单点说就可以理解成是一个命名空间.
第二个参数就是一个对象,里面有三个模块需要处理,第一个是 state,
第二个是 getters,第三个是 actions。
*/
const useCounter = defineStore("counter",{
state:() => ({
count:88,
}),
getters: {
},
actions: {
}
})
//暴露useCounter这个模块
export default useCounter
在页面中使用:
<template>
<div>
<el-button>我是 ElButton</el-button>
<div>store===>counter.js的count值:{
{
counterStore.count }}</div>
</div>
</template>
<script setup>
// 引入创建的store
import useCounter from "./store/counter";
// 调用store里的方法
const counterStore = useCounter();
console.log("counterStore", counterStore.count);
</script>
<style scoped></style>
**注意:**在使用时 ,取值时不用和 vuex 一样还要.state,直接.state里面的count值就行了,写法:counterStore.count。
案例需求,点击按钮加一:
我们分别用两种方法取count值,一个解构,一个不解构
<template>
<div>
<h2>Home Word</h2>
<h2>展示pinia的counter的count值: {
{
counterStore.count }}</h2>
<h2>展示解构出来的pinia的counter的count值: {
{
count }}</h2>
<el-button @click="addCount">function count+1</el-button>
</div>
</template>
<script setup>
// 引入创建的store
import useCounter from "./store/counter";
// 调用store里的方法
const counterStore = useCounter();
// 结构count值
const {
count } = counterStore;
function addCount() {
//这里可以直接操作count,修改(写入)store,在vuex还要commit在mutaitions修改数据
counterStore.count++;
}
</script>
我们发现解构出来的值 失去响应式了。
解决方案:
为了从 store
中提取属性时保持其响应性,你需要使用 storeToRefs()
。它将为每一个响应式属性创建引用。当你只使用 store 的状态而不调用任何 action 时,它会非常有用。
<template>
<div>
<h2>Home Word</h2>
<!-- 使用useCounter的实例获取state中的值 -->
<h2>展示pinia的counter的count值: {
{
counterStore.count }}</h2>
<h2>展示解构出来的pinia的counter的count值: {
{
count }}</h2>
<el-button @click="addCount">function count+1</el-button>
</div>
</template>
<script setup>
import {
storeToRefs } from "pinia";
// 引入创建的store
import useCounter from "./store/counter";
// 调用store里的方法
const counterStore = useCounter();
// 结构count值
const {
count } = storeToRefs(counterStore);
function addCount() {
//这里可以直接操作count,修改(写入)store,在vuex还要commit在mutaitions修改数据
counterStore.count++;
}
</script>
2.3 修改state数据
- 定义一个关于user的Store
import {
defineStore } from 'pinia'
const useUser = defineStore("user", {
state: () => ({
name: "why",
age: 18,
level: 100
}),
actions:{
setCurrent () {
this.level++
}
}
})
export default useUser
- 三种修改state的方法
<template>
<div>
<h2>Home Word</h2>
<h2>姓名: {
{
name }}</h2>
<h2>年龄: {
{
age }}</h2>
<h2>等级: {
{
level }}</h2>
<el-button @click="updateStore">修改user信息</el-button>
<el-button @click="updateStoreLevel">修改level</el-button>
</div>
</template>
<script setup>
import useUser from "./store/user";
import {
storeToRefs } from "pinia";
const userStore = useUser();
const {
name, age, level } = storeToRefs(userStore);
function updateStore() {
// 方法一:一个个的修改状态
// userStore.name = "zimo";
// userStore.age = 20;
// 方法二 :一次性修改多个状态
// userStore.$patch({
// name: "zimo",
// age: 20,
// });
// 方法三:替换state为新的对象
const oldState = userStore.$state;
userStore.$state = {
...userStore.$state,
name: "curry",
level: 200,
};
// 下面会返回true
console.log(oldState === userStore.$state);
}
// 方法四:通过actions修改
const updateStoreLevel = () => {
userStore.setCurrent();
};
</script>
2.4 重置state数据
新增一个重置按钮:
<el-button @click="resetStore">重置user信息</el-button>
新增一个重置方法:
function resetStore() {
userStore.$reset();
}
3. Pinia中的getters
getters 类似于 vue 里面的计算属性,可以对已有的数据进行修饰。getters中可以定义接受一个state作为参数的函数,不管调用多少次,getters中的函数只会执行一次,且都会缓存。
3.1 定义getters
- 基本使用
- 一个getter引入另外一个getter
- getters也支持返回一个函数
- getters中用到别的store中的数据
// 定义关于counter的store
import {
defineStore } from 'pinia'
const useCounter = defineStore("counter", {
state: () => ({
count: 99
}),
getters: {
// 1.基本使用
doubleCount(state) {
return state.count * 2
},
// 2.一个getter引入另外一个getter
doubleCountAddOne() {
// this是store实例,可以直接使用另一个getter
return this.doubleCount + 1
},
// 3.getters也支持返回一个函数
getFriendById(state) {
return function(id) {
return id
}
},
// 4.getters中用到别的store中的数据
showMessage(state) {
//获取user信息,拿到useUser模块
const userStore = useUser()
//拼接信息
return `name:${
userStore.name}-count:${
state.count}`
}
},
})
export default useCounter
3.2 访问getters
<template>
<div>
<!-- 在模板中使用 -->
<h2>基本使用:doubleCount: {
{
counterStore.doubleCount }}</h2>
<h2>
一个getter引入另外一个getter:doubleCountAddOne:
{
{
counterStore.doubleCountAddOne }}
</h2>
<h2>函数id-99: {
{
counterStore.getFriendById(99) }}</h2>
<h2>
getters中获取另一个store中的state/getters数据==>showMessage:{
{
counterStore.showMessage
}}
</h2>
</div>
</template>
<script setup>
import useCounter from "./store/counter";
const counterStore = useCounter();
// 在js文件中使用
const doubleCount = counterStore.doubleCount;
const doubleCountAddOne = counterStore.doubleCountAddOne;
const frend = counterStore.getFriendById(99);
</script>
**注意:**getters中用别的store中的数据 ,在counter模块中拿user模块的store数据,要引入user模块。
4. Pinia中的actions
actions
相当于组件中的 methods
。
可以使用 defineStore()
中的 actions
属性定义,并且它们非常适合定义一些业务逻辑。
和 getters
一样,在 action
中可以通过 this
访问整个 store
实例的所有操作。
4.1 同步
- 定义关于counter的store
// 定义关于counter的store
import {
defineStore } from 'pinia'
const useCounter = defineStore("counter", {
state: () => ({
count: 99,
}),
// 定义actions
actions: {
increment() {
this.count++
},
incrementNum(num) {
this.count += num
}
}
})
export default useCounter
- 同步使用方式
<template>
<div>
<h2>Home Word</h2>
<h2>doubleCount: {
{
counterStore.count }}</h2>
<el-button @click="changeState">修改state</el-button>
</div>
</template>
<script setup>
import useCounter from "./store/counter";
const counterStore = useCounter();
function changeState() {
// 可以通过counterStore对象直接使用
counterStore.increment();
// counterStore.incrementNum(10);
}
</script>
4.2 异步
结合async await 修饰
- 定义store
import {
defineStore } from 'pinia'
const Login = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
name: 'Rlm',
isLogin: true
})
}, 3000)
})
}
const useUser = defineStore("user", {
state: () => ({
user: {
},
name: "123"
}),
actions:{
async getLoginInfo() {
const result = await Login()
this.user = result;
}
}
})
export default useUser
- 页面使用
<template>
<div>
<h2>Home Word</h2>
<h2>user: {
{
userStore.user }}</h2>
<el-button @click="Login">获取user</el-button>
</div>
</template>
<script setup>
import useUser from "./store/user";
const userStore = useUser();
const Login = () => {
userStore.getLoginInfo();
};
</script>
- 多个action互相调用getLoginInfo setName
import {
defineStore } from 'pinia'
const Login = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
name: 'Rlm',
isLogin: true
})
}, 3000)
})
}
const useUser = defineStore("user", {
state: () => ({
user: {
},
name: "123"
}),
actions:{
async getLoginInfo() {
const result = await Login()
this.user = result;
this.setName(result.name)
},
setName (name) {
this.name = name;
}
}
})
export default useUser
5. Pinia的API
5.1 重置 state
$reset() 将会把state所有值 重置回 原始状态。前面2.4 重置state数据有写到。
5.2 订阅 state
类似于 Vuex 的 subscribe 方法,你可以通过 store 的 $subscribe() 方法侦听 state 及其变化。比起普通的 watch(),使用 $subscribe() 的好处是 subscriptions 在 patch 后只触发一次 (例如,当使用上面的函数版本时)。
- 定义user store
import {
defineStore } from 'pinia'
const useUser = defineStore("user", {
state: () => ({
name: "why",
age: 18,
level: 100
}),
actions:{
setCurrent () {
this.level++
}
}
})
export default useUser
- 页面使用
<template>
<div>
<h2>Home Word</h2>
<h2>姓名: {
{
name }}</h2>
<h2>年龄: {
{
age }}</h2>
<h2>等级: {
{
level }}</h2>
<el-button @click="updateStore">修改user信息</el-button>
<el-button @click="updateStoreLevel">修改level</el-button>
<el-button @click="resetStore">重置user信息</el-button>
</div>
</template>
<script setup>
import useUser from "./store/user";
import {
storeToRefs } from "pinia";
const userStore = useUser();
const {
name, age, level } = storeToRefs(userStore);
userStore.$subscribe((mutation, state) => {
// mutation.type // type的三种类型 'direct' | 'patch object' | 'patch function'
// 和 cartStore.$id 一样
// mutation.storeId // 'user'
// payload只有 mutation.type === 'patch object'的情况下才可用
// mutation.payload // 传递给 cartStore.$patch() 的补丁对象。
console.log(mutation, state);
// 每当状态发生变化时,将整个 state 持久化到本地存储。
localStorage.setItem("user", JSON.stringify(state));
});
function updateStore() {
// 方法一:一个个的修改状态 订阅 state type 为 'direct'
// userStore.name = "zimo";
// userStore.age = 20;
// 方法二 :一次性修改多个状态 订阅 state type 为 'patch object'
userStore.$patch({
name: "zimo",
age: 20,
});
// 方法三:替换state为新的对象 订阅 state type 为 'patch function'
// const oldState = userStore.$state;
// userStore.$state = {
// ...userStore.$state,
// name: "curry",
// level: 200,
// };
// 下面会返回true
// console.log(oldState === userStore.$state);
}
// 方法四:通过actions修改 订阅 state type 为 'direct'
const updateStoreLevel = () => {
userStore.setCurrent();
};
// 重置user信息方法
function resetStore() {
userStore.$reset();
}
</script>
默认情况下,state subscription 会被绑定到添加它们的组件上 (如果 store 在组件的 setup() 里面)。这意味着,当该组件被卸载时,它们将被自动删除。如果你想在组件卸载后依旧保留它们,请将 { detached: true }
作为第二个参数,以将 state subscription 从当前组件中分离:
userStore.$subscribe(
(mutation, state) => {
// mutation.type // 'direct' | 'patch object' | 'patch function'
// 和 cartStore.$id 一样
// mutation.storeId // 'user'
// 只有 mutation.type === 'patch object'的情况下才可用
// mutation.payload // 传递给 cartStore.$patch() 的补丁对象。
console.log(mutation, state);
// 每当状态发生变化时,将整个 state 持久化到本地存储。
localStorage.setItem("user", JSON.stringify(state));
},
{
detached: true,
}
);
5.3 订阅 action
你可以通过 store.$onAction() 来监听 action 和它们的结果。传递给它的回调函数会在 action 本身之前执行。after 表示在 promise 解决之后,允许你在 action 解决后执行一个回调函数。同样地,onError 允许你在 action 抛出错误或 reject 时执行一个回调函数。这些函数对于追踪运行时错误非常有用,类似于Vue docs 中的这个提示。
- 定义store
import {
defineStore } from 'pinia'
const Login = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
name: 'Rlm',
isLogin: true
})
}, 3000)
})
}
const useUser = defineStore("user", {
state: () => ({
user: {
},
name: "123",
count:1
}),
actions:{
async getLoginInfo() {
const result = await Login()
this.user = result;
this.setName(result.name)
},
setName (name) {
this.name = name;
},
testOnAction(id) {
this.count++
console.log(this.count)
return Promise.resolve('这是testOnAction返回的值')
},
testOnAction2(...args) {
console.log(...args)
return Promise.reject('这是testOnAction222错误返回的值')
}
}
})
- 页面使用
<template>
<div>
<h2>Home Word</h2>
<h2>user: {
{
userStore.user }}</h2>
<h2>name: {
{
userStore.name }}</h2>
<el-button @click="Login">获取user</el-button>
</div>
</template>
<script setup>
import useUser from "./store/user";
const userStore = useUser();
const Login = () => {
userStore.getLoginInfo();
};
// 订阅 action
const unsubscribe = userStore.$onAction(
({
name, // action 名称
store, // store 实例,类似 `someStore`
args, // 传递给 action 的参数数组
after, // 在 action 返回或解决后的钩子
onError, // action 抛出或拒绝的钩子
}) => {
// 这将在执行 "store "的 action 之前触发。
console.log(`Start "${
name}" with params [${
args.join(", ")}].`);
if (name === "testOnAction") {
// 这将在 action 成功并完全运行后触发。
// 它等待着任何返回的 promise
after((reject) => {
//这里可以执行一些操作
console.log("订阅 action---after===>", reject);
});
}
// 如果 action 抛出或返回一个拒绝的 promise,这将触发
onError((error) => {
console.log(error);
});
}
);
// 手动删除监听器
// unsubscribe();
userStore.testOnAction(1);
userStore.testOnAction2(1, 2, 3, 45);
</script>
6. Pinia 数据持久化
6.1 使用插件
npm i pinia-plugin-persist
- main.js中引入插件
// 引入pinia
import {
createPinia } from 'pinia'
import piniaPluginPersist from 'pinia-plugin-persist'
const pinia = createPinia()
pinia.use(piniaPluginPersist)
app.use(pinia)
- 定义store时,开启persist
import {
defineStore } from 'pinia'
export default defineStore('commonStore', {
//开启数据存储
persist: {
enabled: true },
state: () => {
return {
info: {
}
}
},
})
- persist配置项
persist: {
enabled: true,
strategies: [
{
key: 'all', //存储的key值,默认为store名,
storage: localStorage, //存储的位置,默认为sessionStorage
paths: ['info'] //需要存储的state状态,默认为所有
}
]
},
六、使用scss
1. 安装依赖
npm i -D sass
2. 创建 src/styles/variables.scss 变量文件
添加变量 $bg-color 定义,注意规范变量以 $ 开头
// src/styles/variables.scss
$bg-color:#FFB03E;
3. 配置 vite.config.ts
导入 SCSS 全局变量文件
// vite.config.ts
css: {
// CSS 预处理器
preprocessorOptions: {
//define global scss variable
scss: {
javascriptEnabled: true,
additionalData: `@use "@/styles/variables.scss" as *;`
}
}
}
4. style 使用 SCSS 全局变量
<script setup lang="ts">
import {
ref } from "vue";
defineProps<{
msg: string }>();
const count = ref(0);
</script>
<template>
<h1>{
{
msg }}</h1>
<h1 class="box">hello word</h1>
<div class="card">
<button type="button" @click="count++">count is {
{
count }}</button>
</div>
<div class="card">
<el-button type="success"><i-ep-SuccessFilled />Success</el-button>
<el-button type="info"><i-ep-InfoFilled />Info</el-button>
<el-button type="warning"><i-ep-WarningFilled />Warning</el-button>
<el-button type="danger"><i-ep-WarnTriangleFilled />Danger</el-button>
<el-button type="info"><svg-icon name="bl" />本地 svg</el-button>
</div>
</template>
<style lang="scss" scoped>
.read-the-docs {
color: #888;
}
.box {
background-color: $bg-color;
}
</style>
5. 在 TypeScript 使用
- 新建variables.module.scss 文件
// 导出 variables.scss 文件的变量
:export{
bgColor:$bg-color
}
- 使用变量
<script setup lang="ts">
import {
ref } from "vue";
import variables from "@/styles/variables.module.scss";
console.log("scss变量颜色===>", variables.bgColor);
defineProps<{
msg: string }>();
const count = ref(0);
</script>
<template>
<h1 class="box">hello word</h1>
<h1 style="height: 50px" :style="{ 'background-color': variables.bgColor }">
{
{
msg }}
</h1>
<div class="card">
<button type="button" @click="count++">count is {
{
count }}</button>
</div>
<div class="card">
<el-button type="success"><i-ep-SuccessFilled />Success</el-button>
<el-button type="info"><i-ep-InfoFilled />Info</el-button>
<el-button type="warning"><i-ep-WarningFilled />Warning</el-button>
<el-button type="danger"><i-ep-WarnTriangleFilled />Danger</el-button>
<el-button type="info"><svg-icon name="bl" />本地 svg</el-button>
</div>
</template>
<style lang="scss" scoped>
.read-the-docs {
color: #888;
}
.box {
background-color: $bg-color;
}
</style>
七、使用 UnoCSS
unocss是一个即时的原子CSS引擎,它可以让你用简短的类名来控制元素的样式,而不需要写复杂的CSS代码。
1. 安装依赖
npm install -D unocss
2. 配置 vite.config.ts
// vite.config.ts
import UnoCSS from 'unocss/vite'
export default {
plugins: [
UnoCSS({
/* options */ }),
],
}
3. main.ts 引入 uno.css
import 'uno.css'
4. VSCode 安装 UnoCSS 插件
安装启用后,页面上就能看出哪些 class 使用 unocss 提供 (带有虚线),并且能显示类名对应的样式内容。
- 配置vscode的自动提示
ctrl + shift + p =>
输入 open Setting
=> 选择 首选项:打开用户设置
添加以下配置
"editor.quickSuggestions": {
"strings": true,
"other": true,
"comments": true,
},
配置完成后,在输入类名时会对 unocss 中存在的 class 进行智能提示。
八、使用环境变量
Vite的环境变量主要是为了区分开发、测试、生产等环境的变量。
1. 项目根目录新建 env 配置文件
.env.development
、.env.production
- 开发环境变量配置:.env.development
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
VITE_APP_TITLE = 'my-vue3-element-admin'
VITE_APP_PORT = 9000
VITE_APP_BASE_API = '/dev-api'
- 生产环境变量配置:.env.production
VITE_APP_TITLE = 'my-vue3-element-admin'
VITE_APP_PORT = 9000
VITE_APP_BASE_API = '/prod-api'
2. 环境变量智能提示
新建 src/types/env.d.ts
文件存放环境变量TS类型声明
interface ImportMetaEnv {
/**
* 应用标题
*/
VITE_APP_TITLE: string;
/**
* 应用端口
*/
VITE_APP_PORT: number;
/**
* API基础路径(反向代理)
*/
VITE_APP_BASE_API: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
九、反向代理
本地开发环境通过 Vite 配置中 server 反向代理解决浏览器跨域问题,生产环境则是可以通过 nginx 配置反向代理 。
import {
UserConfig, ConfigEnv, loadEnv, defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import path from "path"; //这个path用到了上面安装的@types/node
const pathSrc = path.resolve(__dirname, "src");
// 引入 icon
import {
ElementPlusResolver } from "unplugin-vue-components/resolvers";
import Icons from "unplugin-icons/vite";
import IconsResolver from "unplugin-icons/resolver";
// svg
import {
createSvgIconsPlugin } from "vite-plugin-svg-icons";
// https://vitejs.dev/config/
export default defineConfig(({
mode }: ConfigEnv): UserConfig => {
const env = loadEnv(mode, process.cwd());
return {
// 路径别名
resolve: {
alias: {
"@": pathSrc, // @代替src
},
},
css: {
// CSS 预处理器
preprocessorOptions: {
//define global scss variable
scss: {
javascriptEnabled: true,
additionalData: `@use "@/styles/variables.scss" as *;`,
},
},
},
server: {
host: "0.0.0.0",
port: Number(env.VITE_APP_PORT),
open: true, // 运行是否自动打开浏览器
proxy: {
// 反向代理解决跨域
[env.VITE_APP_BASE_API]: {
target: 'http://localhost:8989', // 本地接口地址
changeOrigin: true,
rewrite: (path) =>
path.replace(new RegExp("^" + env.VITE_APP_BASE_API), ""), // 替换 /dev-api 为 target 接口地址
},
},
},
plugins: [
vue(),
AutoImport({
// 自动导入 Vue 相关函数,如:ref, reactive, toRef 等
imports: ["vue"],
eslintrc: {
enabled: false, // 是否自动生成 eslint 规则,建议生成之后设置 false
filepath: "./.eslintrc-auto-import.json", // 指定自动导入函数 eslint 规则的文件
},
resolvers: [
// 自动导入 Element Plus 相关函数,如:ElMessage, ElMessageBox... (带样式)
ElementPlusResolver(),
// 自动导入图标组件
IconsResolver({
enabledCollections: ["ep"] }),
],
vueTemplate: true, // 是否在 vue 模板中自动导入
dts: path.resolve(pathSrc, "types", "auto-imports.d.ts"), // 指定自动导入函数TS类型声明文件路径
}),
Components({
resolvers: [
// 自动导入 Element Plus 组件
ElementPlusResolver(),
// 自动注册图标组件
IconsResolver({
enabledCollections: ["ep"], // element-plus图标库,其他图标库 https://icon-sets.iconify.design/
}),
],
dts: path.resolve(pathSrc, "types", "components.d.ts"), // 指定自动导入组件TS类型声明文件路径
}),
Icons({
// 自动安装图标库
autoInstall: true,
}),
// createSvgIconsPlugin({
// // 指定需要缓存的图标文件夹
// iconDirs: [path.resolve(process.cwd(), "src/assets/icons")],
// // 指定symbolId格式
// symbolId: "icon-[dir]-[name]",
// }),
createSvgIconsPlugin({
// 指定需要缓存的图标文件夹
iconDirs: [path.resolve(process.cwd(), "src/assets/icons")],
// 指定symbolId格式
symbolId: "icon-[dir]-[name]",
/**
* 自定义插入位置 'body-last' | 'body-first'
* @default: body-last
*/
inject: "body-last",
/**
* custom dom id
* @default: __svg__icons__dom__
*/
customDomId: "__svg__icons__dom__",
}),
],
};
});