VUE2 基础知识点梳理

一、初识 Vue

1、什么是 vue ?

Vue (读音 /vjuː/,类似于 view)是一套用于构建用户界面的渐进式 javascript 框架。

它是以数据驱动和组件化的思想构建的,与其他大型框架不同的是,Vue 被设计为可以自底向上逐层应用。其核心库只关注视图层,不仅易于上手,还便于与第三方库有项目整合。

2、vue 的优点

  • 渐进式

  • 组件化开发

  • 虚拟 dom

  • 响应式数据

  • 单页面路由

  • 数据与是视图分开

3、Vue 的特点

3.1 渐进式框架

Vue 全家桶,你可以选择不用,或者只选几样去用,比如不用 Vuex

  • 如果只使用 Vue 最基础的声明式渲染的功能,则完全可以把 Vue 当做一个模板引擎来使用

  • 如果想以组件化开发方式进行开发,则可以进一步使用 Vue 里面的组件系统

  • 如果要制作 SPA (单页应用),则可以使用 Vue 里面的客户端路由功能

  • 如果组件越来越多,需要共享一些数据,则可以使用 Vue 里的状态管理

  • 如果想在团队里执行统一的开发流程或规范,则使用构建工具

所以,可以根据项目的复杂度来自主选择使用 Vue 里面的功能

3.2 数据驱动

Vue.js 是数据驱动的,你无需手动操作 DOM。它通过一些特殊的 HTML 语法,将 DOM 和数据绑定起来。一旦你创建了绑定,DOM 将和数据保持同步,每当变更了数据,DOM 也会相应的更新。

在使用 Vue.js 时,你也可以结合其他库一起使用,比如 jQery。

4、Vue和React的异同点

相同点:

  • 都是单向数据流

  • 都使用了虚拟 DOM 的技术

  • 都支持 SSR

  • 组件化开发

不同点:

  • Vue 使用 template,React 使用 JSX

  • 数据改变:Vue 响应式,React 手动 setState

  • Vue 双向绑定,React 单向绑定

  • Vue 状态管理工具 Vuex,React状态管理工具 Redux、Mobx

5、M V VM是什么?

MVVM:页面输入改变数据,数据改变影响页面数据展示与渲染。

M(model):普通的 javascript 数据对象,取数据的地方。

V(view):前端展示页面,展示数据的地方。

VM(viewModel):用于双向绑定数据与页面,就是 vue 实例。

6、Vue 核心功能

  • 基础功能:页面渲染、表单处理提交、帮我们管理 DOM(虚拟 DOM)节点。
  • 组件化开发:增强代码的复用性,复杂系统代码维护更简单。
  • 前端路由:更流畅的用户体验、灵活的在页面切换已渲染组件的显示,不需与后端做多与的交互。
  • 状态集中管理:M V VM 响应式模型基础上实现多组件之间的状态数据同步与管理。
  • 前端工程化:结合 webpack 等前端打包工具,管理多种静态资源,代码,测试,发布等,整合前端大型项目。

7、编程工具

开发工具:Visual Studio Code

常用插件:

  • Auto Rename Tag:能够自动更改结束标签

  • Live Server:自动搭建本地服务器

  • Prettier - Code formatter:代码美化

  • Vetur:但vue组件格式支持

  • vscode-icons:美化文件图标

8、小试牛刀

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>vue快速上手</title>
</head>
<body>
    <div id="app">
        <a v-bind:href="link">{{title}}</a>
        <h1>{{username}}</h1>
        <h2>{{message}}</h2>
        <!-- 点击按钮,调用 fn1() 方法,改变username和message -->
        <button v-on:click="fn1()">点击fn1()</button>
    </div>
</body>
// 注意:在没有使用脚手架时,需要引入 vue 的文件
<script src="../lib/vue.js"></script>
<script>
    new Vue({
        el:'#app',// 绑定节点元素
        data:{
            username:'jack',
            message:'hello vue',
            link:"https://blog.csdn.net/2201_75342929?spm=1000.2115.3001.5343",
            title:'我的博客'
        },
        methods:{
            fn1(){
                // this.指向data中的数据
                alert(this.username);
                this.username = 'rose';
                this.message = '你好 vue';
            }
        },
    });
</script>
</html>

二、Vue 核心概念

vue 实例

通过 new Vue({...}) 创建 vue 对象。

配置对象中的部分内容会被提取到 vue 实例中:

  • data

  • props

  • methods

  • computed

挂载

让 vue 实例控制网页中某个区域的过程,称之为挂载。

挂载的方式:

  1. 通过 el:"css选择器" 进行配置

  2. 通过 vue实例.$mount('css选择器') 进行配置

模板

被 vue 实例控制的页面片段。

        1. 模板的作用是什么?

                为了提高渲染效率,vue 会把模板编译成为虚拟 DOM 树(VNode),然后再生成真是的 DOM。

        2. 模板书写到哪?

                1. 在挂载的元素内部直接书写

                2. 在 template 配置中书写

                3. 在 render 配置中用函数创建

        3. 模板中写什么?    

  1. 静态内容

  2. 插值:{{JS 表达式}}mustache 语法

  3. 常用指令

    1. v-html :绑定元素的innerHTML

    2. v-bind:属性名:绑定属性,可以简写成 :属性名

    3. v-on:事件名:绑定事件,可以简写成 @事件名

    4. v-if:判断元素是否需要渲染,没有该标签(判断时,不断的销毁重建)

    5. v-show:判断元素是否应该显示,默认有这个标签在的,只是给了一个display样式(相当于使用display:none)

    6. v-for:用于循环生成元素

    7. v-bind:key:用于帮助在重新渲染时元素的比对,通常和v-for配合使用,以提高渲染效率

    8. v-model:语法糖,用于实现双向绑定,实际上,是自动绑定了value属性值,和注册了input事件

        4. 模板中的代码环境

        模板中所有的 JS 代码,它的环境均为 vue 实例 ,例如 {{title}} ,得到的结果相当于是 vue实例.title

配置对象

  1. data:数据

  2. template:字符串,配置模板

  3. el:配置挂载的区域

  4. methods:配置方法

  5. computed:配置计算属性

    • 和普通methods 属性的区别是:它具有缓存作用

计算属性和方法的区别:

  1. 计算属性使用时,是当成属性使用,而方法是需要调用的

  2. 计算属性会进行缓存,如果依赖不变,则直接使用缓存结果,不会重新计算

  3. 计算属性可以当成属性赋值

三、Vue 组件

1、组件概念

一个完整的网页是复杂的,如果将其作为一个整体来进行开发,将会遇到下面的困难

  • 代码凌乱臃肿

  • 不易协作

  • 难以复用

vue推荐使用一种更加精细的控制方案——组件化开发:

组件(Component)是 Vue 最强大的功能之一。组件可以扩展 HTML 元素,封装可重用的代码。根据项目需求,抽象出一些组件,每个组件里包含了展现、功能和样式。每个页面,根据自己所需,使用不同的组件来拼接页面。这种开发模式使前端页面易于扩展,且灵活性高,而且组件之间也实现了解耦。

所谓组件化,即把一个页面中区域功能细分,每一个区域成为一个组件,每个组件包含:

  • 功能(JS代码)

  • 内容(模板代码)

  • 样式(CSS代码)

由于没有构建工具的支撑,CSS代码暂时无法放到组件中。

2、组件开发

组件是根据一个普通的配置对象创建的,所以要开发一个组件,只需要写一个配置对象即可

该配置对象和vue实例的配置是几乎一样的。

//组件配置对象
var myComp = {
  data(){// 数据
    return {
      // ...
    }
  },
  computed:{// 配置计算属性:注册 ---components
    //...
  },
  methods:{// 方法
    //...
  },
  template: `....`// 模块代码---可挂载其他组件
}

值得注意的是,组件配置对象和vue实例有以下几点差异:

  • el

  • data必须是一个函数,该函数返回的对象作为数据

  • 由于没有el配置,组件的模板必须定义在template

  • template中的所有HTML元素必须且只能有一个根元素

3、注册组件

注册组件分为两种方式,一种是全局注册,一种是局部注册

全局注册:

一旦全局注册了一个组件,整个应用中任何地方都可以使用该组件。

 全局注册方式

// 参数1:组件名称,将来在模板中使用组件时,会使用该名称
// 参数2:组件配置对象
// 该代码运行后,即可在模板中使用组件
Vue.component('my-comp', myComp)

在模板中,可以使用组件了

<my-comp />
<!-- 或 -->
<my-comp></my-comp>

注意:在一些工程化的大型项目中,很多组件都不需要全局使用。 比如一个登录组件,只有在登录的相关页面中使用,如果全局注册,将导致构建工具无法优化打包 因此,除非组件特别通用,否则不建议使用全局注册

局部注册:

局部注册就是哪里要用到组件,就在哪里注册。

 局部注册方式

在要使用组件的组件或实例中加入一个配置:

// 引入需要使用的组件
import MyComp from '@/scr/components/my-comp/MyComp';// 注意:@ 表示 scr
// 这是另一个要使用MyComp的组件
var otherComp = {
  components:{
    // 属性名为组件名称,模板中将使用该名称
    // 属性值为组件配置对象
    // 注册组件
    "my-comp": MyComp
  },
  template: `
    <div>
		<!-- 使用组件 -->
      <my-comp></my-comp>
    </div>
  `;
}

4、应用组件

在模板中使用组件特别简单,把组件名当作HTML元素名使用即可。

但要注意以下几点:

        1. 组件必须有结束

        组件可以自结束,也可以用结束标记结束,但必须要有结束

        2. 组件的命名

        无论你使用哪种方式注册组件,组件的命名需要遵循规范。

        组件可以使用kebab-case 短横线命名法,也可以使用PascalCase 大驼峰命名法

        下面两种命名均是可以的:

var otherComp = {
  components:{
    "my-comp": myComp,  // 方式1
    MyComp: myComp //方式2
  }
}

实际上,使用小驼峰命名法 camelCase也是可以识别的,只不过不符合官方要求的规范。

使用PascalCase方式命名还有一个额外的好处,即可以在模板中使用两种组件名:

var otherComp = {
  components:{
    MyComp: myComp
  }
}

模板中:

<!-- 可用 -->
<my-comp />
<MyComp />

因此,在使用组件时,为了方便,往往使用以下代码:

var MyComp = {
  //组件配置
}

var OtherComp = {
  components:{
    MyComp // ES6速写属性
  }
}

注意,PascalCase命名不可以直接在html中使用,但可以在template配置中使用。

例如:在 App.vue 里面注册使用一个组件:

<template>
	<div>
        <!-- 使用 -->
        <Test1 />
    </div>
</template>
<script>
	// 导入
	import Test1 from '@/components/Test/Test1.vue';
	// 注册
	export default {
    	components:{
        	Test1,
    	}
	}
// 使用
</script>
<style>
</style>

5、组件树

一个组件创建好后,往往会在各种地方使用它。它可能多次出现在vue实例中,也可能出现在其他组件中。于是就形成了一个组件树:

6、向组件传递数据

大部分组件要完成自身的功能,都需要一些额外的信息,

比如一个头像组件,需要告诉它头像的地址,这就需要在使用组件时向组件传递数据,

传递数据的方式有很多种,最常见的一种是使用组件属性 component props。

首先在组件中申明可以接收哪些属性:

var MyComp = {
  // 声明可以接受哪些属性 
  props:["p1", "p2", "p3"],
  // 和vue实例一样,使用组件时也会创建组件的实例
  // 而组件的属性会被提取到组件实例中,因此可以在模板中使用
  template: `
    <div>
      {{p1}}, {{p2}}, {{p3}}
    </div>
  `
}

在使用组件时,向其传递属性:

var OtherComp = {
  components: {
    // 注册逐渐
    MyComp
  },
  data(){
    return {
      a:1
    }
  },
 // 使用组件时传递属性
  template: `
    <my-comp :p1="a" :p2="2" :p3="3"/>
  `
}

注意:在组件中,属性是只读的,绝不可以更改,这叫做单向数据流

 7、组件的生命周期

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

1、beforeCreate:在实例开始初始化时同步调用。此时数据观测、事件等都尚未初始化。

没有实例化,数据访问不到。【适用较少】

2、created:在实例创建之后调用。此时已完成数据观测、事件方法,但尚未开始DOM编译,即未挂载到document中。 只在页面初始化时执行一次【初始化数据】

可做操作:从服务器获取一些初始化的数据,或者通过 ajax 向服务器发送一些数据(发送请求,访问后端接口拿数据)。【使用频率高】

能拿到数据,能修改数据,

  • 且修改数据不会触发updated beforeUpdate钩子函数

  • 可以在这个钩子函数里发请求,访问后端接口拿数据

  • 判断是否存在el,是否存在template,如果二者都有,以template为主优先, 如果 没有template,会选择el模板。 如果二者都没有,有$mount 也可以调用模板。

3、beforeMount:在mounted之前运行。

  • 编译模板,把data里面的数据和模板生成html。注意此时还没有挂载html到页面上。

  • 真实的dom节点挂载到页面之前

  • 编译模板已经结束,虚拟dom已经存在

  • 可以访问数据,也可以更改数据

  • 且修改数据不会触发updated beforeUpdate钩子函数

4、mounted:在编译结束时调用。此时所有指令已生效,数据变化已能触发DOM更新,但不保证$el已插入文档。

【获取dom节点、修改更新数据】

此时状态: 数据已经渲染在了浏览器的页面上。

可做操作:操作DOM节点

可以对挂载元素的dom节点进行获取,也可以访问后端接口拿数据,修改更新数据,触发更新钩子函数。

5、beforeUpdate:在实例挂载之后,再次更新实例(例如更新 data)时会调用该方法,此时尚未更新DOM结构。

6、updated:在实例挂载之后,再次更新实例并更新完DOM结构后调用。

在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。

7、beforeDestroy:在开始销毁实例时调用,此刻实例仍然有效。

【移除定时器或者移除事件绑定】

此时状态:奄奄一息。

可做操作:移除定时器或者移除事件绑定。但是销毁的时候需要手动销毁.

8、destroyed:在实例被销毁之后调用。此时所有绑定和实例指令都已经解绑,子实例也被销毁。

四、搭建项目的流程

传统工程的问题

  1. 兼容性问题

  2. 使用模块化会导致JS文件增加,从而导致传输文件数增加

  3. 直接使用原始代码会导致文件体积过大

  4. 使用第三方库很不方便

    1. 搜索

    2. 下载

    3. 引用js(某些第三方库可能没有ES6 模块化版本)

  5. vue模板书写在字符串中,没有智能提示,没有代码着色

  6. 难以把样式代码集成到vue组件中

  7. 其他诸多细节问题...

这些问题的本质是:开发的代码和运行的代码要求不同

所以,我们需要一个工具,它能够让我们舒舒服服的写代码,然后通过这个工具转换后,得到一个最适合运行的代码。

在vue中,这个工具就是vue-cli

搭建流程

快速搭建项目 vue-cli 脚手架(Vue2.0),Vue-cli 使用前提 -Node、Webpack。

1. 安装 nodejs,目前建议安装16版本,不要最新版本,如已安装可查看安装的版本:

下载node:Node.js

验证安装:打开终端,查看node和npm版本,验证是否安装成功:

node -v
npm -v

如果安装之前打开了终端,需要在安装后关闭终端,重新打开。

2. 配置源地址:

默认情况下,npm安装包时会从国外的地址下载,速度很慢,容易导致安装失败,因此需要先配置npm的源地址

使用下面的命令更改npm的源地址为淘宝源

npm config set registry http://registry.npm.taobao.org/

更改好了之后,查看源地址是否正确的被更改

npm config get registry

3. 安装 webpack:

webpack通过npm安装,它提供了两个包:

  • webpack:核心包,包含了webpack构建过程中要用到的所有api

  • webpack-cli:提供一个简单的cli命令,它调用了webpack核心包的api,来完成构建过程

安装方式:

  • 全局安装:可以全局使用webpack命令,但是无法为不同项目对应不同的webpack版本

  • 本地安装:推荐,每个项目都使用自己的webpack版本进行构建

npm i -D webpack webpack-cli

使用

webpack

本地安装与全局安装区别: 本地安装:仅将webpack安装在当前项目的node_modules目录中,仅对当前项目有效。 全局安装:将webpack安装在本机,对所有项目有效,全局安装会锁定一个webpack版本,该版本可能不适用某个项目。全局安装需要添加 -g 参数。

4. 安装 vue 脚手架(vue-cli 版本是3.xx):

新建一个目录文件夹,该目录将放置你的工程文件夹,进入该项目打开cmd,终端进入该目录,下载vue-cli。

下载命令:

npm install -g @vue/cli

检查是否安装成功:

vue --version

5. 搭建工程,在哪个文件夹下载的vue-cli,就在哪个文件夹搭建项目

(注意:工程名只能出现英文、数字和短横线)

使用vue-cli提供的命令搭建工程

vue create 工程名

6. 理解工程结构

npm run build 直接 打包为最终dist可运行状态
npm run serve 内存中打包,适合开发环境

安装或拷贝工程

以后安装或者拷贝工程,不需要再拷贝node_modules,只需要把其他关键的部分拷贝,然后再工程目录下执行npm install就行了,会自动帮我们拷贝相关的引用。

一个bug

程序通过脚手架启动之后,可能会报出下面的错误:

sockjs.js?9be2:1609 GET http://192.168.50.82:8080/sockjs-node/info?t=1621067851460 net::ERR_CONNECTION_REFUSED

原因: sockjs-node是一个JavaScript库,提供跨浏览器JavaScript的API,创建了一个低延迟、全双工的浏览器和web服务器之间通信通道。在项目运行以后,network会一直调用这个接口。如果没有使用,那么就一直会报这个异常。

解决办法: 1)找到/node_modules/sockjs-client/dist/sockjs.js文件 2)在1606行,注释掉self.xhr.send(payload);这一行,然后就可以解决了。

