面试题

# 面试题

# CSS

# 布局方式

  • 让不定宽高的元素水平居中
/* 1. */
.father {
  position: relative;
}
.child {
  position: absolute;
  left: 50%;
  transfrom: translateX(-50%);
}

/* 2 table居中*/
.father {
  display: table;
}
.child {
  display: table-cell;
}

/* 3.flex居中 */
.father {
  display: flex;
  align-items: center;
}

/* 4.如果是块元素 */
.child {
  margin-left: auto;
  margin-right: auto;
}

/* 5.如果行内块,图片,文字,块元素 */
.father {
  text-align: center;
}
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
  • 两边固定中间自适应
<!-- 方法1.使用定位 -->
<style>
  .box {
    position: relative;
  }

  .left {
    position: absolute;
    width: 200px;
    left: 0;
  }

  .right {
    position: absolute;
    width: 200px;
    right: 0;
  }

  .center {
    margin-left: 200px;
    margin-right: 200px;
    width: 100%;
  }
</style>
<body>
  <div class="box">
    <div class="left"></div>
    <div class="center"></div>
    <div class="right"></div>
  </div>
</body>

<!-- 方法2. 使用浮动 -->
<style>
  .left {
    float: left;
  }
  .right {
    float: right;
  }
  .center {
    overflow: hidden;
  }
</style>
<body>
  <!-- 注意, 盒子的位置需要发生变化 -->
  <div class="box clearfix">
    <div class="left"></div>
    <div class="right"></div>
    <div class="center"></div>
  </div>
</body>
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
40
41
42
43
44
45
46
47
48
49
50
51
52

# 盒子模型

  • 一种是 IE 盒子模型 border-box 宽或高 = content + border + padding
  • 一种是标准盒子模型 content-box 宽或高 = content 设置这两种盒子模型: box-sizing: border-box/content-box;

# BFC 块级格式化上下文

# BFC 布局规则

  • 内部的 BOX 会在垂直方向,一个接一个的放置
  • BFC 是页面上的一个隔离的独立容器
  • box 垂直方向的距离是由 margin 决定的,属于同一 BFC 的两个相邻元素 margin 会重叠
  • 每个元素的左边,与包含的盒子的左边相接触,即使存在浮动也如此.
  • BFC 的区域不会与 float 重叠
  • BFC 就是页面上一个隔离的容器,容器里面的子元素不会影响到外面的元素.反之也是如此
  • 计算 BFC 的高度时,浮动元素也参与计算.

# 哪些元素会生成 BFC

  • float 不为 none
  • position 不为 static/relative
  • overflow 不为 visible
  • display 的值为 table table-cell table-caption
  • 根元素

# 怎么防止 margin 重叠

  • overflow:hidden;

# 有哪些 BFC 应用

  • 左侧/右侧固定,右侧/左侧 自适应
<style>
  .left {
    float: left;
  }
  .right {
    overflow: hidden;
  }
</style>
1
2
3
4
5
6
7
8

# 弹性布局

  • initial 设置它的属性为它的默认值

  • inherit 让父元素继承该属性

  • 主轴对齐方式 justify-content: flex-start | flex-end | center| baseline

  • 侧轴对齐方式 align-items: flex-start | flex-end | center | space-between | space-around

  • 改变主轴方向 flex-decoration: row | row-reverse | column | column-reverse | initial | inherit

    • row(默认值)水平显示 | 与 row 相同,顺序相反 | 垂直显示 | 垂直显示, 顺序相反 | | 从父元素继承该属性值
  • 给 flex 布局盒子固定宽度: flex-basis: number | auto | initial | inherit

# JS

# 怎么理解闭包(只有 js 特有的属性)

  • 什么是闭包?
    • 闭包是指有权访问另一函数作用域中的变量的函数
  • 闭包产生的条件
    • 函数在定义时的词法作用域以外的地方被调用就会产生闭包(词法作用域: 定义在词法阶段的作用域)
    • 当内部函数 访问了外部函数的局部变量才会产生闭包.
      • 可以通过控制台 sources -> Call Stack -> Closure(如果产生了闭包才会有 Closure)
  • 创建闭包的常见方式,就是在一个函数内部创建另一个函数
  • 闭包的用处
    • 私有化变量
    • 可以读取函数内部的变量
    • 让这些变量始终保持在内存中
  • 闭包的问题:
    • 会占用一块内存,
function f1() {
  var n = 999;
  function f2() {
    console.log(n);
  }
  return f2;
}
var result = f1();
result(); // 999nAdd();result(); // 1000

// 如果想要释放这块内存
result = null;

// 类似闭包,实际并不是闭包  ,因为它并没有在当前词法作用域以外调用
function f1() {
  var n = 999;
  function f2() {
    console.log(n);
  }
  f2();
}
f1();

// 如果想要释放这块内存
result = null;
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

# 递归

# 斐波拉契数列

// 1,1,2,3,5,8,13,21,34,55,89,144   //数组的第n个是第 n-1 个和 n-2 个的和
function outer() {
  var arr = [];
  function fn(n) {
    if (n === 0 || n === 1) {
      return 1;
    }
    if (arr[n]) {
      return arr[n];
    } else {
      var temp = fn(n - 1) + fn(n - 2);
      arr[n] = temp;
      return temp;
    }
  }
  return fn;
}
var fn = outer();
var result = fn(100);
console.log(result);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 垃圾回收机制

  • 内存: 计算机中所有程序的运行都是在内存中进行的, 因此内存的性能对计算机的影响非常打,运行程序需要消耗内存,当程序结束时,内存会得到释放.

  • javascript 分配内存: 当我们定义变量,javascript 需要分配内存储存数据, 无论是值类型或引用类型,都需要储存在内存中.

  • 垃圾回收: 当代码执行结束,分配的内存已经不需要了,这个时候需要将内存进行回收,在 js 中,垃圾回收机器会帮我回收不再需要使用的内存

  • 标记清除法清除(现代浏览器都使用的是标记清除法)

    • 此算法假定设置一个叫做根(root)的对象, 在(js 中,根是全局对象 window),定期的让垃圾回收器从根开始,找到所有从根开始引用的对象, 然后找到这些对象的引用对象, 从根开始,垃圾回收器将找到所有可以获得的对象和所有不能获得的对象
  • 引用计数法清除

    • 如果没有引用指向某个对象(或者函数作用域),那么这个对象或函数作用域就会被垃圾回收机制回收
    • 下面这个例子使用引用计数法不会被回收
    var o = {
      name: obj,
    };
    var obj = {
      name: o,
    };
    
    1
    2
    3
    4
    5
    6

# 介绍 DOM 的三个阶段

  • 事件捕获
  • 目标阶段
  • 事件冒泡

# 封装事件监听和解绑的兼容写法

