WikiWiki
首页
Java开发
Java面试
Linux手册
  • AI相关
  • Python Flask
  • Pytorch
  • youlo8
SEO
uniapp小程序
Vue前端
work
数据库
软件设计师
入门指南
首页
Java开发
Java面试
Linux手册
  • AI相关
  • Python Flask
  • Pytorch
  • youlo8
SEO
uniapp小程序
Vue前端
work
数据库
软件设计师
入门指南
  • Typescript4技术笔记
  • VUE3.0技术笔记
  • Vue2.0基础技术笔记
  • Web前端HTML基础

一、Vue3核心概念及选项式API

1.介绍

image-20240805192359435

  • Vue3官网地址:https://v3.cn.vuejs.org/
  • 下载地址:https://unpkg.com/vue@3.2.36/dist/vue.global.js

2.MVC设计模式与MVVM设计模式

  • 为什么使用Vue框架? 最大的优点:就是不用自己完成复杂的DOM操作了,而由框架帮我们去完成

MVC设计模式

image-20240805192844188

​ 最早的MVC设计模式是出现在后端开发中,主要目的就是让视图层与数据层分离,职责更加清晰,方便开发等等,例如:Spring MVC、ASP.NET MVC等等。

image-20240805194242720

backbone.js存在的问题:

  • 数据流混乱,尤其是多视图多数据场景下
  • 控制层单薄,可有可无

image-20240805194308331

​ 于是2009年Angular.js横空出世,带来了全新的MVVM设计模式,让开发者眼前一亮,除了M和V层以外,就是这个VM层啦,即:viewModel层。MVVM设计模式的核心思想就是不让Model和View这两层直接通信,而是通过VM层来连接。

MVVC

image-20240805194438381

Vue-MVVM

image-20240805194555190

MVVM设计模式比MVP模式的优势:

  • ViewModel能够观察到数据的变化,并对视图对应的内容进行自动更新
  • ViewModel能够监听到视图的变化,并能够通知数据发生变化

虽然最早提出MVVM模式的是Angular.js,但是Vue把MVVM设计模式发扬光大了,Vue也成为了当下最主流的前端框架之一。Vue官网上的一段话:虽然没有完全遵循 MVVM 模型,但是 Vue 的设计也受到了它的启发。因此在文档中经常会使用 vm (ViewModel 的缩写) 这个变量名表示组件实例。MVVM 模型中 M 和 V 不能直接操作,需要VM层加持。但Vue比较灵活,可以直接去操作原生DOM,也就是直接去操作V层。

【视图影响数据-数据影响视图】

 <div id="app">
    {{ message }}
    <input type="text" v-model="message">   <!-- View -->
  </div>
  <script>

    let app = Vue.createApp({
      data(){
        return {
          message: 'hello world'  /*  Model */
        }
      }
    })
    let vm = app.mount('#app')
  </script>

3.选项式API的编程风格与优势

选项式API,即:options API

let vm = createApp({
  methods: {},
  computed: {},
  watch: {},
  data(){},
  mounted(){}
})

这种写法的优势:

  • 只有一个参数,不会出现参数顺序的问题,随意调整配置的位置
  • 非常清晰,语法化特别强
  • 非常适合添加默认值的

4.声明式渲染及响应式数据实现原理

Vue.js 的核心是一个允许采用简洁的模板语法来声明式地将数据渲染进 DOM 的系统:

<div id="counter">
  Counter: {{ counter }}
</div>
const Counter = {
  data() {
    return {
      counter: 0
    }
  }
}
Vue.createApp(Counter).mount('#counter')

声明式编程:不需要编写具体是如何实现的,直接调用声明就可以实现功能。SQL就是比较经典的声明式语言:

SELECT * from user WHERE username = xiaoming
for(var i=0;i<user.length;i++)
{
    if(user[i].username == "xiaoming")
    {
     print("find");
     break;
    }
}

注意:数据是通过 模板语法来完成的,模板语法支持编写JS表达式

响应式数据实现的底层原理:利用JS的Proxy对象。Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,其实就是直接监控值的修改,当值发生改变的时候,可以监控到。

<div id="app"></div> 
<script>
    let data = new Proxy(
        {
            message: "hello",
        },
        {
            get(target) {
                console.log("get")
                return target.message
            },
            set(target, key, value) {
                console.log("set")
                app.innerHTML = value;
            },
        }
    )
    app.innerHTML = data.message;
    setTimeout(() => {
        data.message = "hi"
    }, 2000)
</script>

5.指令系统与事件方法及传参处理

指令系统就是通过自定义属性实现的一套功能,也是声明式编程的体现。

image-20240805201750407

通常在标签上添加 v-* 字样,常见的指令有:

  • v-bind -> 操作标签属性,可通过 : 简写
  • v-on -> 操作事件,可通过 @ 简写
<div id="app">
    <p :title="message">这是一个段落</p>
    <button @click=" message = 'hi' ">点击</button>
</div>
{{ message }}
<script>
    let vm = Vue.createApp({
        data(){
            return {
                message: 'hello'
            }
        }
    }).mount('#app')
</script>

如何添加事件方法,通过methods选项API实现,并且Vue框架已经帮我们帮事件传参机制处理好了。

$event 【对象】 拿到信息

Vue帮我们处理好了如何进行事件传参处理,提供了内部的 $event语法来获取event对象

<div id="app">
	<button @click="toClick($event, 123)">点击</button>
</div>
<script>
    let vm = Vue.createApp({
      methods: {
        toClick(ev, num){
            
        }
      }
    }).mount('#app')
</script>

6.计算属性与侦听器区别与原理

计算属性的目的:简化模板语法中的逻辑编写,使视图更加清晰,并具备语义化

计算属性

模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护,所以过于复杂的逻辑可以移植到计算属性中进行处理。

<div id="app">
{{ reverseMessage }}
</div>
<script>
    let vm = Vue.createApp({
      data(){
        return {
          message: 'hello'
        }
      },
      computed: {
        reverseMessage(){
          return this.message.split('').reverse().join('')
        }
      }
    }).mount('#app')
</script>

计算属性与方法比较像,如下所示:

<div id="app">
{{ reverseMessageMethod() }}<br>
{{ reverseMessageMethod() }}<br>
{{ reverseMessage }}<br>
{{ reverseMessage }}<br>
</div>
<script>
    let vm = Vue.createApp({
        data(){
            return {
                message: 'hello world'
            }
        },
        methods: {
            reverseMessageMethod(){
                console.log(1);
                return this.message.split(' ').reverse().join(' ');
            }
        },
        computed: {
            reverseMessage(){
                console.log(2);
                return this.message.split(' ').reverse().join(' ');
            }
        }
    }).mount('#app');
</script>

计算属性跟方法相比,具备缓存的能力,而方法不具备缓存,所以上面代码执行完,会弹出两次1和一次2。

注意:默认是只读的,一般不会直接更改计算属性,如果想更改也是可以做到的,通过Setter写法实现,官方地址。

既然计算属性编写的是一个函数,而调用的时候以函数名的形式进行使用,其实实现起来也不是特别难的事情:

let computed = {
    num(){
        return 123;
    }
}
let vm = {}
for(let attr in computed){
    Object.defineProperty(vm, attr, {
        value: computed[attr]()
    })
}

侦听器

虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。侦听器的目的:侦听器用来观察和响应Vue实例上的数据变动,类似于临听机制+事件机制。当有一些数据需要随着其它数据变化而变化时,就可以使用侦听器。

<div id="app">
    {{ message }}
</div>
<script>
    let vm = Vue.createApp({
        data(){
            return {
                message: 'hello'
            }
        },
        watch: {
            message(newVal, oldVal){
            }
        }
    }).mount('#app')
</script>

有时候,计算属性 和 侦听器 往往能实现同样的需求,那么他们有何区别呢?

  • 计算属性适合:多个值去影响一个值的应用;而侦听器适合:一个值去影响多个值的应用
  • 侦听器支持异步的程序,而计算属性不支持异步的程序

7.条件渲染与列表渲染及注意点

条件渲染

v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 truthy 值的时候被渲染。

在 JavaScript 中,truthy(真值)指的是在布尔值上下文中,转换后的值为真的值。所有值都是真值,除非它们被定义为 falsy 假值(即除 false、0、-0、0n、“”、null、undefined 和 NaN 以外皆为真值)。

<div id="app">
    <div v-if="isShow">aaaaa</div>
    <div v-else>bbbbb</div>
</div>
<script>
    let vm = Vue.createApp({
        data(){
            return {
                isShow: 0
            }
        }
    }).mount('#app');
</script>

列表渲染

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

<div id="app">
	<div v-for="item, index in list">{{ item }}, {{ index }}</div>
    <div v-for="value, key, index in info">{{ value }}, {{ key }}, {{ index }}</div>
    <div v-for="item in num">{{ item }}</div>
    <div v-for="item in text">{{ item }}</div>
</div>
<script>
    let vm = Vue.createApp({
        data(){
            return {
                list: ['a', 'b', 'c'],
                info: { username: 'xiaoming', age: 20 },
                num: 10,
                text: 'hello'
            }
        }
    }).mount('#app');
</script>

条件渲染与列表渲染需要注意的点

  • 列表渲染需要添加key属性,用来跟踪列表的身份
  • v-if 和 v-for 尽量不要一起使用,可利用计算属性来完成筛选这类功能(因为v-if优先级高于v-for,这样v-if拿不到v-for中的item属性)
  • template标签起到的作用,形成一个整体容器

8.class样式与style样式的三种形态

操作元素的 class 列表和内联样式是数据绑定的一个常见需求。因为它们都是 attribute,所以我们可以用 v-bind 处理它们:只需要通过表达式计算出字符串结果即可。不过,字符串拼接麻烦且易错。因此,在将 v-bind 用于 class 和 style 时,Vue.js 做了专门的增强。表达式结果的类型除了字符串之外,还可以是对象或数组。

  • 字符串
  • 数组
  • 对象
  <div id="app">
    <div :class="myClass">aaaaa</div>
    <div :style="myStyle">bbbbb</div>
  </div>

let vm = Vue.createApp({
    data() {
        return {
            myClass1: 'box box2',
            myClass2: ['box', 'box2'],
            myClass3: { box: true, box2: true },
            myStyle1: 'background: red; color: white;',
            myStyle2: ['background: red', 'color: white'],
            myStyle3: { background: 'red', color: 'white' },
        }
    },
}).mount("#app")

数组和对象的形式要比字符串形式更加的灵活,也更容易控制变化。

9.表单处理与双向数据绑定原理

表单是开发过程中经常要进行操作的,一般需要收集表单数据,发送给后端,或者把后端的数据进行回显等。在Vue中是通过v-model指令来操作表单的,可以非常灵活的实现响应式数据的处理。

<div id="app">
	<input type="text" v-model="message"> {{ message }}
</div>
<script>
let vm = Vue.createApp({
    data() {
        return {
            message: 'hello'
        }
    }
}).mount("#app")
</script>

**【原理】**尽管有些神奇,但 v-model 本质上不过是语法糖。可通过value属性 + input事件来实现同样的效果。

<div id="app">
	<input type="text" :value="message" @input="message = $event.target.value"> {{ message }}
</div>
<script>
let vm = Vue.createApp({
    data() {
        return {
            message: 'hello'
        }
    }
}).mount("#app")
</script>

v-model除了可以处理输入框以外,也可以用在单选框、复选框、以及下拉菜单中。

