sxms

前端基础

1.h5的新特性?css3的新特性?

H5新特性

  1. 拖拽释放api
  2. 自定义属性data-id
  3. 语义化标签
  4. audio、video
  5. canvas
  6. 地理api
  7. localStorage、sessionStorage
  8. 表单控件
  9. webworker、websocket

css3新特性

  1. rgba
  2. border-radius
  3. 盒子模型
  4. 线性渐变
  5. 过渡
  6. 动画
  7. flex
  8. 字体图标

2.盒子水平垂直居中

  1. 绝对定位 + transform:translate(-50%,-50%)
  2. flex布局

3.css选择器优先级

!Important>行内样式>ID 选择器>类选择器>标签>通配符>继承>浏览器默认属性

4.rem的理解

rem 是 CSS3 新增的一个相对单位(root em,根 em),使用 rem 为元素设定字体大小时,仍然是相对大小,但相对的只是 HTML 根元素。

手淘方案:

  1. 拿到设计稿除以 10,得到 font-size 基准值
  2. 引入 flexible
  3. 设计稿 px/ font-size 基准值,即可换算为 rem

5.position

  1. static 静态定位(默认值):不脱离文档流
  2. absolute 绝对定位 :找最近一级带有定位的父级元素进行移动 脱离文档流
  3. relative 相对定位
  4. fixed 固定定位 脱离文档流 参照物浏览器窗口
  5. sticky 粘性定位

6.浮动 float

浮动将元素排除在普通流之外,即元素将脱离文档流,不占据空间。浮动元素碰到包含它的边界或者浮动元素的边界停留。

1、子元素浮动后,不占位置,父元素的高度无法被撑开,影响与父元素同级的元素;

2、与浮动元素同级的非浮动元素(内联元素)会跟随其后;

3、若非第一个元素浮动,则该元素之前的元素也需要浮动,否则会影响页面显示的结构解决方法

清除浮动

  1. 额外标签法 clear:both
  2. 双伪元素清除法
  3. 单伪元素清除法
  4. 构建BFC 为父元素添加 overflow: hidden 溢出隐藏
  5. 定高法

7. 伪类和伪元素

伪类:用于已有元素处于某种状态时为其添加对应的样式,这个状态是根据用户行为而动态变化的; :hover

伪元素:用于创建一些不在DOM树中的元素,并为其添加样式 ; 例如,我们可以通过 :before :after

8.css预处理器

Less、Sass、Stylus,用来预编译Sass或less,增强了css代码的复用性,还有层级、mixin、变量、循环、函数等,具有很方便的UI组件模块化开发能力,极大的提高工作效率

9.vw适配

  • 开发者拿到设计稿(假设设计稿尺寸为750px,设计稿的元素标注是基于此宽度标注)
  • 开始开发,对设计稿的标注进行转换,把px换成vw。比如页面元素字体标注的大小是32px,换成vw为 (100/750)*32 vw
  • 对于需要等比缩放的元素,CSS使用转换后的单位
  • 对于不需要缩放的元素,比如边框阴影,使用固定单位px

10.如何解决 margin“塌陷”?

外边距塌陷共有两种情况:

第一种情况:两个同级元素,垂直排列,上面的盒子给 margin-bottom 下面的盒子给margin-top,那么他们两个的间距会重叠,以大的那个计算。解决这种情况的方法为:两个外边距不同时出现

第二种情况:两个父子元素,内部的盒子给 margin-top,其父级也会受到影响,同时产生上边距,父子元素会进行粘连。

解决方案:

1、为父盒子设置 border,添加 border 后父子盒子就不是真正意义上的贴合(可以设置成透明:border:1px solid transparent);

2、为父盒子添加 overflow:hidden;

3、为父盒子设定 padding 值;

4、为父盒子添加 position:fixed;

5、为父盒子添加 display:table;

11. 通过 CSS 的哪些方式可以实现隐藏页面上的元素?

方式 说明
opacity: 0 通过将元素的透明度设置为0,实现看起来隐藏的效果;但是依然会占用空间并可以进行交互
visibility: hidden 与透明度为0的方案非常类似,会占据空间,但不可以进行交互
overflow: hidden 只会隐藏元素溢出的部分;占据空间且不可交互
display: none 可以彻底隐藏元素并从文档流中消失,不占据空间也不能交互,且不影响布局
z-index: -9999 通过将元素的层级置于最底层,让其他元素覆盖住它,达到看起来隐藏的效果
transform: scale(0,0) 通过将元素进行缩放,缩小为0;依然会占据空间,但不可交互,只是一个视觉效果,不会影响其它盒子的布局。
left: -9999px 通过将元素定位到屏幕外面,达到看起来看不到的效果

12. 如何理解 z-index?

可以将它看做三维坐标系中的z轴方向上的图层层叠顺序。

元素默认的 z-index 为 0,可通过修改 z-index 来控制设置了postion 值的元素的图层位置。

z-index的小坑, 如果父辈元素有定位, 且配置了z-index, 优先按照父辈元素的定位的z-index进行比较层级,当前盒子的z-index层级只是在父元素里面的层级

13. 谈谈你对 flex 的理解?

在真实的应用场景中,通常会遇到各种各样不同尺⼨和分辨率的设备,为了能在所有这些设备上正常的布局我们的应用界面,就需要响应式的界⾯设计方式来满⾜这种复杂的布局需求。

flex 弹性盒模型的优势在于开发⼈员只需要声明布局应该具有的⾏为,⽽不需要给出具体的实现⽅式,浏览器负责完成实际布局,当布局涉及到不定宽度,分布对⻬的场景时,就要优先考虑弹性盒布局。

你能联想到的flex语法有哪些呢?

flex-direction: 调整主轴方向

1
2
3
4
row:主轴方向为水平向右
column:主轴方向为竖直向下
row-reverse:主轴方向为水平向左
column-reverse:主轴方向是竖直向上。

justify-content主要用来设置主轴方向的对齐方式

1
2
3
4
5
flex-start: 弹性盒子元素将向起始位置对齐
flex-end: 弹性盒子元素将向结束位置对齐。
center: 弹性盒子元素将向行中间位置对齐
space-around: 弹性盒子元素会平均地分布在行里
space-between:第一个贴左边,最后一个贴右边,其他盒子均分,保证每个盒子之间的空隙是相等的。

align-items用于调整侧轴的对齐方式

1
2
3
4
flex-start: 元素在侧轴的起始位置对齐。 
flex-end: 元素在侧轴的结束位置对齐。
center: 元素在侧轴上居中对齐。
stretch: 元素的高度会被拉伸到最大(不给高度时, 才拉伸)。

flex-wrap属性控制flex容器是单行或者多行,默认不换行

1
2
nowrap: 不换行(默认),如果宽度溢出,会压缩子盒子的宽度。
wrap: 当宽度不够的时候,会换行。

align-content用来设置多行的flex容器的排列方式

1
2
3
4
5
6
flex-start: 各行向侧轴的起始位置堆叠。 
flex-end: 各行向弹性盒容器的结束位置堆叠。
center: 各行向弹性盒容器的中间位置堆叠。
space-around: 各行在侧轴中平均分布。
space-between: 第一行贴上边,最后一个行贴下边,其他行在弹性盒容器中平均分布。
stretch:拉伸,不设置高度的情况下。

可参考 flex布局教程


JavaScript

1. let var const的区别?

var ES5变量声明方式

  1. 在变量未赋值时,变量undefined(为使用声明变量时也为undefined)
  2. 作用域 var的作用域为方法作用域;只要在方法内定义了,整个方法内的定义变量后的代码都可以使用

let ES6变量声明方式

  1. 在变量为声明前直接使用会报错
  2. 作用域 let为块级作用域 通常let比var范围要小
  3. let禁止 重复声明变量,否则会报错;var可以重复声明

const ES6变量声明

  1. const为常量声明方式;声明变量时必须初始化,在后面出现的代码中不能再修改常量的值
  2. const实际上保证的,并不是变量的值不得改动,而时变量指向的哪个内存地址不得改动

2. js数据类型,区别

基本数据类型:

number,string,boolean,null,undefined,symbol,bigint

引用数据类型:

object,function

object:普通对象,数组对象,正则对象,日期对象,math数学函数对象。

(NaN 是一个数值类型,但不是一个具体的数字。)

3.slice 是干嘛的、splice是否会改变原数组?

  1. slice 是来截取的 参数可以写 slice(3)、slice(-3)、slice(1,3) 返回的是一个新的数组

  2. splice 功能有:插入、删除、替换

    返回:删除的值

    该方法会改变原数组

4.数组去重

1
2
3
4
5
6
const arr = [1,2,3,2]

function unique(arr) {
return [...new Set(arr)]
}
console.log(unique(arr))

5. Javascript 创建对象的几种方式?

  1. 简单对象的创建 使用对象字面量的方式{}
1
const Cat = {};
  1. new 一个function
1
2
3
function Person(){
}
const personOne=new Person();
  1. 使用工厂方式来创建(Object 关键字)
1
const wcDog =new Object();
  1. 使用 Object.create() 创建对象(使用现有对象作为原型)
1
const person = Object.create(anotherPerson);
  1. 使用 ES6 中的类(Class)创建对象(其实质还是使用构造函数):
1
2
3
4
5
6
class Person {
constructor(name) {
this.name = name;
}
}
const person = new Person('John');

6. 如何区分数组和对象?

  1. 通过 ES6 中的 Array.isArray
1
2
Array.isArray([]) //true
Array.isArray({}) //false
  1. 通过 instanceof 来识别
1
2
[] instanceof Array //true
{} instanceof Array //false
  1. 通过调用 constructor 来识别
1
2
{}.constructor //返回 object
[].constructor //返回 Array
  1. 通过 Object.prototype.toString.call 方法来识别
1
2
Object.prototype.toString.call([]) //["object Array"]
Object.prototype.toString.call({}) //["object Object"]

7. 作用域和作用域链

作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离
变量,不同作用域下同名变量不会有冲突。

全局作用域 和 局部作用域(分为 函数作用域 和 块级作用域)

  1. ES6 之前 JavaScript 没有块级作用域,只有全局作用域和函数作用域。
  2. ES6 的到来,为我们提供了‘块级作用域’,可通过新增命令 let 和 const 来体现

什么是作用域链?

作用域本质上是底层的变量查找机制。

在函数被执行时,会优先查找当前函数作用域中查找变量,如果当前作用域查找不到则会依次逐级查找父级作用域直到全局作用域。

总结:

  1. 嵌套关系的作用域串联起来形成了作用域链
  2. 相同作用域链中按着从小到大的规则查找变量
  3. 子作用域能够访问父级作用域,父级作用域无法访问子级作用域
  • 当代码在一个环境中执行时,会创建变量对象的一个作用域链

  • 由子级作用域返回父级作用域中寻找变量,就叫做作用域链

  • 作用域链中的下一个变量对象来自包含环境,也叫外部环境。而再下一个变量对象则来自中的最后一个对象

  • 作用域链前端始终都是当前执行的代码所在环境的变量对象,如果环境是函数,则将其活动对象作为变量对象