var myEvent = {
  addEvent: function (ele, event, func) {
    if (window.addEventListener) {
      ele.addEventListener(event, func, false);
    } else if (window.attachEvent) {
      ele.attachEvent(event, func);
    } else {
      ele['on' + event] = func;
    }
  },
  removeEvent: function (ele, event, func) {
    if (window.removeEventListener) {
      ele.removeEventListener(event, func, false);
    } else if (window.detachEvent) {
      ele.detachEvent('on' + event, func);
    } else {
      ele['on' + event] = null;
    }
  },
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 事件捕获的具体流程

  • window => document => html => body => ... => 目标元素

# event 对象有哪些常用应用

  • 1.阻止默认事件 e.preventDefault()
  • 2.阻止冒泡 e.stopPropagation()
  • 3.阻止相同事件的其他侦听器 e.stopImmediatePropagation()
  • 4.当前绑定事件的元素 e.currentTarget
  • 5.当前被点击的元素 e.target

# 自定义事件

var event = new Event('custome');
ele.addEventListener('custome', function () {}, false);
ele.dispatchEvent(event); // dispatchEvent() 方法给节点派一个合成事件
1
2
3

# 原型与原型链

# 理解

  • 所有的函数(包括构造函数) 都是 Function new 出来的,原型都是 Function.prototype
  • 所有的原型对象都是 Object(构造函数) new 出来的,原型都是 Object.prototype

# instanceof 的原理

  • A instance B 是指 B.prototype 是否在 A 的原型链上(简单理解就是沿着 A 的proto 跟 B 的 prototype 寻找,如果能找到同一引用返回 true,否则返回 false)

  • 示例

function Person() {}
var fn = new Person();
fn.__proto__ === Person.prototype;
fn.__proto__.__proto__.constructor === Object.prototype;

Object.__proto__ === Function.prototype;
// -------------------------------------
// Function是自身创建的
Function.__proto__ === Function.prototype;
// -------------------------------------
// Function.prototype 是对象 所以
Function.prototype.__proto__ === Object.prototype;
1
2
3
4
5
6
7
8
9
10
11
12

# 原型链的继承

// 1.普通继承
// 原理: 改变父类构造函数运行时的this指向
// 缺点: 只实现部分继承 ,父类原型对象上的属性/方法类取不到
function Person(name) {
  this.name = name;
}
function Ming() {
  Person.call(this, name);
}

// 2.借助原型链继承
// 原理: new Person() => Person.__proto__ === Ming.prototype => new Person() => new Person().__proto__ => new Person.prototype
// 缺点: 多个实例公用一个父类的实例对象,修改其中一个实例上的引用对象,会对其他造成影响
function Person(name) {
  this.name = name;
}
function Ming() {}
Ming.prototype = new Person();

// 3.组合继承
function Person(name) {
  this.name = name;
}
function Ming(name) {
  Person.call(this, name);
}
Ming.prototype = Person.prototype;

// 4.组合继承的完美解决方案
function Person(name) {
  this.name = name;
}
function Ming(name, age) {
  this.age = age;
  Person.call(this);
}
Ming.prototype = Object.create(Person.prototype);
Ming.prototype.constructor = Ming;
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

# new 运算符实现机制

  • 创建了一个新对象
  • 将构造函数的作用域赋给新对象(让 this 指向了这个对象)
  • 执行了构造函数(为这个新对象添加属性)
  • 返回新对象

# this 指向 (函数的 4 种调用模式)

  • 普通函数调用 this 执行 window
  • 对象调用 this 指向 调用的这个对象
  • new 构造函数 调用, this 指向 new 构造函数的实例
  • setTimeout setInterval this 指向 window
  • 箭头函数 ,this 指向上下文(context 也就是作用域)
  • call,apply 调用 this 指向你要替换的对象

# 改变 this 指向

  • call 使用 call(this, arg1,arg2,...) //会主动调用函数
  • apply 使用 apply(this, [...]) //会主动调用函数
  • bind 使用 bind(this) //不会主动调用函数

# 性能优化

# 提升页面性能的方法

  • 资源压缩合并,减少 http 请求
  • 非核心代码异步加载
  • 使用浏览器缓存
  • 使用 CDN 缓存
  • 预解析 DNS
  • HTTP 优化,如使用语义化标签,避免重定向
  • CSS 优化,如布局代码写在前面,根据需求加载网络字体,避免使用 css 表达式

# 异步加载的方式

  • async
  • defer
  • 动态脚本加载
    • var script = document.createElement('script')
    • script.type = 'text/javascript'
    • script.src = './js/aaa.js'
    • document.querySelector('head').appendChild(script)

# defer 与 async 的区别

  • defer 是在 html 解析后才会执行,如果有多个, 按加载顺序依次执行
// demo.js
// console.log('demo-js');
// -----------------
// demo1.js
// console.log('demo1-js');
// -----------------
// html结构
// <script src="./demo.js" defer></script>
// <script src="./demo1.js" defer></script>

// <script>
//   console.log('html-js');
// </script>

// 打印结果
// html-js
// demo-js
// demo1-js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  • async 是在加载完成后立即执行,如果是多个,执行顺序与加载顺序无关
// demo.js
// console.log('demo-js');
// -----------------
// demo1.js
// console.log('demo1-js');
// -----------------
// html结构
// <script src="./demo.js" async></script>
// <script src="./demo1.js" async></script>

// <script>
//   console.log('html-js');
// </script>

// 打印结果
/*
  html-js
  demo-js
  demo1-js
*/
// 或
/*
  html-js
  demo1-js
  demo-js
*/
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

# 什么是浏览器缓存

  • 资源文件在电脑磁盘上的备份

# 缓存的分类

  • 强缓存

    • 特点: 不请求,直接使用缓存
    • 相关 HTTP 头部字段:
      • Expires: 过期时间,是个绝对时间,下发的是服务器时间,比较用的是客户端时间,所有有偏差
      • Cache-control: 过期时间,是个相对时间,优先级高,以客户端的相对时间为准,浏览器拿到资源之后的多少时间内都不会再去服务器请求
  • 协商缓存

    • 特点: 浏览器不确定备份是否过期,需要与服务器请求确认.
    • 相关 HTTP 头部字段:
      • 1.Last-Modified/if-Modified-Since 服务器下发时间,客户端请求时间是带上下发时间,服务器判断文件是否过期,存在的问题服务器下发时间难以定义
      • 2.Etag/if-None-Match: 服务器下发 hash 值,客户端请求时带上 hash 值,服务器判断文件是否过期,优先级高.

# 什么是 CDN?

  • 特点: 在不同的地方缓存内容,将用户的请求定到最合适的缓存服务器上去获取内容
  • 优点: 解决 internet 网络拥堵,提高用户访问网络的相应速度

# 预解析 DNS

  • DNS 预解析会消耗前端的性能,优化建议: 减少 DNS 请求次数,进行 DNS 预解析
  • 方式: 让具有此属性的域名自动在后台解析,从而减少用户的等待时间,提升用户体验

# 如何开启 DNS 预解析

<meta http-equiv="x-dns-prefetch-control" content="on" />
(强制打开a标签的DNS预解析,https下默认关闭)
<link rel="dns-prefetch" href="//lanmaom.com" />
1
2
3

# 渲染机制

# DTD 是什么

  • 文档类型定义,浏览器会使用它来判断文档类型,从而决定使用何种协议来解析

# DOCTYPE 是什么

  • 文档类型声明,通知浏览器当前文件用的是哪个 DTD

# 浏览器的渲染过程是怎样的

  • 1.HTML 被解析成 DOM tree CSS 被解析成 CSS Rule tree (同时进行)
  • 2.在布局阶段, DOM tree 和 CSS Rule tree 整合成 render tree (如果上面 DOM tree 或 CSS Rule tree 有一个没有解析完成,布局阶段永远不会进行)
  • 3.元素按照算出来的规则,把元素放到它该出现的位置,通过显卡画到屏幕上

# 重排(Reflow)与重绘(Repaint)

  • 重排

    • 定义: DOM 中各个元素都有自己的盒子模型,需要浏览器根据样式进行计算,并根据计算结果将元素放到特定位置,这就是重排
    • 触发条件:
      • 增.删.改.移 DOM
      • 修改 CSS 样式
      • 改变浏览器窗口的大小
      • 页面滚动
      • 修改网页的默认字体
      • 获取一些 style 信息时 ,offsetWidth, offsetHeight, ...
  • 重绘

    • 当各种盒子的位置,大小以及其他属性改变时,浏览器需要吧这些元素都按照各自的特性回执一遍,这个过程为重绘
    • 触发条件:
      • DOM 改动
      • CSS 某些改动样式的改动,比如 颜色

# 如果减少重绘重排

  • 将多次改变样式属性的操作合并成一次操作
  • 将需要多次重排的元素,position:absolute/fixed;(脱离文档流),它的变化就不会影响到其他元素.例如有动画效果的元素
  • 将对一个元素复杂操作时,先 display:none;操作完成后再显示.这样只在隐藏和显示时触发两次重排.

# HTTP

# HTTP 的主要特点

  • 无状态
  • 无连接

# HTTP 报文的组成部分

  • 请求报文: 请求行 请求头 空行 请求体
  • 响应报文: 状态行 响应头 空行 响应体
    • 请求头: HTTP 方法,页面地址,http 协议及版本
    • 请求头: key-value 值,告诉服务端需要的内容
    • 空行: 告知服务端一下内容为请求体
    • 请求体: 数据部分

# 常见的 HTTP 方法

  • GET 获取资源
  • POST 传输资源
  • PUT 更新资源
  • DELETE 删除资源
  • HEAD 获得报文首部

# GET 和 POST 的区别是什么?

  • 优势

    • GET 请求回退是无害的,而 POST 请求回退会再次提交请求
    • GET 请求会被浏览器主动缓存, 而 POST 不会,除非手动设置
  • 劣势

    • GET 请求不安全,参数暴露在 URL 上,并且会完整保留在浏览器的历史记录里
    • GET 请求在 URL 中传递的参数是有长度限制的,POST 没有限制

# 状态码表示的含义是?

    1. 1XX 指示信息:请求已接收,继续处理
    1. 2XX 成功,请求已经被成功接收
    1. 3XX 重定向
    1. 4XX 客户端错误
    1. 5XX 服务器错误

# 常见状态码

  • 200 请求成功
  • 302 页面重定向
  • 304 服务器已执行 GET,但文件未变化
  • 401 请求未经授权,必须与 WWW-Authenticate 报头域一起使用
  • 404 请求资源不存在
  • 500 服务器发生了不可预期的错误
  • 503 请求未完成,服务器临时过载或宕机

# AJAX 的请求原理

var xhr = new XMLHttpRequest();
xhr.open('GET', '/abc/bcd', true);
xhr.send();
xhr.onreadystatechange = function () {
  if (xhr.readyState === 4 && xhr.status === 200) {
    console.log(xhr.responseText);
  }
};
1
2
3
4
5
6
7
8

# HTTP 的三次握手

  • 第一次握手: 建立连接.客户端发送请求报文,将 SYN(是一个标识)设置 1,Sequence Number 设置 x,客户端进入 SYN_SEND 状态,等待服务器确认
  • 第二次握手: 服务器收到 SYN 报文段。服务器收到客户端的 SYN 报文段,需要对这个 SYN 报文段进行确认,设置 Acknowledgment Number 为 x+1(Sequence Number+1);同时,自己自己还要发送 SYN 请求信息,将 SYN 位置为 1,Sequence Number 为 y;服务器端将上述所有信息放到一个报文段(即 SYN+ACK 报文段)中,一并发送给客户端,此时服务器进入 SYN_RECV 状态;
  • 第三次握手: 客户端收到服务器的 SYN+ACK 报文段。然后将 Acknowledgment Number 设置为 y+1,向服务器发送 ACK 报文段,这个报文段发送完毕以后,客户端和服务器端都进入 ESTABLISHED 状态,完成 TCP 三次握手

为什么三次握手

  • 防止已失效的请求报文段突然又传送到了服务端,因而产生错误

# 通讯类

# WebSocket 双工通信协议

为什么需要 WebSocket? HTTP 协议有一个缺陷: 通信只能由客户端发起. WebSocket 可以由服务端主动推送消息给客户端

// 客户端代码
var ws = new WebSocket('wss://echo.websocket.org');

ws.onopen = function (evt) {
  // 连接打开
  ws.send('Hello');
};

ws.onmessage = function (evt) {
  console.log('Received Message' + evt.data);
  ws.close();
};

ws.onclose = function (evt) {
  console.log('Connection closed');
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# Web Worker

作用: 为 javascript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行

# webWorker 使用注意点
  • 同源限制: 分配给 Worker 线程运行的脚本文件,必须与主线程脚本同源
  • DOM 限制: Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用 document,window,parent 这些对象.但是 Worker 线程可以使用 navigator 对象和 location 对象
  • 通信联系: Worker 线程和主线程不在同一个上下文环境,他们不能直接通信,必须通过消息完成
  • 脚本限制: Worker 线程不能执行 alert()方法和confirm()方法,但是可以使用 XMLHttpRequest 对象发出 AJAX 请求
  • 文件限制: Worker 线程无法读取本地文件,即不能打开本机的文件系统(file//),它所加载的脚本,必须来自网络
# 基本用法

主线程 主线程采用new命令,调用Worker()构造函数,新建一个 Worker 线程

var worker = new Worker('work.js');
worker.postMessage('Hello Worker');
worker.postMessage({ method: 'echo', args: ['Work'] });
worker.onmessage = function (event) {
  console.log(event.data);
  doSomething();
};
//关闭Worker
work.terminate();

function doSomething() {
  //执行任务
  worker.postMessage('work done');
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • Worker 构造函数的参数是一个脚本文件,改文件就是 Worker 所要执行的任务,因为 Worker 不能读取本地文件,所以这个脚本必须来自网络,或通过服务器的形式打开,否则 Worker 会默认失败
  • 然后主线程调用 worker.postMessage() 方法,向 Worker 发消息
  • worker.postMessage() 方法的参数,就是主线程 Worker 的数据,它可以是各种类型,包括二进制数据
  • 接着, 主线程通过worker.onmessage指定监听函数,接收子线程发回来的消息.
  • 上面代码中,事件对象的data属性可以获取 Worker 发来的数据
  • Worker 完成任务后,主线程就可以把它关掉了

Worker 线程 worker 线程内部需要有一个监听函数,监听message事件

// self 代表子线程自身,即子线程全局对象,因此 也可以写成 `this `或 `什么都不写 `
self.addEventListener(
  'message',
  function (e) {
    self.postMessage('you say' + e.data);
  },
  false
);
// 或
// 可以根据主线程发来的数据,worker 线程可以调用不同的方法,例如:
self.addEventListener(
  'message',
  function (e) {
    var data = e.data;
    switch (data.cmd) {
      case 'start':
        self.postMessage('WORKER STARTED: ' + data.msg);
        break;
      case 'stop':
        self.postMessage('WORKER STOPPED: ' + data.msg);
        self.close(); // Terminates the worker.
        break;
      default:
        self.postMessage('Unknown command: ' + data.msg);
    }
  },
  false
);
// 关闭Worker
self.close();
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

Worker 加载脚本 Worker 内部如果要加载其他脚本,有一个专门的方法importScripts()

importScripts('a.js');
// 也可以同时加载多个脚本
importScripts('a.js', 'b.js');
1
2
3

错误处理 主线程可以监听 Worker 是否发生错误,如果发生错误,Worker 会触发主线程的error事件

worker.onerror(function (event) {
  console.log(
    ['ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message].join('')
  );
});

// 或者
worker.addEventListener('error', function (event) {
  // ...
});

// Worker 内部也可以监听 error 事件
1
2
3
4
5
6
7
8
9
10
11
12
# iframe 怎么通信

通过 postMessage 方式通信 otherWindow.postMessage(message, targetOrigin, [transfer]);

  • otherwindow: 其他窗口的引入, 例如 iframe 的 contentWindow, 执行 window.open 返回的窗口对象,或者命名过数值索引的 window.iframes
  • message: 将要发送的数据
  • targetOrigin 指定那些窗口能接受到消息事件, 值可以是字符串 * 表示无限制,或者是一个URL
  • transfer: 是一串和 message 同时传递的 transferable 对象, 这些对象的所有权将被转移给消息的接收方,而发送方将不再保留所有权

postMessage 方法被调用时,会在所有页面脚本执行完毕后像目标窗口派发一个 MessageEvent消息, 改 MessageEvent 消息有四个属性需要注意:

  • type: 表示该 message 的类型
  • data: 为 postMessage 的第一个参数
  • origin: 表示调用 postMessage 方法窗口的源
  • source: 记录调用 postMessage 方法的窗口对象
// 父页面向子页面传参
window.onload = function () {
  ele.contentWindow.postMessage(message, targetOrigin); //例如 http://127.0.0.1:8888
  //  或者
  window.frames[0].postMessage(message, targetOrigin); //例如 http://127.0.0.1:8888
};

// 子页面向父页面传参
top.postMessage(message, targetOrigin); //例如: http://127.0.0.1:9999/father.html ,精确到页面
1
2
3
4
5
6
7
8
9

父页面或子页面接收参数(都是一样的写法)

window.addEventListener('message', function (event) {
  console.log(event.data); //子页面传来的数据
});
1
2
3

测试代码

<!--
  注意在测试的时候
    1.需要通过Server打开
    2.路径不能写成 localhost ,如果是本地,请写: http://127.0.0.1
-->

<!-- iframe1 -->
<!-- father.html -->
<body>
  <h1>主页面</h1>
  <iframe id="myFrame" src="http://127.0.0.1:8888/son.html"></iframe>
  <div>
    <h2>主页面接收消息区域</h2>
    <span id="message"></span>
  </div>

  <script type="text/javascript">
    window.addEventListener(
      'message',
      function (event) {
        console.log('父元素收到信息了:', event.data);
      },
      false
    );

    window.onload = function () {
      document
        .getElementById('myFrame')
        .contentWindow.postMessage('father said', 'http://127.0.0.1:8888');
      // 或者
      // window.frames[0].postMessage('123', 'http://127.0.0.1:9999')
    };
  </script>
</body>

<!-- iframe2 -->
<!-- son.html -->
<body>
  <h2>子页面</h2>
  <div>
    <h3>接收消息区域</h3>
    <span id="message"></span>
  </div>
  <script>
    //发送消息
    setInterval(function () {
      var message = 'Hello!  The time is: ' + new Date().getTime();
      window.top.postMessage(message, 'http://127.0.0.1:9999/father.html');
    }, 4000);

    window.addEventListener(
      'message',
      function (event) {
        console.log(event.data);
        // top.postMessage('消息收到', 'http://127.0.0.1:9999/father.html')
      },
      false
    );
  </script>
</body>
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

# 什么是跨域通信

  • 一个域下的文档或脚本试图去请求另一个域下的资源

# 前后端的通信有哪些

  • ajax: 同源下的通信
  • WebSocket: 不受同源策略限制
  • CORS: 支持跨域通信,也支持同源通信

# 跨域的解决方案有哪些

  • 0.document.domain 跨域
  • 1.jsonp 跨域
  • 2.location.hash 跨域
  • 3.postMessage 跨域
  • 4.WebSocket 跨域
  • 5.CORS 跨域(后台设置响应头)
  • 6.反向代理(在 vue 中经常用到)

  • 0.document.domain

    场景: 两个网页一级域名相同,只是二级域名不同 方案: 浏览器允许通过设置 document.domain 共享 Cookie.这种方法只适用于 Cookie 和 iframe 窗口,LocalStroge 和 IndexDB 无法通过这种方法

  • 1.jsonp 跨域

// 客户端实现:
<script>
  var script = document.createElement('script');
  script.type = 'text/javascript';

  // 传参并指定回调执行函数为onBack
  script.src = 'http://www.domain2.com:8080/login?user=admin&callback=onBack';
  document.head.appendChild(script);

  // 回调执行函数
  function onBack(res) {
    alert(JSON.stringify(res));
  }
</script>
// 服务端(返回时即执行全局函数) onBack({"status": true, "user": "admin"})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • 2.location.hash 跨域

    场景: 当前页面 A 通过 iframe 或 frame 嵌入了跨域的页面 B

// 在A中的伪代码
var B = document.getElementByTagName('iframe');
B.src = B.src + '#' + data;

// 在B中的伪代码
window.onhashchange = function () {
  var data = window.location.hash;
};
1
2
3
4
5
6
7
8
  • 3.postMessage 跨域

    HTML5 引入跨文档通信 API,这个 API 为 window 对象新增了一个 window.postMessage 方法,允许跨窗口通信,不论这两个窗口是否同源

// 例如,窗口A(http:A.com)向跨域的窗口B(http:B.com)发送信息
var popup = window.open('http://B.com', 'title');
popup.postMessage('Hello World!', 'http://B.com');
// 在窗口B中监听
window.addEventListener(
  'message',
  function (event) {
    console.log(event.origin); // 消息发向的网址
    console.log(event.source); // 发送消息的窗口
    console.log(event.data); // 消息内容
  },
  false
);
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 4.WebSocket 跨域
var ws = new WebSocket('wss://echo.websocket.org');
ws.onopen = function (evt) {
  console.log('Connection open ...');
  ws.send('Hello WebSockets!');
};
ws.onmessage = function (evt) {
  console.log('Received Message: ', evt.data);
  ws.close();
};
ws.onclose = function (evt) {
  console.log('Connection closed.');
};
1
2
3
4
5
6
7
8
9
10
11
12
  • 5.CORS 跨域(后台设置响应头)

# 安全类

# 常见攻击方式有哪些

  • CSRF: 跨站请求伪造
  • XSS: 跨站脚本攻击

# CSRF 的原理及防御措施

  • 攻击原理:
    • F 是网站 A 的用户,且已登录,网站 A 对用户 F 进行身份认证后下发了 cookie,保存在 F 浏览器中。
    • F 访问网站 B,网站 B 存在引诱点击(往往是个链接,指向网站 A 的 API 接口,通常是 GET 类型),然后访问了 A 网站,浏览器会自动上传 cookie。
    • 网站 A 对传来的 cookie 进行确认,是合法用户,则执行接口的动作
  • 前提:
    • 网站 A 某个接口存在漏洞
    • 用户在网站 A 已登录
  • 防御措施:
    • Token 验证:用户访问网站时,服务器会自动向本地存储 token,访问接口时,需要回传 token,否则无法通过验证
    • Referer 验证:服务器判断页面来源是否为站点下的页面,如果不是,则拦截
    • 隐藏令牌:与 token 类似,使用方式上的差别,更加隐蔽(比如放在 http 头部)

# XSS 的原理及防御措施

  • 攻击原理: 向页面的合法渠道注入脚本 (在脚本里执行想做的事情,不一定要登录)
  • 攻击方式:
    • 反射型:发出请求时,XSS 代码出现在 URL 中,作为输入传给服务器,服务器解析后响应,XSS 代码随响应内容传回浏览器,最后浏览器执行 XSS 代码,比如插入广告、执行恶性代码(img onerror)、引诱用户点击(按钮点击)
    • 存储型:提交的代码会存储在服务器端(数据库、内存、文件系统等),下次请求目标页面时不用再提交 XSS 代码。
  • 防御措施(让插入的脚本不可执行):
    • 编码:对用户输入的内容进行 HTML Entity 转义,例如</ body>,如果不转义,直接输出到页面上时,DOM 结构就被破坏了。同时还要避免直接进行 HTML Entity 解码,否则,编码跟过滤就失去了意义。
    • 过滤:
      • 移除用户上传的 DOM 属性,如 onerror 等跟事件相关的属性
      • 移除用户上传的 Style 节点、Script 节点、iframe 节点、frame 节点等
    • 校正:使用 DOM Parse 转换(类似的第三方库有 domParse),校正不配对的 DOM 标签,防止用户输入的内容破坏掉我们页面的 DOM 结构。

# 前端性能优化的关键时间点

# 前端性能优化的关键时间点有哪些,分别是什么

  • 开始渲染时间: 浏览器开始绘制页面,在此之前,页面都是白屏,所以也称为白屏时间
  • DOM Ready: dom 解析已经完成,资源还没有加载完成,这个时候用户与页面的交互已经可用了
  • 首屏时间: 用户看到第一屏页面的时间
  • onload: 原始文档和所有引用的内容已经加载完成,用户最明显的感觉就是浏览器上的 loading 状态结束

# 开始时间如何获取,如何优化?

  • 获取方式
    • Chrome 可通过 chrome.loadTimes().firstPaintTime 获取
    • IE9+ 可通过 performance.timing.msFirstPaint
    • 在不支持的浏览器中可以根据上面公式通过获取头部资源加载完成的时刻模拟获近似值
  • 优化建议
    • 优化服务器响应时间,服务端尽早输出
    • 减少 html 文件大小
    • 减少头部资源,脚本尽量放在 body 中

# DOM Ready 时间如何获取? 如何优化?

  • 获取方式:
    • 高级浏览器通过 DOMContentLoaded 事件获取
    • 低版本 webkit 内核浏览器可以通过轮询 document.readyState 来实现
    • IE 中可以通过 setTimeout 不断调用 documentElement 的 doScroll 方法,直到其可用来实现(可参考 jQuery 的实现)
  • 优化建议:
    • 减少 dom 结果的复杂度,节点尽可能少,嵌套不要太深
    • 优化关键呈现路径

# 首屏时间如何获取? 如何优化?

  • 获取方式(这个时间点很重要但是很难获取,一般都只能通过模拟获取一个近似时间)
    • 不断获取屏幕截图,当截图不再变化时,可以视为首屏时间
    • 一般影响首屏的主要因素是图片的加载,通过页面加载完后判断图片是否在首屏内,找出加载最慢的一张即可视为首屏时间。
  • 优化建议
    • 页面首屏的显示尽量不要依赖于 js 代码,js 尽量放到 domReady 后执行或加载
    • 首屏外的图片延迟加载
    • 首屏结构尽量简单,首屏外的 css 可延迟加载

# onload 时间如何获取? 如何优化?

  • 获取方式: 该时间点是 window.onload 事件触发的时间
  • 优化建议:
    • 减少资源的请求数和文件大小
    • 将非初始化脚本放到 onLoad 之后执行
    • 无需同步的脚本异步加载

# VUE

# 基础部分

# vue 数据双向绑定的原理

  • vue 在初始化实例的时候,会把 data 中的所有数据,用 Object.defineProperty 方法全部加给 vm 对象,这个时候 vm 对象就有了 set 和 get 方法,set 方法在属性被赋值的时候自动调用,get 方法在属性被获取值的时候自动调用,用户获取到的属性的值,其实就是 set 或 get 的返回值,也就能实现双向绑定了.

# v-model 语法糖

v-model 用来实现数据双向绑定,一般用在表单元素中

<input
  // 数据 -> 视图
  v-bind:value='msg'
  // 视图 -> 数据
  v-on:input='msg = $event.target.value'
/>
// $event表示事件对象, 在文本框中输入内容,会触发 input 事件,事件触发的时候: 通过事件对象获取到当前文本框的value值,然后赋值给 msg 数据
1
2
3
4
5
6
7
# 组件之间的数据双向绑定

组件之前的数据双向绑定语法糖省略了父组件中接收参数的 input 方法

// 父组件
<childComponent v-model="msg"/>

// 子组件
// <button @click="fn">点击将myMsg数据传给父组件</button>
props: ['msg']
data() {
  return {
    myMsg: this.msg
  }
},
methods: {
  fn() {
    this.$emit('input', this.myMsg)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# MVVM 和 MVC

  • MVVM
    • M 数据模型
    • V 视图
    • VM 视图模型
  • MVC
    • M 数据模型
    • V 视图
    • C 控制器(是视图和数据模型沟通的桥梁,用于处理业务逻辑)
  • 优势
    • MVC 模式,将应用程序划分为三大部分,实现了职责分离
    • 但是,在前端中经常要通过 js 代码来进行一些逻辑操作, 最终还要把这些逻辑操作的结果展示在页面上.也就需要频繁的操作 DOM
    • MVVM 通过 数据双向绑定让数据自动地双向同步
      • V(修改视图) -> M
      • M(修改数据) -> V
    • 数据驱动视图的思想,数据是核心

# 双向数据绑定的原理

  • 原理:
    • 数据劫持,Object.defineProperty 中的getset方法
  • 作用:
    • 指定读取或设置对象属性值得时候,执行的操作
var obj = {};

Object.defineProperty(obj, 'msg', {
  // 设置 obj.msg 执行的操作
  set() {},
  // 读取obj.msg执行的操作
  get() {},
});
1
2
3
4
5
6
7
8

# 怎样动态的添加数据

  • 注意: 只有 data 中的数据才是响应式的,动态添加进来的数据默认认为是非响应式
  • 可以通过以下方式实现动态数据的响应式
    • Vue.set(object, key, value) -- 适用添加单个属性
    • Object.assign() -- 适用添加多个属性
var vm = new Vue({
  data: {
    stu: {
      name: 'jack',
      age: 22,
    },
  },
});

// 通过Vue.set方式
Vue.set(vm.stu, 'gender', 'male');

// 通过Vue.assign
vm.stu = Object.assign({}, vm.stu, { do: 'happy', height: '189' });
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# Vue 中的组件通讯

  • 父组件向子组件传值 props
  • 子组件向父组件传值 $emit
  • 父子,兄弟,跨级组件通讯 bus
  • Vuex 状态管理
  • provide/inject
  • $parent/$children

  • 父子通信
    • props/$emit; $parent/$children; provide/inject
  • 兄弟通信
    • Bus;Vuex
  • 跨级通信
    • Bus; Vuex; provide/inject;

  • 父子,兄弟,跨级组件通讯 bus
// 新建bus.js
import Vue from 'vue'
var bus = new Vue()
export default bus

// 创建一个事件`mounted`mounted () {
  bus.$on('自定义方法名称',(data) => {
    // 接收参数
  })
}
// 触发这个事件
bus.$emit('自定义方法名称', data) //data 需要传的参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • Vuex 状态管理
// 定义数据
export default {
  state: {}, //vuex里的数据
  getters: {}, //相当于计算属性
  mutations: {}, // 可以理解为 修改state里的状态,同步
  actions: {}, // 修改state里的状态, 异步
};
// 在vuex里面调用mutations里的方法 commit('方法名',要传的数据)

// 在组件中调用mutations对象里的方法
this.$store.commit('方法名', 参数);

// 在组件中调用actions对象里的方法
this.$store.dispatch('方法名');
// 如果开启了namespaced: true的模式, dispatch()里面应该要加上模块名,如果模块内部没有设置模块名, 默认就是这个模块的文件名.例如:this.$store.dispatch('user/getUserInfo')

// module模式
/*
  将文件挂载到vuex上
  import details from './details'
  modules: {
    details
  }

  // 在模块中
  export default {
    namespaced: 'details',
    state: {
      a:'',
      b:''
    },
    getters: {},
    mutations: {},
    actions: {}
  }

  // 在组件中使用 (注意: 使用...mapState('details',['a','b'])这种形式的时候,方法名或数据要加 ' 引号)
  import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'

  computed: {
    ...mapState('details',['a','b']) 或者通过自定义命名 ...mapState('details', { mya: a,myb:b })
    ...mapGetters('details',['方法名','方法名']) 或者通过自定义命名 ...mapState('mapGetters', { 自定义方法名: 方法名 })
  },
  methods: {
    ...mapMutations('details', ['方法名'])
    ...mapActions('details', ['方法名'])
  }
*/
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
40
41
42
43
44
45
46
47
48
  • provide/inject
    • 介绍: 允许一个祖先组件想后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效
// 爷爷组件  传递参数
export default {
  provide: {
    name: '我是你爷爷',
  },
};
// 孙子组件 接收参数
export default {
  inject: ['name'],
};
1
2
3
4
5
6
7
8
9
10
  • $parent/$children 与 ref
    • 父元素主动调用子元素里的事件,或使用子元素里的数据
    • ref: 如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素; 如果用在子组件上,引用就指向组件实力
    • $parent/children: 访问父/子实例
    • 弊端: 无法在跨级或兄弟间通信
<component-a ref='domA' />
// this.$refs.domA.数据名
// this.$refs.domA.方法名
1
2
3

# 插槽 slot

  • 当组件内容不固定的时候,通过 slot 接收

# 路由传参的几种方式

  • 1.query 传参
    • url 中显示参数
    • 使用
      • this.$router.push({path: '路由',query: {}})
      • 获取传递的参数: this.$route.query
  • 2.params 传参
    • url 中不显示参数
    • 使用:
      • this.$router.push({name:'路由的名称',params: {}})
      • 获取传递的参数: this.$route.params
  • 3.通过配置路由来实现参数跳转
// 路由配置
{
  path: '/details/:id',
  name: 'details',
  component: details
}
// 跳转方式
this.$router.push({
  path: `/details/${id}`
})
1
2
3
4
5
6
7
8
9
10

# keep-alive

  • 作用:
    • 在组件切换过程中将状态保留在内存中,防止重复渲染 DOM,减少加载时间及性能消耗,提高用户体验
  • props
    • include 字符串或正则表达式. 只有名称匹配的组件才被缓存
    • exclude 字符串或正则表达式. 任何匹配到的组件都不会缓存
    • max 数字. 最多可以缓存多少组件实例
  • 生命周期函数
    • activated
      • 在 keep-alive 组件激活时调用
      • 该钩子函数在服务器端渲染期间不被调用
    • deactivated
      • 在 keep-alive 组件停用时调用
      • 该钩子函数在服务器端渲染期间不被调用

结合 router 使用 keep-alive

/* router.js*/
const Home = resolve => require(['@/components/home/home'], resolve)
const Goods = resolve => require(['@/components/Goods/Goods'], resolve)
export default new Router({
  routes: [
    path: '/',
      name: 'home',
      component: Home,
      redirect: 'goods',
      children: [
        {
          path: 'goods',
          name: 'goods',
          component: Goods,
          meta: {
        	keepAlive: false // 不需要缓存
      	  }
        }
  ]
})
/* App.vue */
<template>
  <div id="app">
    <keep-alive>
      <router-view v-if="$route.meta.keepAlive"></router-view>
    </keep-alive>
    <router-view v-if="!$route.meta.keepAlive"></router-view>
  </div>
</template>
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

# nextTick (异步 DOM 更新)

  • 说明: Vue 异步执行 DOM 更新,监视所有数据改变,一次性更新 DOM
  • 优势: 可以去除重复数据,避免不必要的计算和重复 DOM 操作
  • Vue.nextTick(callback): 在 DOM 更新后,执行某个操作(DOM 操作)
    • vm.$nextTick(() => {})
    • $el: 表示 Vue 管理区域的根元素,是一个 DOM 对象

# computed 和 watch 的区别

  • computed 支持缓存,只有数据依赖发生变化时,才会重新计算

  • 不支持异步, 当 computed 内有异步操作时无效,无法监听数据的变化

  • computed 属性值默认会走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于 data 中声明过或父组件传递的 props 中的数据通过计算得到的值

  • computed 属性属性值是函数,默认会走 get 方法,函数的返回值就是属性的返回值,在 computed 中,属性有一个 get 和 set 方法,数据变化,调用 set 方法

  • 多对一或一对一

  • watch 不支持缓存,数据变,直接触发相应的操作

  • watch 支持异步

  • 一对多的情况

# 内存泄漏

  • 如果一块内存无法被正常的管理(垃圾回收机制),那么就说这块内存泄漏了
  • 正常情况下,JS 引擎会定期查看应用程序中占用的每块内存是否还在被使用,如果这块内存不再被使用了,那么 JS 引擎通过垃圾回收机制 就把这块内存回收,然后,这块内存就可以继续被复用了
  • 如果垃圾回收机制无法回收一块,我们也无法访问的内存,我们就称这块内存泄漏了
    • 常见场景:
      • 缓存: 存在内存中的数据一直没有被清理掉
      • 作用域未释放
      • 无效的 DOM 引用
      • 定时器未清除
    • 解决办法:
      • 手动释放内存
      • 如果元素中有绑定事件,先解绑事件,再移除元素

# 虚拟 DOM

  • 简单理解: 通过 JS 变量的形式对于我们真实的 DOM 进行替代.一个是通过变量的形式存在 JS 里的虚拟 DOM,一个是存在于浏览器里的真实 DOM,但两个都是一一对应的,不会缺少一个.

  • 这种替代的好处

    • 因为浏览器中的真实 DOM 是由 C++代码实现的,DOM 节点其实很复杂
    • 虽然浏览器提供了一些 API 让我们可以通过 JS API 来操作 DOM, 但是这种成本很大,一方面是由于 DOM 本身就很复杂,另一方面是由于本身就是通过 JS 调用 C++代码,也是有开销的.
  • 针对这种直接操作 DOM 开销很大的问题:

    • 尽量少的直接对真实的 DOM 进行操作,就使用 js 变量的形式来模拟,加一个减一个都通过 js 变量来存储记录,最终把结果再渲染为浏览器上的真实 DOM

# 兼容性问题

# 1.Android 端文字未垂直居中

1. 通过line-height等于height设置时有时会出现未垂直居中,换用flex布局的align-items: center;

2.小于12px大小的字体会偏上: 通过transform: scale() 和 transform-origin控制字体缩放
1
2
3

# 2.ios 页面底部 margin-bottom 失效

  • 设置一个空的 div 撑起 margin-bottom 设置的高度

# 3.ios 的 select 不能选择第一项

使用 v-model 绑定 select 且初始化没有匹配项时,ios 无法选择第一项,无法触发 change 事件

//解决办法: 添加第一项,并设置该项disabled及display:none
<div id="example-5">
  <select v-model="selected">
    <option disabled value="" style="display: none;">请选择</option>
    <option>A</option>
  </select>
</div>
1
2
3
4
5
6
7

# 4.输入法弹窗导致窗口变小

输入法弹窗显示时,会导致 window 窗口高度变小

  • 场景:
    • 页面 a,输入法弹窗显示时,跳到页面 b,会导致页面 b 通过 window.innerHeight 获取的高度有问题
  • 解决方法:
    • 1.跳转页面之前,先用 document.activeElement.blur()隐藏虚拟键盘,然后延时 300 毫秒再跳转
    • 2.跳转前先通过 window.innerHeight 获取输入法未弹起时的内容窗口高度,再通过地址拼接传递该高度.

# 5.absolute 定位元素被输入法顶起

  • 当 absolute 元素的父元素设置 height 等于屏幕高度时,输入法弹起,屏幕高度缩小,导致定位元素顶起,遮盖了其他元素
  • 解决方法:
    • input 获取焦点时,让定位元素隐藏或改成静态定位,失去焦点时恢复.

# 6.多行文本溢出隐藏时显示不全

在添加溢出隐藏样式的盒子外再套一层父盒子,给父盒子设置固定高度,撑起来
1

# 7.移动端 placeholder 不垂直居中的问题

  • 设置 input
    • line-height: normal;

# 8.移动端 1px 的问题

  • 产生原因: 设备像素被: dpr = window.devicePixelRatio = 物理像素 / 逻辑像素
  • 解决方案:
      1. 0.5px
      1. viewport + rem
      • 同时通过设置对应 viewport 的 rem 基准值,这种方式就可以像以前一样轻松愉快的写 1px 了。
      • 在 devicePixelRatio = 2 时,设置 meta:
      <meta name="viewport" content="width=device-width,initial-scale=0.5, maximum-scale=0.5, minimum-scale=0.5, user-scalable=no">
      
      1
      • 在 devicePixelRatio = 3 时,设置 meta:
      <meta name="viewport" content="width=device-width,initial-scale=0.3333333333333333, maximum-scale=0.3333333333333333, minimum-scale=0.3333333333333333, user-scalable=no">
      
      1

# Webpack

# webpack 基础

  • entry(入口)
  • output(出口)
  • mode(模式)
  • module(模块)
  • plugins(插件)

# loader 是做什么用的?

  • webpack 默认只能处理 js 模块, 有了其他 loader,webpack 就能处理其他模块了

# plugins 是干嘛用的?

  • plugins 相当于在 webpack 的基础上添加的插件

# webapck 优化

  • 通过 Gzip 压缩 compression-webpack-plugin
  • 通过 splitChunks 切割代码块
  • 生产上去掉 console (transform-remove-console)

# 项目问题

# 手机端

# 多级 tab 切换

项目难点

  • 之前的做法: 一级 tab 是死的, 二级 tab 三级 tab 详情是动态获取的, 这些状态需要保存, 当再次进入页面时,要回到之前的位置, 数据是请求二级 tab 回来之后,拿到二级 tab 第一级,再请求三级 tab,依次... , 这样做的坏处就是: 我要请求三次数据才能把数据全都取回来.每次点击的时候保存 localStorage , 如果不点击,默认取第一个数据
  • 现在的做法: 一次把所有数据都拿回来, 如果请求带了参数,就根据参数返回结果, 如果没有带参数,就返回默认值

# 流程图

# canvasTable

e.offsetX 鼠标相对于事件源元素(srcElement)的 X,Y 坐标 e.clientX 相对于浏览器窗口 onmousedown onmouseup

  • 绘画左边的 canvas
  • 绘画右边的 canvas
  • 绘画中间的 canvas(首先先把他们置为白色,然后再进行绘画)
// 2.动态设置canvas画布的宽高
cas.width = _gridWidth * tableData.length + ''; // 第一个表格的宽度 + 数组的长度
cas.height = _gridHeight * 2 + '';
1
2
3
  • 高亮
  • 小地图
  • 小地图高亮
  • 通过 onscroll 事件(进行联动)

# 其他

# 实现原理

# 实现一个 Object.assign

// 为什么要用defineProperty? 因为通过defineProperty设置的值是不可修改的
Object.defineProperty(Object, 'assign', {
  // value: 该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)
  value: function (target) {
    // 判断这个对象是否为undefined或null
    if (target == null || target == undefined) {
      throw new TypeError();
    }
    // 将所有类型转化为 Object; number类型会转化为Number.prototype 对象;字符串会转成对象 例如: 'name', 会转化为 {0:'n',1:'a',2:'m',3:'e'}
    var to = Object(target);

    for (var i = 0; i < arguments.length; i++) {
      var source = arguments[i];
      // 判断arguments[i],是否为undefined或null
      if (source != null && source != undefined) {
        for (var sourceKey in source) {
          // 为什么不用 source.hasOwnProperty(sourceKey), 因为如果是 string或者number类型 source;
          // 如果source 是一个number类型,会报错,string类型的话一直都是false
          if (Object.prototype.hasOwnProperty.call(source, sourceKey)) {
            // 赋值操作
            to[sourceKey] = source[sourceKey];
          }
        }
      }
    }
  },
  writable: true, //当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符改变。
  configurable: true, //当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除
});
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

# 实现一个 Array.slice()

# 项目难点 - 作业平台

# 前端分页,模糊搜索及排序

  • 首先将这些功能放入一个组件中

  • 功能有: 1.分页 2 .1 模糊搜索,联合搜索.2 精确搜索,多 ip 换行搜索,(通过空格来匹配)3.选中当前页 4.选中所有页 5.通过 ip 进行排序,通过其他方式排序

  • 准备数据阶段

  • 在 props 中接收表格数据

  • 在 data 中设置 tableData 数组 缓存从 props 里的表格数据,方便修改,watch props 里的数组一有变化,赋值给 tableData

  • 在 data 中设置 searchData 数组 缓存搜索后的总数据,用于排序,修改数组

  • 在 data 中设置 cacheSearchData 数组,缓存搜索后的总数据,不动

  • 在 data 中设置 selectedData 数组, 缓存所有选中的数据

  • 在 data 中设置 isSearch boolean 值,判断是否是搜索状态 ``

  • 在 computed 中计算出返回数据的总条数,totalNum,通过判断 isSearch 是拿 searchData 数组的长度还是 tableData 数组里的长度

  • 在 computed 中计算当前要展示的数据的那一页,currentData,也是判断 isSearch 是要展示 tableData 数组,还是展示 searchData 数组中的一页,通过 slice 方法 (this.currentPage-1) X this.pageSize, this.currentPage X this.pageSize

  • 逻辑处理阶段

  • 1.分页, 很好处理,使用 computed 中的 currentData 即可能拿到当前页

  • 2.模糊搜索,精确搜索,多 ip 换行搜索, 因为这些功能会多次用到,所以我将这三个搜索封装在公共的方法中(方法接收 array,key,返回 array 类型, 无 空数组)

/**
 * 模糊匹配
 * @param {Array} array
 * @param {String} key
 * @returns {Array}
 */
export function fuzzyMatching(array, key) {
	const keyArr = key
		.trim()
		.split(' ')
		.filter((item) => item);
	if (!keyArr || !(array instanceof Array)) return;
	// 如果是联合搜索
	if (keyArr.length > 1) {
		return array.filter((item) =>
			keyArr.every((m) => JSON.stringify(Object.values(item)).indexOf(m) > -1)
		);
	} else {
		// 如果关键字搜索
		return array.filter(
			(item) =>
				Object.values(item).filter((m) => String(m).indexOf(keyArr[0]) > -1)
					.length > 0
		);
	}
}

/**
 * 多IP精确匹配
 * @param {Array} array
 * @param {String} str  字符串
 * @param {String} key 需要查找的对象属性名
 * @returns {Array}
 */
export function accurateMatching(array, str, key) {
	return array.filter(
		(item) =>
			str
				.split('\n') //切割成多个ip
				.map((item) => item.trim()) //去掉首尾空格
				.filter((item) => item) //过滤空行
				.indexOf(item[key]) > -1
	);
}
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
40
41
42
43
44
  • 3.选中当前页,选中所有, 这个饿了么有个配置 reserve-selection 设置为 true 时,他通过 selection-change 这个方法来监听数据选中的状态,但是他必须提供一个 key 值, 我是用的 ip 来当做 key 值,getRowKey(row) {return row.ip}那么就 通过 this.selectedData = rows, 然后当你点击的时候 通过 this.$refs.multipleTable.toggleRowSelection(changeSelect[i], true);, 就可以使 selection-change 触发,清除当前页也是一样. (2)选中所有的话, 需要分情况讨论, 选中 searchData 数据中的所有还是选中 table 数据中的所有,这时候就要通过 isSearch 来判断一下了

  • 说了这么多 isSearch , 那么 isSearch 的判断条件,通过 input 框改变触发,绑定的 input 事件.触发后,key.trim()之后判断是否为 true, 如果 trim()之后还是 true 的话,就让 this.isSearch 为 true ,否则为 false.

  • searchData 的逻辑 判断 isSearch 是否为 true, 如果为 true 的话 就通过上面封装的函数,拿到搜索后的值,赋值给 searchData

  • ip 排序和普通排序排序,首先, element 在需要排序的那一行设置 sortable="custom" 给 table 设置自定义排序方法 @sort-change="handleSortChange", 他会传进来一个对象,对象中有两个属性 prop,order; prop->需要排序的属性名,order-> element 会给三个值 null(不排序),ascending(升序),descending(降序); 处理逻辑 : 判断是否为 search 模式,如果是,判断 order 是否为 null 如果为 null 也就是不排序,那就拿 cacheSearchData 里的数据,否则就判断 是否按着 ip 排序,如果用 ip 排序,就用 ip 的排序算法,如果不是,则用普通的排序算法.如果 isSearch 为 false,则使用 this.tableData 处理逻辑一样

    两个排序算法

/**
 * 比较两个IP的大小
 * @param {String} key 对象中 ip的键
 * @param {String} order  排序方式 ascending 升序 ,descending 降序
 * @returns {Number} 1/0/-1
 * @ params{Object} obj1 进行比较的对象1
 * @ params{Object} obj2 进行比较的对象2
 *
 * 示例:
 * compareIP('1.1.1.1', '1.2.1.1'); // 返回-1
 * compareIP('13.37.11.11', '1.2.1.1'); // 返回1
 * compareIP('1.1.1.1', '1.1.1.1'); // 返回0
 *
 * 调用 list.sort(compareIP(prop, order));
 */

export function compareIP(key, order) {
	return (obj1, obj2) => {
		var i = 0;
		const arr1 = obj1[key].split('.').map((item) => parseInt(item));
		const arr2 = obj2[key].split('.').map((item) => parseInt(item));

		return (function compare(i, arr1, arr2, order) {
			if (arr1[i] !== arr2[i]) {
				if (order === 'ascending') {
					return arr1[i] < arr2[i] ? -1 : arr1[i] > arr2[i] ? 1 : 0;
				} else if (order === 'descending') {
					return arr1[i] > arr2[i] ? -1 : arr1[i] < arr2[i] ? 1 : 0;
				}
			} else {
				if (i === 3) {
					return 0;
				}
				return compare(i + 1, arr1, arr2, order);
			}
		})(i, arr1, arr2, order);
	};
}

/**
 * 比较两个值的大小
 * @param {String} key 对象中的键
 * @param {String} order  排序方式 ascending 升序 ,descending 降序
 * @returns {Number} 1/0/-1
 *
 * 默认和数组默认的sort排序一样的 排序算法
 * 调用 list.sort(compareNormal(prop, order));
 */
export function compareSort(key, order) {
	if (order === 'ascending') {
		return (a, b) => (a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0);
		// 降序
	} else if (order === 'descending') {
		return (a, b) => (a[key] > b[key] ? -1 : a[key] < b[key] ? 1 : 0);
	}
}
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
  • 最后优化,给input框使用防抖
function debounce(func, wait) {
  let timeout;
  return function() {
    let context = this; // 指向全局
    let args = arguments;
    if (timeout) {
      clearTimeout(timeout);
    }
    timeout = setTimeout(() => {
      func.apply(context, args); // context.func(args)
    }, wait);
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 项目中使用到的vuex 都是来储存什么的

  • 用户名
  • 权限
  • token

用到过module模式 在执行历史中,储存的有

  • tableData, 表格数据
  • selectMenus, 选中的数据
  • currentLog, 实时日志
  • currentIP, 当前选中的IP

# xterm.js

  • 项目中有返回的liunx输出日志,通过xterm.js还原liunx命令窗口样式,在最上面添加了一行当前表格选中的ip

# 手机端运维服务平台

# 四级tab切换事件

需求: 四级tab切换, 并保存状态 ,60刷新一下当前的数据 1.从保存状态的情况下分析

  • 从别的路由切回来
    1. 有状态(在localStorage中存的有) 从localStorage中拿
    2. 无状态(首次进入, 没有设置localStorage) 拿到请求回来数据的第一个,去请求下一tab的数据并保存localStorage
  • 在当前路由中点击
    1. 点击一级标题 获取一级tab的数据,拿到1级tab数据点击的,去获取第二个tab的数据,拿到第二个tab数据的第一个去获取第三个tab的数据,然后拿到第三个tab的第一个去请求详情,并储存

    2. 点击二级标题 判断一级tab,有无在local中存储,有,则拿储存的,无则拿一级tab第一个数据

    3. 点击三级标题

最后更新时间: 2022/10/23 10:24:55