五、使用 ElementUI

Element,一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库

官方网站

下载 ElementUI

npm install --save element-ui

npm i element-ui -S// 或者官网复制命令下载

引入 Element

可以引入整个 Element,或是根据需要仅引入部分组件。

整体引入:

// 在 main.js 文件夹里,引入:(官网复制)
import 'element-ui/lib/theme-chalk/index.css';
import ElementUI from 'element-ui';
// 再使用 Element
Vue.use(ElementUI);

按需引入:

如果只希望引入部分组件,比如 Button 和 Select,那么需要在 main.js 中写入以下内容:

// 在 main.js 文件夹里,引入:(官网复制)
import { Button, Select } from 'element-ui';
// 再使用 引入的组件
Vue.use(Button)
Vue.use(Select)
/** 或写为
Vue.component(Button.name, Button);
Vue.component(Select.name, Select);
*/

使用 ElementUI

比如,使用 ElementUI 的按钮:

<template>
  <div id="app">
        <el-button type="primary">主要按钮</el-button>
        <el-button type="success">成功按钮</el-button>
        <el-button type="info">信息按钮</el-button>
        <el-button type="warning">警告按钮</el-button>
        <el-button type="danger">危险按钮</el-button>
  </div>
</template>

六、Axios

使用axios

Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。 使用说明 · Axios 中文说明 · 看云

1、下载依赖

$ npm install axios --save

其中--save在新版本的npm中不再被需要了,直接npm install axios即可。

2、引入

// 在需要使用到 axios 文件里引入
import axios from 'axios';

3、使用webpack模式的代理

VUE导入的axios已经考虑到了跨域的问题,所以,我们可以直接使用VUE配置文件进行代理,解决跨域的问题,简单来说,就是把URL做了一次转换。

需要在项目根目录中加入文件vue.config.js:

module.exports = {
    lintOnSave:false, //是否在浏览器中显示代码检查的错误
    devServer:{
        port:8081, //设置端口号,如果默认8080不需要设置,这里设置8081主要是为了不要和后台tomcat的8080接口冲突
        proxy: { //设置代理
            "/api": { // /api 匹配项,匹配拦截;
                target: "http://localhost:8080", //被请求的地址,需要被代理的地址
                changeOrigin: true,//允许跨域
                pathRewrite: {
                    "^/api": "" //重写配置,被代理的接口会多一个‘/api’的前缀,而原本的接口是没有的,所以需要通过此项来将接口的前缀‘/api’转换为‘’
                }
            }
        }
    }
}

这个文件在服务启动的时候会自动加载,不需要我们过多配置,但是,他的实际作用其实就是URL的改写,所以,在我们访问后台接口地址的时候,需要作出相应的改变:

let resp = await axios.get("/api/users/login");

七、组件之间的通信

1、props 传递数据

在父组件子组件添加自定义属性,挂载需要传递的数据,子组件用 props 来接受,接收方式也可以是数组,也可以是对象,子组件接收到数据之后,不能直接修改父组件的数据。会报错,所以当父组件重新渲染时,数据会被覆盖。如果子组件内要修改的话推荐使用 this.$emit("事件名","要传递的值"),那么父页面在调用子组件的时候,就需要调用@change事件。

父组件传递数据:

<!-- 父组件 -->
<template>
    <div>
        <!-- 传递数据 -->
        <UserShow :title="title" :users='users' @changeTitle="changeTitle" />
    </div>
</template>

<script>
import UserShow from './UserShow.vue';
export default {
    data () {
        return {
            title:'用户列表',
            users:[
                {id:1,username:'jack',age:15},
                {id:2,username:'rose',age:16},
                {id:3,username:'tom',age:17},
                {id:4,username:'bob',age:18},
            ]
        }
    },
    components:{// 注册
        UserShow
    },
    methods:{
        changeTitle(value){
            this.title = value
        }
    }
}
</script>

// secoped:引用组件时,避免造成其他组件使用到了该组件的样式,所以加上 scoped,表示只能在该组件内使用
<style scoped>
</style>

子组件接受父组件传递来的数据,通过 $emit 修改:

<!--  -->
<template>
    <div>
        <h2>{{ title }}</h2>
        <el-row>
            <el-col :span="12" :offset="6">
                <el-table
                    :data="users"
                    style="width: 100%"
                >
                    <el-table-column
                        prop="id"
                        label="编号"
                        width="180">
                    </el-table-column>
                    <el-table-column
                        prop="username"
                        label="姓名"
                        width="180">
                    </el-table-column>
                    <el-table-column
                        prop="age"
                        label="年龄">
                    </el-table-column>
                </el-table>
            </el-col>
        </el-row>
        <el-button type="success" plain  @click="handleTitle">点击修改title</el-button>
    </div>
</template>

<script>
export default {
    // props:['users','title'],// 此写法也可以
    props:{
        users:{
            type:Array,// 传递过来的数据类型
            required:true,// 表示父组件必须传递 users 数据过来
        },
        title:{
            type:String,
            default:'用户列表信息',// 如果父组件没有传递 title,则使用默认值
        }
    },
    methods:{
        handleTitle() {
            // 点击按钮修改标题
            // 语法:this.$emit('父组件的方法名','修改后的数据')
            this.$emit('changeTitle','用户信息列表展示')
        }
    }
}
</script>

<style scoped>
    .my_ul{
        list-style: none;
    }
</style>

2、v-model

和 .sync 类似,可以实现将父组件传给子组件的数据为双向绑定,子组件通过 $emit 修改父组件的数据。

// 父组件
<template>
  <div class="">
    <HelloWorld v-model="value"></HelloWorld>
    {{ value }}
  </div>
</template>
​
<script>
import HelloWorld from "../components/HelloWorld.vue";
export default {
  data() {
    return {
      value: "我是父组件里的响应式数据",
    };
  },
  components: { HelloWorld },
};
</script>

// 子组件 HelloWorld
<template>
  <input :value="value" @input="handlerChange" />
</template>
<script>
export default {
  props: ["value"],
  methods: {
    handlerChange(e) {
       // 一定要是 input 事件
      this.$emit("input", e.target.value);
    },
  },
};
</script>

3、 .sync

可以帮我们实现父组件向子组件传递的数据 的双向绑定,所以子组件接收到数据后可以直接修改,并且会同时修改父组件的数据。

4、ref

ref 如果在普通的DOM元素上,引用指向的就是该DOM元素;

如果在子组件上,引用的指向就是子组件实例,然后父组件就可以通过 ref 主动获取子组件的属性或者调用子组件的方法。

// 父组件
<template>
  <div>
    <ShowRefs ref="myRef"/>
    <br>

    <!-- <h4>MyComp---{{ $refs.myRef }}</h4> -->
    <button @click="handle">点击</button>
  </div>
</template>

<script>
import ShowRefs from "@/components/refs/ShowRefs.vue"
export default {
  data () {
    return {
    }
  },
  components: {
    ShowRefs
  },
  methods: {
    handle() {
      console.log(this.$refs.myRef.msg);
      this.$refs.myRef.showMsg();
      this.$refs.myRef.handleClick();
    }
  }
}
</script>
​
// 子组件
<template>
  <div>
    <h2>ShowRefs---{{ msg }}</h2>
    <input type="text" ref="txtRef" id="txt" :value="msg">
    <button @click="handleClick">点击</button>
  </div>
</template>

<script>
export default {
  data () {
    return {
      msg:"hello world"
    }
  },
  created() { 
    console.log("created");
    console.log(this.$refs.txtRef)
  },
  mounted() { 
    console.log("mounted")
    console.log(this.$refs.txtRef)
  },
  methods: {
    handleClick() { 
      // alert(this.$refs.txtRef.value);
      // this.$refs.txtRef.value = "hello";
      this.msg = "大家好";
      this.msg = "哈哈哈";
      this.msg = "嘻嘻嘻";

      this.$nextTick(() => {
        console.log(document.getElementById("txt").value);
      })
    },
    showMsg() {
      console.log("showMsg---" + this.msg)
    }
  }
}
</script>

5、$emit / v-on

在父组件中给子组件绑定自定义事件,然后调用需要的方法,然后在子组件中用 this.$emit 触发父组件的事件,第一个是事件名第二个是参数

 父组件传递数据:

<!-- 父组件 -->
<template>
    <div>
        <!-- 传递数据 -->
        <UserShow :title="title" :users='users' @changeTitle="changeTitle" />
    </div>
</template>

<script>
import UserShow from './UserShow.vue';
export default {
    data () {
        return {
            title:'用户列表',
            users:[
                {id:1,username:'jack',age:15},
                {id:2,username:'rose',age:16},
                {id:3,username:'tom',age:17},
                {id:4,username:'bob',age:18},
            ]
        }
    },
    components:{// 注册
        UserShow
    },
    methods:{
        changeTitle(value){
            this.title = value
        }
    }
}
</script>

// secoped:引用组件时,避免造成其他组件使用到了该组件的样式,所以加上 scoped,表示只能在该组件内使用
<style scoped>
</style>

子组件接受父组件传递来的数据,通过 $emit 修改:

<!--  -->
<template>
    <div>
        <h2>{{ title }}</h2>
        <el-row>
            <el-col :span="12" :offset="6">
                <el-table
                    :data="users"
                    style="width: 100%"
                >
                    <el-table-column
                        prop="id"
                        label="编号"
                        width="180">
                    </el-table-column>
                    <el-table-column
                        prop="username"
                        label="姓名"
                        width="180">
                    </el-table-column>
                    <el-table-column
                        prop="age"
                        label="年龄">
                    </el-table-column>
                </el-table>
            </el-col>
        </el-row>
        <el-button type="success" plain  @click="handleTitle">点击修改title</el-button>
    </div>
</template>

<script>
export default {
    // props:['users','title'],// 此写法也可以
    props:{
        users:{
            type:Array,// 传递过来的数据类型
            required:true,// 表示父组件必须传递 users 数据过来
        },
        title:{
            type:String,
            default:'用户列表信息',// 如果父组件没有传递 title,则使用默认值
        }
    },
    methods:{
        handleTitle() {
            // 点击按钮修改标题
            // 语法:this.$emit('父组件的方法名','修改后的数据')
            this.$emit('changeTitle','用户信息列表展示')
        }
    }
}
</script>

<style scoped>
    .my_ul{
        list-style: none;
    }
</style>

6、$attrs / $listeners

多层嵌套组件传递数据时,如果只是传递数据,而不做中间处理的话就可以用这个,比如父组件向孙子组件传递数据时

v-bind="$attrs":包含父作用域里除 class 和 style 除外的非 props 属性集合。通过 this.$attrs 获取父作用域中所有符合条件的属性集合,然后还要继续传给子组件内部的其他组件,就可以通过 v-bind="$attrs"

v-on="$listeners":包含父作用域里 .native 除外的监听事件集合。如果还要继续传给子组件内部的其他组件,就可以通过 v-on="$linteners"

使用方式是相同的:

<!-- 父组件 -->
<template>
	<div class="block">
        <PageComp :pageSize="5" :currentPage="currentPage" :total="total"
                    @current-change="currentChange" @size-change="sizeChange" :page-sizes="[2, 5, 7, 10]" />
    </div>
</template>
<script>
	import PageComp from '@/components/pager/PageComp.vue';
    export default {
        data() {
            return {
        		pageSize:'5',
                currentPage:'1',
                total:''
    		}
		},
        method:{
            currentChange(v) {
                console.log('这是第'+v+'页');
			},
            sizeChange(v) {
                console.log(v+'条1页');
            }
        }
    }
</script>


<!-- 子组件 -->
<template>
    <div>
        <el-pagination layout="total, sizes, prev, pager, next, jumper"  v-bind="$attrs" v-on="$listeners">
        </el-pagination>
    </div>
</template>

<script>

export default {
    data() {
        return {
        }
    },
    mounted() {
    	console.log(this.$attrs); // 可以拿到父组件传来的数据
        console.log(this.$listerners) //{eventOne: fn}
  	},
}
</script>

7、 $children / $parent的使用

$children:获取到一个包含所有子组件(不包含孙子组件)的 VueComponent 对象数组,可以直接拿到子组件中所有数据和方法等

$parent:获取到一个父节点的 VueComponent 对象,同样包含父节点中所有数据和方法等

8、 provide / inject 祖孙之间通信传值

provide / inject 为依赖注入,说是不推荐直接用于应用程序代码中,但是在一些插件或组件库里却是被常用。

provide:可以让我们指定想要提供给后代组件的数据或方法。

inject:在任何后代组件中接收想要添加在这个组件上的数据或方法,不管组件嵌套多深都可以直接拿来用。

要注意的是 provide 和 inject 传递的数据不是响应式的,也就是说用 inject 接收来数据后,provide 里的数据改变了,后代组件中的数据不会改变,除非传入的就是一个可监听的对象。

9、EventBus全局事件总线

EventBus 是中央事件总线,不管是父子组件,兄弟组件,跨层级组件等都可以完成通信。

// 父组件
<template>
  <div>
    <button @click="handleClick">点击发射传递</button>
    <br>
    <SubComp/>
  </div>
</template>

<script>
import SubComp from "@/components/bus/SubComp.vue"
export default {
  data () {
    return {
    }
  },
  components: {
    SubComp
  },
  methods: {
    handleClick() { 
      this.$eventBus.$emit("sendMsg","hello world")
    }
  }
}
</script>

// 子组件
<template>
  <div>
    <h2>{{ msg }}</h2>
  </div>
</template>

<script>
export default {
  data () {
    return {
      msg:'jack'
    }
  },
  created() { 
    this.$eventBus.$on("sendMsg", (value) => { 
      console.log("sub--" + value);
      this.msg = value;
    })
  }
}
</script>

10、vuex通信

可以定义共享的数据源,在哪里都可以访问 安装配置 使用即可。

11、具名插槽+作用域插槽 slot

具名插槽是在父组件中通过slot属性,给插槽命名,在子组件中通过slot标签,根据定义好的名字填充到对应的位置。

作用域插槽是带数据的插槽,子组件提供给父组件的参数,父组件根据子组件传过来的插槽数据来进行不同的展现和填充内容。在标签中通过 v-slot="value" 来接受数据。

父组件:

<!-- 父组件 -->
<template>
  <div class="list">

    <SlotContainer title="用户列表">
      <template #header>
        <h4>这里是页头说明</h4>
      </template>

      <template #body="data">
        <h4>{{data.info}}</h4>
        <ul>
          <li v-for="(hot) in data.hots" :key="hot.id">{{ hot.name }}</li>
        </ul>
      </template>
      

      <template #footer>
        <h4>这里是页脚说明</h4>
      </template>
      
    </SlotContainer>
    
    <SlotContainer title="用户列表">
      <el-input placeholder="请输入内容">
        <template #prepend>
          hello
        </template>
        <template #append>
          world
        </template>
      </el-input>
    </SlotContainer>

    <SlotContainer title="用户列表">
      <ul>
        <li v-for="(user) in users" :key="user.id">{{ user.name }}</li>
      </ul>
    </SlotContainer>
  </div>
</template>

<script>
import SlotContainer from "@/components/slots/SlotContainer.vue"
export default {
  data () {
    return {
      users: [
        { id: 1, name: '张三', age: 18 },
        { id: 2, name: '李四', age: 19 },
        { id: 3, name: '王五', age: 20 }
      ]
    }
  },
  components:{
    SlotContainer
  }
}
</script>

<style scoped>
.list{
  display: flex;
  justify-content: space-around;
}
ul{
  list-style: none;
  margin:0;
  padding:0;
}
li{
  margin:0;
  padding:0;
}
</style>

子组件:

<!-- 子组件 -->
<template>
  <div class="box">
    <h2>{{ title }}</h2>
    <slot></slot>
    <slot name="header"></slot>
    <slot name="body" :hots="hots" info="hello"></slot>
    <hr>
    <slot name="footer"></slot>
  </div>
</template>

<script>
export default {
  props: ['title'],
  data() {
    return {
      hots: [
        { id: 1, name:"狂飙", hot:300},
        { id: 2, name:"漫长的季节", hot:500},
        { id: 3, name:"浪姐4", hot:400},
        { id: 4, name:"说唱", hot:600},
        { id: 5, name:"第一回合", hot:700},
      ]
    }
  },
  created() { 
    console.log(this);
  }
}
</script>

<style scoped>
.box{
  width: 200px;
  padding:10px;
  text-align: center;
  border:1px solid #ccc;
  box-shadow: 4px 4px 4px rgba(0, 0, 0, .4);
}
</style>

12、 $root

$root 可以拿到 App.vue 里的数据和方法。

八、View 页面

以前的组件,里面既有数据,也有渲染。

现在需要做到数据与组件分离,子组件只做数据渲染,数据由父组件传给子组件。

现在的页面是由一个个的Component组件所组成的,但是这些组件组成了一个页面,我们一个应用程序,是需要很多页面的,比如现在编写的也就是一个主页面,但是还会有详细页面,登录页面,注册页面等等,所以,我们可以将组件再次进行整合,组合成一个个的页面,再进行处理。

做法:

在 src 目录下创建 View 文件夹,在该文件夹下创建一个 Home.vue 页面。

在此页面,使用各个组件,放在页面需要的地方,写好各个组件需要使用到的数据,并且传给组件。

**补充小知识**:注意:如果封装走马灯组件Carousel.vue,要注意一个问题,既然是封装的组件,所以里面的图片肯定是不能固定的,所以,需要主页面传递参数过来

所以要注意一个问题,在调用静态图片的时候,不是直接在assets文件夹下面找文件,因为整个程序需要打包,所以打包之后静态文件的路径和现在是不一样的,所以需要使用 require 函数(require('地址') 引入),指定静态图片现在的位置。当然,可以直接将图片作为一个组件引入。

示例:

