Vue3教程

114 阅读13分钟

Vue基本内容

Vue的安装使用

脚手架方式

创建vue3项目需要先安装nodejs

  1. 配置阿里云镜像(仅第一次执行):npm config set registry https://registry.npmmirror.com
  2. 创建命令:npm create vue@latest
  3. 具体配置
## 配置项目名称
√ Project name: vue3_test
## 是否添加TypeScript支持
√ Add TypeScript?  Yes
## 是否添加JSX支持
√ Add JSX Support?  No
## 是否添加路由环境
√ Add Vue Router for Single Page Application development?  No
## 是否添加pinia环境
√ Add Pinia for state management?  No
## 是否添加单元测试
√ Add Vitest for Unit Testing?  No
## 是否添加端到端测试方案
√ Add an End-to-End Testing Solution? » No
## 是否添加ESLint语法检查
√ Add ESLint for code quality?  Yes
## 是否添加Prettiert代码格式化
√ Add Prettier for code formatting?  No
  1. 下载项目依赖:npm i
  2. 运行项目:npm run dev

项目基本目录结构

├── .vscode:VS Code中配置的插件,如果没有相关插件会出现安装提示。
├── node_modules:node安装的依赖包(脚手架的依赖)
├── public:公共目录,存放静态不会变的文件,会被webpack原封不动的打包到dist文件夹中。
 ├── favicon.ico: 页签图标
├── src:源码文件夹
 ├── assets: 存放静态资源,存放多个组件公用的静态资源,会被webpack当做一个模块打包到js文件中
  └── logo.png
 │── router: 定义应用程序的路由配置
 │── components: 存放公共组件,存放非路由组件和全局组件
  └── HelloWorld.vue
 │── views: 存放页面组件
 │── App.vue: 汇总所有组件
 │── main.js: 入口文件
├── .gitignore: git 版本管制忽略的配置。
├── env.d.ts: 使得ts文件能够识别其他类型文件,并且能够正确引入其他类型文件。
├── index.html: 项目入口文件(一个Vue脚手架项目只有一个html网页)
├── package-lock.json:缓存性文件,使得第二次启动速度更快。
├── package.json: 记录项目有哪些依赖、项目如何运行。
├── README.md: 工程的描述文件。
├── tsconfig.app.json:ts编译器的配置文件。
├── tsconfig.json:ts编译器的配置文件。
├── tsconfig.node.json:ts编译器的配置文件。
├── vue.config.js:文件用于自定义 Vue CLI 的默认配置。

index.html文件

<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="UTF-8">
    <link rel="icon" href="/favicon.ico">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vite App</title>
  </head>
  <body>
    <!-- 定义根容器 -->
    <div id="app"></div>
    <!-- 引入src/main.ts文件 -->
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

/src/main.ts

import './assets/main.css'

// 引入Vue3项目的根组件App组件
import { createApp } from 'vue'
import App from './App.vue'

// 将App组件挂载到index.html文件中的id=app标签上
createApp(App).mount('#app')

/src/App.vue

<template>
  <p>Hello,{{ name }}</p>
</template>

<!--setup属于VUE3的语法糖,可以使用组合式API-->
<script lang="ts" setup name='App'>
  const name = 'Vue3'
</script>

<style scoped>
</style>

CSN方式

通过script标签引入vue3

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

Vue开发工具

Vue开发工具Vue Devtools:在使用 Vue 时,我们推荐在你的浏览器上安装Vue Devtoo ls。它允许你在一个更友好的界面中审查和调试 Vue 应用。

安装方式:直接在浏览器添加插件 Vue.js devtools

Vue API风格

选项式API:可以用包含多个选项的对象来描述组件的逻辑,例如 datamethods 和 mounted。选项所定义的属性都会暴露在函数内部的 this 上,它会指向当前的组件实例。

组合式API:可以使用导入的 API 函数来描述组件逻辑。在单文件组件中,组合式 API 通常会与 <script setup>搭配使用。这个 setup attribute 是一个标识,告诉 Vue 需要在编译时进行一些处理,让我们可以更简洁地使用组合式 API。比如,<script setup> 中的导入和顶层变量/函数都能够在模板中直接使用。

注意:对于Vue3来说推荐使用组合式API,编写起来更加方便简洁。

Vue的API

数据绑定

单向数据绑定:使用v-bind,数据只能从data流向页面

  • v-bind:可以用在任何属性上面进行单向绑定。
  • v-model:只能用在表单类的value属性上面进行双向绑定。

v-model的三个修饰符:

  • lazy:表单失去焦点再收集数据
  • number::输入字符串转为有效的数字
  • trim:输入首位空格过滤
<template>
    <div id="root">
        单向绑定:<input type="text" v-bind:value="name"><br>
        双向绑定:<input type="text" v-model:value="name"><br>
        双向绑定:<input type="text" v-model="name"><br>
    </div>
</template>


<script>
import { ref } from 'vue';

// Vue3写法
export default {
    name: "root",
    setup() {
        const name = ref("");
        
        return {
            name
        }
    }
}

// Vue2写法

</script>

事件处理

事件绑定的两种方式

方式1:v-on:click="function"

方式2:@click="function"

事件对象event会通过实参$event占位符进行传递,Vue会自动将当前事件对象传递给对应实参。

点击事件

点击事件修饰符:

  • prevent: 阻止标签的默认事件
  • stop:阻止事件的冒泡
  • once:只触发一次事件
  • capture:使用事件的捕获模式
  • self:只有event.target是当前操作的元素时才会触发事件
  • passive:事件的默认行为立即执行,无需等待事件回调执行完毕
<template>
    <div id="root">
        <button @click="showInfo($event, "zhangsan")">点我</button>
        
        <!--修饰符的使用-->
        <button @click.passive="showInfo($event, "zhangsan")">点我</button>
    </div>
</template>    

<script>
import { ref } from 'vue';

// Vue3写法
export default{
    name: 'root',
    setup() {
        const showInfo = (event, name) => {
            console.log(name + "  " + event.target.innerText);
        }
        
        return {
            showInfo
        }
    }
}
</script>

滚轮滚动事件

  • @scroll:页面元素滚动条滚动触发事件
  • @wheel:鼠标滚轮滚动触发事件

键盘事件

  • @keyup:键盘松开事件
  • @keydown:键盘按下事件

Vue常用的按键别名:

别名含义
回车enter
删除和退格delete
退出esc
空格space
对其table
up
down
left
rigth

按键别名的使用

<template>
    <div id="root">
        <input type='text' @keyup.enter='showInfo'>
    </div>
</template>    

<script>
import { ref } from 'vue';

// Vue3写法
export default{
    name: 'root',
    setup() {
        const showInfo = (event) => {
            // 获取到input文本框的值
            console.log(event.target.value);
            // 获取按键的缩写和code
            console.log(event.key, event.keyCode);
        }
        
        return {
            showInfo
        }
    }
}
</script>

样式绑定

