前端框架Vue(总结)


创建项目

  1. 在命令行中执行命令
npm create vite@latest
✔ Project name: … vue3_vite4_study
✔ Select a framework: › Vue
✔ Select a variant: › JavaScript

Scaffolding project in /Users/laobai/TempProjects/vue3_vite4_study...

Done. Now run:

cd vue3_vite4_study
npm install
npm run dev
  1. 用WebStorm打开vue3_vite4_study目录

  2. 执行npm install安装

  3. 安装vue-router

    npm install --save vue-router
  4. 安装Pinia

    npm install --save pinia
  5. 安装axios

    npm install --save axios
  6. 在项目目录的src下新建views目录,创建Home.vue组件

    <script setup>
    
    </script>
    
    <template>
      <div>
        Home
      </div>
    </template>
    
    <style scoped>
    
    </style>
  7. 在项目目录的src下新建router目录,创建index.js路由文件

    import { createRouter,createWebHistory } from 'vue-router'
    
    import Home from '../views/Home.vue'
    
    const routes = [
        {
          name:'Home',
          path:'/home',
          component:Home
        },
        {
          path:'/',
          redirect:'/home'
        },
    ]
    
    const router = createRouter({
        history: createWebHistory(),
        routes,
    })
    
    export default router
  8. 修改main.js

    import { createApp } from 'vue'
    import App from './App.vue'
    import router from './router'
    import { createPinia } from 'pinia' // 导入pinia
    
    // const pinia = createPinia()
    const app = createApp(App)
    
    app.use(router) // 注册路由插件
    app.use(createPinia())  // 注册pinia状态插件
    app.mount("#app")
  9. 修改App.vue

    <template>
      <div>
        <router-view></router-view>
      </div>
    </template>
    
    <script setup>
    
    </script>
    
    <style>
    *{
      margin: 0;
      padding: 0;
    }
    </style>
  10. 执行npm run dev启动本地服务即可。

  11. 项目目录结构如下图所示:

选项式和组合式

选项式(Vue-Options-API,VOA),官方已不再推荐,支持Vue2和Vue3;

<template>
  <div @click="changeMsg">{{msg}}</div>
</template>

<script>
  import {ref, defineComponent} from "vue"
  export default defineComponent({
    setup() {
      const msg = ref("hello vue3");
      const changeMsg = ()=> {
        msg.value = "hello PLScript";
      }
      return {
        msg,
        changeMsg
      }
    }
  })
</script>

组合式(Vue-Composition-API,VCA),官方推荐的方式,仅支持Vue3;

组合式分两种写法:

  • setup()函数中写代码,然后进行return
  • 使用<script setup>语法糖来写;

更推荐<script setup>语法糖形式来写,以下及之后的所有示例均使用这种方式。

<template>
  <div @click="changeMsg">{{msg}}</div>
</template>

<script>
  export default{
    name:"App",
    data(){
      return{
        msg:"hello vue3"
      }
    },
    methods:{
      changeMsg(){
        this.msg = "hello AiLynn"
      }
    }
  }
</script>

Vue模板语法与v-指令

便于以下内容的测试,只进行一个文件的导入及访问路由配置,每次只更新导入的文件名及对应的path。

import { createRouter, createWebHistory } from 'vue-router'
import NotFound from '../views/NotFound.vue'
import Home from '../views/Home.vue'
import example from '../views/01/01.vue'

const routes = [
    {
      name:'Home',
      path:'/home',
      component:Home
    },
    {
        name:'Example'
        path:'/1-1',
        component:example
    },
    {
      path:'/',
      redirect:'/home'
    },
    {
        path:"/:pathMatch(.*)*",
        name:"NotFound",
        component: NotFound
    }
]

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

export default router

插值语法

双花括号

<script setup>
  let msg = 'PLScript'
</script>

<template>
  <div>文本插值:{{msg}}</div>
</template>

<style scoped>

</style>

引入ref实现响应式更新数据.(后续会详细复习响应式)

<script setup>
import { ref } from 'vue'
let msg = ref('PLScript')

setTimeout( ()=> {
  msg.value = 'AILynn'
},2000)

</script>

<template>
  <div>文本插值:{{ msg }}</div>
</template>

双花括号

<script setup>
import { ref } from 'vue'
let msg = ref('PLScript')

setTimeout( ()=> {
  msg.value = 'AILynn'
},2000)
</script>

<template>
  <div>文本插值:{{ msg }}</div>
  <div :class="Math.random()>0.5?'red':'blue'">红色还是蓝色</div>
</template>

<style scoped>
.red {
  color:red;
}

.blue {
  color:blue;
}
</style>

插入HTML代码 v-html

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

let text = ref('<h1>你好,PLScript</h1>')
</script>

<template>
  <div v-html="text"></div>
</template>

<style scoped>

</style>

属性绑定 v-bind

可以绑定单个属性,也可以绑定多个属性

属性绑定简写格式::属性='值'

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

let id = ref('AILynn')
let obj = ref({
  id:'PLScript',
  style:{'font-size':'20px'}
})
</script>

<template>
  <div v-bind:id="id">绑定ID</div>
  <div :id="id">简写绑定ID</div>
  <div v-bind="obj">绑定多个属性</div>
</template>

<style scoped>

</style>

调用函数

<script setup>

const getColor = ()=> {
  return Math.random()>0.5?'red':'blue'
}

</script>

<template>
  <div :class="getColor()">红色还是蓝色</div>
</template>

<style scoped>
.red {
  color:red;
}

.blue {
  color:blue;
}
</style>

事件监听 v-on

v-on监听绑定的事件,简写为@事件

<script setup>

const showLog = ()=>{
  console.log("show log")
}

</script>

<template>
  <button @click="showLog()">点我查看log</button>
</template>

<style scoped>

</style>

双向绑定 v-model

v-bind是单向绑定,数据只能从js流向html,即script->template

v-model是双向绑定,数据不但能从js流向html,还可以从html流向js,即script<->template

<script setup>

import {ref} from "vue";
let name = ref("")

</script>

<template>

  <input type="text" placeholder="请输入姓名" v-model="name">
  <h3>姓名:{{name}}</h3>