如何延长作用域链?

执行环境的类型只有两种,全局和局部(函数)。但是有些语句可以在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除具体来说就是执行这两个语句时,作用域链都会得到加强

  1. try - catch 语句的 catch 块;会创建一个新的变量对象,包含的是被抛出的错误对象的声明

  2. with 语句。with 语句会将指定的对象添加到作用域链中

8. map 和 forEach 的区别?

相同点:

  1. 都是循环遍历数组中的每一项
  2. 每次执行匿名函数都支持三个参数,参数分别为item(当前每一项),index(索引值),
    arr(原数组)
  3. 匿名函数中的this都是指向window
  4. 只能遍历数组

不同点:

  1. map()会分配内存空间存储新数组并返回,forEach()不会返回数据。
  2. forEach()允许callback更改原始数组的元素。map()返回新的数组。

9. js遍历对象的方法?

  1. for...in 循环
  2. Object.keys() 方法 获取对象中所有的键 对应 object.value
  3. Object.entries() 方法

10. new操作符具体干了什么呢?

  1. 创建新对象
  2. 构造函数this指向新对象
  3. 执行构造函数代码,修改this,添加新的属性
  4. 返回新对象

11.类数组转换为数组

  • 使用 Array.from()
  • 使用 Array.prototype.slice.call()
  • 使用 Array.prototype.forEach() 进行属性遍历并组成新的数组

12.简单说说 js 中有哪几种内存泄露的情况

  1. 意外的全局变量;
  2. 闭包;
  3. 未被清空的定时器;
  4. 未被销毁的事件监听;
  5. DOM 引用;

13. promise和 async await 区别?

概念
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强
大,简单地说,Promise好比容器,里面存放着一些未来才会执行完毕(异步)的事件的结果,而
这些结果一旦生成是无法改变的
async await也是异步编程的一种解决方案,他遵循的是Generator 函数的语法糖,他拥有内置执
行器,不需要额外的调用直接会自动执行并输出结果,它返回的是一个Promise对象
两者的区别

  1. Promise的出现解决了传统callback函数导致的“地域回调”问题,但它的语法导致了它向纵向
    发展行成了一个回调链,遇到复杂的业务场景,这样的语法显然也是不美观的。而async
    await代码看起来会简洁些,使得异步代码看起来像同步代码,await的本质是可以提供等同
    于”同步效果“的等待异步返回能力的语法糖,只有这一句代码执行完,才会执行下一句。
  2. async await与Promise一样,是非阻塞的。
  3. async await是基于Promise实现的,可以说是改良版的Promise,它不能用于普通的回调函
    数。

14. defer和async区别?

  • defer要等到整个页面在内存中正常渲染结束(DOM结构完全生成,以及其他脚本执行完成),才会执行。多个defer脚本会按照它们在页面出现的顺序加载。==“渲染完再执行”==
  • async一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。多个async脚本是不能保证加载顺序的。==“下载完就执行”==

延迟加载有哪些js方式?

  1. defer : 等 html 全部解析完成,才会执行 js 代码,顺次执行 js 代码。
  2. async:async 和 html 解析是同步的(一起的),不是顺次执行 js 脚本(谁先加载完谁先执行)

15. 同步和异步

同步

  • 指在 主线程上排队执行的任务,只有前一个任务执行完毕,才能继续执行下一个任务。
  • 也就是调用一旦开始,必须这个调用 返回结果(划重点——)才能继续往后执行。程序的执行顺序
    和任务排列顺序是一致的。

异步

  • 异步任务是指不进入主线程,而进入 任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程。
  • 每一个任务有一个或多个 回调函数。前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行。
  • 程序的执行顺序和任务的排列顺序是不一致的,异步的。
  • 我们常用的setTimeout和setInterval函数,Ajax都是异步操作。

16. null 和 undefined 的区别

undefined

  1. 声明了一个变量,但没有赋值
  2. 访问对象上不存在的属性
  3. 函数定义了形参,但没有传递实参
  4. 使用 void 对表达式求值

null

看过作者文献:最初设计js的时候借助了java的语言。

null 会被隐式的转换为 0,很不容易发现错误。

先有null 后有 undefined 是为了填补之前的坑。

具体区别:js 的最初版本是这样区分的,null 是一个表示 “无” 的对象,转为数值时为 0 值,转为数值才为 NaN。

  1. null是一个空值,表示无的对象
  2. null 有属于自己的类型 Null,而不属于Object类型
  3. 二进制的前三位为 0 会被 typeof 判断为对象类型

17. call appy bind的作用和区别?

作用:

都可以改变函数内部的this指向

区别点:

  1. call 和 apply 会调用函数,并且改变函数内部this指向。
  2. call 和 apply 传递的参数不一样,call 传递参数arg1,arg2…形式 apply 必须数组形式[arg]
  3. bind 不会调用函数,可以改变函数内部this指向

应用场景

  1. object.prototype.toString.call() 检测数据类型
  2. apply 经常和数组有关系,比如借助于数学对象实现数组的最大值最小值
  3. 比如改变定时器内部 的this 指向

18. this指向(普通函数、箭头函数)

  1. 谁调用了函数或者方法,那么这个函数或者对象中的this就指向谁
  2. 匿名函数中的this:匿名函数的执行具有全局性,则匿名函数中的this指向是window,而不是调用该匿名函数的对象

箭头函数中的this

  • 箭头函数中的this是在函数定义的时候就确定下来的,而不是在函数调用的时候确定的
  • 箭头函数中的this指向父级作用域的执行上下文;
  • 箭头函数无法使用apply、call和bind方法改变this指向,因为其this值在函数定义的时候就被确定下来

19. 继承

继承一些属性构造的过程和方法

继承的好处

a:提高了代码的复用性

b:提高了代码的维护性

c:让类与类之间产生了关系,是多态的前提

继承的弊端

类的耦合性增强了,但是开发的原则:高内聚,低耦合

  1. 构造函数继承
  2. 原型链继承
  3. 原型式继承
  4. 组合继承
  5. 寄生式继承
  6. 寄生组合式继承
  7. class extends 继承

20. promise

promise的设计之初就是为了解决回调地狱的问题的。

本意是承诺,这个承诺一旦从等待状态变成为其他状态就永远不能更改状态了

Promise的三种状态

  • Pending—-Promise对象实例创建时候的初始状态

  • Fulfilled—-可以理解为成功的状态

  • Rejected—-可以理解为失败的状态

1
2
3
4
5
6
7
8
9
10
let p = new Promise((resolve, reject) => {
reject('reject')
resolve('success')//无效代码不会执行
})
p.then( value => {
console.log(value)
},
reason => {
console.log(reason)//reject
})

当我们在构造 Promise 的时候,构造函数内部的代码是立即执行的

1
2
3
4
5
new Promise((resolve, reject) => {
console.log('1')
resolve('success')
})
console.log('2')

promise的链式调用

如果上一个.then中返回一个新的promise对象,则可以交给下一个.then继续处理。

  • 每次调用返回的都是一个新的Promise实例(这就是then可用链式调用的原因)
  • 如果then中返回的是一个结果的话会把这个结果传递下一次then中的成功回调
  • 如果then中出现异常,会走下一个then的失败回调
  • 在 then中使用了return,那么 return 的值会被Promise.resolve() 包装
  • then中可以不传递参数,如果不传递会透到下一个then中
  • catch 会捕获到没有捕获的异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Promise.resolve(1)
.then(res => {
console.log(res)
return 2 //包装成 Promise.resolve(2)
})
.catch(err => 3)
.then(res => console.log(res))

ajax(url).then(res => {
console.log(res)
return ajax(url1)
}).then(res => {
console.log(res)
return ajax(url2)
}).then(res => console.log(res))

存在一个缺点:无法取消promise,错误需要通过回调函数捕获

场景:

  1. 异步请求
  2. 定时器
  3. 并行异步操作 当需要同时进行多个异步操作,并在它们都完成后执行一些任务时,可以使用 Promise.all 方法。

promise静态常用的方法

  1. promise.all([promise1,promise2…]) 等待机制 等待完才会走 .then
  2. promise.race 赛跑机制

21、JavaScript 内置的常用对象有哪些?

对象及方法

Arguments 函数参数集合

Arguments[ ] 函数参数的数组

Arguments 一个函数的参数和其他属性

Arguments.callee 当前正在运行的函数

Arguments.length 传递给函数的参数的个数

Array 数组

length 属性

join() 将一个数组转成字符串。返回一个字符串。

reverse() 将数组中各元素颠倒顺序

delete 运算符 只能删除数组元素的值,而所占空间还在,总长度没变(arr.length)。

shift() 删除数组中第一个元素,返回删除的那个值,并将长度减 1。

pop() 删除数组中最后一个元素,返回删除的那个值,并将长度减 1。

unshift() 往数组前面添加一个或多个数组元素,长度要改变。arrObj.unshift(“a” ,“b,“c”)

push() 往数组结尾添加一个或多个数组元素,长度要改变。arrObj.push(“a” ,“b”,“c”)

concat( ) 连接数组

slice( ) 返回数组的一部分

sort( ) 对数组元素进行排序

splice( ) 插入、删除或替换数组的元素

toLocaleString( ) 把数组转换成局部字符串

toString( ) 将数组转换成一个字符串

forEach 遍历所有元素

every 判断所有元素是否都符合条件

sort 排序

map 对元素重新组装,生成新数组

filter 过滤符合条件的元素

String 字符串对象

Length 获取字符串的长度。

toUpperCase() 将字符串中的字母转成全大写。如:strObj.toUpperCase()

charAt(index) 返回指定下标位置的一个字符。如果没有找到,则返回空字符串

substr() 在原始字符串,返回一个子字符串

substring() 在原始字符串,返回一个子字符串

split() 将一个字符串转成数组

charCodeAt( ) 返回字符串中的第 n 个字符的代码

concat( ) 连接字符串

fromCharCode( ) 从字符编码创建—个字符串

indexOf( ) 返回一个子字符串在原始字符串中的索引值(查找顺序从左往右查找)。如果没有找到,则返回-1

lastIndexOf( ) 从后向前检索一个字符串

localeCompare( ) 用本地特定的顺序来比较两个字符串

match( ) 找到一个或多个正则表达式的匹配

replace( ) 替换一个与正则表达式匹配的子串

search( ) 检索与正则表达式相匹配的子串

slice( ) 抽取一个子串

toLocaleLowerCase( ) 把字符串转换小写

toLocaleUpperCase( ) 将字符串转换成大写

toLowerCase( ) 将字符串转换成小写

toString( ) 返回字符串

toUpperCase( ) 将字符串转换成大写

valueOf( )

22.谈谈事件委托的理解?

JavaScript 事件代理则是一种简单的技巧,把事件处理器添加到一个上级元素上,这样就避免了把事件处理器添加到多个子级元素上。这主要得益于浏览器的事件冒泡机制。