<div id="app">
    <input type="checkbox" v-model="fruits" value="苹果">苹果<br>
    <input type="checkbox" v-model="fruits" value="西瓜">西瓜<br>
    <input type="checkbox" v-model="fruits" value="哈密瓜">哈密瓜<br>
    {{ fruits }
    <input type="radio" v-model="gender" value="女">女<br>
    <input type="radio" v-model="gender" value="男">男<br>
    {{ gender }}
    <select v-model="city">
      <option value="北京">北京</option>
      <option value="上海">上海</option>
      <option value="杭州">杭州</option>
    </select>
    {{ city }}
</div>
<script>
    let vm = Vue.createApp({
        data(){
            return {
                fruits: ['西瓜', '哈密瓜'],
                gender: '男',
                city: '杭州'
            }
        }
    }).mount('#app');
</script>

10.生命周期钩子函数及原理分析

每个组件在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。

简单来说生命周期钩子函数就是回调函数,在Vue的某个时机去调用对应的回调函数。就像定时器一样,谁调用的定时器的回调函数呢?其实就是定时器内部在调用的。

setTimeout(()=>{
	console.log('2秒后被执行了');
}, 2000)

官方提供的**生命周期图示**

生命周期可划分为三个部分:

  1. 初始阶段:beforeCreate、created、beforeMount、mounted
  2. 更新阶段:beforeUpdate、updated
  3. 销毁阶段:beforeUnmout、unmounted
beforeCreate(){
 console.log( this.message );
 console.log( app.innerHTML );
},
// 响应式数据准备好后触发的生命周期
created(){
 console.log( this.message );   // ✔
 console.log( app.innerHTML );
},
beforeMount(){
 console.log( this.message );   // ✔
 console.log( app.innerHTML );
},
// 等DOM加载完毕后触发的生命周期
mounted(){
 console.log( this.message );   // ✔
 console.log( app.innerHTML );  // ✔
},
beforeUpdate(){   // 在更新数据的时候会触发的生命周期
 console.log( this.message );
 console.log( app.innerHTML ); 
},
updated(){
 console.log( this.message );
 console.log( app.innerHTML );
},
beforeUnmount(){ //卸载时候触发
 console.log( this.message );
 console.log( app.innerHTML );
},

注:一般在,created,mounted中都可以发送数据请求,但是,大部分时候,会在created发送请求。因为这样可以更短的时间去响应数据。

11.搜索关键词加筛选条件的综合案例

案例图示如下

准备好案例的JSON数据

[
  {
    "id": 1,
    "name": "小明",
    "gender": "女",
    "age": 20
  },
  {
    "id": 2,
    "name": "小强",
    "gender": "男",
    "age": 18
  },
  {
    "id": 3,
    "name": "大白",
    "gender": "女",
    "age": 25
  },
  {
    "id": 4,
    "name": "大红",
    "gender": "男",
    "age": 22
  }
]

参考代码

<style>
    .active-gender{
      background: red;
    }
</style>
<div id="app">
    <input type="text" v-model="message">
    <button :class="activeGender('全部')" @click="handleGender('全部')">全部</button>
    <button :class="activeGender('男')" @click="handleGender('男')">男</button>
    <button :class="activeGender('女')" @click="handleGender('女')">女</button>
    <ul>
      <li v-for="item in filterList" :key="item.id">{{ item.name }}, {{ item.gender }}, {{ item.age }}</li>
    </ul>
</div>
<script>
    let vm = Vue.createApp({
      data() {
        return {
          list: [],
          message: '',
          gender: '全部'
        }
      },
      created(){
        fetch('./02-data.json').then((res)=> res.json()).then((res)=>{
          this.list = res;
        })
      },
      computed: {
        filterList(){
          return this.list
                  .filter((v)=> v.name.includes(this.message))
                  .filter((v)=> v.gender === this.gender || '全部' === this.gender);
        }
      },
      methods: {
        activeGender(gender){
          return { 'active-gender': this.gender === gender };
        },
        handleGender(gender){
          this.gender = gender;
        }
      }
    }).mount('#app');
</script>

总结内容

  • 了解核心思想,例如:MVVM设计模式、选项式API优势
  • 了解Vue3各个选项的用法,例如:data、methods、computed等
  • 掌握常见的指令:v-bind、v-on、v-if、v-for等
  • 掌握样式操作、表单操作等行为
  • 了解Vue3的生命周期钩子函数,及如何使用

二、Vue3组件应用及单文件组件 - 抽象独立的代码

1.介绍

全面了解Vue3组件的相关概念,掌握组件之间的通信以及如何封装一个可复用的组件。了解什么是单文件组件SFC,以及脚手架的使用和底层实现机制,对工程化有一定的认知。

课程安排

  • 02-组件的概念及组件的基本使用方式
  • 03-组件之间是如何进行互相通信的
  • 04-组件的属性与事件是如何进行处理的
  • 05-组件的内容是如何组合与分发处理的
  • 06-仿Element Plus框架的el-button按钮组件实现
  • 07-单文件组件SFC及Vue CLI脚手架的安装使用
  • 08-脚手架原理之webpack处理html文件和模块打包
  • 09-脚手架原理之webpack启动服务器和处理sourcemap
  • 10-脚手架原理之webpack处理样式模块和图片模块
  • 11-脚手架原理之webpack处理单文件组件及loader转换
  • 12-Vite3介绍及基本使用
  • 13-仿Element Plus的el-rate评分组件实现(单文件组件)

课前了解

什么是Vite

  • Vite是下一代的前端工具链,即Vue新的项目脚手架,跟Vue CLI功能类似,只不过底层实现的机制不同,官方更推荐在项目开发中使用Vite来替换Vue CLI,因为Vite启动速度更快,不过Vite目前还不算稳定,升级速度较快,2022年7月,Vite发布3.0版本。

Vite与Vue CLI的区别

  • Vue CLI底层是基于Webpack工具实现的,前面也已经介绍过了,并实现了一个基于webpack的简易脚手架。那么Vite是一个轻量级的、速度极快的构建工具,基于浏览器原生ESM的能力,它省略了开发环境的打包过程,利用浏览器去解析imports,在服务端按需编译返回,下面通过图示对比两个脚手架的底层区别。

image-20240807214609594

image-20240807214625312

  • 通过图示可以发现,在开发中Vite没有打包的过程,所以启动速度和编译速度都是很快的。Vue-CLI这种需要打包文件到一起的方式也并不是一无是处,打包器支持了动态模块热替换(HMR):允许一个模块“热替换”它自己,而不会影响页面其余部分,这大大改进了开发体验。

Vite脚手架的基本使用

  1. 安装脚手架
# npm 6.x
npm create vite@latest vite-study
# yarn
yarn create vite vite-study
# pnpm
pnpm create vite vite-study
  1. 选择框架:因为Vite可以和很多框架配合使用,所以这里我们选择:Vite + Vue
? Select a framework: » - Use arrow-keys. Return to submit.
    Vanilla
  > Vue
    React
    Preact
    Lit
    Svelte
  1. 选择变体:这里先选择默认的JavaScript形式,后面章节会通过视频的方式来演示自定义形式
? Select a variant: » - Use arrow-keys. Return to submit.
  > JavaScript
    TypeScript
    Customize with create-vue
    Nuxt
  1. 进入项目:安装第三方模块,并启动项目
cd vite-study
npm install
npm run dev
    VITE v3.1.0 ready in 408 ms
        ➜ Local: http://127.0.0.1:5173/
        ➜ Network: use --host to expose

image-20240807215037990

目录结构

  • Vite的目录结构与Vue CLI的目录结构基本相同,甚至比VueCLI还要简单,打开根目录下的index.htm1页面,代码如下:

    image-20240807215148946

其中'<script type="module"src="/src/main.js"></script>这句直接采用原生ESM的方式进行使用,避免了文件合并打包的l处理,运行速度有了明显的提升。关于更多内容,会在后续章节中进行讲解。

2.组件的概念及组件的基本使用方式

组件的概念

组件是带有名称的可复用实例,通常一个应用会以一棵嵌套的组件树的形式来组织,比如:页头、侧边栏、内容区等组件。

image-20240811142933297

组件可以拥有自己独立的结构,样式,逻辑。这样对于后期的维护是非常方便的。下面给出评分组件与按钮组件的抽离过程。

image-20240811143132834

组件的命名方式与规范

  • 定义组件可通过驼峰、短线两种风格定义
  • 调用组件推荐短线方式
<div id="app">
    <my-head></my-head>
</div>
<script>
let app = Vue.createApp({
    data(){
        return {
        }
    }
})
app.component('my-head', {//添加组件
    //组件结构
    template: `
    <header>
        <div>{{ message }}</div>
        <h2>logo</h2>
        <ul>
            <li>首页</li>
            <li>视频</li>
            <li>音乐</li>
        </ul>
    </header>`,
    data(){
    	return {
        	message: 'hello world'
        }
    }
});
let vm = app.mount('#app');
</script>

根组件

app容器可以看成根组件,所以根组件跟普通组件都具备相同的配置信息,例如:data、computed、methods等等选项。

<div id="app">
	<my-head></my-head>
</div>
<script>
    // 根组件
    let RootApp = {
      data(){
        return {
        }
      }
    };
    // MyHead组件
    let MyHead = {
      template: `
        <header>
          <div>{{ message }}</div>
          <h2>logo</h2>
          <ul>
            <li>首页</li>
            <li>视频</li>
            <li>音乐</li>
          </ul>
        </header>
      `
    };
    let app = Vue.createApp(RootApp)
    app.component('MyHead', MyHead);
    let vm = app.mount('#app');
  </script>

根组件与MyHead组件形成了父子组件。

局部组件与全局组件

局部组件只能在指定的组件内进行使用,而全局组件可以在容器app下的任意位置进行使用。

3.组件之间是如何进行互相通信的

上一个小节中,我们了解了组件是可以组合的,那么就形成了父子组件,父子组件之间是可以进行通信的, 那么为什么要通信呢?主要是为了让组件满足不同的需求。

image-20240811144502320

父子通信

最常见的通信方式就是父传子,或者子传父,那么父传子通过props实现,而子传父则通过emits自定义事件实现。

image-20240811144618942

<div id="app">
    <my-head :count="count" @custom-click="handleClick"></my-head>
</div>
<script>
    let app = Vue.createApp({
        data(){
            return {
                count: 10
            }
        },
        methods: {
            handleClick(data){
              console.log(data);
            }
        }
    })
    app.component('MyHead', {
        props: ['count'],
        emits: ['custom-click'], 
        template: `
        <header>
          <div>{{ count }}</div>
          <h2>logo</h2>
          <ul>
            <li>首页</li>
            <li>视频</li>
            <li>音乐</li>
    	  </ul>
    	</header>`,
        mouted(){
        	this.$emit('custom-click', 'MyHead Data')//
    	}
    });
    let vm = app.mount('#app');
</script>

父子通信需要注意的点

  • 组件通信的props是可以定义类型的,在运行期间会进行检测
  • 组件之间的数据是单向流动的,子组件不能直接修改传递过来的值
  • 但是有时候也需要数据的双向流动,可利用v-model来实现

4.组件的属性与事件是如何进行处理的

​ 有时候组件上的属性或事件并不想进行组件通信,那么Vue是如何处理的呢?

组件的属性与事件

​ **默认不通过props接收的话,属性会直接挂载到组件容器上,事件也是如此,会直接挂载到组件容器上。**可通过 inheritAttrs 选项阻止这种行为,通过指定这个属性为false,可以避免组件属性和事件向容器传递。可通过 $attrs 内置语法,给指定元素传递属性和事件,代码如下:

<div id="app">
    <my-head title="hello world" class="box" @click="handleClick"></my-head>
</div>
<script>
    let app = Vue.createApp({
      data(){
        return {
        }
      },
      methods: {
        handleClick(ev){
          console.log(ev.currentTarget);
        }
      }
    })
    app.component('MyHead', {
      template: `
        <header>
          <h2 v-bind:title="$attrs.title">logo</h2>
          <ul v-bind:class="$attrs.class">
            <li>首页</li>
            <li>视频</li>
            <li>音乐</li>
          </ul>
        </header>
      `,
      mounted(){
        console.log( this.$attrs );   // 也可以完成父子通信操作
      },
      inheritAttrs: false   // 阻止默认的属性传递到容器的操作
    });
    let vm = app.mount('#app');
</script>

$attrs也可以实现组件之间的间接通信。

5.组件的内容是如何组合与分发处理的

​ 在前面的小节中,我们学习了组件之间的通信,让组件之间实现了不同的需求,我们通过给组件添加不同的属性来实现。那么在Vue中如何去传递不同的组件结构呢?这就涉及到了组件内容的分发处理。

插槽slot

​ 在Vue中是通过插槽slot方式来进行分发处理的,Vue 实现了一套内容分发的 API,这套 API 的设计灵感源自 Web Components 规范草案,将 slot 元素作为承载分发内容的出口。

<div id="app">
	<my-head>
    	<p>logo</p>
    </my-head>
</div>
<script>
    let app = Vue.createApp({
      data(){
        return {
          message: 'hello'
        }
      }
    })
    app.component('MyHead', {
      data(){
        return {
        };
      },
      template: `
        <header>
          <slot></slot>
        </header>`,
    });
    let vm = app.mount('#app');
</script>

组件内的结构,即<p>logo</p>会被分发到<slot></slot>所在的区域。

内容分发与插槽的注意点

  • 渲染作用域 -> 插槽只能获取当前组件的作用域
  • 具名插槽 -> 处理多个插槽的需求,通过v-slot指令实现,简写为#
  • 作用域插槽 -> 希望插槽能访问子组件的数据

完整代码如下:

<div id="app">
    <my-head>
      <template #title>
        <p>logo, {{ message }}, {{ count }}</p>
      </template>
      <template #list="{ list }">
        <ul>
          <li v-for="item in list">{{ item }}</li>
        </ul>
      </template>
    </my-head>
  </div>
  <script>
    let app = Vue.createApp({
      data(){
        return {
          message: 'hello'
        }
      }
    })

    app.component('MyHead', {
      data(){
        return {
          count: 123,
          list: ['首页', '视频', '音乐']
        };
      },
      template: `
        <header>
          <slot name="title"></slot>
          <hr>
          <slot name="list" :list="list"></slot>
        </header>
      `,
    });
    let vm = app.mount('#app');
</script>

6.仿Element Plus框架的el-button按钮组件实现

本小节利用前面学习的组件通信知识,来完成一个仿Element Plus框架的el-button按钮组件实现。仿造的地址:uhttps://element-plus.org/zh-CN/component/button.html

实现需求

  • 按钮类型
  • 按钮尺寸
  • 按钮文字
  • 添加图标

完整代码如下:

<style>
    .el-button{
      display: inline-flex;
      justify-content: center;
      align-items: center;
      line-height: 1;
      height: 32px;
      white-space: nowrap;
      cursor: pointer;
      background-color: #ffffff;
      border: 1px solid #dcdfe6;
      border-color: #dcdfe6;;
      color: #606266;
      -webkit-appearance: none;
      text-align: center;
      box-sizing: border-box;
      outline: none;
      transition: .1s;
      font-weight: 500;
      user-select: none;
      vertical-align: middle;
      padding: 8px 15px;
      font-size: 14px;
      border-radius: 4px;
    }
    .el-button--primary{
      color: white;
      background-color: #409eff; 
    }
    .el-button--success{
      color: white;
      background-color: #67c23a; 
    }
    .el-button--large{
      height: 40px;
      padding: 12px 19px;
      font-size: 14px;
    }
    .el-button--small{
      height: 24px;
      padding: 5px 11px;
      font-size: 12px;
      border-radius: 3px;
    }
</style>
<link rel="stylesheet" href="./iconfont/iconfont.css">
<script src="../vue.global.js"></script>
<div id="app">
    <el-button>default</el-button>
    <el-button type="primary" size="small">Primary</el-button>
    <el-button type="success" size="large">Success</el-button>
    <el-button type="success" size="large">
      <template #icon>
        <i class="iconfont iconfangdajing"></i>
      </template>
      Success
    </el-button>
  </div>
<script>
    let ElButton = {
        data(){
            return {
                buttonClass: {
                    'el-button': true,
                    [`el-button--${this.type}`]: this.type !== '', 
                    [`el-button--${this.size}`]: this.size !== '' 
                }
            }
        },
        props: {
            type: {
                type: String,
                default: ''
            },
            size: {
                type: String,
                default: ''
            }
        },
        template: `
        <button :class="buttonClass">
          <slot name="icon"></slot>
          <slot></slot>
    	</button>`
    };

    let vm = Vue.createApp({
        data(){
            return {
            }
        },
        components: {
            ElButton
        }
    }).mount('#app');
</script>

7.单文件组件SFC及Vue CLI脚手架的安装使用

Vue 单文件组件(又名 *.vue 文件,缩写为 SFC)是一种特殊的文件格式,它允许将 Vue 组件的模板、逻辑 与 样式封装在单个文件中。

为什么要使用 SFC

使用 SFC 必须使用构建工具,但作为回报带来了以下优点:

  • 使用熟悉的 HTML、CSS 和 JavaScript 语法编写模块化的组件
  • 让本来就强相关的关注点自然内聚
  • 预编译模板,避免运行时的编译开销
  • 组件作用域的 CSS
  • 在使用组合式 API 时语法更简单
  • 通过交叉分析模板和逻辑代码能进行更多编译时优化
  • 更好的 IDE 支持,提供自动补全和对模板中表达式的类型检查
  • 开箱即用的模块热更新 (HMR) 支持

如何支持SFC

可通过项目脚手架来进行支持,Vue支持Vite脚手架和Vue CLI脚手架。这里我们先来介绍Vue CLI的基本使用方式。

# 安装
npm install -g @vue/cli
# 创建项目
vue create vue-study
# 选择default
default (babel, eslint)
# 启动脚手架
npm run serve

通过localhost:8080进行访问。

脚手架文件的组成

  • src/main.js -> 主入口模块
  • src/App.vue -> 根组件
  • src/components -> 组件集合
  • src/assets -> 静态资源

单文件的代码组成

  • template -> 编写结构
  • script -> 编写逻辑
  • style -> 编写样式
<template>
  <div class="hello">

  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  props: {
    msg: String
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
  margin: 40px 0 0;
}
</style>

其中style中的scoped属性,可以让样式成为局部的,不会影响到其他组件,只会作用于当前组件生效,同时在脚手架下支持常见的文件进行模块化操作,例如:图片、样式、.vue文件等。

8.脚手架原理之webpack处理html文件和模块打包

为了更好的理解项目脚手架的使用,我们来学习一下webpack工具,因为脚手架的底层就是基于webpack工具实现的。

安装

webpack工具是基于nodejs的,所以首先要有nodejs环境,其次需要下载两个模块,一个是代码中使用的webpack模块,另一个是终端中使用的webpack-cli模块。

npm install --save-dev webpack
npm install --save-dev webpack-cli

配置文件

通过编写webpack.config.js文件,来编写webpack的配置信息,完成工具的操作行为。webpack最大的优点就是可以把模块化的JS文件进行合并打包,这样可以减少网络请求数,具体是通过entry和output这两个字段来完成的。

// webpack.config.js 
module.exports = {
  entry: {
    main: './src/main.js'
  },
  output: {
    path: __dirname + '/dist',
    clean: true
  }
}

在终端中进行nodejs脚本build的调用,这样去进行webpack执行,需要我们自己配置一下package.json的脚本。

npm run build   # -> webpack

这样在项目目录下就产生了一个 /dist 文件夹,里面有合并打包后的文件。那么我们该如何预览这个文件呢?一般可通过html文件进行引入,然后再通过浏览器进行访问。

但是html的编写还需要我们自己引入要预览的JS文件,不是特别的方便,所以是否可以做到自动完成html的操作呢?答案是可以利用webpack工具的插件HtmlWebpackPlugin来实现。

这样HtmlWebpackPlugin插件是需要安装的,通过npm i HtmlWebpackPlugin来完成。

// webpack.config.js
module.exports = {
    ...,
    plugins: [
        new HtmlWebpackPlugin({
          template: './public/index.html',
          title: 'vue-study'
        }),
        new VueLoaderPlugin()
      ]
}

9.脚手架原理之webpack启动服务器和处理sourcemap

启动服务环境

目前我们的webpack是没有服务环境的,那么如何启动一个web服务器呢?可以通过webpack-dev-server模块,下载使用即可。

npm install webpack-dev-server

安装好后,再package.json中配置scripts脚本,"serve": "webpack-dev-server",然后运行serve脚本。这样就会启动一个http://localhost:8080的服务。

当开启了web服务后,咱们的/dist文件可以不用存在了,服务会把dist的资源打入到内存中,这样可以大大加快编译的速度,所以/dist文件夹可以删除掉了,不影响服务的启动和访问。

处理sourcemap

socurcemap启动映射文件的作用,可以通过浏览器查找到原始的文件,这样对于调试是非常有帮助的,配置如下:

module.exports = {
    devtool: 'inline-source-map'
}

10.脚手架原理之webpack处理样式模块和图片模块

loader预处理文件

在模块化使用中,默认只会支持JS文件,那么怎么能够让其他类型的文件,例如:css文件、图片文件、json文件等等都可以当作模块化进行使用呢?这就需要使用loader技术。

支持css模块化

可以通过安装,css-loader和style-loader这两个模块来支持css模块化操作。其中css-loader作用是让css文件能够import方式进行使用,而style-loader的作用是把css代码抽离到<style>标签中,这样样式就可以在页面中生效。

module.exports = {
    module: {
    	rules: [
            {
                test: /\.css$/i,
                use: ['style-loader', 'css-loader']
            }
        ]
    }
}

注意数组的顺序是先执行后面再执行前面,所以先写style-loader再写css-loader,这样就可以通过import './assets/common.css'在main.js中进行使用。

图片模块

同样的情况,如果让webpack支持图片模块化也要使用对应的loader,不过在最新版本的webpack中已经内置了对图片的处理,所以只需要配置好信息就可以支持图片模块化。

module.exports = {
    module: {
    	rules: [
            ...,
            {
                test: /\.(png|jpg|gif)$/i,
                type: 'asset/resource'
            }
        ]
    }
}

11.脚手架原理之webpack处理单文件组件及loader转换

处理单文件组件

目前我们的webpack还不支持对.vue文件的识别,也不支持.vue模块化使用,所以需要安装一些模块来实现。

npm install vue @vue/complier-sfc vue-loader

vue模块主要是为了让vue功能生效。@vue/complier-sfc是对单文件组件的支持。vue-loader是把单文件组件进行转换。下面看一下webpack的完整配置,如下:

const HtmlWebpackPlugin = require('html-webpack-plugin');
const { VueLoaderPlugin } = require('vue-loader');

module.exports = {
  entry: {
    main: './src/main.js'
  },
  output: {
    path: __dirname + '/dist',
    clean: true
  },
  devtool: 'inline-source-map',
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ['style-loader', 'css-loader']
      },
      {
        test: /\.(png|jpg|gif)$/i,
        type: 'asset/resource'
      },
      {
        test: /\.vue$/i,
        use: ['vue-loader']
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
      title: 'vue-study'
    }),
    new VueLoaderPlugin()
  ],
  mode: 'development'
};

