Vue

official::Vue.js

text::Javascript

挑战 | Vue.js挑战 (cn-vuejs-challenges.netlify.app)

JS语法框架对比:JavaScript框架语法特性对比 (lainbo.com)


一、Vue前置知识

1 命令

在使用命令前先要安装Node.js

text::Node.js安装

# 创建Vue工程
npm init vue@latest
Project name >> # 不要存在大写
Add TypeScript? # Ts扩展,回车默认No
Add JSX Support? # JSX支持,回车默认No
Add Vue Router for Single Page Application development? # 路由,回车默认No
Add Pinia for state management? # 状态管理,回车默认No
Add Vitest for Unit testing? # 单元测试,回车默认No
Add an End-to-End Testing Solution? #
Add ESLint for code quality? #
Add Prettier for code formatting? #

# 安装
npm install
cnpm install # npm的镜像,更快

# 运行
npm run dev

# 编译发布(编译后的内容在dist中)
npm run build

2 目录结构

.vscode:vscode工具的配置文件

node_modules:Vue项目的运行依赖文件(npm install安装后生成的文件夹)

public:资源文件夹(浏览器图标)

src:源码文件夹

.gitignore:git忽略文件

index.html:入口HTML文件。所有文件都在这个html中运行。

package.json:信息描述文件

README.md:注释文件

vite.config.js: Vue配置文件

注:

  • 一般情况下public放动态资源,src/assets中放静态资源。public可以是放服务器动态请求的资源,而assets是放前端必须要的资源。因为assets中的东西在build以后就编译到程序中,无法进行修改。
<!DOCTYPE html>
<html lang="en">
<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>
<script type="module" src="/src/main.js"></script>
</body>
</html>

{
"name": "vue3_cli_default", // 项目名
"version": "0.0.0", // 项目版本
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview"
},
"dependencies": {
"vue": "^3.2.8" // vue 版本
},
"devDependencies": { // 详细依赖环境
"@vitejs/plugin-vue": "^1.6.0",
"@vue/compiler-sfc": "^3.2.6",
"vite": "^2.5.2"
}
}

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()]
})

3 API风格

选项式API:Vue2写法。在组合式API的基础上实现。常用于项目中部分代码用Vue写。特点是在<script>中分别完善对应的功能后再选择,例如:data()methods等。

组合式API:Vue3写法。常用于整个项目都用Vue写。特点是可以将功能整合起来。

<script>
// 选项式API
export default {
// data() 返回的属性将会成为响应式的状态
// 并且暴露在 this 上
data() {
return {
count: 0
}
},

// methods 是一些用来更改状态与触发更新的函数
// 它们可以在模板中作为事件处理器绑定
methods: {
increment() {
this.count++
}
},

// 生命周期钩子会在组件生命周期的各个不同阶段被调用
// 例如这个函数就会在组件挂载完成后被调用
mounted() {
console.log(`选项式API:The initial count is ${this.count}.`)
}
}
</script>

<template>
<button @click="increment">选项式API:Count is: {{ count }}</button>
</template>
<script setup>
// 组合式API
import { ref, onMounted } from 'vue'

// 响应式状态
const count = ref(0)

// 用来修改状态、触发更新的函数
function increment() {
count.value++
}

// 生命周期钩子
onMounted(() => {
console.log(`组合式API:The initial count is ${count.value}.`)
})
</script>

<template>
<button @click="increment">组合式API:Count is: {{ count }}</button>
</template>

二、Vue基础

下列写法都是选项式写法。

1 文本插值

script中的值会在template中直接显示出来。

每个绑定仅支持单一表达式,也就是一段能够被求值的JavaScript代码。一个简单的判断方法是是否可以合法地写在return后面。

注:在网站上动态渲染任意 HTML 是非常危险的,因为这非常容易造成XSS 漏洞。请仅在内容安全可信时再使用 v-html,并且永远不要使用用户提供的 HTML 内容。

<template>
<h3>模板语法</h3>
<p>{{ msg }}</p>
<p>{{ hello }}</p>
<p>{{ number + 1 }}</p>
<p>{{ ok ? 'YES' : 'NO' }}</p>
<p>{{ message.split('').reverse().join('') }}</p>
</template>

<script>
export default{
data(){
return {
msg:"神奇的语法",
hello:"Hello World!",
number:10,
ok:true,
message:"大家好"
}
}
}
</script>

<!-- 下列是无效语句 -->
<!-- 这是一个语句,而非表达式 -->
{{ var a = 1 }}

<!-- 条件控制也不支持,请使用三元表达式 -->
{{ if (ok) { return message } }}
<!-- 链接解析 -->
<template>
<p>{{ rawHtml }}</p> <!-- 原样输出链接 -->
<p v-html="rawHtml"></p> <!-- 解析后嵌套在p标签内 -->
</template>

<script>
export default{
data(){
return {
rawHtml:"<a href='https://itbaizhan.com'>百战程序员</a>",
}
}
}
</script>

2 属性绑定

双大括号不能在 HTML attributes 中使用。想要响应式地绑定一个attribute,应该使用v-bind指令。

v-bind指令指示Vue将元素的idattribute 与组件的dynamicld属性保持一致。如果绑定的值是null或者undefined,那么该attribute将会从渲染的元素上移除。

因为v-bind非常常用,所以有简写语法,直接写个冒号即可。

<template>
<div class="{{ msg }}">测试</div> <!-- class的值原样输出 -->
<div v-bind:id="dynamicId" v-bind:class="msg">测试</div> <!-- 属性值转化后输出 -->
<div v-bind:id="isNull" v-bind:class="isUndefined">测试</div> <!-- 属性值都被移除 -->
<div :id="dynamicId" :class="msg">测试</div> <!-- 简写语法 -->
<button :disabled="isButtonDisabled">不可点击</button> <!-- 可以动态改变按钮 -->
<div v-bind="objectOfAttrs">测试</div> <!-- 一次绑定多个值 -->
</template>

<script>
export default{
data(){
return {
msg:"active",
dynamicId:"appid",
isNull:null,
isUndefined:undefined,
isButtonDisabled:true,
objectOfAttrs:{
id: "appId", // 属性名:"属性值",可以自定义
class: "appClass",
myAttr: "attr"
}
}
}
}
</script>

<!-- css会对类值为active的类生效 -->
<style>
.active{
color: red;
font-size: 30px;
}
</style>

3 条件渲染

v-if,v-else-if,v-else:为真则渲染,否则不渲染。

v-show类似v-if,也是条件判断,但是v-show后面不可接else。

v-ifv-show

  • v-if是“真实的”按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建。
  • v-if也是惰性的:如果在初次渲染时条件值为false,则不会做任何事。条件区块只有当条件首次变为true时才被渲染。
  • 相比之下,v-show简单许多,元素无论初始条件如何,始终会被渲染,只有CSS display属性会被切换。
  • 总的来说,v-if有更高的切换开销,而v-show有更高的初始渲染开销。因此,如果需要频繁切换,则使用v-show较好;如果在运行时绑定条件很少改变,则v-if会更合适。
<template>
<div v-if="flag">can you see me?</div> <!-- 修改flag的值看不同结果 -->
<div v-else>You should see me.</div>

<div v-if="type === 'A'">A</div> <!-- 修改type的值看不同结果 -->
<div v-else-if="type === 'B'">B</div>
<div v-else-if="type === 'C'">C</div>
<div v-else>Not A/B/C</div>

<div v-show="flag">show!</div> <!-- 修改flag的值看不同结果 -->
<div v-else>No show!</div>
</template>

<script>
export default{
data(){
return {
flag:true,
type:"D"
}
}
}
</script>

4 列表渲染

我们可以使用v-for指令基于一个数组来渲染一个列表。v-for指令的值需要使用item in items形式的特殊语法,其中items是源数据的数组,而item是迭代项的别名。