优点:

1、减少事件注册,节省内存。

2、在 table 上代理所有 td 的 click 事件。

3、在 ul 上代理所有 li 的 click 事件。

4、简化了 dom 节点更新时,相应事件的更新。

5、不用在新添加的 li 上绑定 click 事件。

6、当删除某个 li 时,不用移解绑上面的 click 事件。

缺点:

1、事件委托基于冒泡,对于不冒泡的事件不支持

2、层级过多,冒泡过程中,可能会被某层阻止掉。

3、理论上委托会导致浏览器频繁调用处理函数,虽然很可能不需要处理。所以建议就近委托,比如在 table 上代理 td,而不是在 document 上代理 td。

4、把所有事件都用代理就可能会出现事件误判。比如,在 document 中代理了所有 button 的 click事件,另外的人在引用改 js 时,可能不知道,造成单击 button 触发了两个 click 事件

23.什么是闭包?

定义:

一个作用域可以访问到另外一个函数内部的局部变量,或者说一个函数(子函数)访问另一个函数(父函数)中的变量。此时就会产生闭包,那么这个变量所在的函数我们就称之为闭包函数。

1
2
3
4
5
6
7
8
9
function aaa() {
let a = 0
return function() {
alert(a++)
}
}

let fun = aaa()
fun() // 1

优缺点:

闭包的主要作用:延伸了变量的作用范围,因为闭包函数中的局变量不会等着闭包函数执行完就销毁,因为还有别的函数要调用它,只有等着所有的函数都调用完了它才会销毁。 实现数据的私有。

闭包会造成内存泄露,如何解决:用完之后手动是释放。

优点:

1)可以减少全局变量的定义,避免全局变量的污染

2)能够读取函数内部的变量

3)在内存中维护⼀个变量,可以⽤做缓存

缺点:

1)造成内存泄露

2)闭包可能在⽗函数外部,改变⽗函数内部变量的值。

3)造成性能损失

使用场景:

  1. 封装私有变量
  2. 闭包可以用于创建模块化的代码结构,避免全局变量的污染。
  3. 在处理用户输入或频繁触发的事件时,可以通过闭包来实现防抖(debounce)和节流(throttle)的效果。防抖和节流是优化性能和减少不必要请求的常见方法。
  4. 缓存数据:通过闭包,可以在函数内部缓存一些计算结果或其他重要数据,避免重复计算或请求。

24.for in 和 for of 的区别

1、推荐在循环对象属性的时候使用 for…in,在遍历数组的时候的时候使用 for…of

2、for…in 循环出的是 key,for…of 循环出的是 value

3、注意,for…of 是 ES6 新引入的特性。修复了 ES5 引入的 for…in 的不足

4、for…of 不能循环普通的对象,需要通过和 Object.keys()搭配使用

25、split()和 join()的区别?

split()是把一串字符(根据某个分隔符)分成若干个元素存放在一个数组里即切割成数组的形式;

join() 是把数组中的字符串连成一个长串,可以大体上认为是 split()的逆操作

26.深拷贝

首先浅拷贝和深拷贝只针对引用类型

浅拷⻉: 拷贝的是地址

拷贝对象

  1. object.assign()
  2. 展开运算符

拷贝数组

  1. object.prototype.concat()

如果是简单数据类型拷贝值,不会影响元对象,引用数据类型拷贝的是地址。

深拷⻉: 拷贝的是对象,不是地址

  1. 递归
  2. JSON 对象中的 parse 和 stringify
  3. lodash 里的 cloneDeep
1
2
3
4
5
6
7
⽬前实现深拷⻉的主要是利⽤ JSON 对象中的 parse 和 stringify  

const originArray = [1,2,3,4,5];

const cloneArray = JSON.parse(JSON.stringify(originArray));

console.log(cloneArray === originArray); // false

用到?

  1. 处理嵌套对象和数组:创建一个独立的副本。这样,对副本的修改不会影响原始数据。
  2. 操作不可变的数据
  3. 序列化和反序列化:当你需要将对象转换为字符串进行存储或传输时,深拷贝可以帮助你创建一个完整的副本,并且在后续需要时可以还原为原始对象。

27.原型链

原型

  1. 构造函数通过原型分配的函数是所有对象所共享的
  2. js 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象,所以我们也称为原型对象。
  3. 这个对象可以挂载函数,对象实例化不会多次创建原型上的函数,节约内存。
  4. 我们可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法。
  5. 构造函数和原型对象中的this 都指向实例化对象。

constructor属性

每个原型对象里面都有个 constructor 属性,该属性指向该原型对象的构造函数。

使用场景:

如果有多个对象的方法,我们可以给原型对象采取 对象形式赋值。

但是这样就会覆盖构造函数原型对象原来的内容。

这样修改后的原型对象 constructor 就不再指向当前构造函数了。

此时,我们应该在修改后的原型对象中,添加一个 constructor 指向原来的构造函数。

对象原型 –proto–

实例对象的原型,对象都有一个属性 –proto–,指向构造函数的 prototype 原型对象。

之所以我们对象可以使用构造函数prototype原型对象上的方法和属性,就是因为对象有–proto–原型的存在。

原型链:

所有的实例对象里面都有–proto–对象原型,指向原型对象

所有的原型对象里面有 constructor,指向创造该原型对象的构造函数

  1. 当访问一个对象的属性和方法时,首先查找这个对象本身有没有该属性
  2. 如果没有就查找它的原型(也就是–proto–指向的prototype原型对象)
  3. 如果还没有就查找原型对象上的原型
  4. 依此类推,一直找到为止
  5. –proto–对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线。
  6. 可以使用 instanceof 运算符用于检测构造函数的prototype 属性是否出现在某个实例对象的原型链上。
    Snipaste_2023-10-29_00-09-55.png

28.防抖和节流

**防抖(debounce)**:触发⾼频事件后 n 秒内函数只会执⾏⼀次,如果 n 秒内⾼频事件再次被触发,则重新计算时间

使用场景:

  1. 搜索框防抖
  2. 手机号、邮箱验证输入检测

手写防抖函数

思路:防抖的核心就是利用 settimeout 实现的

  1. 声明一个定时器变量
  2. 当鼠标每次滑动都先判断 是否有定时器了,如果有定时器先清除以前的定时器
  3. 如果没有定时器则开启定时器,记得存到变量里面
  4. 在定时器里面调用要执行的函数
1
2
3
4
5
6
7
8
9
10
function debounce(fn,t) {
let timeId
return function() {
if(timeId) clearTimeout(timeId)
timeId = setTimeout(function() {
fn()
},t)
}
}
box.addEventListener('mousemove',debounce(mouseMove,500))

**节流(throttle)**:⾼频事件触发,但在 n 秒内只会执⾏⼀次,所以节流会稀释函数的执⾏频率

使用场景:

  1. 小米轮播图切换点击效果
  2. 页面尺寸缩放resize
  3. 滚动条滚动

手写节流函数

思路:一样

  1. 声明一个定时器变量
  2. 当鼠标每次滑动都先判断 是否有定时器了,如果有定时器,则不开启新定时器
  3. 如果没有定时器则开启定时器,记得存到变量里面
  4. 定时器里面调用执行的函数
  5. 清空定时器
1
2
3
4
5
6
7
8
9
10
11
12
function throttle(fn,t) {
let timeId = null
return function() {
if(!timeId) {
timeId = setTimeout(function() {
fn()
timeId = null
},t)
}
}
}
box.addEventListener('mousemove',throttle(mouseMove,500))

区别:防抖动是将多次执⾏变为最后⼀次执⾏,节流是将多次执⾏变成每隔⼀段时间执⾏。

29.ES6

  1. let const
  2. 模板字符串
  3. 箭头函数
  4. object.keys() 遍历对象的键
  5. object.assign() 合并对象 常用于对象拷贝
  6. for of
  7. import(用于在一个模块中加载另一个含有 export 接口的模块) export(用于对外输出本模块)
  8. promise
  9. set
  10. class

30.介绍下 Set、Map 的区别

区别

应用场景 Set 用于数据重组,Map 用于数据储存

Set:

成员不能重复

只有键值没有键名,类似数组

可以遍历,方法有 add, delete,has

Map:

本质上是健值对的集合,类似集合

可以遍历,可以跟各种数据格式转换

31.async await

  1. async 用于修饰一个函数,表示一个函数是异步的
  2. await 要用在 async 函数中
  3. await 后面一般会跟一个 promise 对象
  4. await 只会等待 promise 成功的结果

浏览器

共同点:

都是保存在浏览器端、且同源的

区别:

  1. cookie数据始终在同源的http请求中携带(即使不需要),即cookie在浏览器和服务器间来回传递,而sessionStorage和localStorage不会自动把数据发送给服务器,仅在本地保存。cookie数据还有路径(path)的概念,可以限制cookie只属于某个路径下

  2. 存储大小限制也不同,cookie数据不能超过4K,同时因为每次http请求都会携带cookie、所以cookie只适合保存很小的数据,如会话标识。sessionStorage和localStorage虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大

  3. 数据有效期不同,sessionStorage:仅在当前浏览器窗口关闭之前有效;localStorage:始终有效,窗口或浏览器关闭也一直保存,因此用作持久数cookie:只在设置的cookie过期时间之前有效,即使窗口关闭或浏览器关闭

  4. 作用域不同,sessionStorage不在不同的浏览器窗口中共享,即使是同一个页面;localstorage在所有同源窗口中都是共享的;cookie也是在所有同源窗口中都是共享的

  5. web Storage支持事件通知机制,可以将数据更新的通知发送给监听者

  6. Storage的api接口使用更方便

2. 浏览器输入URL发生了什么

  1. URL 解析
  2. DNS 查询
  3. TCP 连接
  4. 处理请求
  5. 接受响应
  6. 渲染页面

3. 浏览器是如何渲染页面的?

不同浏览器内核渲染机制有所区别

  1. HTML 被 HTML 解析器解析成 DOM 树;
  2. CSS 被 CSS 解析器解析成 CSSOM 树;
  3. 结合 DOM 树和 CSSOM 树,生成一棵渲染树(Render Tree),这一过程称为 Attachment;
  4. 生成布局(flow),浏览器在屏幕上“画”出渲染树中的所有节点;
  5. 将布局绘制(paint)在屏幕上,显示出整个页面。

4. 重绘、重排

概念

  1. 重排(Reflow):当渲染树的一部分必须更新并且节点的尺寸发生了变化,浏览器会使渲染树中受到影响的部分失效,并重新构造渲染树
  2. 重绘(Repaint):是在一个元素的外观被改变所触发的浏览器行为,浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。比如改变某个元素的背景色、文字颜色、边框颜色等等

区别:

重绘不一定需要重排(比如颜色的改变),重排必然导致重绘(比如改变网页位置)