通过配置操作后,目前已经可以完成一个小型的脚手架,支持模块化文件,也支持.vue文件的使用,还可以启动web服务器。

12.仿Element Plus的el-rate评分组件实现(单文件组件)

插件的安装

在完成本小节案例之前,先安装一下vsCode和chrome的相关插件。

  • vsCode Plugin : Vue Language Features (Volar)
  • vsCode Plugin : Vue VSCode Snippets
  • Chrome Plugin: Vue.js devtools

这些插件都有助于vue框架的使用。下面就看一下我们要做的案例吧。

前面我们仿Element Plus实现了一个按钮组件,不过没有在脚手架的环境下,本小节会在脚手架的环境下完成一个仿Element Plus的el-rate评分组件实现。仿造组件的地址:https://element-plus.org/zh-CN/component/rate.html

实现需求

  • 最大分值
  • 选中分值
  • 事件交互
<template>
  <ul class="rate">
    <li v-for="index in max" :key="index" @mouseenter=" $emit('update:modelValue', index) " @mouseleave=" $emit('update:modelValue', initValue) " @click=" initValue = index "><i :class="rateClass(index)"></i></li>
  </ul>
</template>

<script>
import '@/assets/iconfont/iconfont.css'
  export default {
    data(){
      return {
        initValue: this.modelValue
      }
    },
    props: {
      max: {
        type: Number,
        default: 5
      },
      modelValue: {
        type: Number,
        default: 0
      }
    },
    emits: ['update:modelValue'],
    methods: {
      rateClass(index){
        return {
          iconfont: true,
          'icon-xingxing': true,
          active: this.modelValue >= index
        }
      }
    }
  }