Vue可以直接通过v-bind:class与Vue中维护的变量进行动态绑定样式,而且可以另外定义class属性进行拼接成一个样式。

<!--变量绑定:会根据变量值动态修改class的值-->
<div class='basic' :class='classStyle'></div>

<!--数组绑定:数组中所有元素都会变成class的值-->
<div class='basic' :class='classArr'></div>

<!--对象绑定:对象中的key就是class值,value是一个bool类型决定是否添加进去-->
<div class='basic' :class='classObj'></div>

条件渲染

  • v-if : 如果为 true, 当前标签才会输出到页面
  • v-else-if:如果为 true, 当前标签才会输出到页面
  • v-else: 如果前面的v-if标签和v-else-if标签为false,当前标签才会输出到页面
  • v-show : 通过控制 display 样式来控制显示/隐藏
<!--如果为 true, 当前标签才会输出到页面-->
<div v-if="n === 1">wenxuan</div>	

<!--如果为 true, 当前标签才会输出到页面-->
<div v-else-if="n === 1">wenxuan</div>

<!--如果前面的v-if标签和v-else-if标签为false,当前标签才会输出到页面-->
<div v-else="n === 1">wenxuan</div>

<!--通过控制 display 样式来控制显示/隐藏-->
<div v-show="n === 1">wenxuan</div>

v-if和v-show的区别:

  • v-if如果表达式为false,元素直接会被移除,所以元素可能无法获取;v-show如果表达式为false,元素存在只是不展示。
  • v-if适用于切换频率比较低的场景,v-show适用于切换频率比较高的场景。

列表渲染

v-for:用于遍历数组或者对象

:key最好使用一条数据的唯一标识,这样子效率更高而且不会出现问题

<div id="root">
    <ul>
        <li v-for="(p, index) in persons" :ke y="p.id">
            {{index}}----{{p.name}}-{{p.age}}
        </li>
    </ul>
    <ul>
        <li v-for="(value, key) in car" :key="key">
            {{value}}
        </li>
    </ul>
</div>

文本处理

  • v-text : 更新元素的 textContent,内容当作文本内容。
  • v-html : 更新元素的 innerHTML,内容会被解析为标签。
<template>
    <div id="root">
         <div v-text="text"></div>
        <div v-html="html"></div> 
    </div>
</template>    

<script>d
import { ref } from 'vue';

export default{
    name: 'root',
    setup() {
        const text = ref('text');
        const html = ref('html');
        
        return {
            text,
            html
        }
    }
}
</script>

ref和reactive函数

通过ref函数和reactive函数来定义响应式数据:

  • ref可以用来定义基本类型、对象类型的响应式数据,reactive只能用来定义对象类型的响应式数据。
  • ref定义的变量必须使用.value来使用,reactive定义的对象变量无需使用.value来使用。

对于基本类型的变量必须使用ref,对于对象类型的变量一般使用reactive。

// 通过ref定义基本类型的响应式变量
const age = ref(18)
const name = ref('wenxuan')
// 通过reactive定义应用类型的响应式变量
const student = reactive({
  name: 'wenxuan',
  age: 18
})

对于对象类型的响应式变量需要通过Object.assign()函数来修改:Object.assign(变量名, 新值)

toRef和toRefs函数

通过toRef和toRefs函数可以将对象类型的响应式数据解构出来变成ref定义的响应式变量。

const person = reactive({
  name: '张三',
  age: 18
})
// 将person中的name结构出来
const name = toRef(person, 'name')
// 将person中所有属性结构出来
const {name, age} = toRef(person)

计算属性computed

当变量需要通过复杂表达式计算出来时,可以通过computed函数定义计算属性。computed在模板第一次解析加载时会调用一次,后面当computed函数中设计到的响应式数据发生改变时也会调用。

const firstName = ref('wen')
const lastName = ref('xuan')

// 定义计算属性-简写形式(但计算属性只需要读取时)
const fullName = computed(() => {
  return firstName.value + '-' + lastName.value
})

// 定义计算属性-完整形式(当计算属性需要读写时)
const fullName = computed({
  get() {
    return firstName.value + '-' + lastName.value
  },
  set(val) {
    const [str1, str2] = val.split('-')
    firstName.value = str1
    lastName.value = str2
  }
})

监视属性watch

当变量变化时需要处理更加复杂的操作时,可以通过watch函数定义监听操作。

情况1:监听基本类型变量

//引入组件的方式:import 组件名称 from '组件路径'
const age = ref(18)

// 定义监视操作
const stopWatchAge = watch(age, (newVal, oldVal) => {
  console.log('age变量发生变化', newVal, oldVal);
  if(newVal > 100 || newVal <= 0) {
    // 停止监视age变量
    stopWatchAge()
  }
})

情况2:监听对象类型变量的所有属性

  • 对于ref定义的对象类型的响应类型数据监听对象属性需要通过deep开启深度监视。
  • 对于reactive定义的对象类型的响应类型数据一定会监听对象类型数据。
const person = ref({
  name: 'wenxuan',
  age: 18
})

// 定义监视操作
watch(person, (newVal, oldVal) => {
  console.log('person变量发生变化', newVal, oldVal);
}, {deep: true})  // deep表示是否开启深度监视,对象类型变量的属性变化是否监视

情况3:监听对象类型变量的某个属性

  • 监视基本类型属性或者对象类型属性,写成函数形式。
  • 监视对象类型属性,如果需要监视属性的属性,需要通过deep开启深度监视。
const person = reactive({
  name: 'wenxuan',
  age: 18
})

// 定义监视person变量的name属性,第一个参数为函数,函数返回需要监视的变量属性
watch(() => person.name, (newVal, oldVal) => {
  console.log('person.name变量发生变化', newVal, oldVal);
})

情况4:监听多个变量

  • watch实现监听多个属性需要指出监视的变量。
  • watchEffect实现监听多个变量无需指出监听的变量,会监听函数中使用到的变量。
const name = ref('wenxuan')
const age = ref(18)

// watch实现监听多个变量
watch([() => name, age], (newVal, oldVal) => {
  console.log('name或者age变量发生变化', newVal, oldVal);
})

// watchEffect实现监听多个变量
watchEffect(() => {
  console.log('name或者age变量发生变化', name.value, age.value)
})

Vue的其他API

Vue一些其他常用的API:

  • shallowRef:创建一个响应式数据,但只对顶层属性进行响应式处理。
  • shallowReactive:创建一个响应式对象,但只对最顶层的属性变成响应式处理。
  • readonly:创建一个会跟随另一个响应式数据变化而变化的变量,但是变量本身不能修改。
  • shallowReadonly:创建一个顶层属性制度,对象内部的嵌套属性仍然可变。
  • toRaw:返回一个响应式变量对应的原始数据。
  • markRaw:标记一个对象永远不会变成响应式变量。
  • customRef:创建一个自定义的ref,并对其依赖项跟踪和更新和触发进行逻辑控制。