export default {
    data() {
        return {
            banners: [// 跑马灯数据
                { url: require('@/assets/banner/t1.jpg'), link: "https://blog.csdn.net/2201_75342929?spm=1000.2115.3001.5343" },
                { url: require('@/assets/banner/t2.jpg'), link: "https://blog.csdn.net/2201_75342929?spm=1000.2115.3001.5343" },
                { url: require('@/assets/banner/t3.jpg'), link: "https://blog.csdn.net/2201_75342929?spm=1000.2115.3001.5343" }
            ],
        }
    },
    components: {
        CarouselComp, 
    },
}

九、Vue-Router

在vue中看到$:代表使用但是是vue自身的属性或者是挂载到vue上的插件(比如route)的属性,目的是为了和咱自己定义的变量做一个区分

安装插件命令:npm install (若失败,可以使用cnpm install)

**补充小知识**:注意:如果需要使用sass语法写css,需要先下载sass:

npm i sass sass-loader -D

然后在style这里写上 lang="scss",就可直接写 sass 语法;或者直接创建一个scss文件引入进来即可。

scss 是 sass 的升级版,下载哪个都一样。

插槽

有时有这种需要,就是一个组件中,可能需要直接嵌套其他的代码,这个时候,就需要使用插槽Slot。

插槽,相当于一个预留位置,可以将任意的内容放入到插槽中。

插槽语法:

<slot></slot>

代码示例:

首先,创建一个 Center.vue 组件

<template>
  <div class="center">
      <!-- 屏幕中央 -->
      <!-- 插槽,相当于一个预留位置,可以将任意的内容放入到插槽中 -->
      <slot></slot>
  </div>
</template>
<style scoped>
.center{
    position: fixed;
    left: 50%;
    top: 50%;
    transform: translate(-50%,-50%);
}
</style>

这个组件唯一的作用就是居中,但是具体要将什么内容居中并没有添加,而是预留了一个Slot插槽,到时候,需要将什么内容居中,直接调用Center组件,将需要添加的内容加入进来即可。

再创建一个注册页面 Reg.vue:

<template>
  <Center>
      注册
  </Center>
</template>
<script>
import Center from '@/components/Center'
export default {
  components:{
      Center
  }
}
</script>

这时,页面上就可以看到注册页面两个字已经被居中了。这就是插槽的作用。

插槽的三种用法:

1. 默认插槽(一个插槽)

当不给插槽取名字的时候,就是默认插槽,此时所有你在子组件里面写的东西都在默认插槽里。

代码示例:

插槽组件写插槽占位:

<div>
	<slot></slot>
</div>

在需要使用插槽的地方--父组件 引入插槽文件并使用:

<template>
	<SlotContainer title="游戏列表">
    	内容
    </SlotContainer>
</template>
<script>
	import SlotContainer from "./SlotContainer.vue";
    components: {
    	SlotContainer
  	}
</script>

2. 具名插槽(多个插槽)

注意:使用具名插槽时,要在子组件写入具名插槽的名称,才能知道此时是使用的哪个插槽,在子组件写名称有 3 种写法:

第一种:
<template #插槽名>
    	内容
</template>
第二种:
<template v-slot:插槽名>
    	内容
</template>
第三种:
<template slot=插槽名>
    	内容
</template>

代码示例:

在需要占位的地方写上插槽,给插槽取名字:

<slot name='插槽名'></slot>

在使用插槽的地方,使用有名字的插槽,此时,就不会影响到其他插槽,在此处写的内容只会在此插槽内显示:

<SlotContainer> 引入的插槽组件
	<template #插槽名>
    	内容
    </template>
</SlotContainer>

其他插槽内容
<SlotContainer> 引入的插槽组件
    内容
</SlotContainer>

3. 作用域插槽(插槽传值)

在插槽组件里写占位插槽,并写入你想要传入的内容

代码示例:

在插槽组件里面传值:

<slot name="cartoonList" :cartoons="cartoons" info="日本动漫"></slot>

<script>
export default {
  props: ['title'],
  data() { 
    return {
      cartoons: ["七龙珠","火影忍者","Bleach","one piece"]
    }
  }
}
</script>

在使用插槽的地方--子组件里面接受值:

注意:接受值时,只能全部接受,作为一个对象传过来,用. 操作符操作数据,不能一个个接收。

<SlotContainer title="动漫列表">
      <template #cartoonList="data"> 此处的cartoonLis是插槽名,此处的data相当于变量名,随便取的
        <h2>{{ data.info }}</h2> // 日本动漫
        <ul>
          <li v-for="(item,i) in data.cartoons" :key="i">{{ item }}</li>
        </ul>
      </template>
</SlotContainer>

ref 与 $refs

我们vue默认肯定是通过数据去触发界面的更改,如果有时候我们想直接获取到界面的dom,可以使用ref和$refs,这两个一般都是一起使用的.

ref:一般用在组件上,相当于给组件一个名字。相当于命名

$refs:其实是一个全局变量,可以获取ref上面给定名字的组件对象。相当于获取

用法:

1、直接放在默认的dom组件节点上,就可以获取dom对象,注意一个问题,我这里虽然在说是dom节点组件,其实我们在vue template使用的原生的dom其实还是vue封装之后的组件

2、如果ref用在子组件上,可以再父组件中直接使用this.$refs.ref名字获取子组件的data数据,甚至是methods里面的方法

注意:ref是和dom进行绑定的,也就是说必须在dom渲染出来之后,才会产生作用。

注意:由于数据更新之后,需要一定的时间来触发界面的变化。当数据更新之后,所以下面通过$ref直接获取界面的信息,是上一次界面的内容。想要获取更新的新的数据,需要使用 this.$nextTick(回调函数)。

子组件:RefComp.vue

<template>
  <div>

    <div ref="myDiv">这是一个简单的div</div>
    <button @click="handleClick()">点击</button>
    <button @click="show()">show点击</button>
  
    <hr>

    <div ref="myDiv2">{{ msg2 }}</div>
    <button @click="changeMsg()">改变初始信息</button>
  </div>
</template>

<script>
export default {
  data () {
    return {
      msg: "这是子组件中的一条信息",
      msg2:"初始信息"
    }
  },
  created() { 
    console.log("---created---")
    console.log(this.$refs.myDiv2);
    //这里获取为undefinded,因为dom还没有挂载到界面上ref就获取不到
  },
  mounted() { 
    console.log("---mounted---")
    console.log(this.$refs.myDiv2);
  },
  updated() { 
    console.log("updated");
  },
  methods: {
    handleClick() { 
      console.log(this.$refs.myDiv);
      this.$refs.myDiv.style.border="1px solid #ccc";
      this.$refs.myDiv.style.padding="10px";
    },
    show() { 
      alert(this.msg);
    },
    changeMsg() { 
      this.msg2 = "改变了之后的信息";
      this.$nextTick(() => console.log("ref值改变的时机:" + this.$refs.myDiv2.innerHTML));
    }
  }
}
</script>

父组件:App.vue

<template>
  <div id="app">
    <RefComp ref="refcomp"/>
    <hr>
    <button @click="getRef()">点击通过ref获取子组件中的内容</button>
  </div>
</template>

<script>
import RefComp from "./components/RefComp";
export default {
  name: 'App',
  components: {
    RefComp
  },
  methods: {
    getRef() { 
    	//调用子组件中data和methods
      console.log("父组件---- " + this.$refs.refcomp.msg);
      this.$refs.refcomp.show();
    }
  }
}
</script>

事件总线 EventBus

注册时间总线对象

Vue.prototype.$bus = new Vue();

发送事件

this.$bus.$emit('事件名','发送的值');
this.$bus.$emit('sendMsg','hello world');

接收事件

this.$bus.$on('sendMsg',(v) => {console.log(v)});

Vue-router

浏览器无论访问什么地址,访问的真实页面始终是index.htmlvue根据不同的地址,渲染不同的组件。由于真实页面是唯一的,用户看到的页面切换,实际上是组件的切换,这种应用称之为单页应用

开发单页应用涉及到两个核心问题:

  1. 在哪个位置切换组件

  2. 访问路径如何对应组件

使用vue-router可以非常轻松的构建单页应用程序

官网地址:Vue Router | Vue.js 的官方路由

安装Vue-router

工程目录下运行下面的代码,安装vue-router

npm i vue-router

注意: 由于现在已经更新了Vue3,所以默认下载的是最新的vue-router 4.X版本,和这个和vue2不兼容

所以我们需要指定版本下载:

npm i vue-router@3.6.5

router-link 和 router-view 区别

  • router-link 使用语法请参考<a>的用法

<router-link> 组件支持用户在具有路由功能的应用中(点击)导航。 通过 to 属性指定目标地址,默认渲染成带有正确链接的<a>标签,可以通过配置 tag 属性生成别的标签.。另外,当目标路由成功激活时,链接元素自动设置一个表示激活的 CSS 类名router-link-active。

<!-- 渲染成li标签 -->
<router-link to="home" tag="li"></router-link>
  • router-view

    <router-view>组件是一个 functional 组件,渲染路径匹配到的视图组件。<router-view>渲染的组件还可以内嵌自己的<router-view>,根据嵌套路径,渲染嵌套组件。

操作示例:

由于路由配置较多,因此常见的做法是将路由单独设置,因此一般会单独创建文件夹进行配置。

示例步骤:

src 下创建 routes 文件夹,这个文件夹主要是路由设置

        1、创建路由配置文件config.js:

这个文件其实就是一个路径对应的配置文件,

该文件内配置示例如下:config.js

export default {
    mode:"history",
    routes: [
        {
            path:"/",
            alias:"/index*",
            name:"Home",
            component:()=>import("@/ViewPage/Home")// 这个路由对应的具体的组件
        },
        {
            path:"/filmsTable",
            name:"FilmsTable",
            component:()=>import("@/ViewPage/FilmsTable")
        },
        {
            path:"/filmsList",
            name:"FilmsList",
            component:()=>import("@/ViewPage/FilmsList")
        },
        {
            path:"/personal",
            name:"Personal",
            component:()=>import('@/ViewPage/PersonalComp'),
            // 权限验证
            meta:{
                auth:true
            }
        },
        {
            path:"/auth",
            name:"Auth",
            component:()=>import('@/ViewPage/Auth')
        },
        {
            path:"/login",
            name:"Login",
            component:()=>import('@/ViewPage/LoginComp')
        },
        {
            path:"/reg",
            name:"Reg",
            component:()=>import('@/ViewPage/RegComp')
        },
        {
            path:"*",
            name:"404",
            component:()=>import('@/ViewPage/NotFound')
        },
    ]
}

路由模式:

  • hash:路径来自于地址栏中#后面的值,这种模式兼容性比较好

  • history:路径来自于真实的地址路径,旧浏览器不兼容

  • abstract:路径来自于内存

        2、创建 index.js 文件:

这个文件主要就是new VueRouter对象,当然需要把配置的内容导入进来:

代码示例如下:

import Vue from 'vue'
import VueRouter from 'vue-router'
import config from './config'; //路由一般配置较多,所以一般单独配置

//1.安装
Vue.use(VueRouter);

//2.创建路由对象
var router = new VueRouter(config);

export default router;

        3、在 main.js 文件中引入

 这里的引入直接使用的是./routers直接指定的是文件夹,实际上其实是指定的./routers/index.js,在vue中约定,如果不指定具体的文件,那么就会直接指向这个文件夹下面的index.js文件

有了路由的配置之后,就需要确定,哪些地方需要使用到路由

在Header上点击不同的链接,相当于就需要跳转切换不同的页面,这也就是路由最大的作用,帮助我们在单页应用中实现页面的跳转。

        4、内容都是需要通过路由去进行切换的,因此,确定要需要切换的部分,在页面上使用 路由标签:<router-view></router-view>,确定这部分的内容是需要进行路由切换的。 实际上,就是通过 js 由原来生成的虚拟DOM,切换成另外一个虚拟DOM。

代码示例如下:

<template>
  <div id="app">
    <el-container>
      <el-header>
        <Header />
      </el-header>
      <el-main>
        <!-- <Home></Home> -->
        <!-- 使用路由标签 -->
        <router-view></router-view>
      </el-main>
    </el-container>
  </div>
</template>
...下面的部分省略

        5、修改 Header 组件或其他使用 a 标签跳转的组件里面的 a 标签链接为路由配置的地址

a标签最大的问题是页面会发生刷新,导致整个页面都会重新渲染,所以,我们使用 router 的专属标签<router-link>进行替换。

to='/index' :声明式导航,直接跟路径

:to='{name:'路由配置的组件名'}':命令式导航,:to 后面是 config 中配置的路由的名字

两种方式均可使用,但推荐使用 命令式导航的方式。

使用 a 标签书写的示例代码如下:

......其他部分省略
<ul class="nav">
    <li><a href="/index">首页</a></li>
    <li><a href="">Voluptates.</a></li>
    <li><a href="">Molestiae?</a></li>
    <li><a href="">Ut.</a></li>
    <li><a href="">Nihil.</a></li>
</ul>
<div class="user">
    <a href="/login">登录</a>
    <a href="/reg">注册</a>
</div>
......

上面使用的还是a标签,a标签最大的问题是页面会发生刷新,导致整个页面都会重新渲染,所以,我们使用router的专属标签<router-link>进行替换:

使用 路由标签 代码示例如下:

<ul class="nav">
    <li>
      <!-- 声明式导航,直接跟路径 -->
      <router-link to="/index">首页</router-link>
    </li>
    <li><a href="">Voluptates.</a></li>
    <li><a href="">Molestiae?</a></li>
    <li><a href="">Ut.</a></li>
    <li><a href="">Nihil.</a></li>
</ul>
<div class="user">
    <!-- 命名式导航,:to后面是config中配置的路由的名字 -->
    <router-link :to="{name:'Login'}">登录</router-link>
    <router-link :to="{name:'Reg'}">注册</router-link>
</div>

此时,简单的通过路由实现跳转已经完成。但首页后面几个可以跟上频道信息,点击频道,就切换到这个频道的新闻列表,那么就会出现问题:切换频道需要传递频道ID,意味着这里的这里需要路由传值 。

路由传值

首先加入新闻列表的页面 ChannelNews.vue:

示例代码如下:

<template>
  <Center>
      新闻页面,频道id是===>{{$route.params.id}}
  </Center>
</template>
<script>
import Center from '@/components/Center'
export default {
  components:{
    Center
  }
}
</script>

当然这个页面最重要的是获取频道id,可以通过路由的代码{{$route.params.id}}得到,但是要得到这个传递过来的值,还需要做一些工作:

1、配置相关路由,表示需要路由传值

2、父组件传递相关的值

所以,首先在路由的 config.js 中加入下面映射关系的代码

...
{
    path:"/channels/:id",
    name:"Channels",
    component:()=>import('@/views/ChannelNews')
},
...

这段代码的意思就是url地址是/channels/频道id,而这个id是需要父页面传递过来的

因此,现在Header组件中,需要加载频道的内容

<template>
  <div class="header">
      <div class="header-container">
          <div class="container">
            <div class="logo">
              <a href="">
                <img :src="logUrl" alt="">
              </a>
            </div>
            <ul class="nav">
                <li>
                  <!-- 声明式导航,直接跟路径 -->
                  <router-link to="/index">首页</router-link>
                </li>
                <li v-for="(channel) in channels.slice(0,5)" :key="channel.channelId">
                  <router-link
                  :to="{
                    name:'Channels',
                    params:{
                      id:channel.channelId
                    }
                  }">
                    {{channel.name}}
                  </router-link>
                </li>
                
            </ul>
            <div class="user">
                <!-- 命名式导航,:to后面是config中配置的路由的名字 -->
                <router-link :to="{name:'Login'}">登录</router-link>
                <router-link :to="{name:'Reg'}">注册</router-link>
            </div>
          </div>
      </div>
  </div>
</template>

<script>
import logo from '@/assets/logo.png'
import {getNewsChannels} from '@/services/NewsService';
export default {
  data() {
    return {
      // logUrl:require('@/assets/logo.png')
      logUrl:logo,
      channels:[]
    }
  },
  async created() {
    let resp = await getNewsChannels();
    this.channels = resp;
  },
}
</script>

<style scoped>
...省略
</style>

当然关键代码就是 created() 函数获取值的部分,与网页模板中对于频道值的循环。

十、Watch 与 命令式导航

为了更好的理解,首先加入新的分页组件

封装分页组件

具体分页API,参考ElementUI Pagination

创建Pager.vue组件

<template>
  <div>
    <el-pagination     
        layout="prev, pager, next" 
        :page-size="limit"
        :current-page="page"
        :pager-count="pagerCount"
        :total="total"
        @current-change="handleCurrentChange">
    </el-pagination>
  </div>
</template>

<script>
export default {
    props:["limit","page","pagerCount","total"],
    methods:{
        handleCurrentChange(page){
            console.log(page);
            this.$emit("pageChange",page);
        }
    }
};
</script>
<style></style>

没有数据的时候,先模拟数据测试一下 ,在ChannelNews.vue中使用

<template>
  <div>
    新闻页面,频道id是===>{{$route.params.id}}
    <Pager 
      :total="total" 
      :page="page" 
      :limit="limit" 
      :pagerCount="pagerCount"
      @pageChange="handlePageChange"></Pager>
  </div>
</template>
<script>
import Center from '@/components/Center'
import Pager from '@/components/Pager'
export default {
  components:{
    Center,Pager
  },
  data(){
    return {
      total:1000,
      pagerCount:11,
      limit:10,
      page:1
    }
  },
  methods:{
    handlePageChange(page){
      console.log("page=====" + page);
    }
  }
}
</script>
<style>
</style>

注意:ElementUI 的分页组件有个小坑,页码 pager-count 属性的数量只能是奇数

然后,加入新闻数据:

<template>
  <div>
    新闻页面,频道id是===>{{$route.params.id}}
    <NewList :news="news"></NewList>
    <Pager 
      :total="total" 
      :page="page" 
      :limit="limit" 
      :pagerCount="pagerCount"
      @pageChange="handlePageChange"></Pager>
  </div>
