面试总结-某迅科技

360 阅读31分钟

技术栈是什么

vue + elementui

node.js写过没有, 是否在项目有实践

有写过, 没有在公司项目中进行实践过

vue响应式原理, 源码研究过吗, 按照源码大概讲一下

[Vue官方图](cn.vuejs.org/v2/guide/re…

data

创建Vue实例时, 遍历data选项的所有属性, 利用Object.defineProperty, 修改属性的setget方法, 对数据进行劫持, 其中get完成依赖收集(添加订阅者), set用于发出更新通知.

每个组件实例都对应一个wather实例(订阅者), 在组件渲染过程中, 会记录依赖的所有数据属性, 之后依赖项发生改变时, set方法会发出通知, 通知wather, 从而使相关联的组件重新渲染

  • vue2数据劫持使用Object.defineProperty实现的, 在ES5中是不可降级的特性, 所以Vue中不支持IE8和IE8以下的版本的浏览器

vue中如何做组件通信

参考 juejin.cn/post/684490…

props/$emit

  • 父组件通过props向子组件传递数据
  • 子组件通过$emit派发指定事件, 给父组件发送通知消息, 父组件通过v-on监听事件

父组件向子组件传值

如下面的例子, 子组件中定义一个msg数据

父组件Father.vue

<template>
  <div>
    <son :msg="msg" />
  </div>
</template>

<script>
import Son from './Son'

export default {
  components: { Son },
  data() {
    return {
      msg: 'this is Father'
    }
  }
}
</script>

子组件Son.vue 在属性props中接收父组件传递的数据

<template>
  <div>
    <p>{{ msg }}</p>
  </div>
</template>

<script>
export default {
  props: {
    msg: {
      type: String,
      required: true
    }
  }
}
</script>

子组件向父组件传值

子组件通过$emit派发一个自定义事件, 父组件中使用v-on监听自定义事件, 在事件处理函数中接收子组件传递的参数.

父组件Father.vue

<template>
  <div>
    <son :msg="msg" @updateMessage="handleChangeMsg" />
  </div>
</template>

<script>
import Son from './Son'

export default {
  components: { Son },
  data() {
    return {
      msg: 'this is Father'
    }
  },
  methods: {
    handleChangeMsg(obj) {
        // obj子组件传递过来的数据
      console.log(obj) // { time: "2020/5/12 下午8:48:17" }
      this.msg = this.msg + ' abc'
    }
  }
}
</script>

子组件Son.vue

<template>
  <div>
    <p>{{ msg }}</p>
    <p>
      <button @click="updateMsg">点击更新</button>
    </p>
  </div>
</template>

<script>

export default {
  props: {
    msg: {
      type: String,
      required: true
    }
  },
  methods: {
    updateMsg() {
      this.$emit('updateMessage', { time: new Date().toLocaleString() })
    }
  }
}
</script>

中央事件总线eventBus

轻量级实现组件通信的方式, 所有的组件使用同一个事件中心, 可以用来注册事件和监听事件 使用场景: 兄弟组件, 父子组件, 跨级组件

具体实现

使用一个空的Vue实例作为eventBus, 并进行导出

  1. 定义eventBus
import  Vue  from  "vue";
export  const  eventBus  =  new  Vue();
  1. 定义A组件, 里面触发事件
<template>
  <div>
    <button @click="handleChangeNum">更新数字</button>
    <p>A组件中: {{ num }}</p>
  </div>
</template>

<script>
import { eventBus } from "../utils/event-bus.js";

export default {
  data() {
    return {
      num: 1
    };
  },
  methods: {
    handleChangeNum() {
      this.num += 1;
      eventBus.$emit("updateNum", this.num);
    }
  }
};
</script>
  1. 定义B组件, 里面监听事件
<template>
  <div>
    <p>B组件中: {{ num }}</p>
  </div>
</template>

<script>
import { eventBus } from "../utils/event-bus.js";

export default {
  data() {
    return {
      num: 1
    };
  },
  mounted() {
    eventBus.$on("updateNum", args => {
      console.log(args);
      this.num = args;
    });
  }
};
</script>
  1. 在一个组件中引入A组件和B组件, 使他们以兄弟组件的形式存在
<template>
  <div>
    <A/>
    <B/>
  </div>
</template>

<script>
import A from "./A";
import B from "./B";

export default {
  components: { A, B }
};
</script>

依赖注入provide/inject

祖先组件通过provide选项提供数据或方法 后代组件通过inject选项注入数据或者方法

使用场景: 深层级的嵌套组件通信

例子一:

祖先组件Food

<template>
  <div>
    <p>Food component, num is {{ num }}</p>
    <div>
      <button @click="addNum">数字加一</button>
    </div>
    <h2>子组件</h2>
    <Fruit/>
  </div>
</template>

<script>
import Fruit from "./Fruit";

export default {
  name: "food",
  provide() {
    return {
      food: this
    };
  },
  components: {
    Fruit
  },
  data() {
    return {
      msg: "I am Food!",
      num: 1
    };
  },
  methods: {
    addNum() {
      this.num += 1;
      console.log(`num is : ${this.num}`);
    }
  }
};
</script>

后代Fruit组件

<template>
  <div>
    <Apple/>
  </div>
</template>

<script>
import Apple from "./Apple";

export default {
  components: { Apple }
};
</script>

后代Apple组件

<template>
  <div>
    <p>Apple component</p>
    <p>message from ancestor: {{ msg }}</p>
    <button @click="handleClick">调用祖先组件的方法</button>
  </div>
</template>

<script>
export default {
  inject: ["food"],
  data() {
    return {
      msg: this.food.msg || ""
    };
  },
  methods: {
    handleClick() {
      const addNum = this.food.addNum;
      addNum && addNum();
    }
  }
};
</script>

ref/refs

ref 被用来给元素或子组件注册引用信息. 引用信息将会注册在父组件的 $refs 对象上. 如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素. 如果用在子组件上,引用就指向组件实例.

简单理解: 通过在组件或者元素上指定ref=引用名称, 在父组件中, 通过this.$refs('引用名称')操作数据或者方法

例子:

父组件

<template>
  <div>
    <RefDemoChild ref="child"/>
    <button @click="say">点我就白给</button>
  </div>
</template>

<script>
import RefDemoChild from "./RefDemoChild";

export default {
  components: { RefDemoChild },
  methods: {
    say() {
      this.$refs["child"].sayYouGood();
    }
  }
};
</script>

子组件

<template>
  <div></div>
</template>

<script>
export default {
  data() {
    return {
      msg: "good~ good~ good~"
    };
  },
  methods: {
    sayYouGood() {
      console.log(`You are ${this.msg}`);
    }
  }
};
</script>

attrs/listeners

在vue2.4中,引入了$attrs$listeners,新增了inheritAttrs选项, 解决跨级组件的通信.

比如有三个组件A B C, 组件C想要使用组件A的数据, 组件C需要$emit通知组件A, 如果使用props的解决方式, 可以把数据和方法传递给组件B, 组件B使用props接收, 然后传递给组件C, 可以一层层传递下去, 写法上太繁琐, 这时就可以考虑使用$attrs$listeners了 我们只需要在B组件中对引入的C组件增加下面两个属性即可绑定所有的属性和事件。

例子: 组件A

<template>
  <div>
    <B :name="name" :age="age" :gender="gender" :hobby="hobby" @updateMyInfo="updateInfo"></B>
  </div>
</template>

<script>
import B from "./AttrB";

export default {
  components: { B },
  data() {
    return {
      name: "LastStarDust",
      age: 18,
      gender: "male",
      hobby: "run"
    };
  },
  methods: {
    updateInfo(params) {
      console.log(params); // 组件C传递的参数 {name: "StarDust"}
    }
  }
};
</script>

组件B

<template>
  <C v-bind="$attrs" v-on="$listeners"/>
</template>

<script>
import C from "./AttrC";

export default {
  components: { C }
};
</script>

组件C

<template>
  <div>
    name: {{ name }}
    age: {{ age }}
    gender: {{ gender }}
    hobby: {{ hobby }}
    <button
      @click="handleClick"
    >点击通知组件A</button>
  </div>
</template>

<script>
export default {
  props: {
    name: String,
    age: Number
  },
  data() {
    const { gender, hobby } = this.$attrs;
    return {
      gender,
      hobby
    };
  },
  created() {
    console.log(this.$attrs); // 没有通过props接收的剩余属性 {gender: "male", hobby: "run"}
    console.log(this.$listeners); // { updateMyInfo: function invoker() }
  },
  methods: {
    handleClick() {
      this.$emit("updateMyInfo", { name: "StarDust" });
    }
  }
};
</script>

总结: 顶层组件按照props方式传递数据/方法, 中间过渡的组件的通过

v-bind="$attrs" v-on="$listeners"

方式将属性和方法往下一层的组件中传递, 在最终的组件中, 通过props属性接收上层组件的数据(当然也可以在this.$attrs中获取), 通知上层组件, 按照this.$emit('自定义事件名')方式发出通知

$children / $parent

$children访问子组件实例 $parent访问父组件实例

例子 父组件中

<template>
  <div>
    <A/>
    <button @click="changeChildInsData">修改第一个子组件实例的数据</button>
  </div>
</template>

<script>
import A from "./A";

export default {
  components: { A },
  data() {
    return {
      info: "LastStarDust"
    };
  },
  methods: {
    changeChildInsData() {
      // 修改第一个子组件实例的msgA数据为'666'
      this.$children[0].msgA = "666";
    }
  }
};
</script>

子组件

<template>
  <div>
    <p>{{ msgA }}</p>
    <p>parentInfo is : {{ parentInfo }}</p>
    <button @click="getParentInfo">获取父组件实例的信息</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      msgA: "this A compoent",
      parentInfo: ""
    };
  },
  methods: {
    getParentInfo() {
      this.parentInfo = this.$parent.info;
    }
  }
};
</script>