Vue组件

组件的规范

编写组件存在一些规范:

  • App组件是Vue工程的顶级组件,其他所有组件都是在App下面的。
  • Components 将数据传递给组件进行渲染。Views 控制路由、获取数据和协调组件之间的交互。

组件的定义和使用

定义组件:

<template>
  <div>Hello,{{ name }}</div>
</template>


<script lang='ts' setup>
import { ref } from 'vue'

const name = ref('wenxuan')
</script>

在其他组件中使用组件

<template>
  <h1>其他组件</h1>
  <ShowInfo />
</template>

<script lang='ts' setup>
import ShowInfo from './ShowInfo.vue'
</script>

组件标签的获取

标签的ref属性:通过ref属性给标签定义一个唯一标识,可以通过ref函数获取DOM元素。

获取普通DOM标签

<template>
    <p1 ref="ref1">ref1</p1>
    <p1 ref="ref2">ref2</p1>
</template>

<script lang='ts' setup>
    // 通过变量名称和ref属性中的标识一一对应
    const ref1 = ref()
    const ref2 = ref()
</script>

获取子组件标签

<!--父组件-->
<template>
    <Son ref="son"/>
    <button @click="showS">显示</button>
</template>

<script lang="ts" setup name="Father">
import { ref } from 'vue';
import Person from './Son.vue'

const son = ref()
function showS() {
  console.log(son.value.name);
} 
</script>

获取所有的子组件标签:$refs会将所有的子组件封装到一个数组中,注意$refs只会获取通过ref属性打标签的子组件。

<template>
  <div class="father">
    <button @click="getAllChild($refs)">获取所有的子组件</button>
    <Child ref="child"/>
  </div>
</template>

<script lang='ts' setup>
function getAllChild(childs:any) {
  for(const key in childs) {
    console.log(childs[key]);
  }
}
</script>

组件之间的通信

父子组件的通信

父子组件的通信:

  • defineProps:可以实现父传子,也可以实现子传父。
  • defineEmit:只能实现子传父。
  • v-model:实现父子组件的双向绑定。

defineProps:父子之间的通信

  • 父 --> 子:直接通过defineProps接收父组件传递过来的参数。
  • 子 --> 父:父组件先传递一个函数,子组件接收到函数并且调用函数传递参数过去。
<!--编写父组件-->
<template>
  <div class="father">
    <Child :name="name" :getCount="getCount"/>
    <p>子组件的统计数据:{{ count }}</p>
  </div>
</template>

<script lang='ts' setup>
import { ref } from 'vue';
import Child from './Child.vue';

// 数据
const name = ref('wenxuan')
const count = ref()

// 函数
function getCount(value:number) {
  count.value = value
}
</script>


<!--编写子组件-->
<script lang='ts' setup>
// 1. 参数:数组中可以接收多个参数,字符串为接收的参数名
defineProps(['name', 'getCount'])

// 2. 数并限定类型:通过泛型指定参数名和参数类型
defineProps<{name:string}>()

// 3. 数并限定类型和默认值:第一个参数指定参数名和参数类型,第二个参数指定默认值
withDefaults(defineProps<{name:string}>(),  {name: () => 'wenxuan'})

// 4. 将参数使用变量保存起来:
const params = defineProps(['name'])
</script>

defineEmit:子组件 --> 父组件

在父组件中可以通过$event获取调用emit时提供的数据,可以是任意类型。

<!--父组件:定义自定义事件-->
<template>
  <div class="father">
    <Child @send-count="sendCount"/>
    <p>子组件的统计数据:{{ count }}</p>
  </div>
</template>

<script lang='ts' setup>
import { ref } from 'vue';
import Child from './Child.vue';

const count = ref()
// 函数
function sendCount(value:number) {
  count.value = value
}
</script>

<!--子组件:接收父组件的自定义事件并调用-->
<script lang='ts' setup>
// 获取父组件的自定义事件
const emit = defineEmits(['send-count'])

// 调用父组件的事件,将参数传递过去
emit('send-count', 10)
</script>

祖孙组件的通信

祖孙组件通信的两种方式:

  • 方式一:祖孙组件的通信通过$attrs实现,子组件的$attrs中包含着父组件传递过过来的所有参数,可以将这些所有参数传递给孙组件,孙组件可以通过defineProps接收。
  • 方式二:祖先组件通过provide将需要传递的变量或者函数传递下去,孙子组件通过inject获取祖先组件传递下来的变量或者函数。

attrs方式需要操作经历过的组件,需要将父组件传递下来的参数一层一层往下传递。provide方式需要接收参数的组件通过inject获取即可,中间的组件不需要进行任何操作。通常使用provide方式,只有当传递参数很多时才会使用$attrs方式

$attrs方式:适用于需要传递的层数不高,并且需要传递的参数很多时使用。

<!--父组件:将参数传递给子组件-->
<Child :a="a" :b="b" :c="c" :d="d"/>


<!--子组件接收部分参数同时将父组件的参数传递过孙组件-->
<template>
  <div class="child">
    <p>a的值:{{ a }}</p>
    <GrandChild v-bind="$attrs"/>
  </div>
</template>

<script lang='ts' setup>
import GrandChild from './GrandChild.vue';
defineProps(['a'])
</script>

<!--孙组件:接收子组件传递过来的参数-->
<script lang='ts' setup>
defineProps(['a', 'b', 'c', 'd'])
</script>

provide:传递的参数较少且不限制传递层数

<!--父组件:传递参数和函数-->
import { inject, provide, ref } from 'vue';
import Child from './Child.vue';

const count = ref(0)
function updateCount(val:number) {
  count.value += val
}
provide('countContext', {count, updateCount})

<!--子组件:接收和使用参数和函数-->
const {count, updateCount} = inject('countContext')

父子组件变量的修改

修改父子组件的变量需要通过想获取组件的标签,同时将需要修改的变量暴露出去。

  • 父组件修改子组件变量:子组件将需要修改的变量暴露出去,父组件对子组件标签ref打标识,父组件通过ref获取标签就能对子组件的值修改。
  • 子组件修改父组件变量:父组件将需要修改的变量暴露出去,子组件通过$parent获取父组件的标签即可对变量修改。

父子组件变量的修改逻辑:都是一个组件将变量暴露出去,然后另一个组件获取需要修改的组件并修改。

父组件修改子组件的变量

<!--父组件通过ref对子组件的变量修改-->
<template>
  <div class="father">
    <button @click="addChildCount">修改子组件的值</button>
    <Child ref="child"/>
  </div>
</template>

<script lang='ts' setup>
import { ref } from 'vue';
import Child from './Child.vue';

// 数据
const child = ref()

// 方法
function addChildCount() {
  child.value.count++
}
</script>

<!--子组件通过defineExpose将需要修改的变量暴露出去-->
import { ref } from 'vue';
const count = ref(0)
defineExpose({count})

子组件修改父组件的变量

