前言
本篇文章依据的参照是B站十几场面试的视频自己整理的问题,自己也是个学习复习的过程 适用人群:跟我一样从事前端工作时长一年多到两年多这样的小伙伴,分享如有问题或者对于知识点解答有不合理地方,可以留言与我讨论,最后祝大家都能找到心仪的工作
前端登录相关问题
注:这个问题是我几乎在每个面试视频中都会被问到的问题,唯一的区别就是对于问题本身的广度和深度的不同,以及配套的一些扩展(如:token相关、登录流程、权限处理)
Token相关
http是无状态,所以才有会 cookie、session、token、jwt的出现
- 什么是token(简短概括)
注:此处还需要了解什么是Cookie、Session,有兴趣最好也了解一下jwt
用来储存用户信息
是一种前后端分离项目对于用户信息的设计的一种解决方案
token就是一个可以记录身份信息的令牌,亦是一种服务端无状态的认证方式
2. token的使用
token的本质就是一个唯一标识符的字符串,可以由UUID生成,也可以由用户ID根据算法进行加密生成,取到token之后再进行解密,取出用户的ID。
token的保存可以放到数据库里或者缓存,但是放进数据库会严重消耗服务器的资源,所以建议放到redis缓存中,这样简易操作,并且加快了查询速度。
- token过期方式处理
注:上面两点阅读即可,只是给大家一个对于token自己去进一步学习的引出,过期方式的处理才是重点
-
跳登录页
缺点:当用户在浏览网页时,会出现突然就需要重新登录的情况,体验很不好
-
刷新token重新发(之前听我一个朋友他们公司一个项目就是用这种暴力解决)
缺点:只要过期前端就重新请求重新获取token的接口,会出现用户一直在登录状态(一直不用登录)
-
每次请求都刷新token(类型于以前没有token概念所用的session解决方案)
缺点:会导致服务器端的计算压力过大
-
token不过期,redis记录过期(有不少公司使用这种方案)
缺点:jwt本身就是为了把验证身份这件事交给客户端,这种方式弊端是要服务器来维护验证用户身份,有点本末倒置的感觉,如果用户数量过大,还需专门去做集群管理。
个人感觉有点得不偿失了 -
双token(这种应该是目前大多数公司的解决方案)
通过登录返回两个token,大多数情况token1设置一天过期时间,token2设置两天过期时间,如果token1过期了,然后使用token2把两个token都刷新,然后又去延续,如果两个token都过期了,那就跳转登录页重新登录。这个方案同时也完美的解决网站区分活跃用户与非活跃用户的业务实现
双token登录实现:抽象双token登录,并实现
登录流程
注:这个分享一个我们公司之前所用的一个登录功能的设计思路 大概阅读即可
-
用户登录注册
旨在前端js层面先做一层验证,拦截,保证在向后台发送请求的时候是以正常的格式请求的
-
用户密码安全性
密码切记不可铭文存储,由于大多数客户的习惯都是使用相同的密码, 如果明文在发生信息泄露的情况下容易发生撞库的事情。所以在密码的保存上最好在后台使用“密钥 + 不可逆加密算” 如 sha1,sha256等有hash算法的不逆的加密算法进行加密后再存入数据库。
-
登录状态保存
由于http协议是无状态的, 所以要记录用户的登录状态就要靠后台相应数据的维护来记录, 我们通常都是登录成功后在seesion中保存登录用户, 然后将用户登录通过cookie返回到客户端, 通过比对cookie和seesion信息来验证用户是否登录。
-
防止cookie被盗用
为了防患cookie被盗用的情况还要在cookie中添加token、登录序列。这两个都是使用MD5进行加密的随机字符串, 作用就是在每次登录验证时, 同时验证token和登录序列还有ip地址, 因为在每次登录验证成功时都会刷新token, 如果cookie被盗用在正主使用旧cookie时出现登录序列相同, token不同而且ip地址多次变更的情况就要记录下此用户账户异常, 并且删除后台session里的登录记录,并提醒用户。
-
密码找回功能
因为后台密码是加密没法提供明文密码的找回, 所以可以设计通过手机短信验证或者邮箱验证, 验证成功后通过直接设置新密码的形式来进行, 对于重置密码的url地址为了安全起见要通过加入时间戳和唯一随机数来保证这个链接只在某个事件内有效, 如果在密码修改成功或者事件过期, 就把这个唯一随机数给删除, 保存密码方式同上。
-
防止恶意攻击
最有效的手段就是加入验证码, 同时记录某个用户在或ip在某个时段内如果尝试的失败登录次数超过一定阈值就限制其在几分钟内不能继续登录。
权限处理
注:前端权限一般可分为 接口权限、按钮权限、菜单权限、路由权限
接口权限
接口权限目前一般采用jwt的形式来验证,没有通过的话一般返回401,跳转到登录页面重新进行登录,
登录完拿到token,将token存起来,通过axios请求拦截器进行拦截,每次请求的时候头部携带token
路由权限
方案一:初始化即挂载全部路由,并且在路由上标记相应的权限信息,每次路由跳转前做校验
缺点:
1. 加载所有的路由,如果路由很多,而用户并不是所有的路由都有权限访问,对性能会有影响
2. 全局路由守卫里,每次路由跳转都要做权限判断
3. 菜单信息写死在前端,要改个显示文字或权限信息,需要重新编译
4. 菜单跟路由耦合在一起,定义路由的时候还有添加菜单显示标题,图标之类的信息,而且路由不一定作为菜单显示,还要多加字段进行标识
方案二:初始化的时候先挂载不需要权限控制的路由,比如登录页,404等错误页。如果用户通过URL进行强制访问,则会直接进入404,相当于从源头上做了控制,登录后,获取用户的权限信息,然后筛选有权限访问的路由,在全局路由守卫里进行调用addRoutes添加路由
1. 全局路由守卫里,每次路由跳转都要做判断
2. 菜单信息写死在前端,要改个显示文字或权限信息,需要重新编译
3. 菜单跟路由耦合在一起,定义路由的时候还有添加菜单显示标题,图标之类的信息,而且路由不一定作为菜单显示,还要多加字段进行标识
菜单权限
方案一:菜单与路由分离,菜单由后端返回,前端定义路由信息,name字段都不为空,需要根据此字段与后端返回菜单做关联,后端返回的菜单信息中必须要有name对应的字段,并且做唯一性校验
每次路由跳转的时候都要判断权限,这里的判断也很简单,因为菜单的name与路由的name是一一对应的,而后端返回的菜单就已经是经过权限过滤的
如果根据路由name找不到对应的菜单,就表示用户有没权限访问
如果路由很多,可以在应用初始化的时候,只挂载不需要权限控制的路由。取得后端返回的菜单后,根据菜单与路由的对应关系,筛选出可访问的路由,通过addRoutes动态挂载
缺点:
1. 菜单需要与路由做一一对应,前端添加了新功能,需要通过菜单管理功能添加新的菜单,如果菜单配置的不对会导致应用不能正常使用
2. 全局路由守卫里,每次路由跳转都要做判断
方案二:(推荐) 菜单和路由都由后端返回,前端统一定义路由组件
在将后端返回路由通过addRoutes动态挂载之间,需要将数据处理一下,将component字段换为真正的组件,如果有嵌套路由,后端功能设计的时候,要注意添加相应的字段,前端拿到数据也要做相应的处理
缺点:
1. 全局路由守卫里,每次路由跳转都要做判断
2. 前后端的配合要求更高
按钮权限
方案一:按钮权限也可以用v-if判断,但是如果页面过多,每个页面页面都要获取用户权限role和路由表里的meta.btnPermissions,然后再做判断
方案二:(推荐,目前我们公司在用这种方式) 通过自定义指令进行按钮权限的判断
{
path: '/permission',
component: Layout,
name: '权限测试',
meta: {
btnPermissions: ['admin', 'supper', 'normal']
},
//页面需要的权限
children: [{
path: 'supper',
component: _import('system/supper'),
name: '权限测试页',
meta: {
btnPermissions: ['admin', 'supper']
} //页面需要的权限
},
{
path: 'normal',
component: _import('system/normal'),
name: '权限测试页',
meta: {
btnPermissions: ['admin']
} //页面需要的权限
}]
}
import Vue from 'vue'
/**权限指令**/
const has = Vue.directive('has', {
bind: function (el, binding, vnode) {
// 获取页面按钮权限
let btnPermissionsArr = [];
if(binding.value){
// 如果指令传值,获取指令参数,根据指令参数和当前登录人按钮权限做比较。
btnPermissionsArr = Array.of(binding.value);
}else{
// 否则获取路由中的参数,根据路由的btnPermissionsArr和当前登录人按钮权限做比较。
btnPermissionsArr = vnode.context.$route.meta.btnPermissions;
}
if (!Vue.prototype.$_has(btnPermissionsArr)) {
el.parentNode.removeChild(el);
}
}
});
// 权限检查方法
Vue.prototype.$_has = function (value) {
let isExist = false;
// 获取用户按钮权限
let btnPermissionsStr = sessionStorage.getItem("btnPermissions");
if (btnPermissionsStr == undefined || btnPermissionsStr == null) {
return false;
}
if (value.indexOf(btnPermissionsStr) > -1) {
isExist = true;
}
return isExist;
};
export {has}
<el-button @click='editClick' type="primary" v-has>编辑</el-button>
小结:关于权限如何选择哪种合适的方案,根据项目选择合适方案,如考虑路由与菜单是否分离,权限需要前后端结合,前端尽可能的去控制,更多的需要后台判断
VueX
注:先说说个人对这个vue的配套插件的看法,它是个全局状态管理的相对完美的解决方案,最大的作用是在于它对于状态变更追踪的处理,让每一步的状态变化都有迹可循,非常适用于组件非常非常之多的大型前端项目。个人在日常的工作中并没有经常用到vuex管理全局状态。如果在项目中如果仅仅只是为了在组件中共享一部分数据,完全没必要用到vuex。个人对于vuex使用的次数很少,因为在公司项目中,如果项目体量非常大,一般在项目设计初期就会用一些方案拆分项目,比如:微服务,还有webpack5出现的模块联邦的概念
State
存储全局需要共享的状态,理论上来说直接获取到仓库对象就可以在任何组件使用和改变状态,但是这样就失去了使用vuex的意义了
Mutations
mutations出现就是为了解决监听数据变化的作用,所以建议使用xuex时,改变数据都使用分发mutaion去改变仓库中的共享状态。注意:mutation不可以使用在异步中,因为分发完之后操作就结束了,不会再记录异步操作的变化
Actions
vuex提供的异步解决方案,理论上讲所有的副作用改变数据,都应该action中去提交mutation去改变数据,虽然vuex并没有明确规定mutations里面不能写异步
Module
从字面意思上也可以知道这是vuex提供的状态作用域分模块解决方案,每一个模块开启命名空间后,都是一个独立的不受其他作用域影响的模块,然后统一在主仓库中引用使用
项目中性能优化、性能优化调试工具
注:提起性能优化,总会一种这玩意离我很近又感觉离的很远的感觉,心里一万句 今天这个逼班就上到这里吧,优化就优化,和我有什么关系,这个话题,一直是前端领域的 热度之王
性能优化标准
既然说性能优化,那他总得有一个公认的标准,这就是我们很多次听到过的,又没有怎么用过的 Lighthouse
在很多公司,都有着自己的性能监控平台,只需要引入相应的sdk,就能分析出你的页面存在的性能问题
除了绝对的业务,需要 特殊的定制,大多数的情况一般都用的是浏览器(Puppeteer)跑Lighthouse,所以我们大多数情况下,可以直接面向 Lighthouse 编程
Lighthouse
lighthouse 是 Google Chrome 推出的一款开源自动化工具,它可以搜集多个现代网页性能指标,分析 Web 应用的性能并生成报告,为开发人员进行性能优化的提供了参考方向。
说起Lighthouse在现代的谷歌浏览器中也已经集成
他可以分析出我们的页面性能,通过几个指标
Lighthouse 会衡量以下性能指标项:
- 首次内容绘制(First Contentful Paint)。即浏览器首次将任意内容(如文字、图像、canvas 等)绘制到屏幕上的时间点。
- 可交互时间(Time to Interactive)。指的是所有的页面内容都已经成功加载,且能够快速地对用户的操作做出反应的时间点。
- 速度指标(Speed Index)。衡量了首屏可见内容绘制在屏幕上的速度。在首次加载页面的过程中尽量展现更多的内容,往往能给用户带来更好的体验,所以速度指标的值约小越好。
- 总阻塞时间(Total Blocking Time)。指First Contentful Paint 首次内容绘制 (FCP)与Time to Interactive 可交互时间 (TTI)之间的总时间
- 最大内容绘制(Largest Contentful Paint)。度量标准报告视口内可见的最大图像或文本块的呈现时间
- 累积布局偏移(# Cumulative Layout Shift)。衡量的是页面整个生命周期中每次元素发生的非预期布局偏移得分的总和。每次可视元素在两次渲染帧中的起始位置不同时,就说是发生了LS(Layout Shift)。
lighthouse的的牛x之处就是它能找出你页面中的一些常规的性能瓶颈,并提出优化建议,比如:
注:具体大家可以去Google开发者工具尝试使用一下,强烈建议,这个玩意个人感觉还是很厉害的
具体性能优化方案
这里只介绍针对vue的手段,因为本人对于react的理解和运用还没有达到炉火纯青
图片懒加载
所谓图片懒加载,就是页面只渲染当前可视区域内的图片,如此一来,减少了其他图片渲染数量,能大大提高SpeedIndex和LCP的时间,从而提高分数
在vue中提起图片懒加载插件,首推vue-lazyload,使用方式简单,功能丰富
虚拟滚动
在一含有长列表页面中,你有没有发现你是往下越滑越卡,此时虚拟滚动就派上用场了, 他的基本原理就是只渲染可视区域内的几条数据,但是模拟出正常滑动的效果,因为每次只渲染可视区域内的数据,在滑动的时候他的性能就会有飞速提升
在vue中比较好用的插件有两个vue-virtual-scroller和vue-virtual-scroll-list
利用v-show、KeepAlive复用dom
我们知道v-show是通过display 控制dom的展示隐藏,他并不会删除dom 而我们在切换v-show的时候其实是减少了diff的对比,而KeepAlive 则是直接复用dom,连diff 的过程都没了,并且他们俩的合理使用还不会影响到初始化渲染。如此一来减少了js 的执行开销,但是值得注意的是,他并不能优化你初始化的性能,而是操作中的性能
分批渲染组件
SpeedIndex 的渐进渲染是提高SpeedIndex的关键,有了这个前提,我们就可以分批异步渲染组件。先看到内容,然后再渲染其他内容
举个例子:
<template>
<div>
{{ data1 }}
</div>
<div v-if="data1">
{{ data2 }}
</div>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
let data1 = ref('')
let data2 = ref('')
// 假设 这是从后端取到的数据
const data = {
data1: '这是渲染内容1',
data2: '这是渲染内容2'
}
data1.value = data.data1
//利用requestAnimationFrame 在空闲的时候当前渲染之后在渲染剩余内容
requestIdleCallback(() => {
data2.value = data.data2
})
return {
data1,
data2
}
},
}
</script>
小结:性能优化一直是一个很火的话题, 不管从面试以及工作中都非常重要,有了这些优化的点,你在写代码或者优化老项目时都能游刃有余,能提前考虑到其中的一些坑,并且规避。但是大家需要明白的是,不要为了性能优化而性能优化,我们在要因地制宜,在不破坏项目可维护性的基础上去优化,千万不要你优化个项目性能是好了,但是大家都看不懂了,这就有点得不偿失了。60分万岁61分浪费,差不多得了,把经历留着去干更重要的事情!
Promise
注:个人认为,Promise的出现最重要是解决了两个问题,1.解决了大多数代码中异步处理方式 2.对于前端混乱的异步生态提供了一个成熟标准的解决方案(这个和前端日常息息相关,就不做过多介绍了,当作重点对待就ok啦)
两个阶段:settled已决阶段 unsettled未决阶段
三种状态: pending(挂起) resolved(成功) rejected(失败) 未决有能力推向已决(resolve,reject)
成功后续处理(thenable) 失败后续处理(catchable) 后续处理是加入微队列
对于Promise遵循的规范了解,可以去读一下我之前写的一篇Promise A+ 规范
JavaScript 的三座大山
1️⃣ 作用域和闭包
作用域 指代码当前上下文,控制着变量和函数的可见性和生命周期。最大的作用是隔离变量,不同作用域下同名变量不会冲突。
作用域链 指如果在当前作用域中没有查到值,就会向上级作用域查询,直到全局作用域,这样一个查找过程所形成的链条就被称之为作用域链。
作用域可以堆叠成层次结构,子作用域可以访问父作用域,反之则不行。
作用域具体可细分为四种:全局作用域、模块作用域、函数作用域、块级作用域
全局作用域: 代码在程序的任何地方都能被访问,例如 window 对象。但全局变量会污染全局命名空间,容易引起命名冲突。
模块作用域: 早期 js 语法中没有模块的定义,因为最初的脚本小而简单。后来随着脚本越来越复杂,就出现了模块化方案(AMD、CommonJS、UMD、ES6模块等)。通常一个模块就是一个文件或者一段脚本,而这个模块拥有自己独立的作用域。
函数作用域: 顾名思义由函数创建的作用域。闭包就是在该作用域下产生,后面我们会单独介绍。
块级作用域: 由于 js 变量提升存在变量覆盖、变量污染等设计缺陷,所以 ES6 引入了块级作用域关键字来解决这些问题。典型的案例就是 let 的 for 循环和 var 的 for 循环。
// var demo
for(var i=0; i<10; i++) {
console.log(i);
}
console.log(i); // 10
// let demo
for(let i=0; i<10; i++) {
console.log(i);
}
console.log(i); //ReferenceError:i is not defined
了解完作用域再来谈谈 闭包: 函数A里包含了函数B,而函数B使用了函数A的变量,那么函数B被称为闭包或者闭包就是能够读取函数A内部变量的函数。
可以看出闭包是函数作用域下的产物,闭包会随着外层函数的执行而被同时创建,它是一个函数以及其捆绑的周边环境状态的引用的组合。换而言之,闭包是内层函数对外层函数变量的不释放。
闭包的特征:
- 函数中存在函数;
- 内部函数可以访问外层函数的作用域;
- 参数和变量不会被 GC,始终驻留在内存中;
- 有内存地方才有闭包。
所以使用闭包会消耗内存、不正当使用会造成内存溢出的问题,在退出函数之前,需要将不使用的局部变量全部删除。如果不是某些特定需求,在函数中创建函数是不明智的,闭包在处理速度和内存消耗方面对脚本性能具有负面影响。
顺便了解一下执行期上下文,产生闭包的原因
执行上下文
执行上下文: 某个函数或者全局代码执行的环境,该环境中包括执行代码所需要的所有信息。
我理解为:执行上下文就相当于一个对象,对象中包含了执行代码所需要的所有信息,当一个函数执行时,需要建立执行上下文,才开始真正执行。
执行上下文中的内容:VO(viriable object)存放需要用到的局部变量
scope:作用域 this
创建VO对象:
1.确定形参
2.确定函数中所有的函数字面量声明 (vo中出现同名属性,直接覆盖)
3.确定所有var关键字的变量声明,提取到上下文中,值为undefined(出现vo中同名属性,则忽略)
2️⃣ 原型和原型链
有对象的地方就有 原型,每个对象都会在其内部初始化一个属性,就是prototype(原型),原型中存储共享的属性和方法。当我们访问一个对象的属性时,js引擎会先看当前对象中是否有这个属性,如果没有的就会查找他的prototype对象是否有这个属性,如此递推下去,一直检索到 Object 内建对象。这么一个寻找的过程就形成了 原型链 的概念。
理解原型最关键的是理清楚__proto__、prototype、constructor三者的关系,我们先看看几个概念:
- __proto__属性在所有对象中都存在,指向其构造函数的prototype对象;prototype对象只存在(构造)函数中,用于存储共享属性和方法;constructor属性只存在于(构造)函数的prototype中,指向(构造)函数本身。
- 一个对象或者构造函数中的隐式原型__proto__ 的属性值指向其构造函数的显式原型 prototype 属性值,关系表示为:
instance.__proto__ === instance.constructor.prototype - 除了 Object,所有对象或构造函数的 prototype 均继承自 Object.prototype,原型链的顶层指向 null:
Object.prototype.__proto__ === null - Object.prototype 中也有 constructor:
Object.prototype.constructor === Object - 构造函数创建的对象(Object、Function、Array、普通对象等)都是 Function 的实例,它们的 proto 均指向 Function.prototype。
3️⃣ 异步和单线程
JavaScript 是 单线程 语言,意味着只有单独的一个调用栈,同一时间只能处理一个任务或一段代码。队列、堆、栈、事件循环构成了 js 的并发模型,事件循环 是 JavaScript 的执行机制。
为什么js是一门单线程语言呢?最初设计JS是用来在浏览器验证表单以及操控DOM元素,为了避免同一时间对同一个DOM元素进行操作从而导致不可预知的问题,JavaScript从一诞生就是单线程。
既然是单线程也就意味着不存在异步,只能自上而下执行,如果代码阻塞只能一直等下去,这样导致很差的用户体验,所以事件循环的出现让 js 拥有异步的能力。
Vue组件之间传值
注:具体的例子就不做考究了,这里只是给大家列出所有的通讯方式,虽然没什么技术含量,但是不是很厉害的面试官,总会以能说的传值方式越多来作为判断标准(并不知道为什么)
1. props
父组件向子组件传送数据,这应该是最常用的方式了
子组件接收到数据之后,不能直接修改父组件的数据。
否则会报错,因为当父组件重新渲染时,数据会被覆盖。如果只在子组件内要修改的话推荐使用 computed
2. .sync 子组件可以修改父组件内容
.sync可以帮我们实现父组件向子组件传递的数据的双向绑定,所以子组件接收到数据后可以直接修改,并且会同时修改父组件的数据
3. v-model
和 .sync 类似,可以实现将父组件传给子组件的数据为双向绑定,子组件通过 $emit 修改父组件的数据
4. ref
ref 如果在普通的DOM元素上,引用指向的就是该DOM元素;
如果在子组件上,引用的指向就是子组件实例;
父组件可以通过 ref 主动获取子组件的属性或者调用子组件的方法
5. $emit / v-on
子组件通过派发事件的方式给父组件数据,或者触发父组件更新等操作
6. $attrs / $listeners
多层嵌套组件传递数据时,如果只是传递数据,而不做中间处理的话就可以用这个,比如父组件向孙子组件传递数据时
$attrs:包含父作用域里除 class 和 style 除外的非 props 属性集合。通过 this.$attrs 获取父作用域中所有符合条件的属性集合,然后还要继续传给子组件内部的其他组件,就可以通过 v-bind="$attrs"
$listeners:包含父作用域里 .native 除外的监听事件集合。如果还要继续传给子组件内部的其他组件,就可以通过 v-on="$linteners"使用方式是相同的
7. $children / $parent
$children:获取到一个包含所有子组件(不包含孙子组件)的 VueComponent 对象数组,可以直接拿到子组件中所有数据和方法等
$parent:获取到一个父节点的 VueComponent 对象,同样包含父节点中所有数据和方法等\
8. provide / inject
provide / inject 是依赖注入,在一些插件或组件库里被常用
provide:可以让我们指定想要提供给后代组件的数据或方法
inject:在任何后代组件中接收想要添加在这个组件上的数据或方法,不管组件嵌套多深都可以直接拿来用
要注意的是 provide 和 inject 传递的数据不是响应式的,也就是说用 inject 接收来数据后,provide 里的数据改变了,后代组件中的数据不会改变,除非传入的就是一个可监听的对象 所以建议还是传递一些常量或者方法
9. EventBus
EventBus 是中央事件总线,不管是父子组件,兄弟组件,跨层级组件等都可以使用它完成通信操作
// 方法一
// 抽离成一个单独的 js 文件 Bus.js ,然后在需要的地方引入
// Bus.js
import Vue from "vue"
export default new Vue()
// 方法二 直接挂载到全局
// main.js
import Vue from "vue"
Vue.prototype.$bus = new Vue()
// 方法三 注入到 Vue 根对象上
// main.js
import Vue from "vue"
new Vue({
el:"#app",
data:{
Bus: new Vue()
}
})
10. Vuex 在超大型项目中使用
注:上面有专门说到vuex,这里就不做解释了
11. $root
作用:访问根组件中的属性或方法
注意:是根组件,不是父组件。$root只对根组件有用
12. slot插槽
就是把子组件的数据通过插槽的方式传给父组件使用,然后再插回来
网络http请求、axios相关问题
注:这里首先我想先说一个我认为在前端面试中达Top0级别的问题,虽然并不知道为什么
在浏览器地址栏中输入一个url,回车之后发生了什么?
个人认为这个问题本身并没有所谓的正确答案,面试官之所以热衷于问这个问题,是因为这个问题很容易就能反映出一个人知识面和素养
这里给大家一个问题答案的架构,可以从如下几步进行论述,每步简易阐述即可
- URL解析
- DNS查询
- TCP连接
- HTTP响应
- 响应请求
- 页面渲染
再给大家提供一个 个人对这个问题会怎样回答的答案(仅供参考)
1.浏览器会将url地址自动补充完整,没有协议,添加上协议,
2.浏览器对url进行编码,出现非ASCII字符,会对其进行自动编码,
3.浏览器会构建一个没有消息体的GET请求,等待服务器响应(这里可以适当说一下,三次握手,四次挥手,还有keep-alive)
4.服务器收到请求,将一个html页面代码组装到消息体中响应给浏览器,
5.浏览器拿到响应后,开始渲染消息体中的html代码,之所以会解析成html代码,是因为响应头指定了消息体类型为Content-type:text/html,
6.浏览器渲染页面过程中,发现其他嵌入资源,使用不阻塞渲染方式,重新请求其他资源,根据响应头中Content-type做相应处理,
7.所有资源下载处理后,浏览器触发window.onload事件。
HTTP 和 HTTPS 以及区别
HTTP(HyperText Transfer Protocol),即超文本运输协议,是实现网络通信的一种规范
- 支持客户/服务器模式
- 简单快速:客户向服务器请求服务时,只需传送请求方法和路径。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快
- 灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记
- 无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间
- 无状态:HTTP协议无法根据之前的状态进行本次的请求处理
HTTPS(HyperText Transfer Protocol Secure)
HTTPS 的出现主要是用来解决 HTTP 以明文形式发送内容不安全的特性
为了保证这些隐私数据能加密传输,让 HTTP 运行安全的 SSL/TLS 协议上,即 HTTPS = HTTP + SSL/TLS,通过 SSL 证书来验证服务器的身份,并为浏览器和服务器之间的通信进行加密
SSL 协议位于 TCP/IP 协议与各种应用层协议之间,浏览器和服务器在使用 SSL 建立连接时需要选择一组恰当的加密算法来实现安全通信,为数据通讯提供安全支持
- 首先客户端通过URL访问服务器建立SSL连接
- 服务器收到客户端请求后,会将网站支持的证书信息(证书中包含公钥)传送一份给客户端
- 客户端的服务器开始协商SSL连接的安全等级,也就是信息加密的等级
- 客户端的浏览器根据双方同意的安全等级,建立会话密钥,然后利用网站的公钥将会话密钥加密,并传送给网站
- 服务器利用自己的私钥解密出会话密钥
- 服务器利用会话密钥加密与客户端之间的通信
通俗来讲:1.发送请求,浏览器拿证书 2.浏览器验证证书 3.进行对称加密 4.用服务器给的公钥加密(一个Key),发送给其他服务器,只有拥有私钥才能解密,后续就用key进行通信。保证数据在传输过程中不被窃取和篡改
区别
- HTTPS是HTTP协议的安全版本,HTTP协议的数据传输是明文的,是不安全的,HTTPS使用了SSL/TLS协议进行了加密处理,相对更安全
- HTTP和HPPS使用连接方式不同,默认端口分别为 80 和 443
- HTTPS由于需要设计加密以及多次握手,性能方面不如HTTP
- HTTPS需要SSL,SSL证书需要钱,功能越强大的证书费用越高
HTTP1.0 HTTP1.1 HTTP2.0
HTTP1.0
- 浏览器与服务器只保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接
HTTP1.1
- 引入了持久连接,即TCP连接默认不关闭,可以被多个请求复用
- 在同一个TCP连接里面,客户端可以同时发送多个请求
- 虽然允许复用TCP连接,但是同一个TCP连接里面,所有的数据通信是按次序进行的,服务器只有处理完一个请求,才会接着处理下一个请求。如果前面的处理特别慢,后面就会有许多请求排队等着
- 新增了一些请求方法
- 新增了一些请求头和相应头
HTTP2.0
- 采用二进制格式而非文本格式
- 完全多路复用,而非有序并阻塞的、只需一个连接即可实现并行
- 使用报头压缩、降低开销
- 服务器推送
浏览器缓存
什么是浏览器缓存?
- 浏览器将请求后的资源存储为离线资源,当下次需要该资源时,浏览器会根据缓存机制来决定直接使用缓存资源还是向服务器请求资源
- 优点:减少不必要的数据的传输,降低服务器的压力。提升客户端的访问速度,从而提升用户的体验
实现方式
-
第一次访问网站的时候,请求服务器,浏览器会根据响应头判断是否对资源进行缓存,如果响应头中有
Expires和Cache-Control则会进行强缓存, -
Expires是http1.0协议的字段,它是用来指定资源到期的时间,是服务器端的具体时间点,浏览器会使用它的到期时间和本地时间对比,如果本地时间被修改或者和服务器端的时间差距比较大,那就会造成不准确的问题 -
Cache-Control是http1.1协议的字段,它使用的是相对时间,解决了Expires的问题,如果两者同时存在,那么Cache-Control的优先级更高 -
当强缓存失效后,会使用协商缓存,浏览器携带缓存标识(If-Modified-Since和If-None-Match)向服务器发送请求,服务器收到请求,如果发现请求头中含有If-Modified-Since字段,则会根据它的值Last-Modified与服务器上资源最后修改的时间做对比,如果服务器上的资源最后修改的时间大于If-Modified-Since的值,那就会重新返回资源,状态码是200,否则返回状态码304,继续使用缓存文件。
-
如果服务器发现请求头中含有的是
If-None-Match字段,则会根据它的Etag字段值与服务器上该资源的Etag值进行对比,如果一致返回状态码304,代表资源没有更新,继续使用缓存文件,如果不一致就返回新的资源文件,状态码为200 -
If-Modified-Since和If-None-Match的区别就是If-Modified-Since是根据资源最后修改的时间进行判断是否重新请求新的资源,即使资源内容没有修改修改时间发生变化也会请求服务器的资源,If-None-Match则是根据资源的内容进行判断,如果资源内容没有改变,即使最后修改时间发生变化也不会请求服务器的资源。If-None-Match的优先级更高,如果他们两个同时存在,那只有If-None-Match生效。