Vuex

这里就不写了,太多了。。。

JS里如何做异步编程

单线程

JS是单线程的, 单线程就是排队,前一个任务不完成后一个就不能无法开始, 如果其中一个任务耗时很长, 就会出现停顿在耗时任务的地方, 其它任务无法执行, 可能会出现假死情况.

为了解决上述问题, JavaScript中将任务分为两种: 同步任务和异步任务

同步任务: 函数执行后, 会立即获得预期结果, 此外, 前面一个任务执行完毕后, 才会执行后面一个任务. 异步: 函数执行后, 函数马上返回, 但是不会马上返回预期结果, 需要等待一段时间, 返回结果后通过回调的方式获得结果

异步编程的方式

回调函数

看一个ajax请求的例子 请求完成以后, 在回调函数中定义处理逻辑代码

ajax(url, () => {
    // 业务逻辑
})

但是, 如果请求有多个, 并且后一个请求依赖前一个的数据, 容易出现回调地狱问题

ajax(url, () => {
    // 处理逻辑1
    ajax(url1, () => {
        // 处理逻辑2
        ajax(url2, () => {
            // 处理逻辑3
        })
    })
})

事件监听

HTML结构

  <button id="btn">点我</button>

JavaScript: 监听按钮的click事件, 事件触发时, 执行事件处理函数, 打印一些内容

  const btnObj = document.getElementById('btn')
  btnObj.addEventListener('click', (e) => {
      // 事件触发时执行
    console.log('you are very good!')
  })