<!--父组件将需要修改的变量暴露出去-->
const count = ref(0)
defineExpose({count})

<!--子组件通过$parent获取父组件标签-->
<template>
  <div class="child">
    <button @click="updateFatherCount($parent)">修改父组件的变量</button>
  </div>
</template>

<script lang='ts' setup>
function updateFatherCount(parent:any) {
  parent.count++
}
</script>

任意组件的通信

任意组件的通信:

  • pinia:Pinia 是 Vue 的状态管理库,主要用于管理 Vue 应用中的全局状态,确保不同组件之间可以共享和管理状态。
  • mitt:Mitt 是一个简单而强大的事件总线库,主要用于消息的订阅和发布。

Vue3整合pinia和mitt看下面的小结。

组件的生命周期

生命周期图示

Vue3生命周期图示.png

生命周期函数

  • setup():开始创建组件之前执行;
  • onBeforeMount():组件挂载到节点上之前执行的函数;
  • onMounted():组件挂载完成后执行的函数;
  • onBeforeUpdate():组件更新之前执行的函数;
  • onUpdated():组件更新完成之后执行的函数;
  • onBeforeUnmount():组件卸载之前执行的函数;
  • onUnmounted():组件卸载完成后执行的函数;

父子组件的挂载顺序:子组件挂载完毕之后,再开始挂载父组件,所以App组件是最后挂载的。

组件生命周期函数的使用

<script lang='ts' setup>
// 组件挂载之前执行的函数
import { onBeforeMount } from "vue";

onBeforeMount(() => {
    console.log("组件挂载到页面之前执行"); 
})
</script>

组件API封装复用

自定义hook:可以将函数中使用的API进行封装,然后将API暴露出去,其他Components可以引入复用API。

/src/hooks/useSum.ts 编写复用的数据和函数,通过函数暴露出去

import { ref } from "vue";

export default function() {
  const sum = ref(0)

  const increment = () => {
    sum.value++;
  }

  const decrement = () => {
    sum.value--
  }

  // 向外部暴露出去
  return {sum, increment, decrement}
}

在组件中使用API

import useSum from '@/hooks/useSum';

const {sum, increment, decrement} = useSum();

组件的插槽

组件插槽的作用:子组件大部分片段相同只有部分片段不同,可以让子组件在根据不同的条件渲染部分不同的片段。

默认插槽

默认插槽:在父组件的子标签内容填写需要插入的标签和数据,在子组件中通过<slot>标签标注需要插入的位置。

<!--父组件:标签中包含需要插入的内容-->
<template>
  <div class="father">
  <Child title="游戏">
    <ul>
      <li v-for="game in games" :key="game.id">{{ game.name }}</li>
    </ul>
  </Child>
  <Child title="美食">
    <img :src="imgUrl" alt="">
  </Child>
  <Child title="影视">
    <video :src="videoUrl"></video>
  </Child>
  </div>
</template>

<!--子组件:标注需要插入的位置-->
<slot></slot>

具名插槽

具名插槽:当子组件中存在多个内容需要插入到很多位置时,可以给内容命名,同时子组件中插槽也命名。

<!--父组件:通过template包括片段,并且给片段命名-->
<template>
  <div class="father">
    <Child title="游戏">
      <template #s1><p>内容1</p></template>
      <template #s2><p>内容2</p></template>
    </Child>
  </div>
</template>

<!--子组件:布置插槽,同时给插槽命名-->
<template>
  <div class="child">
    <slot name="s2"></slot>
    <slot name="s1"></slot>
  </div>
</template>

作用域插槽

作用域插槽:数据在子组件上,但是根据数据生成的结构由父组件决定。

<!--父组件:接收子组件的参数,并展示到结构中-->
<template>
  <div class="father">
    <Child>
      <template v-slot="params">
        <ul>
          <li v-for="game in params.games" :key="game.id">{{ game.name }}</li>
        </ul>
      </template>
    </Child>
  </div>
</template>

<!--子组件:准备并传递参数-->
<template>
  <div class="child">
    <slot :games="games"></slot>
  </div>
</template>

<script setup lang="ts" name="Child">
import { reactive } from 'vue';

const games = reactive([
  {id:'001', name: '英雄联盟'},
  {id:'002', name: '王者荣耀'},
  {id:'003', name: '斗罗大陆'},
  {id:'004', name: '金铲铲'},
])
</script>

Vue实战技术

Vue3整合mitt

mitt:实现任意组件间的通信,和消息的发布订阅功能类似。

mitt准备环境

安装mitt:npm i mitt

配置emitter:新建/src/utils/emitter.ts

// 引入mitt
import mitt from 'mitt'

// 创建emitter
const emitter = mitt()

// 暴露emitter
export default emitter

mitt的使用

接收数据的组件中:绑定事件

import emitter from '@/utils/emitter';
import { onUnmounted, ref } from 'vue';
import Child from './Child.vue';

const data = ref('')
emitter.on('send-data', (val) => {
  console.log("接收到的:", data);
  // 将接收到的数据放到响应式变量
  data.value = val
})

// 组件销毁时结伴事件
onUnmounted(() => {
  emitter.off('send-data')
})

发送数据的组件中,触发事件

import emitter from "@/utils/emitter";
emitter.emit('send-data', 'wenxuan')

Vue3项目实战

项目开发配置

配置自动打开浏览器

在package.json文件中对scripts/dev配置项改成

vite --open

配置src别名

src别名配置:使得项目中填写路径时@可以标识./src/,简化了路径的填写。

// vite.config.ts
import {defineConfig} from 'vite'
import path from 'path'
export default defineConfig({
    resolve: {
        alias: {
            "@": path.resolve("./src") // 相对路径别名配置,使用 @ 代替 src
        }
    }
})
// tsconfig.json 
{
  "compilerOptions": {
    "baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录
    "paths": { //路径映射,相对于baseUrl
      "@/*": ["src/*"] 
    }
  }
}

配置环境变量

  1. 创建三个配置文件
  • .env.development:开发环境的配置文件
  • .env.production:生产环境的配置文件
  • .env.test:测试环境的配置文件
  1. 编写三个配置文件
# .env.development
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'development'  # 标识生产环境
VITE_APP_TITLE = 'xxx'    # APP应用标题
VITE_APP_BASE_API = '/dev-api'  # 开发环境的URL
VITE_SERVE = 'http://localhost:8080' # 开发环境后端接口的路径
# .env.production
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'production'  # 标识生产环境
VITE_APP_TITLE = 'xxx'    # APP应用标题
VITE_APP_BASE_API = '/prop-api'  # 生产环境的URL
VITE_SERVE = 'http://localhost:8080' # 生产环境后端接口的路径
# .env.test
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'test'  # 标识测试环境
VITE_APP_TITLE = 'xxx'    # APP应用标题
VITE_APP_BASE_API = '/test-api'  # 测试环境的URL
VITE_SERVE = 'http://localhost:8080' # 测试环境后端接口的路径