</template>
<script>
import Center from '@/components/Center'
import Pager from '@/components/Pager'
import NewList from '@/components/NewList'
import {getNewsList} from '@/services/NewsService'
export default {
  components:{
    Center,Pager,NewList
  },
  data(){
    return {
      total:1000,
      pagerCount:11,
      limit:10,
      page:1,
      news:[]
    }
  },
  methods:{
    handlePageChange(page){
      console.log("page=====" + page);
    },
    async getData(){
      let resp = await getNewsList(
        this.$route.params.id,
        this.page,
        this.limit
      );
      this.news = resp.contentlist;
      this.total = resp.allNum;
    }
  },
  created() {
    this.getData();
  },
}
</script>

新闻列表数据是显示出来了,但是现在点击分页并不能改变新闻列表的值。

关键点是,现在点击分页页码,不可能和在created中获取的数据产生任何关联,因此代码要做出修改,应该是点击分页页面的时候去获取远程数据,并且在每次改变的时候要改变数据。

试着把获取数据的内容放到触发分页的方法中:

...前后代码省略
methods:{
    handlePageChange(page){
      console.log("page=====" + page);
      this.getData();
    },
    ......
}

但是现在其实还是有一个小bug,如果在url地址上输入分页信息,并不是实现分页的效果 比如输入:127.0.0.1:8080/频道id?page=3 这种形式,我们并不能捕获?page=3后面的查询参数,要实现这个效果,我们可以使用命令式导航 。

命令式导航

之前我们都是通过 <router-link> 标签来实现路由导航,也可以通过编码的方式实现导航

this.$router.push();

我们可以直接使用下面的代码:

...其他代码省略
computed:{
    page(){
      return +this.$route.query.page || 1;
    }
},
methods:{
    handlePageChange(page){
      console.log("page=====" + page);
      // console.log("this.$route.query.page1=====" + this.$route.query.page);
      this.$router.push("?page="+page);
      // console.log("this.$route.query.page2=====" + this.$route.query.page);
      this.getData();
    },
    ...其他代码省略
}

his.$router.push("?page="+page);这句代码的意义是:如果当前url地址不变化,就将当前url地址后面加上page变量,比如之前是 http://localhost:8080/channels/5572a108b3cdc86cf39001cd,加上这句代码之后就会变成 http://localhost:8080/channels/5572a108b3cdc86cf39001cd?page=3,相当于把当前页面参数传递了过去

而之前放在data()中的变量,也直接换成了计算属性,

this.$route.query.page 这句代码的意义是,从当前路由中取出page属性的值,注意值是一个字符串,所以return +this.$route.query.page || 1;这句代码的意义是返回当前的页码,并且转换为数字类型,如果没有,就返回1

所以这一段代码加入之后,分页已经可以正常使用了。

注意:

this.$router.push("?page="+page); 

只是一种简写,如果前面的参数可能发生变化,比如每次切换频道,频道id都会发生变化,那就需要使用下面的写法:

this.$router.push({
    name:"Channels",
    params:{
      id:this.$route.params.id
    },
    query:{
      page:page
    }
})

所以,最后我们的代码可以改成下面的样子:

......其他省略
computed:{
    page(){
      return +this.$route.query.page || 1;
    }
},
methods:{
    handlePageChange(page){
      this.$router.push({
          name:"Channels",
          params:{
            id:this.$route.params.id
          },
          query:{
            page:page
          }
      })
      this.getData();
    }
    ....
}

现在能点击不同的页码就行分页了,但是还有一个重要问题,初始没有,当然,同样可以在created() 钩子函数中调用获取异步数据的方法,让数据在初始化的时候显示一下就可以了。

Watch 侦听属性

但是现在其实还有一个更重要的 bug,我们切换不同的频道,发现频道 id 其实有变化,但是,新闻列表界面却没有任何变化。

watch 的作用可以监控一个值的变换,并调用因为变化需要执行的方法。可以通过 watch 动态改变关联的状态。

简单来说,就是在某个值上,绑定一个事件监听,当这个值发生了变化,就会执行相关的方法

使用方式其实和计算属性,方法差不多:

watch 语法:

...
watch:{
    "监听的值":{
        //设置为true表示一开始就需要监听,不然第一次没有值的时候是不会监听的
        immediate:true,
        handler(){
            //需要执行的方法
        },
        //开启深度监听
        //deep:true
    }   
}
...

因此,在代码中我们可以加入watch侦听属性

...其他省略
// created() {
//   this.getData();
// },
watch: {
    "$route.params.id":{
      immediate:true,
      handler(){
        console.log(this.$route);
        this.getData();
      }
    }
},

这样,一个完整的路由跳转就完成了。

computed与watch的区别

其实上面的效果,使用 computed 计算属性一样可以实现,下面的是简单实现

computed: {
  channelId(){
    getNewsContent(this.$route.params.id).then(result => { 
      this.news = result.showapi_res_body.pagebean.contentlist;
    })
    return this.$route.params.id
  }
},
  
//界面
<h2>{{channelId}}</h2>
<NewList :news="news"></NewList>

那为什么要使用watch,不使用computed呢?

最主要的原因是:computed 的计算是惰性的,只有在依赖的数据发生变化时才会重新计算。而watch则会在数据变化时立即执行操作,因此它的执行是及时的。什么叫惰性?我们举例说明:

例如,假设我们有一个data对象,它有两个属性x和y,还有一个computed属性z,计算方式为x + y。那么当我们修改x或y的值时,z不会立即重新计算,只有在我们获取z的值时才会重新计算。这就是computed的惰性计算。

而如果我们使用watch来监听x和y的变化,并在变化时执行一些操作,那么每当x或y的值发生变化时,相应的操作就会立即执行,而不需要等到获取计算结果时才执行。

比如,你把上面在界面显示的<h2>{{channelId}}</h2>这句话去掉,你会发现界面马上不会有显示了,因为computed 我们不去获取他的值,他就不会进行计算。对于我们这里其实就是channelId()这个计算属性你不去获取,里面的内容就不会去执行。而watch是及时的,只要监听的值改变了,就会执行。

$attrs 与 $props 的区别

this.$attrs 可以获取组件传递过来的所有属性,但是如果属性已经在 props 中声明过了,就不会获取。反过来说,如果 props 中一个属性都没有声明,那么我们就可以直接在 this.$attr中获取所有属性。

this.$props 可以获取在 props 中声明的所有属性。

v-bind与v-on

我们通常的用法都是 v-bind:id='10' v-bind:msg='hello' 或者直接简写 :id :msg

其实,v-bind 是可以单独使用的,比如我们传递的是一大堆对象值。

let post = {
	id:1,
	msg:'hello'
}

我们可以直接通过 v-bind 获取

v-bind = 'post'
其实这个写法就相当于
v-bind:id = 'post.id' v-bind:msg = 'post.msg'

同理 直接使用 v-on 也是一个意思。

KeepAlive

<KeepAlive> 是一个内置组件,它的功能是在多个组件间动态切换时缓存被移除的组件实例。

地址:KeepAlive | Vue.js

简单来说,如果我们多次要切换页面,那么就存在页面组件的创建createddestroy,比如我们的路由,如果我们想要提高效率,就可以通过KeepAlive标签,缓存我们的组件。

<KeepAlive> 的使用很简单,只需要包裹住我们需要切换的标签即可,比如下面的路由标签:

<keep-alive>
	<router-view></router-view>
</keep-alive>

我们可以将不同的组件创建生命周期函数created与destroy,看看有没有<keep-alive>的区别:

created() {
    console.log("xxx created");
},
destroyed() {
    console.log("xxx destroyed");
},

我们还能为每个组件创建名字,然后在<keep-alive>标签中使用排除和包含:

LoginComp.vue组件
export default {
  name:"loginComp",
  ......
}

RegComp.vue组件
export default {
  name:"regComp",
  ......
}

其中 :max 表示最大缓存实例数

<keep-alive include="loginComp,regComp" max="2">
	<router-view></router-view>
</keep-alive>

十一、混入 mixins

如果有时候,我们很多页面,都有获取新闻列表的事情要做,有着类似的功能,这些功能代码分散在组件不同的配置中。

 于是,我们可以把这些配置代码抽离出来,利用混入融合到组件中。

具体的做法非常简单:

// 抽离的公共代码
const common = {
  data(){
    return {
      a: 1,
      b: 2
    }
  },
  created(){
    console.log("common created");
  },
  computed:{
    sum(){
      return this.a + this.b;
    }
  }
}

/**
 * 使用comp1,将会得到:
 * common created
 * comp1 created 1 2 3
 */
const comp1 = {
  mixins: [common] // 之所以是数组,是因为可以混入多个配置代码
  created(){
    console.log("comp1 created", this.a, this.b, this.sum);
  }
}

混入并不复杂,更多细节参见官网 。

十二、Vuex

Vuex 是什么?

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

官网地址:Vuex 是什么? | Vuex

什么是“状态管理模式”?

让我们从一个简单的 Vue 计数应用开始:

new Vue({
  // state
  data () {
    return {
      count: 0
    }
  },
  // view
  template: `
    <div>{{ count }}</div>
  `,
  // actions
  methods: {
    increment () {
      this.count++
    }
  }
})

这个状态自管理应用包含以下几个部分:

  • state,驱动应用的数据源;

  • view,以声明方式将 state 映射到视图;

  • actions,响应在 view 上的用户输入导致的状态变化。

以下是一个表示“单向数据流”理念的简单示意:

但是,当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:

  • 多个视图依赖于同一状态。

  • 来自不同视图的行为需要变更同一状态。

对于问题一,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。

对于问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。

因此,我们为什么不把组件的共享状态抽取出来,以一个全局单例模式管理呢?在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!

通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。

安装

安装vuex

在页面中引入vuex

npm install vuex --save

由于 vue 最新的版本是 vue3,vuex 直接下载的是最新的版本和 vue3 进行匹配的,所以我们要用和 vue2 匹配的 vuex 需要指定版本号:

npm install vuex@3.6.2 --save

该库提供了一个构造函数Vuex.Store,通过该构造函数,即可创建一个数据仓库:

import Vuex from 'vuex';

// 把Vuex放到全局
Vue.use(Vuex);

var store = new Vuex.Store({
  // 仓库数据配置
    state:{
        
    }
})

new Vue({
  // 其他配置
  store
})

// 导出仓库
export default store;

vuex 工作流程

核心

Vuex 主要有五部分:

  • state:包含了store中存储的各个状态。

  • getters: 类似于 Vue 中的计算属性,根据其他 getter 或 state 计算返回值。

  • mutations: 一组方法,是改变store中状态的执行者,只能是同步操作。参数:状态,传过来的要改变的值

state:{
	...
},
// 改变store中的状态,同步操作
mutations:{
     changeName(state,payload) {
          state.username = payload;
     }
}

                界面通过 commit 来进行同步修改

methods:{
        handleChange(){
            this.$store.commit('changeName','angela')
        }
}
  • actions: 一组方法,其中可以包含异步操作。参数:context:上下文,payload:修改后的值
// 改变store中的状态,异步操作
actions:{
     // context:上下文,payload:修改后的值
     异步方法名(context,payload){
         setTimeout(()=>{
             context.commit('changeName',payload);
         },1000)
     }
}

                界面通过 dispatch 来进行异步修改

methods:{
        handleChange(){
            // 异步
            this.$store.dispatch('异步方法名','杨')
        }
}
  • modules: 模块拆分

State

Vuex 使用 state 来存储应用中需要共享的状态。为了能让 Vue 组件在 state更改后也随着更改,需要基于state 创建计算属性。

数据配置:

var store = new Vuex.Store({
  // 仓库数据配置
  state: {
    count: 2,
    todos: [
      { id: 1, text: '完成SpringMVC学习', done: true },
      { id: 2, text: '完成Vue的学习', done: false },
      { id: 3, text: '完成Spring Cloud学习', done: false },
      { id: 4, text: '完成Mybatis学习', done: true }
    ]
  },
  .......
}

可以在data()或者计算属性中调用

export default {
    data() {
        return {
            count:this.$store.state.count,
            todos:this.$store.state.todos,
        } 
    },
    ......
}

注意:state里面的数据在使用的时候,一般是挂在computed里面的,因为如果你挂在data上面,只会赋值一次,不会跟着vuex里面的变化而同步变化,当然也可以通过watch $store去解决这个问题,所以,如下:

computed:{
    count() {return this.$store.state.count},
    todos() {return this.$store.state.todos},
}

Getter

类似于 Vue 中的 计算属性(可以认为是 store 的计算属性),getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

Getter 方法接受 state 作为其第一个参数:

......
getters: {
    doneTodos: (state) => {
      return state.todos.filter(todo => todo.done)
    },
}
......

Getter 会暴露为 store.getters 对象,可以以属性的形式访问这些值:

在组件中可以很方便的调用getters中的值

this.$store.getters.doneTodos

Getter 方法也接受 state 和其他 getters 作为前两个参数。

......
getters: {
    doneTodos: (state) => {
      return state.todos.filter(todo => todo.done)
    },
    donesCount:(state,getters) => {
      return getters.doneTodos.length
    },
}
......

调用

this.$store.getters.donesCount

也可以通过让 getter 返回一个函数,来实现给 getter 传参。在对 store 里的数组进行查询时非常有用。

getters: {
    doneTodos: (state) => {
      return state.todos.filter(todo => todo.done)
    },
    donesCount:(state,getters) => {
      return getters.doneTodos.length
    },
    getTodoById: (state) => (id) => {
      return state.todos.find(todo => todo.id === id)
    }
    // ...上面的方法完整写法应该是下面这个样子
    // getTodoById:function(state){
    //   return function(id){
    //     let r = state.todos.find(function(item){
    //       return item.id === id
    //     })
    //     return r;
    //   }
    // }
  },

调用:

......
methods:{
    getTodo(){
        return this.$store.getters.getTodoById(2)
    },
}
......

Mutation

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。也就是说,前面两个都是状态值本身,mutations才是改变状态的执行者。

注意:mutations只能是同步地更改状态。

Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数。

......
mutations: {
    increment1 (state) {
        //变更状态
      state.count++
    },
}
......

调用:

methods:{
    getIncrement1(){
        this.$store.commit("increment1");
        console.log(this.$store.state.count);
    },
}

提交载荷(Payload)

注意:作为payload的参数有且只有一个

比如 increment2(state,payload1,payload2) 这种写法是不被允许的,如果要传入多个参数,只能将参数封装在一个对象中传入increment2(state,{payload1,payload2})

......
mutations: {
    increment1 (state) {
      state.count++
    },
    increment2 (state, payload) {
      state.count = payload
    },
    increment3 (state, payload) {
      state.todos.push(payload.todo)
    } 
},
......

调用

methods:{
    getIncrement1(){
        this.$store.commit("increment1");
        console.log(this.$store.state.count);
    },
    getIncrement2(){
        this.$store.commit("increment2",20);
        console.log(this.$store.state.count);
    },
    getIncrement3(){
        this.$store.commit("increment3",{
            todo:{id: 5, text: 'xxxx', done: true}
        });
        console.log(this.$store.state.todos);
    }
}

其中,increment2 第一个参数是state,后面的参数是向 store.commit 传入的额外的参数,即 mutation 的 载荷(payload)

store.commit方法的第一个参数是要发起的mutation类型名称,后面的参数均当做额外数据传入mutation定义的方法中。

规范的发起mutation的方式如下:

// 以载荷形式
this.$store.commit("increment3",{
    todo:{id: 5, text: 'xxxx', done: true}
});

额外的参数会封装进一个对象,作为第二个参数传入mutation定义的方法中。

mutations: {
  increment (state, payload) {
    increment3 (state, payload) {
      state.todos.push(payload.todo)
    }
  }
}

Action

想要异步地更改状态,就需要使用action。action并不直接改变state,而是发起mutation。

注册一个简单的 action:

actions: {
    add1(context) {
      context.commit('increment1')
      console.log(store.state.count);
    },
    add2(context,payload) {
      setTimeout(()=>{
        context.commit('increment2',payload);
        console.log(store.state.count);
      },1000);
      
    },
    add3(context,payload) {
      setTimeout(()=>{
        context.commit('increment3',payload);
        console.log(store.state.todos);
      },1000)     
    }
}

Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。

发起action的方法形式和发起mutation一样,只是换了个名字dispatch。

// 以一般形式调用
this.$store.dispatch("add2", 20);
// 以对象形式分发Action
this.$store.dispatch({
    type: "add3",
    todo: { id: 6, text: "ooooo", done: false },
});

Action处理异步的正确使用方式

想要使用action处理异步工作很简单,只需要将异步操作放到action中执行(如上面代码中的setTimeout)。

// 假设 getData() 和 getOtherData() 返回的是 Promise
actions: {
  async actionA ({ commit }) {
    commit('gotData', await getData())
  },
  async actionB ({ dispatch, commit }) {
    await dispatch('actionA') // 等待 actionA 完成
    commit('gotOtherData', await getOtherData())
  }
}

Action与Mutation的区别

Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。

  • Action 可以包含任意异步操作,而Mutation只能且必须是同步操作。

Module

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。 这时我们可以将 store 分割为模块(module)每个模块拥有自己的 state 、 getters 、mutations 、actions 、甚至是嵌套子模块——从上至下进行同样方式的分割。

简单示例:

const moduleA = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: { ... },
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
// 注意:要使用模块调用方法的话,每个模块中需要有 namespaced:true 这个属性
 namespaced:true
//调用getters模块方法          
this.$store.getters['a/doneCount']
//调用mutations方法  
this.$store.commit("a/increament1");
//调用actions方法  
this.$store.dispatch("a/add1");

辅助函数方法

在我们使用Vuex的过程中,我们可以选择使用Vuex提供的辅助函数来获取我们需要的StateGettersMutations或者Actions,这样我们就不用通过$store来获取了。

为了简便起见,Vuex 提供了四个辅助函数方法用来方便的将这些功能结合进组件。

  1. mapState

  2. mapGetters

  3. mapMutations

  4. mapActions

// $store获取
// this.$store.state.模块名称.属性
this.$store.state.moduleA.count
this.$store.getters.storeCount
this.$store.mutations.moduleA.increment
this.$store.actions.moduleA.increment

// 使用mapXXX获取
// 辅助函数('模块名称',['该模块仓库的属性或者方法']
mapState('moduleA', ['count'])
mapGetters('moduleA', ['storeCount'])
mapMutations('moduleA', ['increment'])
mapActions('moduleA', ['increment'])

我们可以看到,通过this.$store来获取属性或者方法,我们需要撰写许多同质化的代码。为了更优雅的获取我们想要的属性或者方法,Vuex提供了一些辅助APImapStatemapGettersmapMutationsmapActions

示例代码:

import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'

export default {
    // ...
    computed: {
        /**
         -----mapState-----
         注意:如果没有模块,第一个参数就不写
         ...mapState('模块的名字',[模块中state的属性])
         例如:...mapState('counter',['count','number','sum'])
         
         如果想要更改属性名:可这样写:
         ...mapState('counter',{
         	更改后的名字:'更改前的名字',
         	更改后的名字:'更改前的名字',
         	...
         	如果想要自己增加属性:
         	计算属性名:(state) => state.count + state.number + 10
         })
         例如:
         ...mapState('counter',{
         	c1:'count',
         	c2:'number',
         	c3:'sum',
         	c4:(state) => state.count + state.number + 10
         })
        */
      localComputed () { /* ... */ },
        // 使用对象展开运算符将此对象混入外部对象中
      ...mapState({
        // 为了能够使用 `this` 获取局部状态,必须使用常规函数
        count(state) {
          return state.count + this.localCount
        }
      }),
      ...mapGetters({
        getterCount(state, getters) {
          return state.count + this.localCount
        }
      })
    }
    methods: {
      ...mapMutations({
          // 如果想将一个属性另取一个名字,使用以下形式。注意这是写在对象中
           add: 'increment' // 将 `this.add()` 映射为`this.$store.commit('increment')`
        }),
      ...mapActions({
          add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
        })
    }
}

如果结合进组件之后不想改变名字,可以直接使用数组的方式。

methods: {
    ...mapActions([
      'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`

      // `mapActions` 也支持载荷:
      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
    ]),
}

在工程中使用Vuex

在工程中使用vuex肯定是要遵循工程化的内容,使用模块化的方式:

 在工程中使用 Vuex 示例:

1、创建channelStore模块

import {getNewsChannels} from '@/services/NewsService'

export default{
    namespaced:true,
    state:{
        channels:[],
        isLoading:false
    },
    mutations:{
        setIsLaoding(state,payload){
            state.isLoading = payload;
        },
        setChannels(state,payload){
            state.channels = payload;
            console.log(state.channels);
        }
    },
    actions:{
        async fetchChannels(context){
            let resp = await getNewsChannels();
            context.commit("setChannels",resp);   
        }
    }
}

2、创建index.js文件

这个文件的主要作用是创建vuex的store对象,并引用module模块。

import Vue from 'vue'
import vuex from 'vuex'
import channelStore from './channelStore';

Vue.use(vuex);

var store = new vuex.Store({
    modules:{
        channelStore
    }
});

export default store;

3、在main.js引入store

在main.js主要是注入store对象,并且调用action的fetchChannels方法,在程序启动的时候就访问远程channel数据放入到store中。

...其他代码省略
import store from './stores'

//程序启动的时候就调用action
store.dispatch("channelStore/fetchChannels");

new Vue({
  render: h => h(App),
  router,
  store
}).$mount('#app')

4、替换Channels.vue中的数据获取方式

import {mapState} from 'vuex'
export default {
    //其他代码省略
    computed:{
        ...mapState("channelStore",["channels"]),
        //其他代码省略
    },
    watch: {
      channels:{
          immediate:true,
          handler(){
              if(this.channels.length > 0){
                  this.changeChannel(this.channels[0].channelId);
              }
          }
      }
    },
}

除了Channels.vue组件,在Header.vue组件中其实也用到了频道数据

因此,也可以直接进行替换

5、替换Header.vue中关于频道的数据

// 模板代码省略
import { mapState } from "vuex";
import logo from '@/assets/logo.png'
export default {
  components:{
    logo
  },
  data() {
    return {
      logUrl:logo,
    }
  },
  computed:{
    ...mapState("channelStore", ["channels"]),
  }
}

以上,就是使用 vuex 的大概流程。

十三、JWT

什么是JWT?

Json web token (JWT),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519)。该 token 被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