定时器

settimeout 一次定时器, 执行一次后就不会再次执行 setInterval 永久定时器, 每过一段时间, 就执行一次

例子

  const timerId = setTimeout(() => {
    console.log("Hello World");
  }, 3000);

  const timerId2 = setInterval(() => {
    console.log("Hello World " + new Date().toLocaleDateString());
  }, 1000);

Promise

Promise 是一个对象,它代表了一个异步操作的最终完成或者失败

基本语法

  new Promise( function(resolve, reject) {...} /* executor */  );
  • executor说明
  • executor是带有 resolve 和 reject 两个参数的函数 。
  • Promise构造函数执行时立即调用executor 函数, resolve 和 reject 两个函数作为参数传递给executor。
  • resolve 和 reject 函数被调用时,分别将promise的状态改为fulfilled(完成)或rejected(失败)。
  • executor 内部通常会执行一些异步操作,一旦异步操作执行完毕(可能成功/失败)
    • 要么调用resolve函数来将promise状态改成fulfilled
    • 要么调用reject 函数将promise的状态改为rejected。
  • 如果在executor函数中抛出一个错误,那么该promise 状态为rejected。executor函数的返回值被忽略。

一个 Promise有以下几种状态:

  • pending: 初始状态,既不是成功,也不是失败状态。
  • fulfilled: 意味着操作成功完成。
  • rejected: 意味着操作失败

这个承诺一旦从等待状态变成为其他状态就永远不能更改状态了,比如说一旦状态变为 resolved 后,就不能再次改变为Fulfilled

参考 JS 异步编程六种方案 juejin.cn/post/684490… Promose developer.mozilla.org/zh-CN/docs/…

例子

const myFunc = () => {
  return new Promise((resolve, reject) => {
    const num = Math.random() * 10;
    console.log(num);
    if (num > 5) {
      resolve("大于5");
    } else {
      reject("小于5");
    }
  });
};

myFunc()
  .then(res => {
    console.log(res);
  })
  .catch(res => {
    console.log(res);
  });

async/await

async

async 是“异步”的简写,而 await 可以认为是 async wait 的简写。所以应该很好理解 async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。

  • async/await是基于Promise实现的,它不能用于普通的回调函数。
  • async/await与Promise一样,是非阻塞的。
  • async/await使得异步代码看起来像同步代码,这正是它的魔力所在

一个函数如果加上 async ,那么该函数就会返回一个 Promise

例子

  const fn = async () => {
    return "Hello World";
  };

  console.log(fn());

上面这个例子, 不使用async, 函数的返回值是字符串"Hello World", 在使用async修饰以后, 返回值是一个promise对象, 要拿到返回值, 可以使用promise.then的方式

  fn().then(res => {
    console.log(res); // 打印"Hello World"
  });

如果我们的fn函数没有返回值, 那么默认情况会返回一个undefined, 在then获取的值就是undefined

await

一般来说,都认为 await 是在等待一个 async 函数完成。不过按语法说明,await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,就是没有特殊限定)。

因为 async 函数返回一个 Promise 对象,所以 await 可以用于等待一个 async 函数的返回值——这也可以说是 await 在等 async 函数,但要清楚,它等的实际是一个返回值

注意到 await 不仅仅用于等 Promise 对象,它可以等任意表达式的结果,所以,await 后面实际是可以接普通函数调用或者直接量的。所以下面这个示例完全可以正确运行

function getSomething() {
  return "something";
}

async function testAsync() {
  return Promise.resolve("hello async");
}

async function test() {
  const v1 = await getSomething();
  const v2 = await testAsync();
  console.log(v1, v2);
}

test(); // something hello async

await 等到了要等的,然后呢

await 等到了它要等的东西,一个 Promise 对象,或者其它值,然后呢?我不得不先说,await 是个运算符,用于组成表达式,await 表达式的运算结果取决于它等的东西。