引发重排

  1. 添加、删除可见的dom
  2. 元素的位置改变
  3. 元素的尺寸改变(外边距、内边距、边框厚度、宽高、等几何属性)
  4. 页面渲染初始化
  5. 浏览器窗口尺寸改变
  6. 获取某些属性。当获取一些属性时,浏览器为取得正确的值也会触发重排,它会导致队列刷新,这些属性包括:offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight、getComputedStyle() (currentStyle in IE)。所以,在多次使用这些值时应进行缓存

优化方案

浏览器会维护1个队列,把所有会引起重排,重绘的操作放入这个队列,等队列中的操作到一定数量或者到了一定时间间隔,浏览器就会flush队列,进行一批处理,这样多次重排,重绘变成一次重排重绘

减少 reflow/repaint:

  1. 不要一条一条地修改 DOM 的样式。可以先定义好 css 的 class,然后修改 DOM 的className。

  2. 不要把 DOM 结点的属性值放在一个循环里当成循环里的变量。

  3. 为动画的 HTML 元件使用 fixed 或 absoult 的 position,那么修改他们的 CSS 是不会reflow 的。

  4. 千万不要使用 table 布局。因为可能很小的一个小改动会造成整个 table 的重新布局。(table及其内部元素除外,它可能需要多次计算才能确定好其在渲染树中节点的属性,通常要花3倍于同等元素的时间。这也是为什么我们要避免使用table做布局的一个原因。)

  5. 不要在布局信息改变的时候做查询(会导致渲染队列强制刷新)

5. 事件循环(Event loop)

js 是一门单线程执行的语言。也就是说,同一时间只能做一件事情。

为了防止某个耗时任务导致程序假死的问题,异步代码由js 委托给宿主环境(浏览器、node环境)等待执行。

JavaScript 的事件分两种

  1. 宏任务:包括整体代码 script,setTimeout,setInterval
  2. 微任务:Promise.then(非 new Promise是同步的),catch、process.nextTick(node 中)

具体执行:

事件的执行顺序——先执行宏任务,然后执行微任务,任务有同步的任务和异步的任务,同步的进入主线程,异步的进入 Event Table 并注册函数,异步事件完成后,会将回调函数放在队列中,如果还有异步的宏任务,那么就会进行循环执行上述的操作

主 线程会不断从任务队列中按顺序取任务执行,每执行完一个任务都会检查microtask队列是否为空(执行完一个 任务的具体标志是函数执行栈为空),如果不为空则会一次性执行完所有microtask。然后再进入下一个循环去 任务队列中取下一个任务执行

详细步骤

  1. 选择当前要执行的宏任务队列,选择一个最先进入任务队列的宏任务,如果没有宏任务可以选择,则会 跳转至microtask的执行步骤。

  2. 将事件循环的当前运行宏任务设置为已选择的宏任务。

  3. 运行宏任务。

  4. 将事件循环的当前运行任务设置为null。

  5. 将运行完的宏任务从宏任务队列中移除。

  6. microtasks步骤:进入microtask检查点。

  7. 更新界面渲染。

  8. 返回第一步。

6. 跨域

跨域是什么?

跨域(Cross-Origin)指的是在浏览器中,当一个请求的源(Origin)与目标资源的源不一致时,即发生跨域访问。在默认情况下,浏览器的同源策略(Same-Origin Policy)会阻止这种跨域访问。同源策略是为了保护用户的信息安全,防止恶意网站对其他网站的资源进行访问和操作。

同源策略规定几个约束

  1. 协议相同
  2. 域名相同
  3. 端口号相同

同源策略限制内容有

  • cookie、localstorage、indexedDB 等
  • dom节点
  • ajax 请求

跨域解决⽅法:

1、jsonp⽅式

2、代理服务器的⽅式

3、服务端允许跨域访问(CORS)

4、取消浏览器的跨域限制

7.常见code码

200 - 请求成功

301 - 资源(⽹⻚等)被永久转移到其它URL

403 - Forbidden 服务器理解请求客户端的请求,但是拒绝执⾏此请求

404 - 请求的资源(⽹⻚等)不存在

500 - 内部服务器错误

502 - Bad Gateway 作为⽹关或者代理⼯作的服务器尝试执⾏请求时,从远程服务器接收到了⼀个⽆效的响应

8. http 和 https 的区别

  1. HTTP 明⽂传输,数据都是未加密的,安全性较差,HTTPS 数据传输过程是加密的,安全性较好。

  2. 使⽤ HTTPS 协议需要到 CA(Certificate Authority,数字证书认证机构) 申请证书,⼀般免费证书较少,因⽽需要⼀定费⽤

  3. HTTP ⻚⾯响应速度⽐ HTTPS 快,主要是因为 HTTP 使⽤ TCP 三次握⼿建⽴连接,客户端和服务器需要交换 3 个包,⽽ HTTPS除了 TCP 的三个包,还要加上 ssl 握⼿需要的 9 个包,所以⼀共是 12 个包。

  4. HTTP 和 HTTPS 使⽤的是完全不同的连接⽅式,⽤的端⼝也不⼀样,前者是 80,后者是 443。

  5. HTTPS 其实就是建构在 SSL/TLS 之上的 HTTP 协议,所以,要⽐较 HTTPS ⽐ HTTP 要更耗费服务器资源。

9. 前端优化策略

1、减少http请求数

2、将脚本往后挪,减少对并发下载的影响

3、避免频繁的DOM操作

4、压缩图⽚

5、gzip压缩优化,对传输资源进⾏体积压缩(html,js,css)

6、按需加载

7、组件化

8、减少不必要的Cookie(Cookie存储在客户端,伴随着HTTP请求在浏览器和服务器之间传递,由于cookie在访问对应域名下的资源时都会通过HTTP请求发送到服务器,从⽽会影响加载速度,所以尽量减少不必要的Cookie。)

10.介绍一下 websocket

websocket 是一种网络通信协议,是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通 信的协议,这个对比着 HTTP 协议来说,HTTP 协议是一种无状态的、无连接的、单向的应用层协议,通信请求只能由客户端发起,服务端对请求做出应答处理。HTTP 协议无法实现服务器主动向客户端发起消息,websocket 连接允许客户端和服务器之间进行全双工通信, 以 便 任一方都可以通过建立的连接将数据推送到另一端。websocket 只需要建立一次连接, 就 可 以一直保持连接状态

11.webpack

webpack 的作⽤就是处理依赖,模块化,打包压缩⽂件,管理插件。

1、webpack打包原理

把所有依赖打包成⼀个 bundle.js ⽂件,通过代码分割成单元⽚段并按需加载。

2、webpack的优势

(1) webpack 是以 commonJS 的形式来书写脚本滴,但对 AMD/CMD 的⽀持也很全⾯,⽅便

旧项⽬进⾏代码迁移。

(2)能被模块化的不仅仅是 JS 了。

(3) 开发便捷,能替代部分 grunt/gulp 的⼯作,⽐如打包、压缩混淆、图⽚转base64等。

(4)扩展性强,插件机制完善

12.Get 和 Post 的区别以及使用场景

区别

1、Get 使用 URL 或 Cookie 传参。而 Post 将数据放在 body 中

2、Get 的 URL 会有长度上的限制,则 Post 的数据则可以非常大

3、Post 比 Get 安全,因为数据在地址栏上不可见

最本质的区别

基于 http 协议进行请求, 其实 GET 和 POST 无区别, 只是请求时的方式不同, 都可以携带请求体, 也可以在 URL 带参数区别来自于浏览器对 URL 长度的限制, 请求体大小来源于服务器的限制

还有语义的区别:

GET 是获取, POST 是提交

Get 是用来从服务器上获得数据,而 post 是用来向服务器上传递数据

13. HTTP有哪些⽅法?

HTTP 1.0 标准中,定义了3种请求⽅法:GET、POST、HEAD

HTTP 1.1 标准中,新增了请求⽅法:PUT、PATCH、DELETE、OPTIONS、TRACE、CONNECT

14. 各个HTTP方法的具体作用是什么?

方法 功能
GET 通常⽤于请求服务器发送某些资源
POST 发送数据给服务器
HEAD 请求资源的头部信息, 并且这些头部与 HTTP GET ⽅法请求时返回的⼀致。
该请求⽅法的⼀个使⽤场景是在下载⼀个⼤⽂件前先获取其⼤⼩再决定是否要下载, 以此可以节约带宽资源
PUT ⽤于全量修改⽬标资源 (看接口, 也可以用于添加)
DELETE ⽤于删除指定的资源
OPTIONS ⽤于获取⽬的资源所⽀持的通信选项 (跨域请求前, 预检请求, 判断目标是否安全)
TRACE 该方法会 让服务器 原样返回任意客户端请求的信息内容, 用于诊断和判断
CONNECT HTTP/1.1协议中预留给能够将连接改为管道⽅式的代理服务器
(把服务器作为跳板,让服务器代替用户去访问其它网页, 之后把数据原原本本的返回给用户)
PATCH ⽤于对资源进⾏部分修改

GET POST PUT PATCH DELETE

GET/DELETE 参数是在地址栏中传递的

PUT/PATCH/POST 参数是在请求体传递的

Vue

1.谈谈你对 Vue 生命周期的理解?

(1)生命周期是什么?

Vue 实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模版、挂载 Dom -> 渲染、更新 -> 渲染、卸载等一系列过程,我们称这是 Vue 的生命周期

(2)各个生命周期的作用

beforeCreate 组件实例被创建之初,组件的属性生效之前
created 组件实例已经完全创建,属性也绑定,但真实 dom 还没有生成,$el 还不可用
beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用
mounted el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子
beforeUpdate 组件数据更新之前调用,发生在虚拟 DOM 打补丁之前
update 组件数据更新之后
activited keep-alive 专属,组件被激活时调用
deadctivated keep-alive 专属,组件被销毁时调用
beforeDestory 组件销毁前调用
destoryed 组件销毁后调用

2. keep-alive

1、什么是keep-alive?

keep-alive 是 Vue 的内置组件,当它包裹动态组件时,会缓存不活动的组件实例,⽽不是销毁它们。keep-alive 是⼀个抽象组件:它⾃身不会渲染成⼀个 DOM 元素,也不会出现在⽗组件链中

2、keep-alive的优点

在组件切换过程中 把切换出去的组件保留在内存中,防⽌重复渲染DOM,减少加载时间及性能消耗,提⾼⽤户体验性。

3、keep-alive有三个属性

include : 只有匹配的组件会被缓存

exclude : 任何匹配的组件都不会被缓存

max : 最多可以缓存多少组件实例

4、keep-alive的使⽤会触发两个⽣命周期函数?

这两个函数分别是

activated 当组件被激活(使⽤)的时候触发 可以简单理解为进⼊这个⻚⾯的时候触发

deactivated 当组件不被使⽤的时候触发 可以简单理解为离开这个⻚⾯的时候触发

3. 数据双向绑定原理

数据变化更新视图

  • 输入框内容变化时,Data 中的数据同步变化。即 View => Data 的变化。
  • Data 中的数据变化时,文本节点的内容同步变化。即 Data => View 的变化

vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的⽅式来实现的.

数据劫持、vue是通过Object.defineProperty()来实现数据劫持,