起源

说起JWT,我们应该来谈一谈基于 token 的认证和传统的 session 认证的区别。

传统的session认证

我们知道,http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据 http 协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证。

但是这种基于 session 的认证使应用本身很难得到扩展,随着不同客户端用户的增加,独立的服务器已无法承载更多的用户,而这时候基于 session 认证应用的问题就会暴露出来。

基于session认证所显露的问题

Session: 每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。

扩展性: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。

CSRF: 因为是基于 cookie 来进行用户识别的,cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。

基于 token 的鉴权机制

基于 token 的鉴权机制类似于 http 协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于 token 认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。

流程上是这样的:

  • 用户使用用户名密码来请求服务器

  • 服务器进行验证用户的信息

  • 服务器通过验证发送给用户一个 token

  • 客户端存储 token,并在每次请求时附送上这个 token 值

  • 服务端验证 token 值,并返回数据

这个 token 必须要在每次请求时传递给服务端,它应该保存在请求头里, 另外,服务端要支持CORS(跨来源资源共享)策略,一般我们在服务端这么做就可以了Access-Control-Allow-Origin: * 。当然,如果前端做了代理的情况下就不需要纠结这些问题。

JWT长什么样?

JWT是由三段信息构成的,将这三段信息文本用.链接一起就构成了Jwt字符串。就像这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

JWT的构成

第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature)。

header

jwt 的头部承载两部分信息:

  • 声明类型,这里是jwt

  • 声明加密的算法 通常直接使用 HMAC SHA256

完整的头部就像下面这样的JSON:

{
  'typ': 'JWT',
  'alg': 'HS256'
}

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

playload

载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分

  • 标准中注册的声明

  • 公共的声明

  • 私有的声明

标准中注册的声明 (建议但不强制使用) :

  • iss: jwt 签发者

  • sub: jwt 所面向的用户

  • aud: 接收 jwt 的一方

  • exp: jwt 的过期时间,这个过期时间必须要大于签发时间

  • nbf: 定义在什么时间之前,该 jwt 都是不可用的.

  • iat: jwt 的签发时间

  • jti: jwt 的唯一身份标识,主要用来作为一次性 token ,从而回避重放攻击。

公共的声明 : 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息。但不建议添加敏感信息,因为该部分在客户端可解密。

私有的声明 : 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64 是对称解密的,意味着该部分信息可以归类为明文信息。

定义一个payload:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

然后将其进行 base64 加密,得到 Jwt 的第二部分。

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

signature

jwt 的第三部分是一个签证信息,这个签证信息由三部分组成:

  • header (base64后的)

  • payload (base64后的)

  • secret

这个部分需要 base64 加密后的 header 和 base64 加密后的 payload 使用.连接组成的字符串,然后通过 header 中声明的加密方式进行加严 secret 组合加密,然后就构成了 jwt 的第三部分。

将这三部分用.连接成一个完整的字符串,构成了最终的 jwt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

注意:secret是保存在服务器端的,jwt 的签发生成也是在服务器端的,secret 就是用来进行 jwt的签发和 jwt 的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个 secret,那就意味着客户端是可以自我签发 jwt 了。

如何应用?

一般是在请求头里加入Authorization,并加上Bearer 标注:

fetch('api/user/1', {
  headers: {
    'Authorization': 'Bearer ' + token
  }
})

服务端会验证token,如果验证通过就会返回相应的资源。整个流程就是这样的:

 JWT 最重要的是需要在后台封装数据,所以,首先是:

后台代码

1、javabean对象

首先需要可以用来传值的 javabean 对象,jwt 都是用来登录验证使用,所以这里是登录 User 对象

User.java

@Data
@EqualsAndHashCode(callSuper = true)
@TableName("t_user")
public class User extends Model implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "user_pkid", type = IdType.AUTO)
    private Integer userPkid;
    private String username;
    private String password;
    //...其他代码省略
}

2、导入jwt需要的jar包

<!-- jsonwebtoken -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.4.0</version>
</dependency>

注意:如果你使用的是JDK11,那么接下来的代码你在运行的时候可能会报错。

org.springframework.web.util.NestedServletException: 
Handler dispatch failed; 
nested exception is java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter

javax/xml/bind/DatatypeConverter这个类没找到,原因JAXB API是java EE 的API,JDK11删除了这个工具。

因此,如果你使用报错的话,需要自己手动的导入依赖

<!-- jaxb依赖包 -->
<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.3.0</version>
</dependency>
<dependency>
    <groupId>com.sun.xml.bind</groupId>
    <artifactId>jaxb-impl</artifactId>
    <version>2.3.0</version>
</dependency>
<dependency>
    <groupId>com.sun.xml.bind</groupId>
    <artifactId>jaxb-core</artifactId>
    <version>2.3.0</version>
</dependency>
<dependency>
    <groupId>javax.activation</groupId>
    <artifactId>activation</artifactId>
    <version>1.1.1</version>
</dependency>

3、封装JWTUtil

JwtUtil.java

public class JwtUtil {
    /**
     * 用户登录成功后生成Jwt
     * 使用Hs256算法  私匙使用用户密码
     *
     * @param ttlMillis jwt过期时间
     * @param user      登录成功的user对象
     * @return
     */
    public static String createJWT(long ttlMillis, User user) {
        //指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        //生成JWT的时间
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);

        //创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
        Map<String, Object> claims = new HashMap<String, Object>();
        claims.put("id", user.getUserPkid());
        claims.put("username", user.getUsername());
        claims.put("password", user.getPassword());

        //生成签名的时候使用的秘钥secret,这个方法本地封装了的,一般可以从本地配置文件中读取,切记这个秘钥不能外露哦。它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
        String key = user.getPassword();

        //生成签发人
        String subject = user.getUsername();

        //下面就是在为payload添加各种标准声明和私有声明了
        //这里其实就是new一个JwtBuilder,设置jwt的body
        JwtBuilder builder = Jwts.builder()
                //如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setClaims(claims)
                //设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
                .setId(UUID.randomUUID().toString())
                //iat: jwt的签发时间
                .setIssuedAt(now)
                //代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
                .setSubject(subject)
                //设置签名使用的签名算法和签名使用的秘钥
                .signWith(signatureAlgorithm, key);
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);
            //设置过期时间
            builder.setExpiration(exp);
        }
        return builder.compact();
    }


    /**
     * Token的解密
     * @param token 加密后的token
     * @param user  用户的对象
     * @return
     */
    public static Claims parseJWT(String token, User user) {
        //签名秘钥,和生成的签名的秘钥一模一样
        String key = user.getPassword();

        //得到DefaultJwtParser
        Claims claims = Jwts.parser()
                //设置签名的秘钥
                .setSigningKey(key)
                //设置需要解析的jwt
                .parseClaimsJws(token).getBody();
        return claims;
    }


    /**
     * 校验token
     * 在这里可以使用官方的校验,我这里校验的是token中携带的密码与数据库一致的话就校验通过
     * @param token
     * @param user
     * @return
     */
    public static Boolean isVerify(String token, User user) {
        //签名秘钥,和生成的签名的秘钥一模一样
        String key = user.getPassword();

        //得到DefaultJwtParser
        Claims claims = Jwts.parser()
                //设置签名的秘钥
                .setSigningKey(key)
                //设置需要解析的jwt
                .parseClaimsJws(token).getBody();

        if (claims.get("password").equals(user.getPassword())) {
            return true;
        }

        return false;
    }
}

4、持久层登录

@Repository("userMapper")
public interface UserMapper extends BaseMapper<User> {
    @Select("select * from t_user where username=#{username} and password=#{password}")
    User login(@Param("username") String username, @Param("password") String password);
}

5、业务层调用

@Service("userService")
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Resource
    private UserMapper userMapper;

    @Override
    public User login(String username, String password) {
        return userMapper.login(username,password);
    }
}

6、controller接口

@PostMapping("/login")
@ResponseBody
public ResultVO login(String username,String password,HttpServletResponse response){
    User user = userService.login(username,password);

    if(user != null && user.getUserPkid() > 0){

        String token = JwtUtil.createJWT(36000000, user);
        response.setHeader("Authorization",token);
        return ResultVO.success(user);
    }
    else{
        return ResultVO.fail(ResultCode.USER_LOGIN_ERROR);
    }
}

这里的ResultVO与ResultCode是反馈给前端数据的封装

ResultCode.java

public enum ResultCode {
    SUCCESS(0,"成功"),
    // 参数错误1001-1999
    BODY_NOT_MATCH(400,"请求的数据格式不符!"),
    PARAM_IS_INVALID(1001,"参数无效"),
    PARAM_IS_BLANK(1002,"参数为空"),
    PARAM_TYPE_BAND_ERROR(1003,"参数类型错误"),
    PARAM_NOT_COMPLETE(1004,"参数缺失"),
    PARAM_NUMBER_MISS(1005,"参数个数不匹配"),
    // 用户错误2001-2999
    USER_NOT_LOGGED_IN(2001,"用户未登录"),
    USER_LOGIN_ERROR(2002,"账户不存在或密码错误"),
    USER_ACCOUNT_FORBIDDEN(2003,"账户已被禁用"),
    USER_NOT_EXIST(2004,"用户不存在"),
    USER_HAS_EXISTED(2005,"用户已存在"),
    USER_PASS_ERROR(2006,"密码错误"),
    USER_AUTHENTICATION_ERROR(2007,"认证失败"),
    USER_AUTHORIZATION_ERROR(2008,"没有权限"),
    // 服务器错误3001-3999
    SERVER_OPTIMISTIC_LOCK_ERROR(3001,"操作冲突"),
    SERVER_INNER_ERROR(3002,"服务器内部错误"),
    SERVER_UNKNOW_ERROR(3003,"服务器未知错误"),
    SERVER_EMPTY_RESULT_DATA_ACCESS_ERROR(3004,"没有找到对应的数据");

    private Integer code;
    private String message;
    ResultCode(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
    public Integer code(){
        return this.code;
    }
    public String message(){
        return this.message;
    }
}

ResultVO.java

@Data
@ApiModel(value = "前台数据封装")
public class ResultVO implements Serializable {
    @ApiModelProperty("成功或者失败的代码号")
    private Integer code;
    @ApiModelProperty("成功或者失败的信息")
    private String message;
    @ApiModelProperty("返回前台的值")
    private Object data;
    @ApiModelProperty("返回集合的数量")
    private Long count;
    @ApiModelProperty("返回分页之后的页数")
    private Long pages;

