标签搜索

Vue3 学习笔记(四)

sunshine
2022-12-24 / 0 评论 / 61 阅读
温馨提示:
本文最后更新于2024年08月27日,已超过86天没有更新,若内容或图片失效,请留言反馈。

章节介绍

本章将学习Vue3组合式API详解 - 大型应用的高端写法。

本章学习目标

本章将学习Vue3组合式API(即:Composition API),组合式API是Vue3组织代码的一种新型写法,有别于选项式的API。学会使用这种风格进行编程,并应用在项目之中。

什么是组合式API

组合式API是有别于选项式API的另一种新型写法,它更适合编写复杂的应用,假如我们要完成一个搜索功能,可能会有如下功能需要实现:

  • 搜索提示列表
  • 搜索结果列表
  • 搜索记录列表
搜索案例

那么分别通过选项式和组合式是如何组织和实现代码的呢?可以发现选项式组织代码的时候,代码的跳跃性特别的强,对于后期维护特别的不方便;而组合式则是把相同功能的代码放到一起,功能独立,易于维护。

选项式VS组合式

课程安排

-setup方法与script_setup及ref响应式
-事件方法_计算属性_reactive_toRefs
-生命周期_watch_watchEffect
-跨组件通信方案provide_inject
-复用组件功能之use函数
-利用defineProps与defineEmits进行组件通信
-利用组合式API开发复杂的搜索功能
-利用组合式API开发搜索提示列表
-利用组合式API开发搜索结果列表
-利用组合式API开发搜索历史列表

setup方法与script_setup及ref响应式

setup方法与script_setup

在Vue3.1版本的时候,提供了setup方法;而在Vue3.2版本的时候,提供了script_setup属性。那么setup属性比setup方法对于操作组合式API来说会更加的简易。


<template>
  <div>
    <h2>setup方法</h2>
    {{ count }}
  </div>
</template>

// Vue3.1
<script>
export default {
  setup () {
    let count = 0;
    return {
      count
    }
  }
}
</script>

// Vue3.2
<script setup>
let count = 0;
</script>

setup方法是需要把数据进行return后,才可以在template标签中进行使用,而setup属性方式定义好后就可以直接在template标签中进行使用。

ref响应式

如何在组合式API中来完成数据的响应式操作,通过的就是ref()方法,需要从vue模块中引入这个方法后才可以使用。

<script setup>
import { ref } from 'vue';
let count = ref(0);   // count -> { value: 0 }
//count += 1;   //✖
count.value += 1;   // ✔
</scirpt>

count数据的修改,需要通过count.value的方式,因为vue底层对响应式数据的监控必须是对象的形式,所以我们的count返回的并不是一个基本类型,而是一个对象类型,所以需要通过count.value进行后续的操作,那么这种使用方式可能会添加我们的心智负担,还好可以通过Volar插件可以自动完成.value的生成,大大提升了使用方式。

那么现在count就具备了响应式变化,从而完成视图的更新操作。

那么ref()方法还可以关联原生DOM,通过标签的ref属性结合到一起,也可以关联到组件上。

<template>
  <div>
    <h2 ref="elem">setup属性方式</h2>
  </div>
</template>
<script setup>
import { ref } from 'vue';
let elem = ref();
setTimeout(()=>{
  console.log( elem.value );   //拿到对应的原生DOM元素
}, 1000)
</script>

事件方法_计算属性 reactive_toRefs

事件方法与计算属性

下面看一下在组合式API中是如何实现事件方法和计算属性的。

<template>
  <div>
    <button @click="handleChange">点击</button>
    {{ count }}, {{ doubleCount }}
  </div>
</template>
<script setup>
import { computed, ref } from 'vue';
let count = ref(0);
let doubleCount = computed(()=> count.value * 2)
let handleChange = () => {
  count.value += 1;
};
</script>

事件方法直接就定义成一个函数,计算属性需要引入computed方法,使用起来是非常简单的。

reactive与toRefs

reactive()方法是组合式API中另一种定义响应式数据的实现方式,它是对象的响应式副本。

<template>
  <div>
    <h2>reactive</h2>
    {{ state.count }}
  </div>
</template>

<script setup>
import { reactiv} from 'vue';
let state = reactive({
  count: 0,
  message: 'hi vue'
})
state.count += 1;
</script>

reactive()方法返回的本身就是一个state对象,那么在修改的时候就不需要.value操作了,直接可以通过state.count的方式进行数据的改变,从而影响到视图的变化。

ref()和reactive()这两种方式都是可以使用的,一般ref()方法针对基本类型的响应式处理,而reactive()针对对象类型的响应式处理,当然还可以通过toRefs()方法把reactive的代码转换成ref形式。