其中会有getter()和setter⽅法;当读取属性值时,就会触发getter()⽅法,

在view中如果数据发⽣了变化,就会通过Object.defineProperty( )对属性设置⼀个setter函数,

当数据改变了就会来触发这个函数;

实现一个监听器 ->实现一个解析器 -> 实现一个订阅者 -> 实现一个订阅器 Dep

4. 路由守卫

1.全局路由守卫

beforeEach(to, from, next) 全局前置守卫,路由跳转前触发

beforeResolve(to, from, next) 全局解析守卫 在所有组件内守卫和异步路由组件被解析之后触发

afterEach(to, from) 全局后置守卫,路由跳转完成后触发

2.路由独享守卫

beforeEnter(to,from,next) 路由对象单个路由配置 ,单个路由进⼊前触发

3.组件路由守卫

beforeRouteEnter(to,from,next) 在组件⽣命周期beforeCreate阶段触发

beforeRouteUpdadte(to,from,next) 当前路由改变时触发

beforeRouteLeave(to,from,next) 导航离开该组件的对应路由时触发

4.参数

to: 即将要进⼊的⽬标路由对象

from: 即将要离开的路由对象

next(Function):是否可以进⼊某个具体路由,或者是某个具体路由的路径

5. Vuex

Vuex有五个核⼼概念:state,getters,mutations,actions,modules

  1. state:vuex的基本数据,⽤来存储变量

  2. geeter:从基本数据(state)派⽣的数据,相当于state的计算属性

  3. mutation:提交更新数据的⽅法,必须是同步的(如果需要异步使⽤action)。每个 mutation 都有⼀个字符串的 事件类型 (type) 和 ⼀个 回调函数 (handler)。

回调函数就是我们实际进⾏状态更改的地⽅,并且它会接受 state 作为第⼀个参数,提交载荷作为第⼆个参数。

  1. action:和mutation的功能⼤致相同,不同之处在于 ==》1. Action 提交的是 mutation,⽽不是直接变更状态。 2. Action 可以包含任意异步操作。

  2. modules:模块化vuex,可以让每⼀个模块拥有⾃⼰的state、mutation、action、getters,使得结构⾮常清晰,⽅便管理。

6. 组件通讯(⽗、⼦)

  1. ⽗组件向⼦组件传值:⽗组件通过属性的⽅式向⼦组件传值,⼦组件通过 props 来接收

  2. ⼦组件向⽗组件传值:⼦组件绑定⼀个事件,通过 this.$emit() 来触发

  3. 其他⽅式:缓存、vuex、eventBus事件总线、provide inject

7. 怎么定义vue-router的动态路由?

在router⽬录下的index.js⽂件中,对path属性加上/:id。 使⽤router对象的params.id

8. 2.0和3.0的区别

双向绑定:

V2:使⽤Object.defineProperty

V3:使⽤ES6的新特性proxy来劫持数据,当数据改变时发出通知

根元素:

V2: 必须要有⼀个根元素

V3: ⽆要求

diff算法:

V2: 虚拟Dom全量⽐较

V3: 增加了静态标记PatchFlag

生命周期不同

9. computed 与 watch 的区别

computed⽀持缓存,相依赖的数据发⽣改变才会重新计算;watch不⽀持缓存,只要监听的数据变化就会触发相应操作

computed不⽀持异步,当computed内有异步操作时是⽆法监听数据变化的;watch⽀持异步操作

computed属性的属性值是⼀函数,函数返回值为属性的属性值,computed中每个属性都可以设置set与get⽅法。watch监听的数据必须是data中声明过或⽗组件传递过

computed

  1. 它是计算属性。主要用于值的计算并一般会返回一个值。所以它更多⽤于计算值的场景
  2. 它具有缓存性。当访问它来获取值时,它的 getter 函数所计算出来的值会进行缓存
  3. 只有当它依赖的属性值发生了改变,那下⼀次再访问时才会重新调⽤ getter 函数来计算
  4. 它适⽤于计算⽐较消耗性能的计算场景
  5. 必须要有一个返回值

watch

  1. 它更多的是起到 “观察” 的作⽤,类似于对数据进行变化的监听并执行回调。

    主要⽤于观察 props 或 本组件data的值,当这些值发生变化时,执⾏处理操作

  2. 不一定要返回某个值

建议

  1. 当目的是进⾏数值计算,且依赖于其他数据,那么推荐使用 computed

  2. 当需要在某个数据发生变化的, 同时做⼀些稍复杂的逻辑操作,那么推荐使⽤ watch

10.Route和router的区别

  1. route:是路由信息对象,包括“path,parms,hash,name“等路由信息参数。

  2. Router:是路由实例对象,包括了路由跳转⽅法,钩⼦函数等。

11.vue-router 路由模式有⼏种?

vue-router 有 3 种路由模式:hash、history、abstract:

hash: 使⽤ URL hash 值来作路由。⽀持所有浏览器,包括不⽀持 HTML5 History Api 的浏览器;

history : 依赖 HTML5 History API 和服务器配置。具体可以查看 HTML5 History 模式;

abstract : ⽀持所有 JavaScript 运⾏环境,如 Node.js 服务器端。如果发现没有浏览器的API,路由会⾃动强制进⼊这个模式.

12. Object.defineProperty 和 Proxy 的区别

Object.defineProperty 和 Proxy 的区别如下:

  1. Proxy 可以直接监听对象而非属性;
  2. Proxy 可以直接监听数组的变化;
  3. Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等 是 Object.defineProperty 不具备的
  4. Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改
  5. Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准 的性能红利
  6. Object.defineProperty 兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题, 而且无法用 polyfill 磨平,因此 Vue 的作者才声明需要等到下个大版本( 3.0 )才能用 Proxy 重 写

13. vue3 新特性有哪些?

1、性能提升

  • 响应式性能提升,由原来的 Object.defineProperty 改为基于ES6的 Proxy ,使其速度更快,消除警告。
  • 重写了 Vdom ,突破了 Vdom 的性能瓶颈。
  • 进行模板编译优化。
  • 更加高效的组件初始化

2、更好的支持 typeScript

  • 有更好的类型推断,使得 Vue3 把 typeScript 支持得非常好

3、新增Composition API

  • Composition API 是 vue3 新增的功能,比 mixin 更强大。它可以把各个功能模块独立开来,提高代码逻辑的可复用性,同时代码压缩性更强

4、新增组件

  • Fragment 不再限制 template 只有一个根几点。
  • Teleport 传送门,允许我们将控制的内容传送到任意的 DOM 中。
  • Supense 等待异步组件时渲染一些额外的内容,让应用有更好的用户体验。

5、Tree-shaking:支持摇树优化

  • 摇树优化后会将不需要的模块修剪掉,真正需要的模块打到包内。优化后的项目体积只有原来的一半,加载速度更快

6、Custom Renderer API: 自定义渲染器

  • 实现 DOM 的方式进行 WebGL 编程

14. v-show 与 v-if 区别

v-show和v-if的区别:

v-show是css切换,v-if是完整的销毁和重新创建。

使⽤频繁切换时⽤v-show,运⾏时较少改变时⽤v-if

15. vue中v-if和v-for优先级在vue2和vue3中的区别

实践中不管是vue2或者vue3都不应该把v-if和v-for放在一起使用。

  • 在 vue 2.x 中,在一个元素上同时使用 v-if 和 v-for 时, v-for 会优先作用。
  • 在 vue 3.x 中, v-if 总是优先于 v-for 生效。
  • vue2中v-for的优先级是高于v-if的,放在一起,会先执行循环在判断条件,并且如果值渲染列表中一小部分元素,也得再每次重渲染的时候遍历整个列表,比较浪费资源。
  • vue3中v-if的优先级是高于v-for的,所以v-if执行时,它调用相应的变量如果不存在,就会导致异常

16. script setup 是干啥的?

scrtpt setup 是 vue3 的语法糖,简化了组合式 API 的写法,并且运行性能更好。使用 script setup 语法糖的特点:

  • 属性和方法无需返回,可以直接使用。
  • 引入组件的时候,会自动注册,无需通过 components 手动注册。
  • 使用 defineProps 接收父组件传递的值。
  • useAttrs 获取属性,useSlots 获取插槽,defineEmits 获取自定义事件。
  • 默认不会对外暴露任何属性,如果有需要可使用 defineExpose 。

17. reactive与ref的区别?

Vue3 中的 ref 和 reactive 是 Vue3 中用于数据管理的两种不同的响应式 API。

ref 用于创建一个包装简单值的响应式引用,例如一个数字、字符串或对象。当 ref 创建一个响应式引用时,它返回一个对象,该对象具有一个 value 属性,该属性指向实际值。当 ref 返回的对象中的 value 属性更改时,组件将自动重新渲染。

reactive 用于创建一个响应式对象,该对象可以包含多个属性和嵌套属性。当使用 reactive 创建响应式对象时,返回的对象是一个代理对象,该对象具有与原始对象相同的属性,并且任何对代理对象属性的更改都将触发组件的重新渲染。

18. v-model的使用?

v-model实现双向绑定的语法糖,常用于表单与组件之间的数据双向绑定.

V-model的原理:

  • v-bind绑定一个value属性

  • v-on指令给当前元素绑定input事件

可看出v-model绑定在表单上时,v-model其实就是v-bind绑定value和v-on监听input事件的结合体

组件上的双向绑定(原理)

v-model绑定在组件上的时候做了以下步骤

  • 在父组件内给子组件标签添加 v-model ,其实就是给子组件绑定了 value 属性
  • 子组件内使用 prop 创建 创建 value 属性可以拿到父组件传递下来的值,名字必须是 value。
  • 子组件内部更改 value 的时候,必须通过 $emit 派发一个 input 事件,并携最新的值
  • v-model 会自动监听 input 事件,把接收到的最新的值同步赋值到 v-model 绑定的变量上

19. vuex中的辅助函数怎么使用?

vuex的辅助函数有4个

  • mapState 函数返回的是一个对象。通常,我们需要使用一个工具函数将多个对象合并为一个,以使我们可以将最终对象传给 computed 属性。
  • mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性,因此你可以这样来使用他
  • mapMutations 辅助函数将组件中的 methods 映射为 store.commit,其原理就是将this.montify 映射为this.$store.commit(‘montify’)
  • mapActions在组件中使用 this.$store.dispatch(‘prodect’) 分发 action,或者使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用

20. slot是什么?有什么作用?原理是什么?

slot又名插槽,是Vue的内容分发机制,组件内部的模板引擎使用slot元素作为承载分发内容的出口。插槽slot是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。

slot又分三类,默认插槽,具名插槽和作用域插槽。

  • 默认插槽:又名匿名插槽,当slot没有指定name属性值的时候一个默认显示插槽,一个组件内只有有一个匿名插槽。
  • 具名插槽:带有具体名字的插槽,也就是带有name属性的slot,一个组件可以出现多个具名插槽。
  • 作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。

实现原理:

当子组件vm实例化时,获取到父组件传入的slot标签的内容,存放在vm.$slot中,默认插槽为vm.$slot.default具名插槽为vm.$slot.xxx,xxx 为插槽名

当组件执行渲染函数时候,遇到slot标签,使用slot中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽

21. $nextTick的使用

用法:将回调延迟到下次DOM更新循环之后执行。在修改数据之后立即使用它,然后等待DOM更新。它跟全局方法 Vue.nextTick 一样,不同的是回调的 this 自动绑定到调用它的实例上。

$nextTick() 的应用场景

在vue的生命周期 created() 钩子函数中进行 dom 操作,一定要放在 $nextTick() 函数中执行。在 created() 钩子函数执行的时候 DOM 其实并未进行任何渲染,而此时进行 DOM 操作无异于徒劳,所以此处一定要将 DOM 操作的代码放进 nextTick() 的回调函数中。

mounted() 钩子函数,因为该钩子函数执行时,所有的 DOM 挂载和 渲染都已完成,此时在该钩子函数中进行任何 DOM 操作都不会有问题

在数据变化后要执行某个操作,而这个操作需要随数据改变而改变DOM结构时,这个操作都是需要放置 $nextTick() 的回调函数中。

22. v-for中的key

语法: key=”唯一值”

作用:给列表项添加的唯一标识。便于Vue进行列表项的正确排序复用