    public ResultVO(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

    public ResultVO(ResultCode resultCode, Object data){
        this.code = resultCode.code();
        this.message = resultCode.message();
        this.data = data;
    }
    public ResultVO(ResultCode resultCode){
        this.code = resultCode.code();
        this.message = resultCode.message();
    }
    // 返回成功
    public static ResultVO success(){
        ResultVO resultVO = new ResultVO(ResultCode.SUCCESS);
        return resultVO;
    }
    // 返回成功
    public static ResultVO success(Object data){
        ResultVO resultVO = new ResultVO(ResultCode.SUCCESS,data);
        return resultVO;
    }
    // 返回失败
    public static ResultVO fail(ResultCode resultCode){
        ResultVO resultVO = new ResultVO(resultCode);
        return resultVO;
    }
    // 返回失败
    public static ResultVO fail(Integer code,String message){
        ResultVO resultVO = new ResultVO(code,message);
        return resultVO;
    }
    // 返回失败
    public static ResultVO fail(ResultCode resultCode, Object data){
        ResultVO resultVO = new ResultVO(resultCode,data);
        return resultVO;
    }
}

前端处理(普通写法)

当然前端首先需要登录页面,直接使用的 ElenentUI 组件,并且还带有页面验证。

1、先写登陆页面 Login.vue

<!--  -->
<template>
    <div>
        <Center>
            <h2>{{ isLoading }}</h2>
            <el-form :model="ruleForm" status-icon :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
                <el-form-item label="用户名" prop="username">
                    <el-input type="text" v-model="ruleForm.username" autocomplete="off" clearable></el-input>
                </el-form-item>
                <el-form-item label="密码" prop="password">
                    <el-input type="password" v-model="ruleForm.password" clearable></el-input>
                </el-form-item>
                <el-form-item>
                    <el-button type="primary" @click="submitForm('ruleForm')">提交</el-button>
                    <el-button @click="resetForm('ruleForm')">重置</el-button>
                </el-form-item>
            </el-form>
        </Center>
    </div>
</template>

<script>
import Center from '@/components/CenterComp.vue';
// import { login } from '@/apis/userApi'
import { mapState } from 'vuex'
export default {
    components: {
        Center,
    },
    computed:{
        ...mapState('userStore',['user','isLoading']),
    },
    data() {
        var validateName = (rule, value, callback) => {
            if (value === '') {
                callback(new Error('请输入账号'));
            } else {
                callback();
            }
        };
        var validatePass = (rule, value, callback) => {
            if (value === '') {
                callback(new Error('请输入密码'));
            }
            else {
                callback();
            }
        };
        return {
            ruleForm: {
                username: '',
                password: '',
            },
            rules: {
                username: [
                    { validator: validateName, trigger: 'blur' }
                ],
                password: [
                    { validator: validatePass, trigger: 'blur' }
                ],
            }
        };
    },
    methods: {
        submitForm(formName) {
            this.$refs[formName].validate(async (valid) => {
                if (valid) {
                    console.log(formName);
                    console.log(this.ruleForm);
                    alert('submit!');
                } else {
                    console.log('error submit!!');
                    return false;
                }
            });
        },
        resetForm(formName) {
            this.$refs[formName].resetFields();
        }
    }
}
</script>

2、创建 UserApi.js 请求数据

由于后台是的方法必须要post提交,但是axios的post提交默认发送数据时,数据格式是Request Payload,而并非我们常用的Form Data格式,后端未必能正常获取到,所以在发送之前,需要使用qs模块对其进行处理。

因此需要先导入qs模块

npm install qs --save

UserService.js

import axios from 'axios';

// 获取用户
export async function login(username,password){
    // 使用 axios 向后端发起请求
    let resp = await axios.post('/api/users/login',{
        username,
        password,
    });
    console.log(resp);
    // 如果请求成功了才往本地存储token,否则如果没请求成功,就是undefined,这样的话就会把undefined存入本地
    if(resp.data.status === 1){
        localStorage.setItem('token',resp.data.token);
    }
    return resp;
}

这里的 login 方法做了两件事情

1、将用户名密码发送到后台进行验证 2、将后台反馈回来的jwt数据保存在本地浏览器的localStorage

这样,我们可以在 login.vue 页面做一下提交测试了,当然,在提交的代码中应该加上 UserService 中的 login 方法,看看后台和反馈的效果,数据有没有被写到 localStorage 中

3、在 Login.vue 页面使用接口请求数据

Login.vue 页面

<script>
// 其他代码省略
    export default {
        // 其他代码省略
         methods: {
        	submitForm(formName) {
            	this.$refs[formName].validate(async (valid) => {
                	if (valid) {
                    	// console.log(formName);
                    	// console.log(this.ruleForm);
                    	// let resp = await 		login(this.ruleForm.username,this.ruleForm.password);
                    	// console.log(resp);
                    	console.log('LoginComp' ,r);``
                    	console.log('LoginComp',this.user);
                    	if(r.status === 1){
                        	this.$router.push('/');
                    	} else {
                        	alert('用户名或密码错误!!!')
                    	}
                	} else {
                    	console.log('error submit!!');
                    	return false;
                	}
            	});
        	},
        	resetForm(formName) {
            	this.$refs[formName].resetFields();
        	}
    	}
    }
</script>

此时,就可以成功获取到数据,并成功登陆了。

接下来演示使用 Vuex 来写登录:

前端处理(Vuex 写法)

把登录的数据请求加入到vuex中:

1、创建 userStore.js 仓库对象

import {login} from '@/apis/userApi.js';// login 是登录接口
export default {
    namespaced: true,
    state: {
        isLoading:true,
        user:null,
    },
    // 同步操作改变状态
    mutations:{
        setIsLoading(state,payload){
            state.isLoading = payload;
        },
        setUser(state,payload){
            state.user = payload;
        }
    },
    // 异步操作,改变状态
    actions:{
        async fetchUser(context,payload){
            context.commit('setIsLoading',true);
            let { username,password } = payload;
            let result = await login(username,password);
            console.log('userStore',result);
            if(result.data.status === 1 ){
                context.commit('setUser',result.data)
            }
            context.commit('setIsLoading',false);
            return result.data;
        }
    }
}

2、在 index.js 中引入引入仓库对象

import Vue from 'vue';
import Vuex from 'vuex';
// 引入用户模块
import userStore from './userStore';
// 引入新闻模块
import news from './newStore'

// 把Vuex 放到全局
Vue.use(Vuex);
// 创建仓库
let store = new Vuex.Store({
    modules:{
        userStore,
        news
    }
});
export default store;

3、再次更改 Login.vue 中登录的 js 代码,替换为调用vuex中的 action

// 其他代码省略
<script>
import Center from '@/components/CenterComp.vue';
// import { login } from '@/apis/userApi'
import { mapState } from 'vuex'
export default {
    // 其他代码省略
    methods: {
        submitForm(formName) {
            this.$refs[formName].validate(async (valid) => {
                if (valid) {
                    // console.log(formName);
                    // console.log(this.ruleForm);
                    // let resp = await login(this.ruleForm.username,this.ruleForm.password);
                    // console.log(resp);
                    let r = await this.$store.dispatch('userStore/fetchUser', this.ruleForm);
                    console.log('LoginComp' ,r);
                    console.log('LoginComp',this.user);
                    if(r.status === 1){
                        this.$router.push('/');
                    } else {
                        alert('用户名或密码错误!!!')
                    }
                } else {
                    console.log('error submit!!');
                    return false;
                }
            });
        },
        resetForm(formName) {
            this.$refs[formName].resetFields();
        }
    }
}
</script>

此时,用户已经可以成功登陆账号了。但登陆账号后,右上角的登陆和注册字样并没有发生变化,登陆成功后还是显示登陆和注册,而不是显示欢迎你xxx,所以,此时还需要更改代码。

4、欢迎你,xxx

此时,需要更改头部组件里的header

header.vue

<!--  -->
<template>
    <div class="header">
        <div class="header-container">
            <div class="container">
                <div class="logo">
                    <a href="">
                        <img :src="logUrl" alt="">
                    </a>
                </div>
                <ul class="nav">
                    <li><router-link :to="{name:'Home'}">首页</router-link></li>
                    <!-- 新闻 -->
                    <li v-for="channel in channels" :key="channel.channelId">
                        <router-link 
                            :to="{
                                name:'News',
                                params:{
                                    id:channel.channelId
                                },
                                query:{
                                    page:1
                                }
                            }">{{ channel.name }}</router-link>
                    </li>
                    <li><router-link :to="{name:'Input'}">输入框</router-link></li>
                    <!-- 电影 -->
                    <!-- <li><router-link :to="{name:'Films'}">电影</router-link></li> -->
                </ul>
更改行+          <div v-if="user != null">
更改行+               <span>欢迎你:<router-link to="/">{{ user.name }}</router-link></span>
更改行+               <span><a href="">注销</a></span>
                </div>
                <div v-else class="user">
                    <router-link :to="{name:'Login'}">登录</router-link>
                    <router-link :to="{name:'Reg'}">注册</router-link>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
import logo from '@/assets/logo.png';
import {mapState} from 'vuex';// 引入mapState
import getChannelData from '@/mixins/getChannelData';
export default {
    mixins:[getChannelData],
    data() {
        return {
            // 静态图标希望传递给页面参数进行显示,可以直接将图片作为一个组件引入。
            // 当然,还是可以按照原来的方式,通过`require('地址')`引入
            // logUrl:require('@/assets/logo.png'),
            logUrl: logo,
        }
    },
更改行+ computed:{
更改行+     ...mapState('userStore',['user','isLoading'])
更改行+ }

}
</script>
<style scoped lang="scss">
    @import './headerStyle.scss';
</style>

验证登录

当然 jwt 最重要的功能并不是登录发送一个 jwt 数据就算了,保留这个数据,主要就是为了保持状态,因此,我们完全可以在下次登录的时候,通过 jwt,验证该浏览器的用户是否已经登录。

这个需求就是,页面一打开,就需要发送 axios 请求,验证用户。

当然,首先还是后台代码,在 Controller 中加入 verify 验证登录的代码:

1、后台verify验证

@RequestMapping("/verify")
@ResponseBody
public ResultVO verify(HttpServletRequest request){
    String authHeader = request.getHeader("authorization");
    System.out.println("=====>" + authHeader);
    //截取前面的Bearer
    String token = authHeader.substring(7);
    // 由于登录的时候在jwt中保存了用户的主键id
    //因此这里再次从jwt中取出id数据,验证该用户
    int userId = 0;
    try {
        userId = JWT.decode(token).getClaim("id").asInt();
    } catch (JWTDecodeException j) {
        System.out.println(j.getStackTrace());
        //throw new RuntimeException("访问异常!");
    }
    User user = userService.getById(userId);
    if(user != null){
        Boolean verify = JwtUtil.isVerify(token, user);
        if(verify){
            return ResultVO.success(user);
        }
        else{
            return ResultVO.fail(ResultCode.USER_AUTHENTICATION_ERROR);
        }
    }
    else{
        return ResultVO.fail(ResultCode.SERVER_EMPTY_RESULT_DATA_ACCESS_ERROR);
    }
}

2、UserApi 中编写远程验证方法

// 验证用户是否登录
export async function whoami(){
    // 使用 axios 向后端发起请求
    // 因为我们是验证登陆,所以,我们需要把token当成参数传过去。但是此时,是我们自己手动加的,如果其他地方也需要带上token,那我们不可能每一次都手动来添加,所以,我们就创建一个 utils 文件夹,创建一个axios-intercepter.js文件,使用 axios拦截器,,只要我们访问的是我们自己的后端,那他就会自动帮我们带上token
    let resp = await axios.get('/api/getToken');
    console.log(resp);
    return resp;
}

因为我们是验证登陆,所以,我们需要把token当成参数传过去。但是此时,是我们自己手动加的,如果其他地方也需要带上token,那我们不可能每一次都手动来添加,所以,我们就创建一个 utils 文件夹,创建一个axios-intercepter.js文件,使用 axios拦截器,,只要我们访问的是我们自己的后端,那他就会自动帮我们带上token

3、创建 token 拦截器

utils文件夹 --> axios-intercepter.js文件

import axios from "axios";
// Add a request interceptor
axios.interceptors.request.use(function (config) {
    // Do something before request is sent
    let token = localStorage.getItem("token");
    console.log('axios拦截器',token)
    if (token && config.url.indexOf('/api') != -1) {
        // 大部分的后端解析,是按照Auth2的规则来解析的
        // 要求传送过去的数据需要有一个Bearer的前缀
        config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
}, function (error) {
    // Do something with request error
    return Promise.reject(error);
});

// Add a response interceptor
axios.interceptors.response.use(function (response) {
    // Any status code that lie within the range of 2xx cause this function to trigger
    // Do something with response data
    return response;
}, function (error) {
    // Any status codes that falls outside the range of 2xx cause this function to trigger
    // Do something with response error
    return Promise.reject(error);
});

4、在 main.js 文件中引入拦截器

// 引入 axios 拦截器
import '@/utils/axios-Interceptors';

5、vuex仓库的actions中调用异步方法给state赋值

// 引入 验证登陆的api
import {login,whoami} from '@/apis/userApi.js'
export default {
    namespaced: true, // 开启命名空间
    // 其他代码省略
    // 异步操作,改变状态
    actions: {
        // 其他代码省略
        async userVerify(context) {
            context.commit('setIsLoading', true);
            let result = await whoami();
            console.log("vuex----whoami---", result);
            if (result.data.status === 1) {
                context.commit('setUser', result.data);
            }
            context.commit('setIsLoading', false);
        },
    }
}

由于一打开页面暂时还没有任何axios请求,找不到地方调用上面的 userVerify 方法,所以人为的造一个 axios 请求:

6、人为创造打开页面发起 axios 请求

在 main.js 文件里面

// 验证登陆,由于一打开页面暂时还没有任何axios请求,所以人为的造一个
store.dispatch("userStore/userVerify");

7、在 header.vue 里面加上 isLoading

<!--  -->
<template>
	// 其他代码省略
                <!-- 情况1.正在加载中 -->
添加行+          <span v-if="isLoading">正在加载中...</span>
                <!-- 情况2.登录成功 -->
                <div v-if="user != null">
                    <span>欢迎你:<router-link to="/">{{ user.name }}</router-link></span>
                    <span><a href="">注销</a></span>
                </div>
                <!-- 情况3.没有登录 -->
                <div v-else class="user">
                    <router-link :to="{name:'Login'}">登录</router-link>
                    <router-link :to="{name:'Reg'}">注册</router-link>
                </div>
            </div>
        </div>
    </div>
</template>

到此时。登录效果就已经实现了。但是还想要两个效果,点击退出能退出登录,点击个人中心,直接进入个人中心页面。

加入个人中心与退出

退出页面(Vuex 版本)

退出方法可能也需要在很多地方调用,放入到vuex中统一管理,并且需要情况state.user的数据

注销(退出)页面需要两个步骤:1、删除浏览器的 token,2、删除仓库中的 user 信息。

两个步骤做完,浏览器里面没有token 信息,说明用户未登录,此时就会跳到登录页面提示用户登录。

1、在 header.vue 页面,给注销按钮,添加点击事件

// 其他代码省略
<!-- 情况2.登录成功 -->
<div v-if="user != null">
      <span>欢迎你:<router-link to="/">{{ user.name }}</router-link></span>
      <span><a href="" @click.prevent="handleLoginOut">注销</a></span>
</div>

2、在 userApi.js 里面写注销方法 loginOut()

// 注销
export function loginOut() { 
    localStorage.removeItem('token');
}

3、在 userStore.js 文件里 actions 里面写注销操作,并引入 api里面的loginOut()

import {
    login,
    whoami,
    loginOut
} from '@/apis/userApi.js'
export default {
    // 异步操作,改变状态
    actions: {
        // 其他代码省略
        userLoginOut(context) {
            loginOut();
            context.commit('setUser', null);
        }
    }
}

4、此时,在 header.vue 里面改变界面就可以使用仓库里面的 userLoginOut 操作

<!--  -->
<template>
    <div class="header">
        // 其他代码省略
                <!-- 情况2.登录成功 -->
                <div v-if="user != null">
                    <span>欢迎你:<router-link to="/">{{ user.name }}</router-link></span>
                    <span><a href="#" @click.prevent="handleLoginOut">注销</a></span>
                </div>
    </div>
</template>

<script>
export default {
    // 其他代码省略
    methods:{
        handleLoginOut() {
            this.$store.dispatch('userStore/userLoginOut');
        }
    }

}
</script>

此时,就可以成功注销并跳转到登录页面了。

个人中心

1、新建一个个人中心 Personal.vue 文件

<template>
    <div>
        <h1>用户信息</h1>
        <template v-if="user">
            <h2>{{ user.id }}</h2>
            <h2>{{ user.username }}</h2>
            <h2>{{ user.name }}</h2>
        </template>
        <template v-else>
            <h2>用户不存在</h2>
        </template>
    </div>
</template>

<script>
import { mapState } from 'vuex';
export default {
    computed:{
        ...mapState('userStore',['user','isLoading'])
    },
    data() {
        return {
        }
    }
}
</script>

2、在路由 config.js 文件里面配置个人中心文件

// 其他代码省略
{
    path:'/personal',
    name:'Personal',
    component:()=>import('@/viewPage/PersonalComp.vue')
},

3、在 header.vue 文件里面用户那加上 to

// 其他代码省略
<template>
	<!-- 情况2.登录成功 -->
    <div v-if="user != null">
         <span>欢迎你:<router-link to="/personal">{{ user.name }}</router-link></span>
         <span><a href="#" @click.prevent="handleLoginOut">注销</a></span>
    </div>
</template>

此时,点击用户已经可以成功跳转到用户信息页面了。

在没有登录的情况下是不允许访问个人信息页面的,所以 PersonalComp.vue 这里可以用 watch 来监听用户是否登录,没有登录就跳转到 Login.vue

// 其他代码省略
<script>
	// 监听user是否登录成功,若没有,就不能进入个人信息页面,直接去到登录页面
    watch: {
        "user": {
            immediate: true,
            handler(newVal) {
                console.log(newVal);
                if (!newVal) {
                    this.$router.push("/login");
                }
            }
        }
    }
</script>

在此时,没有可以成功监听用户是否登录了。没有登录就 不能进入个人信息的页面。但我们还可以使用导航守卫的方式来操作。

十四、导航守卫

但是,Personal页面是属于每个用户自己的页面,在没有登录的时候是不允许访问的,里面有一些重要的,私密的数据。也就是说你没有登录,那就根本不允许你访问这个页面

但是现在,如果直接在url中输入 http://127.0.0.1:8080/personal,是直接能够访问到这个页面的

所以,目的就是,没有登录,就不应该等通过url地址访问到这个页面,具体要怎么做,如下图:

 一般情况下,我们需要在中间加入一个鉴权页面,这个页面对我们到底有没有权限访问到要访问的页面进行判断跳转,当然这个中间页面其实还有一个更加重要的目的:在进行导航的时候,是根据url地址来进行判断的,当页面刷新,第一次通过url进入的时候,如果你用到了仓库远程访问,这个时候信息回馈的还没有那么及时,不能判断仓库中是否已经存在值,所以这个中间鉴权页面还有一个功能就是等待异步数据的加载,然后再进行导航.

概念

导航守卫又称路由守卫,实时监控路由跳转时的过程,在路由跳转的各个过程执行相应的操作,类似于生命周期函数,在开发过程中经常被使用,比如用户点击一个页面,如果未登录就调到登录页面,已登录就让用户正常进入。

 路由守卫分类

全局路由一共分为三类:全局守卫,路由独享的守卫,组件内的守卫。

1、全局守卫(使用较多)

全局守卫:就是进入所有的路由之前,都会进的一个守卫。

全局守卫一般写在路由的最外层配置里面,就是 routes 文件夹下面的 index.js 文件里面。

全局守卫里分三种守卫:

  • router.beforeEach(全局前置守卫)

    当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中。

  • router.beforeResolve(全局解析守卫)

  • router.afterEach(全局后置守卫)

1.router.beforeEach(全局前置守卫)
用户在未登录的时候进入Personal页面,我们就让用户跳转到登录页面,在已登录的时候让用户正常跳转到点击的Personal页面。
2.router.beforeResolve(全局解析守卫)
和全局前置守卫类似,区别是在跳转被确认之前,同时在所有组件内守卫和异步路由组件都被解析之后,解析守卫才调用。
3.router.afterEach(全局后置守卫)
只接受to和from,不会接受 next 函数也不会改变导航本身

2、路由独享守卫

独享守卫只有一种:beforeEnter。 该守卫接收的参数与全局守卫是一样的,但是该守卫只在其他路由跳转至配置有 beforeEnter 路由表信息时才生效。 router 配置文件内容:

{
path: '/about',
name: 'about',
component: about,
beforeEnter:(to,from,next)=>{
  console.log(to);
  console.log(from);
  next()
}

3、组件内守卫

组件内守卫一共有三个:

  • beforeRouteEnter,

  • beforeRouteUpdate,

  • beforeRouteLeave

三者分别对应:进入该路由时执行,该路由中参数改变时执行,离开该路由时执行。

<template>
  <div>关于页面</div>
</template>
<script>
  export default {
    name: "about",
    beforeRouteEnter(to, from, next) {
      //进入该路由时执行
    },
    beforeRouteUpdate(to, from, next) {
      //该路由参数更新时执行
    },
    beforeRouteLeave(to, from, next) {
      //离开该路由时执行
    }
  }
</script>
<style scoped>
</style>

每个守卫方法接收三个参数:

  • to: Route: 即将要进入的目标 路由对象

  • from: Route: 当前导航正要离开的路由

  • next: Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。

    • next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。

    • next('/') 或者 next({ path: '/' }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。你可以向 next 传递任意位置对象,且允许设置诸如 replace: true、name: 'home' 之类的选项以及任何用在 router-link 的 to prop 或 router.push 中的选项。

当然,不论是这里的参数也好,还是守卫方法也好,还有很多,这里做一下简单介绍。

路由独享守卫和组件内守卫的区别:

组件内守卫:组件可以另外给他其他路由,可以重新配置路由,此时,针对的还是这一个组件,可以同时针对某一个或者某几个路由。

完整导航流程:

这些路由甚至有进入的先后顺序,大致如下:

  1. 导航被触发。

  2. 在失活的组件里调用离开守卫。

  3. 调用全局的 beforeEach 守卫。

  4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。

  5. 在路由配置里调用 beforeEnter。

  6. 解析异步路由组件。

  7. 在被激活的组件里调用 beforeRouteEnter。

  8. 调用全局的 beforeResolve 守卫 (2.5+)。

  9. 导航被确认。

  10. 调用全局的 afterEach 钩子。

  11. 触发 DOM 更新。

  12. 用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。

Vue路由跳转报错问题解决

 这个报错的原因:使用新导航取消了从“/auth”到“/personal”的导航。

主要是由于导航守卫的连续跳转引起的问题,只需要在导航配置文件的index.js中加入下面的代码就好了:

const originalPush = VueRouter.prototype.push;

VueRouter.prototype.push = function push(location) {
  return originalPush.call(this, location).catch((err) => err);
};

 使用步骤:

1、在导航配置(config.js)文件中加入必须的标志

比如,这里的/personal导航是需要我们验证的,所以在/personal导航中加入判断标志,目的是为了其他不需要验证的导航就可以直接通过。

{
     path: '/personal',
     name: 'Personal',
     component: () => import('@/viewPage/PersonalComp.vue'),
     meta: {
         auth: true
     }
},

这里其实就是给导航的meta标志中加入了一个auth的标识值,在导航守卫的时候,直接判断这个值,如果有就需要导航守卫。

2、在View文件夹中加入鉴权页面(Auth.vue),并配置导航

加入一个Auth.vue页面,这个页面主要是为了等待异步数据,然后实现导航效果

<template>
    <Center>
        <h1>正在登录中......</h1>
    </Center>
</template>

<script>
import {mapState} from 'vuex';
import Center from '@/components/Center.vue'

export default {
  components: { Center },
  computed: {...mapState("userStore",["user","isLoading"])},
  methods: {
    handleLogin(){
      if(this.isLoading){
        return;
      }
      if(this.user){
        this.$router.push({name:"Personal"})
      }else{
        this.$router.push({name:"Login"})
      }
    }
  },
  watch: {
    user:{
      immediate:true,
      handler(){
        this.handleLogin();
      }
    },
    isLoading:{
      immediate:true,
      handler(){
        this.handleLogin();
      }
    },
  },
}
</script>
<style></style>

配置导航数据:

{
    path: "/auth",
    name: 'Auth',
    component: () => import("@/viewPage/AuthComp.vue")
},

3、配置导航

在导航的 index.js 中加入下面的配置

import store from "@/stores/";
router.beforeEach(function(to, from, next) {
  console.log("to", to);
  console.log("from", from);
  console.log("====>" + store.state.userStore.isLoading);
  console.log("====>" + store.state.userStore.user);
  if (to.meta.auth) {
    if (store.state.userStore.isLoading) {
      next({ name: "Auth" });
    } else if (store.state.userStore.user) {
      next(); //允许进入
    } else {
      next({ name: "Login" });
    }
  } else {
    next();
  }
});

这里配置了一个全局前置守卫

当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中。

十五、自定义指令

除了核心功能默认内置的指令 (v-modelv-show等等),Vue 也允许注册自定义指令。注意,在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。

比如有时候,我们想页面加载的时候,就对输入框进行自动聚焦。现在就可以通过指令实现相关的功能:

// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
  // 当被绑定的元素插入到 DOM 中时……
  inserted: function (el) {
    // 聚焦元素
    el.focus()
  }
})

如果想注册局部指令,组件中也接受一个 directives 的选项:

directives: {
  focus: {
    // 指令的定义
    inserted: function (el) {
      el.focus()
    }
  }
}

然后你可以在模板中任何元素上使用新的 v-focus property,如下:

<input v-focus>

钩子函数

一个指令定义对象可以提供如下几个钩子函数 (均为可选):

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。

  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。

  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。

  • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。

  • unbind:只调用一次,指令与元素解绑时调用。

钩子函数参数

指令钩子函数会被传入以下参数:

  • el:指令所绑定的元素,可以用来直接操作 DOM。

  • binding:一个对象,包含以下 property:

    • name:指令名,不包括 v- 前缀。

    • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2。

    • oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。

    • expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"。

    • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"。

    • modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }。