如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。

如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。

看到上面的阻塞一词,心慌了吧……放心,这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行。

基本示例:

function takeLongTime() {
    return new Promise(resolve => {
        setTimeout(() => resolve("long_time_value"), 3000);
    });
}

takeLongTime().then(v => {
    console.log("got", v);
});

改用aysnc/await

function takeLongTime() {
    return new Promise(resolve => {
        setTimeout(() => resolve("long_time_value"), 1000);
    });
}

async function test() {
    const v = await takeLongTime();
    console.log(v);
}

test();

takeLongTime() 没有申明为 async。实际上,takeLongTime() 本身就是返回的 Promise 对象,加不加 async 结果都一样,前面也说async加在一个函数就会返回返回一个promise对象

参考 理解 JavaScript 的 async/await segmentfault.com/a/119000000…

自己封装promise来用吗

import axios from "axios";

export default {
  data() {
    return {
      list: []
    };
  },
  async created() {
    const data = await this.getNews();
    const { code, result } = data;
    if (data && code === 200) {
      this.list = result;
    } else {
      console.log(data);
    }
  },
  methods: {
    getNews() {
      return new Promise((resolve, reject) => {
        const url = "https://api.apiopen.top/getWangYiNews";
        const params = {
          page: 1, // 页码
          count: 10 // 返回总数
        };
        axios
          .post(url, params)
          .then(res => {
            resolve(res.data);
          })
          .catch(err => {
            reject(err);
          });
      });
    }
  }
};

js的事件循环机制(eventLoop)

  • 一开始script整个脚本作为一个宏任务执行
  • 执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列
  • 当前宏任务执行完出队,检查微任务列表,有则依次执行,直到全部执行完
  • 执行浏览器UI线程的渲染工作
  • 检查是否有Web Worker任务,有则执行
  • 执行完本轮的宏任务,回到2,依此循环,直到宏任务和微任务队列都为空

微任务包括:MutationObserverPromise.then()或catch()Promise为基础开发的其它技术,比如fetch APIV8的垃圾回收过程、Node独有的process.nextTick

宏任务包括scriptsetTimeoutsetIntervalsetImmediateI/OUI rendering

注意⚠️:在所有任务开始的时候,由于宏任务中包括了script,所以浏览器会先执行一个宏任务,在这个过程中你看到的延迟任务(例如setTimeout)将被放到下一轮宏任务中来执行。

  • 参考

了解https的加密过程

HTTPS(全称:Hyper Text Transfer Protocol over Secure Socket Layer 超文本传输安全协议)

HTTPS是在HTTP和TCP的中间加入了一层 -- SSL/TLS层,是HTTP协议的安全版

  • SSL:安全套接层
  • TLS:安全传输层

HTTPS解决的问题

  • 信息加密传输:第三方无法窃听;
  • 校验机制:一旦被篡改,通信双方会立刻发现;
  • 身份证书:防止身份被冒充;

HTTPS流程

  1. 客服端发送HTTPS请求给服务端
  2. 服务端返回配置好的公钥证书
  3. 客户端验证公钥证书有效性 a. 无效,显示警告信息 b. 有效,使用伪数子随机数生成一个会话密钥
  4. 使用服务端的公钥加密会话密钥
  5. 发送加密后的会话密钥给服务端
  6. 服务端使用自己的私钥解密,解密成功,获得客户端的会话密钥
  7. 服务端使用会话密钥加密内容
  8. 发送加密后的内容给客户端
  9. 客户端使用会话密钥解密,获得内容

参考 深入理解 https 通信加密过程 klionsec.github.io/2017/07/31/… 深入理解HTTPS工作原理 github.com/ljianshu/Bl… HTTPS加密过程详解 segmentfault.com/a/119000001… 彻底搞懂HTTPS的加密机制 zhuanlan.zhihu.com/p/43789231

http缓存怎么做的

缓存分为强缓存和协商缓存两种,区别在于使用本地缓存时,是否发起请求给服务器,验证本地缓存是否失效,其中协商缓存需要和服务器进行协商,最终确定是否使用本地缓存

强缓存

不发送请求到服务器,直接使用本地缓存。

强缓存主要通过ExpiresCache-Control >两种响应头实现

优先级:Cache-Control > Expires

Expires

Expires是HTTP/1.0提出的一个表示资源过期时间的header,它描述的是一个绝对时间,由服务器返回。 Expires 受限于本地时间,如果修改了本地时间,可能会造成缓存失效

Expires: Wed, 21 Oct 2015 07:28:00 GMT

Cache-Control

Cache-Control 是 HTTP/1.1 中新增的属性,在请求头和响应头中都可以使用,常用的属性值如有:

  • max-age:从发起请求开始,最长缓存多少秒
  • no-cache:不使用强缓存,需要与服务器验证缓存是否新鲜
  • no-store:不使用任何缓存,每次都向服务器请求最新的资源
  • private:专用于个人的缓存,中间代理、CDN 等不能缓存该响应
  • public:响应可以被中间代理、CDN 等缓存
  • must-revalidate:在缓存过期前可以使用,过期后必须向服务器验证