列表有如下:

  • <p v-for="item in names">:依次获取names中的值存到item中。
  • <p v-for="item of names">:同理,用of也行。
  • <p v-for="(item,index) in names">:依次获取names中的值存到item中,获得下标存入index中。
  • <p v-for="(item,index) of names">:同理。

对象有如下:

  • <p v-for="(value,key,index) in userInfo">:依次获得值,键,下标。
<template>
<div v-for="item in names">{{ item }}</div>
<!-- 获得列表中对象值 -->
<div v-for="item in result">
<p>{{ item.title }}</p>
<img :src="item.avator" alt="" />
</div>
<!-- 获得对象值 -->
<p v-for="(value,key,index) in userInfo">{{ value }} - {{ key }} - {{ index }}</p>
</template>

<script>
export default {
data() {
return {
names: ["uyi", "mio", "tsumugi"],
result: [
{
id: 1,
title: '百度',
avator: 'https://www.baidu.com/img/flexible/logo/pc/peak-result.png'
},
{
id: 2,
title: '淘宝',
avator: 'https://gw.alicdn.com/mt/TB1ZnvPLFXXXXa3XFXXXXXXXXXX-63-63.png'
}
],
userInfo: {
name: "ikun",
age: 24,
sex: "man"
}
}
}
}
</script>

5 Key管理

Vue默认按照”就地更新”的策略来更新通过v-for渲染的元素列表。当数据项的顺序改变时,Vue不会随之移动DOM元素的顺序,而是就地更新每个元素,确保它们在原本指定的索引位置上渲染。

比如原始数据顺序是:1、2、3。当改成:1、3、2后,Vue不会仅仅改变两者的顺序,而是连带所有数据全部重新渲染,这样十分消耗性能。

为了给Vue一个提示,以便它可以跟踪每个节点的标识,从而重用和重新排序现有的元素,你需要为每个元素对应的块提供一个唯一的key attribute

温馨提示:

  • key在这里是一个通过v-bind绑定的特殊attribute
  • 推荐在任何可行的时候为v-for提供一个key attribute
  • key绑定的值期望是一个基础类型的值,例如字符串或number类型
  • 不建议使用index作为key的值,因为index是会随数组改变而改变,我们要确保每一条数据的唯一索引不会改变,可以使用数据库中返回的id作为索引。
<template>
<!-- 用id作为唯一key -->
<div v-for="(item,index) in result" :key="item.id">
<p>{{ item.title }}</p>
<img :src="item.avator" alt="" />
</div>
</template>

<script>
export default {
data() {
return {
result: [
{
id: 1,
title: '百度',
avator: 'https://www.baidu.com/img/flexible/logo/pc/peak-result.png'
},
{
id: 2,
title: '淘宝',
avator: 'https://gw.alicdn.com/mt/TB1ZnvPLFXXXXa3XFXXXXXXXXXX-63-63.png'
}
]
}
}
}
</script>

6 事件管理

我们可以使用v-on指令(简写为@)来监听DOM事件,并在事件触发时执行对应的JavaScript。用法:v-on:click="methodName"@click="handler"

事件处理器的值可以是:

  1. 内联事件处理器:事件被触发时执行的内联JavaScript语句(与onclick类似)。通常用于简单场景。
  2. 方法事件处理器:一个指向组件上定义的方法的属性名或是路径。
<!-- 内联事件处理器 -->
<template>
<h3>内联事件处理器</h3>
<button @click="count++">Inline add 1</button>
<p>Count is: {{ count }}</p>
</template>

<script>
export default {
data() {
return {
count: 0
}
}
}
</script>


<!-- 方法事件处理器 -->
<template>
<h3>方法事件处理器</h3>
<button @click="addCount">Method add 1</button>
<p>Count is: {{ count }}</p>
</template>

<script>
export default {
data() {
return {
count: 0
}
},
// 所有的方法或者函数都放在这里
methods:{
addCount(){
this.count++;
}
}
}
</script>

7 事件传参

事件参数可以获取event对象和通过事件传递数据。

<template>
<!-- 无参传递事件 -->
<button @click="addCount">Add</button>
<p>{{ count }}</p>

<!-- 有参传递事件 -->
<p @click="getNameHandler($event,item)" v-for="(item,index) of names" :key="index">{{ item }}</p>
</template>

<script>
export default {
data() {
return {
count: 0,
names: ["uyi","mio","tsumugi"]
}
},
methods:{
// 无参可以直接获得e
addCount(e){
// 控制台输出
console.log(e.target.innerHTML = "A");
this.count++;
},
// 带参获得事件(点击名字可以输出对应结果)
getNameHandler(e,name)
{
console.log(name);
console.log(e);
}
}
}
</script>

8 事件修饰符

在处理事件时调用event.preventDefault()event.stopPropagation()是很常见的。尽管我们可以直接在方法内调用,但如果方法能更专注于数据逻辑而不用去处理DOM事件的细节会更好。

official::更多修饰符

因此Vue为v-on提供了事件修饰符,常见如下:

  • .stop:阻止事件冒泡
  • .prevent:阻止默认事件
  • .once:事件只会被触发一次
  • .enter:回车事件触发的
<template>
<h3>点击但不跳转</h3>
<a @click.prevent="clickHandle" href="https://www.baidu.com">百度</a>

<h3>父元素不冒泡</h3>
<!-- 阻止子元素向上冒泡到父元素 -->
<div @click="clickDiv">
<p @click.stop="clickP">测试冒泡</p>
</div>
</template>

<script>
export default {
data() {
return {
}
},
methods:{
clickHandle(e){
console.log("被点了喵");
},
clickDiv(){
console.log("div被点了喵");
},
clickP(){
console.log("P被点了喵");
}
}
}
</script>

9 数组侦测

变更方法:

  • Vue能够侦听响应式数组的变更方法,并在它们被调用时触发相关的更新。这些变更方法包括:push()、pop()、shift()、unshift()、splice()、sort()、reverse()

替换一个数组:

  • 变更方法,顾名思义,就是会对调用它们的原数组进行变更。相对地,也有一些不可变(immutable)方法,例如flter()concat()slice(),这些都不会更改原数组,而总是返回一个新数组。当遇到的是非变更方法时,我们需要将旧的数组替换为新的
<template>
<!-- 数组变化帧听 -->
<button @click="addListHandle">添加数据</button>
<ul>
<li v-for="(item,index) of names" :key="index">{{ item }}</li>
</ul>

<!-- 合并数组 -->
<button @click="concatHandle">合并数组</button>
<h3>数组1</h3>
<p v-for="(item,index) of nums1" :key="index">{{ item }}</p>
<h3>数组2</h3>
<p v-for="(item,index) of nums2" :key="index">{{ item }}</p>
</template>

<script>
export default {
data() {
return {
names: ["uyi","mio","tsumugi"],
nums1: [1,2,3,4,5],
nums2: [6,7,8,9,10]
}
},
methods:{
addListHandle(){
// UI会自动更新
// this.names.push("azi");
// console.log(this.names);

// 不会引起UI自动更新
this.names.concat(["azi"]);
console.log(this.names.concat(["azi"]));

// 重新赋值更新UI
// this.names = this.names.concat(["azi"]);
// console.log(this.names);
},
concatHandle(){
// 同理上面
this.nums1 = this.nums1.concat(this.nums2);
}
}
}
</script>

10 计算属性

模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护。因此我们推荐使用计算属性来描述依赖响应式状态的复杂逻辑。

重点区别:

  • 计算属性:计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。

  • 方法:方法调用总是会在重渲染发生时再次执行函数。

<template>
<h3>{{ itbaizhan.name }}</h3>
<!-- 计算属性仅计算一次,不加括号 -->
<p>{{ itbaizhanContent }}</p>
<p>{{ itbaizhanContent }}</p>
<p>{{ itbaizhanContent }}</p>
<p>{{ itbaizhanContent }}</p>
<!-- 函数会被调用四次 -->
<p>{{ itbaizhanContents() }}</p>
<p>{{ itbaizhanContents() }}</p>
<p>{{ itbaizhanContents() }}</p>
<p>{{ itbaizhanContents() }}</p>
</template>