<template>
  <div>
    <h2>reactive</h2>
    {{ state.count }}, {{ count }}
  </div>
</template>

<script setup>
import { reactive, toRefs } from 'vue';

let state = reactive({
  count: 0
})
let { count } = toRefs(state);   //  let count = ref(0)
setTimeout(() => {
  //state.count += 1;
  count.value += 1;
}, 1000)

</script>

生命周期_watch_watchEffect

生命周期钩子函数

在学习选项式API的时候,我们学习了生命周期钩子函数,那么在组合式API中生命周期又是如何使用的呢?下面我们从图中先看一下对比的情况吧。

生命周期对比

那么具体的区别如下:

  • 组合式中是没有beforeCreate和created这两个生命周期,因为本身在组合式中默认就在created当中,直接定义完响应式数据后就可以直接拿到响应式数据,所以不需要再有beforeCreate和created这两个钩子
  • 组合式的钩子前面会有一个on,类似于事件的特性,那就是可以多次重复调用
<script>
import { onMounted, ref } from 'vue';
let count = ref(0);
onMounted(()=>{
  console.log( count.value );
});
onMounted(()=>{
  console.log( count.value );
});
onMounted(()=>{
  console.log( count.value );
});
</script>

watch与watchEffect

这里先说一下watchEffect的用法,为了根据响应式状态自动应用和重新应用副作用,我们可以使用 watchEffect 函数。它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。

watchEffect常见特性:

  • 一开始会初始触发一次,然后所依赖的数据发生改变的时候,才会再次触发
  • 触发的时机是数据响应后,DOM更新前,通过flush: 'post' 修改成DOM更新后进行触发
  • 返回结果是一个stop方法,可以停止watchEffect的监听
  • 提供一个形参,形参主要就是用于清除上一次的行为
<template>
  <div>
    <h2>watchEffect</h2>
    <div>{{ count }}</div>
  </div>
</template>

<script setup>
import { ref, watchEffect } from 'vue';
let count = ref(0);
// const stop = watchEffect(()=>{
//   console.log(count.value);
// }, {
//   flush: 'post'
// })

// setTimeout(()=>{
//   stop();
// }, 1000)

// setTimeout(()=>{
//   count.value += 1;
// }, 2000)

watchEffect((cb)=>{
  console.log(count.value);
  cb(()=>{
    //更新前触发和卸载前触发,目的:清除上一次的行为(停止上一次的ajax,清除上一次的定时器)
    console.log('before update');
  })
})

setTimeout(()=>{
  count.value += 1;
}, 2000)
</script>

再来看一下watch侦听器的使用方式,如下:

<script setup>
import { ref, watch } from 'vue';
let count = ref(0);
watch(count, (newVal, oldVal) => {
  console.log(newVal, oldVal);
})
setTimeout(()=>{
  count.value = 1;
}, 2000)
</script>

那么watch与watchEffect的区别是什么呢?

  • 懒执行副作用
  • 更具体地说明什么状态应该触发侦听器重新运行
  • 访问侦听状态变化前后的值

跨组件通信方案provide_inject

依赖注入

在Vue中把跨组件通信方案provide_inject也叫做依赖注入的方式,前面我们在选项式中也学习了它的基本概念,下面看一下在组合式API中改如何使用。

// provide.vue
<template>
  <div>
    <my-inject></my-inject>
  </div>
</template>
<script setup>
import MyInject from '@/inject.vue'
import { provide, ref, readonly } from 'vue'
//传递响应式数据
let count = ref(0);
let changeCount = () => {
  count.value = 1;
}
provide('count', readonly(count))
provide('changeCount', changeCount)

setTimeout(()=>{
  count.value = 1;
}, 2000)
</script>

//inject.vue
<template>
  <div>
    <div>{{ count }}</div>
  </div>
</template>

<script setup>
import { inject } from 'vue'
let count = inject('count')
let changeCount = inject('changeCount')
setTimeout(()=>{
  changeCount();
}, 2000);

</script>

依赖注入使用的时候,需要注意的点:

  • 不要在inject中修改响应式数据,可利用回调函数修改
  • 为了防止可设置成 readonly

复用组件功能之use函数

为了更好的组合代码,可以创建统一规范的use函数,从而抽象可复用的代码逻辑。利用use函数可以达到跟mixin混入一样的需求,并且比mixin更加强大。

// useCounter.js
import { computed, ref } from 'vue';
function useCounter(num){
  let count = ref(num);
  let doubleCount = computed(()=> count.value * 2 );
  return {
    count,
    doubleCount
  }
}