协商缓存

协商缓存会发起请求询问服务端是否可以使用本地缓存。 浏览器第一次请求数据时,服务器会将缓存标识与数据一起响应给客户端,客户端将它们备份至缓存中。 再次请求时,客户端会将缓存中的标识发送给服务器,服务器根据此标识判断。若未失效,返回304状态码,浏览器拿到此状态码就可以直接使用缓存数据了

Last-Modified、If-Modified-Since

Last-Modified/If-Modified-Since 的值代表的是文件的最后修改时间

  • Last-Modified:服务端设置,第一次请求服务端会把资源的最后修改时间放到 Last-Modified 响应头中 ,告诉浏览器该资源的最后修改时间。

  • if-Modified-Since: 客户端设置,浏览器再次请求服务器的时候,请求头包含此字段,后面跟着在缓存中获得的最后修改时间。服务端收到此请求头发现有if-Modified-Since,则与被请求资源的最后修改时间进行对比,如果一致则返回304和响应报文头,浏览器只需要从缓存中获取信息即可。 从字面上看,就是说:从某个时间节点算起,是否文件被修改了

    • 如果已经被修改:那么完整的响应体,把新资源给客服端,服务器返回:状态码200 OK
    • 如果没有被修改:那么只需传输响应头header,不传输响应体,服务器返回:状态码304 Not Modified

但是如果在本地打开缓存文件,就会造成 Last-Modified 被修改,所以在 HTTP / 1.1 出现了 ETag

ETag、If-None-Match

ETag/If-None-Match 的值是一串 hash 码,代表的是一个资源的标识符,当服务端的文件变化的时候,它的 hash码会随之改变。

  • ETag 第一次请求时,服务端通过该字段告诉浏览器当前资源在服务器的唯一标识

  • If-None-Match 再次请求时,浏览器请求头中设置If-None-Match,对应的值为之前的缓存标识。服务器接收到次报文后发现If-None-Match则与被请求资源的唯一标识进行对比。

    • 不同,说明资源被改动过,则响应整个资源内容,返回状态码200。
    • 相同,说明资源无心修改,则响应header,浏览器直接从缓存中获取数据信息。返回状态码304。

参考 缓存(二)——浏览器缓存机制:强缓存、协商缓存 github.com/amandakelak… 图解 HTTP 缓存 juejin.cn/post/684490…

顺序

大致的顺序

  • Cache-Control —— 请求服务器之前
  • Expires —— 请求服务器之前
  • If-None-Match (Etag) —— 请求服务器
  • If-Modified-Since (Last-Modified) —— 请求服务器

最佳的方式

核心:保证资源最新的情况下,尽量命中强缓存,减少请求次数

  • HTML:使用协商缓存。
  • CSS&JS&图片:使用强缓存,文件命名带上hash值。

生产中的体现

  • webpack可以在打包的时候,给文件的命名上带上hash值
entry:{
    main: path.join(__dirname,'./main.js'),
    vendor: ['vue', 'axios']
},
output:{
    path:path.join(__dirname,'./dist'),
    publicPath: '/dist/',
    filname: 'bundle.[chunkhash].js'
}
  • 在vue-cli3.x脚手架打包下的js、css、img、font等静态文件名都是包含hash,所以每次打包index.html加载出来的文件都不会出现相同名称文件,因此也不会出现缓存问题。

keepalive是什么,为什么要用

HTTP协议采用请求-应答模式,有普通模式,也有KeepAlive模式。

非KeepAlive模式时,每个请求/应答客户和服务器都要新建一个连接,完成 之后立即断开连接(HTTP协议为无连接的协议); 当使用Keep-Alive模式(又称持久连接、连接重用)时,Keep-Alive功能使客户端到服 务器端的连接持续有效,当出现对服务器的后继请求时,Keep-Alive功能避免了建立或者重新建立连接。

作用:避免重复连接

如何设置keepalive

客户端设置,服务端根据情况决定是否使用

  • HTTP1.0中是默认关闭keepalive模式的,客户端在请求头中字段 Connection: Keep-Alive,当服务器收到附带有Connection: Keep-Alive的请求时,它也会在响应头中添加一个同样的字段来使用Keep-Alive。这样一来,客户端和服务器之间的HTTP连接就会被保持,不会断开(超过Keep-Alive规定的时间,意外断电等情况除外),当客户端发送另外一个请求时,就使用这条已经建立的连接。

  • HTTP1.1中默认启用Keep-Alive模式, 默认情况下HTTP1.1中所有连接都被保持,除非在请求头或响应头中指明要关闭:Connection: Close

使用keepalive优点

  • 减少TCP连接
  • 减少网络拥塞,
  • 减少拿到响应的延时
  • 会更好的处理错误:不会粗暴地直接关闭连接,而是report,retry

http建立请求的过程

  1. 域名解析
  2. 建立tcp连接
  3. 浏览器给服务发送请求命令
  4. 浏览器发送请求头信息
  5. 服务器应答
  6. 服务发送应答头
  7. 服务器给浏览器发送数据
  8. 服务器关闭tcp连接

http2.0的东西有那些