<script>
export default {
data() {
return {
itbaizhan:{
name: "百战程序员",
content: ["前端","Java","Python"]
}
}
},
// 计算属性
computed:{
itbaizhanContent(){
return this.itbaizhan.content.length > 0 ? "Yes": "No";
}
},
// 方法
methods:{
itbaizhanContents(){
return this.itbaizhan.content.length > 0 ? "Yes": "No";
}
}
}
</script>

11 Class绑定

数据绑定的一个常见需求场景是操纵元素的CSS class列表,因为classattribute,我们可以和其他attribute一样使用v-bind 将它们和动态的字符串绑定。但是,在处理比较复杂的绑定时,通过拼接生成字符串是麻烦且易出错的。因此,Vue专门为classv-bind用法提供了特殊的功能增强。除了字符串外,表达式的值也可以是对象或数组。

在数组中也可以有条件地渲染某个class,使用三元表达式即可。

数组中也可以嵌套对象,但是对象中不能嵌套数组。

<template>
<p :class="{'active': isActive,'text-danger': hasError}">Class样式绑定1</p>
<!-- 可以直接绑定一个类 -->
<p :class="classObject">Class样式绑定2</p>
<!-- 可以绑定一个数组 -->
<p :class="[arrActive,arrHasError]">Class样式绑定3</p>
<!-- 有条件绑定 -->
<p :class="[isActive ? 'active text-danger' : '']">Class样式绑定4</p>
<!-- 数组嵌套对象 -->
<p :class="[{ 'active':isActive }, errorClass]">Class样式绑定5</p>
</template>

<script>
export default {
data(){
return{
// 改变bool值使类带上对应值
isActive:true,
hasError:false,
classObject:{
'active':true,
'text-danger':true
},
arrActive:"active",
arrHasError:"text-danger",
errorClass:"text-danger"
}
}
}
</script>
<style>
.active{ font-size: 30px; }
.text-danger{ color: red; }
</style>

12 Style绑定

数据绑定的一个常见需求场景是操纵元素的CSS style列表,因为styleattribute,我们可以和其他
attribute一样使用v-bind将它们和动态的字符串绑定。但是,在处理比较复杂的绑定时,通过拼接生成字符串是麻烦且易出错的。因此,Vue专门为stylev-bind用法提供了特殊的功能增强。除了字符串外,表达式的值也可以是对象或数组。

一般不使用,因为Style绑定的权重很高,后期想改比较麻烦,要使用推荐使用class绑定。

<template>
<!-- 记得加单位 -->
<p :style="{color:activeColor,fontSize:fontSize +'px' }">Style绑定1</p>
<!-- 绑定类 -->
<p :style="styleobject" >Style绑定2</p>
<!-- 绑定数组(一般不用) -->
<p :style="[styleobject]">Style绑定3</p>
</template>

<script>
export default {
data(){
return{
activeColor: "green",
fontSize: 30,
styleobject: {
color : "red",
fontsize: "30px"
}
}
}
}
</script>

13 侦听器

侦听数据的变化,然后执行对应操作。具有响应式的数据可以被侦听,比如{{ value }}

我们可以使用watch选项在每次响应式属性发生变化时触发一个函数。

<template>
<h3>侦听器</h3>
<p>{{ message }}</p>
<button @click="updateHandle">修改数据</button>
</template>

<script>
export default {
data(){
return{
message: "Hello"
}
},
methods: {
updateHandle(){
this.message = "World";
}
},
watch:{
// 与属性名相同(参数1是新数据,参数2是老数据)
message(newValue,oldValue){
console.log(newValue,oldValue);
}
}
}
</script>

14 表单输入绑定

在前端处理表单时,我们常常需要将表单输入框的内容同步给JavaScript 中相应的变量。手动连接值绑定和更改事件监听器可能会很麻烦,v-model指令帮我们简化了这一步骤。

v-model也有修饰符

  • .lazy
    默认情况下,v-model会在每次input事件后更新数据。你可以添加.lazy 修饰符来改为在每次change事件后更新数据。比如输入完成后点击搜索或其他地方才会更改数据。
  • .number:只接受数字类型。
  • .trim:输出会去除前后空格。
<template>
<h3>表单输入绑定</h3>
<form>
<!-- 数据回显 -->
数据:<input type="text" v-model="message">
<p>{{ message }}</p>
<!-- 懒惰回显-->
懒惰:<input type="text" v-model.lazy="lazy">
<p>{{ lazy }}</p>
<!-- 数字回显 -->
数字:<input type="text" v-model.number="number">
<p>{{ number }}</p>
<!-- 去空格回显-->
左右去空格:<input type="text" v-model.trim="trim">
<p>{{ trim }}</p>
<!-- 勾选修改 -->
<input type="checkbox" id="checkbox" v-model="checked" />
<label for="checkbox">{{ checked }}</label>
</form>
</template>

<script>
export default {
data(){
return{
message: "",
lazy: "",
number: "",
trim: "",
checked:false
}
}
}
</script>

15 模板引用

虽然Vue的声明性渲染模型为你抽象了大部分对DOM的直接操作,但在某些情况下,我们仍然需要直接访问底层DOM元素。要实现这一点,我们可以使用特殊的ref attribute

挂载结束后引用都会被暴露在this.$refs之上。

对模板操作:

  • 内容改变:{{ 模板语法 }}
  • 属性改变:v-bind:指令:指令
  • 事件:v-on:click@click
  • DOM:ref="name"$refs.ref_name.js_func。如果没有特别的需求,不要操作DOM!
<template>
<!-- 绑定ref用于执行JS函数 -->
<div ref="container" class="container">{{ content }}</div>
<input ref="username" type="text" />
<button @click="getElementHandle">获取元素</button>
</template>
<script>
export default {
data(){
return{
content:"内容"
}
},
methods:{
getElementHandle(){
// 通过$refs.ref_name.js_func执行原生JS函数
this.$refs.container.innerHTML = "哈哈哈";
console.log(this.$refs.username.value);
}
}
}
</script>

三、 Vue组件

1 组件基础

组件最大的优势就是可复用性。

当使用构建步骤时,我们一般会将Vue组件定义在一个单独的.vue文件中,这被叫做单文件组件(简称SFC)

在components中创建的Vue,通过App.vue进行关联。

<!-- 组件书写 -->
<!-- 承载Html标签(必有) -->
<temp1ate>
<div>承载标签</div>
</template>

<!-- 承载JS逻辑(可以没有) -->
<script>
export default {}
</script>

<!-- 承载CSS样式(可以没有) -->
<!-- scoped表示样式只作用于当前内部元素,不会影响别的组件 -->
<style scoped>

</style>
<!-- 在App.vue中 -->
<script setup>
// 第一步:引入组件
import MyComponent from "./components/MyComponent.vue"

export default {
// 第二步:注入组件
components:{
MyComponent
}
}
</script>

<template>
<!-- 第三步:显示组件,两种写法 -->
<MyComponent/>
<my-component/ >
</template>

<style>
</style>

2 简单样式

APP.vue中引入:Header、Main、Aside。

Main.vue中继续引入:Article。

Aside.vue中继续引入:Item。

<!-- Header.vue-->
<template>
<h3>Header</h3>
</template>
<style scoped>
h3{
width: 100%;
height: 100px;
border: 5px solid #999;
text-align: center;
line-height: 100px;
box-sizing: border-box;
}
</style>
<!-- Article.vue -->
<template>
<h3>Article</h3>
</template>

<style scoped>
h3{
width: 80%;
margin: 0 auto;
text-align: center;
line-height: 100px;
box-sizing: border-box;
margin-top: 50px;
background: #999;
}
</style>
<!-- Main.vue -->
<template>
<div class="main">
<h3>Main</h3>
<Article/>
<Article/>
</div>
</template>

<script>
import Article from "./Article.vue"
export default {
components: {
Article
}
}
</script>
<style scoped>
.main{
float: left;
width: 70%;
height: 400px;
border: 5px solid #999;
box-sizing: border-box;
}
</style>
<!-- Item.vue -->
<template>
<h3>Item</h3>
</template>
<style scoped>
h3{
width: 80%;
margin: 0 auto;
text-align: center;
line-height: 100px;
box-sizing: border-box;
margin-top: 10px;
background: #999;
}
</style>
<!-- Aside.vue -->
<template>
<div class="aside">
<h3>Aside</h3>
<Item />
<Item />
<Item />
</div>
</template>

<script>
import Item from "./Item.vue"
export default {
components: {
Item
}
}
</script>

<style scoped>
.aside{
float: right;
width: 30%;
height: 400px;
border: 5px solid #999;
box-sizing: border-box;
}
</style>
<!-- App.vue -->
<script>
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
// 第一步:引入组件
import Header from "./components/Header.vue"
import Main from "./components/Main.vue"
import Aside from "./components/Aside.vue"

export default {
// 第二步:注入组件
components:{
Header,
Main,
Aside
}
}
</script>

<template>
<Header />
<Main />
<Aside />
</template>

3 注册方式

全局注册虽然很方便,但有以下几个问题:

  • 全局注册,但并没有被使用的组件无法在生产打包时被自动移除(也叫"tree-shaking")。如果你全局注册了一个组件,即使它并没有被实际使用,它仍然会出现在打包后的JS文件中。

  • 全局注册在大型项目中使项目的依赖关系变得不那么明确。在父组件中使用子组件时,不太容易定位子组件的实现。和使用过多的全局变量一样,这可能会影响应用长期的可维护性。

// 局部注册,在任意vue中
<script>
import Item from "./Item.vue"
export default {
components: {
Item
}
}
</script>


// 全局注册,在main.js中,注册后再任意组件中都可以用
import { createApp } from 'vue'
import App from './App.vue'
import Header from "./components/Header.vue"

const app = createApp(App)

// 在创建app后,挂载app前写组件
app.component("Header",Header)

app.mount('#app')

4 组件传递数据

组件与组件之间不是完全独立的,而是有交集的,那就是组件与组件之间是可以传递数据的传递数据的解决方案就是props

通过props传递数据,不仅可以传递字符串类型的数据,还可以是其他类型,例如:数字、对象、数组等。但实际上任何类型的值都可以作为props的值被传递。

props传递数据,只能从父级传递到子级,不能反其道而行。

<!-- 父组件 -->
<template>
<h3>Parent</h3>
<!-- 传递数据 -->
<Child title="Parent数据" demo="测试" />
<!-- 传递动态数据 -->
<Child :msg="message" :age="age" />
<!-- 传递数组数据 -->
<Child :names="names" />
<!-- 传递对象数据 -->
<!-- <Child :userInfo="userInfo" /> -->
</template>

<script>
import Child from "./Child.vue"
export default {
data(){
return{
message:"Parent dynamic data!",
age:20,
names:["yui","mio","tsumugi"],
userInfo:{
name: "kurumi",
age: 16
}
}
},
components:{
Child
}
}
</script>


<!-- 子组件 -->
<!-- 没有的参数默认是传递undefined -->
<template>
<h3>Child</h3>
<p>{{ title }}</p>
<p>{{ demo }}</P>
<p>{{ msg }}</P>
<p>{{ age }}</P>
<ul>
<li v-for="(item, index) of names" :key="index">{{ item }}</li>
</ul>
<p>{{ userInfo.name }}</p>
<p>{{ userInfo.age }}</p>
</template>

<script>
export default {
data(){
return{}
},
// 子组件接收数据
props: ["title","demo","msg","age","names","userInfo"]
}
</script>

5 组件数据验证

添加组件数据验证:

  • type:类型验证
  • default:默认值
  • required:是否必须
  • validator:自定义验证函数

主:prop的数据是只读的,不可修改。

<!-- 父组件 -->
<template>
<h3>Parent</h3>
<Child :single="single" :multiple="multiple"/>
</template>

<script>
import Child from "./Child.vue"
export default {
data(){
return{
single: 20,
multiple: "123"
}
},
components: {
Child
}
}
</script>


<!-- 子组件 -->
<template>
<h3>Child</h3>
<p>{{ single }}</P>
<p>{{ multiple }}</P>
<p>{{ age }}</P>
<p v-for="(item,index) of names" :key="index">{{ item }}</p>
</template>
<script>
export default {
data() {
return {}
},
props: {
// 数据验证
single: {
// 类型验证,验证不通过爆警告
type: String,
// 必须项
required: true
},
multiple: {
// 多个类型验证
type: [Number, Array, Object]
},
age: {
type: Number,
// 默认值
default: 10
},
names: {
type: Array,
// 数字可以字符串可以直接default,但是数组或对象必须用工厂函数返回默认值
default(){
return ["yui","mio","tsumugi"]
}
}
}
}
</script>
<!-- 验证类的同时还验证类中的数据 -->
<script>
export default {
props: {
value: {
type: Object,
required: true,
validator: (value) => {
return typeof value.title === 'string' &&
typeof value.content === 'string' &&
typeof value.src === 'string' &&
typeof value.time === 'number';
},
},
},
}
</script>

6 组件事件

在组件的模板表达式中,可以直接使用$emit方法触发自定义事件。

触发自定义事件的目的是组件之间传递数据。

组件之间数据传递:

  • 父传子:props
  • 子传父:this.$emit
<!-- 父组件 -->
<template>
<h3>组件事件</h3>
<Child @someEvent="getHandle" />
<p>父元素:{{ message }}</p>
</template>

<script>
import Child from "./Child.vue"
export default {
data(){
return{
message: ""
}
},
components:{
Child
},
methods:{
getHandle(data){
this.message = data;
}
}
}
</script>



<!-- 子组件 -->
<template>
<h3>Child</h3>
<button @click="clickEventHandle">传递数据</button>
</template>

<script>
export default {
data(){
return{
msg: "Child数据!"
}
},
methods: {
clickEventHandle(){
//自定义事件
this.$emit("someEvent",this.msg)
}
}
}
</script>

7 组件侦听

组件配合v-model使用,可以实现一个组件读取数据,另一个组件接收数据。

<!-- 父组件 -->
<template>
<h3>Main</h3>
<p>搜索内容为:{{ search }}</p>
<Child @searchEvent="getSearch" />
</template>

<script>
import Child from "./Child.vue"
export default {
data(){
return{
search: ""
}
},
components: {
Child
},
methods: {
getSearch(data){
this.search = data;
}
}
}
</script>

<!-- 子组件 -->
<template>
搜索:<input type="text" v-model="search">
</template>

<script>
export default {
data(){
return{
search: ""
}
},
//侦听器
watch: {
search(newValue,oldValue){
this.$emit("searchEvent", newValue)
}
}
}
</script>

8 props实现子传父

props可以传递数据,因此可以利用子组件传递函数,在函数中携带参数,从而利用props实现子传父。

<!-- 父组件 -->
<template>
<h3>ComponentA</h3>
<p>父元素:{{ message }}</p>
<Child title="标题" :onEvent="dataFn"/>
</template>

<script>
import Child from "./Child.vue"
export default {
data(){
return{
message: ""
}
},
components: {
Child
},
methods: {
dataFn(data){
this.message = data;
}
}
}
</script>

<!-- 子组件 -->
<template>
<h3>Child</h3>
<p>{{ title }}</p>
<!-- 类似回调函数 -->
<p>{{ onEvent('传递数据')}}</p>
</template>

<script>
export default {
data(){
return{}
},
props: {
title: String,
onEvent: Function
}
}
</script>