export default useCounter;
<template>
  <div>
    <h2>use函数</h2>
    <div>{{ count }}, {{ doubleCount }}</div>
  </div>
</template>
<script setup>
import useCounter from '@/compotables/useCounter.js'
let { count, doubleCount } = useCounter(123);
setTimeout(()=>{
  count.value += 1;
}, 2000);
</script>

通过useCounter函数的调用,就可以得到内部return出来的对象,这样就可以在.vue文件中进行功能的使用,从而实现功能的复用逻辑。

利用defineProps与defineEmits进行组件通信

在组合式API中,是通过defineProps与defineEmits来完成组件之间的通信。

defineProps

defineProps是用来完成父子通信的,基本使用跟选项式中的props非常的像,代码如下:

// parent.vue
<template>
  <div>
    <h2>父子通信</h2>
    <my-child :count="0" message="hello world"></my-child>
  </div>
</template>
<script setup>
import MyChild from '@/child.vue'
</script>

// child.vue
<template>
  <div>
    <h2>hi child, {{ count }}, {{ message }}</h2>
  </div>
</template>
<script setup>
import { defineProps } from 'vue'
const state = defineProps({   // defineProps -> 底层 -> reactive响应式处理的
  count: {
    type: Number
  },
  message: {
    type: String
  }
});
console.log( state.count, state.message );
</script>

defineEmits

defineEmits是用来完成子父通信的,基本使用跟选项式中的emits非常的像,代码如下:

// parent.vue
<template>
  <div>
    <h2>子父通信</h2>
    <my-child @custom-click="handleClick"></my-child>
  </div>
</template>
<script setup>
import MyChild from '@/child.vue'
let handleClick = (data) => {
  console.log(data);
}
</script>

// child.vue
<script setup>
import { defineEmits } from 'vue'
const emit = defineEmits(['custom-click']);
setTimeout(()=>{
  emit('custom-click', '子组件的数据');
}, 2000)
</script>

利用组合式API开发复杂的搜索功能

从本小节我们要完成章节介绍中出现的搜索页面,利用组合式API去实现。先进行开发前的准备工作。

数据接口

后端地址:https://github.com/Binaryify/NeteaseCloudMusicApi

后端接口:

​ 搜索建议:/search/suggest?keywords=海阔天空

​ 搜索结果:/search?keywords=海阔天空

反向代理

由于我们的前端是localhost:8080,而后端是localhost:3000,这样会存在跨域问题,所以可以通过脚手架下的vue.config.js进行反向代理的配置。

// vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  runtimeCompiler: true,
  devServer: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,   
        pathRewrite: {
          '^/api': ''
        }
      }
    }
  }
})

布局与样式

我们提前把布局和样式做好了,直接在上面进行逻辑的开发,可直接查看 13_search.vue 这个文件,到此我们已经准备好了开发前的准备工作。

利用组合式API开发搜索提示列表

本小节完成搜索页面的提示列表功能。

<template>
  <div class="search-input">
    <i class="iconfont iconsearch"></i>
    <input type="text" placeholder="搜索歌曲" v-model="searchWord" @input="handleToSuggest">
    <i v-if="searchWord" @click="handleToClose" class="iconfont iconguanbi"></i>
  </div>
  <template v-if="searchType == 1">
    ...
  </template>
  <template v-else-if="searchType == 2">
    ...
  </template>
  <template v-else-if="searchType == 3">
    <div class="search-suggest">
      <div class="search-suggest-head">搜索“ {{ searchWord }} ”</div>
      <div class="search-suggest-item" v-for="item in suggestList" :key="item.id" @click="handleItemResult(item.name), handleAddHistory(item.name)">
        <i class="iconfont iconsearch"></i>{{ item.name }}
      </div>
    </div>
  </template>
</template>
<script setup>
import { ref } from 'vue';
import axios from 'axios';
import '@/assets/iconfont/iconfont.css';

function useSearch(){
  let searchType = ref(1);
  let searchWord = ref('');
  let handleToClose = () => {
    searchWord.value = '';
    searchType.value = 1;
  };
  return {
    searchType,
    searchWord,
    handleToClose
  }
}

function useSuggest(){
  let suggestList = ref([]);
  let handleToSuggest = () => {
    if(searchWord.value){
      searchType.value = 3;
      axios.get(`/api/search/suggest?keywords=${searchWord.value}`).then((res)=>{
        let result = res.data.result;
        if(!result.order){
          return;
        }
        let tmp = [];
        for(let i=0;i<result.order.length;i++){
          tmp.push(...result[result.order[i]]);
        }
        suggestList.value = tmp;
      })
    }
    else{
      searchType.value = 1;
    }
  };
  return {
    suggestList,
    handleToSuggest
  }
}