发展史图

基本概念

  • 流(Stream):已建立的TCP连接上的双向字节流,可以承载一个或多个消息。
  • 帧(Frame):通信的基本单位。
  • 消息(Message):一个完整的HTTP请求或响应,由一个或多个帧组成。特定消息的帧在同一个流上发送,这意味着一个HTTP请求或响应只能在一个流上发送。

一个TCP连接上可以有任意数量的流。

二进制分帧

HTTP/2 采用二进制格式传输数据,而非 HTTP 1.x 的文本格式,二进制协议解析起来更高效。

HTTP / 1 的请求和响应报文,都是由起始行,首部和实体正文(可选)组成,各部分之间以文本换行符分隔。 HTTP/2 将请求和响应数据分割为更小的帧,并且它们采用二进制编码。

多路复用

多路复用,代替原来的序列和阻塞机制。所有就是请求的都是通过一个 TCP连接并发完成。

HTTP 1.x 中,如果想并发多个请求,必须使用多个 TCP 链接,且浏览器为了控制资源,还会对单个域名有 6-8个的TCP链接请求限制

HTTP/2中:

  • 同域名下所有通信都在单个连接上完成。
  • 单个连接可以承载任意数量的双向数据流。
  • 数据流以消息的形式发送,而消息又由一个或多个帧组成,多个帧之间可以乱序发送,因为根据帧首部的流标识可以重新组装。

参考 面试官问:你了解HTTP2.0吗? juejin.cn/post/684490… 一文读懂 HTTP/2 特性 zhuanlan.zhihu.com/p/26559480

服务器推送

服务端可以在发送页面HTML时可以预测客户端需要的资源,主动推送其它资源,而不用等到浏览器解析到相应位置,发起请求再响应。

服务端可以主动推送,客户端也有权利选择是否接收。如果服务端推送的资源已经被浏览器缓存过,浏览器可以通过发送RST_STREAM帧来拒收。主动推送也遵守同源策略,服务器不会随便推送第三方资源给客户端。

头部压缩

HTTP/2 对消息头采用 HPACK 进行压缩传输,能够节省消息头占用的网络的流量。而 HTTP/1.x 每次请求,都会携带大量冗余头信息,浪费了很多带宽资源。

头部压缩需要在浏览器和服务器端之间:

  • 维护一份相同的静态字典,包含常见的头部名称,以及常见的头部名称和值的组合
  • 维护一份相同的动态字典,可以动态的添加内容
  • 通过静态Huffman编码对传输的首部字段进行编码

防止网站的安全

XSS跨站脚本攻击

在网站上注入恶意的客户端代码。当被攻击者登陆网站时就会自动运行这些恶意代码,从而,攻击者可以突破网站的访问权限,冒充受害者

2种情况下,容易发生 XSS 攻击

  1. 数据从一个不可靠的链接进入到一个 Web 应用程序。
  2. 没有过滤掉恶意代码的动态内容被发送给 Web 用户。

3类XSS攻击

  • 存储型 XSS 注入型脚本永久存储在目标服务器上。 当浏览器请求数据时,脚本从服务器上传回并执行。 常见的有留言板和评论

  • 反射型 XSS(非持久型) 当用户点击一个恶意链接,或者提交一个表单,或者进入一个恶意网站时,注入脚本进入被攻击者的网站。 Web服务器将注入脚本,比如一个错误信息,搜索结果等 返回到用户的浏览器上。由于浏览器认为这个响应来自"可信任"的服务器,所以会执行这段脚本。

  • 基于 DOM 的 XSS 通过修改原始的客户端代码,受害者浏览器的 DOM 环境改变,导致有效载荷的执行。 也就是说,页面本身并没有变化,但由于DOM环境被恶意修改,有客户端代码被包含进了页面,并且意外执行。

防御手段

  • httpOnly: 服务端在 cookie 中设置 HttpOnly 属性,使js脚本无法读取到 cookie 信息。
  // koa
  ctx.cookies.set(name, value, {
      httpOnly: true // 默认为 true
  })
  • 过滤
  • 输入检查,一般是用于对于输入格式的检查,例如:邮箱,电话号码,用户名,密码……等,按照规定的格式输入。前后端进行都进行检查
  • HTML编码转义 某些情况下,不能对用户数据进行严格过滤,需要对标签进行转义

一个正常的用户输入了 5 < 7 这个内容,会被转义,变成了 5 &lt; 7

  • JS编码转义 使用“\”对特殊字符进行转义

CSRF跨站点请求伪造

跨站点请求伪造(Cross-Site Request Forgeries),冒充用户发起请求(在用户不知情的情况下), 完成一些违背用户意愿的事情(如修改用户信息,删初评论等)。

造成危害

  1. 利用已通过认证的用户权限更新设定信息等;
  2. 利用已通过认证的用户权限购买商品;
  3. 利用已通过的用户权限在留言板上发表言论。