</script>

<style scoped>
.rate{
  display: flex;
  list-style: none;
}
.rate i{
  font-size: 30px;
  color: #ccc;
}
.rate .active{
  color: blueviolet;
}
</style>

调用评分组件,如下:

<template>
  <h2>hello vue</h2>
  <el-rate v-model="value1"></el-rate>{{ value1 }}
  <el-rate :max="6" v-model="value2"></el-rate>{{ value2 }}
</template>

<script>
import ElRateVue from './components/ElRate.vue'
export default {
  name: 'App',
  data(){
    return {
      value1: 0,
      value2: 3
    }
  },
  components: {
    ElRate: ElRateVue
  }
}
</script>

章节总结

小伙伴大家好,本章学习了Vue3组件应用及单文件组件 - 抽象独立的代码。

总结内容

  • 全面掌握组件的基本操作:通信、属性、事件、内容分发等
  • 实现可复用的组件,例如:按钮组件、评分组件等
  • 实现可复用的组件,例如:按钮组件、评分组件等
  • 脚手架原理分析,实现一个基于webpack工具的简易脚手架
  • 了解Vite脚手架,下一代前端开发与构建工具

三、Vue3语法系统进阶-全面掌握Vue3特性

1.章节介绍

小伙伴大家好,本章将学习Vue3语法系统进阶 - 全面掌握Vue3特性。

本章学习目标

本章将学习Vue3的高级特性,通俗易懂的方式来理解这些高级知识点,对面试、复杂应用开发及全面了解Vue3框架都有很大的帮助。

课程安排

  • 02-ref属性在元素和组件上的分别使用
  • 03-利用nextTick监听DOM更新后的情况
  • 04-自定义指令与自定义全局属性及应用场景
  • 05-复用组件功能之Mixin混入
  • 06-插件的概念及插件的实现
  • 07-Element Plus框架的安装与使用
  • 08-transition动画与过渡的实现
  • 09-动态组件与keep-alive组件缓存
  • 10-异步组件与Suspense一起使用
  • 11-跨组件间通信方案 Provide_Inject
  • 12-Teleport实现传送门功能
  • 13-虚拟DOM与render函数及Diff算法

2.ref属性在元素和组件上的分别使用

ref属性

​ 前面我们介绍过,Vue是基于MVVM设计模式进行实现的,视图与数据不直接进行通信,但是Vue并没有完全遵循这一原则,而是允许开发者直接进行原生DOM操作。

在Vue中可通过ref属性来完成这一行为,通过给标签添加ref属性,然后再通过vm.$refs来获取DOM,代码如下:

<template>
  <div>
    <h2>ref属性</h2>
    <button @click="handleClick">点击</button>
    <div ref="elem">aaaaaaaaa</div>
    <div ref="elem2">bbbbbbbbb</div>
  </div>
</template>

<script>
  export default {
    methods: {
      handleClick(){
        // ref属性添加到元素身上,可以获取到当前元素的原生DOM
        console.log( this.$refs.elem );
        console.log( this.$refs.elem2 );
      }
    }
  }
</script>

除了可以把ref属性添加给DOM元素外,还可以把ref属性添加给组件,这样可以获取到组件的实例对象,可以间接的实现组件之间的通信,代码如下:

<template>
  <div>
    <h2>ref属性</h2>
    <my-head ref="elem3"></my-head>
  </div>
</template>

<script>
  import MyHead from '@/2_头部组件.vue'
  export default {
    methods: {
      handleClick(){
        // ref属性添加到组件身上,可以获取到当前组件的vm对象(实例对象)
        console.log( this.$refs.elem3 );
        console.log( this.$refs.elem3.message );
        this.$refs.elem3.handleMessage('根组件的数据');
        //$refs 也可以实现间接的父子通信
      }
    }
  }
</script>

2_头部组件.vue文件:

<template>
  <div>
    hello myhead
  </div>
</template>

<script>
  export default {
    data(){
      return {
        message: '头部组件的消息'
      }
    },
    methods: {
      handleMessage(data){
        console.log(data);
      }
    }
  }
</script>

3.利用nextTick监听DOM更新后的情况

​ 本小节我们将学习一下nextTick方法,它的主要作用是将回调推迟到下一个 DOM 更新周期之后执行。在更改了一些数据以等待 DOM 更新后立即使用它。

​ 默认情况下,数据的更新会产生一个很小的异步延迟,所以直接再数据改变后取获取DOM是得不到DOM更新后的结果,而得到的是DOM更新前的结果。

<template>
  <div>
    <h2>hello nextTick</h2>
    <div ref="elem">{{ message }}</div>
  </div>
</template>

<script>
  export default {
    data(){
      return {
        message: 'hello world'
      }
    },
    mounted(){
      setTimeout(()=>{
        this.message = 'hi vue';
        console.log( this.$refs.elem.innerHTML );  // 'hello world'
       
      }, 2000)
    }
  }
</script>

如何才能得到DOM更新后的结果呢,可以有两种方案,第一种就是利用生命周期updated这个钩子函数,第二种就是利用我们讲的nextTick方法,支持两种风格即回调和promise。

<template>
  <div>
    <h2>hello nextTick</h2>
    <div ref="elem">{{ message }}</div>
  </div>
</template>

<script>
  export default {
    data(){
      return {
        message: 'hello world'
      }
    },
    mounted(){
      setTimeout(()=>{
        this.message = 'hi vue';
        /* this.$nextTick(()=>{
          console.log( this.$refs.elem.innerHTML );   // 'hi vue'
        }) */
        this.$nextTick().then(()=>{
          console.log( this.$refs.elem.innerHTML );  // 'hi vue'
        })
      }, 2000)
    },
    updated(){
       console.log( this.$refs.elem.innerHTML );  // 'hi vue'
    }
  }
</script>

4.自定义指令与自定义全局属性及应用场景

除了核心功能默认内置的指令 (例如 v-model 和 v-show),Vue 也允许注册自定义指令,来实现一些封装功能。

自定义指令的实现

首先我们先来实现一个简单的v-color指令,用于给元素添加背景色,代码如下:

<template>
  <div>
    <h2>自定义指令</h2>
    <div @click="handleClick" v-color="color">aaaaaaa</div>
  </div>
</template>

<script>
  export default {
    data(){
      return {
        color: 'red'
      }
    }, 
    //创建局部的自定义指令
    directives: {
       color: {
        mounted(el, binding){ //初始
          el.style.background = binding.value
        },
        updated(el, binding){//更新
          el.style.background = binding.value
        }
      } ,
      color: (el, binding) => {   // el 返回DOM元素  binding 返回值
        el.style.background = binding.value
      }
    }
  }
</script>

这里的回调函数是指令中mounted生命周期和updated生命周期的简写方式。

下面我们来完成一个实际可以应用的指令,按钮权限指令,一般情况下这种指令不会局部使用,而是全局使用,所以可以通过vue来实现一个全局的按钮权限指令,代码如下:

// main.js
app.directive('auth', (el, binding) => {
    let auths = ['edit', 'delete'];
    let ret = auths.includes(binding.value);
    if(!ret){
    	el.style.display = 'none';
    }
});

// demo.vue
<template>
<button v-auth="'edit'">编辑</button>
</template>

自定义全局属性

添加一个可以在应用的任何组件实例中访问的全局 property,这样在引入一些第三方模块的时候,就不用每一次进行import操作,而是直接通过this对象去访问全局属性即可,下面举一个例子,实现一个http的全局属性。

// main.js
app.config.globalProperties.$http = http;

//demo.vue
<script>
export default {
   created(){
       this.$http.get();
   }
}
</script>

5.复用组件功能之Mixin混入

Mixin混入

本小节我们来了解一下mixin混入,它是选项式API的一种复用代码的形式,可以非常灵活的复用功能。

// mymixin.js
const myMixin = {
  data(){
    return {
      message: '复用的数据'
    }
  },
  computed: {
    message2(){
      return '复用的数据2'
    }
  }
};
export {
  myMixin
}
// mymixin.vue
<template>
  <div>
    <h2>mixin混入</h2>
    <div>{{ message }}</div>
    <div>{{ message2 }}</div>
  </div>
</template>
<script>
  import { myMixin } from '@/mymixin.js'
  export default {
    mixins: [myMixin]
  }
</script>

这样就可以直接在.vue中使用这些混入的功能。当然这种方式是局部混入写法,也可以进行全局混入的写法,代码如下:

// main.js
import { myMixin } from '@/mymixin.js'
app.mixin(myMixin)

mixin存在的问题也是有的,那就是不够灵活,不支持传递参数,这样无法做到差异化的处理,所以目前比较推荐的复用操作还是选择使用组合式API中的use函数来完成复用的逻辑处理。后面章节我们会学习到这种组合式API的应用。

6.插件的概念及插件的实现

​ 插件是自包含的代码,通常向 Vue 添加全局级功能。例如:全局方法、全局组件、全局指令、全局mixin等等。基于Vue的第三方模块都是需要通过插件的方式在Vue中进行生效的,比如:Element Plus、Vue Router、Vuex等等。

// myplugin.js
import * as http from '@/http.js'
export default {
  install(app, options){
    console.log(options);
    app.config.globalProperties.$http = http;
    app.directive('auth', (el, binding) => {
      let auths = ['edit', 'delete'];
      let ret = auths.includes(binding.value);
      if(!ret){
        el.style.display = 'none';
      }
    });
    app.component('my-head', {
      template: `<div>hello myhead</div>`
    })
  }
}
// main.js 让插件生效
import myplugin from './myplugin.js'
app.use(myplugin, { info: '配置信息' })

可以看到,让插件生效的语法为app.use,这样就可以跟Vue结合到一起,所以插件就可以独立出去,成为第三方模块。

7.Element Plus框架的安装与使用

前面小节中介绍了自定义插件的实现,那么本小节来看下一比较出名的第三方插件Element Plus如何安装与使用。

Element Plus框架

Element Plus是一套基于PC端的组件库,可以直接应用到很多管理系统的后台开发中,使用前需要先下载安装,除了下载组件库以外,最好也下载组件库对应的icon图标模块,如下:

npm install element-plus @element-plus/icons-vue

接下来把element plus完整引入到Vue中,包装全局组件,全局样式。

import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

app.use(ElementPlus)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
	app.component(key, component)
}

基本使用方式

