# 230222vuebase **Repository Path**: newsegmentfault/230222vuebase ## Basic Information - **Project Name**: 230222vuebase - **Description**: vue2 基础课程讲解代码 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2023-06-14 - **Last Updated**: 2023-06-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README 课程安排: vue2基础: 10天左右 vue2项目:10天左右 vue3基础:4天左右 vue3项目:8天左右 数据可视化:2天左右 小程序:8天左右 实战.... 精讲... 课前须知: 问:你不问,我不知道你会不会 练:一定要敲,量变引起质变 ​ 知道,了解,熟悉,精通 这是4码事 关于浏览器插件 vue-devtooles: 打开浏览器"扩展程序"页面,把插件拖入浏览器即可 关于地址 gitee地址:https://gitee.com/newsegmentfault/230222vuebase 学习方法: 是什么 为什么 怎么做? vue2 基础 目录说明: * assets - 课件(笔记)使用图片 * base - 未使用脚手架 # day01 ## vue简介 尤雨溪 参考文档(官网) vue2: https://v2.cn.vuejs.org/ 渐进式:类似于迭代开发,vue.js只是一些核心代码,可以让你搭建基本页面 如果你的页面功能相对比较丰富,那么需要相关的一些 **[插件]** 去完成。 插件:就是一些功能代码模块。它是为了给已经完成的功能代码,额外去添加功能用的。 官方插件:vuex、vue-router......官方出品的 都是vue官方的插件 第三方插件:也是为了给vue去添加功能用的,但是是别的人写的。比如axios **数据为尊 动态显示页面** ## 初谈vue - 操作页面内容 * 原生写法 ```js document.getElementById('app').innerHTML = '我爱你' ``` * jquery写法 在 react 和 vue 这些框架出来之前,jquery曾经一统江湖。 一统江湖的原因: 1. 写法简介 2. 处理了绝大多数兼容性 ```js $('#app').html('xxxx') ``` 为什么jquery不行了? 因为操作真实的DOM,会引发重排和重绘,例如: ```html ``` 拿到每个 li 遍历去修改内容,引发大量的重排重绘,大量的重排重绘会引发页面性能问题 > 重排: 重新排列,位置发生变化 > > 重绘: 重新绘制,颜色改变 * vue(数据为尊) 页面变化的时候操作虚拟DOM,更新页面使用的是 fragment(文档碎片:理解成一个内存容器) ```js 真实的页面长如下结构:
我爱你
虚拟DOM: { tag: 'div', attr: { id: 'app', }, children: [ { tag: 'div', attr: { class: 'box' } } ] } ``` 当数据发生变化的时候,页面更新内容。此时对比虚拟DOM的差异,知道要更新的内容之后,将要更新的内容放到 fragment 这个容器当中,在这个容器中进行更页面,而这个容器,不在页面上,而在内存中。当在容器中更新完毕之后,在把更新完毕的DOM放回到页面 书写步骤: 1. 引入 vue.js 文件 2. 创建 vue 实例 3. 书写挂载点 4. 配置数据 5. 在挂载点内,直接写数据 ```html
{{ msg }}
``` ## vue的基本使用 了解即可,之后并不会出现多个的情况 1. 一个 vm 实例,多个挂载点 只会挂载第一个挂载点,第二个挂载点关联不上 vm 实例 2. 多个 vm 实例,一个挂载点 再创建一个 vm 实例内,还挂载到同一个 div 上,第二个创建的 vm 实例关联不上 --- 挂载点: 创建 vm 实例的时候只是在 js 中创建了一个对象,我们最终要通过这个对象来操作页面中的内容,所以需要将页面中需要操作的DOM告诉 vm 实例,那么这个DOM就是挂载点 挂载方式: * el 配置 选择器 * el 配置 真实DOM * vm.$mount('.app') -> 小括号中写选择器 * vm.$mount(document.querySelector('.app')) -> 真实DOM ```js let vm = new Vue({ el: '.app', // el: document.querySelector('.app'), data: { abc: '我爱你' } }) // vm.$mount('.app') // vm.$mount(document.querySelector('.app')) ``` --- data 数据: 在创建 vm 实例的时候,传入的配置项中有一个 data 选项,这个选项用来配置挂载点中要渲染的数据 data 配置方式: ```js let vm = new Vue({ el: '.app', // data: { // abc: '我爱你222' // }, data() { return { abc: '你是个好人' } } }) ``` 目前我们使用 data 对象配置形式即可,关于 data 可以配置成函数的形式,我们在后续的讲解中会详细讲到 ## 插值语法 当我们写法挂载点之后,挂载点中的内容叫做模板 模板是要显示数据,使用插值语法 {{ }} , {{ }} 中写什么? {{ }} 中写 data 配置项中的数据,在这个语法中写 js 表达式,双花括号相当于变成了 js 的分割符, 注意:data 配置项中的数据不是直接拿到了模板中使用,而是在创建vm实例的时候,把data中的数据,挂到了 vm 实例身上,而我们在模板去中写内容的时候,写的是当前vm实例上的东西 注意:在模板中,this 指向实例,一般情况下我们在写模板内容的时候都会把 this 省略掉(注意:新手在写的时候要省略掉,后期会说为什么) ```html
{{ abc }}
{{ this.abc }}
{{ '我爱你' + abc }}
``` > 回顾: > > **表达式:由变量、常量、运算符组成的式子叫表达式** > **注意:表达式是有值的** ## 单向数据绑定 给元素中设置内容方式有几种? 1. 插值语法 ```html
{{ msg }}
``` 2. 指令语法 v-html 这是一个指令,当使用指令的时候,等号后面跟的双引号变成了 js 的分隔符,意思是在这个双引号之间写 js 内容 ```html
``` > 注意:这个数据是从data的拿的吗?这个数据是从vm实例上拿的,全写的情况下应该是 `
` 只不过一般省略了this > > v-html 还可以解析要渲染文本的标签 --- 给元素设置属性的方式 使用 v-bind 指令给元素设置属性 ```html
``` 注意: `v-bind:prop="表达式"` 可以简写成 `:prop="表达式"` --- 不管是给模板中设置内容还是绑定属性,都是 vm 的 data 配置项中的数据绑定在了页面上,数据发生变化后页面也会发生变化(数据驱动页面,从 浏览器 vue 开发者工具看出来),这个叫单项数据绑定 ## 双向数据绑定和MVVM 问题: 现在有一个 input 框,给一个数据之后让 input 框显示这个数据 ```html ``` 当修改这个 input 框中的内容的时候,绑定的数据发生变化了没有?如果没有的话,怎么才能让数据发生变化? 答: 使用 v-model 指令即可,当数据发生变化的时候页面发生改变,当 input 框内容发生变化的时候,数据也会发生改变(可以从 浏览器 vue 开发者工具看出来) ```html
{{ msg }}
``` 这里的 v-model 指令就是双向数据绑定,数据绑定到页面 input 显示,同时页面 input 输入也会改变数据 > v-model 的全写是 `v-model:value="xxx"` --- 理解 vue 的 MVVM 设计模式: ![](assets/mvvm.png) ## v-on 和 methods 我们已经可以修改页面的内容和属性了,页面还有哪些操作?点击事件 问题:如何给模板(挂载点)中的元素绑定点击事件? ```html




``` `v-on:click="clickHand"` 冒号之后是事件类型,等号之后的双引号是分隔符,双引号里面写 js 内容,这里目前写的 methods 中配置的函数 `methods` 是一个配置项,这个配置项中都写方法,`methods` 中配置的 `clickHand` 方法给点击按钮的时候去绑定时的回调 `v-on:click="clickHand"` 这里的 `clickHand` 其实也是 `this.clickHand`,只是我们省略了this(这里目前就不要加this,关于事件的this我们后面强调) 注意: v-on: 指令 是有简写的 `v-on:click="clickHand"` ---> `@click="clickHand"` methods 中写的方法也会挂到 vm 实例上 疑问: 在data中配置的函数数据,也可以作为事件的回调进行绑定 为什么 data 中写函数可以使用,还要发明 methods ? 按下不表 ## 事件 - 参数 事件我们已经知道怎么写了,那么关于事件回调的参数呢? ```html




``` 1. 当事件回调不传参的时候,默认参数 是 event 事件对象 2. 当事件回调传参的时候,在绑定事件的位置,写小括号传参 `@click="clickHandler2('我爱你')"` 小括号中可以放什么? 可以直接放 js 表达式,还可以放 data 中配置的数据 3. 如果想要参数和事件对象一起呢? `@click="clickHandler3($event, message)"` 注意: 在模板中调用 methods 中的方法,在方法后面加小括号的时候 需要传事件对象的时候,使用$event,这个是固定写法 ## vue中的 this 指向 回顾 this,问:js中总共由几种 this ? * 全局函数 ```js function fn () { console.log(this) } fn(); ``` 函数的直接调用,this 是指向 window 的 谁调用就指向谁,这里是 window 调用 * 对象中的方法 ```js var obj = { name: '旺财', eat: function () { console.log(this) } } obj.eat(); // 这里this指向obj,这里是obj调用 ``` ```js const fn = obj.eat; fn(); // window 调用 ``` * 构造函数中的 this 指向即将创建出来的实例对象 ```js function Person() { console.log(this) } new Person() ``` * 事件回调中的 this 指向事件源 ```js
function () { console.log(this) } ``` * 箭头函数 - 箭头函数中是没有this的,this指向父级作用域中的 this * 严格模式下 this 是 undefined 的 (一般用不到,但是要知道) * call 和 apply 和 bind ```js var obj1 = { name: '旺财', eat: function () { console.log(this) } } var obj2 = { name: '小黑', } obj1.eat(); // this指向obj1 obj1.eat.call(obj2); // this指向obj2,call改变了this指向 ``` * 在 vue2 中 this 指向 vue 当中 this 永远指向实例对象 ```html
``` ## Object.defineProperty() Object.defineProperty() 功能:给对象设置属性用的,使用该方法设置的属性,在设置值和获取值的时候,可以感知到设置值和获取值的过程 参数: * 参数一: 要添加属性的目标对象 * 参数二: 要添加的属性名 * 参数三: 是一个配置对象,配置当前添加的这个属性的 返回值: 返回参数一这个对象 ```js var obj = { name: '张三' } // 给对象添加属性 obj.age = 18; obj['sex'] = '男'; var str = '我爱你' Object.defineProperty(obj, 'height', { // value: 176, // 设置的当前这个属性的属性值 // writable: true, // 配置是否可修改当前这个属性 enumerable: true, // 配置当前属性是否可枚举 configurable: false, // 配置当前属性是否可以重新定义(被删除) // 获取值的函数,在我们获取当前属性的时候会走get方法 get() { return str }, // 设置值的函数,给当前属性设置值的时候,会走set方法 set(val) { str = val; } }) console.log(obj.height); obj.height = '身高和爱你没关系'; ``` 注意: Object.defineProperty 中 get set 方法和 value writable 冲突 --- 拓展:Object 还有哪些常用方法 ```js Object.defineProperty Object.defineProperties Object.create() Object.assign() Object.keys() ``` ## vue2响应式原理 问题一:我们在创建 vm 实例的时候,传入的配置项目data中的数据是如何到vm实例上的? 问题二:当我们修改数据的时候,页面怎么就更新了? ### 数据代理 vue2中响应式原理,主要是通过 数据代理 和 数据劫持 实现的,数据代理 和 数据劫持 底层用的都是 Object.defineProperty() 方法 什么叫数据代理? 数据代理的意义在于,将配置项中的 data 数据代理到 vm 实例上 让我们访问数据的时候,使用 this.xxx 可以直接访问到数据 ```js function VM(options) { let data = options.data; // 拿到配置的对象 // 数据代理的实现 // Object.keys(data) // 拿到data所有属性组成的数组 Object.keys(data).forEach(key => { Object.defineProperty(this, key, { get() { return data[key] }, set(val) { console.log('数据代理set') data[key] = val } }) }) } const vm = new VM({ data: { name: '张三', age: 20 } }) console.log(vm.name) ``` ### 数据劫持 什么叫数据劫持? 数据劫持的意义在于,我们修改数据的时候,需要更新DOM显示 更新DOM这个过程需要我们可以劫持到数据的设置/获取 当修改数据的时候,通过 Object.defineProperty() 劫持到数据的修改,在set和get方法中,更新DOM ```js function VM(options) { let data = options.data; // 拿到配置的对象 // 数据代理的实现 // Object.keys(data) // 拿到data所有属性组成的数组 Object.keys(data).forEach(key => { ... }) // 数据劫持 Object.keys(data).forEach(key => { defineReactive(data, key, data[key]) }) } function defineReactive(data, key, value) { // 注意:这里data拿到的是地址 注意:形参相当于是变量 // data作为一个普通的对象是拿不到设置值和获取值得过程 // 需要使用 Object.defineProperty() 重新给data定义一下属性,让属性具有set和get方法,可以拦截到设置值和获取值的过程 Object.defineProperty(data, key, { get() { return value; }, set(val) { console.log('数据劫持set') value = val; // 当截获到给data设置值的时候,此时应该通知页面更新 // updateDOM() --- 调用更新DOM方法 } }) } const vm = new VM({ data: { name: '张三', age: 20 } }) console.log(vm.name) vm.name = '李四' // 当给vm.name设置值的时候,走数据代理的set方法,因为当前是给vm下的某一个属性设置值 // 在数据代理的set中,我们写了 // set(val) { // console.log('数据代理set') // data[key] = val // } // 此时走了 data[key] = val,这行代码,那么此时是给 data的属性设置值 // 而刚刚在 defineReactive 这个函数中把data所有的属性都重写了,变成了可以拦截到设置值和获取值的效果 console.log(vm.name) // 疑问: 数据劫持函数的参数三 value 不能直接使用 obj.name 来代替 get 返回的值吗? // 报错 - 有了递归了 // let obj = { // name: 'xxx' // } // Object.defineProperty(obj, 'name', { // get() { // return obj.name // } // }) // console.log('obj.name', obj.name) ``` --- 回顾 - 闭包形成的条件: 1. 函数的嵌套 2. 内部函数引用外部函数变量 3. 外部函数被调用 ## computed 和 watch 需求:给姓和名的数据,最终得到姓名,有几种方式? ```html

第一种: 模板中插值语法字符串拼接

{{ firstName + lastName }}

第二种: 指令中字符串拼接

第三种: 通过methods中方法调用返回值

{{ getFullName() }}
{{ getFullName() }}
{{ getFullName() }}

第四种: 通过计算属性 computed 得到

{{ fullName }}
{{ fullName }}
{{ fullName }}

第五种: 通过 watch 来监视数据的变化

{{ fullName2 }}
``` # day02 ### computed - 计算属性 计算属性 是一个配置项,在这个配置项当中可以配置属性 这个属性是给vm实例配置的,当vm实例使用这个属性的时候,会自动去走这个函数 计算属性是一个属性,当模板中没有使用到这个属性的时候,它不会去计算;当模板中使用了这个属性,会走这个方法计算出来,并且把结果缓存起来 只要计算属性所依赖的数据不发生变化,就不会重新计算 只要依赖的数据发生变化就会重新计算 **两种写法** ```js computed: { // 第一种写法: 函数写法 fullName() { return this.firstName + ' - ' + this.lastName }, // 第二种写法: 对象写法 fullName: { get() { return this.firstName + ' - ' + this.lastName }, set(val) { console.log('走了set', val) } } } ``` ### watch - 监听 watch 也是一个配置项,用来监听数据的变化 配置当前实例上已存在的数据,当监听的数据发生变化的时候会执行回调,在回调中可以进行一些 js 的逻辑(同步/异步)操作 **两种写法** ```js watch: { // 函数写法 fullName(nval, oval) { console.log(nval) }, // 对象写法 fullName: { handler(nval, oval) { console.log(nval) }, immediate: true, deep: true }, // 监听某个对象下的具体某个属性 "obj.name": { handler(nval, oval) { console.log(nval) }, immediate: true, deep: true }, } ``` immediate - 是否初始化时执行一次监听回调,默认 false deep - 是否开启深度监听,默认 false --- 重点: methods 和 computed 区别 1. methods 需要调用 computed不需要 2. 计算属性会把结果缓存在内存中,只要依赖的数据不发生变化,就不会重新计算 computed 和 watch 的区别 1. computed 是计算属性,计算出来是一个属性,这个属性是自身不存在,然后计算得出得 watch 是监视,对已存在数据的监视 2. computed 中不能存在异步,因为计算属性的值依赖return watch 中是可以存在异步的,监视到行为之后,可以延时出处理,执行操作 ## 条件渲染 ### v-if v-if 条件渲染指令有三个 `v-if`、`v-else`、`v-else-if` v-if 用于条件性地渲染一块内容。这块内容只会在指令的表达式返回真值时才被渲染。 ```html

测试文本1

``` v-else、v-else-if 指令必须和 v-if 指令一起使用,先写 v-if 再写 v-else、v-else-if , 和 js 的逻辑 if 语句一样 ```html

测试文本1

测试文本2

``` ### v-show v-show 只有这一个指令,同样用于条件性渲染一块内容 ```html

测试文本1

测试文本2

``` 以上代码用来模拟 v-if 和 v-else 一样的显示效果 --- v-if 和 v-show 的区别? v-if 条件指令中隐藏的元素,在真实的DOM中是不存在的 v-show 条件指令中隐藏的元素,在真实的DOM中是存在的,只是当前的元素被隐藏掉了(使用css `display: none;` 隐藏的元素) ## 列表渲染 v-for 指令用于列表的渲染,写法有点类似于 js 中的 for...in 语句写法 ```html
序号: {{ index }} - id: {{ item.id }} - 内容: {{ item.content }}
``` > 注意: > key是一个唯一标识,key的作用主要是为了高效的更新虛拟DOM,其原理是vue在diff对比虚拟DOM的过程中通过key可以精准判断两个节点是否是同一个,从而避免频繁更新不同元素,减少DOM操作量,提高性能。 ## 列表过滤 需求:列表的过滤 详情:在页面中有 input 框,input中输入内容,列表的展示需要 和 输入的内容相同 ```html
id: {{ item.id }} - 内容: {{ item.content }}
``` --- 拓展:你用过哪些数组方法,列举出来,越多越好(面试题) ```js push、pop、shift、unshift slice concat join reverse splice toString valueOf // 返回当前调用数据的值 sort forEach filter map find findIndex some every reduce ``` >什么场景下会用到 valueOf ? > >``` >var a = [3, 5] >var b = { name: '张三' } >console.log( a + b ) > >引用数据类型的运算: >1. 会先调用 valueOf 这个方法,得到数据的值,看是不是一个基本值(基本数据类型) > a.valueOf() -> [3, 5] > b.valueOf() -> { name: '张三' } >2.如果是基本值直接进行运算,如果不是基本值,那么此时调用 toString() 转成字符串 > a.toString() -> "3,5" > b.toString() -> '[object Object]' >3. 转成字符串之后,就可以进行运算了 >``` ## 列表排序 需求:列表的排序 在页面中有 input 框,input中输入内容,列表的展示需要 和 输入的内容相同 数据中每个对象都有年龄,设置三个按钮,"按升序排列"、"按降序排列"、"按原序排列",点击之后列表展示的结果将有顺序 注意:这里点击排序,也是不能改原数据,并且不能把过滤给丢失掉 ```html
id: {{ item.id }} - 内容: {{ item.content }} - 年龄: {{ item.age }}
``` 以上列表的过滤和排序都是使用 computed 实现的,computed能实现的东西,watch一定能够实现,接下来使用watch 实现一下,列表的排序和过滤 ## 列表的排序和过滤 - watch 实现 ```html
id: {{ item.id }} - 内容: {{ item.content }} - 年龄: {{ item.age }}
``` ## vue中数组数据的响应式变化 需求:数组数据,修改数据数据有几种方式? 修改下标为0位置的content ```html
{{ item.content }}
``` Vue 能够侦听响应式数组的变更方法,并在它们被调用时触发相关的更新。这些变更方法包括: - `push()` - `pop()` - `shift()` - `unshift()` - `splice()` - `sort()` - `reverse()` ## 绑定 class 和 style ```html
内容1

绑定一个class

单独绑定一个class,可以直接绑定字符串,或者数据
内容1
内容1

绑定多个class

数组绑定,数组中直接放类名,或者放数据数据中写类名
内容1

绑定多个类型,但是不确定哪个类名好使

对象绑定的时候,属性名是类名,控制这个类名存在与否,是一个布尔值
哈哈哈

哈哈哈
``` * 绑定class三种写法 * 直接绑定 - 绑定一个class `
内容1
` * 数组绑定 - 绑定多个class `
内容1
` * 对象绑定 - 绑定多个class ```html
哈哈哈
new Vue({ ..., data: { isA: true } }) ``` * 绑定 style ```html
哈哈哈
``` # day03 ## 事件拓展 - $event来源 结论:在模板中尽量不要写this 疑问:在模板中写回调的时候,携带上 this 有的情况下会报错,什么情况下会报错?为什么会报错? 注意:在推导过程看不懂的情况下记住结论即可 ```html
``` ![](assets/$event来源.png) 发现: 当@click绑定事件的时候, 回调函数不书写小括号的时候,直接就是把这个函数传到的底层 回调函数书写小括号的时候,底层给你传的内容包裹了一层,包裹的这层函数的return的值才是真正传到底层上的值 结论:只要加小阔好,底层上就包裹一层 当传入的内容加小括号,且加了this `@click="this.testHandler($event, 9527)"` 此时这里的this指向window,非严格模式下指向window,严格模式下指向undefined ## 事件深入 ### 事件传参 * 默认不传参是回调用接收到的是事件对象 `` * 回调中参数一接的是 9527 `` * 既有事件对象和参数 `` ### 事件修饰符 * .prevent 修饰符 需求:点击a标签的时候会默认的进行跳转,能不能不让 a 标签点击的时候跳转? 可以,阻止事件的默认行为即可 ```html 百度一下 methods: { baiduClick(e) { console.log('跳转百度') // e.preventDefault(); // 阻止默认行为 }, } ``` * .stop 需求:两个div嵌套的时候,点击内部的div可以触发到外部div绑定的事件,可以不可以阻止这种行为? 可以,阻止事件冒泡即可 ```html
methods: { box1Click() { console.log('box1触发') }, box2Click(e) { console.log('box2触发') // e.stopPropagation(); // 阻止冒泡 }, } ``` * 不常用的事件修饰符 * .capture 默认事件是冒泡的,加.capture修饰符,事件变为捕获触发 * .self 修饰符,只有自己点击事件被触发时候才会执行,冒泡过来的不会执行回调 * .once 事件只触发一次 (和.stop的作用一样,都是阻止冒泡,只不过针对的绑定元素不一样) * .passive 用在滚轮事件上 当滚轮滚动的时候,会触发一个事件,如果在这个回调中有非常耗时的操作 例如说: 循环打印10万次'i love you',此时等待这个耗时的操作执行完毕之后,才能页面滚动(这是正常的) 当加了.passive之后,先去滚了页面,在执行这个回调操作 * 键盘事件 - 一般在input框中会用到 .enter 键盘事件修饰符 ```html methods: { keydownHandler() { console.log('keydown') }, keyupHandler(e) { // if (e.keyCode == 13 || e.code == 'Enter') { // console.log('敲击了回车', e.keyCode, e.code) // } console.log('执行了回车事件回调') }, } ``` --- 不常用键盘修饰符 ```html .enter .tab .delete (捕获“删除”和“退格”键) .esc .space .up .down .left .right ``` ## 收集表单数据 需求: 注册账号 ![](assets/收集表单数据.png) ```html
用户名:
密码:
邮箱:
性别:
爱好: 抽烟 喝酒 烫头
城市:
是否同意该协议
``` ## vue内置指令 * v-text 渲染文本 * v-html 渲染html * v-show 通过css控制显示和隐藏 * v-if DOM中直接不渲染 条件控制(true、false) v-else v-else-if * v-for 列表渲染,像forin 注意: 绑定key * v-on 绑定事件,简写 v-on:click --> @click * v-bind 绑定属性,v-bind:aa="xxx" --> :aa="xxx" * v-model 双向数据绑定,用在表单元素上,用来收集用户输入的内容 * v-slot -----> 单独说,后面讲 * v-pre 加了该指令,标签中的内容直接渲染,不会解析 * v-once 一次,加了该指令的元素,元素的内容只会被解析一次 * v-cloak 当网络不好的时候,vue.js这个文件还没请求回来的时候,此时页面请求回来了 页面会展示出插值语法来,对用户显示很不友好 使用 v-cloak 来解决闪动插值语法这个现象 1. 给有插值语法的标签添加这个指令 2. 通过css控制当前元素不显示 ```html ``` 未使用v-cloak效果 ![](assets/未使用v-cloak.webp) 使用v-cloak效果 ![](assets/使用v-cloak.webp) * vue获取真实DOM 步骤: 1. 给标签设置 ref 属性,属性值自己取 2. 在vue中,使用 $refs 来获取 $refs是一个对象,这个对象会把页面中所有有ref属性的元素拿到 $refs对象中的属性名是标签上的ref属性的属性值 ```html
哈哈
呵呵
methods: { getEl() { console.log(this.$refs) console.log(this.$refs.boxRef) console.log(this.$refs.qwerRef) } } ``` ## 过渡和动画 需求: 点击按钮让div盒子显示和隐藏, 需要过渡的效果是宽高和透明度 步骤: 1. 将要过渡的元素放到 transition 标签中 (这个标签是vue提供的) 2. 书写类名 v-enter 进入前 v-enter-to 进入后 v-enter-active 过程中 3. 如果页面中过多个过渡的元素,给 transition 标签添加 name 属性 类名 v- 开头变成 transition 的 name 属性值开头 ```html
``` animation 复合属性: | 属性名 | 解释 | | --- | --- | | animation-name | 指定要绑定到选择器的关键帧的名称 | | animation-duration | 动画指定需要多少秒或毫秒完成 | | animation-timing-function | 设置动画将如何完成一个周期,完成动画的速率曲线 - ease 先快后慢 linear 匀速直线 | | animation-delay | 设置动画在启动前的延迟间隔 | | animation-iteration-count | 定义动画的播放次数 - infinite 无限次 | | animation-direction | 指定是否应该轮流反向播放动画 - reverse 反向 | | animation-fill-mode | 规定当动画不播放时(当动画完成时,或当动画有一个延迟未开始播放时),要应用到元素的样式。 | | animation-play-state | 指定动画是否正在运行或已暂停 running - 运行中 paused - 暂停 | > 拓展问题: > css中哪些属性可以设置过渡? > 所有可以连续的值都可以过渡,例如:宽高、颜色、透明度都可以过渡,display不能过渡 ## 生命周期钩子函数 生命周期钩子函数分4大部分 创建、挂载、更新、销毁 细分为8个创建前、创建后、挂载前、挂载后、更新前、更新后、销毁前、销毁后 * beforeCreate - 创建前,获取不到数据 * created - 创建后,可以获取到数据,创建指的不是实例的创建,创建指的是初始化数据和事件 * beforeMount - 挂载前,获取不到DOM * **mounted** - 挂载后,可以获取到DOM元素 这个钩子最常用,我们一般会把网络请求 和 一些异步操作 放在这个钩子中 * beforeUpdate - 更新前,可以获取到数据,获取不到DOM * updated - 更新后,可以获取到数据,可以获取到DOM。 更新前后指的是DOM的更新前后,不是数据的更新前后,只有当数据发生变化之后才会触发更新前和更新后钩子 * beforeDestroy - 销毁前 * destroyed - 销毁后 通过 v-if 隐藏的时候可以触发销毁(写在组件上,后续说) 手动触发销毁 `vm.$destroy()` ```html
哈哈

我爱你

``` ![](assets/生命周期.png) ## 自定义过滤器 * 自定义过滤器 是用来格式化文本使用的 * 在哪用? 在 插值语法 和 v-bind 指令中使用 * 怎么玩(步骤): 1. 定义过滤器 2. 使用 | 来使用过滤器 * 需求: 对所有要显示的数值加上10(局部注册) 对所有要显示的数值减上10(全局注册) > 全局注册 和 局部注册 > 全局注册相当于在任何地方都可以用 > 局部注册只在当前实例中(模板中)使用 ```html
{{ n }}
{{ n + 10 }}
{{ n | addTen }}
{{ n | subTen }}
``` ### 自定义过滤器练习 需求:展示时间 格式化时间戳 -> 2022-09-04 15:17:45 日期 -> 2022-09-04 时间 -> 15:17:45 ```html
{{ newDate }}
{{ newDate | dateTimeFormat }}
{{ newDate | dateFormat }}
{{ newDate | timeFormat }}
``` ## 自定义指令 * 自定义指令 我们自己去定义指令,去书写指令的逻辑 * 需求: 自定义指令 - 将所有字符转成大写(局部注册) 自定义指令 - 将所有字符转成小写(全局注册) ```html
{{ msg }}
``` ### 自定义指令练习 需求: 点击按钮之后2秒内不能再次点击 ```html
``` # day04 ## 自定义插件 1. 定义插件: 写一个对象,给这个对象必须暴露一个 install 方法 ```js (function( window ) { // 使用立即执行函数为了防止插件中的一些变量受到污染, // 例如说定义的abc const abc = ''; // 有可能外部也用到abc这个变量会污染这个变量,模块化一下 // 声明一个对象,这个对象就是个插件 const MyPlugin = {} window.MyPlugin = MyPlugin; // 全局可以访问到 MyPlugin.install = function (Vue, options) { // 1. 添加全局方法或 property Vue.myGlobalMethod = function () { // 逻辑... console.log('myGlobalMethod') } // 2. 添加全局资源 Vue.directive('my-directive', { bind(el, binding, vnode, oldVnode) { // 逻辑... console.log('插件自定义的指令'); } }) // 3. 注入组件选项 // Vue.mixin({ // created: function () { // // 逻辑... // } // }) // 4. 添加实例方法 Vue.prototype.$myMethod = function (methodOptions) { // 逻辑... console.log('实例方法 $myMethod 调用') } } })( window ); ``` 2. 使用插件: Vue.use(插件名) --> 使用插件本质上就是调用了插件的 install 方法 ```html
``` ## 组件 什么是组件? 由html、css、js组成的代码块,组件的目的为了复用 在vue中组件分为 单文件组件 和 非单文件组件 * 非单文件组件 一个文件不是一个组件 * 单文件组件 一个文件就是一个组件 组件的步骤: 1. 定义组件 2. 注册组件 3. 使用组件 ### 自定义组件-非单文件组件 1. 定义组件 Vue.extend() 定义组件,里面需要传入一个配置项,这个配置项和 new Vue()中传入的配置一摸一样,唯独没有el这个配置项 Vue.extend()这个函数返回一个函数,为什么 ? 这个函数是在页面中使用组件的时候 new 的 ```js const vueComponent = Vue.extend({ // 注意: 组件中data配置项,必须使用函数形式 // data: { // n: 8 // }, data() { return { n: 7 } }, // template 配置像来配置组件的html // 注意: 模板中必须有一个根节点(根标签),这是规定 template: `
我爱你,高圆圆
` }) ``` template 配置像来配置组件的html 注意: 模板中必须有一个根节点(根标签),这是规定 > 注意: 组件中data配置项,必须使用函数形式,为什么? > 如果使用对象形式的话,页面中有多个组件实例的时候,会公用一套数据 2. 注册组件 Vue.component() 用来创建组件,两个参数 参数一: 组件名 参数二: Vue.extend()返回的函数 ```js Vue.component('mybutton', vueComponent); ``` 3. 使用组件 页面上在使用这个组件,本质上实在 new 一个组件实例,需要的是一个构造函数 ```html
``` --- 以上写法是分开步骤写的,我们可以把步骤1和步骤2合并到一起,Vue.component()的参数二直接放配置项 ```js // 这个是简写,合并了之前的步骤1和步骤2,定义并注册了 Vue.component('mybutton', { data() { return { n: 7 } }, // template 配置像来配置组件的html // 注意: 模板中必须有一个根节点(根标签),这是规定 template: `
我爱你,高圆圆
` }) ``` --- 以上写法是全局注册,组件除了全局注册以外还可以进行局部注册 ```html
``` ### 自定义组件-单文件组件 单文件组件顾名思义,一个文件就是一个组件,在这个我们可以创建一个 `App.vue` 文件作为单独的一个组件,这个组件作为整个页面的根组件来使用 ```vue ``` 同时在 `App.vue` 组件中引入了 `mybutton.vue` 组件,除了根组件以外,一般情况下我们把组件放在 `components` 这个文件夹下 `components/mybutton.vue` ```vue ``` 然后将 `App.vue` 组件引入到 `index.html` 文件中进行渲染即可(两种写法) * 第一种写法 - 注册 App 组件在模板中直接写App组件 ```html
``` * 第二种写法 - 写 template 配置项,在template配置项中写 `` 在模板中就不用再写 App 组件了 ```html
``` 注意: 此时是不能运行的,为什么? 1. script标签不认识ES6语法 - 给script标签加 type="module" 可以让script标签认识ES6语法 2. 不认识.vue文件 - .vue文件无法被解析,所以需要上脚手架工具,把.vue文件转成浏览器可以识别的 > 关于浏览器解析ES6语法拓展 > > `index.html` > > ```html > > ``` > > `99.js` > > ```js > export default { > a: 100, > fn() { > console.log('i love you') > } > } > ``` ## 脚手架的安装 和 目录结构(参考全家桶) 一、使用脚手架工具 `vue-cli` 构建项目,`vue-cli` 本质上还是使用的 `webpack` 在构建项目(脚手架工具目前最新版本是5,之前的一些老项目还有2/3/4版本) 创建项目: 1. 创建脚手架5/4/3的vue项目, 并运行 ```shell npm install -g @vue/cli 安装脚手架5/4/3的版本(目前是5版本) vue create vue-demo 使用安装的脚手架创建一个新的vue项目 npm run serve 运行创建的项目命令 ``` 2. 创建脚手架2的vue项目 ```shell npm install -g @vue/cli-init vue init webpack vue-demo npm run dev ``` 两种创建方式的差异(了解): 1. webpack配置 (1) 2脚手架: 配置是暴露的, 我们可以直接在里面修改配置 (2) 4/3脚手架: 配置是包装隐藏了, 需要通过脚手架扩展的vue.config.js来配置 (3) 5脚手架: vue.config.js直接生成了 2. 运行启动命令 (1) 2: npm run dev (2) 3/4/5: npm run serve > 目录介绍 > > vue.config.js 是vue留给我们配置webpack的一个通道,我们自己配置webpack的内容写在vue.config.js文件中 > > README.md 当前项目的说明文档(程序员自己写,这个文件一般会在上传到 git 远端后,在远端直接渲染出来) > > package.json 包的信息,包括依赖,启动项目的指令都在这个文件中 > > package-lock.json 依赖的依赖信息 > > jsconfig.json 配置js的 > > babel.config.js 配置 babel 内容 > > .gitignore 上传git的忽略文件 > > src 文件夹是我们要写代码的地方 > > src/assets 是静态资源 > > src/components 放组件 > > src/App.vue 根组件 > > src/main.js 入口文件 > > public 静态资源 > > 注意:src/assets 和 public 都是静态资源,但是有区别,assets中的内容会被webpack处理, public 中的内容不会被webpack处理 二、eslint的禁用 1. 局部禁用某个错误提示 - 单个文件禁用ESlint的 ```js /* eslint-disable no-unused-vars */ ``` 2. package.json 当中找到 eslintConfig 项,全局配置禁用某些错误提示 ```json "rules": { "no-unused-vars":"off", // off关闭当前规则 "no-unused-vars":0, // 和off一样 "no-unused-vars":1, // 规则警告 "no-unused-vars":2, // 规则报错 } ``` 3. 开发阶段直接关闭eslint的提示功能 `vue.config.js` ```js // 这个文件是脚手架给的一个通道,这个文件当中配置的webpack配置项最终都会合并到真正的webpack.config.js当中 module.exports = { // 写自己想要配置的东西去覆盖系统自带的 // 关闭ESLint的规则 lintOnSave: false } ``` > ESLint规则: > http://eslint.cn/docs/rules/ > > https://eslint.nodejs.cn/docs/latest/rules/ > > vscode中vue文件高亮插件: vetur ## 组件模板解析 - Vue渲染两种方式 1. render:h => h(App) 2. components注册组件,template解析,但是vue导入需要导入带解析器的版本 > 为什么? 不带模板编译器的体积小 ![](assets/vue的两种渲染方式.png) ![](assets/vue的两种渲染方式对比.png) ## git的基本使用6大步 先有本地再有远程 1. 创建本地 2. 创建远程 3. 关联本地和远程 4. 本地修改推向远程 5. 远程修改拉向本地 先有远程再有本地 6. 如果来的一个新员工,那么此时项目在远程已经存在,这个人只需要clone ## 验证上午写的单文件组件 ## 组件间传参 - props ### props传参 父子组件之间的传参使用props 1. 在子组件标签上,使用 v-bind 绑定属性进行传参 2. 在子组件中,配置props来接收父组件传过来的参数 在接收数据的时候总共有三种形式 1. 数组形式 ```js props: ['qwer'], ``` 将绑定在组件标签上的属性名,直接在数组接收一下即可 2. 对象形式 ```js props: { qwer: String, }, ``` 接收的qwer是绑定的属性名, String规定了接收的数据类型 3. 配置对象的形式 ```js props: { qwer: { required: true, // 必须传,不传报错 type: String, // 限制类型 default: '我爱你,杨幂' // 不传该属性的时候默认值,与required互斥 } }, ``` ### 单项数据流向 不能直接修改props传过来的数据,使用props父组件给子组件传参的时候,遵循单向数据流规则 单向数据流规则: 数据是单行向下传递的,不能够子组件修改父组件传过来的数据。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。 当使用props传递对象的时候,子组件可以去修改对象中的属性,注意:这是不被允许的 虽然没有问题,但是违反了 单向数据流的规则,我们不要这么做,这样会把数据的流向改的混乱,不利于理解 举个例子: 就像过马路,遵循红绿灯,传对象能修改对象属性,相当于半夜路上没车,你闯红灯 总结: props传参分为两大类 1. 非函数数据 - 让子组件展示使用 非函数数据又分为: * 基本数据类型 - 子组件不能改 * 引用数据类型 - 子组件不能改地址,可以改属性(不建议,违反单向数据流) 2. 函数数据 - 让子组件调用,修改父组件的数据 # day05 ## todoList案例 1. 静态页面 - 静态组件拆分 拿静态页面过来拆分组件即可,需要拆html、css、js 2. 数据初始化展示 数据在哪? - 放在所有组件的公用的父组件当中 数据长什么样子? ```js todos: [ { id: 1001, content: '抽烟', isSel: false }, { id: 1002, content: '喝酒', isSel: true }, { id: 1003, content: '烫头', isSel: false }, ] ``` 1. main组件中每个item的展示 需要去遍历todos,循环展示每个Item组件 2. footer组件中,总个数 和 选中个数的展示 需要todos数据的,长度是总个数,选中个数需要计算 ```js // 选中个数 computed: { selCount() { return this.todos.reduce((prev, item) => prev + item.isSel, 0) } } ``` 3. 交互 1. Header组件中输入内容,创建一条数据,添加到todos当中 > 注意: > > 1. 输入重复的内容,拦截住,给提示,不能让用户输入重复内容 > > 需要拿到todos这个数据去判断已有数据的content和输入内容 keyword 不能重复 > > ```js > // 1. 使用some - 判断数组中有一个和我们输入的内容相容,返回true > const isRepate = this.todos.some((item, index, currentArr) => { > return item.content === this.keyword.trim() > }) > if (isRepate) { > alert('输入的内容有重复,请重新输入') > this.keyword = '' > return > } > // 2. 使用map + includes > // this.todos.map(item => item.content) --> ['抽烟', '喝酒', '烫头', '打豆豆'] > if (this.todos.map(item => item.content).includes(this.keyword.trim())) { > alert('输入的内容有重复,请重新输入') > this.keyword = '' > return > } > ``` > > 2. 当输入添加完数据的时候,需要清空keyword > 3. 添加数据的时候需要调用父组件传过来的函数,让父组件App修改todos数据 2. 每个Item的选中 点击每个Item的checkbox,拿到点击的哪一个,拿到索引(索引从main组件循环的地方传进来),调用App传过来的函数,把索引传 过去,让App组件去修改数据的选中状态 > 注意:函数是一层一层传下来的,从App传到Main不动,直接继续往Item传 3. 删除按钮 鼠标移入、移出显示隐藏 先把button按钮的隐藏样式去除掉,使用一个变量来控制button的显示和隐藏,我们使用的是v-show 这个变量在移入Item的时候变为true,移出变为false 点击删除按钮删除Item 获取到索引值(索引从main组件循环的地方传进来),调用App组件传过来的函数(一层一层往下传),告诉App组件要删除数据的索引 4. Footer组件中的全选 之前在数据展示的时候只是展示,之前使用的是计算属性的函数形式,现在这个属性有交互了,可以去设置这个值,计算属性就需要改成对象的形式,有set方法,当值发生改变的时候,可以获取到,获取到之后调用父组件传过来的方法,让父组件修改所有item的选中状态 5. 删除所有已选中的Item 调用父组件传过来的函数,让父组件删除已选中的数据 4. 数据持久化 localStorage 和 sessionStorage 区别? localStorage 只要不删除,一直存在于浏览器当中 sessionStorage 只要关闭浏览器重新打开,里面的内容就会清空 可以存储的数据大小大约为5M 使用方式: ```js // 设置值 localStorage.setItem(key, value) // 获取值 localStorage.getItem(key) // 删除值 localStorage.removeItem(key) // 注意 - value只能存的是字符串,我们存数据的时候需要转成json字符串 ``` todolist案例中如何使用的: 1. 设置值 只要页面中的数据发生变化的时候,就需要存储一下这个数据(使用的watc监听) 2. 获取值 当页面刷新的时候,直接去 localStorage 当中获取之前存的值,获取到就用之前的,获取不到设置默认的 遗留的BUG: 1. 每个Item的选中状态发生改变的时候,刷新页面不好使 使用watch监视数据的时候,需要使用 `deep: true` 深度监听 2. 当删除掉所有的item的时候,全选按钮变成了true 问题是由于数组的 every 方法,当调用这个方法的时候,数组是空数组也会返回true,我们只需要把空数组的情况单独讨论即可 # day06 ## 自定义事件 * 系统事件 * 事件类型 - 有限个数 click mouseenter ... * 触发机制 - 浏览器触发的,触发之后会给一个事件书香 * 自定义事件 * 事件类型 - 无限个数 * 触发机制 - 自己使用 `$emit` 触发,参数自己给 ## todolist - 改成自定义事件 所有的交互都改成了自定义事件,只要是原来调用函数修改父组件App数据的地方都改了 1. Header添加 2. Main - Item 选中状态 3. Main - Item 删除按钮 4. Footer组件 - 全选 5. Footer组件 - 删除已选中 ## 自定义事件 - 关于参数 - $event $event * 在系统事件当中,模板中写 $event 是事件对象 * 在自定义事件中,$event 是触发事件时候传的第一个参数 ## .native 修饰符 当给组件绑定自定义事件的时候,此时加上 .native ,事件变为系统事件,绑定到组件的跟标签上 ## $on、$off、$once * $on - 获取到组件实例,绑定事件,留下回调 注意: $on 可以对相同的事件类型进行多次绑定 * $off - 解绑事件 * $off() - 当前组件实例上的事件全部解绑 * $off('事件类型') - 解绑某一个事件类型 * $off('事件类型', 回调) - 解绑某一个事件的某一个回调 * $once 绑定的事件只能触发一次,触发一次之后自动解绑 ## $on 、 $emit 在哪 $on 和 $emit 在Vue的原型对象上,组件实例 vc 和 vue 实例 vm 之间的关系? ```js VueComponet.prototype = Object.create(Vue.prototype) ``` ![](assets/vm和vc的关系.png) > 终极原型链: > > ![](assets/终极原型链.png) > > > ![](assets/终极原型链2.png) ![](assets/终极原型链3.png) ## 事件总线 全局事件总线本质是一个对象,必须符合两个条件 1. 所有组件都能访问 2. 必须可以使用 $on 、 $emit 步骤: 1. 安装总线 ```js new Vue({ beforeCreate() { Vue.prototype.$bus = this; // 安装总线 } }) ``` 2. 接收数据 在接收数据的组件中,找到总线,绑定事件,留下回调,接收数据 ```js this.$bus.$on('事件类型', 回调) ``` 3. 发送数据 在发送数据的组件中,找到总线,触发事件,发送数据 ```js this.$bus.$emit('事件类型', 参数) ``` ![](assets/全局事件总线.png) ## PubSub pubsub 是一个第三方的插件,用于跨组件间的通信 步骤: 1. 安装 `npm i pubsub-js -S` 2. 接收数据 ```js import PubSub from 'pubsub-js' PubSub.subscribe('消息类型', 回调) ``` 3. 发送数据 ```js import PubSub from 'pubsub-js' PubSub.publish('消息类型', 参数) ``` > 注意: pubsub 在回调当中第一个参数永远是消息类型,从第二个参数开始才是真实传递的参数 # day07 ## userAjax - 案例 github的两个测试接口:获取比较火的仓库及获取用户 接口1: https://api.github.com/search/repositories?q=v&sort=stars 接口2: https://api.github.com/search/users?q=aa --- 1. 静态组件拆分 2. 数据初始化展示 - 没有初始化需要展示的内容 3. 交互 输入内容,点击按钮,携带输入的内容发送请求获取数据,拿到数据之后展示列表 1. 获取到输入的内容 2. 点击按钮,携带上收集到的输入内容,发送请求 发送请求? 请求放在哪个组件好? 请求放到Main组件当中,header组件在点击按钮的时候只提供一个搜索的关键词 * 把输入的内容点击按钮给了Main组件(总线传参) * 拿到keyword数据发请求 发请求,拿数据,展示列表 3. 需要完善交互 - 状态切换 第一进入页面 - 展示欢迎页 在发送请求的过程中 - 展示 '正在加载' 发送完请求的时候,获取到数据展示 获取不多或者报错给提示 ### vue-resource 使用(了解) 使用vue-resource发送请求,但是现在用的不多,了解就好 插件使用步骤: 1. 安装 `npm install vue-resource` 2. 注册使用插件 ```js import Vue from 'vue' import VueResource from 'vue-resource' // 插件,提供了intasll方法 Vue.use(VueResource) // 本质上是在调用插件的install方法 ``` 3. 代码中书写 `this.$http.get()` > 使用方式类似axios,失败的提示信息略有不同 ## 跨域 * 什么是跨域? 违反了同源策略的叫跨域 * 什么是同源策略 协议、域名、端口号相同,就是同源 * 注意: 跨域会发生在哪里? 跨域只会发生在浏览器端,服务端是不会跨域 浏览器发请求的时候跨域,浏览器会发什么请求 * 普通请求 - a标签、form表单 * ajax请求 - 为了局部刷新出现的技术,当前页面不改变,发送网络请求,拿到数据,修改当前页面部分内容 只有ajax请求会发生跨域 为什么请求 github 的api发生了跨域,还能使用? 因为github的服务器处理过来,允许这个接口发生跨域 自己造一个跨域,看看跨域如何诞生 ```js const express = require('express'); const app = express(); app.get('/userinfo', (request, response) => { // 拓展 // 后端解决跨域,设置CORS头 // response.setHeader('Access-Control-Allow-Origin', '*'); const userinfo = { username: '尼古拉斯·赵四', age: 44, intro: '大米饭前吃还是饭后吃' } response.send(userinfo) }) app.listen(3000, () => { console.log('server runing... port 3000'); }); ``` ```js async getUserInfo() { // 跨域 // let res = await axios.get(`http://127.0.0.1:3000/userinfo`) // webpack-dev-serve 代理解决跨域 let res = await axios.get(`/api/userinfo`) } ``` 解决跨域 ```js devServer: { // 看门狗 proxy: { // 标识 '/api': { // 带有 /api 这个标识的就放行,进行转发 target: 'http://127.0.0.1:3000', // 路径重写 - 因为接口的路径中没有/api,而/api是标识,在路径中需要去掉,所以要重写 pathRewrite: { '^/api': '' } } } } ``` ![](assets/webpack代理.png) ## Vuex 1. 状态管理是什么: Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式,是一个官方插件。 它采用集中式存储管理应用的所有组件的状态(数据),并以相应的规则保证状态以一种可预测的方式发生变化。 我们也可以认为它也是一种组件间通信的方式,并且适用于任意组件 2. 理解:对vue应用中多个组件的共享状态(数据)进行集中式的管理(读/写) 3. 为什么要有这个(问题): * **多个视图依赖于同一状态(数据)** * 来自不同视图的行为需要变更同一状态 * 以前的解决办法 a. 将数据以及操作数据的行为都定义在父组件 b. 将数据以及操作数据的行为传递给需要的各个子组件(有可能需要多级传递) * vuex就是用来解决这个问题的 4. 什么时候用: Vuex 可以帮助我们管理共享状态,并附带了更多的概念和框架。这需要对短期和长期效益进行权衡。 也就是说应用简单(组件比较少)就不需要使用(但是可以),如果应用复杂,使用就会带来很大的便捷 ### 使用步骤: 1. 安装 `npm i vuex@3 -S` 2. 使用 ```js import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) ``` 3. 创建并暴露一个 store ```js export default new Vuex.Store() ``` 4. 创建vm实例的时候,关联store(目的是为了可以使用 this.$store) ### 核心概念 * state -> 放数据 * mutations -> 修改 state 中的数据,不能有if、for、异步 * actions -> 调用mutations,可以有if、for、异步,并且是vue和vuex之间桥梁 * getters -> 像vue中计算属性的get方法,计算state中没有的数据 ### 调用方式 调用actions `this.$store.dispatch('actions')` 调用mutations ```js actions: { getData({ commit }) { commit('mutaions') } } ``` mutations修改数据 ```js mutations: { add(state, res) { // res 是传过来的参数 state.data = res; } } ``` > 注意: actions 和 mutaions 调用的传参只能传一个 ### userAjax - Vuex版本 Main组件中所有的数据放到store中,通过修改store中的数据进行页面显示 `src/store/index.js` ```js import axios from 'axios' import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) const state = { isFirst: true, // 第一次进入页面 isLoading: false, // 是否正在加载 errMessage: "", // 保存错误信息 list: [] } // 思路二: 通过请求的状态来设置我们的数据 const mutations = { // 发请求之前的状态 BEFORE_REQUEST(state) { state.isFirst = false; state.isLoading = true; state.errMessage = "" }, // 发请求成功 SUCCESS(state, list) { state.list = list state.isLoading = false }, // 请求失败 FAILED(state, errMsg) { state.errMessage = errMsg state.isLoading = false } } const actions = { async getData({ commit }, keyword) { // 发请求之前 commit('BEFORE_REQUEST') try { let result = await axios.get(`https://api.github.com/search/users?q=${ keyword }`) let res = result.data.items.map(item => ({ id: item.id, username: item.login, avatar_url: item.avatar_url, html_url: item.html_url })) // 请求成功 commit('SUCCESS', res) } catch (error) { // 请求失败 commit('FAILED', error.message) } } } const getters = {} export default new Vuex.Store({ state, mutations, actions, getters, }) ``` > json-server 使用步骤 > > 1. 安装 > > `npm i json-server` > > 2. 终端使用 > > `json-server --watch --port 8000 db.json` > > 此时就在8000端口启动了一个服务,需要当前路径下由 db.json 文件 # day08 ### vuex辅助函数 辅助开发者的函数,因为直接使用this来写太长了,费劲,所以出了辅助函数 mapState 和 mapGetters 映射的内容映射在 computed 当中 mutaions 和 actions 映射的内容映射在 methods 当中 只是我们一般不会映射 mutaions, mutaions 是为了让 actions 去调用的,所以一般不会映射过来 #### 映射state 数组写法 `...mapState(['count'])` 对象写法 ```js ...mapState({ count: 'count' // 属性名是页面中使用,属性值是state中的属性 }), ``` 唯独state有这种特殊的写法,属性值是函数的形式 ```js ...mapState({ count: state => state.count }), ``` #### 映射getters 数组写法 `...mapGetters(['newCount'])` 对象写法 ```js ...mapGetters({ // 属性名是组件中要使用的,属性值直接拿getters中的属性 newCount123: 'newCount' }) ``` #### 映射actions 数组写法 `...mapActions(['increment', 'decrement', 'evenIncreament', 'asyncIncreament']),` 对象写法 ```js ...mapActions({ // 属性名是组件中要使用的,属性值是actions中的属性 incrementAdd: 'increment', decrement: 'decrement' }) ``` 使用映射actions传参 ```js this.incrementAdd(参数); // 这里的参数会在actoions函数的第二个参数接收到 actions: { incrementAdd({ commit }, 参数) { // 这里的参数就是调用actions映射函数的参数传递过来的 } } ``` ### 模块化 我们目前把所有的数据直接放在 state 配置项目中,当页面中用的数据过多的时候,把所有数据都放在一个 state 中进行管理会很混乱 在store当中配置 modules ,配置每一个模块, 模块: ```js export default { state, mutations, actions, getters } ``` store配置 ```js import home from './modules/home' import user from './modules/user' export default new Vuex.Store({ modules:{ home, user } }) ``` 只有state加了一层,其他的mutations和actions和getters使用方式和之前一样 state使用方式 ```js // 直接调用 $store.state.home.data1 // 辅助函数 ...mapState({ data1: state => state.home.data1 }) ``` ### 命名空间 在开启模块化的时候,只是将每个模块的 state 模块了,mutations、actions、getters还是和之前一样,放在大的store中,容易产生命名冲突的问题,如何解决 mutations、actions、getters 中命名冲突问题? 开启命名空间,在模块化的基础上配置 `namespaced: true` 选项即可开启命名空间,开启命名空间之后,每个模块中的 state 和开启模块化的时候一模一样,只是 mutations、actions、getters 相当于变成了一个独立的区域,使用方式和写法也发生了改变 #### state 写法 - 和模块化下的写法一样 > 注意: 使用辅助函数映射的时候只能写对象写法,例如: > > ```js > computed: { > ...mapState({ > count: state => state.home.count, > count: state => state.模块名.数据名 > }) > } > ``` > > 也支持数组写法 > > ```js > ...mapState("user", ["count"]), > ...mapState("模块名", ["数据名"]), > ``` #### actions 写法 ```js // 普通写法 $store.dispatch('模块名/actions方法名') $store.dispacth('home/increment') // 映射写法 methods: { ...mapActions(模块名, ['actions方法名']) ...mapActions('home', ['increment']) } ``` > mutions写法和actions写法保持一直 #### getters 写法 ```js // 普通写法 $store.getters['模块名/getters属性名'] $store.getters['home/tenCount'] // 映射写法 computed: { ...mapGetters('模块名', ['getters属性名']) ...mapGetters('home', ['tenCount']) } ``` 图片说明:蓝色内容是命名空间,红色内容是模块化 ![](assets/vuex模块化和命名空间.png) ![](assets/vuex模块化和命名空间2.png) ## 插槽 什么是插槽? 父子组件之间的一种通信方式,父组件给子组件传html和css 插槽共分为 普通插槽、具名插槽、作用域插槽 ### 普通插槽 #### 子组件 - Child `默认内容,在父祖家没有使用默认插槽的时候渲染` #### 父组件 - App ```html // 简写 组件标签之间的内容会被渲染到子组件的slot标签位置 // 全写 ``` ### 具名插槽 #### 子组件 - Child `` #### 父组件 - App ```html ``` ### 作用域插槽 #### 子组件 - Child `` > :userinfo="userinfo" slot标签的属性(除了name属性)会收集起来变成一个对象,供父组件写插槽内容的时候使用 #### 父组件 - App ```html ``` > 注意: > > 1. v-slot指令有简写,简写成#,v-slot:qwer="scope" ----> #qwer="scope" > 2. 当插槽单独使用的时候,template标签是可以省略的 ## 动态组件渲染 - component * 什么是动态组件 多个组件挂载到同一个组件上,通过参数动态的切换不同组件就是动态组件。 * 书写形式: `` 这里的 componentName 可以是: * **已注册组件的名字** * **一个组件的配置对象** * 内置组件: component:是vue里面的一个内置组件。 > vue内置的组件还包括: > transition、keep-alive ... 案例: * 已注册的组件名: ```html ``` * 一个组件的配置对象 ```html ``` ### 动态加载某一目录下的所有组件 - 拓展 在 webpack 的依赖管理中,提供了一个方法 `require.context()` 来创建一个上下文环境(可以理解为某一目录下的所有文件组成的一个容器),使用方式如下: ```js const context = require.context('./components', false, /\.vue$/) // require.context() // 功能: 创建一个上下文环境 // 参数: // 参数一: 要检索的目标路径 // 参数二: 是否对子目录进行检索 // 参数三: 正则,用来匹配目标文件 // 返回值: 函数 // 上下文环境函数提供了一个keys方法,通过调用该方法,可以拿到当前上下文环境中所有的文件路径组成的数组 // context.keys() context.keys().forEach(path => { // 上下文环境函数调用,将检索出来的文件路径传入,可以得到上下文环境中的一个文件详细内容 // 这个详细内容会作为一个js模块来解析展示 const module = context(path) }) ``` 案例: ```html ``` ## 路由 * vue-router 是什么? 是vue官方的一个路由插件,专门用来实现一个SPA应用 基于vue的项目基本都会用到此库(vuex、vue-router 这两个插件应用比较广泛) * 单页Web应用(single page web application,SPA),例如:掘金 * 整个应用只有一个完整的页面(这个完整的页面,由多个组件组成) * 点击页面中的链接不会刷新页面, 本身也不会向服务器发普通请求(a、form表单) * 当点击路由链接时, 只会做页面的局部更新(组件切换) * 切换过来之后,数据都需要通过ajax请求获取, 并在前端异步展现 * 什么是路由? 一种映射关系,一个key-value的映射关系 路由 分为前端路由 和 后端路由 ### 前端路由: ```js { path: '/home', component: Home } ``` ### 后端路由 ```js app.get('/userinfo', function (req, res) {}) ``` ## 路由的使用步骤: 1. 安装 `npm i vue-router@3 -S` 2. 引入使用 ```js import Vue from 'vue' import VueRouter from 'vue-router' Vue.use(VueRouter) ``` 3. 创建路由器并暴露 `export default new VueRouter({ ... })` 4. 在创建vm实例的实例的时候关联路由器 ```js import router from '@/router' new Vue({ ..., router }) ``` ![](assets/router和route.png) ### 一级路由拆分 1. 定义 - 创建一个.vue文件 2. 注册 - 在路由器中注册路由 ```js export default new VueRouter({ routes: [ { path: '/home', component: Home }, { path: '/about', component: About }, { path: '/', redirect: '/home' } ] }) ``` 3. 使用 * 点击的位置 - 需要使用 router-link 标签 * 展示的位置 - router-view ### 二级路由拆分 * Message * News ```js { path: '/home', component: Home, children: [ { // path: '/home/message', path: 'message', // 简写 component: Message }, { path: 'news', // 简写 component: News }, { path: '', // 不写的话匹配到的就是 /home redirect: 'message' } ] }, ``` ### 三级路由拆分 * Message -> MsgDetail 传参:传参通过url去传的,两中传参形式,param 和 query 目标url: /home/message/msgdetail/1001?content=高圆圆 其中 1001 是消息id params传参,content=高圆圆 是消息内容 query传参 1. 首先跳转的时候要拼接出这个url `` 2. 路由中解析参数 ```js { path: 'msgdetail/:msgId', // params 占位 component: MsgDetail } ``` 3. 组件接参数: ```html
{{ $route.params.msgId }}
{{ $route.query.content }}
``` ### router-link 的三种写法 ```html ``` ### 路由的props参数映射 当路由配置了 props 之后,参数会映射到组件当中,在组件中使用props接收参数即可使用 ```js { name: 'msgdetail', path: 'msgdetail/:msgId', component: MsgDetail, // 布尔值: 只能传params参数 props: true, // 对象形式 - 可以传递额外的参数 props: { text: '哈哈哈' }, // 函数形式 - 都可以传 props: (route) => { // route 是当前的路由对象 $route return { msgId: route.params.msgId, content: route.query.content, text: '额外参数' } } } ``` * News -> NewsDetail 声明式导航 - route-link标签 编程式导航 - $router.push() 编程式导航也是三种写法,和 route-link 中的 to 一样 * $router.back() -- 后退 * $router.push() -- 有历史记录 * $router.replace() -- 没有历史记录 匹配数据 - $route 是可以被监听到的 ```js watch: { $route: {}, // 监听某一个对象中的某一个集体属性值,可以使用下面这种监听 "$route.params.newId": {} } ``` ### keep-alive 缓存组件不被销毁 * include - 配置的组件不会被销毁 * exclude - 配置的会被销毁 * max - 最大缓存数 写法: ```html 字符串写法 数组写法 同时支持正则,不讨论 ``` #### 组件配置项 name 作用 1. 在 keep-alive 中的 include 和 exclude 中配置使用 2. 浏览器的 dev-tools 中可以搜索到组件 3. 组件注册 Vue.componet(Home.name, Home) ### 重复点击 编程式导航 报错 ```js const originPush = VueRouter.prototype.push // 存一下之前VueRouter的push方法 VueRouter.prototype.push = function (localtion) { return originPush.call(this, localtion).catch(() => {}) } const originReplace = VueRouter.prototype.replace // 存一下之前VueRouter的replace方法 VueRouter.prototype.replace = function (localtion) { return originReplace.call(this, localtion).catch(() => {}) } ``` ### hash 和 history 路由差别 路由有两种模式,hash模式 和 history模式(router 默认不配置该选项的时候就是 hash) * hash 模式就是路径中带有 # 这种形式 http://localhost:8080/#/home/news hash模式一般在开发环境使用,生产环境很少使用,几乎不用 ```js export default new VueRouter({ mode: 'hash', ... }) ``` * history 一般在上线的时候会使用 http://localhost:8080/home/news 开发的时候使用刷新样式丢掉了,样式丢掉是路径找不对的问题,如何解决 解决: 添加配置 devServer添加: historyApiFallback: true, // 任意的 404 响应都被替代为 index.html - 现在默认配置上,脚手架3版本之前是没有的 output添加: publicPath: '/', // 引入打包的文件时路径以/开头 - 现在默认配置上,脚手架3版本之前是没有的 修改 index.html中引入的css由 ./ 相对路径变成 / 绝对路径 ```js export default new VueRouter({ mode: 'history', ... }) ``` > 注意:history 模式在上线的时候需要特殊处理,到时候细说 ### $set 当初始化的data数据中没有某一条数据的时候,而在页面交互的时候,需要添加一个新的数据,让新的数据具有响应式,用 `$set` (本质上 $set 就是给数据走了一遍数据劫持,让数据具有了响应式) ```js this.$set(this.userinfo, 'intro', '吃饺子能不能蘸醋') // 功能: 添加响应式数据 // 参数: // 参数一: 想要添加数据的对象 // 参数二: 想要添加的属性 // 参数三: 想要添加属性的值 // 返回值: 想要添加属性的值 ``` ### scoped 作用:将样式限制在当前组件和子组件的根标签上 做了什么事? 1. 把当前组件的所有的css样式都加了 data-v-xxx 这个属性 xxx是hash值,是唯一的,整个项目都是唯一的,作为了标签的属性 例如: ```css h2[data-v-xxx] { color: red } ``` 2. 将当前组件的html标签和子组件的根标签加了 data-v-xxx 这个属性 ```css h2[data-v-xxx] { color: red } // 只有满足h2标签和data-v-xxx的才会采用 color: red 这个样式 // 而拥有data-v-xxx这个属性的只在当前组件和子组件的根标签 ``` ![](../230222vuebase/assets/scoped-1.png) ![](../230222vuebase/assets/scoped-2.png) 拓展内容: ## Render函数 介绍 ```js new Vue({ render: h => h(App) }).$mount('#app') ``` 在main.js中我们见到的render配置项,配置了一个函数,这个函数叫做render函数 render函数中有一个参数 h, 这个h函数还有一个名字: createElement, 调用这个h函数生成了虚拟DOM,并且这个函数的返回值作为了render函数的返回值 注意: 在.vue文件中的所有内容,都会最终转化成render函数,而render函数比模板写法更接近编译器 也就是说: 我们写的组件编译过程如下 `.vue文件的内容 -> 编译成render函数 -> 编译出VNode(虚拟DOM) -> 编译出真实DOM` 而我们如果使用 render 函数渲染的话,整个编译过程将变成 `render函数 -> 编译出VNode(虚拟DOM) -> 编译出真实DOM` 这样将省略一个步骤,效率更高,这也解释了我们为什么要慢慢学习render函数 为什么说是慢慢学习render函数(同问题: 为什么尤雨溪设计vue的时候,不摒弃.vue文件,直接使用render函数呢)? 因为render函数对于初学者而言并不友好,在没有深入理解过vue的设计思路和模式的时候,直接让开发者上手render函数,对于vue框架的推广会造成很大阻力 > 其实对于绝大多数场景来说,书写 template 方式都可以得到满足,且性能不错,但是对于某一些特殊场景而言render函数更合适(后续再说) ### render 用法 render函数中可以写逻辑,render的返回值是 `createElement` 函数的返回值,所以我们重点研究 `createElement` 函数 ``` createElement: 第一个参数是标签名类型必须是 String 或 组件配置项 第二个是属性值 我们后面来讲,类型是Object 第三个是子级虚拟节点 (VNodes) 可以是 String|Array ``` #### createElement 参数二具体说明 ```js { // 与 `v-bind:class` 的 API 相同, // 接受一个字符串、对象或字符串和对象组成的数组 'class': { foo: true, bar: false }, // 与 `v-bind:style` 的 API 相同, // 接受一个字符串、对象,或对象组成的数组 style: { color: 'red', fontSize: '14px' }, // 普通的 HTML attribute attrs: { id: 'foo' }, // 组件 prop props: { myProp: 'bar' }, // DOM property domProps: { innerHTML: 'baz' }, // 事件监听器在 `on` 内, // 但不再支持如 `v-on:keyup.enter` 这样的修饰器。 // 需要在处理函数中手动检查 keyCode。 on: { click: this.clickHandler }, // 仅用于组件,用于监听原生事件,而不是组件内部使用 // `vm.$emit` 触发的事件。 nativeOn: { click: this.nativeClickHandler }, // 自定义指令。注意,你无法对 `binding` 中的 `oldValue` // 赋值,因为 Vue 已经自动为你进行了同步。 directives: [ { name: 'my-custom-directive', value: '2', expression: '1 + 1', arg: 'foo', modifiers: { bar: true } } ], // 作用域插槽的格式为 // { name: props => VNode | Array } scopedSlots: { default: props => createElement('span', props.text) }, // 如果组件是其它组件的子组件,需为插槽指定名称 slot: 'name-of-slot', // 其它特殊顶层 property key: 'myKey', ref: 'myRef', // 如果你在渲染函数中给多个元素都应用了相同的 ref 名, // 那么 `$refs.myRef` 会变成一个数组。 refInFor: true } ``` ### Render Demo ##### render基本使用 ```js export default { render(createElement) { //
我爱你
return createElement('div', {}, '我爱你') //
// 我爱你 // 高圆圆 //
return createElement('div', {}, [ createElement('span', {}, '我爱你'), createElement('span', {}, '高圆圆'), ]) } } ``` ##### v-html 和 v-text 直接渲染DOM相关内容 ```js export default { data() { return { msg: '我爱你', message: '我爱你', } }, render(createElement) { return createElement('div', { domProps: { innerText: this.msg, innerHTML: this.message } }) } } ``` ##### v-bind:class ```js export default { render(createElement) { return createElement('div', { // class: 'bgRed', // class: ['bgRed', 'f30'], class: { bgRed: true, // bgRed bgGreen bgBlue f30: this.isF30 // 布尔值 } }, '哈哈') } } ``` ##### 事件 ```js export default { render(createElement) { return createElement('button', { on: { click: (e) => { console.log(e, this) } } }, '点击我试试') } } ``` ##### 需求: 点击按钮,切换div显示(事件 和 v-if) ```js export default { data() { return { isShow: false } }, render(createElement) { if (this.isShow) { return createElement('div', {}, [ createElement('button', { on: { click: (e) => { this.isShow = !this.isShow } } }, '切换'), createElement('div', { class: { bgRed: true, f30: this.isF30 } }, '哈哈') ]) } else { return createElement('div', {}, [ createElement('button', { on: { click: (e) => { this.isShow = !this.isShow } } }, '切换') ]) } } } ``` ##### v-for展示li列表 ```js export default { data() { return { list: [ { id: 10100, content: '抽烟' }, { id: 10200, content: '喝酒' }, { id: 10300, content: '烫头' }, ], } }, render(createElement) { return createElement('ul', {}, [ (this.list.map(item => createElement('li', {}, item.content))) ]) } } ``` ##### v-model ```js export default { data() { return { keyword: '' } }, render(createElement) { return createElement('input', { attrs: { value: this.keyword }, on: { input: e => { this.keyword = e.target.value } } }) } } ``` ##### v-show ```js export default { render(createElement) { data() { return { isShow: false } }, return createElement('div', {}, [ createElement('button', { on: { click: (e) => { this.isShow = !this.isShow } } }, '切换'), createElement('div', { directives: [ { name: 'show', value: this.isShow } ] }, '哈哈') ]) } } ``` ##### 自定义指令 ```js export default { data() { return { text: 'I Love You', } }, directives: { upper(el, binding) { el.innerHTML = binding.value.toUpperCase() } }, render(createElement) { return createElement('div', { directives: [ { name: 'upper', value: this.text } ] }) } } ``` ##### 自定义过滤器 > this.$options 可以拿到当前的配置项 ```js export default { filters: { addTen(arg) { return arg + 10 } }, render(createElement) { return createElement('div', {}, this.$options.filters.addTen(this.num)) } } ``` ##### ref ```js export default { mounted() { console.log(this.$refs.myRef) }, render(createElement) { return createElement('div', { ref: 'myRef' }) } } ``` ##### 组件 props 传参 ```js // 父组件 import MyComp from '@/components/MyComp' export default { render(createElement) { return createElement(MyComp, { props: { title: this.msg, content: this.text } }) } } ``` ```js // MyComp子组件 export default { props: { title: String, content: String }, render(createElement) { return createElement("div", {}, [ createElement('h5', {}, this.title), createElement('div', {}, this.content), ]) } } ``` ##### 组件自定义事件(点击按钮修改父组件传递的数据) ```js // 父组件 import MyComp from '@/components/MyComp' export default { render(createElement) { return createElement(MyComp, { props: { title: this.msg, content: this.text }, on: { changeMsg: (text) => { this.msg += text } }, }) } } ``` ```js // MyComp子组件 export default { props: { title: String, content: String }, render(createElement) { return createElement("div", {}, [ createElement('button', { on: { click: (e) => { this.$emit('changeMsg', '燕子') } } }, '修改父组件传过来的title'), createElement('h5', {}, this.title), createElement('div', {}, this.content), ]) } } ``` ##### 插槽 ###### 默认插槽 ```js // 父组件 import MyComp from '@/components/MyComp' export default { render(createElement) { return createElement(MyComp, {}, [ createElement('div', {}, '哈哈') ]) } } ``` ```js // MyComp子组件 export default { render(createElement) { return createElement("div", {}, [ createElement('h5', {}, '测试标题'), this.$slot.default ]) } } ``` ###### 具名插槽 ```js // 父组件 import MyComp from '@/components/MyComp' export default { render(createElement) { return createElement(MyComp, {}, [ createElement('div', {}, '哈哈'), createElement('div', { slot: 'qwer', }, '呵呵'), ]) } } ``` ```js // MyComp子组件 export default { render(createElement) { return createElement("div", {}, [ createElement('button', {}, '按钮'), createElement('h5', {}, '测试标题'), this.$scopedSlots.default(), createElement('div', {}, '测试文本'), this.$scopedSlots.qwer() ]) } } ``` ###### 作用域插槽 ```js // 父组件 import MyComp from '@/components/MyComp' export default { data() { return { msg: '我爱你', message: '我爱你', } }, render(createElement) { return createElement(MyComp, { props: { title: this.msg, content: this.text }, scopedSlots: { qwer: props => createElement('em', {}, props.intro) } }, [ createElement('strong', {}, 'so easy!!') ]) } } ``` ```js // MyComp子组件 export default { props: { title: String, content: String }, data() { return { userinfo: { username: '贾玲', age: 21 }, intro: '喜剧演员' } }, render(createElement) { return createElement("div", {}, [ createElement('button', {}, '按钮'), createElement('h5', {}, this.title), this.$scopedSlots.default(), createElement('div', {}, this.content), this.$scopedSlots.qwer({ intro: this.intro }) ]) } } ``` ### render案例 使用render函数写slot_page案例(自己下去写) ### Render函数 - JSX 地址: https://v2.cn.vuejs.org/v2/guide/render-function.html#JSX 1. 安装bable的包 ``` npm install @vue/babel-preset-jsx @vue/babel-helper-vue-jsx-merge-props ``` 2. 修改 babel.config.js 文件 ```js module.exports = { presets: [ // '@vue/cli-plugin-babel/preset' // cli默认的插件 '@vue/babel-preset-jsx' // 手动安装支持jsx插件 ] } ``` 3. render函数中使用 jsx 即可 ```jsx export default { data() { return { count: 2 } }, methods: { clickHandler(e, num) { console.log(e) console.log(num) this.count++ } }, render() { return
我爱你,高圆圆
爱了 { this.count } 次

} } ``` ---------------- ##### 关于快捷键 ```js 选中多行单词 alt + 双击鼠标左键 选中多行 alt + 单击鼠标左键 按下滚轮键上下拖拽 选中代码上下移动 alt + up alt + down 代码的缩进 往后退 ---- tab键 往前推 ---- shift + tab Live server 快速启动 alt + l alt + o (alt不松手) vscode 启动终端快捷键 ctrl + ` vscode 侧边栏快捷键 ctrl + b ```