let { searchType, searchWord, handleToClose } = useSearch();
let { suggestList, handleToSuggest } = useSuggest();

</script>

利用组合式API开发搜索结果列表

本小节完成搜索页面的结果列表功能。

<template>
  <div class="search-input">
    <i class="iconfont iconsearch"></i>
    <input type="text" placeholder="搜索歌曲" v-model="searchWord" @input="handleToSuggest" @keydown.enter="handleToResult($event)">
    <i v-if="searchWord" @click="handleToClose" class="iconfont iconguanbi"></i>
  </div>
  <template v-if="searchType == 1">
    ...
  </template>
  <template v-else-if="searchType == 2">
    <div class="search-result">
      <div class="search-result-item" v-for="item in resultList" :key="item.id">
        <div class="search-result-word">
          <div>{{ item.name }}</div>
        </div>
        <i class="iconfont iconbofang"></i>
      </div>
    </div>
  </template>
  <template v-else-if="searchType == 3">
    ...
  </template>
</template>
<script setup>
import { ref } from 'vue';
import axios from 'axios';
import '@/assets/iconfont/iconfont.css';

function useSearch(){
  ...
}
function useSuggest(){
  ...
}
function useResult(){
  let resultList = ref([]);
  let handleToResult = () => {
    if(!searchWord.value){
      return;
    }
    axios.get(`/api/search?keywords=${searchWord.value}`).then((res)=>{
      let result = res.data.result;
      if(!result.songs){
        return;
      }
      searchType.value = 2;
      resultList.value = result.songs;
    })
  };
  let handleItemResult = (name) => {
    searchWord.value = name;
    handleToResult();
  };
  return {
    resultList,
    handleToResult,
    handleItemResult
  }
}  

let { searchType, searchWord, handleToClose } = useSearch();
let { suggestList, handleToSuggest } = useSuggest();
let { resultList, handleToResult, handleItemResult } = useResult();
</script>

利用组合式API开发搜索历史列表

本小节完成搜索页面的历史列表功能。

<template>
  <div class="search-input">
    <i class="iconfont iconsearch"></i>
    <input type="text" placeholder="搜索歌曲" v-model="searchWord" @input="handleToSuggest" @keydown.enter="handleToResult($event), handleAddHistory($event)">
    <i v-if="searchWord" @click="handleToClose" class="iconfont iconguanbi"></i>
  </div>
  <template v-if="searchType == 1">
    <div class="search-history">
      <div class="search-history-head">
        <span>历史记录</span>
        <i class="iconfont iconlajitong" @click="handleToClear"></i>
      </div>
      <div class="search-history-list">
        <div v-for="item in historyList" :key="item" @click="handleItemResult(item)">{{ item }}</div>
      </div>
    </div>
  </template>
  <template v-else-if="searchType == 2">
    ...
  </template>
  <template v-else-if="searchType == 3">
    ...
  </template>
</template>
<script setup>
import { ref } from 'vue';
import axios from 'axios';
import '@/assets/iconfont/iconfont.css';

function useSearch(){
  ...
}
function useSuggest(){
  ...
}
function useResult(){
  ...
}  
function useHistory(){
  let key = 'searchHistory';
  let getSearchHistory = () => {
    return JSON.parse(localStorage.getItem(key) || '[]');
  };
  let setSearchHistory = (list) => {
    localStorage.setItem(key, JSON.stringify(list));
  };
  let clearSearchHistory = () => {
    localStorage.removeItem(key);
  };
  let historyList = ref(getSearchHistory());
  let handleAddHistory = (arg) => {
    if(!searchWord.value){
      return;
    }
    if(typeof arg === 'string'){
      searchWord.value = arg;
    }
    historyList.value.unshift(searchWord.value);
    historyList.value = [...new Set(historyList.value)];
    setSearchHistory(historyList.value);
  };
  let handleToClear = () => {
    clearSearchHistory();
    historyList.value = [];
  };
  return {
    historyList,
    handleAddHistory,
    handleToClear
  };
} 

let { searchType, searchWord, handleToClose } = useSearch();
let { suggestList, handleToSuggest } = useSuggest();
let { resultList, handleToResult, handleItemResult } = useResult();
let { historyList, handleAddHistory, handleToClear } = useHistory();
</script>

总结内容

  • 了解了什么是组合式API,具备怎么的特性
  • 学习常见的组合式API语法,如:ref、reactive、watchEffect等
  • 组合式API之间的通信方案,如:父子组件、跨组件等
  • 通过实战案例,复杂的搜索应用,体验组合式的强大
感觉很棒,欢迎点赞 OR 打赏~
1
分享到:

评论 (0)

取消