element plus中提供了非常多的常见组件,例如:按钮、评分、表单控件等等。

<template>
  <div>
    <h2>element plus</h2>
    <el-button @click="handleClick" type="primary" icon="Clock">Primary</el-button>
    <el-button @click="handleClick2" type="success">Success</el-button>
    <el-rate v-model="value1" />
    <el-icon><Clock /></el-icon>
  </div>
</template>
<script>
  import { ElMessage, ElNotification } from 'element-plus';
  export default {
    data(){
      return {
        value1: 3
      }
    },
    mounted(){
      setTimeout(()=>{
        this.value1 = 5;
      }, 2000)
    },
    methods: {
      handleClick(){
        ElMessage.success('提示成功的消息');
      },
      handleClick2(){
        ElNotification({
          title: '邮件',
          message: '您今日的消费记录'
        });
      }
    }
  }
</script>

除了常见的组件外,element plus中也提供了一些逻辑组件,这些逻辑组件是可以直接在JavaScript中进行使用,例如:ElMessage,ElNotification等方法。

8.transition动画与过渡的实现

在Vue中推荐使用CSS3来完成动画效果。当在插入、更新或从 DOM 中移除项时,Vue 提供了多种应用转换效果的方法。

transition动画

Vue中通过两个内置的组件来实现动画与过渡效果,分别是:<transition>和<transition-group>,代码如下:

<template>
  <div>
    <h2>hello transition</h2>
    <button @click=" isShow = !isShow ">点击</button>
    <transition name="slide" mode="out-in">
      <div v-if="isShow" class="box"></div>
      <div v-else class="box2"></div>
    </transition>
  </div>
</template>
<script>
  export default {
    data(){
      return {
        isShow: true
      }
    }
  }
</script>
<style scoped>
.box{
  width: 200px;
  height: 200px;
  background: skyblue;
}
.box2{
  width: 200px;
  height: 200px;
  background: pink;
}
.slide-enter-from{
  opacity: 0;
  transform: translateX(200px);
}
.slide-enter-to{
  opacity: 1;
  transform: translateX(0);
}
.slide-enter-active{
  transition: 1s;
}

.slide-leave-from{
  opacity: 1;
  transform: translateX(0);
}
.slide-leave-to{
  opacity: 0;
  transform: translateX(200px);
}
.slide-leave-active{
  transition: 1s;
}
</style>

其中<transition>组件通过name属性去关联CSS中的选择器,CSS中的选择器主要有6种,分别:

  • v-enter-from:进入动画的起始状态。在元素插入之前添加,在元素插入完成后的下一帧移除。
  • v-enter-active:进入动画的生效状态。应用于整个进入动画阶段。在元素被插入之前添加,在过渡或动画完成之后移除。这个 class 可以被用来定义进入动画的持续时间、延迟与速度曲线类型。
  • v-enter-to:进入动画的结束状态。在元素插入完成后的下一帧被添加 (也就是 v-enter-from 被移除的同时),在过渡或动画完成之后移除。
  • v-leave-from:离开动画的起始状态。在离开过渡效果被触发时立即添加,在一帧后被移除。
  • v-leave-active:离开动画的生效状态。应用于整个离开动画阶段。在离开过渡效果被触发时立即添加,在过渡或动画完成之后移除。这个 class 可以被用来定义离开动画的持续时间、延迟与速度曲线类型。
  • v-leave-to:离开动画的结束状态。在一个离开动画被触发后的下一帧被添加 (也就是 v-leave-from 被移除的同时),在过渡或动画完成之后移除。

默认情况下,进入和离开在两个元素身上是同时执行的,如果想改变其顺序,需要用到mode属性,其中out-in表示先离开再进入,而in-out表示先进入再离开。

9.动态组件与keep-alive组件缓存

动态组件

动态组件可以实现在同一个容器内动态渲染不同的组件,依一个内置组件<component>的is属性的值,来决定使用哪个组件进行渲染。

<template>
  <div>
    <h2>动态组件</h2>
    <button @click=" nowCom = 'my-com1' ">组件1</button>
    <button @click=" nowCom = 'my-com2' ">组件2</button>
    <button @click=" nowCom = 'my-com3' ">组件3</button>
 	<component :is="nowCom"></component>
  </div>
</template>

<script>
import MyCom1 from '@/13_MyCom1.vue'
import MyCom2 from '@/14_MyCom2.vue'
import MyCom3 from '@/15_MyCom3.vue'
  export default {
    data(){
      return {
        nowCom: 'my-com1'
      }
    },
    components: {
      'my-com1': MyCom1,
      'my-com2': MyCom2,
      'my-com3': MyCom3
    }
  }
</script>

keep-alive组件

当我们点击的时候,就会进行组件的切换。在每次切换的过程中都会重新执行组件的渲染,这样组件操作的行为就会还原,而我们如何能够保证组件不变呢?可以利用<keep-alive>对组件进行缓存,这样不管如何切换,都会保持为初始的组件渲染,这样可以很好的保留之前组件的行为。

组件的切换也可以配合<transition>完成动画的切换。

<template>
  <div>
    <h2>动态组件</h2>
    <button @click=" nowCom = 'my-com1' ">组件1</button>
    <button @click=" nowCom = 'my-com2' ">组件2</button>
    <button @click=" nowCom = 'my-com3' ">组件3</button>
    <transition name="slide" mode="out-in">
      <keep-alive>
        <component :is="nowCom"></component>
      </keep-alive>
    </transition>
  </div>
</template>

10.异步组件与Suspense一起使用

异步组件

在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。

在上一个小节的动态组件的基础上,进行异步组件的演示。首先可以打开chrome浏览器的network网络,可以观察到在动态组件切换的时候,network网络中没有进行任何请求的加载,这证明了在初始的时候,相关的动态组件就已经加载好了。

所以对于大型项目来说,如果能实现按需载入的话,那么势必会对性能有所提升,在Vue中主要就是利用defineAsyncComponent来实现异步组件的。

<script>
import { defineAsyncComponent } from 'vue'
export default {
    data(){
        return {
            nowCom: 'my-com1'
        }
    },
    components: {
        'my-com1': defineAsyncComponent(() => import('@/MyCom1.vue')),
        'my-com2': defineAsyncComponent(() => import('@/MyCom2.vue')),
        'my-com3': defineAsyncComponent(() => import('@/MyCom3.vue'))
    }
}
</script>

Suspense组件

由于异步组件是点击切换的时候才去加载的,所以可能会造成等待的时间,那么这个时候可以配合一个loading效果,在Vue中提供了一个叫做<Suspense>的组件用来完成loading的处理。

<template>
	<suspense>
        <component :is="nowCom"></component>
        <template #fallback>
            <div>loading...</div>
		</template>
	</suspense>
</template>

11.跨组件间通信方案 Provide_Inject

跨组件通信方案

正常情况下,我们的组件通信是需要一级一级的进行传递,通过父子通信的形式,那么如果有多层嵌套的情况下,从最外层把数据传递给最内层的组件就非常的不方便,需要一级一级的传递下来,那么如何才能方便的做到跨组件通信呢?

可以采用Provide 和 inject 依赖注入的方式来完成需求,代码如下:

image-20240811223440575

// provide.vue
<script>
export default {
    provide(){
        return {
            message: 'hello provide',
            count: this.count,
            getInfo(data){
                console.log(data);
            }
        }
    }
}
</script>

// inject.vue
<template>
<div>
    hello inject, {{ message }}, {{ count }}
 </div>
</template>

<script>
export default {
    inject: ['message', 'getInfo', 'count'],
    mounted(){
        this.getInfo('hello inject');
    }
}
</script>

Provide与Inject注意点

  • 保证数据是单向流动的,从一个方向进行数据的修改
  • 如果要传递响应式数据,需要把provide改造成工厂模式发送数据

12.Teleport实现传送门功能

Teleport组件

Teleport可以实现传送门功能,也就是说逻辑属于当前组件中,而结构需要在组件外进行渲染,例如:按钮模态框组件。

// 模态框.vue
<template>
  <div>
    <button @click=" isShow = true ">点击</button>
    <teleport to="body">
      <div v-if="isShow">模态框</div>
    </teleport>
  </div>
</template>

<script>
  export default {
    data(){
      return {
        isShow: false
      }
    }
  }
</script>
// 调用模态框.vue
<template>
  <div>
    <h2>传送门</h2>
    <my-modal></my-modal>
  </div>
</template>

<script>
import MyModal from '@/模态框.vue'
  export default {
    components: {
      'my-modal': MyModal
    }
  }
</script>

逻辑组件

但是往往我们需要的并不是普通组件的调用方式,而是逻辑组件的调用方式,那么如何实现逻辑组件呢?代码如下:

//  定义逻辑组件,modal.js

import { createApp } from 'vue';
import ModalVue from '@/模态框.vue';

function modal(){
  let div = document.createElement('div');
  createApp(ModalVue).mount(div);
  document.body.append(div);
}

export default modal;
// 调用逻辑组件
<template>
  <div>
    <h2>传送门</h2>
    <button @click="handleClick">点击</button>
  </div>
</template>

<script>
import modal from '@/modal.js'
export default {
  methods: {
    handleClick(){
      modal();
    }
  }
}
</script>

13.虚拟DOM与render函数及Diff算法

虚拟DOM

Vue框架帮我们完成了大量的DOM操作,那么在底层Vue并没有直接操作真实的DOM,因为真实的DOM直接去操作是非常好性能的,所以最好在JS环境下进行操作,然后在一次性进行真实DOM的操作。

const vnode = {
  type: 'div',
  props: {
    id: 'hello'
  },
  children: [
    /* 更多 vnode */
  ]
}

那么在Vue中是如何把<template>模板中的字符串编译成虚拟DOM的呢?需要用到内置的render函数,这个函数可以把字符串转换成虚拟DOM。

04-03-虚拟DOM

Diff算法

当更新的时候,一个依赖发生变化后,副作用会重新运行,这时候会创建一个更新后的虚拟 DOM 树。运行时渲染器遍历这棵新树,将它与旧树进行比较,然后将必要的更新应用到真实 DOM 上去。

而两个虚拟DOM进行对比的时候,需要加入一些算法提高对比的速度,这个就是Diff算法。

04-04-Diff算法

在脚手架下我们推荐使用template来完成结构的编写,那么也可以直接通过render函数进行虚拟DOM的创建,代码如下:

<!-- <template>
  <div>
    <h2>render</h2>
  </div>
</template> -->
<script>
  import { h } from 'vue';
  export default {
    render(){
      return h('div', h('h2', 'render2'))
    }  
  }
</script>

<style scoped>

</style>

章节总结

小伙伴大家好,本章学习了Vue3语法系统进阶 - 全面掌握Vue3特性。

总结内容

  • 学习:ref属性、nextTick方法
  • 学习:自定义指令、Mixin混入
  • 学习:插件的概念、Element Plus框架的使用
  • 学习:动画与过渡、动态组件、异步组件
  • 学习:跨组件通信、传送门、虚拟DOM与render函数

四、Vue3组合式API详解 - 大型应用的高端写法

1.章节介绍

小伙伴大家好,本章将学习Vue3组合式API详解 - 大型应用的高端写法。

本章学习目标

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

什么是组合式API

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

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

image-20240813194856470

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

image-20240813195037827

课程安排

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

2.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>

3.事件方法_计算属性 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>

4.生命周期_watch_watchEffect

生命周期钩子函数

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

image-20240813200759198

那么具体的区别如下:

  • 组合式中是没有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的区别是什么呢?

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

5.跨组件通信方案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

6.复用组件功能之use函数

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

