一、Vue 3.3 新特性
1、defineOptions
有 <script setup>
之前,如果要定义 props
,emits
可以轻而易举地添加一个与 setup
平级的属性
但是用了 <script setup>
后,就没法这么干了,setup
属性已经没有了,自然没法添加与其平级的属性
为了解决这一问题,引入 defineProps
与 defineEmits
这两个宏。但这只解决了 props
与 emits
两个属性
若要定义组件的 name
或其他自定义的属性,还是得回到最原始的用法 —— 再添加一个普通的 <script>
标签
这样就会存在两个<script>
标签,让人无法接受
所以在 Vue3.3 中新引入了 defineOptions
宏。顾名思义,主要用来定义 Options API
的选项。
可以用 defineOptions
定义任意的选项,props
、emits
、expose
、slots
除外(因为这些可以使用 defineXXX
实现)
<script setup>
defineOptions({
name: 'LoginIndex'
})
</script>
2、defineModel
在 Vue3 中,自定义组件上使用 v-model
,相当于传递一个 modelValue
属性,同时触发 update:modelValue
事件
<Child v-model="isVisible">
// 相当于
<Child :modelValue="isVisible" @update:modelValue="isVidible=$event">
需要先定义 props,再定义 emits,有许多重复的代码。若需要修改此值,还需要手动调用 emit 函数
从 Vue 3.4 开始,推荐的实现方式是使用 defineModel()
宏:
<!-- Child.vue -->
<script setup>
const model = defineModel()
function update() {
model.value++
}
</script>
<template>
<div>parent bound v-model is: {{ model }}</div>
</template>
<!-- Parent.vue -->
<Child v-model="count" />
defineModel()
返回的值是一个 ref。它可以像其他 ref 一样被访问以及修改,不过它能起到在父组件和当前变量之间的双向绑定的作用:
-
它的
.value
和父组件的v-model
的值同步; -
当它被子组件变更了,会触发父组件绑定的值一起更新。
这意味着你也可以用 v-model
把这个 ref 绑定到一个原生 input 元素上,在提供相同的 v-model
用法的同时轻松包装原生 input 元素:
<script setup>
const model = defineModel()
</script>
<template>
<input v-model="model" />
</template>
二、Pinia
Pinia 是 Vue 的最新状态管理工具
,是 Vuex 的替代品
-
提供更加简单的 API(去掉了 mutation)
-
提供符合组合式风格的 API(和 Vue3 新语法统一)
-
去掉了 modules 的概念,每一个 store 都是一个独立地模块
-
配置 TypeScript 更加友好,提供可靠的类型推断
1、手动添加 Pinia 到 Vue 项目
在实际开发项目的时候,关于 Pinia 的配置,可以在项目创建时自动添加(以下方式为手动添加)
-
使用 Vite 创建一个空的 Vue3 项目
npm create vue@latest
-
按照官方文档安装 Pinia 到项目中
Pinia | The intuitive store for Vue.js
npm install pinia
Tip:
如果你的应用使用的 Vue 版本低于 2.7,你还需要安装组合式 API 包:
@vue/composition-api
。如果你使用的是 Nuxt,你应该参考这篇指南。
如果你正在使用 Vue CLI,你可以试试这个非官方插件。
创建一个 pinia 实例 (根 store) 并将其传递给应用:
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
app.mount('#app')
2、Pinia 基本语法
-
准备一个 store(单独一个 js 文件)
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', ()=>{
// 声明数据 state - count
const count = ref(100)
// 声明操作数据的方法 action (普通函数)
const addCount = () => count.value++
const subCount = () => count.value--
// 声明基于数据派生的计算属性 getters (computed)
const double = computed(() => count.value * 2)
// 声明数据 state - msg
const msg = ref('hello pinia')
return{
count,
double,
msg,
addCount,
subCount
}
})
-
在需要 store 中数据的组件中导入并使用
// App.vue
<script setup>
import Son1 from '@/components/Son1_Com.vue'
import Son2 from '@/components/Son2_Com.vue'
import { useCounterStore } from '@/store/counter.js'
const counterStore = useCounterStore()
console.log(counterStore)
</script>
<template>
<div>
<h1>我是根组件
- {{ counterStore.count }}
- {{ counterStore.msg }}
</h1>
<Son1></Son1>
<Son2></Son2>
</div>
</template>
// Son1_Com.vue
<script setup>
import { useCounterStore } from '@/store/counter.js'
const counterStore = useCounterStore()
</script>
<template>
<div>
我是Son1.vue - {{ counterStore.count }} - {{ counterStore.double }}
<button @click="counterStore.addCount">+</button><br>
- {{ counterStore.msg }}
</div>
</template>
// Son2_Com.vue
<script setup>
import { useCounterStore } from '@/store/counter.js'
const counterStore = useCounterStore()
</script>
<template>
<div>
我是Son2.vue - {{ counterStore.count }} - <button @click="counterStore.subCount">-</button><br>
- {{ counterStore.msg }}
</div>
</template>
3、action 异步写法
编写方式:异步 action 函数的写法和组件中获取异步数据的写法完全一致
接口地址:http://geek.itheima.net/v1_0/channels
需求:在 Pinia 中获取频道列表数据并把数据渲染 APP 组件的模板中
import { ref } from 'vue'
import { defineStore } from 'pinia'
import axios from 'axios'
export const useChannelStore = defineStore('channel', () => {
const channelList = ref([])
const getList = async () => {
// 支持异步
const { data:{ data } } = await axios.get('http://geek.itheima.net/v1_0/channels')
channelList.value = data.channels
console.log(data.channels)
}
return{
channelList,
getList
}
})
<script setup>
import { useChannelStore } from '@/store/channel.js'
const channelStore = useChannelStore()
</script>
<template>
<button @click="channelStore.getList">获取频道</button>
<ul>
<li v-for="item in channelStore.channelList" :key="item.id">{{ item.name }}</li>
</ul>
</div>
</template>
4、storeToRefs 方法
store
是一个用 reactive
包装的对象,这意味着不需要在 getters 后面写 .value
,就像 setup
中的 props
一样,如果你写了,我们也不能解构它:
<script setup>
const store = useCounterStore()
// ❌ 这将不起作用,因为它破坏了响应性
// 这就和直接解构 `props` 一样
const { name, doubleCount } = store
name // 将始终是 "Eduardo"
doubleCount // 将始终是 0
setTimeout(() => {
store.increment()
}, 1000)
// ✅ 这样写是响应式的
// 💡 当然你也可以直接使用 `store.doubleCount`
const doubleValue = computed(() => store.doubleCount)
</script>
为了从 store 中提取属性时保持其响应性,你需要使用 storeToRefs()
。它将为每一个响应式属性创建引用。当你只使用 store 的状态而不调用任何 action 时,它会非常有用。请注意,你可以直接从 store 中解构 action,因为它们也被绑定到 store 上:
<script setup>
import { storeToRefs } from 'pinia'
const store = useCounterStore()
// `name` 和 `doubleCount` 是响应式的 ref
// 同时通过插件添加的属性也会被提取为 ref
// 并且会跳过所有的 action 或非响应式 (不是 ref 或 reactive) 的属性
const { name, doubleCount } = storeToRefs(store)
// 作为 action 的 increment 可以直接解构
const { increment } = store
</script>
5、Pinia 持久化
官网文档:Home | pinia-plugin-persistedstate
-
安装插件
npm i pinia-plugin-persistedstate
-
main.js 使用
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
-
创建 Store 时,将
persist
选项设置为true
import { defineStore } from 'pinia'
export const useStore = defineStore(
'main',
() => {
const someState = ref('你好 pinia')
return { someState }
},
{
persist: true,
},
)
三、趣味小知识
为什么取名 Pinia?【官方解释】
Pinia (发音为
/piːnjʌ/
,类似英文中的 “peenya”) 是最接近有效包名 piña (西班牙语中的 pineapple,即“菠萝”) 的词。 菠萝花实际上是一组各自独立的花朵,它们结合在一起,由此形成一个多重的水果。 与 Store 类似,每一个都是独立诞生的,但最终它们都是相互联系的。 它(菠萝)也是一种原产于南美洲的美味热带水果。
一 叶 知 秋,奥 妙 玄 心