防御手段

  • 验证码;强制用户必须与应用进行交互,才能完成最终请求。此种方式能很好的遏制 csrf,但是用户体验比较差。

  • 尽量使用 post ,限制 get 使用;上一个例子可见,get 太容易被拿来做 csrf 攻击,但是 post 也并不是万无一失,攻击者只需要构造一个form就可以。

  • Referer check;请求来源限制,此种方法成本最低,但是并不能保证 100% 有效,因为服务器并不是什么时候都能取到 Referer,而且低版本的浏览器存在伪造 Referer 的风险。

  • token;token 验证的 CSRF 防御机制是公认最合适的方案。 整体思路如下:

    • 第一步:后端随机产生一个 token,把这个token 保存到 session 状态中;同时后端把这个token 交给前端页面;
    • 第二步:前端页面提交请求时,把 token 加入到请求数据或者头信息中,一起传给后端;
    • 后端验证前端传来的 token 与 session 是否一致,一致则合法,否则是非法请求。
    • 若网站同时存在 XSS 漏洞的时候,这个方法也是空谈。

点击劫持

点击劫持,是指利用透明的按钮或连接做成陷阱,覆盖在 Web 页面之上。 然后诱使用户在不知情的情况下,点击那个连接访问内容的一种攻击手段。这种行为又称为界面伪装(UI Redressing) 。

大概有两种方式:

  • 攻击者使用一个透明 iframe,覆盖在一个网页上,然后诱使用户在该页面上进行操作,此时用户将在不知情的情况下点击透明的 iframe 页面;

  • 攻击者使用一张图片覆盖在网页,遮挡网页原有的位置含义。

防御

HTTP头中设置X-Frame-Options

X-Frame-Options是一个HTTP响应头,是用来给浏览器 指示允许一个页面 可否在 <frame>, <iframe>, <embed> 或者 <object> 中展现的标记。站点可以通过确保网站没有被嵌入到别人的站点里面,从而避免 clickjacking 攻击。

X-Frame-Options有三个可选值

  • deny表示该页面不允许在 frame 中展示,即便是在相同域名的页面中嵌套也不允许。
  • sameorigin表示该页面可以在相同域名页面的 frame 中展示。
  • allow-from uri表示该页面可以在指定来源uri的 frame 中展示

参考 X-Frame-Options developer.mozilla.org/zh-CN/docs/…

判断当前网页是否被 iframe 嵌套

// 检测当前网站是否被第三方iframe引用
// 若相等证明没有被第三方引用,若不等证明被第三方引用。当发现被引用时强制跳转百度。
if(top.location != self.location){
    top.location.href = 'http://www.baidu.com'
}

iframe

<iframe> 标签表示一个内联框架元素。 一个内联框架可以将其它html页面嵌入当前页面中

防御

判断自己是否被其它页面使用iframe嵌入

// 检测当前网站是否被第三方iframe引用
// 若相等证明没有被第三方引用,若不等证明被第三方引用。当发现被引用时强制跳转百度。
if(top.location != self.location){
    top.location.href = 'http://www.baidu.com'
}

呈现在 iframe 框架中的页面进行操作限制

sandbox属性对呈现在 iframe 框架中的内容启用一些额外的限制条件。属性值可以为空字符串(这种情况下会启用所有限制),也可以是用空格分隔的一系列指定的字符串。有效的值有:

  • allow-same-origin:允许被视为同源,即可操作父级DOM或cookie等
  • allow-top-navigation:允许当前iframe的引用网页通过url跳转链接或加载
  • allow-forms:允许表单提交
  • allow-scripts:允许执行脚本文件
  • allow-popups:允许浏览器打开新窗口进行跳转
  • “”:设置为空时上面所有允许全部禁止

参考 <iframe> developer.mozilla.org/zh-CN/docs/…

opener

如果在项目中需要 **打开新标签 **进行跳转一般会有两种方式

  • 方式一 使用a标签
<a target='_blank' href='http://www.baidu.com'>
  • 方式二 使用window.open
window.open('http://www.baidu.com')

以上的方式有一定的安全风险 通过这两种方式打开的页面可以使用 window.opener来访问源页面的 window 对象。

例子: A页面通过 <a>window.open 方式,打开B页面。 但是B页面存在恶意代码如下:

 window.opener.location.replace('https://www.baidu.com') // 仅针对打开新标签有效

此时,用户正在浏览新标签页,但是原来网站的标签页已经被导航到了百度页面。 恶意网站可以伪造一个足以欺骗用户的页面,使得进行恶意破坏。 即使在跨域状态下 opener 仍可以调用 location.replace 方法。

防御

对于a标签

设置<a>标签的rel属性进行控制,属性值有:

  • noopener:会将 window.opener 置空,从而源标签页不会进行跳转(存在浏览器兼容问题)
  • noreferrer:兼容老浏览器/火狐。禁用HTTP头部Referer属性(后端方式)。
  • nofollow:为了SEO权重优化

最终版是:

<a target="_blank" href="" rel="noopener noreferrer nofollow">a标签跳转url</a>

对于window.open

在跳转之前,把新标签页的opener属性设置为null

<button onclick='openurl("http://www.baidu.com")'>click跳转</button>

function openurl(url) {
    const newTab = window.open();
    newTab.opener = null;
    newTab.location = url;
}

真实环境中怎么做跨域请求的