3. 添加四条指令

  • dev:开发环境的启动命令
  • build:test:打包测试环境的版本命令
  • build:pro:打包生产环境的版本命令
  • preview:生成预览的生产环境代码
// package.json
 "scripts": {
    "dev": "vite --open",
    "build:test": "vue-tsc && vite build --mode test",
    "build:pro": "vue-tsc && vite build --mode production",
    "preview": "vite preview"
},

配置全局注册组件

通过自定义插件实现配置全局注册组件无需在组件中引入可以直接使用,当一个组件被很多组件使用到时可以将该组件配置到全局注册中。比如SVG组件。

  1. 编写注册全局注册的插件
// /src/components/index.ts
//引入项目中全部的全局组件
import SvgIcon from './SvgIcon/index.vue'
//全局对象
const allGlobalComponent:any = { SvgIcon }
//对外暴露插件对象
export default {
  //必须叫做install方法
  install(app: any) {
    // 注册项目全部的全局组件
    Object.keys(allGlobalComponent).forEach((key) => {
      // 将组件注册为全局组件
      app.component(key, allGlobalComponent[key])
    })
  },
}
  1. 使用注册全局组件的插件
// main.ts

const app = createApp(App)
import GlobalComponent from './components'
app.use(GlobalComponent)

配置项目相关样式

注意:如果使用了scss则需要参考Vue3整合Sass先下载依赖

  1. 配置全局样式
<!--/src/styles/index.scss-->
<!--编写全局样式-->

<!--main.ts:引入全局样式-->
import './styles/index.scss'
  1. 配置清除默认样式

注意:清除默认样式的代码可以去npm官网下载:www.npmjs.com ,搜索reset.scss

<!--/src/styles/reset.scss-->
<!--编写全局样式-->

<!--/src/styles/index.scss:引入清除默认样式-->
@import './reset.scss'
  1. 配置scss全局变量

scss全局变量的配置:全局变量保存在/src/styles/variable.scss

注意:需要先配置src目录的别名