</template>

<style scoped>

</style>

v-model有三个修饰符:

.lazy:当脱离焦点后再进行同步

<input type="text" placeholder="请输入姓名" v-model.lazy="name">

.number:会将v-model绑定的数据转成number

<input type="text" placeholder="请输入姓名" v-model.number="name">

.trim:自动过滤用户的首尾的空白字符

<input type="text" placeholder="请输入姓名" v-model.trim="name">

条件渲染

v-if,后面可以跟v-elsev-else-if形成多条件分支。

v-show,与v-if用法相同,区别是v-if销毁创建的dom,而v-show只是更改display样式。即v-if的资源开销要比v-show要大。

所以,v-show适合频繁显示或隐藏的标签,而v-if不适合频繁的显示或隐藏的标签。

<script setup>

import {ref} from "vue";

let name = ref(true)
let age = ref(true)
// const showName = ref(true)

</script>

<template>
  <button @click="name=!name">show</button>
  <button @click="age=!age">if</button>
  <h3 v-show="name">PLScript</h3>
  <h3 v-if="age">18</h3>
</template>

<style scoped>

</style>

v-if后面跟着v-elsev-else-if可以实现分支效果

<script setup>

import {ref} from "vue";

let status = ref(true)

</script>

<template>
  <button @click="status=!status">吃饭/消化</button>
  <h3 v-if="status">吃饱了</h3>
  <h3 v-else>饿了</h3>
</template>

<style scoped>

</style>

循环遍历 v-for

v-for用于遍历列表或对象

语法格式:

  • v-for="item in items"
  • v-for="(item, index) in items"
  • v-for="(value, key) in object"
<script setup>
import {ref} from "vue";

let list = ref(["吃饭","睡觉","打豆豆"])
let obj = ref({
  name:'AILynn',
  age:18
})
</script>

<template>
  <li v-for="todo in list">{{todo}}</li>
  <div v-for="(todo,index) in list">{{ index }}-{{ todo }}</div>
  <div v-for="(value,key,index) in obj">{{ index }}-{{ key }}-{{ value }}</div>
</template>

<style scoped>

</style>

v-pre

通过v-pre修饰的元素,会进行原样输出,而不会按照Vue模板语法进行解析,比如{{js表达式}}依然显示为,既不会作为插值语法,也不会进行表达式的运算

<script setup>

import {ref} from "vue";

let name = ref("AILynn")

</script>

<template>
<h3 v-pre>{{name}}</h3>
</template>

v-once

v-once修饰的元素,只渲染一次

<script setup>

import {ref} from "vue";
let name = ref("AILynn")

</script>

<template>

  <input type="text" placeholder="请输入姓名" v-model.lazy="name">
  <h3 v-once>姓名:{{name}}</h3>

</template>

响应式

reactive

在Vue组合式API中,使用reactive()函数接收一个普通的对象,把对象数据转化为响应式对象并返回。响应式对象意味着当有数据发生变化时,会自动触发组件重新渲染。

<script setup>

import {ref,reactive} from "vue";

let name=ref('PLScript')

setTimeout(()=>{
  // name = 'AILynn'  // 这种写法不生效,需要.value才可以。
  name.value = 'AILynn'
},2000)

let person1 = ref({
  name:'PLScript',
  age:18
})
person1.value.age++

let person2 = reactive({
  name:'AILynn',
  age:20
})
person2.age++

</script>

<template>
  <h3>{{person1.name}}</h3>
  <h3>{{person1.age +1}}</h3>
  <h3>{{person2.name}}</h3>
  <h3>{{person2.age +1}}</h3>

</template>

ref

ref()是创建一个包装式对象,含有一个响应式属性value。它和reactive()的差别是,reactive()没有包装属性valueref()是在reactive()基础上进行的二次封装。reative()是将引用类型数据转换为响应式数据,而ref()不仅能够将引用类型数据转换为响应式数据,还可以将基本类型转换为响应式数据,并可以获取DOM元素。

JavaScript的基本数据类型:NumberStringBooleanNullUndefinedSymbol;引用数据类型:Object(除了基本数据类型以外的都是对象)

<script setup>

import {ref} from "vue";

let name=ref('PLScript')

setTimeout(()=>{
  // name = 'AILynn'  // 这种写法不生效,需要.value才可以。
  name.value = 'AILynn'
},2000)
</script>

<template>
  <h3>{{name}}</h3>

</template>

script中调用ref()调用的对象是需要.value的,而在template中就不需要.value,ref会自动解包(自动解包有个前提,只有顶级变量才可以自动解包)

<script setup>

import {ref} from "vue";

let name=ref('PLScript')

setTimeout(()=>{
  // name = 'AILynn'  // 这种写法不生效,需要.value才可以。
  name.value = 'AILynn'
},2000)

let person1 = ref({
  name:'PLScript',
  age:18
})
person1.value.age++

</script>

<template>
  <h3>{{person1.name}}</h3>
  <h3>{{person1.age +1}}</h3>

</template>

计算属性和方法

计算属性和方法的区别:

  • 计算属性在没有改变响应式变量的时候,只会计算一次,存入内存,下次取结果从内存中获取。当响应式变量发生改变,会再次触发重新计算这个结果。
  • 方法每次都会重新计算,会造成性能损耗;
  • 计算属性只是一个属性值,而方法调用时要加()括号。
<script setup>
import {computed} from "vue";

let a=10
let b=20

let sum = computed(()=>{
  return a+b
})

let getSum = ()=>{
  return a+b
}

</script>

<template>
  <h3>{{ sum }}</h3>
  <h3>{{ getSum() }}</h3>

</template>

事件处理

内联事件处理

内联事件处理就是在标签中直接写JS代码

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

let age = ref('18')

</script>

<template>
  <button v-on:click="age++">点我加1</button>
  <h3>年龄:{{age}}</h3>

</template>

方法事件处理

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

let age = ref('18')

const plClick = (index)=> {
  if (index===3) {
    console.log('PLScript')
  } else {
    console.log(index)
  }
}
</script>

<template>
  <button v-on:click="age++">点我加1</button>
  <h3>年龄:{{age}}</h3>