9 依赖注入

正常情况下,如果祖父的数据要传递给孙子,需要先传递给父亲,再传递给孙子,这一问题成为”prop 逐级透传”。

provideinject可以帮助我们解决这一问题。一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。

provideinject只能由上到下传递,不能反向传递。

<!-- grandparent.vue -->
<template>
<h3>Grandparent</h3>
<Parent/>
</template>

<script>
import Parent from "./Parent.vue"
export default {
data(){
return{
message: "Grandparent Data"
}
},
// 通过Data赋值
provide(){
return{
message: this.message
}
},
// 直接赋值
// provide:{
// message: "爷爷的财产"
// },
components:{
A
}
}
</script>

<!-- parent.vue -->
<template>
<h3>Parent</h3>
<Child />
</template>

<script>
import Child from "./child.vue"
export default {
components: {
Child
},
}
</script>

<!-- child.vue -->
<template>
<h3>Child</h3>
<p>{{ message }}</p>
</template>

<script>
export default {
inject: ["message"]
}
</script>
// 全局注册,在main.js中,注册后再任意组件中都可以用
import { createApp } from 'vue'
import App from './App.vue'
import Header from "./components/Header.vue"

const app = createApp(App)

// 在创建app后,挂载app前写组件,全局提供数据
app.provide(name,data)
app.provide("message","I'm a message")

app.mount( '#app')

10 插槽

用于在组件之间传递模板。

<slot>元素是一个插槽出口(slot outlet),标示了父元素提供的插槽内容(slot content)将在哪里被渲染。

使用时,父元素模板定义子元素,在子元素中写模板。子元素使用<slot>双标签调用出父元素的定义的内容。

插槽中可以书写动态数据,动态数据是存储在父组件中的。

插槽可以有默认值,当父组件的插槽中内容为空时,将显示子组件中插槽标签中的内容。

具名插槽:给插槽一个属性值,用于区分不同的插槽。写法是<template v-slot:name>,可以简写成<template #name>

<!-- 父组件 -->
<template>
<!-- 父元素中的子元素标签用双标签,在标签中写模板 -->
<Child>
<div>
<h3>slot title</h3>
<p>slot content</p>
<p>{{ content }}</p>
</div>
</Child>
<!-- 通过名字区分插槽 -->
<Child>
<template v-slot:first>
<h3> I'm first </h3>
</template>
<template #second>
<h3> I'm second</h3>
</template>
</Child>
<!-- 使用父子组件的数据,:name="别名",然后点出里面的属性 -->
<Child v-slot:second="childData">
<h3>{{ message }} -- {{ childData.msg }}</h3>
</Child>
</template>

<script>
import Child from "./Child.vue"
export default {
data(){
return{
content: "Hello World!",
message: "Parent Data!"
}
},
components: {
Child
},
}
</script>

<!-- 子组件 -->
<template>
<h3>Child</h3>
<!-- 使用插槽。注:标签名不能改-->
<!-- 将父组件中插槽中的内容注释后,将显示下列默认内容 -->
<slot>无name默认值</slot>
<!-- 用name指向父组件中v-slot指向的模板 -->
<slot name="first">first默认值</slot>
<!-- 传递msg数据给父组件 -->
<slot name="second" :msg="childMessage">second默认值</slot>
</template>

<script>
export default{
data(){
return{
childMessage:"Child Data!"
}
}
}

</script>

11 组件生命周期

生命周期函数

  • 创建期:beforeCreate、created
  • 挂载期:beforeMount、mounted
  • 更新期:beforeUpdate、updated
  • 销毁期:beforeUnmount、unmounted

当组件进入对应的时期后,将执行对应的函数的内容。

<template>
<h3>组件生命周期</h3>
<p>{{ message }}</p>
<button @click="updateHandle">更新数据</button>
</template>
<script>
export default {
data(){
return{
message: "老数据"
}
},
methods: {
updateHandle(){
this.message="新数据"
}
},
beforeCreate(){
console.log("组件创建之前");
},
created(){
console.log("组件创建之后");
},
beforeMount(){
console.log("组件渲染之前");
},
mounted(){
console.log("组件创建之后");
},
beforeupdate(){
console.log("数据更新之前");
},
updated(){
console.log("数据更新之后");
},
beforeUnmount(){
console.log("组件卸载之前");
},
unmounted(){
console.log("组件卸载之后");
}
}
</script>

12 生命周期应用

常见应用:

  • 通过ref获取元素DOM结构。在beforeMount()前无法获得,因为组件未挂载。在mounted()后可以操作DOM结构,此时组件挂载。
  • 模拟网络请求渲染数据。在beforeCreate()前无法获得,因为组件未创建,没有data(),数据无法存储。在created()后则可以将获得的数据存储在data()中。但是实际上为了让网页先显示结构再显示数据,所以一般是在mounted()中作数据获得的操作,因为先获得数据没有结构也没有实际作用。
<!-- 读取DOM结构 -->
<template>
<h3>组件生命周期函数应用</h3>
<p ref="name">百战程序员</p>
</template>

<script>
export default {
beforeMount(){
// 此时组件未挂载,获得undefined
console.log(this.$refs.name);
},
mounted(){
// 此时组件挂载,获得百战程序员
console.log(this.$refs.name);
}
}
</script>


<!-- 模拟网络请求渲染数据 -->
<!-- 结果只有mio,因为在创建之前没有banner,banner还是undefined -->
<template>
<ul>
<li v-for="(item,index) of banner" :key="index">
<h3>{{ item.title }}</h3>
<p>{{ item.content }}</p>
</li>
</ul>
</template>

<script>
export default {
data(){
return{
banner: []
}
},
beforeCreate(){
// 模拟网络获得数据
this.banner = [
{
title: "uyi",
content: "guitar"
}
]
},
created(){
// 模拟网络获得数据
this.banner.push(
{
title: "mio",
content: "bass"
}
)
},
// 为了让网页先显示结构再显示数据,所以一般是在mounted中作数据获得的操作
mounted(){
this.banner.push(
{
title: "tsumugi",
content: "keyboard"
}
)
}
}
</script>

13 组件切换

有些场景会需要在两个或多个场景之间来回切换,比如Tab界面。

当使用<component is="...">来在多个组件间作切换时,被切换掉的组件会被卸载。我们可以通过<keep-alive>组件强制被切换掉的组件仍然保持“存活”的状态。

在下列案例中,如果不加上<keep-alive>,切换组件时,组件会被卸载,这样更新了数据后再切换数据也会重置。但是加上<keep-alive>后组件将不会卸载,数据也就能保存下来。

<!-- Parent.vue -->
<template>
<!-- keep-alive必须包裹确定的组件,不能再包裹button,否则报错 -->
<keep-alive>
<!-- 加载组件,is绑定 -->
<component :is="tabComponent"></component>
</keep-alive>
<button @click="changeHandle">切换组件</button>
</template>
<script>
import A from "./A.vue"
import B from "./B.vue"
export default {
data(){
return{
// 组件名绑定,要用字符串
tabComponent : "A",
}
},
components:{A,B},
methods:{
changeHandle(){
// 切换组件,要用字符串
this.tabComponent = this.tabComponent == "A" ? "B" : "A"
}
},
}
</script>


<!-- A.vue -->
<template>
<h3>我是A</h3>
<p>{{ message }}</p>
<button @click="updateHandle">更新数据</button>
</template>

<script>
export default {
data(){
return{
message: "老数据"
}
},
beforeUnmount(){
console.log("组件卸载之前");
},
unmounted(){
console.log("组件卸载之后");
},
methods: {
updateHandle(){
this.message = "新数据"
}
}
}
</script>


<!-- B.vue -->
<template>
<p>我是B</p>
</template>

<script>
export default {
data(){
return{
}
},
beforeUnmount(){
console.log("组件卸载之前");
},
unmounted(){
console.log("组件卸载之后");
}
}
</script>

14 异步组件

在大型项目中,我们可能需要拆分应用为更小的块,并仅在需要时再从服务器加载相关组件。Vue提供了defineAsyncComponent方法来实现此功能。

下列案例中,若不做成异步,则在网络请求中会一次性加载A,B。若将B做成了异步请求,则一开始并不会请求B,当用户点击切换后才会在网络中请求B。请求后将保存在本地,多次切换后都不会再发送网络请求。

<!-- Parent.vue。A.vue和B.vue在上一节 -->
<template>
<component :is="tabComponent"></component>
<button @click="changeHandle">切换组件</button>
</template>
<script>
// 导入组件
import { defineAsyncComponent } from 'vue'
import A from "./A.vue"
// 设置成异步
const B = defineAsyncComponent(() => import('./B.vue'))

export default {
data(){
return{
// 组件名绑定,要用字符串
tabComponent : "A",
}
},
components:{A,B},
methods:{
changeHandle(){
// 切换组件,要用字符串
this.tabComponent = this.tabComponent == "A" ? "B" : "A"
}
},
}
</script>

四、Vue其他

1 透传 Attributes

“透传attribute”指的是传递给一个组件,却没有被该组件声明为propsemitsattribute或者v-on事件监听器。最常见的例子就是classstyleid。当一个组件以单个元素为根作渲染时,透传的attribute会自动被添加到根元素上。

注:在实际场景中并不常用。

<!-- 父组件 -->
<template>
<Child class="attr-container" />
</template>

<script>
import Child from "./Child.vue"
export default {
components: {
Child
}
}
</script>

<!-- 子组件 -->
<template>
<!-- 必须是唯一根元素 -->
<h3>透传属性</h3>
<!-- <p>再多加一个标签就会失效</p> -->
</template>

<script>
// 禁用属性继承,添加后class将不会继承下来
// export default{
// inheritAttrs: false
// }
</script>

<style>
.attr-container{
color: red;
}
</style>

2 引入第三方库

2.1 Swiper

常用动画库:offifcial::Swiper Vue.js

首先:npm i swiper进行安装

<!-- 导航+轮播图案例 -->
<template>
<!-- 导航栏属性设置 -->
<swiper :navigation="true" :modules="modules" class="mySwiper">
<!-- 单个栏目设置 -->
<swiper-slide >
<div class="slide slide1"></div>
</swiper-slide>
<swiper-slide >
<div class="slide slide2"></div>
</swiper-slide>
<swiper-slide >
<div class="slide slide3"></div>
</swiper-slide>
<swiper-slide >
<div class="slide slide4"></div>
</swiper-slide>
<swiper-slide >
<div class="slide slide5"></div>
</swiper-slide>
</swiper>
</template>

<script>
// 导入轮播图
import { Swiper, SwiperSlide } from 'swiper/vue';
// 导入导航栏
import { Navigation } from 'swiper/modules';
// 导入对应css
import 'swiper/css';
import 'swiper/css/navigation';

export default {
// 导航栏注册
setup() {
return {
modules: [Navigation],
}
},
// 轮播图注册
components: {
Swiper,
SwiperSlide,
},
};
</script>

<style scoped>
.swiper-slide {
height: 405px;
display: flex;
align-items: center;
justify-content: center;
}
/* 切割图片 */
.slide{
height: 405px;
width: 144px;
}
.slide1 {
background: url("src/assets/img/mygo.png") no-repeat 0px 0px;
}
.slide2 {
background: url("src/assets/img/mygo.png") no-repeat -144px 0px;
}
.slide3 {
background: url("src/assets/img/mygo.png") no-repeat -288px 0px;
}
.slide4 {
background: url("src/assets/img/mygo.png") no-repeat -432px 0px;
}
.slide5 {
background: url("src/assets/img/mygo.png") no-repeat -576px 0px;
}
</style>

3 Vuex

Vuex是一个专为Vue.js应用程序开发的状态管理模式+库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

简单来说,状态管理可以理解成为了更方便的管理组件之间的数据交互,提供了一个集中式的管理方案,任何组件都可以按照指定的方式进行读取和改变数据。

最常用的核心概念包含:

  • state:存储数据
  • Getter:对数据进行过滤
  • Mutation:修改数据
  • Action:类似Mutation,但是Action是提交Mutation,不是变更状态,并且Action可以包含任意异步操作

official::Vuex (vuejs.org)

安装:npm i vuex

新建文件:src/store/index.js

// index.js
import { createStore } from "vuex"

// Vuex的核心作用就是管理组件之间的状态
export default store = createStore({
// 所有的状态都放在这里(数据)
state: {
// 定义数据
counter: 0;
},
// 对数据进行过滤
getters: {
getCount(state){
return state.counter > 0 ? state.counter : "counter小于0,不符合要求"
}
},
// 修改数据
mutatons: {
addCounter(state, num){
state.counter += num;
}
},
// 为异步操作做准备
actions: {
asyncAddCounter({ commit }){
axios.get("http://iwenwiki.com/api/generator/list.php")
.then(res => {
commit("addCounter", res.data[0])
})
}
}
})
// 在main.js中引入store
import store from "./store"

createApp(App).use(store).mount("#app");
<!-- 直接在组件中进行使用 -->
<!-- 读取方法1:用于单次 -->
<template>
<p>counter = {{ $store.state.counter }}</p>
<p>counter = {{ $store.state.getCount }}</p>
<button @click="addClickHandle">增加</button>
<button @click="addAsyncClickHandle">异步增加</button>
</template>
<script>
export default{
methods: {
addClickHandle(){
// 固定调用方式
this.$store.commit("addCounter", 10);
},
addAsyncClickHandle(){
this.$store.dispatch("asyncAddCounter");
}
}
}
</script>

<!-- 读取方法2:用于多次 -->
<template>
<p>counter = {{ counter }}</p>
<p>counter = {{ getCounter }}</p>
<button @click="addClickHandle">增加</button>
</template>
<script>
import { mapState, mapGetters, mapMutations } from "vuex"
export default{
computed: {
// 专门来读取vuex的数据
...mapState(["counter"]),
...mapGetters(["getCounter"])
},
methods: {
...mapMutations(["addCounter"]),
addClickHandle(){
this.addCounter(20);
},
addAsyncClickHandle(){
this.asyncAddCounter();
}
}
}

</script>

4 Pinia

由于 Pinia 在生态系统中能够承担相同的职责且能做得更好,因此 Vuex 现在处于维护模式。对于新的应用,建议使用 Pinia。

official::Pinia (vuejs.org)

安装:npm i pinia

新建:src/stores/index.js

使用:

  • State:属性
  • Method:方法
  • Getter:基于 store 中 state 创建的计算属性,它返回的是派生自 state 的值,并且会根据 state 的变化自动更新
  • Action:用于修改 state 或执行副作用(如异步操作)的方法。在 Pinia 中,可以直接在 action 内部修改 state,而无需通过 mutation。
// main.js引入pinia
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App);
app.use(createPinia());
app.mount('#app');
// src/stores/index.js
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useCsrfTokenStore = defineStore('csrf',() => {
// 定义属性
const csrfToken = ref('');

// 定义方法
const get = () => { return csrfToken.value; };

const set = (token) => { csrfToken.value = token;};

// 定义 getter
const tokenIsNotEmpty = computed(() => !!csrfToken.value);

// 定义 action,从服务器获取并设置 CSRF 令牌
async fetchAndSetToken() {
const response = await axios.get('/api/csrf-token');
set(response.data.token);
}

// 返回变量或方法
return { get, set, tokenIsNotEmpty, fetchAndSetToken };
})
<!-- 使用 -->
<template>
<div>
<button @click="fetchAndSetToken">Fetch and Set Token</button>
<p v-if="tokenIsEmpty">No token fetched yet.</p>
<p v-else>Current token: {{ csrfToken }}</p>
</div>
</template>