// vite.config.ts
export default defineConfig({
  // 配置scss全局变量的文件路径
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@use "@/styles/variable.scss";`
      }
    }
  }
})

全局变量的使用

// /src/styles/variable.scss
$color: red;

注意:使用变量时需要添加variable前缀,variable为全局变量的文件名

<!--App.vue-->
<style scoped lang="scss">
div {
  color: variable.$color;
}
</style>

配置变量分离

项目变量分离:比如项目的标题,LOGO的路径等等,可以将这些变量信息保存到一个ts文件中,在组件中引入使用,需要修改时无需找组件,只需要找到ts1文件即可。

  1. 定义变量
// /src/setting.ts
export default {
    title: '硅谷甄选运营平台', // 项目标题
    logo: '/public/logo.png', // 项目logo路径
    loginHidden: false, // 项目logo是否隐藏
}
  1. 组件中使用变量
<!--/src/layout/logo/index.vue-->
<script lang='ts' setup name='Logo'>
import setting from '@/setting.ts'
</script>

<template>
<div class='logo'>
    <img :src="setting.logo" alt="">
    <p>{{ setting.title }}</p>
</div>
</template>

配置项目组件规范

项目路由配置规范:

  • 一级路由组件放置在/src/views目录下面
  • 一级主路由的组件放置在/src/layout目录下面
  • 二级路由放置到对应的一级路由目录下面
  • 非路由组件放置到/src/components目录下面

配置多环境解决跨域问题

参考配置环境变量先配置环境

解决跨域问题

// /vite.config.ts
import { defineConfig, loadEnv } from 'vite'

// https://vite.dev/config/
export default defineConfig(({ command, mode }) => {
  // 获取配置文件定义的变量,process.cwd()表示配置文件在根目录下
  const env = loadEnv(mode, process.cwd())
  return {
    // 代理跨域
    server: {
      proxy: {
        // 动态获取后端接口的前缀
        [env.VITE_APP_BASE_API]: {
          // 动态获取后端接口的地址
          target: env.VITE_SERVE,
          changeOrigin: true,
          rewrite: path => path.replace(/^\/api/, '')
        }
      }
    }
  }
})

Vue3整合第三方库

Vue3整合axios

Axios 是一个基于 promise 网络请求库,作用于node.js和浏览器中。在服务端它使用原生 node.js http 模块, 而在客户端 (浏览端) 则使用 XMLHttpRequests。

  1. axios依赖下载

axios文档:github.com/axios/axios

方式1:npm install axios
方式2:bower install axios
方式3:yarn add axios
  1. axios前端解决跨域问题

如果后端已经解决跨域问题,则前端无需再次解决,也就是这一步可以省略。

找到vite.config.ts配置文件,在defineConfig对象中添加属性:

  • /api:以api开头的请求会进行代理
  • target:代理的目标URL
  • changeOrigin:是否修改请求头中的 Origin 字段
  • rewrite:改写URI
// vite.config.ts
export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: path => path.replace(/^\/api/, '')
      }
    }
  }
})
  1. axios网络请求封装

注意:

  • baseURL中引入了环境变量,所以需要先配置环境变量。
  • 若整合了Element-Plus可以将下面两处的注释打开。
// /src/utils/request.ts
import axios from 'axios'
// 若整合Element-Plus
// import { ElMessage } from "element-plus";

const getErrorMessage = (code:number) => {
    switch(code) {
        case 400: return "语义有误"
        case 401: return "服务器认证失败"
        case 403: return "服务器拒绝访问"
        case 404: return "URL地址有误"
        case 500: return "服务器出现异常"
        case 502: return "服务器无响应"
        default: return "未知异常"
    }
}

// 创建一个axios对象
const requests = axios.create({
    // 配置基础路径
    baseURL: import.meta.env.VITE_APP_BASE_API,
    // 配置请求超时时间
    timeout: 5000,
})

// 请求拦截器
requests.interceptors.request.use((config) => {
    // config:配置对象,对象中有一个很重要的属性headers
    return config
})

// 响应拦截器
requests.interceptors.response.use((res) => {
    // 响应成功的拦截回调函数
    // 返回响应值中的data值
    return res.data;
}, (error) => {
    // 响应失败的拦截回调函数
    const {response} = error;
    const msg = getErrorMessage(response.status)
    console.log("code = " + response.status);
    console.log("msg = " + msg);
    // ElMessage({
    //     type: 'error',
    //     message: response.status + "-" + msg
    // })
    // 返回异常,可以在发送请求的代码中进行捕获异常
    return Promise.reject(error);
})

// 对外暴露axios实例requests
export default requests

请求拦截器的config中包含请求的一系列信息:URL、请求方法、请求头、请求数据等内容,通常可以在这里进行如下处理:

  • 添加请求头:为请求动态添加认证信息、令牌(Token)等。
  • 处理请求参数:发送请求之前,对请求的参数进行格式化或修改。
  • 日志记录:记录请求的相关信息,如请求的 URL、请求时间等,方便调试。
  • 错误处理:处理一些全局的错误逻辑,比如验证 Token 是否过期,如果过期就跳转到登录页面。
  1. axios的API接口统一管理

将API接口统一管理放置在/src/api目录下面,并且将不同类型的接口按照文件夹分开,每个文件夹下面存放两个ts文件

  • index.ts:存放API的方法和URL
  • type.ts:存放API的请求参数类型和响应数据类型
// /src/api/user/type.ts
// 登录请求参数
export interface loginForm {
    username:string,
    password:string
}

interface dataType {
    token: string
}

// 登录接口返回信息
export interface loginResponseData {
    code:number,
    data:dataType
}
// /src/api/user/index.ts
// 用户相关的接口
import requests from "../../utils/requests";
import type { 
    loginForm
} from "./type";

// 用户接口相关的URL
enum API {
    LOGIN_URL = '/user/login',
    USERINFO_URL = '/user/info'
}

// 登录接口的请求方法
export const reqLogin = (data:loginForm) => requests.post(API.LOGIN_URL, data)

注意:axios发送请求时的键值对参数通过params指定,请求时的请求体参数通过data属性指定。

axios的常用配置

{
    // url:用于请求的服务器的URL
    url: "/test",
    
    // method:创建请求时使用的方法
    method: "get",
    
    // baseUrl:设置自动加到url前面的路径,如果url是一个绝对的请求路径则不会加到url前面
    baseUrl: "http://localhost:8080/api",
    
    // headers:设置请求头数据
    headers: {
        'X-Requested-With': 'XMLHttpRequest'
    }
    
    // params:设置url上键值对形式的参数
    params: {
        "name": "zhangsan"
    },
    
    // data:设置添加到请求体上的参数
    data: {
        "name": "zhangsan"
    },
    
    // timeout:设置请求超时时间,单位为毫秒
    timeout: 1000,
    
    // responseType:设置请求响应参数格式,默认是json
    responseType: "json",
   
}

axios的响应结构


{
    // data:由服务器提供的响应数据
    data: {},

    // status:服务器响应的 HTTP 状态码
    status: 200,

    // statusText:来自服务器响应的 HTTP 状态信息
    statusText: 'OK',

    // headers:服务器响应的头
    headers: {},

    // config:为请求提供的配置信息
    config: {},
    
    // request:生成响应的请求
    request: {}
}

Vue3整合router

  1. router依赖的下载

进入vue项目中,执行下面命令即可下载router依赖

npm i vue-router
  1. 编写路由常量
// /src/router/routers.ts
// 对外暴露路由
export const constantRoute = [
    {
        path: '/login',
        component: () => import('@/views/login/index.vue'),
        name: 'login'
    },
]
  1. router的配置项

路由工作模式:

  • history模式:createWebHistory(),URL更加美观,URL中不带#,但是需要服务端配合处理路径问题,否则刷新会出现404错误。
  • hash模式:createWebHashHistory(),URL中会携带#,且在SEO优化仿麦呢相对更差。但是不需要服务端处理路径。

routes中的name属性,给路由命名,使得路由跳转时可以通过name使用。

// /src/router/index.ts
import { createRouter, createWebHashHistory } from "vue-router";
import { constantRoute } from "./routers";

const router = createRouter({
    history: createWebHashHistory(),
    routes: constantRoute
})

export default router;
  1. router全局注入
// /src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

const app = createApp(App)
app.use(router)
app.mount('#app');
  1. router的跳转

声明式跳转:在template中进行路由跳转的配置

<!-- 跳转到指定路由,类似于a标签 -->
<router-link :to="{path: '/about'}">About</router-link>

<!-- 组件展示的指定位置:会根据路由和配置项中去匹配对应的组件,所以一个组件中最多只能有一个router-view标签 -->
<router-view />

编程式跳转:在script中通过ts代码实现路由跳转

路由相关的两个对象:

  • useRouter:路由实例,通过该实例可以进行路由操作,例如导航、替换、前进或后退。
  • useRoute:当前激活的路由对象。它是只读的,可以访问与当前路由相关的信息(如路由参数、查询参数、路径等)。
import { useRouter } from "vue-router";

const router = useRouter();
router.push({
  path: '/index'
});
  1. router的嵌套

News组件中需要展示Detail组件内容的地方通过<router-view />标签展示。

// /src/router/routers.ts
{
  path: '/news',
  component: News,
  name: 'News',
  children: [
    {
      path: 'detail',
      component: Detail,
      name: 'detail'
    }
  ]
}
  1. router的传参

6.1 路由传query参数

query参数表示键值对:/home?key=value

<!--News.vue:传递参数-->
<template>
  <RouterLink
    :to="{
      name: 'detail',
      query: {
        id: id
      }
    }"
  >
    {{news.title}}
  </RouterLink>
</template>

<!--Detail.vue:接收参数-->
<script setup lang="ts" name="Detail">
import { toRefs } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute()
// 接收成响应式数据
const { query } = toRefs(route);

// 接收成非响应式数据
const { query } = route
</script>

6.2 路由传params参数

在路由配置项中占位,也就是指定参数的key

// 在src/router/routers.ts:配置路由params参数占位
{
    path:'/detail/:id',	
    name: 'detail',
    component: News,
}

News.vue:传递参数

<!--声明式路由,传递params参数-->
<router-link 
    :to="{
        // 注意params传参只能使用name,使用path会失效
        name:'detail',
        params:{
            id: '1'
        }
    }"
>跳转</router-link>

<!-- 编程式导航 -->
<script setup lang='ts' name='News'>
import { useRoute, useRouter } from "vue-router";

const router = useRouter();
router.push({
  // 注意params传参只能使用name,使用path会失效
  name: '/detail',
  params: {
      id: "1"
  }
})
</script>

Detail.vue:接收参数

import { toRefs } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute()
// 接收成响应式数据
const { params } = toRefs(route);

// 接收成非响应式数据
const { params } = route

6.3 路由的props属性

路由的props属性可以简化接收参数的方式,在路由项的配置中指定路由接收时可以通过defineprops接收。

// /src/route/routers.ts:配置路由项
// 写法一:开启props,把收到了每一组params参数,作为props传给Detail组件
{
  name:'detail',
  path:'detail/:id/:title/:content',
  component:Detail,
  props:true
}

// 写法二:将路由的query参数作为props传给Detail组件
{
  name:'detail',
  path:'detail,
  component:Detail,
  props(route){
    return route.query
  }
}

Detail组件接收时可以通过defineProps接收

// Detail.vue:接收参数
defineProps(['id', 'title', 'content'])
  1. router的重定向

路由重定向:将指定路由重定向到另一个路由,在router的配置项中配置

// 将 / 路由重定向到 /about
{
    path:'/',
    redirect:'/about'
}

Vue3整合pinia

pinia:用于实现组件共享数据,将数据保存到pinia的state中,每个组件都能获取到里面的值。

  1. pinia准备环境

执行下面命令,下载pinia依赖

npm i pinia
  1. pinia全局注入
// /src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'

const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
app.mount('#app');
  1. pinia的使用
// //src/store/count.ts
import { defineStore } from "pinia";
import { ref } from "vue";

export const useCountStore = defineStore('count', () => {
  const sum = ref(0)

  function increment(value:number) {
    sum.value += value
  }

  function decrement(value:number) {
    sum.value -= value
  }

  return {sum, increment, decrement}
})
  1. pinia在组件中使用
// App.vue
import { useCountStore } from "@/store/count";
import { storeToRefs } from "pinia";
import { ref } from "vue";
// 数据
const countStore = useCountStore()

// 将store中的变量变成响应式
const { sum } = storeToRefs(countStore)

// 方法
function add() {
    countStore.increment(1)
}
function minus() {
    countStore.increment(1)
}
  1. pinia的监听

在组件中通过store.$subscribe()实现监听,当store中的变量发生变化时,会执行subscribe()传入的回调函数。

监听某一个具体的变量

const countStore = useCountStore()
watch(() => countStore.sum, () => {
    console.log('sum变量发生变化')
})

监听任意变量变化

const countStore = useCountStore()
countStore.$subscribe((mutate, state) => {
    console.log("countStore中发生变化的变量信息", mutate);
    console.log("countStore中变量变化后的状态", state);
})

Vue3整合Element-Plus

  1. 安装Element-Plus
npm i element-plus
pnpm install element-plus @element-plus/icons-vue
  1. Vue中引入Element-Plus

完整引入:会导致文件比较大,但是代码编写方便

// 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')

按需自动引入:文件不会那么大,但是需要什么就引入什么代码编写麻烦

需要先安装两个库:

  • npm i unplugin-auto-import
  • npm i unplugin-vue-components
// 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'

export default defineConfig({
  // ...
  plugins: [
    // ...
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ],
})
  1. 实现国际化

当Element-Plus使用全局引入时

// main.ts
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
//@ts-ignore忽略当前文件ts类型的检测否则有红色提示(打包会失败)
import zhCn from 'element-plus/es/locale/lang/zh-cn'

app.use(ElementPlus, {
    locale: zhCn
})

当Element-Plus使用局部自动引入时

<!--App.vue-->
<script setup lang="ts">
import zhCn from 'element-plus/es/locale/lang/zh-cn'
</script>

<template>
<el-config-provider :locale="zhCn">
<div class="app">

</div>
</el-config-provider>
</template>

Vue3整合Element-Plus-Icon

  1. 下载Element-plus-Icon
npm install @element-plus/icons-vue

Vue3整合SVG图标

  1. 下载SVG依赖
npm install vite-plugin-svg-icons -D
  1. 配置SVG插件
// vite.config.ts

import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import path from 'path'
export default () => {
  return {
    plugins: [
      createSvgIconsPlugin({
        // 指定svg图标的存放路径
        iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
        // 指定生成图表元素的id格式
        symbolId: 'icon-[dir]-[name]',
      }),
    ],
  }
}
  1. 编写SVG配置代码
// main.ts
import 'virtual:svg-icons-register'
  1. 添加需要的SVG文件

SVG文件获取:www.iconfont.cn/

将SVG文件放入 /src/assets/icons

5. 封装SVG组件

<!--/src/assets/icons/index.vue-->
<template>
<div>
  <svg :style="{ width: width, height: height }">
    <use :xlink:href="prefix + name" :fill="color"></use>
  </svg>
</div>
</template>
  
<script setup lang="ts">
  defineProps({
    //xlink:href属性值的前缀
    prefix: {
      type: String,
      default: '#icon-'
    },
    //svg矢量图的名字
    name: String,
    //svg图标的颜色
    color: {
      type: String,
      default: ""
    },
    //svg宽度
    width: {
      type: String,
      default: '16px'
    },
    //svg高度
    height: {
      type: String,
      default: '16px'
    }
  })
</script>
  1. 使用SVG组件

注意:使用SVG组件的前提是需要在/src/assets/icons目录下面放置好了对应的svg文件,同时SvgIcon标签的name属性填写文件名。

<script setup lang="ts">
import SvgIcon from './components/svgIcon/index.vue'
</script>

<template>
<div class="app">
  <SvgIcon name="home" color="red"></SvgIcon>
</div>
</template>

Vue3整合Sass

Sass:一种扩展 CSS 的预处理器,它添加了一些功能和特性,使得 CSS 的编写更加高效和模块化。它最终会被编译为标准的 CSS 文件,从而可以在浏览器中运行。

  1. 下载Sass
npm install sass sass-loader --save-dev
  1. 设置sass
<style scoped lang="scss">

</style>

Vue3整合mock

mock:通过模拟后端 API 请求,返回假数据,来加速前端开发和调试的一个常用方案。

  1. 下载mock相关依赖
pnpm install -D vite-plugin-mock mockjs
  1. 配置mock插件

注意:vite.config.ts默认可能是以对象的形式编写的,需要改成函数式写法。

  • command === 'serve':仅在开发环境启用 Mock
  • mockPath: 'mock':Mock 数据文件的存放目录
  • logger: true:在控制台打印请求日志
import { viteMockServe } from 'vite-plugin-mock'
export default defineConfig(({ command })=> {
  return {
    plugins: [
      viteMockServe({
        localEnabled: command === 'serve',
        mockPath: 'mock',
        logger: true,
      }),
    ],
  }
})
  1. 编写假的接口
// /mock/user.ts
//用户信息数据
function createUserList() {
    return [
        {
            userId: 1,
            username: 'wenxuan',
            password: 'wenxuan',
            desc: '平台管理员',
            token: 'wenxuan Token',
        },
    ]
}

export default [
    // 用户登录接口
    {
        url: '/api/user/login',//请求地址
        method: 'post',//请求方式
        response: ({ body }) => {
            //获取请求体携带过来的用户名与密码
            const { username, password } = body;
            //调用获取用户信息函数,用于判断是否有此用户
            const checkUser = createUserList().find(
                (item) => item.username === username && item.password === password,
            )
            //没有用户返回失败信息
            if (!checkUser) {
                return { code: 201, data: { message: '账号或者密码不正确' } }
            }
            //如果有返回成功信息
            const { token } = checkUser
            return { code: 200, data: { token } }
        },
    },
    // 获取用户信息
    {
        url: '/api/user/info',
        method: 'get',
        response: (request) => {
            //获取请求头携带token
            const token = request.headers.token;
            //查看用户信息是否包含有次token用户
            const checkUser = createUserList().find((item) => item.token === token)
            //没有返回失败的信息
            if (!checkUser) {
                return { code: 201, data: { message: '获取用户信息失败' } }
            }
            //如果有返回成功信息
            return { code: 200, data: {checkUser} }
        },
    },
]
  1. 测试接口是否可用

注意:测试接口前需要先下载axios的依赖

// /src/main.ts
// 测试用户登录接口
import axios from 'axios'
axios({
    url: '/api/user/login',
    method: 'post',
    data: {
        username: 'wenxuan',
        password: 'wenxuan'
    }
})

Vue3整合progress

progress:可以实现路由进度条功能

  1. 下载progress依赖
npm i progress
  1. 添加路由进度条

通过路由全局首位实现路由进度条

// /src/router/index.ts
import { createRouter, createWebHashHistory } from "vue-router";
import { constantRoute } from "./routers";
import Nprogress from 'nprogress';
import 'nprogress/nprogress.css';

const router = createRouter({
    history: createWebHashHistory(),
    routes: constantRoute
})

// 全局前置路由
router.beforeEach((to:any, from:any, next:any) => {
    Nprogress.start()
    next()
})

// 全局后置路由
router.afterEach((to:any, from:any) => {
    Nprogress.done()
})

export default router;
  1. 修改进度条的颜色

修改进度条颜色之后,需要重启项目才能生效。

// /node_modules/nprogress/npgress.css
#nprogress .bar {
  background: #29d;
}
  1. 关闭路由条的圆圈加载
// /src/router/index.ts
Nprogress.configure({ showSpinner: false })

项目常用功能开发

路由不存在重定向404

// /src/router/routers.ts
// 对外暴露路由
export const constantRoute = [
    // 与上述路由均不匹配时重定向到404
    {
        path: '/:pathMatch(.*)*',
        redirect: '/404',
        name: 'Any'
    },
]

登录功能TOKEN保存

登录功能TOKEN保存到pinia和本地存储

  1. 封装本地存储TOKEN的工具方法。
  2. 在pinia中提供token变量和userLogin登录方法。
  3. userLogin方法中调用登录接口,并根据返回值决定对应的登录成功和登录失败逻辑。

封装本地存储TOKEN的方法

// /src/utils/token.ts
export const SET_TOKEN = (token:any) => {
    localStorage.setItem("TOKEN", token)
}

export const GET_TOKEN = () => {
    return localStorage.getItem("TOKEN")
}

pinia实现token的保存

// /src/store/modules/user.ts
import { defineStore } from "pinia";
import { ref } from "vue";
import { reqLogin } from "@/api/user";
import type { loginForm, loginResponseData } from "../../api/user/type";
import { GET_TOKEN, SET_TOKEN } from "../../utils/token";

export const useUserStore = defineStore('User', () => {
    const token = ref(GET_TOKEN())

    async function userLogin(data:loginForm) {
        // 调用登录接口
        let result:loginResponseData = await reqLogin(data)
        
        if(result.code == 200) {
            token.value = (result.data.token as string)
            // 将token本地存储
            SET_TOKEN(result.data.token)
            return 'ok'
        } else {
            // 登录失败
            return Promise.reject(new Error(result.data.message))
        }
        
    }

    return {token, userLogin}
})

动态显示组件

<template>
    <component :is='componentName'></component>
</template>

<script setup lang='ts'>
import { ref } from 'vue'
const componentName = ref('Home')
</script>

路由添加动画特效

路由添加动画特效:需要将路由封装到一个组件中,然后组件通过router-view展示出来。

<template>
<div class='main'>
    <router-view v-slot="{ Component }">
        <transition name="fade">
            <!-- 渲染layout一级路由的子路由 -->
            <component :is="Component" />
        </transition>
    </router-view>
</div>
</template>

<style scoped lang="scss">
.fade-enter-from {
    opacity: 0;
    transform: scale(0);
}

.fade-enter-active {
    transition: all 1s;
}

.fade-enter-to {
    opacity: 1;
    transform: scale(1);
}
</style>

组件刷新功能

  1. 通过一个变量和v-if实现组件的销毁和创建,从而实现组件的刷新。
  2. 通过nextTick函数变化变量的值,nextTick可以在组件销毁时调用。

nextTick:组件更新循环接收后执行的回调函数,这是异步函数。比如组件销毁完成后会执行nextTick传入的异步函数。

<template>
<component :is="Component" v-if="flag"/>
</template>

<script lang='ts' setup name='Main'>
import useLayoutSettingStore from '@/store/modules/setting';
import { nextTick, ref, watch } from 'vue';

// 变量
const layoutSettingStore = useLayoutSettingStore()
// 控制Main组件是否销毁
const flag = ref(true)

// 函数
// 监听设置仓库中的刷新变量是否变化
watch(() => layoutSettingStore.refsh, () => {
    // 刷新Main组件
    flag.value = false
    nextTick(() => {
        flag.value = true
    })
})
</script>

全屏切换功能

const fullScreen = () => {
    if(document.fullscreenElement) {
        document.exitFullscreen()
    } else {
        document.documentElement.requestFullscreen()
    }
}

登录和退出功能

退出登录功能

  1. 向服务器发送退出登录请求
  2. pinia仓库中相关数据清空(token、username、role、permission)
  3. 跳转到登录页面

路由鉴权功能

常见的路由鉴权功能:

  1. 未登录时(不存在token):能访问/login路由,不能访问其他路由
  2. 已登录时(存在token):不能访问/login路由,能访问其他路由
  3. 用户是否登录可以通过判断pinia中是否含有token,

页面标题动态变化

路由变化页面标题动态变化

  1. 配置路由项中在meta中添加title属性
  2. 配置全局后置路由设置文档的标题document.title
// /src/router/routers.ts
export const constantRoute = [
    {
        path: '/login',
        component: () => import('@/views/login/index.vue'),
        name: 'login',
        meta: {
            title: '登录',
        },
    }
]
// /src/router/index.ts
import { createRouter, createWebHashHistory } from "vue-router";
import { constantRoute } from "./routers";

const router = createRouter({
    history: createWebHashHistory(),
    routes: constantRoute
})

// 配置全局后缀路由守卫
router.afterEach((to:any, from) => {
    document.title = to.meta.title
})

export default router;

暗黑模式的设置

  1. 添加Element-dark的样式
// /src/main.ts
import 'element-plus/theme-chalk/dark/css-vars.css'
  1. 通过在html标签上设置dark样式即可
const changeDark = () => {
    // 获取html根节点
    const htmlDocuemnt = document.documentElement
    darkTitle.value ? htmlDocuemnt.className = 'dark' : htmlDocuemnt.className = ''
}

权限划分功能

权限的分类

  • 路由权限:指能不能显示出路由,跳转到对应页面的路由。
  • 按钮权限:指有没有指定按钮的权限,比如增删改查按钮的权限。

路由的划分

  • 常量路由:constantRoute,任意用户都能访问的路由。
  • 异步路由:asnycRoute,不同用户访问的权限不同的路由。
  • 任意路由:anyRoute,与常量路由、异步路由都不匹配的路由(404)。

用户路由权限的实现:在pinia小仓库中保存用户拥有的路由权限信息。

用户按钮权限的实现: