Vue

45

Vue的API书写风格有两种,一种是选项式API,一种是组合式API

npm create vue@latest

这条命令可以初始化一个vue项目


基础语法

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>

  <div id="app">
    用户名:<p>{{ username }}</p>
    年龄:<p>{{ age }}</p>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
  <script>
    const vm = new Vue({
      el: '#app',
      data() {
        return {
          username: '张三',
          age: 18
        }
      }
    }); 
  </script>
</body>

</html>

这样可以动态输入数据

   <p>{{1 > 2 ? 'yes' : 'no'}}</p>

在花括号中还可以写逻辑运算符

  <script>
    const vm = new Vue({
      el: '#app',
      data() {
        return {
          title: "hello vue"
        }
      },
      methods: {
        output() {
          return "title is " + this.title
        }
      }
    }); 
  </script>
<h1>{{ output() }}</h1>

可以在methods中写各种方法

      computed: {
        outputContent() {
          console.log("computed");
          return "title is " + this.title
        }
      }

当然还有一个计算属性。如果要打印多个的话,用{{ output() }} 就会重新获取,如果用 {{ outputContent }} 的话每次调用就会用缓存的数据(它具有缓存属性),可以提高性能

还有一个属性监听器watch :

      watch: {
        title(newValue, oldValue) {
          console.log("watch", newValue, oldValue);
        }
      }

指令

    <p v-text="title"></p>

也可以通过v-text 来向花括号那样插值,用法是一样的

<p v-html="htmlContent"></p>

v-html 可以向标签内覆写html并渲染出来

<p v-for="item in 5">Content</p>

v-for 可以循环输出多个标签

<p v-for="item in arr">Content:{{ item }}</p>

可以把数组中的内容循环输出

   <p v-for="(item,key,index) in obj">Content:{{ item }}{{ key }}{{ index }}</p>
obj:{a:1,b:2,c:3}

遍历对象

<p v-if="true">标签内容</p>
<p v-if="false">标签内容</p>

v-if 可以根据真假来控制div的显示与不显示,不显示就会销毁,当然也可以用v-show来控制显示,但它控制的只是display的样式

v-for建议写上key属性

    <p v-bind:title="title">1</p>
    <p :title="title">1</p>

事件指令

    <button v-on:click="title = 'hello button'">按钮</button>
    <button v-on:click="output">按钮</button>
    <button @click="output">按钮</button>

表单指令

v-model 可以双向数据绑定

    <input type="text" v-model="inputValue">
    <p v-text="inputValue"></p>
      data() {
        return {
          inputValue: ""
        }

这样更改输入框的内容文字标签内的也会跟着动态改变

<select v-model.number="n">

转成数字

修饰符

<input type="text" v-model.trim="inputValue">

.trim 可以去除输入框两端的空格


入口

main.ts:

import './assets/main.css'

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

createApp(App).mount('#app')

这个入口文件就初始化了App到index.html的#app上

App.vue 中,在<template>标签中写页面的结构(HTML)<script>中写js或ts,<style>标签中写样式

App.vue

<template>
  <div class="app">
    <h1>Hello World</h1>
  </div>
</template>

<script lang="ts">
export default {
  name: 'App',
}
</script>

<style>
.app {
  background-color: #ddd;
  box-shadow: 0 0 10px;
  border-radius: 10px;
  padding: 20px;
}
</style>

我们可以在components 中写其他页面,比如Person.vue:

<template>
    <div class="person">
        <h2>姓名:{{ name }}</h2>
        <p>年龄:{{ age }}</p>
        <button @click="showTel">查看联系方式</button>
    </div>
</template>

<script lang="ts">
export default {
    name: 'Person',
    data() {
        return {
            name: 'John Doe',
            age: 30,
            telephone: '123-456-7890'
        }
    },
    methods: {
        showTel() {
            alert(`电话:${this.telephone}`);
        }
    }
}
</script>

<style scoped>
.person {
    background-color: skyblue;
    border: 1px solid #ccc;
    border-radius: 5px;
    padding: 10px;
}
</style>

style里面的scoped是让这个样式只在这个vue文件里面生效的

但是我们还需要让App.vue认识Person.vue

import Person from './components/Person.vue'

export default {
  name: 'App', //组件名
  components:{Person} //注册组件
}

然后我们使用<Person/> 就可以让那个页面出现在页面里了

组件的通信方式

<template>
  <div class="app">
    <Connect msg="Hello from App.vue" count="5" />
  </div>
</template>

<script lang="ts">
import Connect from './components/Connect.vue'


export default {
  name: 'App', //组件名
  components: { Connect } //注册组件
}
</script>

<style>
.app {
  background-color: #ddd;
  box-shadow: 0 0 10px;
  border-radius: 10px;
  padding: 20px;
}
</style>
<template>
    <p>{{ msg }}</p>
    <p>Count: {{ count }}</p>
</template>

<script lang="ts">
export default {
    name: 'Connect',
    props: {
        msg: String,
        count: {
            type: [Number, String],
            default: 100,
            required: true
        }
    },
    data() {
        return {
            name: 'John Doe'
        }
    }
}
</script>

我门可以通过属性来传值,然后通过props来接收,还可以规定接收值的类型,也可以用default 来设置默认值。可以用required 来规定这个数据是否是必须的。

这是父传子的方式,当然子组件也可以传给父组件,我们就需要用.emit 设置一个自定义触发事件

<template>
  <div class="app">
    <Connect msg="Hello from App.vue" count="5" @child-count-change="handler" />
    <p>{{ childData }}</p>
  </div>
</template>

<script lang="ts">
import Connect from './components/Connect.vue'


export default {
  name: 'App', //组件名
  components: { Connect }, //注册组件
  data() {
    return {
      childData: 0
    };
  },
  methods: {
    handler(childCount: any) {
      console.log('子组件的count值变化了', childCount)
      this.childData = childCount
    }
  }
}
</script>

<style>
.app {
  background-color: #ddd;
  box-shadow: 0 0 10px;
  border-radius: 10px;
  padding: 20px;
}
</style>
<template>
    <p>{{ msg }}</p>
    <p>Count: {{ count }}</p>
    <button @click="handler"></button>
</template>

<script lang="ts">
export default {
    name: 'Connect',
    props: {
        msg: String,
        count: {
            type: [Number, String],
            default: 100,
            required: true
        }
    },
    data() {
        return {
            childCount:0
        }
    },
    methods: {
        handler() {
            this.childCount++
            this.$emit('child-count-change',this.childCount)
        }
    }
}
</script>

这样{{ childData }} 就可以获取组件里面传过来的数据了。@child-count-change 可以对child-count-change 进行监听,监听到数据后会执行这个属性内的方法并把值传进去

组合式API

刚才我们写的都是选项式API

export default {
    name: 'Person',
    setup() {
        //数据
        let name = '张三';
        let age = 18;
        let tel = '12345678901';

        //方法
        function changeName() {
            name = '李四';
        }

        function changeAge() {
            age += 1;
        }

        function showTel() {
            alert(`电话:${tel}`);
        }

        return { name, age, tel, changeName, changeAge, showTel }
    }
}

我们可以把变量和方法都写进setup() ,但是这里面的变量还不是响应式的。所有变量和方法都要用return返回。我们也可以用return () => a 来返回一个简单的数据。但是这样每写一个就要在return里面写一个,十分麻烦

<script lang="ts">
export default {
    name: 'Person'
}
</script>
<script lang="ts" setup>
let a = 10;
</script>

我们把代码写在有setup后缀的script标签里就可以不用return就直接使用

<script lang="ts" setup name="Person222">

可以在这个标签里写上name属性,这样就不需要export那个代码来了

我们现在需要把setup里面数据变为响应式的

import { ref } from 'vue';

//数据
let name = ref('张三');
let age = ref(18);
let tel = '12345678901';

//方法
function changeName() {
    name.value = '李四';
}

function changeAge() {
    age.value += 1;
}

要先引入ref函数,然后它会变成一个对象,然后通过.value改值

想要对=将对象类型的数据进行响应式处理,就需要用reactive 替换ref

reactive声明的如果直接更改整个对象会导致失去响应式,需要用Object.assign()来浅拷贝才行。

toRefs与toRef

如果每个变量都要写ref的话未免就太麻烦了,我们就可以用toRefs或者toRef

import { reactive,toRefs,toRef } from 'vue';
let person = reactive({
    name: 'John Doe',
    age: 18
});

let {name,age} = toRefs(person);
let nl = toRef(person,'age');

function changeName() {
    name.value = 'Jane Doe';
}
function changeAge() {
    age.value = 25;
}

Computed属性

<template>

    <div class="person">
        姓:<input type="text" v-model="firstName"></input><br>
        名:<input type="text" v-model="lastName"></input><br>
        全名:<span>{{ firstName }}-{{ lastName }}</span>
    </div>

</template>

<script lang="ts" setup name="Person">
import { ref } from 'vue';
let firstName = ref('张');
let lastName = ref('三');


</script>
<style scoped>
.person {
    background-color: skyblue;
    border: 1px solid #ccc;
    border-radius: 5px;
    padding: 10px;
}

button {
    margin: 0 5px;
}
</style>

这里写了一个组合姓名的逻辑,但是如果我想要名字首字母大写,在模板里面写太多代码明显是不符合规范的,这时候我们就可以用到计算属性

<template>

    <div class="person">
        姓:<input type="text" v-model="firstName"></input><br>
        名:<input type="text" v-model="lastName"></input><br>
        全名:<span>{{ fullName }}</span>
    </div>

</template>

<script lang="ts" setup name="Person"> 
import { ref,computed } from 'vue';
let firstName = ref('zhang');
let lastName = ref('san');

let fullName = computed(() => {
    return firstName.value.slice(0, 1).toUpperCase() + firstName.value.slice(1) + '-' + lastName.value;
});


</script>
<style scoped>
.person {
    background-color: skyblue;
    border: 1px solid #ccc;
    border-radius: 5px;
    padding: 10px;
}

button {
    margin: 0 5px;
}
</style>

这样就行了,但是computed方法是只读的

import { ref, computed } from 'vue';
let firstName = ref('zhang');
let lastName = ref('san');

let fullName = computed({
    get() {
        return firstName.value.slice(0, 1).toUpperCase() + firstName.value.slice(1) + '-' + lastName.value;
    },
    set(vl) {
        const [str1,str2] = vl.split('-');
        firstName.value = str1
        lastName.value = str2
        console.log(vl);
    }
});

get()set()就可以做到改变fullname的值,set()会收到新改的值作为参数

Watch

import { ref, watch } from 'vue';

let sum = ref(0);

function changeSum() {
    sum.value+=1;
}

watch(sum,(newVal, oldVal) => {
    console.log(`sum的值从${oldVal}变成了${newVal}`);
} );

我们可以引入并使用watch函数,然后用回调函数接收旧数据与新数据

const stopWatch = watch(sum,(newVal, oldVal) => {
    console.log(`sum的值从${oldVal}变成了${newVal}`)
    if (newVal > 10) {
        stopWatch();
    }
});

达到条件停止监视

但是如果监听对象的话监听的只是对象的地址变化,如果要监听内部的属性就不可以,所以我们需要手动开启深度模式

watch(person, (newVal, oldVal) => {
    console.log('person changed from', oldVal, 'to', newVal);
}, { deep: true });

在后面追加一个对象,在里面可以写上这个监视函数的各种配置,deep就是控制深度模式,immediate可以让数据没有变化,页面刚加载的时候执行一次监视。如果只写一个形参那么就会默认接收最新的数据。reactive默认开启深度监视

如果我们只想监听对象里一个特定的属性,就可以用getter函数

watch(()=>{return person.name},(newValue,oldValue) => {
console.log(newValue,oldValue);
})

这样就会接收到person.name

watch(person.car,(newValue,oldValue) => {
console.log(newValue,oldValue);
})

对象里面的“对象”可以直接监听,不过还是建议写成函数的,因为如果整个改person.car不会监听到

watch([()=> person.age,()=> person.car],(newValue,oldValue) => {
console.log(newValue,oldValue);
})

监听多个

watchEffect

watch([temp, height], (value) => {
    let [newTemp, newHeight] = value

    if(newTemp >= 60 || newHeight >= 100){
        console.log("发请求")
    }
})

监听里面的逻辑需要多少数据就要监听多少数据,这样未免太麻烦了,特别是在数据特别多的时候

watchEffect(() => {
    if(temp.value >= 60 || height.value >= 100){
        console.log("发请求")
    }
})

这时候就可以用watchEffect,可以自动监视用到的数据。watchEffect会在页面加载自动执行内部的代码一次

ref属性

<template>

    <div class="person">
       <h1>中国</h1>
       <h2 ref="title2">北京</h2>
       <h3>尚硅谷</h3>
       <button @click="showLog">点击我输出h2</button>
    </div>

</template>

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

// 创建一个title2,用于存储ref标记的内容
let title2 = ref()

function showLog() {
    console.log(title2.value);
}
</script>


<style scoped>
.person {
    background-color: skyblue;
    border: 1px solid #ccc;
    border-radius: 5px;
    padding: 10px;
}

button {
    margin: 0 5px;
}
</style>

可以用ref属性代替id,他的value会输出标签本身。defineExpose可以放在子组件,这样父组件获取子组件的ref就可以获取子组件内部的变量

TypeScript

接口

//定义一个接口用于限制person对象的具体属性
export interface PersonInter {
    id: string,
    name: string,
    age:number
}

导出后我们可以在别的地方使用

import { type PersonInter } from '@/types/'


let person: PersonInter = {
    id: "asdsjf01",
    name: "张三",
    age: 18
}

let personsList: Array<PersonInter> = [
    {
        id: "asdsjf01",
        name: "张三",
        age: 18
    },
    {
        id: "asdsjf02",
        name: "李四",
        age: 20
    },
    {
        id: "asdsjf03",
        name: "王五",
        age: 22
    }
 ]

接口可以规定每个属性都应该是什么属性名。还可以用Array<PersonInter> 规定数组里面放什么样的对象

export type Persons = Array<PersonInter>

也可以自定义一个类型,把Array<PersonInter>替换为Persons

export interface PersonInter {
    id: string,
    name: string,
    age?:number
}

加问号的就是可选参数

断言:

talkList: JSON.parse(localStorage.getItem('loveTalk') as string)

TS可能担心返回的不是正确的类型,因为JSON.parse只接受字符串类型的数据,但是localStorage这里取过来的也有可能是null类型,这时候我们就可以在后面加上as String来“保证”它返回的一定是字符串类型,这样编译的时候就不会报错了,如果运行的时候返回不正确的类型还是会报错

断言还有一种老写法:

<类型>名

Props

我们可以用这个向其他组件传递数据

父组件:

    <Person a="哈哈"/>

子组件:

import { defineProps } from 'vue'

defineProps(["a","b"])

引入之后在调用defineProps() ,传入一个数组,数组内写上数据名就可以接收到了,我们可以直接在{{}}写上传进来的数据名

但是这个a如果我们使用console.log()是打印不出来的,它还不是一个变量。但是这个函数会给一个对象的返回值,内部包含着数据,我们可以把返回值赋值给一个变量,这样就能打印出来了:

let x = defineProps(["a","b"])

console.log(x.a)

let x = defineProps<{ list: Persons }>()

我们还可以限制它传进来的值是什么类型的。在list后面加上问号就代表这个数据可传可不传

import { defineProps,withDefaults } from 'vue'
import { type Persons } from '@/types/'

withDefaults(defineProps<{ list: Persons }>(), {
  list: () => [{ id: "000", name: "默认", age: 0 }]
})

可以设置默认值,但是list冒号后面收到的必须是一个函数返回的返回值

但是这样只能传字符串,我们可以在前面加上冒号来传数据:

<Child :car="car" :sendToy="getToy"/>

生命周期

Vue2的生命周期

钩子就是生命周期函数

debugger; 可以在这个位置停止代码的运行

//创建前的钩子
  beforeCreate() {
    console.log('创建前');
  },
  //创建完毕的钩子
  created() {
    console.log('创建完毕');
  },
  //挂载前的钩子
  beforeMount() {
    console.log('挂载前');
  },
  //挂载完毕的钩子
  mounted() {
    console.log('挂载完毕');
  },
  //更新前的钩子
  beforeUpdate() {  
    console.log('更新前');
  },
  //更新完毕的钩子
  updated() {
    console.log('更新完毕');
  },
  //销毁前的钩子
  beforeDestroy() {
    console.log('销毁前');
  },
  //销毁完毕的钩子
  destroyed() {
    console.log('销毁完毕');
  }

Vue3的生命周期

//创建
console.log('创建');

//挂载
onBeforeMount(() => {
    console.log('挂载前');
});

onMounted(() => {
    console.log('挂载完');
});

onBeforeUpdate(() => {
    console.log('更新前');
});

onUpdated(() => {
    console.log('更新完');
});

//卸载
onBeforeUnmount(() => {
    console.log('卸载前');
});

onUnmounted(() => {
    console.log('卸载完');
});

父组件也是有生命周期的,并且子组件比父组件先挂载

自定义hook

import {ref,reactive} from 'vue'

let sum = ref(0)
let dogList = reactive(['https:\/\/images.dog.ceo\/breeds\/clumber\/n02101556_3736.jpg'])


function add(){
    sum.value += 1;
}

async function getDog(){
    fetch('https://dog.ceo/api/breeds/image/random')
    .then(response => response.json())
    .then(data => {
        dogList.push(data.message)
    })
    .catch(error => console.error('Error:', error));
}

这里用fetch获取了狗的图片,但是如果我们有很多要获取的东西就要写多个方法,这样会很乱并降低可读性。

我们可以创建一个hook 文件夹,在下面写上useDog.ts (命名规范就是useSth)

import { ref, reactive } from 'vue'


export default function qwe() {
    let dogList = reactive(['https:\/\/images.dog.ceo\/breeds\/clumber\/n02101556_3736.jpg'])

    async function getDog() {
        fetch('https://dog.ceo/api/breeds/image/random')
            .then(response => response.json())
            .then(data => {
                dogList.push(data.message)
            })
            .catch(error => console.error('Error:', error));
    }

    //向外部提供
    return { dogList, getDog }
}

我们在useSum文件夹里写入:

import { ref } from 'vue'


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

    function add() {
        sum.value += 1
    }
    return { sum, add }
}

最后我们在Peson.vue只需要调用这些写好的方法就行了

import useSum from '@/hooks/useSum'
import useDog from '@/hooks/useDog'

const { sum, add } = useSum()
const { dogList, getDog } = useDog()

当然,在hooks里面也可以写钩子,计算属性等等

路由

在使用路由之前,我们需要使用npm i vue-router来安装一下“路由器”,我们还需要创建一个router文件夹,并在下面创建一个index.ts ,在里面引入组件,并且创建一个路由器并导出

//创建一个路由器并暴露出去
import { createRouter, createWebHistory } from "vue-router";
import Home from '@/components/Home.vue'
import News from '@/components/News.vue'
import About from '@/components/About.vue'


//创建路由器
const router = createRouter({ //路由器的工作模式
    history: createWebHistory(),
    routes: [
        {
            path: '/home',
            component: Home
        },
        {
            path: '/news',
            component: News
        },
        {
            path: '/about',
            component: About
        }
    ]
})

export default router; //暴露出去

然后回到main.ts使用路由器:

import './assets/main.css'

import { createApp } from 'vue'
import App from './App.vue'
import router from './router' //引入路由器

//创建一个应用
const app = createApp(App)
//使用路由器
app.use(router)
//挂载整个应用到app容器中
app.mount('#app')

但是这时候Vue还不知道要把路由后的内容放在哪里,这时候就需要我们在App.vue里面引入

import {RouterView} from 'vue-router'

<RouterView></RouterView>

然后内容就会渲染在那个标签里了。如果需要做到切换路由,我们需要引入RouterLink,然后把a标签替换为<RouterLink></RouterLink> ,在to属性中写上要前往的地方。我们还可以使用active-class属性来规定激活点击之后会获得什么类名

路由组件一般会放在viewspages文件夹里

嵌套路由

有时候我们在路由里面还要写一个路由,这个就叫嵌套路由。我们需要来到router/index.ts引入子路由,然后找到要添加嵌套路由的路由:

        {
            path: '/news',
            component: News,
            children: [
                {
                    path: 'detail',
                    component: Detail
                }
            ]
        }

注意,子路由的路径不需要写斜杠

query参数

 <RouterLink to="/news/detail?a=哈哈&b=你好&c=嘿嘿">{{ news.title }}</RouterLink>

我们在to里面的路径写上查询字符串,就可以在接收的组件里面去接收:

<template>
    <ul class="news-list">
        <li>编号:{{ route.query.a }}</li>
        <li>标题:{{ route.query.b }}</li>    </ul>
</template>

<script setup lang="ts" name="About">
import { useRoute } from "vue-router";
const route = useRoute();
</script>

模板字符串传值:

                <RouterLink :to="`/news/detail?id=${news.id}&title=${news.title}&content=${news.content}`">{{ news.title }}</RouterLink>

我们也可以选择一种可读性更好的写法:

        <ul>
            <li v-for="news in newsList" :key="news.id">
                <router-link :to="{
                    path: '/news/detail',
                    query: {
                        id: news.id,
                        title: news.title
                    }
                }">
                    {{ news.title }}
                </router-link>
            </li>
        </ul>

params参数

我们还可以用路径的方式传参数:

           <router-link to="/news/detail/哈哈/嘿嘿">{{ news.title }}</router-link>

但是我们需要在router/index.ts里配置好占位符,规定路径后面哪个路径对应哪个参数(在后面写上?表示可选参数):

{
            path: '/news',
            component: News,
            children: [
                {
                    path: 'detail/:id/:title',
                    component: Detail
                }
            ]
        },
<template>
    <ul class="news-list">
        <li>编号:{{ route.params.id }}</li>
        <li>标题:{{ route.params.title }}</li>
    </ul>
</template>

<script setup lang="ts" name="About">
import { useRoute } from "vue-router";
const route = useRoute();
</script>

也可以使用对象写法:

<router-link 
                :to="{
                    name: 'news-detail',
                    params: {
                        id: news.id,
                        title: news.title
                    }
                }
                    ">
                    {{ news.title }}
                </router-link>

但是使用对象写法不能写路径,只能写路由名称,这个名称需要预先配置好

props配置

你想想,如果仅仅接收一个数据就要写那么多代码,未免过于麻烦了,这时候我们就需要props配置

props就相当于把数据放入了标签的属性当中

<template>
    <ul class="news-list">
        <li>编号:{{ id }}</li>
        <li>标题:{{ title }}</li>
    </ul>
</template>

<script setup lang="ts" name="About">
defineProps(["id", "title"])
</script>

启用props后就可以把接收过来的params参数直接使用

函数式写法:

  {
            path: '/news',
            component: News,
            children: [
                {
                    name: 'news-detail',
                    path: 'detail/:id?/:title?',
                    component: Detail,
                    props(route) {
                        return route.query
                    }
                }
            ]
        }
则有,

这样queryparams的都可以接收,但是一般只有query用这个,params一般直接用props:true

replace属性

路由默认的模式是push模式,也就是说,在浏览器的历史记录里面,每到一个新的路由都会新建一条历史记录,而replace从始至终只会显示一条你最后浏览的路由

我们只需要给RouerLink加上replace模式就能切换到了:

<RouterLink replace></RouterLink>

编程式路由导航

如果我们需要设计一个button,点击就能跳转到对应路由,但是如果没有学编程式路由导航,那就只能用RouterLink

import { onMounted } from 'vue';
import { useRouter } from 'vue-router';

const router = useRouter();

onMounted(() => {
    setTimeout(() => {
        router.push('/news');
    }, 3000);
})

直接对router进行操作以实现编程式路由导航,直接一push就能跳转到/news页面。要想实现点击按钮跳转页面,我们只需要给button绑定一个onclick事件就行,但是要记住一定要给调用的函数里面传入一个值。router.push()里面和to的写法都是一样的

重定向

        {
            path: '/',
            redirect: 'home'
        }

这个配置项可以把指定的路径重定向到一个路径

Pinia

Pinia就是一个集中式的状态管理工具。一些组件共享使用的数据就可以放入Pinia进行集中管理

搭建环境

npm i pinia

修改main.ts:

import './assets/main.css'

import { createApp } from 'vue'
import App from './App.vue'
import {createPinia} from "pinia";

const app = createApp(App)

//创建pinia
const pinia = createPinia()
//安装pinia
app.use(pinia)

app.mount('#app')

存储/读取数据

我们需要先在src目录下创建一个store文件夹,在这个文件夹里面可以创建各种用于存储数据的ts文件。比如我们可以创建一个count.ts来存储计数相关数据:

import {defineStore} from "pinia";

export const useCountStore = defineStore('count',{
    //真正存储数据的地方
    state:()=>{
        return {
            sum:6
        }
    }
});

state就是存储数据的地方。

import {useCountStore} from "@/store/count";

const countStore = useCountStore()

let n = ref(1)

function add() {
    countStore.sum += n.value
}

function minus() {
    countStore.sum -= n.value
}

只要一引入,就可以使用这个数据了。

修改数据

 countStore.sum -= n.value

可以以复合直觉的方式直接修改

    countStore.$patch({
        sum: 888,
        school: '尚硅谷',
        address: '北京'
    })

也可以这样批量一次性修改