<script setup>
import { useCsrfTokenStore } from '@/stores/csrf';
const csrfTokenStore = useCsrfTokenStore();

// 直接使用 store 的 state 和 methods
const { csrfToken, tokenIsEmpty, fetchAndSetToken } = csrfTokenStore;
</script>

五、Vue网络

1 Vue结构

应用实例:每个Vue应用都是通过createApp函数创建一个新的应用实例。

根组件:我们传入createApp的对象实际上是一个组件,每个应用都需要一个”根组件”,其他组件将作为其子组件。

挂载应用:应用实例必须在调用了.mount()方法后才会渲染出来。该方法接收一个”容器”参数,可以是一个实际的DOM元素或是一个CSS选择器字符串。

src/assets:存放css,图片等资源。

<!-- App.vue -->
import { createApp } from 'vue'
// 挂载根组件,组件从App.vue开始
import App from './App.vue'

// app:Vue实例对象
// 在一个Vue项目中,有且只有一个Vue的实例对象
const app = createApp(App)

// 挂载应用
mount('#app')
<!-- 在index.html中 -->
<body>
<!-- 挂载对应的位置是此处,之后所有的页面都将放在这个位置 -->
<div id="app"></div>
<!-- vue代码最后都会被编译成main.js,然后引入main.js执行代码 -->
<script type="module" src="/src/main.js"></script>
</body>

2 main.js

vue代码最后都会被编译成main.js,然后引入main.js执行代码。

// main.js
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App);

// 卸载app中间
// 定义全局变量(名字是$serverIP,$符号不是必须的)
app.config.globalProperties.$serverIP = 'localhost:8000';

app.mount('#app')
<script>
// any.vue
export default {
created() {
// 使用全局变量
console.log(this.$serverIP);
}
};
</script>

3 axios

基于promise网络请求库,作用于node.js和浏览器中。

安装:npm i axios

文档:official::Axios中文文档

4 路由配置

安装:

  • npm i axios
  • npm i qs

路径创建:

  • /src/api/index.js:访问网页
  • /src/api/path.js:网络路径
  • /src/config/conster.js:本地网址管理
  • /src/utils/request.js:网络请求处理
// index.js
import axios from "../utils/request.js"
import path from "./path.js"

const api = {
getIndex(){
return axios.get(path.baseUrl + path.suffix);
}
}

// 导出api
export default api;
// path.js
// 一条路径
const base = {
baseUrl: "localhost:8000",
suffix: "/iAmIn.html"
}

// 导出base给别的js使用
export default base;
// conster.js
let module = {
// 开发阶段接口地址
dev: 'http://127.0.0.1:5173',
// 测试阶段接口地址
test: '',
// 发布阶段接口地址
pro: ''
}

export const BASE_URL = module.dev
// request.js
import axios from "axios"
import qs from 'qs';
import { BASE_URL } from '/src/config/conster.js'

// 超时设置
const instance = axios.create({
baseURL: BASE_URL,
timeout: 5000
})

// 常用场景:拦截器
// 发送拦截
instance.interceptors.request.use(
config => {
// post格式做额外处理
if(config.method === "post"){
config.data = qs.stringify(config.data);
}


// config:包含网络请求的所有信息
return config;
},
error => {
return Promise.reject(error);
}
)

// 详细错误显示
const errorHandle = (status,info) =>{
switch(status){
case 400:
console.log("语义有误");break;
case 401:
console.log("服务器认证失败");break;
case 403:
console.log("服务器拒绝访问");break;
case 404:
console.log("地址错误");break;
case 500:
console.log("服务器遇到意外");break;
case 502:
console.log("服务器无响应");break ;
default:
console.log(info);break;
}
}

// 获取拦截
instance.interceptors.response.use(
response => {
NProgress.done();
return response.status === 200 ? Promise.resolve(response) : Promise.reject(response);
},
error => {
NProgress.done();
// 解构赋值
const { response } = error;
// 主要关注错误处理
errorHandle(response.status, response.info);
}
)

export default instance;
<!-- 使用api -->
<script>
import api from "../api/index.js"
export default{
mounted(){
api.getIndex().then(res =>{
console.log(res.data);
})
}
}
</script>

5 跨域问题

JS采取的是同源策略。

同源策略是浏览器的一项安全策略,浏览器只允许js代码请求和当前所在服务器域名,端口,协议相同的数据接口上的数据,这就是同源策略。

也就是说,当协议、域名、端口任意一个不相同时,都会产生跨域问题,所以又应该如何解决跨域问题呢。

跨域常见异常:

  • Access to XMLHttpRequest at "url" from origin "url" has been blocked by CORS policy: No 'Access-Control-Allow-Origin’ header is present on the requested resource.
  • GETnet::ERR_FAILED 200
  • Uncaught (in promise)

后台跨域:cors

前台跨域:proxy

解决完跨域配置之后,要重启服务器才有用。

注:配置代理的时候'/api'如果直接写成'/',这样表示http://127.0.0.1:80/后面的路由都进行代理,这样会导致你加载本地资源会出错,因为你把加载本地资源的路径全部都代理到服务端去了,浏览器会向服务器进行资源请求,这样就会导致页面报错。

切记:本地同时前后端联合调试的时候,目标请求地址不能用 localhost,必须改成 127.0.0.1,否则会报错:http proxy error: Error: connect ECONNREFUSED ::1:8000

// 在vite.config.js的defineConfig后面添加下列代码,修改url即可
export default defineConfig({
plugins: [vue()],
// 在 vite.config.js 后面添加下列代码,修改url即可
server: {
https: true, // 开启https
proxy: {
// 配置需要代理的路径,http://<url>/api/后的所有路由都会被代理
// '/api'指的是以该名字开头的路径都会替换成下列url,可以路由配置的基网址可以写成'/api'
'/api': {
target: '<url>', // 服务器地址(协议,域名,端口(默认80))
changeOrigin: true, // 允许跨域
ws: true, // 允许websocket代理
rewrite: (path) => path.replace(/^\/api/, '') // 重写路径,将api替换成空
}
}
},
})

6 页面跳转

在Vue中,我们可以通过vue-router路由管理页面之间的关系。

Vue Router是Vue.js的官方路由。它与Vue.js核心深度集成,让用Vue.js构建单页应用变得轻而易举。

安装:npm i vue-router

路径创建:

  • /src/router/index.js:路由配置文件
  • /src/views/name.vue:路由对应的文件

histroy的参数:

  • createwebHashHistory():显示出的网址如下:home:http://localhost : 8080/#/,about:http://localhost:8080/# / about。原理是a标签锚点连接。
  • createwebHistory():显示出的网址如下:home:http://localhost:8080,about:http://localhost:8080/ about。此种方式,需要后台配合做重定向,否则会出现404问题。原理是H5的pushState()