<!-- 传入参数的方法 -->
  <div v-for="(item, index) in 5" @click="plClick(index)">{{index}}</div>

</template>

<style scoped>

</style>

在内联事件处理器中访问事件参数

有时需要在内联处理器中访问原生DOM事件,可以向处理器方法传入一个特殊的$event变量或者使用内联箭头函数来实现。

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

let age = ref('18')

const plClick = (index)=> {
  if (index===3) {
    console.log('PLScript')
  } else {
    console.log(index)
  }
}

const plWarn = (message, event)=>{
  if (event) {
    event.preventDefault()
  }
  alert(message)
}
</script>

<template>
  <button v-on:click="age++">点我加1</button>
  <h3>年龄:{{age}}</h3>

<!-- 传入参数的方法 -->
  <div v-for="(item, index) in 5" @click="plClick(index)">{{index}}</div>

  <!-- 使用特殊的 $event 变量 -->
  <button @click="plWarn('Form cannot be submitted yet.', $event)">Submit</button>

  <!-- 使用内联箭头函数 -->
  <button @click="(event) => plWarn('Form cannot be submitted yet.', event)">Submit</button>

</template>

表单数据双向绑定

文本、多行文本、单选框、复选框及下拉选择器的双向绑定演示

<script setup xmlns="http://www.w3.org/1999/html">
import {ref} from "vue";

let f = ref({
  form: {
    hobby:"",
    hobbies:{},
    places:[],
    placesChecked:[false,false,false,false]
  }
})

let addOrRemove = (e,name) => {
  // 由于使用的是ref,所以访问对象属性需要加.value
  console.log(f.value)
  if (e.target.checked) {
    if (f.value.form.places.findIndex(e => e == name) == -1) {
      f.value.form.places.push(name)
    }
  } else {
    f.value.form.places.splice(f.value.form.places.findIndex(e => e == name),1)
  }
}

</script>

<template>

  <form method="post" action="http://localhost:8080">
<!--  文本  -->
    <p>账号名是:{{f.form.username}}</p>
    <input v-model="f.form.username" placeholder="请输入用户名">
<!--  多行文本 -->
    <br>
    <div style="height: 50px"></div>
    <p>个性签名是:{{f.form.sign}}</p>
    <textarea v-model="f.form.sign" placeholder="个性签名"></textarea>
<!--  单选框 -->
    <br>
    <div style="height: 50px"></div>
    <label for="checkbox">你最爱的是:{{f.form.hobby}}</label>

    《三国演义》
    <input type="radio" id="one" name="hobby" v-model="f.form.hobby" value="sanguo">
    《西游记》
    <input type="radio" id="two" name="hobby" v-model="f.form.hobby" value="xiyou">
    《水浒传》
    <input type="radio" id="three" name="hobby" v-model="f.form.hobby" value="shuihu">
    《红楼梦》
    <input type="radio" id="four" name="hobby" v-model="f.form.hobby" value="honglou">
<!-- 复选框 -->
    <br>
    <div style="height: 50px"></div>
    <label for="checkbox">你喜爱的是【多选】:{{f.form.hobbies}}</label>

    《三国演义》
    <input type="checkbox" id="one" name="sanguo" v-model="f.form.hobbies['sanguo']">
    《西游记》
    <input type="checkbox" id="two" name="xiyou" v-model="f.form.hobbies['xiyou']">
    《水浒传》
    <input type="checkbox" id="three" name="shuihu" v-model="f.form.hobbies['shuihu']">
    《红楼梦》
    <input type="checkbox" id="four" name="honglou" v-model="f.form.hobbies['honglou']">


    <br>
    <div style="height: 50px"></div>
    <label for="checkbox">你去过的地方是【多选】:{{f.form.places}}</label>

    北京
    <input type="checkbox" id="one" @change="addOrRemove($event,'北京')" v-model="f.form.placesChecked[0]">
    上海
    <input type="checkbox" id="two" @change="addOrRemove($event,'上海')" v-model="f.form.placesChecked[1]">
    天津
    <input type="checkbox" id="three" @change="addOrRemove($event,'天津')" v-model="f.form.placesChecked[2]">
    重庆
    <input type="checkbox" id="four" @change="addOrRemove($event,'重庆')" v-model="f.form.placesChecked[3]">

<!-- 下拉选择器 -->
    <br>
    <div style="height: 50px"></div>
    <div>Selected:{{f.form.selected}}</div>
    <select v-model="f.form.selected">
      <option disabled value="">请选择一个</option>
      <option>北京</option>
      <option>上海</option>
      <option>深圳</option>
    </select>

  </form>
  <button type="submit" @click.prevent="console.log(f.form)">提交</button>

</template>

生命周期

生命周期

组合式API

<script setup>

import {onBeforeMount, onBeforeUnmount, onBeforeUpdate, onMounted, onUnmounted, onUpdated, ref} from "vue";

let person = ref({
  name:'AILynn'
})

const modifyPersonName = ()=>{
  // 使用的是ref,所以需要访问person.value
  person.value.name='PLScript'
}

onBeforeMount(()=>{
  console.log('1.数据挂载之前触发,可以去后端查询数据,准备挂载')
})

onMounted(()=>{
  console.log('2.数据挂载之后触发')
})

onBeforeUpdate(()=>{
  console.log('3.数据修改之前触发')
})

onUpdated(()=>{
  console.log('4.数据修改之后触发')
})

onBeforeUnmount(()=>{
  console.log('5.数据卸载之前触发')
})

onUnmounted(()=>{
  console.log('6.数据卸载完成之后触发')
})

</script>

<template>
  <h3>{{person.name}}</h3>
  <button @click="modifyPersonName()">改名字</button>

</template>

<style scoped>

</style>

选项式API

<script>
export default {
  data(){
    return {
      name:'AILynn'
    }
  },
  methods:{
    modifyPersonName(){
      this.name='PLScript'
    }
  },
  beforeCreate() {
    console.log('1.页面创建之前触发')
  },
  created() {
    console.log('2.页面创建之后触发')
  },
  beforeMount() {
    console.log('3.数据挂载之前触发')
  },
  mounted() {
    console.log('4.数据挂载之后触发')
  },
  beforeUpdate() {
    console.log('5.数据修改之前触发')
  },
  updated() {
    console.log('6.数据修改之后触发')
  },
  beforeUnmount() {
    console.log('7.数据卸载之前触发')
  },
  unmounted() {
    console.log('8.数据卸载完成之后触发')
  }
}
</script>

<template>
  <h3>{{name}}</h3>
  <button @click="modifyPersonName">改名字</button>
</template>

监听器watch

当一个响应式的变量,值发生变化后,会触发监听器,从而可以根据这个值的变化,去做其他事件处理。

<script setup>

import {ref,watch} from "vue";

let person = ref({
  name:'AILynn',
  age:'16',
  marry:false
})

const modifyPersonAge = ()=>{
  // 使用的是ref,所以需要访问person.value
  person.value.age='22'
}

watch(()=> person.value.age, (newValue,oldValue)=>{
  console.log('person.age变化之后的值:',newValue)
  console.log('person.age变化之前的值:',oldValue)
  if(newValue >= 20){
    person.value.marry=true
  }
})

</script>

<template>
  <h3>{{person.name}}</h3>
  <button @click="modifyPersonAge()">长大了</button>
  <h3>{{person.name}}是否可以结婚:{{person.marry}}</h3>

</template>

组件

组件的定义

组件是Vue的一大特色,每个页面都是由一个或多个组件构成的。组件时可复用的Vue实例,可以把页面中在多个场景中重复使用的部分,抽出为一个组件进行复用。并且还可以给组件传递不同的数据和方法,让组件有不一样的效果。

定义组件的属性

新建组件demo1.vue

<script setup>

// 在子组件中可以使用defineProps声明该组件需要接收的props,
// 需要传递一个包含props字段的对象,每个字段表示该props的默认值和类型等信息。
const props = defineProps({
  // 只指定类型时
  // title:String
  // 即指定类型又指定默认值
  title:{
    type:String,
    default:'PLScript'
  }
})
</script>

<template>
  <h3>{{props.title}}</h3>
</template>

<style scoped>

</style>

在组件01.vue中引入demo01.vue,来实现在01页面上显示并修改demo01组件的内容

<script setup>
import cps1 from '../../components/demo01.vue'
import {ref} from "vue";

const a = ref({
  title:'除去巫山不是云'
})
</script>

<template>
  <h3>组件的使用</h3>
  <cps1></cps1>
  <cps1 :title="'曾经沧海难为水'"></cps1>
  <cps1 :title=a.title></cps1>
</template>

<style scoped>

</style>

demo01.vue

<script setup>

// 在子组件中可以使用defineProps声明该组件需要接收的props,
// 需要传递一个包含props字段的对象,每个字段表示该props的默认值和类型等信息。
const props = defineProps({
  // 只指定类型时
  // title:String
  // 即指定类型又指定默认值
  // 定义属性
  title:{
    type:String,
    default:'海纳百川'
  },
  // 定义对象
  user:{
    type:Object,
    default:()=>{
      return {
        name:'LaoBai',
        age:18
      }
    }
  },
  // 定义数组
  mobiles:{
    type:Array
  }
})
</script>

<template>
  <h3>{{props.title}}</h3>
  <h3>{{props.user.name}}已经{{props.user.age}}了,竟然是说{{props.title}}</h3>
  <h3 v-for="mobile in props.mobiles">{{mobile.brand}}-{{mobile.price}}</h3>
</template>

<style scoped>

</style>

01.vue

<script setup>
import cps1 from '../../components/demo01.vue'
import {ref} from "vue";

const a = ref({
  title:'除去巫山不是云'
})

const moblies=[{brand:'Apple',price:'5999'},{brand: 'SanSang',price:'7999'},{brand: 'HuaWei',price: '9999'}]

</script>

<template>
  <h3>组件的使用</h3>
  <cps1></cps1>
  <cps1 :title="'曾经沧海难为水'"></cps1>
  <cps1 :title=a.title></cps1>
  <cps1 :title="'生而无畏'" :user="{name:'AiLynn',age:28}"></cps1>
  <cps1 :mobiles="moblies"></cps1>
</template>

父子组件传参与插槽

父组件向子组件传递参数

如果组件A中使用组件B,那么A叫做B的父组件,B叫做A的子组件。

上面的例子中,01.vue是父组件,demo01.vue是01.vue的子组件。

父组件向子组件传递数据使用诸如下面的方式:

<cps1 :title="'曾经沧海难为水'"></cps1>
<cps1 :title=a.title></cps1>
<cps1 :title="'生而无畏'" :user="{name:'AiLynn',age:28}"></cps1>
<cps1 :mobiles="mobiles"></cps1>

子组件向父组件传递数据有两种方式:

1、事件发射;(推荐使用)

2、函数回调:父组件传一个回调函数给子组件,子组件要发送数据的时候,调用父组件的函数。

02.vue父组件

<script setup>

import Demo02Form from "../../components/demo02-form.vue";

// 获取子组件的form数据(配合事件发射方式)
const getFormData = (form)=> {
  console.log("子组件传递过来的数据",form)
}

// 获取子组件的form数据 (配合回调函数方式)
const getChildComponentData = (form)=> {
  console.log("子组件传递过来的数据,采用函数传递的方式",form)
}
</script>

<template>
  <h3>子组件传递参数到父组件</h3>
  <!-- 使用子组件默认值 -->
<!--  <demo02-form></demo02-form>   -->
  <!-- 在父组件重新给子组件赋值 -->
  <demo02-form :title1="'手机号'" :title2="'验证码'" :input-name="'phone'" :getData="getChildComponentData" @submitForm="getFormData"></demo02-form>

</template>

<style scoped>

</style>

demo02-form.vue子组件

<script setup>

import {ref} from "vue";