// useCounter.js
import { computed, ref } from 'vue';
function useCounter(num){//遵循这个规范 use函数必须以use开头
  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文件中进行功能的使用,从而实现功能的复用逻辑。

7.利用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>

8.利用组合式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 这个文件,到此我们已经准备好了开发前的准备工作。

9.利用组合式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>

10.利用组合式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>

11.利用组合式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>

章节总结

小伙伴大家好,本章学习了Vue3组合式API详解 - 大型应用的高端写法。

总结内容

  • 了解了什么是组合式API,具备怎么的特性
  • 学习常见的组合式API语法,如:ref、reactive、watchEffect等
  • 组合式API之间的通信方案,如:父子组件、跨组件等
  • 通过实战案例,复杂的搜索应用,体验组合式的强大

六、VueRouter路由与Vuex状态管理 - 组织与架构应用

1.章节介绍

小伙伴大家好,本章将学习VueRouter路由与Vuex状态管理 - 组织与架构应用。

本章学习目标

本章将学习Vue3中的路由与状态管理,随着前后端分离式开发的兴起,单页面应用开发(即SPA页面)也越来越流行,所以前端路由与共享状态也越来越重要!

课程安排

  • 02-什么是前端路由以及路由两种模式实现原理
  • 03-路由的基本搭建与嵌套路由模式
  • 04-动态路由模式与编程式路由模式
  • 05-命名路由与命名视图与路由元信息
  • 06-路由传递参数的多种方式及应用场景
  • 07-详解route对象与router对象
  • 08-路由守卫详解及应用场景
  • 09-Vuex状态管理的概念及组件通信的总结
  • 10-Vuex共享状态的基本开发流程
  • 11-Vuex处理异步状态及应用场景
  • 12-Vuex计算属性和辅助函数的使用
  • 13-Vuex-persist对数据进行持久化处理
  • 14-Vuex分割模块及多状态管理
  • 15-组合式API中使用Router和Vuex注意事项
  • 16-Router_Vuex的任务列表综合案例
  • 17-Vue状态管理之Pinia存储库
  • 18-搭建 Vite3 + Pinia2 组合模式

2.什么是前端路由以及路由两种模式实现原理

路由的概念来源于服务端,在服务端中路由描述的是 URL 与处理函数之间的映射关系,当然也会处理不同的URL来展示不同的视图界面。

随着Ajax的盛行,无刷新交互成为了当下的主流,我们更希望在无刷新的情况下完成不同URL来展示不同的视图界面,即在一个页面中完成路由的切换(俗称:单页面应用开发SPA),这就是前端路由。

那么如何做到在一个页面中完成URL与UI的映射关系呢?一般我们有两种实现方案:

  1. hash模式
  2. history模式

hash模式

hash 是 URL 中 hash (#) 及后面的那部分,常用作锚点在页面内进行导航,改变 URL 中的 hash 部分不会引起页面刷新。我们通过 hashchange 事件来监听 hash 值的改变,这样就可以显示不同的UI内容,示例代码如下:

<body>
  <ul>
    <!-- 定义路由 -->
    <li><a href="#/home">home</a></li>
    <li><a href="#/about">about</a></li>
    <!-- 渲染路由对应的 UI -->
    <div id="routerView"></div>
  </ul>
</body>
<script>
window.addEventListener('hashchange', onHashChange)
onHashChange()
function onHashChange () {
  switch (location.hash) {
    case '#/home':
      routerView.innerHTML = 'Home'
      break;
    case '#/about':
      routerView.innerHTML = 'About'
      break;
  }
}
</script>

history模式

history对象提供了pushState方法和popstate事件,pushState方法可以让URL发生改变但并不会引起页面的刷新,而popstate事件则用来监听URL改变的值,这样就可以显示不同的UI内容,示例代码如下:

<body>
  <ul>
    <!-- 定义路由 -->
    <li><a href="/home">home</a></li>
    <li><a href="/about">about</a></li>
    <!-- 渲染路由对应的 UI -->
    <div id="routerView"></div>
  </ul>
</body>
<script>
let linkList = document.querySelectorAll('a[href]')
for(let i=0;i<linkList.length;i++){
    linkList[i].addEventListener('click', function(e){
      e.preventDefault()
      history.pushState(null, '', this.getAttribute('href'))
      onPopState()
    })
}
window.addEventListener('popstate', onPopState)
onPopState()
function onPopState () {
  switch (location.pathname) {
    case '/home':
      routerView.innerHTML = 'Home'
      break;
    case '/about':
      routerView.innerHTML = 'About'
      break;
  }
}
</script>

注意:以上代码要在服务器环境下运行,才会生效。

这种history模式存在一个问题,那就是当刷新页面的时候就会出现找不到页面的情况,即:Cannot GET /home。这主要是因为history模式的URL地址跟普通的URL地址没有任何区别,刷新的时候服务器会去找相关的资源,我们在服务器上根本就没有这个资源,就会出现找不到的现象。

解决这个问题,你需要做的就是在你的服务器上添加一个简单的回退路由。如果 URL 不匹配任何静态资源,它应提供与你的应用程序中的 index.html 相同的页面。漂亮依旧!

下面是nginx服务器实现的示例:

location / {
  try_files $uri $uri/ /index.html;
}

Vue框架给我们提供了一个第三方的路由框架,即:vue-router,官网地址:https://router.vuejs.org/zh/index.html。vue-router提供了两种路由模式,可自由选择,而且在开发阶段,脚手架还帮我们处理了history找不到页面的情况。

3.路由的基本搭建与嵌套路由模式

在前面的小节中,已经介绍了什么是前端路由以及前端路由所具备的特点。本小节就来对路由进行实际搭建吧。

vue路由的搭建

路由在vue中属于第三方插件,需要下载安装后进行使用。版本说明一下,Vue3搭配的是Vue Router4,目前正常安装的话,就是路由4的版本。如下:

npm install vue-router

安装好后,需要对路由进行配置,并且与Vue进行结合,让路由插件生效。在/src/router/index.js创建配置文件。

image-20240814201102307

可以通过 createWebHistory()来创建history模式的路由,也可以通过createWebHashHistory()来创建hash模式的路由。那么在浏览器中输入的URL该如何展示对应的组件呢?可以通过router-view这个组件来实现。

除了router-view进行展示外,还可以通过router-link方式进行声明式的路由跳转,代码如下:

<template>
  <div>
    <router-link to="/">首页</router-link> | 
    <router-link to="/about">关于</router-link>
    <router-view></router-view>
  </div>
</template>

嵌套路由模式

往往我们的路由是比较复杂的,需要的层级也比较多,那么就会产生嵌套路由的模式,比如:localhost:8080/about/foo和localhost:8080/about/bar

import Foo from '@/views/Foo.vue'
import Bar from '@/views/Bar.vue'
const routes = [
    {
        path: '/about',
        component: About,
        children: [
            {
                path: 'foo',
                component: Foo
            },
            {
                path: 'bar',
                component: Bar
            }
        ]
    }
]

可以看到嵌套路由是通过children属性来完成的,那么对于这种嵌套路由写法,我们对应的router-view也要在一级路由对应页面中添加一个,这样才能切换显示二级路由所对应的页面。

4.动态路由模式与编程式路由模式

动态路由模式

所谓的动态路由其实就是不同的URL可以对应同一个页面,这种动态路由一般URL还是有规律可循的,所以非常适合做类似于详情页的功能。

image-20240814201915086

// router/index.js,定义动态路由

const routes = [
  {
  	path: 'foo/:id',
    component: Foo
  }
]

// views/Foo.vue,获取动态路由的id
export default {
  mounted(){
      console.log( this.$route.params.id );
  }
}

编程式路由

前面介绍过如何在页面中对路由进行切换,可以采用声明式写法router-link来完成,但是往往这种方式不够灵活,首先不能更灵活的控制结构和样式,其次不能自动进行路由的跳转,必须点击操作才行。

那么编程式路由就会在JavaScript中通过逻辑的方式进行路由跳转,我们把这种写在JS中进行跳转路由的方式叫做编程式路由,这样方式会更加的灵活。

<template>
  <button @click="handleToBar">编程式路由跳转</button>
</template>
<script>
  export default {
    methods: {
      handleToBar(){
        this.$router.push('/about/bar');
      }
    }
  }
</script>

可以看到在动态路由中使用到了一个route对象,而在编程式路由中使用了一个router对象,那么这两个比较重要的路由对象,会在后面的小节中给大家进行详细的讲解。

5.命名路由与命名视图与路由元信息

命名路由

在路由跳转中,除了path 之外,你还可以为任何路由提供 name 的形式进行路由跳转。那么name方式的好处如下:

  • 没有硬编码的 URL
  • params 的自动编码/解码
  • 防止你在 url 中出现打字错误
  • 绕过路径排序(如显示一个)
// router/index.js,定义命名路由
const routes = [
    {
        path: '/about/bar',
        name: 'bar',
        component: Bar
    },
    {
        path: '/about/foo/:id',
        name: 'foo',
        component: Foo
    }
];
<!-- About.vue,使用命名路由跳转 -->
<template>
 	<div>
        <router-link :to="{ name: 'bar' }">bar</router-link>
        <router-link :to="{ name: 'foo', params: {id: 123} }">foo 123</router-link>
    </div>
</template>

name的方式也支持动态路由形式。

命名视图

有时候想同时 (同级) 展示多个视图,而不是嵌套展示,这个时候就非常适合使用命名视图。

image-20240814203018707

通过components字段配置多个组件,根据不同的router-view去渲染不同的组件。

路由元信息

有时,你可能希望将任意信息附加到路由上,如过渡名称、谁可以访问路由等。这些事情可以通过接收属性对象的meta属性来实现。

const routes = [
    {
        path: '/about/bar',
        name: 'bar',
        component: Bar,
        meta: { auth: false }
    },
    {
        path: '/about/foo/:id',
        name: 'foo',
        component: Foo,
        meta: { auth: true }
    }
];

定义好meta元信息后,可通过route对象去访问meta属性。

<!-- Foo.vue -->
<script>
export default {
    mounted(){
        this.$route.meta.auth   // true
    }
}
</script>

6.路由传递参数的多种方式及应用场景

路由传参

我们经常要在路由跳转的时候传递一些参数,这些参数可以帮助我们完成后续的一些开发和一些处理。路由的传递参数主要有以下三种方式:

  • query方式(显示) -> $route.query
  • params方式(显示) -> $route.params
  • params方式(隐式) -> $route.params

两种显示传递数据的差异点主要为,query是携带辅助信息,而params是界面的差异化。

<!-- About.vue -->
<template>
  <div>
    <router-link :to="{ name: 'bar', query: { username: 'xiaobai' } }">bar</router-link>
    <router-link :to="{ name: 'foo', params: { username: 'xiaoqiang' } }">foo</router-link>
  </div>
</template>

<!-- Bar.vue -->
<script>
    export default {
        mounted(){
            console.log(this.$route.query);
        }
    }
</script>
<!-- foo.vue -->
<script>
    export default {
        mounted(){
            console.log(this.$route.params);
        }
    }
</script>

前两种都是显示传递数据,那么第三种是隐式传递数据,这种方式并不会把数据暴露出来。

<!-- About.vue -->
<template>
  <div>
    <router-link :to="{ name: 'bar', params: { username: 'xiaoqiang' } }">bar</router-link>
  </div>
</template>

<!-- Bar.vue -->
<script>
    export default {
        mounted(){
            console.log(this.$route.params);
        }
    }
</script>

但是这里需要注意以下,隐式发送过来的数据,只是临时性获取的,一旦刷新页面,隐藏的数据就会消失,所以在使用的时候要额外注意以一下。

7.详解route对象与router对象

在前面小节中,我们频繁的使用过route对象和router对象,这两个对象在路由中非常的重要,下面我们来详细的学习一下。

route对象与router对象

首先route对象用来获取路由信息,而router对象用来调用路由方法的。具体区别在于,route对象是针对获取操作的,主要是操作当前路由的,而router对象是针对设置操作的,主要是操作整个路由系统对应的功能。

route对象具体功能如下:

  • fullPath 路径
  • hash 哈希值
  • href 路径
  • matched 路由划分-1-二级
  • meta 权限信息
  • name 名称
  • params 参数
  • path 路由路径
  • query 参数

主要就是一些路由信息,像常见的动态路由参数,query数据,meta元信息,url路径等等。

router对象具体功能如下:

  • addRoute 动态添加配置信息
  • afterEach
  • back 后退-历史记录
  • beforeEach
  • beforeResolve
  • currentRoute
  • forward
  • getRoutes 路由表的拆分{可以循环处理所有路由信息}
  • go
  • hasRoute
  • push
  • removeRoute

主要就是一些方法,动态改变路由表,路由守卫, 历史记录的前进后退,编程式路由等等。

8.路由守卫详解及应用场景

正如其名,vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。这里有很多方式植入路由导航中:全局的,单个路由独享的,或者组件级的。

守卫主要的作用就是在进入到指定路由前做一个拦截,看一下我们是否具备权限,如果有权限就直接进入,如果没权限就跳转到其他页面。

路由守卫分类

一般可以分为三种路由守卫使用的方式:

  • 全局环境的守卫
  • 路由独享的守卫
  • 组件内的守卫

先来看一下如何设置全局的路由守卫,一般在路由配置文件中进行设置。

router.beforeEach((to, from, next)=>{ //进入前的守卫
  if(to.meta.auth){
    next('/');
  }
  else{
    next();
  }
})

其中**to表示需要进入到哪个路由,from表示从哪个路由离开的,那么next表示跳转到指定的页面**。

有时候我们只是想给某一个指定的路由添加守卫,那么可以选择设置路由独享的守卫方式。

const routes = [
    {
        name: 'bar',
        component: Bar,
        beforeEnter(to, from, next){
            if(to.meta.auth){
                next('/');
            }
            else{
                next();
            }
        }
    }
];

还可以通过在**.vue文件中进行路由守卫的设置**,代码如下:

<script>
  export default {
    name: 'FooView',
    beforeRouteEnter(to, from, next){
      if(to.meta.auth){
        next('/');
      }
      else{
        next();
      }
    }
  }
</script>

完整的导航解析流程

  1. 导航被触发。
  2. 在失活的组件里调用 beforeRouteLeave 守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
  5. 在路由配置里调用 beforeEnter。
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter。
  8. 调用全局的 beforeResolve 守卫(2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

9.Vuex状态管理的概念及组件通信的总结

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

通俗点说就是不用考虑组件之间的嵌套关系(例如:父子、祖孙、兄弟等),就可以让组件之间进行互相通信。

06-01-vuex状态管理

组件五把数据存储到Vuex共享状态中,组件六使用数据的话就可以再从Vuex共享状态中进行获取,从而完成组件之间的通信。这种通信方式不需要考虑组件之间的嵌套关系,所以通信起来会更加的灵活。

下面总结一下Vue组件通信的各种方案:

  1. 父子通信,props与emits
  2. 跨组件通信,provide与inject
  3. $attrs方式
  4. $refs方式
  5. Vuex状态管理方式

10.Vuex共享状态的基本开发流程

在上一个小节中,我们介绍了什么是Vuex,本小节我们一起来看一下Vuex共享状态的基本开发流程。首先我们来看一下Vuex的经典流程图。

image-20240814205353202

​ 我们可以看到,基本上就是先准备好一个共享数据state,然后渲染我组件中,通过组件调用dispatch -> actions -> commit -> mutations的方式来进行state修改

那么这里我们先不考虑dispatch -> actions,因为这两个环节是处理异步程序的,那么我们直接组件去调用commit就可以触发mutations中定义的方法,这样在这个方法中进行state的修改。

首先在/src/store/index.js创建一个状态管理的配置文件,然后在main.js中让vuex于vue进行结合,就像我们路由的使用一样。

//  store/index.js
const store = createStore({
  state: {
    count: 0
  },
  mutations: {
    change(state, payload){
      state.count += payload;
    }
  }
});

//  main.js
import store from './store'
app.use(store)

下面看一下如何在页面中显示state数据和如何通过commit修改我们的共享数据,代码如下:

<template>
  <div>
    <button @click="change(5)">点击</button>
    hello home, {{ $store.state.count }}
  </div>
</template>
<script>
    export default {
        name: 'HomeView',
        methods: {
            handleClick(){
                this.$store.commit('change', 5);
            }
        }
    }
</script>

11.Vuex处理异步状态及应用场景

本小节中将讲解一下Vuex中如何处理异步程序,因为在上一个小节中提到过,mutations中只能处理同步,不能处理异步,所以异步的工作就交给了 actions 来完成。

那么如何触发actions中定义的方法呢,就需要通过dispatch进行触发,具体代码如下:

const store = createStore({
  state: {
    count: 0
  },
  actions: {
    change(context, payload){
      setTimeout(()=>{
        context.commit('change', payload)
      }, 1000)
    }
  },
  mutations: {
    change(state, payload){
      state.count += payload;
    }
  }
});
<script>
    export default {
        name: 'HomeView',
        methods: {
            handleClick(){
               this.$store.dispatch('change', 5);
            }
        }
    }
</script>

​ 这样在vue devtools插件中就可以更好的观察到异步数据的变化。那么异步处理的应用场景有哪些呢?异步的获取后端的数据,这样可以利用状态管理来充当MVC架构中的C层,不仅衔接前后端,还能对数据进行共享,可以在切换路由的时候做到减少请求次数,而且状态管理配合本地存储进行数据持久化也是非常的方便。

12Vuex计算属性和辅助函数的使用

Vuex中除了提供常见的异步处理和同步处理外,还提供了一些辅助的操作方式,比如:状态管理下的计算属性和简化操作的辅助函数。

getters计算属性

在Vuex中通过定义getters字段来实现计算属性,代码如下:

const store = createStore({
  state: {
    count: 0
  }
  getters: {
    doubleCount(state){
      return state.count * 2;
    }
  }
});
<template>
  <div>
    {{ count }}, {{ doubleCount }}
  </div>
</template>

当count数据发生改变的手,对应的计算属性doubleCount也会跟着发生改变。

辅助函数

在Vuex中为了让我们使用共享数据或调用方法的时候,更加简单易用,提供了对应的辅助函数,分别为:mapState、mapGetters、mapMutations、mapActions。

<template>
  <div>
    <button @click="change(5)">点击</button>
    hello home, {{ count }}, {{ doubleCount }}
  </div>
</template>

<script>
  import { mapState, mapGetters, mapActions } from 'vuex';
  export default {
    name: 'HomeView',
    methods: {
      ...mapActions(['change'])
    },
    computed: {
      ...mapState(['count']),
      ...mapGetters(['doubleCount'])
    }
  }
</script>

辅助函数最大的优点就是可以处理大量共享数据的需求,这样写起来会非常的简便,因为只需要往数组里添加子项即可。

13.Vuex-persist对数据进行持久化处理

默认情况下Vuex状态管理是不会对数据进行持久化存储的,也就是说当我们刷新浏览器后,共享状态就会被还原。那么我们想要在刷新的时候能够保持成修改后的数据就需要进行持久化存储,比较常见的持久化存储方案就是利用本地存储来完成,不过自己去实现还是比较不方便的,下面我们通过第三方模块来实现其功能。

模块github地址:https://github.com/championswimmer/vuex-persist。根据地址要求的配置操作如下:

// npm install vuex-persist
	
import VuexPersistence from 'vuex-persist';
const vuexLocal = new VuexPersistence({
  storage: window.localStorage,
  reducer: (state) => ({count: state.count})//配置:指定字段持久化
})

const store = createStore({
  state: {
    count: 0,
    msg: 'hello'
  }
  plugins: [vuexLocal.plugin]
});

​ 默认情况下,vuex-persist会对所有共享状态进行持久化,那么如果我们只需要对指定的属性进行持久化就需要配置 reducer字段,这个字段可以指定需要持久化的状态。

​ 这样当我们修改了state下的count,那么刷新的时候会不会还原了,并且通过chrome浏览器中Application下的Local Storage进行查看。

14Vuex分割模块及多状态管理

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。

那么这个时候,所有共享状态的值都在一起,所有的方法也都放在了一起,维护起来非常的不方便。那么Vuex中可利用modules属性来配置模块化的共享状态,那么对于后期维护起来是非常方便的,也利于大型项目的架构。

在/store下创建一个modules文件夹,然后编写一个message.js,代码如下:

const state = {
  msg: 'hello'
};
const getters = {
  upperMsg(state){//计算属性
    return state.msg.toUpperCase()
  }
};
const actions = {};
const mutations = {//方法
  change(state, payload){
    state.msg = payload;
  }
};
export default {
  namespaced: true,
  state,
  getters,
  actions,
  mutations
}

模块中的选项跟index.js中的选项是一样的,对外提供接口的时候,可以看到一个namespaced字段,这表示当前模块的一个命名空间,主要是为了防止跟其他模块之间产生冲突,在调用的时候需要携带对应的命名空间标识符才行。

再来看一下index.js如何去收集我们的模块,并如何去使用我们的模块。

// store/index.js
import message from '@/store/modules/message'
const store = createStore({
  modules: {
    message
  }
});
<!-- About.vue -->
<template>
  <div>
    <button @click="change('hi')">点击</button>
    hello about, {{ msg }}, {{ upperMsg }}
  </div>
</template>

<script>
import { mapState, mapGetters, mapMutations } from 'vuex';
  export default {
    name: 'AboutView',
    methods: {
      // handleClick(){
      //   this.$store.commit('message/change', 'hi');
      // }
      ...mapMutations('message', ['change'])
    },
    computed: {
      //  msg(){
      //   return this.$store.message.msg;
      //  },
      //  upperMsg(){
      //   return this.$store.getters['message/upperMsg']
      //  }
      ...mapState('message', ['msg']),
      ...mapGetters('message', ['upperMsg'])
    }
  }
</script>

在辅助函数的情况下,也可以进行很好的调用,辅助函数的第一个参数就是命名空间的名字。

15.组合式API中使用Router和Vuex注意事项

​ 前面介绍的路由和状态管理都是在选项式API中进行使用的,那么路由和状态管理在组合式API中使用的时候,需要注意哪些问题呢?

​ 主要就是路由会提供两个use函数,分别为:useRoute和useRouter;同理状态管理页提供了一个use函数,useStore来操作的。

先来看一下路由相关use函数的使用情况:

<script setup>
import { useRoute, useRouter } from 'vue-router'
const route = useRoute();
const router = useRouter();
console.log( route.meta );
console.log( router.getRoutes() );
</script>

基本上跟选项式API中的用法是一样的,并没有太大的区别。

再来看一下状态管理相关use函数的使用情况:

<script setup>
import { useStore } from 'vuex'
const store = useStore();
console.log( store.state.count );
console.log( store.state.message.msg );
</script>

得到store对象,接下来的操作也是跟选项式API中使用的是一样的。

16.Router_Vuex的任务列表综合案例

本小节将对本章学习的路由加状态管理做一个综合案例,通过案例来巩固本章所学知识点。

image-20240814212543537

首先先来配置案例中的路由,主要有三个页面,分别对应所有任务,已完成任务,未完成任务。

import { createRouter, createWebHistory } from 'vue-router'
import Todo from '@/views/Todo.vue'
import Complete from '@/views/Complete.vue'
import Incomplete from '@/views/Incomplete.vue'
const routes = [
  {
    path: '/',
    redirect: '/todo'
  },
  {
    path: '/todo',
    component: Todo
  },
  {
    path: '/complete',
    component: Complete
  },
  {
    path: '/incomplete',
    component: Incomplete,
  }
];
const router = createRouter({
  history: createWebHistory(),
  routes
})
export default router;

在来配置一下Vuex状态管理,主要对任务列表进行共享状态处理:

import { createStore } from "vuex";
const store = createStore({
  state: {
    todoList: [
      {
        isChecked: true, id: 1, task: '第一个任务'
      },
      {
        isChecked: false, id: 2, task: '第二个任务'
      }
    ]
  },
  actions: {
    
  },
  mutations: {
    add(state, payload){
      state.todoList.unshift({ isChecked: false, id: state.todoList.length, task: payload });
    }
  },
  getters: {
    complete(state){
      return state.todoList.filter((v)=> v.isChecked)
    },
    incomplete(state){
      return state.todoList.filter((v)=> !v.isChecked)
    }
  }
});
export default store;

最后看一下三个页面的基本逻辑处理:

<!-- Todo.vue -->
<template>
  <div>
    <ul>
      <li v-for="item in todoList" :key="item.id" :class="{ through: item.isChecked }">
        <input type="checkbox" v-model="item.isChecked"> {{ item.task }}
      </li>
    </ul>
  </div>
</template>
<script setup>
import { computed, defineComponent } from 'vue';
import { useStore } from 'vuex';
defineComponent({
  name: 'TodoView'
});
const store = useStore();
const todoList = computed(()=> store.state.todoList)
</script>
<!-- Complete.vue -->
<template>
  <div>
    <ul>
      <li v-for="item in todoList" :key="item.id" :class="{ through: item.isChecked }">
        <input type="checkbox" v-model="item.isChecked"> {{ item.task }}
      </li>
    </ul>
  </div>
</template>
<script setup>
import { computed, defineComponent } from 'vue';
import { useStore } from 'vuex';
defineComponent({
  name: 'CompleteView'
});
const store = useStore();
const todoList = computed(()=> store.getters.complete)
</script>
<!-- Incomplete.vue -->
<template>
  <div>
    <ul>
      <li v-for="item in todoList" :key="item.id" :class="{ through: item.isChecked }">
        <input type="checkbox" v-model="item.isChecked"> {{ item.task }}
      </li>
    </ul>
  </div>
</template>
<script setup>
import { computed, defineComponent } from 'vue';
import { useStore } from 'vuex';
defineComponent({
  name: 'IncompleteView'
});
const store = useStore();
const todoList = computed(()=> store.getters.incomplete)
</script>

17.Vue状态管理之Pinia存储库

Pinia 最初是为了探索 Vuex 的下一次迭代会是什么样子,结合了 Vuex 5 核心团队讨论中的许多想法。最终,我们意识到 Pinia 已经实现了我们在 Vuex 5 中想要的大部分内容,并决定实现它 取而代之的是新的建议。

与 Vuex 相比,Pinia 提供了一个更简单的 API,具有更少的规范,提供了 Composition-API 风格的 API,最重要的是,在与 TypeScript 一起使用时具有可靠的类型推断支持。

Pinia API 与 Vuex4有很大不同,即:

  • mutations 不再存在。他们经常被认为是 非常冗长。他们最初带来了 devtools 集成,但这不再是问题。
  • 无需创建自定义复杂包装器来支持 TypeScript,所有内容都是类型化的,并且 API 的设计方式尽可能利用 TS 类型推断。
  • 不再需要注入、导入函数、调用函数、享受自动完成功能!
  • 无需动态添加 Store,默认情况下它们都是动态的,您甚至都不会注意到。请注意,您仍然可以随时手动使用 Store 进行注册,但因为它是自动的,您无需担心。
  • 不再有 modules 的嵌套结构。您仍然可以通过在另一个 Store 中导入和 使用 来隐式嵌套 Store,但 Pinia 通过设计提供平面结构,同时仍然支持 Store 之间的交叉组合方式。 您甚至可以拥有 Store 的循环依赖关系。
  • 没有 命名空间模块。鉴于 Store 的扁平架构,“命名空间” Store 是其定义方式所固有的,您可以说所有 Store 都是命名空间的。

总之Pinia简化了Vuex的操作,这也是未来Vuex5的趋势,下面就来尝试使用一下Pinia吧。

用你最喜欢的包管理器安装 pinia:

yarn add pinia
# 或者使用 npm
npm install pinia

目前最新版本为"pinia": "^2.0.16",然后把pinia当插件的方式在Vue脚手架中生效:

import { createPinia } from 'pinia'
const pinia = createPinia()
createApp(App).use(pinia).mount('#app')

继续在src文件夹中创建/stores/counter.js,并写入如下代码:

import { defineStore } from 'pinia'

// 第一个参数是应用程序中 store 的唯一 id,是必要的,Pinia 使用它来将 store 连接到 devtools。
export const useCounterStore = defineStore('counterStore', {
  state: () => ({
    counter: 0
  }),
  actions: {
    add(){
      this.counter++
    }
  }
})

除了上面的选项式写法外,pinia还支持组合式的写法,代码如下:

import { defineStore } from 'pinia'
import { ref } from 'vue'

// 第二个参数,可以写成回调函数的写法,这样就支持组合式API的使用
export const useCounterStore = defineStore('counterStore', ()=>{
    const counter = ref(0)
    const add = () => {
        counter.value++
    }
    return { counter, add }
})

以上两种风格的写法是等价的,接下来就是如何去调用这个模块了。在App.vue中引入counter.js并使用:

<template>
<button @click="handleClick">点击</button>{{ counter }}
</template>
<script setup>
import { storeToRefs } from 'pinia';
import { useCounterStore } from './stores/counter';
let counterStore = useCounterStore()
//下面操作可使共享状态具备响应式
let { counter } = storeToRefs(counterStore);
//下面四种操作行为均可修改counter值,并具备响应式变化
let handleClick = () => {
    // counter.value++;               // 1
    // counterStore.counter++;        // 2
    // counterStore.$patch({          // 3
    //   counter: counter.value + 1
    // })
    counterStore.add();               // 4
}
</script>

add()方法可以直接编写异步程序和传参处理:

actions: {
  add(n){
    setTimeout(()=>{
      this.counter += n;
    }, 1000)
  }
}

总结:Pinia去掉了繁琐的mutations,异步同步都采用actions来完成,并且简化了modules的使用方式等等。

18.搭建 Vite3 + Pinia2 组合模式

前面我们介绍过Vite和Pinia,其中Vite是最新的脚手架,基于原生ESM方式;而Pinia则是最新的状态管理,比Vuex使用起来更加简单。

本小节我们将演示Vite如何去搭配Pinia来完成项目的开发。

首先对Vite3脚手架进行初始化的安装。

  1. 安装脚手架

    # npm 6.x
    npm create vite@latest vite-study
    # yarn
    yarn create vite vite-study
    # pnpm
    pnpm create vite vite-study
    
  2. 选择框架:因为Vite可以和很多框架配合使用,所以这里我们选择:Vite + Vue

    ? Select a framework: » - Use arrow-keys. Return to submit.
        Vanilla
    >   Vue
        React
        Preact
        Lit
        Svelte
    
  3. 选择变体:这里先选择自定义形式

    ? Select a variant: » - Use arrow-keys. Return to submit.
        JavaScript
        TypeScript
    >   Customize with create-vue
        Nuxt
    
  4. 选择安装Pinia

    ? Add Pinia for state management? >> No / Yes
    Yes
    
  5. 进入项目:安装第三方模块,并启动项目

    cd vite-study
    npm install
    npm run dev
    
    VITE v3.1.0  ready in 408 ms
    
      ➜  Local:   http://127.0.0.1:5173/
      ➜  Network: use --host to expose
    

在安装好Vite后,打开/src/stores可以看到自动安装好了一个示例模块counter.js,代码如下:

import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const doubleCount = computed(() => count.value * 2)
  function increment() {
    setTimeout(()=>{
      count.value++
    }, 2000)
  }
  return { count, doubleCount, increment }
})

这里的风格可以是上一个小节中介绍的配置写法,也可以利用组合式API风格来编程Pinia。这里做了一个共享状态count,又做了一个计算属性doubleCount,还有一个方法increment。

在共享方法中是不分同步还是异步的,对于vue devtools都是可以很好的进行监控的,所以比Vuex使用起来要简单一些。下面看一下共享状态是如何进行渲染和方法调用的。

<!-- App.vue -->
<script setup>
    import { useCounterStore } from './stores/counter';
    import { storeToRefs } from 'pinia';
    const counterStore = useCounterStore();
    const { count, doubleCount } = storeToRefs(counterStore);
    const handleClick = () => {
        counterStore.increment();
    };
</script>
<template>
	<header>
    	<button @click="handleClick">点击</button>
    	{{ count }}, {{ doubleCount }}
    </header>
</template>

Pinia对于模块化的操作方式也比Vuex要简单一些,直接在/stores创建下新创建一个模块JS即可,如:message.js。message.js的代码跟counter.js的代码是一样的格式,使用的时候也是一样的操作行为。

章节总结

小伙伴大家好,本章学习了VueRouter路由与Vuex状态管理 - 组织与架构应用。

总结内容

  • 了解什么是前端路由,以及Vue3中如何使用vue-router路由
  • 掌握路由的基本操作,如:编程式路由、动态路由、路由守卫等
  • 了解什么是共享状态,以及Vue3中如何使用vuex共享状态
  • 掌握vuex的基本操作,如:同步异步方法、模块化、持久化等
  • 综合应用以及Vuex下一代版本,Pinia存储库

完成以下练习题,示例如下:

要求如下:

  1. 数字为共享状态 state.page,默认值为0
  2. 点击下一页可以切换不同的路由
  3. 进入对应的路由,可修改state.page值
// App.vue
<template>
  <h2>任务列表</h2>
  <div>
    <button @click="handleNextPage">下一页</button> -> {{ page }}
  </div>
  <router-view></router-view>
</template>

参考答案:

//App.vue
<script setup>
  import { computed } from 'vue';
  import { useStore } from 'vuex';
  import { useRouter } from 'vue-router';
  let urls = ['/todo', '/complete', 'incomplete'];
  let store = useStore();
  let router = useRouter();
  let page = computed(() => store.state.page);
  let handleNextPage = () => {
    urls[page.value] && router.push(urls[page.value]);
  }
</script>

//Todo.vue
<script setup>
import { defineComponent } from 'vue';
import { useStore } from 'vuex';
defineComponent({
  name: 'TodoView'
});
let store = useStore();
store.commit('next', 1);
</script>

//Complete.vue 
<script setup>
import { defineComponent } from 'vue';
import { useStore } from 'vuex';
defineComponent({
  name: 'CompleteView'
});
let store = useStore();
store.commit('next', 2);
</script>

//Incomplete.vue
<script setup>
import { defineComponent } from 'vue';
import { useStore } from 'vuex';
defineComponent({
  name: 'IncompleteView'
});
let store = useStore();
store.commit('next', 3);
</script>

//index.js
<script>
const store = createStore({
  state: {
    page: 0
  },
  mutations: {
    next(state, payload){
      state.page = payload;
    }
  }
});
</script>

测试题(选择)

在Vuex中处理哪个属性用来处理异步?

  • [ ] A:mutations
  • [x] B:actions
  • [ ] C:getters
  • [ ] D:modules

参考答案:

   选项 B ( Vuex中规定使用actions来处理异步程序 )

最近更新:: 2025/8/21 14:43
Contributors: yanpeng_
Prev
Typescript4技术笔记
Next
Vue2.0基础技术笔记