  • vnode:Vue 编译生成的虚拟节点。移步 VNode API 来了解更多详情。

  • oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。

这是一个使用了这些 property 的自定义钩子样例:

//界面
<div id="hook-arguments-example" v-demo:foo.a.b="message"></div>

//js
Vue.directive('demo', {
  bind: function (el, binding, vnode) {
    var s = JSON.stringify
    el.innerHTML =
      'name: '       + s(binding.name) + '<br>' +
      'value: '      + s(binding.value) + '<br>' +
      'expression: ' + s(binding.expression) + '<br>' +
      'argument: '   + s(binding.arg) + '<br>' +
      'modifiers: '  + s(binding.modifiers) + '<br>' +
      'vnode keys: ' + Object.keys(vnode).join(', ')
  }
})

new Vue({
  el: '#hook-arguments-example',
  data: {
    message: 'hello!'
  }
})

//结果
name: "demo"
value: "hello!"
expression: "message"
argument: "foo"
modifiers: {"a":true,"b":true}
vnode keys: tag, data, children, text, elm, ns, context, fnContext, fnOptions, fnScopeId, key, componentOptions, componentInstance, parent, raw, isStatic, isRootInsert, isComment, isCloned, isOnce, asyncFactory, asyncMeta, isAsyncPlaceholder

上面的内容,基本都是vue官方的教程。其实对于我们自己来理解 vue 的指令,可以简单理解为就是一个代码的复用,而且这个复用是和每一个使用指令的html元素绑定在一起的。而且经常在这个复用里面,我们经常会写上一些DOM的拓展。

创建指令 @/directives/TooltipDirective.js

export const TooltipDirective = {
  inserted(el, binding) {
    //创建tooltip
    const tooltip = document.createElement("div");
    tooltip.style.display = 'none'
    tooltip.style.position = 'absolute'
    tooltip.style.zIndex = 9999
    tooltip.style.backgroundColor = '#333'
    tooltip.style.color = '#fff'
    tooltip.style.padding = '8px'
    tooltip.style.borderRadius = '4px'
    tooltip.style.fontSize = '14px'
    tooltip.textContent = binding.value;
    //将tooltip添加到绑定指令的标签中
    el.appendChild(tooltip);
    //鼠标移入,移出和移动事件
    el.addEventListener("mouseenter", showTooltip);
    el.addEventListener("mouseleave", hideTooltip);
    el.addEventListener("mousemove", move);
    //鼠标移动时,tooltip的位置
    function move(e) { 
      let left = `${e.clientX-el.offsetLeft}px`;
      let top = `${e.clientY-el.offsetTop}px`;
      tooltip.style.left = left
      tooltip.style.top = top;
    }
    //显示tooltip
    function showTooltip() {
      tooltip.style.display = "block";
    }
    //隐藏tooltip
    function hideTooltip() {
      tooltip.style.display = "none";
    }
  },
};

引入指令

<div class="box" :style="cssVar" v-tooltip="'这是一个测试'">
 ......
<div>

//js
import {TooltipDirective} from '@/directives/TooltipDirective'
export default {
  data () {
    return {
    }
  },
  computed: {
    cssVar() { 
      return {
        '--width':this.$attrs.width,
        '--height':this.$attrs.height
      }
    }
  },
  directives: {
    tooltip: TooltipDirective
  },
}

//css
.box{
  position:relative;
  border:1px solid #ccc;
  width:var(--width);
  ......
}

这里顺便引入了一个**小的知识点**:怎么在vue2的css中引入变量值 主要就是3点:

1、利用计算属性 2、利用css变量,上层声明,下层使用 3、css中引入变量

一些小的重要的知识点:

1、不要直接循环通过 v-model 修改数组别名的值

<div v-for="(book,i) in books" :key="i">
    <input type="text" v-model="book">
    上面的做法是错误的,book只是books的别名,不能直接通过v-model修改
   
    我们可以直接通过数组下标绑定数组具体的元素
    <input type="text" v-model="books[i]">
    
 </div>

2、子组件通过v-model直接修改props中的值

父组件:

<template>
  <div>
    <h2>{{ username }}</h2>
    <hr>
      <!-- .sync:子组件修改时,父组件也同步修改 -->
    <ShowTips :username.sync="username" />
  </div>
</template>

<script>
import ShowTips from './ShowTips.vue';
export default {
  data () {
    return {
      username:"jack"
    }
  },
  components: {
    ShowTips
  },

}
</script>

子组件:

<template>
     <div>
   
       <!-- <input type="text" :value="username" @input="handleChange"/> -->
   
       <input type="text" v-model="localUsername">
       
       <hr>
       {{ books }}
       <div v-for="(book,i) in books" :key="i">
         <input type="text" v-model="books[i]">
       </div>
   
     
     </div>
   </template>
   
   <script>
   export default {
     props:['username'],
     data () {
       return {
         books: ["vue", "react", "angular"],
         // localUsername:this.username
       }
     },
     computed: {
       localUsername: {
         get() { 
           return this.username
         },
         set(newVal) { 
             // this.$emit('update:要修改的属性',newVal)
           this.$emit("update:username", newVal);
         }
       }
     },
     methods: {
       handleChange(e) { 
         this.$emit('changeName',e.target.value)
       }
     },
     // watch: {
     //   localUsername: {
     //     handler(newVal) { 
     //       this.$emit("update:username", newVal);
     //     }
     //   }
     // }
   }
</script>

防抖和节流

防抖和节流,应该算是前端经常使用的优化内容。

JavaScript 中的函数大多数情况下都是由用户主动调用触发的,除非是函数本身的实现不合理,否则一般不会遇到跟性能相关的问题。

但是在一些少数情况下,函数的触发不是由用户直接控制的。在这些场景下,函数有可能被非常频繁地调用,而造成大的性能问题。解决性能问题的处理办法就有函数防抖函数节流

如果文本框的文字每次被改变(键盘按下事件),我都要把数据发送到服务器,得到搜索结果,这是非常恐怖的!想想看,我搜索“google”这样的单词,至少需要按 6 次按键,就这一个词,我需要向服务器请求 6 次,并让服务器去搜索 6 次,但我只需要最后一次的结果就可以了。如果考虑用户按错的情况,发送请求的次数更加恐怖。这样就造成了大量的带宽被占用,浪费了很多资源。

对于这些情况的解决方案就是函数防抖(debounce)或函数节流(throttle),其核心就是限制某一个方法的频繁触发。

简单来说,这里要在输入框中使用防抖的话,其实就是不要每次触发输入框的input或者change事件的时候马上就做出操作,稍等片刻之后再做就行。

函数防抖(debounce)

函数防抖,就是指触发事件后在规定时间内函数只能执行一次,如果在 规定时间内又触发了事件,则会重新计算函数执行时间。

简单的说,当一个动作连续触发,则只执行最后一次。 如,坐公交,司机需要等最后一个人进入才能关门。每次进入一个人,司机就会多等待几秒再关门。

函数节流(throttle)

限制一个函数在规定时间内只能执行一次。 如,乘坐地铁,过闸机时,每个人进入后3秒后门关闭,等待下一个人进入。

 竖线的疏密代表事件执行的频繁程度。可以看到,正常情况下,竖线非常密集,函数执行的很频繁。而debounce(函数防抖)则很稀疏,只有当鼠标停止移动时才会执行一次。throttle(函数节流)分布的较为均已,每过一段时间就会执行一次。

常见应用场景 函数节流的应用场景 间隔一段时间执行一次回调的场景有:

  • 滚动加载,加载更多或滚到底部监听

  • 谷歌搜索框,搜索联想功能

  • 高频点击提交,表单重复提交 函数防抖的应用场景 连续的事件,只需触发一次回调的场景有:

  • 搜索框搜索输入。只需用户最后一次输入完,再发送请求

  • 手机号、邮箱验证输入检测

  • 窗口大小Resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。

实验步骤:

在没有使用函数防抖之前:

<template>
    <div>
        <input type="text" @input="handleInput" />
    </div>
</template>

<script>
export default {
    methods:{
        handleInput(e){
            console.log(e.target.value);
        }
    }
}
</script>

每次输入,就会出现下面的效果:

 如果直接这样写的话,每次输入触发input事件,那么都会进行打印。其实我们只需要在进行操作的打印语句上,加上一点延迟即可:

<template>
    <div>
        <input type="text" @input="handleInput" />
    </div>
</template>

<script>
export default {
    data() {
        return {
        }
    },
    methods:{
        handleInput(e){
            setTimeout(() => {
                console.log(e.target.value);
            }, 1000);
        }
    }
}
</script>

这么做确实做到了延迟,但是仔细观察你会发现问题,还是触发了多次的@input事件,并不是我们需要的。因为整个@input事件函数应该和防抖函数绑定,这样才能做到多次输入,只会触发一次。

输入框防抖指令:

1、封装防抖工具函数,utils 文件夹 -> heloer.js

/**
 * 函数防抖
 * @param {function} func 一段时间后,要调用的函数
 * @param {number} wait 等待的时间,单位毫秒
 */

export function debounce(fn,delay){
    // 设置变量,记录 setTimeout 得到的 id
    let timer = null;
    return function (...args) {
        if(timer != null) {
            // 如果有值,说明目前正在等待中,清除它
            clearTimeout(timer);
        }
        // 重新开始计时
        setTimeout(() => {
            fn.apply(this,args);
        },delay)
    }
}

2、页面引入:

<template>
    <div>
        <!-- 函数防抖 -->
        <input type="text" @input="handleInput" />
    </div>
</template>

<script>
// 引入防抖函数
import { debounce } from "@/utils/helper.js";
export default {
    data() {
        return {
        }
    },
    methods:{
        debounceInput:debounce(function (e) {
            console.log(e.target.value);
        },1000),
        handleInput(e){
            this.debounceInput(e)
        }
    }
}
</script>

此时,输入框防抖已经完成,这样可以做到,多次输入仅仅触发一次。

但是假如我们页面有许多地方都需要使用防抖,那我们就需要写很多方法来使用防抖函数,所以,为了代码复用,我们就可以封装一个防抖指令

3、src目录下,directives 文件夹=>创建 debounce.js 文件

// 封装指令

// 引入 防抖函数
import {debounce } from '@/utils/helper.js'
export default {
    inserted(el,binding) {
        el.oninput = debounce((e) => {
            // 由于不知道具体执行的操作的是什么,因此,`binding.value`实际传递的应该是一个函数.
            binding.value(e.target.value)
        },500)
    }
}

4、在页面引入防抖指令并使用

<template>
    <div>
        <!-- 函数防抖 -->
        <h2>函数防抖</h2>
        <input type="text" @input="handleInput" />
        <br>
        <h2>防抖指令</h2>
        <input type="text" v-debounce="handleDirective">
    </div>
</template>

<script>
// 引入防抖函数
import { debounce } from "@/utils/helper.js";
// 引入防抖指令
import debounceDirective from '@/directives/debounce.js'
export default {
    data() {
        return {
        }
    },
    // 给防抖指令一个别名
    directives:{
        debounce:debounceDirective
    },
    methods:{
        debounceInput:debounce(function (e) {
            console.log(e.target.value);
        },1000),
        handleInput(e){
            this.debounceInput(e)
        },
        // 防抖函数指令
        handleDirective(v){
            console.log(v);
        }
    }
}
</script>

封装函数节流的函数

const throttle = function (fn, delay) {
    let canRun = true,
        firstTime = true;
    return function (...args) {
        if (!canRun) return; // 注意,这里不能用timer来做标记,因为setTimeout会返回一个定时器id
        canRun = false;
        // 第一次直接执行
        if (firstTime) {
            fn.apply(this, args)
        }
        setTimeout(() => {
            if (firstTime) {
                firstTime = false;
            } else {
            		fn.apply(this, args)
            }
            canRun = true;
        }, delay)
    }
}

和具体的情况结合一下使用,比如要封装一个搜索框,该搜索框查询api中的数据,并且返回响应的数据,显示在搜索框下面。

这里有一个新的 css 小知识:就是 js 里面的样式 css 也可以用,和 css变量的用法

css知识1: css变量的用法

<template>
	<!-- 知识1:在上面声明的style的--width,在style里面也可以使用到这个width -->
	<div class='box' style="--width:400px;">
        <div class='son'>
            