// 接收父组件传过来的值,使用defineProps进行接收
const props = defineProps({
  title1:{
    type:String,
    default:()=>{
      return '用户名'
    }
  },
  title2:{
    type:String,
    default:()=>{
      return '密码'
    }
  },
  inputName:{
    type:String,
    default:()=>{
      return 'userName'
    }
  },
  // 声明getData属性,用于子给父传递数据,配合回调函数方式
  getData:{
    type:Function,
    default: ()=> {
      return (form)=> {
        return { form }
      }
    }
  }
})

// 定义要从子组件返给父组件的事件集合,使用defineEmits (发射事件)
const emits = defineEmits(['submitForm'])

// 定义响应式变量formData,初始form为空。用于template中进行双向绑定
const formData = ref({
  form:{}
})

const submit = ()=> {
  console.log(formData.value.form)
  // 触发发射事件
  emits('submitForm',formData.value.form)
  // 把值返回给父组件的调用函数,即将form返回给父组件
  props.getData(formData.value.form)
}

</script>

<template>
  <div>
    {{props.title1}} <input :name="props.inputName" v-model="formData.form[props.inputName]">
  </div>
  <div>
    {{props.title2}} <input name="password" v-model="formData.form.password">
  </div>
  <button type="button" @click="submit">提交</button>
</template>

<style scoped>

</style>

动态组件

使用component标签可以渲染组件,is="组件名称"。(使用的组件,需要import)

<component :is="组件的名称"></component>

自定义属性的数据双向绑定

子组件修改数据后,父组件的数据自动同步改变。而且自定义任何的属性都可以。

父组件03.vue

<script setup>

import Demo03 from "../../components/demo03.vue";
import {ref} from "vue";
import Demo04Form from "../../components/demo04-form.vue";

let person = ref({
  name: 'AILynn',
  form:{}
})

// setInterval(()=>{
//   console.log(person.value.form)
// },2000)
</script>

<template>
  <h1>自定义属性数据双向绑定</h1>
  <demo03 v-model:name="person.name"></demo03>
  <demo04-form v-model:form="person.form"></demo04-form>
</template>

<style scoped>

</style>

子组件demo03.vue

<script setup>
  let props = defineProps(['name']) // 必须声明属性名称
  let emits = defineEmits(['update:name']) // 事件发射必须是upadate:属性名称

  let changeName = ()=>{
    emits('update:name','laobai')
  }
</script>

<template>
  <h1>{{ props.name }}</h1>
  <button @click="changeName">点击改名字</button>
</template>

<style scoped>

</style>

子组件demo04-form.vue

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

  let props = defineProps(['form'])
  let emits =defineEmits(['update:form']) // 事件发射必须是upadate:属性名称

  // 子绑父
  // 方法一:事件监听watch
  // 监听props的属性改变,赋值给当前的formData.value.form
  // watch(()=> props.form, (newValue,oldValue)=>{
  //   console.log('新的值:',newValue)
  //   console.log('旧的值:',oldValue)
  //   formData.value.form=props.form
  // })

  // 方法二:声明周期 (推荐)
  onMounted(()=>{
    console.log('从父组件传过来的值:',props.form)
    formData.value.form = props.form
  })

  let formData = ref({
    form:{}
  })
</script>

<template>
  <div>
    用户名:<input type="text" name="userName" v-model="formData.form.userName">
  </div>
  <div>
    密码:<input type="password" name="password" v-model="formData.form.password">
  </div>
  <button type="button" @click="emits('update:form',formData.form)">提交</button>
</template>

<style scoped>

</style>

slot插槽

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

没有指明name的插槽为默认插槽,指定了name的插槽为具名插槽。若默认插槽有多个,同一个组件会插入多次。具名插槽在引用时需要再template中指定所引用的插槽名称<template #插槽名称>,插槽名称前用#

子组件demo05-slot.vue

<script setup>

</script>

<template>
  <div>
    <slot name="header"></slot>
    <h1>以上是header</h1>
    <div style="border: solid red">
      <slot name="content"></slot>
    </div>
  </div>
  <h1>以下是footer</h1>
  <div style="border: solid green">
    <slot name="footer"></slot>
  </div>
</template>

<style scoped>

</style>

父组件04.vue

<script setup>

import Demo05Slot from "../../components/demo05-slot.vue";
</script>

<template>

  <demo05-slot>
    <template #header>
      <h1>在header中插入了内容</h1>
    </template>
    <template #content>
      <h1>在content中插入了内容</h1>
    </template>
    <template #footer>
      <h1>在footer中插入了内容</h1>
    </template>
  </demo05-slot>
</template>

<style scoped>

</style>

在子组件中定义属性内容,绑定到某个插槽上,父组件引用的时候可以同步展示

子组件中使用<slot :user="data.user" name="header"></slot>来绑定到插槽

父组件在使用时使用v-slot='user'来获取。需要注意的是,v-slot是来获取没有名字的默认插槽的,具名插槽则直接在名称后面直接获取<template #header="user">

子组件demo05-slot.vue

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

  let data =ref({
    user:{
      name:'AILynn',
      age:18,
      phone:8006668888
    }
  })
</script>

<template>
  <div>
    <slot :user="data.user" name="header"></slot>
    <h1>以上是header</h1>
    <div style="border: solid red">
      <slot :age="data.user.age" name="content"></slot>
    </div>
  </div>
  <h1>以下是footer</h1>
  <div style="border: solid green">
    <slot name="footer"></slot>
  </div>
</template>

<style scoped>

</style>

父组件04.vue

<script setup>

import Demo05Slot from "../../components/demo05-slot.vue";
</script>

<template>

  <demo05-slot>
    <template #header="user">
      <h1>在header中插入了内容{{ user }}</h1>
    </template>
    <template #content="age">
      <h1>在content中插入了内容{{ age }}</h1>
    </template>
    <template #footer>
      <h1>在footer中插入了内容</h1>
    </template>
  </demo05-slot>
</template>

<style scoped>

</style>

provide提供和inject注入

上面举的例子都是父子间的数据传输,若涉及到爷孙等跨多级的数据传输可以使用provide在最外层的组件中进行声明,使用inject在内层的组件中进行使用。

provide

<script setup>
import { provide } from 'vue'
provide('message','hello!') // 两个参数:名称,值
</script>

inject

 <script setup>
import { inject } from 'vue'
const message = inject('message')
</script>

Vue-Router路由

Vue Routervue.js的官方路由,在本小节开始的部分<创建项目>中已经进行了安装。

创建路由文件和main.js使用路由

在项目的src目录下新建router目录,在router目录下创建index.js路由文件,主要是配置路径及对应的页面。

import { createRouter,createWebHistory } from 'vue-router'

// 导入需要用到的组件
import NotFound from '../views/NotFound.vue'
import example from '../views/03/04.vue'

// 配置组件路由
const routes = [
    {
        name:'Example',  // 路由名称
        path:'/3-4',  // 路径
        component:example   // 对应的组件
    },
    {
      name:'Home',
      path:'/home',
      component: ()=> import('../views/Home.vue')  // 懒加载的方式导入组件,即用到时才导入。推荐使用
    },
    {
      path:'/',	// 根节点
      redirect:'/home'
    },
    {
        path:"/:pathMatch(.*)*", 	// 未配置的路径
        name:"NotFound",
        component: NotFound
    }
]

const router = createRouter({
    // 使用基于浏览器history API的路由模式 (正常的URL,无#)
    history: createWebHistory(),
    routes,
})

export default router

在main.js中导入路由

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

const app = createApp(App)

app.use(router) // 注册路由插件

app.mount("#app")

router-view表示哪个地方需要显示其他子页面

router-link表示要跳转到哪个页面,类似a标签

修改App.vue

<template>
  <div style="display: flex">
    <div style="border: solid red">
      <div>
        <router-link to="3-1">组件的定义与使用</router-link>
      </div>
      <div>
        <router-link to="3-2">父子组件间的传递</router-link>
      </div>
    </div>
    <div style="border: solid green;flex: 1">
      <router-view></router-view>
    </div>
  </div>

</template>

其中,<router-view>应用在需要显示子页面的页面中,而<router-link>可以用于任何需要进行页面跳转的组件中,to=指向要跳转的页面。

在路由文件index.js中新增如下代码:

{path: '/3-1', component:()=> import('../views/03/01.vue')},
{path: '/3-2', component:()=> import('../views/03/02.vue')},

路径传参和query传参

useRouteuseRouter

// useRoute通常是用来接收路径传参、查询参数、判断当前的页面路由path等
let route = useRoute();

// useRouter可实现编程式(JS代码中通过router.push()方法完成页面跳转)导航
let router = useRouter();

01.vue

<script setup>

import {useRoute, useRouter} from "vue-router";

// useRoute通常是用来接收路径传参、查询参数、判断当前的页面路由path等
let route = useRoute();
console.log('route',route)
console.log('路径参数',route.params.userId)
console.log('查询参数',route.query)

// useRouter可实现编程式(JS代码中通过router.push()方法完成页面跳转)导航
let router = useRouter();
console.log('router',router)
</script>

<template>
  <h1>这是路径传参+query传参的例子</h1>
</template>

App.vue

<template>
  <div style="display: flex">
    <div style="border: solid red">
      <div>
        <router-link to="/3-1">组件的定义与使用</router-link>
      </div>
      <div>
        <router-link to="/3-2">父子组件间的传递</router-link>
      </div>
      <div>
        <router-link to="/4-1/996">路径传参</router-link>
      </div>
      <div>
        <button @click="router.push({path:'/4-1/996',query:{name:'laobai',age:18}})">路径传参+query传参</button>
      </div>
    </div>
    <div style="border: solid green;flex: 1">
      <router-view></router-view>
    </div>
  </div>

</template>

<script setup>
import router from "./router/index.js";

</script>

<style>
*{
  margin: 0;
  padding: 0;
}
</style>

router/index.js

{path: '/4-1/:userId', component:()=> import('../views/04/01.vue')},

嵌套路由

在上面的例子的基础上,增加一个页面,并在这个页面文件的同级目录新建一个目录存放孙页面

02.vue

<script setup>

</script>

<template>
  <h1>这是子路由</h1>
  <div>
    <router-link to="/4-2-1">子页面1</router-link>
    <router-link to="/4-2-2" style="margin-left: 20px">子页面2</router-link>
  </div>
  <div style="border: solid orange">
    <router-view/>
  </div>
</template>

<style scoped>

</style>

在02.vue同级目录新建子页面目录,在子页面目录中新建01.vue和02.vue两个孙页面

<script setup>

</script>

<template>
  <h1>内嵌子页面1</h1>
</template>

<style scoped>

</style>

配置路由文件index.js,在4-2路由的内部增加childern子路由配置(可根据项目的实际情况增加多级子路由)。

{path: '/3-1', component:()=> import('../views/03/01.vue')},
    {path: '/3-2', component:()=> import('../views/03/02.vue')},
    {path: '/3-3', component:()=> import('../views/03/03.vue')},
    {path: '/4-1/:userId', component:()=> import('../views/04/01.vue')},
    {
        path: '/4-2',
        component:()=> import('../views/04/02.vue'),
        children:[
            {path: '/4-2-1',component:()=> import('../views/04/子页面/01.vue')},
            {path: '/4-2-2',component:()=> import('../views/04/子页面/02.vue')},
        ]
    },

页面预览效果如下:

路由全局前置、后置守卫

路由的前置后置守卫就是在跳转之前、之后可以做一些判断或处理

这里新建一个03.vue,个人中心页面,需要登录后才能访问。实现这个功能,这里使用router.beforeEach()跳转之前拦截来实现。(全局拦截 beforeEach每次路由都进行拦截,to是要跳转的页面,from是从哪个页面跳过来的,next不执行就不会继续执行)

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

主要代码在路由文件index.js中

import { createRouter,createWebHistory } from 'vue-router'

import NotFound from '../views/NotFound.vue'
import Home from '../views/Home.vue'
import example from '../views/03/04.vue'

const routes = [
    {
      name:'Login',
      path:'/login',
      component:Login
    },
    {
      name:'Home',
      path:'/home',
      component: ()=> import('../views/Home.vue')
    },
    {path: '/3-1', component:()=> import('../views/03/01.vue')},
    {path: '/3-2', component:()=> import('../views/03/02.vue')},
    {path: '/3-3', component:()=> import('../views/03/03.vue')},
    {path: '/4-1/:userId', component:()=> import('../views/04/01.vue')},
    {
        path: '/4-2',
        component:()=> import('../views/04/02.vue'),
        children:[
            {path: '/4-2-1',component:()=> import('../views/04/子页面/01.vue')},
            {path: '/4-2-2',component:()=> import('../views/04/子页面/02.vue')},
        ]
    },
    {path: '/4-3', component:()=> import('../views/04/03.vue')},
    {path: '/login', component:()=> import('../views/04/login.vue')},

    {
        name:'Example',
        path:'/3-4',
        component:example
    },
    {
      path:'/',
      redirect:'/home'
    },
    {
        path:"/:pathMatch(.*)*",
        name:"NotFound",
        component: NotFound
    }
]

const router = createRouter({
    // 使用基于浏览器history API的路由模式 (正常的URL,无#)
    history: createWebHistory(),
    routes,
})

//  beforeEach每次路由都进行拦截,to是要跳转的页面,from是从哪个页面跳过来的,next不执行就不会继续执行
router.beforeEach((to, from, next)=>{
    if(to.path==='/4-3') {  // 判断跳转的页面是否为/4-3 (这里判断的是路径,若路由进行了命名,也可以通过名称to.name进行判断)
        next({path:'/login'})   // 是的话,跳转到登录页面,否则跳到else语句
    }else next();   // next() 什么都不做,继续执行后面的语句
})

export default router

另,可以在路由配置中对单个路由添加元信息(meta:{})设置一些状态

{
      path:"/4-4",
      component: ()=> import('../views/04/04.vue'),
      meta:{
          requiredAuth:true,    // 需要进行授权
          keepalive:true        // 需要缓存存活状态
      }
}

这样配置后,上面对于是否需要登录的判断可以使用元信息中的requiredAuth,例如:

router.beforeEach((to, from, next) =>{

  let isAuthenticated = localStorage.getItem("token")
  // 判断当前页面不是Login,并localStorage中不含有token,且meta的requiredAuth为True,跳转到登录页
  if(to.name !== 'Login' && !isAuthenticated && to.meta.requiredAuth) next ({name:'Login'})
  // 否则继续执行操作
  else next()
})

实例:滚动条记录滚动位置

App.vue中的router-view需要调整

<div style="border: solid green;flex: 1">
  <router-view></router-view>
</div>

改为:

<div style="border: solid green;flex: 1">
      <router-view v-slot="{Component}">
        <keep-alive>
          <component :is="Component" :key="route.path" v-if="route.meta.keepalive"/>
        </keep-alive>
        <component :is="Component" :key="route.path" v-if="!route.meta.keepalive"/>
      </router-view>
    </div>

动态组件来判断访问的组件,判断路由中元信息的keepalive是否为true,true即进行状态保存,false不加任何处理继续执行。

新增05.vue示例滚动条页面

<script setup>
import {onActivated, ref} from "vue";
import {onBeforeRouteLeave} from "vue-router";

let scroll=ref(null)  // 滚动条的初始化位置值
let scrollTop=ref(0)  // 滚动条滚动后的位置值

onActivated(()=>{ // 再次进入组件时要做的处理
  scroll.value.scrollTop = scrollTop.value
})

onBeforeRouteLeave(()=>{  // 离开这个组件时要做的处理
  scrollTop.value = scroll.value.scrollTop
})
</script>

<template>
  <div ref="scroll" style="background: rosybrown;height: 500px;overflow-y: auto;border: solid red">
    <h1 v-for="i in 100">{{i}}</h1>
  </div>
</template>

<style scoped>

</style>

两点需要注意:

  • 在需要定节点上增加了ref="scroll",ref若绑定在dom节点上,得到的就是原生dom节点;ref若绑定在组件上,得到的就是组件对象;
  • 应用到了组件内的两个钩子函数onBeforeRouteLeave()来实现离开组件时保存位置和onActivated()来实现加载组件时赋值已经保存的位置信息

在路由文件index.js中增加新建页面的路由

{
        path:"/4-5",
        component: ()=> import('../views/04/05.vue'),
        meta:{
            keepalive:true        // 需要缓存存活状态
        }
    }

Pinia状态管理

Pinia是Vue的专属状态管理库,它允许跨组件或页面共享状态。所谓状态管理,是把所有需要用到的相同响应式变量的组件的变量,存到内存中,然后在每个需要用到的组件里边使用,形成一个全局共享变量。

Vue2的状态管理使用Vuex,Vue3在继续支持Vuex的同时,还支持新开发了Pinia,当然Pinia也支持Vue2。

Pinia起始于2019年11月,Pinia其实就是Vuex的升级版本。根据官网的描述,Pinia将取代Vuex,即Pinia=Vuex5。

Pinia在本小节开始的部分<创建项目>中已经进行了安装。

在main.js中进行注册使用

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { createPinia } from 'pinia' // 导入pinia

const app = createApp(App)

app.use(router) // 注册路由插件
app.use(createPinia())  // 注册pinia状态插件
app.mount("#app")

在项目的src目录下创建stores目录,这个目录中存放全部的状态js文件。新建购物车shopCart.js

import {defineStore} from "pinia";
import {computed, ref} from "vue";

// 通过defineStore()来定义状态
export const useShopCart = defineStore('shopCart', ()=>{
    // 默认的购物车商品数量0
    const count = ref(0);
    // 定义商品的统一价格为10元,下面计算商品的总价格
    const allPrice = computed(()=> count.value * 10);

    // 增加商品
    function increment(){
        count.value++
    }

    // 减少商品
    function reduce(){
        count.value--
    }

    return {count, increment,reduce}
})

新建页面01.vue

<script setup>

import Pinia01 from "../../components/status/pinia01.vue";
import Pinia02 from "../../components/status/pinia02.vue";
import Pinia03 from "../../components/status/pinia03.vue";
</script>

<template>
  <h1>Pinia状态管理</h1>
  <Pinia01/>
  <Pinia02/>
  <Pinia03/>
</template>

<style scoped>

</style>