createWebHashHistory使用URL的哈希部分(如example.com/#/path)作为路径,这种模式下,浏览器不会发送实际的请求,而是通过监听URL的变化来实现前端路由的跳转。这种模式的优点是兼容性好,甚至可以在不支持HTML5历史模式的浏览器中正常使用,但URL中的哈希部分可能不太美观。

createWebHistory使用真实的URL路径(如example.com/path),需要服务器的支持,通过HTML5的history API来实现前端路由的跳转。这种模式的优点是URL更加美观,没有哈希部分,但兼容性可能稍差,需要服务器的配置支持。

需要注意的是,createWebHistory只有在服务器配置正确的情况下才能正常工作,而createWebHashHistory则不需要服务器配置。因此,在选择使用哪种创建函数时,需要考虑项目的需求以及服务器的配置情况。

// index.js
import { createRouter, createWebHashHistory } from 'vue-router'
// 引入组件(一般只引入首页,其他写出函数引入)
import HomeView from '../views/index.vue'

const routes = [
{
// 路由路径
path: '/',
// 为路由命名
name: 'home',
// 对应组件
component: HomeView
},
{
path: '/about',
name: 'about',
// 直接绑定
component: () => import('../views/about.vue')
}
]

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

export default router;

<!-- App.vue -->
<template>
<!-- 设置路由跳转 -->
<router-link to="/">首页</router-link>
<router-link to="/about">关于</router-link>
<!-- 显示路由 -->
<router-view></router-view>
</template>
// main.js
import { createApp } from 'vue'
import App from './App.vue'

// 引入文件夹,使用use安装路由
import router from './router'
createApp(App).use(router).mount('#app')

7 路由带参

在路由中携带参数。常见业务是页面中存在列表项,然后点击每个列表项查看详情。

// 路由index.js
{
// :name表示参数,如果想带多个参数只需要继续斜杆冒号即可
path: "/list/:name[/:other...]",
name: "list",
component: () => import("../views/ListView.vue")
}
<!-- vue中带参 -->
<li><router-link to="/list/内蒙">内蒙旅游十大景区</router-link></1i>
<li><router-link to="/list/北京">北京旅游十大景区</router-link></li>
<li><router-link to="/list/四川">四川旅游十大景区</router-link></li>
<!-- 详细界面中使用参数 -->
<p> {{ $route.params.name}} 城市旅游景区详情</p>

8 路由嵌套

页面中继续有页面选项。

// 路由index.js
const routes = [
{
path: '/about',
name: 'about',
// 重定向,功能是用来显示默认第一个页面
redirect: "/about/us",
component: () => import("../views/AboutView.vue"),
children:[
{
// 二级导航不要加斜杠
path: "us",
component: () => import("../views/AboutSub/AboutUS.vue")
},
{
path: "info",
component: () => import("../views/AboutSub/AboutInfo.vu e")
}
]
}
]
<!-- vue中使用 -->
<template>
<div class="about">
<!-- 直接写路径即可 -->
<router-link to="/about/us">关于我们</router-link>
<router-link to="/about/info">关于信息</router-link>
<router-view></router-view>
</template>

六、Vue3

1 升级

六大亮点:

  • Performance:性能更比Vue 2.0强。
  • Tree shaking support:可以将无用模块”剪辑“,仅打包需要的。
  • composition API:组合API
  • Fragment, Teleport, Suspense:”碎片“,Teleport即Protal传送门,”悬念“
  • Better TypeScript support:更优秀的Ts支持(Vue3的底层是Ts)
  • Custom Renderer APl:暴露了自定义渲染API

2 组合式API

Vue3组合了API,在setup()中,可以定义数据、方法、props和context等。

在2.x中,组件的方法中可以通过this获取到当前组件的实例,并执行data变量的修改,方法的调用,组件的通信等等,但是在3.x中,setup()在beforeCreate和created时机就已调用,无法使用和2.x一样的this。但是可以通过接收setup(props,ctx)的方法,获取到当前组件的实例和props。总之,在setup中,this指向的是undefined

<template>
<div class="hello">
<p>{{ message }}</p>
<ul>
<li v-for="(item,index) in names.list" :key="index">{{ item }}</li>
</ul>
<button @click="clickHandle">按钮</button>
<p>{{ msg }}</p>
</div>
</template>

<script>
import { ref, reactive } from "vue"
export default {
name:'Helloworld',
// 定义props接收数据
props:{
msg:String
},
//组合式API(props传参,ctx表示this)
setup(props,ctx) {
// ref:定义简单数据
const message = ref("我是消息")
//reactive:定义复杂数据(数组、对象)
const names = reactive({
list: [ "yui", "mio" , "tsumugi"],
})

// 定义函数
function clickHandle(){
message.value="我是新的消息";
}

// props赋值
const msg = props.msg;

// 查看ctx的数据(包括attrs、emit、slots等)
console.log(ctx);

// 定义的数据需要返回出去才能被使用
return{
message,
names,
// 函数也需要返回
clickHandle,
msg
}
}
}
</script>
<!-- 直接将setup写到标签中,不用return -->
<template>
<div class="hello">
<p>{{ message }}</p>
<ul>
<li v-for="(item, index) in names.list" :key="index">{{ item }}</li>
</ul>
<button @click="clickHandle">按钮</button>
<p>{{ msg }}</p>
</div>
</template>

<script setup>
import { ref, reactive } from "vue"

// 获取props数据
const props = defineProps({
msg: String
})

// ref:定义简单数据
const message = ref("我是消息")

// reactive:定义复杂数据(数组、对象)
const names = reactive({
list: ["yui", "mio", "tsumugi"],
})

// 定义函数
function clickHandle() {
message.value = "我是新的消息"
}

// props赋值
const msg = props.msg
</script>

3 setup使用生命周期函数

Options API Hook inside setup
beforeCreate Not needed*
created Not needed*
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted
<template>
<div class="hello"></div>
</template>

<script setup>
import { onMounted } from "vue";

// 比以前有优势,以前同一个生命周期函数只能存在一个,现在可以存在多个
onMounted(() =>{ console.log("生命周期函数:onMounted1");});
onMounted(() =>{ console.log("生命周期函数:onMounted2");});

</script>

4 依赖注入

Provide / lnject

  • provide()inject()可以实现嵌套组件之间的数据传递。
  • 这两个函数只能在setup()函数中使用。
  • 父级组件中使用provide()函数向下传递数据。
  • 子级组件中使用inject()获取上层传递过来的数据。
  • 不限层级。
<!-- 父组件的script -->
<script setup>
import { provide } from "vue"

proivde("customVal", "我是父组件向子组件传递的值");
</script>

<!-- 子组件的script -->
<script setup>
import { inject } from "vue"

const customVal = inject("customVal");
</script>

七、element-plus

1 基础使用

css组件库

official::Element Plus (element-plus.org)

必须安装:npm i element-plus

按需引入额外安装:

  • npm i unplugin-vue-components
  • npm i unplugin-auto-import
// 全部组件引入
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 ')
// 部分组件引入(修改vite.config.js)
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()],
}),
],
})
<template>
<el-row class="mb-4">
<el-button>Default</el-button>
<el-button type="primary">Primary</el-button>
<el-button type="success">Success</el-button>
<el-button type="info">Info</el-button>
<el-button type="warning">Warning</el-button>
<el-button type="danger">Danger</el-button>
</el-row>
</template>

2 字体处理

字体使用需要额外安装。

安装:npm i @element-plus/icons-vue

创建文件:src/plugins/icons.js

// icons.js
import * as components from "@element-plus/icons-vue";
export default {
install: (app) => {
for (const key in components) {
const componentconfig = components[key];
app.component(componentconfig.name,componentconfig);
}
},
};
// main.js引用
import elementIcon from "./plugins/icons"
app.use(elementIcon)
<!-- 使用图标 -->
<el-icon class="expand" color="#409EFC" :size="30">
<expand />
</el-icon>

八、其他插件

1 顶端加载条

在顶端新增一个路由加载条,可以看见网页的加载进度。

安装:npm i nprogress -S

// 在/src/router/index.js中加入下列代码
// 顶端加载条
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'

router.beforeEach((to,from,next))=>{
NProgress.start();
next();
}

router.afterEach((to,from))=>{
NProgress.done();
}

九、其他问题

1 网络

Q1:GET http://127.0.0.1:5173/api/mainPage/login.html net::ERR_CONNECTION_REFUSED

A1:只能用localhost访问,否则修改package.json——scripts——dev——vite --host IP来新增IP。

2 访问类中元素

Q1:this.id无法访问到id值。

A1:

const menuItems = ref([
{
id: "1",
name: "首页",
router: this.id,
subMenu: null,
},
{
id: "2",
name: "消息",
router: this.id,
subMenu: null,
]);

3 无限发包

什么getterundefined

// 错误,要写一个函数
const value = computed(props.value)

// 正确
const value = computed(() => props.value)