为什么加key:Vue 的默认行为会尝试原地修改元素(就地复用

实例代码:

1
2
3
4
5
6
7
<ul>
  <li v-for="(item, index) in booksList" :key="item.id">
    <span>{{ item.name }}</span>
    <span>{{ item.author }}</span>
    <button @click="del(item.id)">删除</button>
  </li>
</ul>

注意:

  1. key 的值只能是字符串 或 数字类型
  2. key 的值必须具有唯一性
  3. 推荐使用 id 作为 key(唯一),不推荐使用 index 作为 key(会变化,不对应)

23.data必须是一个函数

1、data为什么要写成函数

一个组件的 data 选项必须是一个函数。目的是为了:保证每个组件实例,维护独立的一份数据对象。

每次创建新的组件实例,都会新执行一次data 函数,得到一个新对象。

24.编程式导航,如何跳转传参?

1.path路径跳转

  • query传参

    1
    2
    3
    4
    5
    6
    7
    8
    9
    this.$router.push('/路径?参数名1=参数值1&参数2=参数值2')

    this.$router.push({
      path: '/路径',
      query: {
        参数名1: '参数值1',
        参数名2: '参数值2'
      }
    })
  • 动态路由传参

    1
    2
    3
    4
    5
    this.$router.push('/路径/参数值')

    this.$router.push({
      path: '/路径/参数值'
    })

2.name命名路由跳转

  • query传参

    1
    2
    3
    4
    5
    6
    7
    this.$router.push({
      name: '路由名字',
      query: {
        参数名1: '参数值1',
        参数名2: '参数值2'
      }
    })
  • 动态路由传参 (需要配动态路由)

    1
    2
    3
    4
    5
    6
    this.$router.push({
      name: '路由名字',
      params: {
        参数名: '参数值',
      }
    })

25. 什么是 M V VM

Model-View-ViewModel 模式

image-20210223221853817

Model 层: 数据模型层

通过 Ajaxfetch 等 API 完成客户端和服务端业务模型的同步。

View 层: 视图层

作为视图模板存在,其实View 就是⼀个动态模板。

ViewModel 层: 视图模型层

负责暴露数据给 View 层,并对 View 层中的数据绑定声明、 指令声明、 事件绑定声明, 进行实际的业务逻辑实现。

数据变化了, 视图自动更新 => ViewModel 底层会做好监听Object.defineProperty,当数据变化时,View 层会自动更新

视图变化了, 绑定的数据自动更新 => 会监听双向绑定的表单元素的变化,⼀旦变化,绑定的数据也会得到⾃动更新。

26. MVVM的优缺点有哪些?

优点

  1. 实现了视图(View)和模型(Model)的分离,降低代码耦合、提⾼视图或逻辑的复⽤性

  2. 提⾼了可测试性:ViewModel 的存在可以帮助开发者更好地编写测试代码

  3. 能⾃动更新 DOM:利⽤双向绑定,数据更新后视图⾃动更新,让开发者从繁琐的⼿动操作 DOM 中解放出来

缺点

  1. Bug 难被调试:因为使⽤了双向绑定的模式,当我们看到界⾯发生异常了,有可能是 View 的代码产生的 Bug,

    也有可能是Model 代码的问题。数据绑定使得⼀个位置的 Bug 被快速传递到别的位置,

    要定位原始出问题的地⽅就变得不那么容易了

    可采用的调试方案:

    (1) 注释掉一段代码, 确定代码的位置

    (2) debugger 打断点 或者 console 进行调试

  2. 在⼀个⼤的模块中 Model 也会很⼤,虽然使⽤上来说⽅便了,但如果⻓期持有不释放内存,就会造成更多的内存消耗

    占用的是 浏览器的 内存

项目

1.分析登录流程

传统思路都是登录校验通过之后,直接调用接口,获取token之后,跳转到主页。

  • vue-element-admin的登录思路:
  1. 登录表单校验通过
  2. 调用Vuex提供的登录的action
  3. 登录的Action中会调用接口
  4. 登录接口如果成功执行,会返回token
  5. 利用Vuex的特性,将token共享的到Vuex中,这样Vuex就统一管理了token,别的地方想要使用,直接通过Vuex就可以
  6. 登录接口会调用单独封装的请求模块(api)
  7. 请求模块中又会使用用到axios封装的请求工具
  8. 而请求工具又要考虑区分 开发环境和生产环境的问题
  9. 请求时还要考虑前后分离项目产生的跨域问题,要使用代理解决跨域

2.登录模块业务实现思路

  1. 首先设计并开发出登录页面的静态页面,设置Rules校验函数,对手机号和密码实现校验。基础校验和统一校验。
  2. 使用token 信息作为用户登录的唯一标识,并且存储在LocalStorage中,通过Vuex 统一管理token ,并且实现token 的持久化。
  3. 利用 axios 中设置请求拦截器,在每次请求的请求头中,注入 token 信息,作为登录的标识。
  4. 配合Vue-Router 中的beforeEach 前置导航守卫函数,实现对 token 信息的统一监测,和拦截登录。

3.主页模块的实现思路

  1. 登录成功后,根据业务的需求,配合suss 实现对样式的二次修改。
  2. 初始化 Vuex 中的 mutations 信息,更新登录后用户的信息收集,封装 action 获取用户资料
  3. 利用 Vuex 中的 getters 属性,完成用户登陆后的视图层渲染
  4. 封装 action ,实现用户退出登录,调用 commit 方法,清除 Vuex 中保存的 token 信息
  5. 根据后端检测 token 返回的状态码,设置拦截器,对失效 token 信息实现拦截登录,并提示用户token失效

4.登录流程

  1. 拉取 vue-admin-template 代码,进行一些改造。

  2. 设计并且开发登录页面,进行表单校验,基础校验 + 统一校验

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!--
    基础校验
    el-form :model="表单对象" :rules="规则对象"
    el-form-item prop属性指定一下要使用哪条规则
    el-input v-model双向绑定

    统一校验
    1. 获取表单的实例对象
    2. 调用validate方法 const res = await this.$refs.loginForm.validate()
    -->
  3. Vuex 中实现用户的模块

    1. 删除模板中原有的内容进行重写

    2. 开启命名空间,导出 vuex 子模块

    3. 实现 token 的vue数据持久化

      1
      2
      1.Token数据时,一份存入vuex,一份存入cookie
      2. vuex中初始化Token时,优先从本地cookie取,取不到再初始化为空串儿
    4. 实现登录的actions方法

      1
      2
      3
      4
      5
      6
      7
      login() {
      this.$refs.form.validate((isOK) => {
      if (isOK) {
      this.$store.dispatch("user/login", this.loginForm)
      }
      })
      }
  4. Vue-cli 代理解决跨域:配置文件可以直接配置代理 vue.config.js -> devServer -> proxy -> api

  5. axios 二次封装,将 axios 请求方法,封装到 request 模块

    1. 配置基础地址 ,超出时间

    2. 请求拦截器 - 根据获取仓库中的token 来判断 然后 在header 中统一注入 token

    3. 响应拦截器 - 解构数据 - 处理异常

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      // 通用配置
      // 1. axios实例化 axios.create() 基地址配置 baseURL + 超时时间 timeout(100ms)
      // 拓展:create方法可以调用多次 每次执行都会生成一个独一无二的实例
      // export default const a = asiox.create({ baseURL: 'a.com' })
      // export default const b = asiox.create({ baseURL: 'b.com' })

      // 2. 请求拦截器 请求头中添加token数据 接口鉴权 统一配置
      // 客户端发送请求 - 请求拦截器(针对请求参数做处理) - 后端
      // 拓展:可以添加多个请求拦截器
      // 客户端请求 - 拦截器1(处理参数) - 拦截器2 - 后端
      // 最后一定要return config 失败就执行promise.reject(error)

      // 3. 响应拦截器 数据剥离 res.data / token失效401错误处理 / 前端自定义错误处理?
      // 后端 - 响应拦截器 - 客户端
      // 成功回调 200-300
      // 失败回调 不在这个之间
      // axios默认包裹了一层data 把响应数据解构出来 判断如果业务成功 返回用户所需要的数据 业务失效 错误提示 return 一个error (new一个)
      // // 所有的响应错误信息,统一处理

  6. 封装 api 接口

    将请求封装成方法,统一存放到 api 模块,与页面分离,可维护性高

    新建 api/user.js 提供注册 Api 函数

    1
    2
    3
    4
    5
    6
    import request from '@/utils/request'

    // 注册接口
    export const register = (data) => {
    return request.post('/user/register', data)
    }
  7. 区分环境(表单)

    1. 开发环境 developent
    2. 生产环境 production
  8. 登录联调

    1. 封装登录的api
    2. vuex 中的用户模块调用登录接口
    3. 登录成功后,跳转到主页
    4. 区分表单不同环境下的不同数据
  9. 主页鉴权验证

    1. 访问主页-有token放过,没有token跳到登录页
    2. 访问登录-有token跳到主页,没有token放过
    3. 前置守卫 - beforeEach 中 来处理逻辑 白名单

5.智慧园区

1.搜索功能

思路分析:把各种搜索条件当作请求参数发给后端 后端会根据字段对数据库数据做过滤筛选拿到符合条件返回

        1. 表单组件的双向绑定收集到当前的请求数据
        2. 把收集到的表单参数发送接口给后端那符合条件的数据
        3. 把拿到的数据关系显示在列表中

2.excel导出

  1. 实际开发过程中的导出

    1. 前端主导(xlsx)

      流程:调用列表接口把要导出的数据拿到 -> 数据的二次转化 -> [excel 工作簿 - 工作表 - 单元格数据] - 使用xlsx创建一个工作簿 - 使用xlsx方法创建一个工作表 - 把工作表添加到工作簿 – [使用中文替换中文表头] 调用xlsx的导出方法

      工作中遇到了需求,参考代码 换接口 数据二次处理 处理表头

    2. 后端主导(最常见)

      流程:前端直接调用导出接口 - 后端会把数据转换成excel文件流当成返回值返回 - 直接触发浏览器的下载功能

  2. 两种方案的本质区别:

    把数据转化成excel的过程发生在哪里?如果发生在浏览器 前端主导 如果发生在后端服务器 后端主导

    前端主导 - 处理数据量不能太大

    后端主导 - 适合处理量大或量小都可

3.网络请求的优化

  1. 场景: 限制了请求个数 保证只有打开时才请求
  2. 怎么做到?
    1. 判断第一个参数row是否能在第二个参数rows中找到 如果能找到代表打开了
    2. find 通过匹配找到符合条件的第一项 然后把找到的项返回
    3. findIndex 通过匹配找到符合条件的第一项 然后把找到项的下标值返回 splice(index,1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 只有展开时获取数据并绑定
async expandHandle(row, rows) {
// console.log('展开或关闭', row, rows) 点击行row.id rows数组对象
// row: 当前行的对象 rows数组对象
// 1. 先拿到当前行的数据
// 2. 使用当前行的企业数据,获取下面的合同列表数据
// 3. 把拿到的合同列表存入企业对象中 但是row里面没有存放的位置

// 优化网络请求 只在打开时才去触发 核心:拿到当前是打开的条件 做判断
// 判断条件:第一个row是否能在第二个rows中找到 如果找到了 代表打开了 如果找不到 代表收起了
// find findIndex
const isExpend = rows.find(item => item.id === row.id)
if (isExpend) {
// 如果找到了这一项,才回去调用接口
const res = await getRentListAPI(row.id)
// eslint-disable-next-line require-atomic-updates
row.rentList = res.data

}

4.后端返回数据字段不够前端自定义字段

默认的返回的企业列表数据中没有一个存放合同列表的数据的数组

1
2
3
4
5
6
this.list = res.data.rows.map(item => {
return {
...item,
rendList: [] // 通过映射自定义添加字段
}
})

5.树状组件怎么实现的?

树形组件:用层级结构展示信息,可展开或折叠。

  1. element-ui提供了树组件el-tree的应用

  2. 组件中放置树形组件

  3. 获取后台的数据

  4. 递归转化树形结构

    1. 首先分析数据的关联关系
    2. 封装递归函数根据关联关系转化层级结构
    3. 通过分析了解到,父级的id为子级的pid
    4. 封装公共方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /**
    *
    * 列表型数据转化树形
    */

    export function transListToTreeData(list, rootValue) {
    const arr = []
    list.forEach(item => {
    if (item.pid === rootValue) {
    // 找到了匹配的节点
    // 当前节点的id 和 当前节点的子节点的pid是想等的
    const children = transListToTreeData(list, item.id) // 找到的节点的子节点
    item.children = children // 将子节点赋值给当前节点
    arr.push(item)
    }
    })
    return arr
    }

    // 将列表型的数据转化成树形数据 => 递归算法 => 自身调用自身 => 一定条件不能一样, 否则就会死循环
    // * 遍历树形 有一个重点 要先找一个头儿

6.记住我优化

  1. 基础实现逻辑
    如果当前用户选中了记住,在登录时把用户的信息存入本地 在组件初始化的时候去取数据 回填
    如果当前没有选中,在登录时把数据清空

7.数据基础渲染

  1. 基础实现逻辑
    1. 准备静态模版(elementUI)
    2. 解决初始报错 在data中把模版绑定的数据都声明一遍
    3. 封装接口(url/ method / 参数[名称 + 类型 + 参数数量])
    4. 组件中封装一个独立的方法 在方法中调用接口函数 (复用的好处 调用之前做一些额外的参数处理)
    5. 选择一个合适生命周期钩子函数调用独立的方法 (created / mounted 都可以)
    6. 使用数据渲染模版 (数据驱动视图)

8.列表基础渲染

  1. 实现步骤

    1. 按照接口文档请求列表接口封装一下(url/method/参数)
    2. data准备响应式的数据(以后端接口实际返回为主)
    3. 在methods封装一个方法(参数的二次处理 + 调用接口 + 数据赋值)
    4. 生命周期钩子函数调用这个方法(created / mounted)
    5. 把响应式数据绑定组件身上(文档组件要求通过什么属性绑定就通过生命属性)
  2. 分页功能

    1. 分页的逻辑
      页数 = 总条数 / 单页的条数

    2. 组件分好页
      传入总数 :total=”100”
      单页条数 :pageSize=”2” 默认10

      1
      2
      3
      4
      5
      6
      7
      8
      9
      <!-- 
      1. 页数分出来 (页数 = 总条数 / 每页条数)
      2. 点击每页的时候获取当前页的数据重新渲染到table上
      -->
      <el-pagination
      layout="total, prev, pager, next"
      :page-size="params.pageSize"
      :total="total"
      />
    3. 点击分页交互的实现

      1. 点击时拿到当前点击的页数(父组件从子组件获取内部的数据 子传父)
        @current-change=”pageChange”

        1
        2
        3
        4
        5
        6
        7
        pageChange(page) { 
        // console.log(page) // 回调参数 拿到的是当前页
        // 把点击的页数赋值给请求参数页数
        this.params.page = page
        // 使用最新的请求参数获取列表数据
        this.getCardList()
        }
      2. 使用当前的页数去后端要当前页的数据重新渲染到table

        1
        2
        this.params.page = page
        this.getList()

9.状态适配

  1. 场景
    后端返回的数据无法直接显示到页面中 0/1 男女

  2. 转化的状态码数量只有两个
    解决方案:三元表达式 status === 0? ‘女’ :’男’

    转化的状态码比较多
    解决方案:映射的方案

    1
    2
    3
    4
    5
    6
    7
    8
    function format(status) {
    const MAP = {
    0:'女',
    1:'男'
    }
    return MAP[status]
    }

带有模板的状态格式化

  1. 直接可以把后端的数据渲染出来 prop指定要渲染的字段

  2. 不能直接渲染 需要格式化 格式化出来的内容一九是一个文本 string

    1. :formatter=”formatStatus” 不需要手动传参 自动传入
    2. 插槽 + 插值表达式 [渲染出来的是函数的返回值]
    1
    2
    3
    4
    <template default="{row}">
    {{ formatStatus(row.status) }}
    </template>

10.上传图片

  1. 上传的流程
    点击上传按钮 -> el-upload [打开本地文件选择框 + 上传前的文件校验] -> File ->
    new FormData()[接口要求传递一个formdata类型的数据] -> 往formData的对象中append字段
    -> append(‘file’,file) append(‘type’,’business…’) -> 使用完整的formData对象提交接口完成 上传

  2. 细节问题

    1. el-upload
      1. 非常简单 不需要做任何的自定义配置 默认的配置项完成上传就行了
      2. 需要自定义场景 :http-request=’function’
      3. 如果添加了上传前的校验 流程:先执行上传前的校验函数 函数返回值为true 再执行upload上传函数
      4. 函数参数 :上传前的函数 file对象 [size / type做校验] 上传函数 res对象 { file:File }
    2. 上传接口接口参数
      1. 常规的接口 contentType application/json
      2. 上传接口 contentType application/form-data
    3. 前后端校验逻辑要保持一致
      文件大小 小于5m / 文件类型 jpeg 前端要以这个为主
      [因为接口不只是可以通过浏览器提交 也可以通过其他方式提交 基于界面操作类校验会失败]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!--
:http-request="uploadRequest" 自定义上传
action 本来是一个用来配置默认上传的接口地址
因为我们覆盖了 所以用一个 # 占个位置 消除必填警告
input type="file" 本身具备选择文件的能力
覆盖原因:默认的配置上传不够灵活 仅支持一些简单的上传

如果想要完全自定义上传 http-request
在选择文件之后 自动执行upload函数 并且把一个对象传给我们
对象中有一个file属性 就是我们要上传的对象

上传前校验:
1. 上传图片之前加一层校验 目的为了限制用户上传的文件类型和大小
2. 如果我们添加了beforeUpload这个属性方法 这个函数中必须return的数据为true
才会继续执行 upload 方法 如果校验不通过 暂停执行 不会走上传逻辑
3. file对象中两个属性
size: 文件大小 / 1024/1024 = M
type: 文件类型 image/文件类型
-->

<el-form-item label="营业执照">
<el-upload
action="#"
:http-request="uploadRequest"
>
<el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div>
</el-upload>
</el-form-item>

<!--
上传实现流程:
1. el-upload 打开本地文件 并且校验这个文件是否符合要求 - File
2. :http-request = 'upload'
3. 按照接口的要求格式 得到类型为FormData对象 new FormData()
4. 按照要求往 formData中添加字段数据 fd.append('字段名','字段值')
5. 调用上传接口
6. 拿到返回的文件地址和其有用的信息id 存入data中的响应式数据的位置 将来提交表单
-->

11.菜单路由控制权限

  1. 权限数据的生成(RBAC)
    1. 新增一个角色 给角色配置权限数据
    2. 新增一个员工 给员工分配这个角色 员工就有了当前角色下所有的权限数据
  2. 完整的实现流程
    1. 调用接口获取当前员工的权限数据 permission [‘park:building:add_edit’]

      1. 在Vuex中编写逻辑,user/state里存放个人用户信息
      2. action里调用时,把permissions目标数据return出去 给另一个js模块使用
      3. permission文件中触发action,获取用户信息(有token时)
    2. 对权限数据做格式化处理 产生两个权限数据 一级路由权限数组 + 二级路由权限数组

    3. 把路由表拆分成两部分 动态路由表[需要加权限控制] + 静态路由表[不需要加权限控制]

      1. 拆分动态路由表导出使用 asyncRoutes
      2. 初始化时候只处理静态路由表 routes: […routes]
    4. 以一级和二级权限数组作为对主动态路由表做过滤筛选处理 -> 有资格加入到路由系统中的动态路由表

      1. 使用一级权限点过滤一级路由 使用二级权限点过滤二级路由 最终得到显示左侧的路由表
      2. 调用函数获取最终的动态路由
    5. 调用router的addRoute方法把动态路由表依次添加到路由系统中 访问url可以渲染对应的组件

    6. 使用动态路由表数据通过存入Vuex然后利用它响应式的特性 渲染到左侧菜单中

      1. vuex新增一个模块,menu模块,先以静态的路由表作为初始值
      2. 在得到过滤之后的动态路由表之后,和之前的静态做一个结合
      3. 在sidebar组件中结合v-for指令做使用Vuex中的数据做渲染
    7. 解决切换用户有缓存的bug 方案:在用户退出登录时

      1. 调用清空路由的reset方法

      2. 手动把Vuex中的数据也清空

      3. 用户信息也清空

  3. 每一个独立的小功能封装成一个独立的小函数 维护方便
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (!store.state.user.profile.id) {
// 1. 调用action函数获取用户权限数据
const permissions = await store.dispatch('user/getProfile')
// 2. 把后端的权限数组格式化成我们自己的俩个权限数据
console.log('当前的权限数据为:', permissions)
const firstRoutePerms = getFirstRoutePerms(permissions)
console.log('一级路由权限', firstRoutePerms)
const secondRoutePerms = getSecondRoutePerms(permissions)
console.log('二级路由权限', secondRoutePerms)
// 3. 根据权限标识过滤路由表 最终得到显示到左侧的路由表
const routes = getRoutes(firstRoutePerms, secondRoutePerms, asyncRoutes)
console.log('最终路由表', routes)
// 4. addRoute动态添加 (当浏览器中访问路由的路径 显示渲染出来对应的组件)
routes.forEach(route => router.addRoute(route))
// 5. 存入Vuex渲染左侧菜单
store.commit('menu/setMenuList', routes)
}

12.按钮控制权限

概念:根据当前用户的权限数据控制按钮的显示和隐藏

思路:每一个需要做权限控制的按钮都有一个自己独有的 标识 , 如果标识可以在权限数据列表中找到,则显示,找不到就隐藏
方案:应用中可能会有很多按钮需要做权限控制,所以方案一定是全局生效的,我们提供俩种可选的方案

全局指令方案 和 高阶组件 方案。

13.微前端

微前端是一种前端架构模式,它将大型单体应用程序分解为小的、松散耦合的部分,每个部分都可以独立开发、测试和部署。

使用乾坤方案进行改造.

1. 基础运行原理

  1. 监听路由变化 实现一个注册子应用配置的方法

  2. 匹配子应用 实现路由的匹配

  3. 加载子应用 实现加载子应用

  4. 渲染子应用 实现子应用的渲染

  5. 前端加载3D模型

    1. 下载模型解析包
    2. 拉取模型并渲染
    3. 添加进入条
  6. 大屏适配

    1. 安装组件 npm i v-scale-screen
    2. 使用组件并制定高度

开发中遇到的问题

  1. Vue 中使用样式无法生效 deep

  2. 解决history页面404访问问题

    1. 当我们刷新页面,发现404

    这是因为我们采用了history的模式,地址的变化会引起服务器的刷新,我们只需要在app.js对所有的地址进行一下处理即可

  3. 在 Vue 中使用 axios 下载 excel 文档,出现乱码

    4.使用 webpack 打包项目,导致图片无法显示 : 文件路径错误 配置问题:Webpack配置文件中是否有正确的loader配置来处理图片文件。

1.refresh_token

在调用登录接口成功后会返回 tokenrefreshTokentoken 有效时间设置了 8 个小时,refreshToken 有效时间设置了 3 天,当 token 失效后 refreshToken 仍然有效,此时可以通过 refreshToken 来为 token 续期,所谓的续期就是调用后端提供的一个接口,然后把 refreshToken 发送给服务端,服务端重新返回新的 tokenrefreshToken

  1. 判断当前的 token 有没有失效可以根据接口返回的状态码进行判断,当 token 失效后会返回 401,在响应拦截器中可以统一获取所有接口返回的状态码,然后对其进行判断

  2. 要想通过应用实例 getApp 来读取 refreshToken 必须提前读取本地存储的数据并存储到应用实例当中

  3. 调用接口把 refreshToken 发送给服务端换取新的 tokenrefreshToken

    接口中所需要的接口 refreshToken 需要通过自定义的头信息 Authorization 来传递

2.实现vant组件自动按需加载,和自动导入

  1. 安装 yarn add unplugin-vue-components -D
  2. 配置 plugins 里的 Components

3.@vueuse/core

介绍 @vueuse/core 组合api库,使用 useXxx 函数获取设备宽度,动态设置滚动距离

@vueuse/core 介绍:文档

  • 是一个基于 组合API 封装的库
  • 提供了一些网站开发常用的工具函数,且得到的是响应式数据

需求:

  • 在 375 宽度设备,滚动宽度为 150
  • 在其他设备需要等比例设置滚动的宽度
  • scrollWidth = 150 / 375 * deviceWidth 就可以适配

@vueuse/core 应用:

1
pnpm add @vueuse/core
1
2
3
import { useWindowSize } from '@vueuse/core'

const { width } = useWindowSize()

小结:

  • 如果遇见一些常见的需求可以先看看 @vueuse/core 是否提供,这样可以提高开发效率。
    • 如果:窗口尺寸,滚动距离,是否进入可视区,倒计时,…等等。

4.极速问诊

image-20231028232009299.png

5.问诊室-websocket介绍

什么是 websocket ? https://websocket.org/

  • 是一种网络通信协议,和 HTTP 协议 一样。

为什么需要websocket ?

  • 因为 HTTP 协议有一个缺陷:通信只能由客户端发起。

我们项目中使用 socket.io-client 来实现客户端代码,它是基于 websocket 的库。

问诊室-socket.io使用

目的:掌握 socket.io 的基本使用

  1. socket.io 什么?
    • socket.io 是一个基于 WebSocket 的 CS(客户端-服务端)的实时通信库
    • 使用它可以在后端提供一个即时通讯服务
    • 它也提供一个 js 库,在前端可以去链接后端的 socket.io 创建的服务
    • 总结:它是一套基于 websocket 前后端即时通讯解决方案
  2. socket.io 如何使用?
1
pnpm add socket.io-client

如何建立连接?

1
2
3
4
import io from 'socket.io-client'
// 参数1:不传默认是当前服务域名,开发中传入服务器地址
// 参数2:配置参数,根据需要再来介绍
const socket = io()

如何确定连接成功?

1
2
3
socket.on('connect', () => {
// 建立连接成功
})

如何发送消息?

1
2
// chat message 发送消息事件,由后台约定,可变
socket.emit('chat message', '消息内容')

如何接收消息?

1
2
3
4
// chat message 接收消息事件,由后台约定,可变
socket.on('chat message', (ev) => {
// ev 是服务器发送的消息
})

如何关闭连接?

1
2
// 离开组件需要使用
socket.close()

小结:

  • sockt.io

    在前端使用的js库需要知道哪些内容?

    • 如何建立链接 io('地址')
    • 连接成功的事件 connect
    • 如何发消息 emit + 事件
    • 如何收消息 on + 事件
    • 如果关闭连接 close()

6. Vue 项目权限处理

现在权限相关管理系统用的框架都是element提供的vue-element-admin模板框架比较常见。

权限控制常见分为三大块

  • 菜单权限控制
  • 按钮权限控制
  • 请求url权限控制。

权限管理在后端中主要体现在对接口访问权限的控制,在前端中主要体现在对菜单访问权限的控制。

  1. 按钮权限控制比较容易,主要采取的方式是从后端返回按钮的权限标识,然后在前端进行显隐操作 v-if / disabled。

  2. url权限控制,主要是后端代码来控制,前端只需要规范好格式即可。

  3. 剩下的菜单权限控制,是相对复杂一些的

    (1) 需要在路由设计时, 就拆分成静态路由和动态路由

    ​ 静态路由: 所有用户都能访问到的路由, 不会动态变化的 (登录页, 首页, 404, …)

    ​ 动态路由: 动态控制的路由, 只有用户有这个权限, 才将这个路由添加给你 (审批页, 社保页, 权限管理页…)

    (2) 用户登录进入首页时, 需要立刻发送请求, 获取个人信息 (包含权限的标识)

    image-20210309031043592

    (3) 利用权限信息的标识, 筛选出合适的动态路由, 通过路由的 addRoutes 方法, 动态添加路由即可!

    (4) router.options.routes (拿的是默认配置的项, 拿不到动态新增的) 不是响应式的!

    ​ 为了能正确的显示菜单, 为了能够将来正确的获取到用户路由, 我们需要用vuex管理routes路由数组

    (5) 利用vuex中的 routes, 动态渲染菜单

7. 如何处理 打包出来的项目(首屏)加载过慢的问题

SPA应用: 单页应用程序, 所有的功能, 都在一个页面中, 如果第一次将所有的路由资源, 组件都加载了, 就会很慢!

加载过慢 => 一次性加载了过多的资源, 一次性加载了过大的资源

  • 加载过多 => 路由懒加载, 访问到路由, 再加载该路由相关的组件内容
  • 加载过大 => 图片压缩, 文件压缩合并处理, 开启gzip压缩等

比如:

  1. 配置异步组件, 路由懒加载

    1
    const login = () => import('../pages/login.vue')
  2. 图片压缩: 使用 webp 格式的图片, 提升首页加载的速度

  3. CDN加速: 配置CDN加速, 加快资源的加载效率 (花钱)

  4. 开启 gzip 压缩 (一般默认服务器开启的, 如果没开, 确实可能会很慢, 可以让后台开一下)

8. 你在项目中遇到过什么技术难题

问题: 考察解决问题的能力!

话术: 前端要学的东西确实很多,但是并不夸张, 肯多花点时间沉淀一般都会有解决方案

一般遇到难题 (这些前端所谓的难题, 一般都是一些没有做过, 没有尝试过得一些业务), 我们要时刻保持独立思考,

知道自己要做什么业务由此决定要学什么知识, 然后实现业务, 举一反三,总结归纳!

比如1: 如果之前没有做过国际化, 换肤, 没有做过支付, 权限控制, 没有做过即时通信websocket, excel导入导出, 就会觉得很难,

但其实真正上手花时间去学着做了, 也都能逐步思考解决相关页面, 这些其实也都还 ok

比如2: 有时候, 复杂的或者困难的, 并不是技术层面的, 而是业务需求方面的, 需要进行大量树形结构的处理

展示列表式数据时, 展示图表数据时, 筛选条件关联条件多了, 组件与组件的联动关系的控制也比较麻烦,

将联动的条件, 存vuex, 然后 => 进行分模块管理也是比较合适的选择

组件封装

1.nav-bar 组件结构

掌握:van-nav-bar组件的基础使用,抽取到 cp-nav-bar 组件,作为通用组件

2.radio-btn 组件封装

实现:按钮组单选框组件

3.支付抽屉组件封装

  • 组件需要实现哪些功能?
    • 展示微信支付和支付宝支付,可以选择
    • 展示支付金额,传入订单ID用于生成订单支付链接
    • 打开关闭抽屉
    • 关闭后的业务可自定义
  • 需要暴露哪些 props 参数?
    • orderId actualPayment onClose show
  • 需要提供哪些 emits 事件?
    • update:show

4.将树形的操作内容单独抽提成组件

**目标**: 将树形的操作内容单独抽提成组件

封装单独的树操作栏组件

通过第一个章节,我们发现,树形的顶级内容实际和子节点的内容是一致的,此时可以将该部分抽提成一个组件,节省代码

5.封装一个通用的工具栏

**目标**:封装一个通用的工具栏供大家使用

6.新增员工的弹层组件

7.个人详情组件和岗位详情组件封装

8.封装上传图片组件