在src目录下的component目录中新建status目录,存放状态相关组件。新建三个组件pinia01.vue

<script setup>
import {useShopCart} from "../../stores/shopCart.js";

let shopCart = useShopCart() // 使用购物车状态存储
</script>

<template>
  <h1>组件1-购物车中商品的数量:{{shopCart.count}}</h1>
  <button @click="shopCart.increment()">增加商品</button>
  <button @click="shopCart.reduce()">减少商品</button>
</template>

<style scoped>

</style>

最终预览效果如下:

Axios

使用Axios整合Vue来实现请求数据。(之前是用原生的ajax来请求数据,后面用jQuery的ajax,而在前后端分离的项目中,使用Axios请求数据)

Axios在本小节开始的部分<创建项目>中已经进行了安装。

Axios特性

  • 从浏览器中创建XMLHttpRequests
  • 从node.js创建http请求
  • 支持Promise API (链式函数)
  • 拦截请求和响应
  • 转换请求数据和响应数据
  • 取消请求
  • 自动转换JSON数据
  • 客户端支持防御XSRF(跨站请求伪造)

发送Get和Post请求

用flask写一个api演示请求接口

import json

from flask import Flask, request, make_response
from flask_cors import cross_origin

app = Flask(__name__)

@app.route('/get')
def pl_get():
    name = request.args.get("name")
    age = request.args.get("age")
    response = make_response({"name": name, "age": age})
    response.headers["Access-Control-Allow-Origin"] = "*"

    return response


@app.route("/post", methods=["post"])
@cross_origin()
def pl_post():
    request_data = json.loads(request.data)
    name = request_data.get("name")
    age = request_data.get("age")
    response = make_response({"name": name, "age": age})
    return response


if __name__ == '__main__':
    app.run()

在新建的01.vue实现Get和Post请求

<script setup>
import axios from "axios";

let plGet = ()=> {
  axios.get('http://127.0.0.1:5000/get?name=laobai&age=18').then(function (response) {
    console.log(response);
  })
}

let plPost = ()=> {
  axios.post('http://127.0.0.1:5000/post',{
    name:"laobai",
    age:18
  }).then(function (response) {
    console.log(response);
  })
}
</script>

<template>
  <h1>Axios的例子</h1>
  <button @click="plGet">Get请求</button>
  <button @click="plPost">Post请求</button>
</template>

发送结果预览如下:

通过config参数发送请求

可以将url、method、data、parms等请求信息都放在axios方法中发送请求

<script setup>
import axios from "axios";
import {http} from "../../utils/RequestUtils.js";

let plGet = ()=> {
  axios.get('http://127.0.0.1:5000/get?name=laobai&age=18').then(function (response) {
    console.log(response);
  })
}

let plPost = ()=> {
  axios.post('http://127.0.0.1:5000/post',{
    name:"laobai",
    age:18
  }).then(function (response) {
    console.log(response);
  })
}

let reqByConf = ()=> {
  axios({
    method:'POST',
    url:'http://127.0.0.1:5000/post',
    data:{
      name:"laobai",
      age:18
    }
  }).then(response => {
    console.log(response)
  })
}

</script>

<template>
  <h1>Axios的例子</h1>
  <button @click="plGet">Get请求</button>
  <button @click="plPost">Post请求</button>
  <button @click="reqByConf">通过config参数直接发送请求</button>
</template>

Axios的二次封装

axios提供了axios.create()方法,可以进行axios的二次封装,可以对请求URL、超时时间等通用内容进行封装,可以将不同可归类的内容分别封装成多个新的axios。

RequestUtils.js

import axios from "axios";

export const plAxios = axios.create({
    baseURL:"http://localhost:5000",
    timeout:5000
})

01.vue

<script setup>
import axios from "axios";
import {plAxios} from "../../utils/RequestUtils.js";

let plGet = ()=> {
  axios.get('http://127.0.0.1:5000/get?name=laobai&age=18').then(function (response) {
    console.log(response);
  })
}

let plPost = ()=> {
  axios.post('http://127.0.0.1:5000/post',{
    name:"laobai",
    age:18
  }).then(function (response) {
    console.log(response);
  })
}

let reqByConf = ()=> {
  axios({
    method:'POST',
    url:'http://127.0.0.1:5000/post',
    data:{
      name:"laobai",
      age:18
    }
  }).then(response => {
    console.log(response)
  })
}

let reqByHC = ()=> {
  plAxios.post('/post',{
    name:"laobai",
    age:18
  }).then(response => {
    console.log(response)
  })
}
</script>

<template>
  <h1>Axios的例子</h1>
  <button @click="plGet">Get请求</button>
  <button @click="plPost">Post请求</button>
  <button @click="reqByConf">通过config参数直接发送请求</button>
  <button @click="reqByHC">使用自定义的Axios完成请求发送</button>
</template>

请求拦截器和响应拦截器

请求拦截器:发生在请求之前,一般被用来统一添加token等内容。处理完成后,需要return放行继续执行;

响应拦截器:对响应数据进行处理,如提取请求数据中的某个字段值等;

import {plAxios} from "../../utils/RequestUtils.js";

plAxios.interceptors.request.use(config => {
  console.log("请求拦截器")  // 发生在请求之前,一般被用来统一添加token等内容
  return config   // 对请求信息处理完成后进行放行
}, err => {
  console.log("发生错误了")
  return Promise.reject(err)
})

plAxios.interceptors.response.use(config => {
  console.log("响应拦截器")  // 对响应数据进行处理

}, err => {
  console.log("发生错误了")
  return Promise.reject(err)
})

文章作者: 老百
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 老百 !
 上一篇
Django基础介绍 Django基础介绍
Django是Python Web开发的主流框架,它鼓励快速开发和简洁、实用的设计,旨在帮助开发人员尽快将应用程序从概念到完成。
2023-10-15
下一篇 
前端框架Vue(七) 前端框架Vue(七)
Vue 是一款用于构建用户界面的 JavaScript 框架。本小节介绍Pinia,Pinia是Vue的专属状态管理库,它允许跨组件或页面共享状态,对组合式API有更好的支持。
2023-09-07
  目录