还有一种action的写法,但是意义不大,就是在store文件把数据处理的相关逻辑写好,比如:

    actions:{
        increment(value:number){
            this.$patch({
                sum: this.sum + value
            })
        }

然后直接在其他文件调用数据上的这个方法:

countStore.increment(n.value)

但是这样每次实用数据都要写store很麻烦,我们这时候就可以用storeToRefs来简化写法:

import {storeToRefs} from "pinia";

const countStore = useCountStore()
const {sum, school, address} = storeToRefs(countStore)

可能你会问,我都有toRefs了,为什么还要用这个东西,因为toRefs会把store里面所有的属性都变成响应式的,性能很差劲

getters的使用

import { defineStore } from "pinia";

export const useCountStore = defineStore('count', {
    actions: {
        increment(value: number) {
            this.$patch({
                sum: this.sum + value
            })
        }
    },
    //真正存储数据的地方
    state: () => {
        return {
            sum: 6,
            school: 'tsinghua',
            address: 'beijing'
        }
    },
    getters: {
        bigSum(state) {
            return state.sum * 100
        },
        upperSchool(state) {
            return state.school.toUpperCase()
        }
    }
});

我们可以在配置文件里写一个getters属性来对数据进行一些操作,其实就类似于计算属性。之后在其他文件直接写这个函数的名字就能获取返回的修改的值了

$subscribe的使用

talkStore.$subscribe((mutate, state) => {
   localStorage.setItem('loveTalk', JSON.stringify(state.talkList))
})

我们可以用$subscribe监视数据本身的变化,并传入两个分别代表更新后的详情与更新后的数据的参数。我们可以把pinia数据改成用localStorage引用,这样就能保留了

talkList: JSON.parse(localStorage.getItem('loveTalk') as string)

组合式写法

import { defineStore } from "pinia";

import { reactive } from "vue";
export const useTalkStore = defineStore('talk', () => {
    //相当于state
    const talkList = reactive(
        JSON.parse(localStorage.getItem('loveTalk') as string) || []
    );

    //相当于action
    async function getATalk() {
        const res = await fetch(`https://api.zxki.cn/api/twqh`);
        const data = await res.text();
        talkList.unshift({ id: Date.now().toString(), title: data });
    }

    return {
        talkList,
        getATalk
    };
});

还可以以选项式的写法去写,但是记住一定要return出来

组件通信

父传子很简单,只需要传属性就够了

<Child :car="car">

但是子传父不一样,需要父组件先给子组件传递一个函数,子组件再通过调用这个函数传递给父组件

父组件传递函数:

<Child :car="car" :sendToy="getToy"/>

子组件:

<button @click="sendToy(toy)">把玩具给父亲</button>

子组件这里就需要通过一个按钮来调用这个函数并把数据穿过去,父组件那里在通过定义好的函数对数据进行处理

自定义事件

$event 是事件对象,在TS的lei类型是:Event

自定义事件这个东西是专门用于子传父的。我们可以像@click那样自定义一个事件,在这个事件里面写上要调用的函数,然后在子组件里触发事件并传值,以此来达到子传父的效果

<Child @send-toy="saveToy"/>

	function saveToy(value:string){
		console.log('saveToy',value)
		toy.value = value
	}

比如在父组件的子组件标签里先写上一个自定义事件

<button @click="emit('send-toy',toy)">测试</button>

	let toy = ref('奥特曼')
	// 声明事件
	const emit =  defineEmits(['send-toy'])

之后就可以在子组件通过emit写上事件名称与要传的值把数据传给父组件。但是必须要在子组件先通过defineEmits()定义一个事件,并赋值给一个变量。

mitt

这玩意可以实现组件间的任意通信。原来的通信方式太绕了,使用这个的话,只要两个组件连接到mitt就可以很方便地互相通信

使用mitt要先npm i mitt安装。然后创建src/utils文件夹并创建emitter.ts

import mitt from 'mitt'

// 调用mitt得到emitter,emitter能:绑定事件、触发事件
const emitter = mitt()

// 暴露emitter
export default emitter

然后我们还需要在main.ts引入

import emitter from './utils/emitter'

emitter能够绑定事件与触发事件

all是获取所有事件,emit触发事件,off解绑事件,on绑定事件

emitter.all.clear()

这个可以批量解绑所有事件

使用方式

数据的接受方需要绑定一个事件,发送方才能发过来:

emitter.on('send-toy',(value:any)=>{
		toy.value = value
	})
	// 在组件卸载时解绑send-toy事件
	onUnmounted(()=>{
		emitter.off('send-toy')
	})

这里的卸载是必不可少的,否则组件被卸载后数据监听还一直在那里。

<button @click="emitter.emit('send-toy',toy)">玩具给弟弟</button>

发送方这里直接调用事件即可触发。

v-model

或许我们看过一些UI组件库,封装了一个输入框,他们可以直接在标签上写

 <AtguiguInput v-model="username"/>

来双向绑定,这是怎么实现的呢?

实际上,v-model是只能给input标签双向绑定的,你是给一个HTML元素上的这个东西,如果里给你的自定义组件上这个是没有效果的,所以这个东西需要我们去单独封装一个。

<input type="text" v-model="username"> 
<input type="text" :value="username" @input="username = (<HTMLInputElement>$event.target).value"> 

v-model的原理实际上是这样的,给value整上冒号,后面绑定了一个input事件,只要输入就触发这个事件,把数据更新成用户最新输入的数据。

<AtguiguInput v-model="username"/> <AtguiguInput 
      :modelValue="username" 
      @update:modelValue="username = $event"
    /> -->

自定义组件使用v-model的原理也差不多。但是我们还需要去在底层改一改。

<template>
  <input 
    type="text" 
    :value="ming"
    @input="emit('update:ming',(<HTMLInputElement>$event.target).value)"
  >
  <br>
  <input 
    type="text" 
    :value="mima"
    @input="emit('update:mima',(<HTMLInputElement>$event.target).value)"
  >
</template>

<script setup lang="ts" name="AtguiguInput">
  defineProps(['ming','mima'])
  const emit = defineEmits(['update:ming','update:mima'])
</script>

<style scoped>
  input {
    border: 2px solid black;
    background-image: linear-gradient(45deg,red,yellow,green);
    height: 30px;
    font-size: 20px;
    color: white;
  }
</style>

我们要在自定义组件这里设定一个emit接收更新事件,再把更新过来的值传到input

但是在新版中已经有了更好的做法:

https://cn.vuejs.org/guide/components/v-model

$attrs

我们可以用$attrs来实现祖传孙

在vue的设计中,props传的数据接收方没有接收的数据会放到attrs里,这时候我们就可以通过v-bind绑定$attrs来传递给对应的组件。

父:

  <div class="father">
    <h3>父组件</h3>
		<h4>a:{{a}}</h4>
		<h4>b:{{b}}</h4>
		<h4>c:{{c}}</h4>
		<h4>d:{{d}}</h4>
		<Child :a="a" :b="b" :c="c" :d="d" v-bind="{x:100,y:200}" :updateA="updateA"/>
  </div>
</template>

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

	let a = ref(1)
	let b = ref(2)
	let c = ref(3)
	let d = ref(4)

	function updateA(value:number){
		a.value += value
	}
</script
>

	<div class="grand-child">
		<h3>孙组件</h3>
		<h4>a:{{ a }}</h4>
		<h4>b:{{ b }}</h4>
		<h4>c:{{ c }}</h4>
		<h4>d:{{ d }}</h4>
		<h4>x:{{ x }}</h4>
		<h4>y:{{ y }}</h4>
		<button @click="updateA(6)">点我将爷爷那的a更新</button>
	</div>
</template>

<script setup lang="ts" name="GrandChild">
	defineProps(['a','b','c','d','x','y','updateA'])
</script>

$parent与$refs

$refs$parent这两个变量分别代表着组件的子元素与父元素,我们通过操控这两个变量,来实现子孙之间的通信。

<button @click="getAllChild($refs)">让所有孩子的书变多</button>
	function getAllChild(refs:{[key:string]:any}){
		console.log(refs)
		for (let key in refs){ 
			refs[key].book += 3
		}
	}  

比如这个就可以直接可以通过遍历修改每个子元素某个变量的值,但是在修改变量之前我们还需要把它暴露出去:

defineExpose({toy,book})

provide_inject

这是一种专门用来实现祖孙传递的一个工具。可能你会问,为什么我不直接使用$attrs呢?因为$attrs还需要一个中间组件进行传递,很麻烦。

  import {ref,reactive,provide} from 'vue'

  let money = ref(100)
  let car = reactive({
    brand:'奔驰',
    price:100
  })
  function updateMoney(value:number){
    money.value -= value
  }

  // 向后代提供数据
  provide('moneyContext',{money,updateMoney})
  provide('car',car)

我们可以在祖组件引入provide来向后代提供数据,方法。

<template>
  <div class="grand-child">
    <h3>我是孙组件</h3>
    <h4>银子:{{ money }}</h4>
    <h4>车子:一辆{{car.brand}}车,价值{{car.price}}万元</h4>
    <button @click="updateMoney(6)">花爷爷的钱</button>
  </div>
</template>

<script setup lang="ts" name="GrandChild">
  import { inject } from "vue";

  let {money,updateMoney} = inject('moneyContext',{money:0,updateMoney:(param:number)=>{}})
  let car = inject('car',{brand:'未知',price:0})
</script>

然后在孙组件内引入。这里的第一个是接收的数据,第二个参数则是默认值(如果祖组件没有传递数据默认用默认值),默认值还可以起到一个告诉TS传过来的数据是什么结构,类型的作用,否则会飘红。

插槽

默认插槽

我们可以使用插槽在组件内插入HTML元素。

      <Game>
        <span>nihao</span>
      </Game>

把组件标签写成闭标签的形式,然后在内部写上要加入的HTML元素

<template>
  <div class="game">
    <h2>游戏列表</h2>
    <slot>11</slot>
  </div>
</template>

插入内容就会在子组件的slot标签内显示。如果在slot标签内先写上一些内容,那就会作为默认值。

具名插槽

顾名思义就是具有名字的插槽。我们可以把指定的HTML元素放到指定的插槽当中。我们需要在子组件的slot插槽定义一个name属性并写上插槽名,回到父组件,对HTML元素外包裹一个template标签,标签写上v-slot:插槽名,就可以把这个内容放到对应的插槽了,v-slot:插槽名 只能写在组件标签或template标签上。这个还有一个语法糖,v-slot:插槽名 可以简写为#插槽名 的形式。

默认插槽的name属性其实是default,不过平常一般不用单独写出来。

实例:

      <Category>
        <template #s2>
          <video video :src="videoUrl" controls></video>
        </template>
        <template #s1>
          <h2>今日影视推荐</h2>
        </template>
      </Category>
<template>
  <div class="category">
    <slot name="s1">默认内容1</slot>
    <slot name="s2">默认内容2</slot>
  </div>
</template>

作用域插槽

如果我们想将一个遍历子组件数据的列表塞到插槽里面,就会遇到访问不到子组件内部数据的问题,这时候我们就需要使用作用域插槽把数据传给父组件

<template>
  <div class="game">
    <h2>游戏列表</h2>
    <slot :youxi="games" x="哈哈" y="你好"></slot>
  </div>
</template>

我们要先在子组件的插槽标签内写上要传递的数据

      <Game>
        <template v-slot="params">
          <ul>
            <li v-for="y in params.youxi" :key="y.id">
              {{ y.name }}
            </li>
          </ul>
        </template>
      </Game>

副组件里可以在v-slot里接受,并使用数据。

        <template #default="{youxi}">
          <h3 v-for="g in youxi" :key="g.id">{{ g.name }}</h3>
        </template>

还可以使用花括号对传递过来的数据进行解构,这样就不用每次都写params或者什么别的了。#default="{youxi}" 等价于v-slot:"{youxi}"

一些API

shallowRef与shallowReactive

shallowRefshallowReactive定义的数据只能操作数据的第一层,对象不能深入修改。shallowRef最多只能访问到.valueshallowReactive只能访问到第一层属性,

readonly与shallowReadonly

let a = readonly(b) 可以把一个数据变为只读属性,并且依旧跟随原来数据的响应式,但无法直接修改。

shallowReadonly() 只限制第一层的访问权限。比如用这个定义了一个对象,那么只有第一层不能修改,再深层的数据就可以修改了。

toRaw与markRaw

toRaw() 可以把一个响应式对象的数据提取出来变成普通的对象。Vue的对象是Proxy对象,对于其他库或者外部系统不能直接使用,就需要这个方法来转换一下。转换后的数据就失去响应式了,不会受原来的数据影响。

let b = markRaw(a) 定义的数据会使其永远不会变成响应式,你无法用ref或者reactive转变它。

teleport

我们可以使用<teleport to="name"></teleport>把包裹在其中的组件依靠于to中的元素

全局API

app.component("name",name) 在main.ts中使用可以把一个组件挂在到全局当中,可以在任何地方使用这个组件而不用引入

自定义指令

其他