通过CORS实现跨域请求

跨域资源共享(CORS)是一种机制,是W3C标准。它允许浏览器向跨源服务器,发出XMLHttpRequestFetch请求。并且整个CORS通信过程都是浏览器自动完成的,不需要用户参与。

而使用这种跨域资源共享的前提是,浏览器必须支持这个功能,并且服务器端也必须同意这种"跨域"请求。因此实现CORS的关键是服务器需要服务器。通常是有以下几个配置需要在服务端设置:

  • Access-Control-Allow-Origin 指定了允许访问该资源的外域 URI
  • Access-Control-Allow-Methods 指明了实际请求所允许使用的HTTP方法
  • Access-Control-Allow-Headers 指明了实际请求中允许携带的请求头
  • Access-Control-Allow-Credentials 预检测请求的响应中时,它指定了实际的请求是否可以使用credentials
  • Access-Control-Max-Age (预检请求)的返回结果可以被缓存多久

简单请求和非简单请求

浏览器将CORS请求分成两类:简单请求和非简单请求

  • 请求方法是以下三种方法之一:

    • HEAD
    • GET
    • POST
  • 在排除用户代理自动设置的首部字段(例如 Connection ,User-Agent),HTTP的头信息不超出以下几种字段

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type (需要注意额外的限制),值仅限于下列三者之一:
    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded
  • DPR
  • Downlink
  • Save-Data
  • Viewport-Width
  • Width`

凡是同时满足上面两种情况的就是简单请求,反之则非简单请求,浏览器对这两种请求的处理不一样。

  • 例子 对于简单请求来说,浏览器之间发送CORS请求,具体来说就是在头信息中,增加一个origin字段,表明请求来源,方便后端进行请求源的对比

下面是请求报文和响应报文的内容

GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Referer: http://foo.example/examples/access-control/simpleXSInvocation.html
Origin: http://foo.example

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2.0.61
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml

[XML Data]

在请求报文中比较重要的是Origin字段,表示请求的来自哪个服务器 在响应报文中主要是Access-Control-Allow-Origin字段,这里设置为*,当前资源允许任意来源的服务器访问

预检请求

需预检的请求的要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。"预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。

大概流程

  • 当我们发起跨域请求时,判断是简单请求还是非简单请求
    • 如果是简单请求,则不会触发预检,直接发出正常请求。
    • 如果是非简单请求,浏览器会帮我们自动触发预检请求,也就是 OPTIONS 请求,用于确认目标资源是否支持跨域。
  • 浏览器会根据服务端响应的 header 自动处理剩余的请求,如果支持跨域,则继续发出正常请求,如果不支持,则在控制台显示错误。

跨域时如果请求被拦截, 可以在console控制台看到服务端返回的数据吗

不能

跨域问题 OPTIONS 报404/401 It does not have HTTP ok status

服务端设置跨域请求时,判断是不是预检请求,如果是快速返回

//设置跨域访问
app.all('*', (req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*')
  // res.header('Access-Control-Allow-Origin', 'http://www.baiduc.com')
  res.header('Access-Control-Allow-Headers', 'Authorization,X-API-KEY, Origin, X-Requested-With, Content-Type, Accept, Access-Control-Request-Method, token' )
  res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PATCH, PUT, DELETE')
  if(req.method === "OPTIONS") {
    res.send(200);/*让options请求快速返回*/
  } else {
    next();
  }
})

跨域时请求头里面会不会带上cookie

发生跨域时,不会带上cookie,如果需要带上cookie,通过以下方式实现

  • 客户端,设置withCredentialstrue
  • 服务端,设置Access-Control-Allow-Credentials响应头为true, 即可允许跨域请求携带 Cookie

客服端的不同实现方式

  • 原生 在open XMLHttpRequest后,设置withCredentials为true即可让该跨域请求携带 Cookie。 注意携带的是目标页面所在域的 Cookie。
 var xhr = new XMLHttpRequest();
 xhr.open('GET', url);
 xhr.withCredentials = true;
 xhr.send();
  • jquery
   $.ajax({
      url: url,
      xhrFields: {
         withCredentials: true
      }
   });
  • axios axios的withCredentials默认是false的,可以在发起请求的时候设置

局部设置

 axios(url, {
     method: "post",
     data: someJsonData,
     withCredentials: true
 })

全局设置

axios.defaults.withCredentials = true;

参考 ajax跨域时,如何带上目标地址需要的cookie github.com/Mmzer/think…

如果不能带cookie, 如何设置让它带上cookie

上一题的答案

小程序里面怎么去做鉴权

登录

  • 调用wx.login()获取临时凭证code
  • 把code放到请求发送给后端
  • 后端拿着code,存储在后端的appid和Appsecret,到微信服务器,换取session_key和openid
  • 后端对session_key和openid和自定义登录态进行关联,返回给小程序
  • 小程序拿到响应结果,获取一些openid,自定义登录信息token等信息

用户点击取消授权, 返回什么

授权失败fail user deny

获取用户头像需要授权吗

头像和昵称是隐私信息,需要授权的

获取openid需要用户授权吗

不需要,直接调用接口