    	</div>
    </div>
</template>
<style scoped lang="scss">
    .box{
        width:var(--width);
    }
    .son{
        width:var(--width);
    }
</style>

css知识2: js 里面的样式 css 也可以用

父组件 App.vue

// 引入和注册代码省略
<!-- 输入框组件 -->
<MyInput width="300px" height="100px" v-model="msg" placeholder="请输入..." />

子组件 MyInput.vue

<template>
	<!-- 知识2:这里的style可以接受从父组件传过来的一个对象 -->
	<div class='box' style="cssVar">
        <div class='son'>
            
    	</div>
    </div>
</template>
<script>
    export default {
        computed:{
        	cssVar() {
            	return {
                	// 这里的宽高是从父组件 App.vue 传过来的
                	'--width':this.$attrs.width,
                	'--height':this.$attrs.height
            	}
        	}
    	}
    }
</script>
<style scoped lang="scss">
    .box{
        width:var(--width);
    }
    .son{
        width:var(--width);
    }
</style>

案例:搜索内容,下面显示和内容有关的数据(模糊查询)

<!-- 1、写上页面和样式 MyInput.vue -->
<template>
    <div class="box" :style="cssVar">
        <el-input 
                  v-bind="$attrs" 
                  v-on="$listeners" 
                  class="content" 
                  // 13、
                  @focus="showSuggest"
      			  @blur="hideSuggest"
                  // 7、绑定方法
                  v-debounce="handleInput" 
        >
            <template v-for="(slot, key) in $slots" :slot="key">
                <slot :name="key"></slot>
            </template>
        </el-input>

        <!-- 搜索内容显示 -->
        <div class="suggest-container" v-show="isShow">
            <div class="suggest-body">
                <div class="suggest-detail-list">
                    <a href="#" class="suggest-detail">
                        // 11、遍历电影数组,显示数据
                        <span class="detail-name" v-for='(film) in films' :key='film._id'>{{film.name}}</span>
                    </a>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
// 2、引入获取电影的 api
import {getFilms} from '@/apis/FilmsApi';
// 4、引入防抖指令
import debounceDirective from '@/directives/debounce'
export default {
    data() {
        return {
            isShow: true,
            // 8、写电影数组
            films:[],
        }
    },
    methods: {
        // 6、写一个方法
        async handleInput(v) {
            // 12、如果输入框里面没有值,就不显示数据,直接返回不运行后续代码
            if(!v) {
                // this.isShow = false;
                return;
            }
            // 9、写方法内容接收电影数据
            let result = await getFilms();
            // 15、如果查询出来的结果没有,就直接返回
            // if(result.status !==1 ) return;
            result = result.data;
            // 10、筛选过滤和输入框相等的数据并赋值给电影数组
            this.films = result.filter(film => film.name.includes(v));
        },
        // 14、
        showSuggest() { 
            // 如果输入框有值,才显示下面的信息框,没有就不显示
      		if (this.$attrs.value) { 
        		this.isShow = true;
      		}
      
    	},
    	hideSuggest() { 
      		this.isShow = false;
    	}
    },
    // 5、给防抖指令一个别名
    directives:{
        debounce:debounceDirective
    },
    computed: {
        cssVar() {
            return {
                // 这里的宽高是从父组件 App.vue 传过来的
                '--width': this.$attrs.width,
                '--height': this.$attrs.height
            }
        }
    },
    // 3、验证是否有数据
    // created() {
    //     getFilms().then(res =>{
    //         console.log(res);
    //     })
    // }
}
</script>

<style scoped lang="scss">
:root {
    --width: 400px;
}

@mixin shadow {
    content: "";
    position: absolute;
    bottom: 15px;
    box-shadow: 0 15px 3px rgba(0, 0, 0, .3);
    widows: 40%;
    max-width: 300px;
    height: 20px;
}

.box {
    position: relative;
    border: 1px solid #ccc;
    width: var(--width);

    &::before {
        @include shadow;
        left: 3px;
        transform: rotate(-3deg);
    }

    &::after {
        @include shadow;
        right: 3px;
        transform: rotate(3deg);
    }

    .content {
        position: relative;
        z-index: 9999;
    }
}

.suggest-container {
    width: 240px;
    margin-left: 10px;
    border: 1px solid #ccc;
    margin-top: 1px;
    border-bottom: none;
    font-size: 14px;
    position: absolute;
    background-color: #fff;
    overflow: hidden;
    z-index: 999;

    .suggest-body {
        border-bottom: 1px solid #ccc;

        .suggest-detail-list {
            width: 100%;
            float: left;

            .suggest-detail {
                border-bottom: 1px solid #e5e5e5;
                width: 100%;
                color: #333;
                display: block;
                text-decoration: none;

                .detail-name {
                    overflow: hidden;
                    text-overflow: ellipsis;
                    white-space: nowrap;
                    display: block;
                    height: 68px;
                    line-height: 68px;
                }
            }
        }
    }
}
</style>

十六、Web Worker

为什么需要 Web Worker

由于JavaScript语言采用的是单线程,同一时刻只能做一件事,如果有多个同步计算任务执行,则在这段同步计算逻辑执行完之前,它下方的代码不会执行,从而造成了阻塞,用户的交互也可能无响应。

但如果把这段同步计算逻辑放到Web Worker执行,在这段逻辑计算运行期间依然可以执行它下方的代码,用户的操作也可以响应了。

Web Worker 是什么

HTML5 提供并规范了 Web Worker 这样一套 API,它允许一段 JavaScript 程序运行在主线程之外的另外一个线程(Worker 线程)中。

Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程就会很流畅,不会被阻塞或拖慢。

Web Worker 的分类

Web Worker 根据工作环境的不同,可分为专用线程 Dedicated Worker 和共享线程 Shared Worker

Dedicated Worker的Worker只能从创建该Woker的脚本中访问,而SharedWorker则可以被多个脚本所访问。

在开发中如果使用到 Web Worker,目前大部分主要还是使用 Dedicated Worker 的场景多,它只能为一个页面所使用。这里主要还是讨论Dedicated Worker的场景

Web Worker的使用限制

同源限制 分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。

文件限制 Worker 线程无法读取本地文件(file://),会拒绝使用 file 协议来创建 Worker实例,它所加载的脚本,必须来自网络。

DOM 操作限制 Worker 线程所在的全局对象,与主线程不一样,区别是:

  • 无法读取主线程所在网页的 DOM 对象

  • 无法使用document、window、parent这些对象

通信限制 Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成,交互方法是postMessage和onMessage,并且在数据传递的时候, Worker 是使用拷贝的方式。

脚本限制 Worker 线程不能执行alert()方法和confirm()方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求,也可以使用setTimeout/setInterval等API。

基本 API

  • 创建worker对象:const worker = new Worker(aURL, options);

  • worker.postMessage: 向 worker 的内部作用域发送一个消息,消息可由任何 JavaScript 对象组成

  • worker.onmessage:当 worker 的父级接收到来自其 worker 的消息时,会在 Worker 对象上触发 message 事件

  • worker.onerror: 当 worker 出现运行中错误时,它的 onerror 事件处理函数会被调用。它会收到一个扩展了 ErrorEvent 接口的名为 error 的事件

  • worker.terminate: 立即终止 worker。该方法并不会等待 worker 去完成它剩余的操作;worker 将会被立刻停止

使用方式

常见的使用方式有直接使用脚本创建方式使用 Blob URL 创建方式,这里只介绍使用脚本创建的方式。

// 主线程下创建worker线程
const worker = new Worker('./worker.js')

// 向worker线程发送消息
worker.postMessage('主线程发送hello world') 

// 监听接收worker线程发的消息
worker.onmessage = function (e) {
    console.log('主线程收到worker线程消息:', e.data)
}

这里注意几个点:

1、worker.js就是需要执行的脚本,其实你就可以理解为需要在副线程中运行的代码。

2、主副线程之间通信,需要通过发消息的方式,所以你会看到worker.postMessageworker.onmessage事件。

3、worker.postMessage主线程发送消息之后,由副线程中的代码获取消息(也就是worker.js中的代码),然后再经过副线程的处理,将处理之后的消息返回。

4、worker.onmessage接收副线程处理之后的数据。

worker.js

self.onmessage = function (e) { 
  console.log('worker接收到的数据', e.data);
  self.postMessage("worker线程收到的:" + e.data);
}

Web Worker 的执行上下文名称是 self,无法调用主线程的 window 对象的。上述写法等同于以下写法:

this.addEventListener("message", function (e) {
    // e.data表示主线程发送过来的数据
    this.postMessage("worker线程收到的:" + e.data); // 向主线程发送消息
}); 

实战

比如之前的搜索框,我们直接从后端读取了大量的数据,然后在大量数据中进行了filter遍历查询,如果数据量太大,这样就会造成后续的卡顿,因此,我们可以按照下面的思路进行优化。

1、数据分片

2、分片之后,再分片查询过滤

3、合并过滤之后的内容

简单来说,就是在全部数据值查询太浪费时间,那么我们就把数据分堆,每一堆每一堆的进行查询过滤,然后再把查询过滤的之后的数据组合起来。而且,我们在每一堆数据进行查询的时候,最好就设置为一个线程,加快计算时间。因此,我们之前封装搜索框的代码,我们可以在把后端查询之后的代码,进行分片分线程处理

methods:{
    dataFilter(data, partSize) { 
      let result = [];
      for (let i = 0; i < data.length; i += partSize) { 
        result.push(data.slice(i, i + partSize));
      }
      return result;
    },
    async handleInput(v) {
      if (!v) {
        this.isShow = false;
        return;
      }
      this.isShow = true;
      let resp = await getAllUsers();
      if (resp.code !== 0) return;
      let result = resp.data;
      //在所有数据中查询
      // this.users = result.filter(user => user.name.includes(v));

      //分片通过web worker进行处理
      let workers = [];

      let dataSlice = this.dataFilter(resp.data, 10);
      dataSlice.forEach(dataPart => { 
        // 使用 Web Workers 进行并行筛选
        const worker = new Worker("./worker.js");
        //发送分片之后的数据以及过滤条件,在副线程中进行处理  
        worker.postMessage({ value: dataPart, filter:v });
        workers.push(worker);
      })

      Promise.all(workers.map(worker => new Promise(resolve => { 
        worker.onmessage = e => {
          console.log("接收处理过后的数据:",e.data);
          resolve(e.data);
        }
      }))).then(data => {
        console.log("--------"); 
        console.log(data)
        this.users = data.flat(Infinity);
      })
    },
}

需要注意的是:const worker = new Worker("./worker.js");由于这里的路径并不会直接被webpack解析,所以,这里./worker.js路径指的是项目根路径,所以最简单的处理,我们可以暂时./worker.js文件放在public文件夹下面

worker.js

self.onmessage = function (e) { 
  console.log('worker接收到的数据', e.data.value);
  let data = e.data.value;
  let filter = e.data.filter;
  let result = data.filter(item => item.name.includes(filter));
  self.postMessage(result);
}

VUE 小细节

计算属性 computed

计算属性方法不能够直接在输入框里面修改值,否则会报错;如果想要修改 computed 的值,需要在 computed 里面写上 get(){} 和 set(){}。

<template>
    <div>
        <h2>{{ firstName }}</h2>
        <h2>{{ lastName }}</h2>
        <h2>{{ fullName }}</h2>

        <input type="text" v-model="fullName">
    </div>
</template>

<script>
export default {
    data () {
        return {
            firstName:'杰',
            lastName:'克',
        }
    },
    computed:{// 计算属性
        fullName:{
            get(){
                return this.firstName + " " + this.lastName;
            },
            set(value){
                console.log(value);
                let arr = value.split(' ');
                if(arr.length == 2){
                    this.firstName = arr[0];
                    this.lastName = arr[1];
                } else {
                    this.firstName = arr[0];
                }
            }
        }
    }
}
</script>

defineProperty



let obj = {
    id:1,
    name:'jack',
    _sex:'男',
    sex:'',
    score:99,
}

// console.log(obj);// { id: 1, name: 'jack', _sex: '男', sex: '', score: 99 }

// Object.defineProperty(要定义的对象名,'要定义的属性',{定义的数据属性})
Object.defineProperty(obj,'score',{
    writable:false,// 属性是否可以被重写
    // enumerable:false,// 属性是否可以被遍历
    configurable:false,// 是否可以删除目标属性或是否可以再次修改属性的特性
});

// obj.score=77;// 注意:上面定义了score不能被重写,所以打印出来还是99
// console.log(obj);// { id: 1, name: 'jack', _sex: '男', sex: '', score: 99 }

// // 注意:上面定义了属性不可以被遍历,所以打印不出来,看不见这个属性了
// console.log(obj);// { id: 1, name: 'jack', _sex: '男', sex: '' }

// delete obj.id;// 可以删除id
// console.log(obj);// { name: 'jack', _sex: '男', sex: '' }
// delete obj.score;// 注意:不可以删除score,因为上面定义了score属性不能被删除,注意先把是否被遍历注释掉或改为true,否则看不见该属性
// console.log(obj);// { id: 1, name: 'jack', _sex: '男', sex: '', score: 99 }


// 控制_sex不被遍历出来,就不会被别人看见这个属性
// Object.defineProperty(obj, '_sex', {
//   enumerable: false,
// })

let user = {
    id:1,
    name:'张三',
    sex:'女'
}

Object.defineProperty(obj,'sex',{
    // 当访问监听的这个sex时,会调用get()
    get() {
        // 把obj._sex赋值给sex,如果什么都不写,会返回undefined
        return this._sex;
    },
    // 当修改obj里的sex时,会调用set()
    set(value){// value就是你修改后的值,注意:不能在set()里面修改被监听的sex的值,否则会造成死循环,但可以修改其他属性值
        user.sex = value;// 把修改后的值赋值给sex
    }
});
obj.sex = '保密'
console.log(user);// { id: 1, name: '张三', sex: '保密' }


let b2 = {
    id:2,
    name:'斗罗大陆',
    author:'唐家三少'
}
let b1 = {
    id:1,
    name:'斗破苍穹',
    author:'',
    _author:'天蚕土豆'
}

Object.defineProperty(b1,'author',{
    get(){
        return "【" + this._author + "】";
    },
    set(value){
        this._author = value;
    }
});
b1.author = '唐家三少';
console.log(b1.author);

Object.defineProperty(b1,'author',{
    get(){
        return b2.author
    },
    set(value){
        b2.author = value;
    }
});
console.log(book.author);

book.author = "辰东"

console.log(b.author);

$set的使用

一、什么场景下使用$set

set为解决Vue2中双向数据绑定失效而生,只需要关注什么时候双向数据绑定会失效就可以了。

示例:

1.利用数组中某个项的索引直接修改该项的时候

arr[index] = newValue

 2.直接修改数组的长度的时候

arr.length = newLength

3.由于 JavaScript 的限制,Vue2不能检测对象属性的添加或删除

详情查看官方文档

二、Vue.set 和 this.$set的关系

 this.$set 实例方法,该方法是全局方法 Vue.set 的一个别名。

三、$set 用法

1.对于数组

this.$set(Array, index, newValue)

2.对于对象

this.$set(Object, key, value)

四、实例场景

需求:data中未定义,手动给form添加age属性,并且点击按钮进行自增。

        如果使用 this.form.age = 10 这种方式,不能进行添加和自增,数据无法响应式。

        此时便需要使用 this.$set方式实现响应式

<template>
	<div>
		<h1> {{ form }} </h1>   <!--{name:'xxx'}/ {name:'xxx',age:10}-->
		<button @click="add">添加</button>
	</div>
</template>
<script>
export default{
	data(){
		return{
			form:{
				name: 'xxx',
			}
		}
	}
	methods:{
		add(){
			if(!this.form.age){
				this.$set(this.form, age, 10) // 成功
				
				// Vue2中监听不到动态给对象添加的属性的
				// this.form.age = 10   // 失败无法添加,更无法自增
			} else {
				this.form.age++
			}
		}
	}
}
</script>

$nextTick的作用

vue中的Dom更新是异步的,是异步的意味着当被处理数据是动态变化时,此时对应的Dom未能及时更新(同步更新)就会导致数据已经更新(model层已经更新)而视力层未更新(Dom未更新)此时就需要使用nextTick了。
当你想要拿到更新后的Dom,一定要在nextTic的回调函数中去获取更新后的Dom的值。

语法:

this.$nextTick(()=>{
 在这里获取dom更新后的值
})

修改数据后Model层会立即同步更新了,而 Dom 并没有及时更新,而在回调函数中的 Dom 的值是更新后的,那么回调函数到底做了什么 呢,在数据变化之后立即使用 this.$nextTick(callback),callback又称延迟回调,而此回调在 dom 更新完成后就会自动调用(它会等待Dom更新完成)。

应用场景

需求:有一个 div,默认用 v-if 将它隐藏,点击一个按钮后,改变 v-if 的值,让它显示出来,同时拿到这个 div 的文本内容。如果 v-if 的值是 false,直接去获取 div 内容是获取不到的,因为此时div 还没有被创建出来,那么应该在点击按钮后,改变 v-if 的值为 true,div 才会被创建,此时再去获取。

<div id="app">
    <div id="div" v-if="showDiv">这是一段文本</div>
    <button @click="getText">获取div内容</button>
</div>
<script>
var app = new Vue({
    el : "#app",
    data:{
        showDiv : false
    },
    methods:{
     	/*此处不加nextTick这个方法,就会报错,因为DOM更新是在下一次事件循环,才更新,所以此时获取不到div元素。
    	getText:function(){
            this.showDiv = true;
            var text = document.getElementById('div').innnerHTML;
             console.log(text);
        }*/
        getText:function(){
            this.showDiv = true;
            this.$nextTick(function(){
                  var text = document.getElementById('div').innerHTML;
                 console.log(text);  
            });
        }
    }
})
</script>

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值