前端-进阶
前端进阶
1.浏览器
1.1 cookie sessionStorage localStorage 区别
参考答案:
共同点:都是保存在浏览器端、且同源的
区别:
cookie数据始终在同源的http请求中携带(即使不需要),即cookie在浏览器和服务器间来回传递,而sessionStorage和localStorage不会自动把数据发送给服务器,仅在本地保存。cookie数据还有路径(path)的概念,可以限制cookie只属于某个路径下
存储大小限制也不同,cookie数据不能超过4K,同时因为每次http请求都会携带cookie、所以cookie只适合保存很小的数据,如会话标识。sessionStorage和localStorage虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大
数据有效期不同,sessionStorage:仅在当前浏览器窗口关闭之前有效;localStorage:始终有效,窗口或浏览器关闭也一直保存,因此用作持久数据;cookie:只在设置的cookie过期时间之前有效,即使窗口关闭或浏览器关闭
作用域不同,sessionStorage不在不同的浏览器窗口中共享,即使是同一个页面;localstorage在所有同源窗口中都是共享的;cookie也是在所有同源窗口中都是共享的
web Storage支持事件通知机制,可以将数据更新的通知发送给监听者
web Storage的api接口使用更方便
1.2 如何写一个会过期的localStorage,说说想法
参考答案:
两种方案:惰性删除 和 定时删除
惰性删除
惰性删除是指,某个键值过期后,该键值不会被马上删除,而是等到下次被使用的时候,才会被检查到过期,此时才能得到删除。我们先来简单实现一下:
var lsc = (function (self) { var prefix = 'one_more_lsc_' /** * 增加一个键值对数据 * @param key 键 * @param val 值 * @param expires 过期时间,单位为秒 */ self.set = function (key, val, expires) { key = prefix + key; val = JSON.stringify({'val': val, 'expires': new Date().getTime() + expires * 1000}); localStorage.setItem(key, val); }; /** * 读取对应键的值数据 * @param key 键 * @returns {null|*} 对应键的值 */ self.get = function (key) { key = prefix + key; var val = localStorage.getItem(key); if (!val) { return null; } val = JSON.parse(val); if (val.expires < new Date().getTime()) { localStorage.removeItem(key); return null; } return val.val; }; return self; }(lsc || {}));
上述代码通过惰性删除已经实现了可过期的localStorage缓存,但是也有比较明显的缺点:如果一个key一直没有被用到,即使它已经过期了也永远存放在localStorage。为了弥补这样缺点,我们引入另一种清理过期缓存的策略。
定时删除
定时删除是指,每隔一段时间执行一次删除操作,并通过限制删除操作执行的次数和频率,来减少删除操作对CPU的长期占用。另一方面定时删除也有效的减少了因惰性删除带来的对localStorage空间的浪费。
每隔一秒执行一次定时删除,操作如下:
- 随机测试20个设置了过期时间的key。
- 删除所有发现的已过期的key。
- 若删除的key超过5个则重复步骤1,直至重复500次。
具体实现如下:
var lsc = (function (self) { var prefix = 'one_more_lsc_' var list = []; //初始化list self.init = function () { var keys = Object.keys(localStorage); var reg = new RegExp('^' + prefix); var temp = []; //遍历所有localStorage中的所有key for (var i = 0; i < keys.length; i++) { //找出可过期缓存的key if (reg.test(keys[i])) { temp.push(keys[i]); } } list = temp; }; self.init(); self.check = function () { if (!list || list.length == 0) { return; } var checkCount = 0; while (checkCount < 500) { var expireCount = 0; //随机测试20个设置了过期时间的key for (var i = 0; i < 20; i++) { if (list.length == 0) { break; } var index = Math.floor(Math.random() * list.length); var key = list[index]; var val = localStorage.getItem(list[index]); //从list中删除被惰性删除的key if (!val) { list.splice(index, 1); expireCount++; continue; } val = JSON.parse(val); //删除所有发现的已过期的key if (val.expires < new Date().getTime()) { list.splice(index, 1); localStorage.removeItem(key); expireCount++; } } //若删除的key不超过5个则跳出循环 if (expireCount <= 5 || list.length == 0) { break; } checkCount++; } } //每隔一秒执行一次定时删除 window.setInterval(self.check, 1000); return self; }(lsc || {}));
1.3 如何定时删除localstorage数据
参考答案:
定时删除是指,每隔一段时间执行一次删除操作,并通过限制删除操作执行的次数和频率,来减少删除操作对CPU的长期占用。另一方面定时删除也有效的减少了因惰性删除带来的对localStorage空间的浪费。
每隔一秒执行一次定时删除,操作如下:
- 随机测试20个设置了过期时间的key。
- 删除所有发现的已过期的key。
- 若删除的key超过5个则重复步骤1,直至重复500次。
具体实现如下:
var lsc = (function (self) { var prefix = 'one_more_lsc_' var list = []; //初始化list self.init = function () { var keys = Object.keys(localStorage); var reg = new RegExp('^' + prefix); var temp = []; //遍历所有localStorage中的所有key for (var i = 0; i < keys.length; i++) { //找出可过期缓存的key if (reg.test(keys[i])) { temp.push(keys[i]); } } list = temp; }; self.init(); self.check = function () { if (!list || list.length == 0) { return; } var checkCount = 0; while (checkCount < 500) { var expireCount = 0; //随机测试20个设置了过期时间的key for (var i = 0; i < 20; i++) { if (list.length == 0) { break; } var index = Math.floor(Math.random() * list.length); var key = list[index]; var val = localStorage.getItem(list[index]); //从list中删除被惰性删除的key if (!val) { list.splice(index, 1); expireCount++; continue; } val = JSON.parse(val); //删除所有发现的已过期的key if (val.expires < new Date().getTime()) { list.splice(index, 1); localStorage.removeItem(key); expireCount++; } } //若删除的key不超过5个则跳出循环 if (expireCount <= 5 || list.length == 0) { break; } checkCount++; } } //每隔一秒执行一次定时删除 window.setInterval(self.check, 1000); return self; }(lsc || {}));
1.4 localStorage 能跨域吗
参考答案:
不能
解决方案:
- 通过postMessage来实现跨源通信
- 可以实现一个公共的iframe部署在某个域名中,作为共享域
- 将需要实现localStorage跨域通信的页面嵌入这个iframe
- 接入对应的SDK操作共享域,从而实现localStorage的跨域存储
1.5 memory cache 如何开启
参考答案:
memory cache 如何开启是一种比较特殊的缓存,他不受max-age、no-cache等配置的影响,即使我们不设置缓存,如果当前的内存空间比较充裕的话,一些资源还是会被缓存下来。但这种缓存是暂时的,一旦关闭了浏览器,这一部分用于缓存的内存空间就会被释放掉。如果真的不想使用缓存,可以设置no-store,这样,即便是内存缓存,也不会生效。
1.6 localstorage的限制
参考答案:
- 浏览器的大小不统一,并且在IE8以上的IE版本才支持localStorage这个属性
- 目前所有的浏览器中都会把localStorage的值类型限定为string类型,这个在对我们日常比较常见的JSON对象类型需要一些转换
- localStorage在浏览器的隐私模式下面是不可读取的
- localStorage本质上是对字符串的读取,如果存储内容多的话会消耗内存空间,会导致页面变卡
- localStorage不能被爬虫抓取到
1.7 浏览器输入URL发生了什么
参考答案:
- URL 解析
- DNS 查询
- TCP 连接
- 处理请求
- 接受响应
- 渲染页面
1.8 浏览器如何渲染页面的?
参考答案:
所以可以分析出基本过程:
1. HTML 被 HTML 解析器解析成 DOM 树;
2. CSS 被 CSS 解析器解析成 CSSOM 树;
结合 DOM 树和 CSSOM 树,生成一棵渲染树(Render Tree),这一过程称为 Attachment;
生成布局(flow),浏览器在屏幕上“画”出渲染树中的所有节点;
将布局绘制(paint)在屏幕上,显示出整个页面。
不同的浏览器内核不同,所以渲染过程不太一样。
WebKit 主流程
Mozilla 的 Gecko 呈现引擎主流程
由上面两张图可以看出,虽然主流浏览器渲染过程叫法有区别,但是主要流程还是相同的。
Gecko 将视觉格式化元素组成的树称为“框架树”。每个元素都是一个框架。WebKit 使用的术语是“呈现树”,它 由“呈现对象”组成。对于元素的放置,WebKit 使用的术语是“布局”,而 Gecko 称之为“重排”。对于连接 DOM 节点和可视化信息从而创建呈现树的过程,WebKit 使用的术语是“附加”。
1.9 重绘、重排区别如何避免
参考答案:
重排(Reflow):当渲染树的一部分必须更新并且节点的尺寸发生了变化,浏览器会使渲染树中受到影响的部分失效,并重新构造渲染树。
重绘(Repaint):是在一个元素的外观被改变所触发的浏览器行为,浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。比如改变某个元素的背景色、文字颜色、边框颜色等等
区别:重绘不一定需要重排(比如颜色的改变),重排必然导致重绘(比如改变网页位置)
引发重排
4.1 添加、删除可见的dom
4.2 元素的位置改变
4.3 元素的尺寸改变(外边距、内边距、边框厚度、宽高、等几何属性)
4.4 页面渲染初始化
4.5 浏览器窗口尺寸改变
4.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)不要在布局信息改变的时候做查询(会导致渲染队列强制刷新)
1.10 事件循环Event loop
参考答案:
主线程从"任务队列"中读取执行事件,这个过程是循环不断的,这个机制被称为事件循环。此机制具体如下:主 线程会不断从任务队列中按顺序取任务执行,每执行完一个任务都会检查microtask队列是否为空(执行完一个 任务的具体标志是函数执行栈为空),如果不为空则会一次性执行完所有microtask。然后再进入下一个循环去 任务队列中取下一个任务执行。
详细步骤:
1. 选择当前要执行的宏任务队列,选择一个最先进入任务队列的宏任务,如果没有宏任务可以选择,则会 跳转至microtask的执行步骤。
2. 将事件循环的当前运行宏任务设置为已选择的宏任务。
3. 运行宏任务。
4. 将事件循环的当前运行任务设置为null。
5. 将运行完的宏任务从宏任务队列中移除。
6. microtasks步骤:进入microtask检查点。
7. 更新界面渲染。
8. 返回第一步。
执行进入microtask检查的的具体步骤如下:
1. 设置进入microtask检查点的标志为true。
2. 当事件循环的微任务队列不为空时:选择一个最先进入microtask队列的microtask;设置事件循环的当 前运行任务为已选择的microtask;运行microtask;设置事件循环的当前运行任务为null;将运行结束 的microtask从microtask队列中移除。
3. 对于相应事件循环的每个环境设置对象(environment settings object),通知它们哪些promise为 rejected。
4. 清理indexedDB的事务。
5. 设置进入microtask检查点的标志为false。
需要注意的是:当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件, 然后再去宏任务队列中取出一个 事件。同一次事件循环中, 微任务永远在宏任务之前执行。
1.11 let a = "sssssss",分别存在哪儿?
参考答案:
使用let声明的全局变量不是挂在window对象下的,声明的全局变量存在于一个块级作用域中。
具体查看,我们可以通过打印一个全局函数,在let声明的全局变量在全局函数的scope下,我们平时使用时直接 用变量名称就能访问到
具体位置如下图:
1.12 浏览器垃圾回收机制
参考答案:
1. 介绍
浏览器的 Javascript 具有自动垃圾回收机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存。其原理是:垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。但是这个过程不是实时的,因为其开销比较大并且GC时停止响应其他操作,所以垃圾回收器会按照固定的时间间隔周期性的执行。
不再使用的变量也就是生命周期结束的变量,当然只可能是局部变量,全局变量的生命周期直至浏览器卸载页面才会结束。局部变量只在函数的执行过程中存在,而在这个过程中会为局部变量在栈或堆上分配相应的空间,以存储它们的值,然后在函数中使用这些变量,直至函数结束,而闭包中由于内部函数的原因,外部函数并不能算是结束。
还是上代码说明吧:
function fn1() { var obj = {name: 'hanzichi', age: 10}; } function fn2() { var obj = {name:'hanzichi', age: 10}; return obj; } var a = fn1(); var b = fn2(); 复制代码
我们来看代码是如何执行的。首先定义了两个function,分别叫做fn1和fn2,当fn1被调用时,进入fn1的环境,会开辟一块内存存放对象{name: 'hanzichi', age: 10},而当调用结束后,出了fn1的环境,那么该块内存会被js引擎中的垃圾回收器自动释放;在fn2被调用的过程中,返回的对象被全局变量b所指向,所以该块内存并不会被释放。
这里问题就出现了:到底哪个变量是没有用的?所以垃圾收集器必须跟踪到底哪个变量没用,对于不再有用的变量打上标记,以备将来收回其内存。用于标记的无用变量的策略可能因实现而有所区别,通常情况下有两种实现方式:标记清除和引用计数。引用计数不太常用,标记清除较为常用。
2. 标记清除
js中最常用的垃圾回收方式就是标记清除。当变量进入环境时,例如,在函数中声明一个变量,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为“离开环境”。
function test(){ var a = 10 ; //被标记 ,进入环境 var b = 20 ; //被标记 ,进入环境 } test(); //执行完毕 之后 a、b又被标离开环境,被回收。 复制代码
垃圾回收器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记(闭包)。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾回收器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。 到目前为止,IE9+、Firefox、Opera、Chrome、Safari的js实现使用的都是标记清除的垃圾回收策略或类似的策略,只不过垃圾收集的时间间隔互不相同。
3. 引用计数
引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1。如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1。当这个值的引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾回收器下次再运行时,它就会释放那些引用次数为0的值所占用的内存。
function test(){ var a = {} ; //a的引用次数为0 var b = a ; //a的引用次数加1,为1 var c =a; //a的引用次数再加1,为2 var b ={}; //a的引用次数减1,为1 } 复制代码
Netscape Navigator3是最早使用引用计数策略的浏览器,但很快它就遇到一个严重的问题:循环引用。循环引用指的是对象A中包含一个指向对象B的指针,而对象B中也包含一个指向对象A的引用。
function fn() { var a = {}; var b = {}; a.pro = b; b.pro = a; } fn(); 复制代码
以上代码a和b的引用次数都是2,fn()执行完毕后,两个对象都已经离开环境,在标记清除方式下是没有问题的,但是在引用计数策略下,因为a和b的引用次数不为0,所以不会被垃圾回收器回收内存,如果fn函数被大量调用,就会造成内存泄露。在IE7与IE8上,内存直线上升。
我们知道,IE中有一部分对象并不是原生js对象。例如,其内存泄露DOM和BOM中的对象就是使用C++以COM对象的形式实现的,而COM对象的垃圾回收机制采用的就是引用计数策略。因此,即使IE的js引擎采用标记清除策略来实现,但js访问的COM对象依然是基于引用计数策略的。换句话说,只要在IE中涉及COM对象,就会存在循环引用的问题。
var element = document.getElementById("some_element"); var myObject = new Object(); myObject.e = element; element.o = myObject; 复制代码
这个例子在一个DOM元素(element)与一个原生js对象(myObject)之间创建了循环引用。其中,变量myObject有一个属性e指向element对象;而变量element也有一个属性o回指myObject。由于存在这个循环引用,即使例子中的DOM从页面中移除,它也永远不会被回收。
举个栗子:
- 黄色是指直接被 js变量所引用,在内存里
- 红色是指间接被 js变量所引用,如上图,refB 被 refA 间接引用,导致即使 refB 变量被清空,也是不会被回收的
- 子元素 refB 由于
parentNode
的间接引用,只要它不被删除,它所有的父元素(图中红色部分)都不会被删除
另一个例子:
window.onload=function outerFunction(){ var obj = document.getElementById("element"); obj.onclick=function innerFunction(){}; }; 复制代码
这段代码看起来没什么问题,但是obj引用了document.getElementById('element'),而document.getElementById('element')的onclick方法会引用外部环境中的变量,自然也包括obj,是不是很隐蔽啊。(在比较新的浏览器中在移除Node的时候已经会移除其上的event了,但是在老的浏览器,特别是ie上会有这个bug)
解决办法:
最简单的方式就是自己手工解除循环引用,比如刚才的函数可以这样
myObject.element = null; element.o = null; window.onload=function outerFunction(){ var obj = document.getElementById("element"); obj.onclick=function innerFunction(){}; obj=null; }; 复制代码
将变量设置为null意味着切断变量与它此前引用的值之间的连接。当垃圾回收器下次运行时,就会删除这些值并回收它们占用的内存。
要注意的是,IE9+并不存在循环引用导致Dom内存泄露问题,可能是微软做了优化,或者Dom的回收方式已经改变
4. 内存管理
4.1 什么时候触发垃圾回收?
垃圾回收器周期性运行,如果分配的内存非常多,那么回收工作也会很艰巨,确定垃圾回收时间间隔就变成了一个值得思考的问题。IE6的垃圾回收是根据内存分配量运行的,当环境中存在256个变量、4096个对象、64k的字符串任意一种情况的时候就会触发垃圾回收器工作,看起来很科学,不用按一段时间就调用一次,有时候会没必要,这样按需调用不是很好吗?但是如果环境中就是有这么多变量等一直存在,现在脚本如此复杂,很正常,那么结果就是垃圾回收器一直在工作,这样浏览器就没法儿玩儿了。
微软在IE7中做了调整,触发条件不再是固定的,而是动态修改的,初始值和IE6相同,如果垃圾回收器回收的内存分配量低于程序占用内存的15%,说明大部分内存不可被回收,设的垃圾回收触发条件过于敏感,这时候把临街条件翻倍,如果回收的内存高于85%,说明大部分内存早就该清理了,这时候把触发条件置回。这样就使垃圾回收工作职能了很多
4.2 合理的GC方案
1. 基础方案
Javascript引擎基础GC方案是(simple GC):mark and sweep(标记清除),即:
- 遍历所有可访问的对象。
- 回收已不可访问的对象。
2. GC的缺陷
和其他语言一样,javascript的GC策略也无法避免一个问题:GC时,停止响应其他操作,这是为了安全考虑。而Javascript的GC在100ms甚至以上,对一般的应用还好,但对于JS游戏,动画对连贯性要求比较高的应用,就麻烦了。这就是新引擎需要优化的点:避免GC造成的长时间停止响应。
3. GC优化策略
David大叔主要介绍了2个优化方案,而这也是最主要的2个优化方案了:
分代回收(Generation GC) 这个和Java回收策略思想是一致的,也是V8所主要采用的。目的是通过区分“临时”与“持久”对象;多回收“临时对象”区(young generation),少回收“持久对象”区(tenured generation),减少每次需遍历的对象,从而减少每次GC的耗时。如图:
这里需要补充的是:对于tenured generation对象,有额外的开销:把它从young generation迁移到tenured generation,另外,如果被引用了,那引用的指向也需要修改。 这里主要内容可以参考
深入浅出Node
中关于内存的介绍,很详细~
增量GC 这个方案的思想很简单,就是“每次处理一点,下次再处理一点,如此类推”。如图:
这种方案,虽然耗时短,但中断较多,带来了上下文切换频繁的问题。
因为每种方案都其适用场景和缺点,因此在实际应用中,会根据实际情况选择方案。
比如:低 (对象/s) 比率时,中断执行GC的频率,simple GC更低些;如果大量对象都是长期“存活”,则分代处理优势也不大。
1.13 顺序存储结构和链式存储结构的比较
参考答案:
优缺点
- 顺序存储时,相邻数据元素的存放地址也相邻(逻辑与物理统一);要求内存中可用存储单元的地址必须是连续的。
- 优点:存储密度大(=1),存储空间利用率高。
- 缺点:插入或删除元素时不方便。
- 链式存储时,相邻数据元素可随意存放,但所占存储空间分两部分,一部分存放结点值,另一部分存放表示结点间关系的指针
- 优点:插入或删除元素时很方便,使用灵活。
- 缺点:存储密度小(<1),存储空间利用率低。
使用情况
- 顺序表适宜于做查找这样的静态操作;
- 链表宜于做插入、删除这样的动态操作。
- 若线性表的长度变化不大,且其主要操作是查找,则采用顺序表;
- 若线性表的长度变化较大,且其主要操作是插入、删除操作,则采用链表
顺序表与链表的比较
- 基于空间的比较
- 存储分配的方式
- 顺序表的存储空间是静态分配的
- 链表的存储空间是动态分配的
- 存储密度 = 结点数据本身所占的存储量/结点结构所占的存储总量
- 顺序表的存储密度 = 1
- 链表的存储密度 < 1
- 存储分配的方式
- 基于时间的比较
- 存取方式
- 顺序表可以随机存取,也可以顺序存取
- 链表是顺序存取的
- 插入/删除时移动元素个数
- 顺序表平均需要移动近一半元素
- 链表不需要移动元素,只需要修改指针
- 存取方式
1.14 token 能放在cookie中吗
参考答案:
能
解析:
- token 是在客户端频繁向服务端请求数据,服务端频繁的去数据库查询用户名和密码并进行对比,判断用户名和密码正确与否,并作出相应提示,在这样的背景下,token 便应运而生。
- 「简单 token 的组成」:uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token 的前几位以哈希算法压缩成的一定长度的十六进制字符串)
token认证流程
- 客户端使用用户名跟密码请求登录
- 服务端收到请求,去验证用户名与密码
- 验证成功后,服务端签发一个 token ,并把它发送给客户端
- 客户端接收 token 以后会把它存储起来,比如放在 cookie 里或者 localStorage 里
- 客户端每次发送请求时都需要带着服务端签发的 token(把 token 放到 HTTP 的 Header 里)
- 服务端收到请求后,需要验证请求里带有的 token ,如验证成功则返回对应的数据
1.15 js如何获取/禁用cookie
参考答案:
js如何获取cookie
假设cookie中存储的内容为:name=jack;password=123
则在B页面中获取变量username的值的JS代码如下:
arusername=document.cookie.split(";")[0].split("=")[1]; //JS操作cookies方法! //写cookies function setCookie(name,value){ var Days = 30; var exp =newDate(); exp.setTime(exp.getTime() + Days*24*60*60*1000); document.cookie = name +"="+ escape (value) +";expires="+ exp.toGMTString(); } //读取cookies function getCookie(name){ var arr,reg=new RegExp("(^| )"+name+"=([^;]*)(;|$)"); if(arr=document.cookie.match(reg)) return unescape(arr[2]); else return null; }
1.16 cookie
参考答案:
1. cookie 是什么?
- cookie 是存储于访问者计算机中的变量。每当一台计算机通过浏览器来访问某个页面时,那么就可以通过 JavaScript 来创建和读取 cookie。
- 实际上 cookie 是存于用户硬盘的一个文件,这个文件通常对应于一个域名,当浏览器再次访问这个域名时,便使这个cookie可用。因此,cookie可以跨越一个域名下的多个网页,但不能跨越多个域名使用。
- 不同浏览器对 cookie 的实现也不一样。即保存在一个浏览器中的 cookie 到另外一个浏览器是 不能获取的。
PS:cookie 和 session 都能保存计算机中的变量,但是 session 是运行在服务器端的,而客户端我们只能通过 cookie 来读取和创建变量
2. cookie 能做什么?
用户在第一次登录某个网站时,要输入用户名密码,如果觉得很麻烦,下次登录时不想输入了,那么就在第一次登录时将登录信息存放在 cookie 中。下次登录时我们就可以直接获取 cookie 中的用户名密码来进行登录。
PS:虽然 浏览器将信息保存在 cookie 中是加密了,但是可能还是会造成不安全的信息泄露
类似于购物车性质的功能,第一次用户将某些商品放入购物车了,但是临时有事,将电脑关闭了,下次再次进入此网站,我们可以通过读取 cookie 中的信息,恢复购物车中的物品。
PS:实际操作中,这种方法很少用了,基本上都是将这些信息存储在数据库中。然后通过查询数据库的信息来恢复购物车里的物品
页面之间的传值。在实际开发中,我们往往会通过一个页面跳转到另外一个页面。后端服务器我们可以通过数据库,session 等来传递页面所需要的值。但是在浏览器端,我们可以将数据保存在 cookie 中,然后在另外页面再去获取 cookie 中的数据。
PS:这里要注意 cookie 的时效性,不然会造成获取 cookie 中数据的混乱。
3. 怎么使用 cookie?
语法
document.cookie = "name=value;expires=evalue; path=pvalue; domain=dvalue; secure;”
对各个参数的解释
- name=value 必选参数
这是一个键值对,分别表示要存入的 属性 和 值。
比如:
document.cookie="name=中文"; //为了防止中文乱码,我们可以使用encodeURIComponent()编码;decodeURIComponent()解码 document.cookie = encodeURIComponent("name")+"="+encodeURIComponent("中文");
2. expires=evalue 可选参数
该对象的有效时间(可选)只支持GTM 标准时间,即要将时间转换,toUTCString()(默认为当前浏览器 会话有用,关闭浏览器就消失);
比如:
var date = new Date(); date.setTime(date.getTime()+2000);//获取当前时间并加上 2 秒钟 alert(date.toUTCString());//格林威治时间 (GMT) 把 Date 对象转换为字符串,并返回结果 alert(date.toGMTString());//与上面的结果一样,但是这个方法已经被上面取代了 document.cookie="name=vae;expires="+date.toUTCString(); alert(document.cookie); // name=vae setTimeout(function(){alert(document.cookie)},4000);//4 秒后打印空的字符串
3. path=pvalue 可选参数
限制访问 cookie 的目录,默认情况下对于当前网页所在的同一目录下的所有页面有效
4.domain=dvalue 可选参数
用于限制只有设置了的域名才可以访问;如果没有设置,则默认在当前域名访问
比如设置 test.com 表示域名为test.com的服务器共享该Cookie
5.secure=true|false 可选参数,默认是 true 不安全传输
安全设置,指明必须通过 安全的通信通道来传输(https) 才能获得 cookie,true 不安全,默认值;false 安 全,必须通过 https 来访问
比如:如果你设置 document.cookie = "name=vae;secure"
上面的代码如果是在 http 的协议中访问,那么是访问不了的
//设置 cookie function setCookie(objName, objValue, objHours){//添加cookie var str = objName + "=" + encodeURIComponent(objValue); if (objHours > 0) {//为0时不设定过期时间,浏览器关闭时cookie自动消失 var date = new Date(); var ms = objHours * 3600 * 1000; date.setTime(date.getTime() + ms); str += "; expires=" + date.toUTCString(); } document.cookie = str; } //获取 cookie function getCookie(objName){//获取指定名称的cookie的值 //多个cookie 保存的时候是以 ;空格 分开的 var arrStr = document.cookie.split("; "); for (var i = 0; i < arrStr.length; i++) { var temp = arrStr[i].split("="); if (temp[0] == objName){ return decodeURIComponent(temp[1]); }else{ return ""; } } } //为了删除指定名称的cookie,可以将其过期时间设定为一个过去的时间 function delCookie(name){ var date = new Date(); date.setTime(date.getTime() - 10000); document.cookie = name + "=a; expires=" + date.toUTCString(); }
注意:
(1)cookie可能被禁用。当用户非常注重个人隐私保护时,他很可能禁用浏览器的cookie功能;
(2)cookie是与浏览器相关的。这意味着即使访问的是同一个页面,不同浏览器之间所保存的cookie也是不能互相访问的;
(3)cookie可能被删除。因为每个cookie都是硬盘上的一个文件,因此很有可能被用户删除; (4)cookie安全性不够高。所有的cookie都是以纯文本的形式记录于文件中,因此如果要保存用户名密码等信息时,最好事先经过加密处理。
(5)cookie 在保存时,只要后面保存的 name 相同,那么就会覆盖前面的 cookie,注意是完全覆盖,包括失效时间,pat
1.17 cookie 禁用
参考答案:
问题描述:
sessionID通过cookie保存在客户端,如果将cookie禁用,必将对session的使用造成一定的影响。
解决这个问题的办法是:URL重写
URL重写
- servlet中涉及向客户端输出页面元素的时候,可以在相应的请求地址外面包上一层方法,也就是说使用response.encodeURL(“请求地址”);为请求地址添加一个JSESSIONID的值
- servlet中涉及跳转到新的页面时,可以使用response.encodeRedirectURL(“请求地址”);为请求地址添加一个JSESSIONID的值
1.18 调试工具
参考答案:
谷歌浏览器自带的调试工具:
- Elements:可查看网页页面代码(修改只是当前使用有效),也可实时调试修改页面ccs代码样式。
- console:记录开发者开发过程中的日志信息,也可在里面写js代码。一般页面运行时js报错都是可以在这里看到反馈和定位bug原因及其位置。
- Sources:断点调试JS,可以查看程序代码执行的过程,断点调试对于每一个程序员来说可是很重要。
- Network:从发起网页页面请求开始,分析HTTP请求后得到的各个请求资源信息(“小编有时候就利用这下载一些网站不给下载的在线视频,比如教学视频”)。
- Timeline:记录并分析网站的生命周期所发生的各类事件,分析渲染js执行的每一个阶段。
- Application:记录网站加载的各个资源信息。
- Security:判断网页是否安全。
- Audits:对当前网页的网络利用及网页性能进行检测,并给出一些优化建议。
Postman
几乎所有前端应用程序都发送和接收JSON响应和请求。 应用程序通过请求 API 可以做很多事情,例如身份验证,用户数据传输,甚至是一些简单的事情,例如获取所在位置的当前天气。
Postman 是调试接口的最佳工具之一。 它适用于 MacOS,Windows 和Linux的系统, 可以快速轻松地直接发送REST,SOAP和GraphQL请求。
使用 Postman,我们可以调整请求,分析响应和调试问题。 当不确定问题出在前端还是后端时,这是很有帮助的。
CSS Lint
CSSLint 是一个用来帮你找出 CSS 代码中问题的工具,它可做基本的语法检查以及使用一套预设的规则来检查代码中的问题,规则是可以扩展的。
JSON Formatter & Validator
地址:https://jsonformatter.curiousconcept.com/
在未格式化的 JSON 中很难发现语法错误或键值不正确的键,因为它很难读取。 对于 压缩的 JSON 文件,要发现其中的错误是比较难的,所以我们需要一种格式化的工具。
JSON Formatter & Validator tool 就是一个格式化 JSON 的工具,只需输入压缩的JSON
格式,就能获得正确格式。该工具也可以验证 JSON 到 RFC 标准。
Sentry
无论测试如何完善的程序,bug总是免不了会存在的,有些bug不是每次都会出现,测试时运行好好的代码可能在某个用户使用时就歇菜了,可是当程序在用户面前崩溃时,你是看不到错误的,当然你会说:”Hey, 我有记日志呢”。 但是说实话,程序每天每时都在产生大量的日志,而且分布在各个服务器上,并且如果你有多个服务在维护的话,日志的数量之多你是看不过来的吧。等到某天某个用户实在受不了了,打电话来咆哮的时候,你再去找日志你又会发现日志其实没什么用:缺少上下文,不知道用户什么操作导致的异常,异常太多(从不看日志的缘故)不知如何下手 等等。
Sentry就是来帮我们解决这个问题的,它是是一个实时事件日志记录和聚合平台。它专门用于监视错误和提取执行适当的事后操作所需的所有信息, 而无需使用标准用户反馈循环的任何麻烦。
Sentry是一个日志平台, 它分为客户端和服务端,客户端(目前客户端有Python, PHP,C#, Ruby等多种语言)就嵌入在你的应用程序中间,程序出现异常就向服务端发送消息,服务端将消息记录到数据库中并提供一个web节目方便查看。Sentry 由 python 编写,源码开放,性能卓越,易于扩展,目前著名的用户有Disqus, Path, mozilla, Pinterest
等。
JSHint
JSHint 是一个 Javascript 代码分析检测工具,不仅可以帮助我们检测到 JS 代码错误和潜在问题,也能帮助我们规范代码开发。
JSHint 扫描一个用JavaScript编写的程序,并报告常见的错误和潜在的bug。潜在的问题可能是语法错误、隐式类型转换导致的错误、泄漏变量或其他完全的问题。
JSHint 扫描用 JavaScript 编写的程序,并报告常见的错误和潜在的错误。 潜在的问题可能是语法错误,由于隐式类型转换导致的错误,变量泄漏或其他完全原因。
BrowserStack
地址:https://www.browserstack.com/
现在拥有各自内核的浏览器越来越多,各自的特性也千差万别。如果作为一个前端攻城师想要检测网站在不同的操作系统和移动平台下的各种浏览器的兼容性,那是相当痛苦不堪的。看到有在自己电脑上装虚拟机配置各种环境,有自己的电脑上组建好这样的环境,然后一一测试,可是人的精力毕竟有限,我们没法在同一台电脑上装那么多系统,那么多浏览器的。幸好出了个 BrowserStack 是前端的福音呀。
BrowserStack 是一款提供网站浏览器兼容性测试的在线云端测试工具,从而开发测试人员不必再准备很多虚拟机或者手机模拟器。
BrowserStack 是一个提供网站浏览器兼容性测试的在线云端应用,支持9大操作系统上的100多款浏览器。支持本地测试,支持与Visual Studio集成。或者你也可以直接前往 http://modern.ie 在线测试,现在注册可以免费试用三个月,三个月后是收费的,三个月后要是你想用又不想付费作为天朝的开发者你懂得。
2.移动端
1.2 移动端适配方案
参考答案:
适配思路
设计稿(750*1334) ---> 开发 ---> 适配不同的手机屏幕,使其显得合理
原则
- 开发时方便,写代码时设置的值要和标注的 160px 相关
- 方案要适配大多数手机屏幕,并且无 BUG
- 用户体验要好,页面看着没有不适感
思路
- 写页面时,按照设计稿写固定宽度,最后再统一缩放处理,在不同手机上都能用
- 按照设计稿的标准开发页面,在手机上部分内容根据屏幕宽度等比缩放,部分内容按需要变化,需要缩放的元素使用 rem, vw 相对单位,不需要缩放的使用 px
- 固定尺寸+弹性布局,不需要缩放
viewport 适配
根据设计稿标准(750px 宽度)开发页面,写完后页面及元素自动缩小,适配 375 宽度的屏幕
在 head 里设置如下代码
<meta name="viewport" content="width=750,initial-scale=0.5">
initial-scale = 屏幕的宽度 / 设计稿的宽度
为了适配其他屏幕,需要动态的设置 initial-scale 的值
<head> <script> const WIDTH = 750 const mobileAdapter = () => { let scale = screen.width / WIDTH let content = `width=${WIDTH}, initial-scale=${scale}, maximum-scale=${scale}, minimum-scale=${scale}` let meta = document.querySelector('meta[name=viewport]') if (!meta) { meta = document.createElement('meta') meta.setAttribute('name', 'viewport') document.head.appendChild(meta) } meta.setAttribute('content',content) } mobileAdapter() window.onorientationchange = mobileAdapter //屏幕翻转时再次执行 </script> </head>
缺点就是边线问题,不同尺寸下,边线的粗细是不一样的(等比缩放后),全部元素都是等比缩放,实际显示效果可能不太好
vw 适配(部分等比缩放)
- 开发者拿到设计稿(假设设计稿尺寸为750px,设计稿的元素标注是基于此宽度标注)
- 开始开发,对设计稿的标注进行转换,把px换成vw。比如页面元素字体标注的大小是32px,换成vw为 (100/750)*32 vw
- 对于需要等比缩放的元素,CSS使用转换后的单位
- 对于不需要缩放的元素,比如边框阴影,使用固定单位px
关于换算,为了开发方便,利用自定义属性,CSS变量
<head> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1"> <script> const WIDTH = 750 //:root { --width: 0.133333 } 1像素等于多少 vw document.documentElement.style.setProperty('--width', (100 / WIDTH)) </script> </head>
注意此时,meta 里就不要去设置缩放了
业务代码里就可以写
header { font-size: calc(28vw * var(--width)) }
实现了按需缩放
rem 适配
- 开发者拿到设计稿(假设设计稿尺寸为750px,设计稿的元素标是基于此宽度标注)
- 开始开发,对设计稿的标注进行转换
- 对于需要等比缩放的元素,CSS使用转换后的单位
- 对于不需要缩放的元素,比如边框阴影,使用固定单位px
假设设计稿的某个字体大小是 40px, 手机屏幕上的字体大小应为 420/750*40 = 22.4px (体验好),换算成 rem(相对于 html 根节点,假设 html 的 font-size = 100px,)则这个字体大小为 0.224 rem
写样式时,对应的字体设置为 0.224 rem 即可,其他元素尺寸也做换算...
但是有问题
举个 ,设计稿的标注 是40px,写页面时还得去做计算,很麻烦(全部都要计算)
能不能规定一下,看到 40px ,就应该写 40/100 = 0.4 rem,这样看到就知道写多少了(不用计算),此时的 html 的 font-size 就不能是 100px 了,应该为 (420*100)/750 = 56px,100为我们要规定的那个参数
根据不同屏幕宽度,设置 html 的 font-size 值
<head> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1"> <script> const WIDTH = 750 //设计稿尺寸 const setView = () => { document.documentElement.style.fontSize = (100 * screen.width / WIDTH) + 'px' } window.onorientationchange = setView setView() </script> </head>
对于需要等比缩放的元素,CSS使用转换后的单位
header { font-size: .28rem; }
对于不需要缩放的元素,比如边框阴影,使用固定单位px
header > span.active { color: #fff; border-bottom: 2px solid rgba(255, 255, 255, 0.3); }
假设 html 的 font size = 1px 的话,就可以写 28 rem 了,更方便了,但是浏览器对字体大小有限制,设为 1px 的话,在浏览器中是失效的,会以 12px(或者其他值) 做一个计算 , 就会得到一个很夸张的结果,所以可以把 html 写的大一些
使用 sass 库时
JS 处理还是一样的,但看着好看些
@function px2rem($px) { @return $px * 1rem / 100; } header { font-size: px2rem(28); }
以上的三种适配方案,都是等比缩放,放到 ipad 上时(设计稿以手机屏幕设计的),页面元素会很大很丑,有些场景下,并不需要页面整体缩放(viewport 自动处理的也很好了),所以有时只需要合理的布局即可。
弹性盒适配(合理布局)
<meta name="viewport" content="width=device-width">
使用 flex 布局
section { display: flex; }
总结一下,什么样的页面需要做适配(等比缩放)呢
- 页面中的布局是栅格化的
换了屏幕后,到底有多宽多高很难去做设置,整体的都需要改变,所以需要整体的缩放
- 头屏大图,宽度自适应,高度固定的话,对于不同的屏幕,形状就会发生改变(放到ipad上就变成长条了),宽度变化后,高度也要保持等比例变化
以上所有的适配都是宽度的适配,但是在某些场景下,也会出现高度的适配
比如大屏,需要适配很多的电视尺寸,要求撑满屏幕,不能有滚动条,此时若换个屏幕
此时需要考虑小元素用 vh, 宽和高都用 vh 去表示,中间的大块去自适应,这就做到了大屏的适配,屏幕变小了,整体变小了(体验更好),中间这块撑满了屏幕
对于更复杂的场景,需要更灵活考虑,没有一种适配方式可以囊括所有场景。
2.2 开发APP的技术栈是怎么样的
参考答案:
手机 App 的技术栈可以分成三类:原生 App 技术栈 (native technology stack)、混合 App 技术栈 (hybrid technology stack)、跨平台 App 技术栈 (cross-platform technology stack),H5 开发主要用在混合技术栈。但是,跨平台技术栈的某些容器也会用到(比如 React Native),因为它们的 UI 层借鉴了 Web 模型。混合技术栈和跨平台技术栈的基础,都是原生技术栈,因为最终都要编译成原生App。所以,不管使用哪一种技术栈,多多少少要了解一些各平台的原生技术。
解析:
(1)原生 App 技术栈 (native technology stack)
原生技术栈指的是,只能用于特定手机平台的开发技术。比如,安卓平台的 Java 技术栈,iOS 平台的 Object-C 技术栈或 Swift 技术栈。
这种技术栈只能用在一个平台,不能跨平台。
(2)混合 App 技术栈 (hybrid technology stack)
混合技术栈指的是开发混合 App 的技术,也就是把 Web 网页放到特定的容器中,然后再打包成各个平台的原生 App。所以,混合技术栈其实是 Web 技术栈 + 容器技术栈,典型代表是 PhoneGap、Cordova、Ionic 等框架。
如果已经掌握了 Web 技术,这个技术栈就主要学习容器提供的 API Bridge,网页通过它们去调用底层硬件的 API。
(3)跨平台 App 技术栈 (cross-platform technology stack)
跨平台技术栈指的是使用一种技术,同时支持多个手机平台。它与混合技术栈的区别是,不使用 Web 技术,即它的页面不是 HTML5 页面,而是使用自己的语法写的 UI 层,然后编译成各平台的原生 App。
这个技术栈就是纯粹的容器技术栈,React Native、Xamarin、Flutter 都属于这一类。学习时,除了学习容器的 API Bridge,还要学习容器提供的 UI 层,即怎么写页面。
WebView 控件
讲解具体的技术栈之前,大家需要知道,不管什么技术,最终在 App 里面显示网页,一定需要一个网页引擎,这样才能解析网页。
通常情况下,App 内部会使用 WebView 控件作为网页引擎。这是系统自带的控件,专门用来显示网页。应用程序的界面,只要放上 WebView,就好像内嵌了浏览器窗口,可以显示网页。
不同的 App 技术栈要显示网页,区别仅仅在于怎么处理 WebView 这个原生控件。
- 原生技术栈:需要开发者自己把 WebView 控件放到页面上。
- 混合技术栈:页面本身就是网页,默认在 WebView 中显示。
- 跨平台技术栈:提供一个 WebView 的语法,编译的时候将其换成原生的 WebView。
注意,不同系统的 WebView 控件名称不一样,安卓系统就叫 WebView,iOS 系统有较老的 UIWebView,也有较新的 WKWebView,作用都是一样的,差异在于功能的强弱。
原生技术栈
原生技术栈分成 iOS 和安卓两个平台。
简单说,iOS 的原生技术栈就是使用 Object-C 语言或 Swift 语言,在 Xcode 开发环境中编程。安卓的原生技术栈,则是使用 Java 语言或 Kotlin 语言,开发环境是 Android Studio。
混合技术栈
上面的原生技术栈需要自己新建 WebView 实例,相比之下,混合技术栈就简单多了。因为页面就是网页,所以容器已经设置好了 WebView,开发者直接写页面即可。
框架种类
混合技术栈的各种容器框架之中,历史最悠久是 PhoneGap,诞生于2009年。后来在2011年被 Adobe 公司收购,改名为 Adobe PhoneGap。
Adobe 公司将 PhoneGap 的核心代码,后来都捐给了 Apache 基金会,作为一个全新的开源项目,名为 Apache Cordova。
PhoneGap 和 Cordova 现在是两个独立发展的开源项目,但是彼此有密切的关系,可以简单理解成 Cordova 是 PhoneGap 的内核,PhoneGap 是 Cordova 的发行版。
后来,其他人也开始基于 Cordova 封装自己的框架,所以市场上有许多基于 Cordova 的开源框架,比较著名的有 Ionic、Monaca、Framework7 等。
所有这些框架的共同点,都是使用 Web 技术(HTML5 + CSS + JavaScript)开发页面,再由框架分别打包成 iOS 和安卓的 App 安装包。它们的优点是开发简单、周期短、成本低,缺点是功能和性能都很有限。
跨平台技术栈
上面的混合技术栈使用 HTML 语言编写页面,再用 WebView 控件加载页面,所以只写一次页面,就能支持多个平台。跨平台技术栈也能做到多平台支持,但是原理完全不同。
跨平台技术栈的框架,都是使用自己的语法编写页面,不使用 Web 技术,编译的时候再将其转为原生控件,或者使用自己的底层控件,生成原生 App。这样就完全解决了 Web 页面性能不佳的问题。下面介绍三个这样的框架。
- React Native: 使用 JavaScipt 语言编写页面
- Xamarin:使用 C# 语言编写页面
- Flutter:使用 Dart 语言编写页面
- React Native
(1)原理
2013年, Facebook 公司发布了 React 框架。这个框架是为网页开发设计的,核心思想是在网页之上,建立一 个 UI 的抽象层,所有数据操作都在这个抽象层完成(即在内存里面完成),然后再渲染成网页的 DOM 结构, 这样就提升了性能。
很快,工程师们就意识到了,UI 抽象层本质上是一种数据结构,与底层设备无关,不仅可以渲染成网页,也可 以渲染成手机的原生页面。这样的话,只要写一次 React 页面,就能分别编译成 iOS 和安卓的原生 App。这就 是 React Native 项目的由来。
注意,React Native 虽然也使用 JavaScript 语言,并且写法看上去像 Web 页面,但其实所有控件都是自己定义 的,编译时再一一翻译为对应的原生控件。举例来说,React Native 的文本渲染控件是,翻译成 iOS 控件为 UIView,翻译成安卓控件为TextView
。这种做即保证了性能,又做到了跨平台支持,所以一诞生就引起开发者 的关注,成了热门技术。
还有一个 NativeScript 框架,跟 React Native 很像,也是使用 JavaScript 语言,然后编译成原生控件。不过, 它的开发模型是基于 Angular.js,而不是 React。
(2)React Native 的问题
React Native 的想法虽然很美好,但是实际开发中出现了各种各样的问题。
最主要的一个问题是, UI 抽象层翻译出来的 iOS 和安卓原生页面,做不到完全一致,尤其是复杂页面,样式或 功能存在差异。编译出来两个平台的原生 App 往往是一个正常,另一个会出现各种奇怪的小毛病。ReactNative 的底层还是没有做到无缝适配,它至今没有发布 1.0 版(2019年底是 0.61 版),这多多少少也说明了一些题。
如果你想用 React Native 做到 iOS 和安卓体验一致,并且充分发挥原生控件的功能,就需要同时熟悉 React Native、iOS、安卓三个平台,这对开发者的要求实在太高了。Airbnb 公司在使用 React Native 两年后,宣 布放弃,改用原生技术栈。
Xamarin
Xamarin 是微软公司的跨平台 App 开发框架,原理跟 React Native 很相似,只不过它的语言是 C#。
它的使用需要 Visual Studio,这里就不举例了。
Flutter
Flutter 是谷歌公司最新的跨平台开发框架。它为了解决 React Native 的平台差异问题,采用了一个完全不同的方案。
它自己实现了一套控件。打包的时候,会把这套控件打包进每一个 App,因此不存在调用原生控件的问题。不管什么平台,都调用内嵌的自己那套控件,就能做到 iOS 和安卓体验完全一致。
Flutter 历史还不长,应用还不广泛,API 也没稳定下来。但是很值得关注。
它是 Flutter 的官方语言,接近 JavaScript 语法,但是多了静态类型支持。
2.3 描述一下移动端跨平台
参考答案:
跨平台开发的目的
- 线上动态性,不需要频繁更新版本即可实现新业务的上线;
- 增加代码复用,减少开发者对多个平台差异适配的工作量,解决多端不一致的问题;
- 提高业务专注的同时,提供比web更好的体验;
- 降低开发成本.
跨平台开发流派
- Web 流:也被称为 Hybrid 技术,它基于 Web 相关技术来实现界面及功能
Cordova,AppCan,小程序,快应用
- 代码转换流:将某个语言转成 Objective-C、Java 或 C#,然后使用不同平台下的官方工具来开发
java2OC,OC2Java,java2C#
- 编译流:将某个语言编译为二进制文件,生成动态库或打包成 apk/ipa/xap 文件
Xamarin
- 虚拟机流:通过将某个语言的虚拟机移植到不同平台上来运行
Flutter,Titanium,React Native,Weex
跨平台开发主流技术
- Flutter(Google)
- ReactNative(FaceBook)
- Weex(Alibaba)
- Hybrid App
- Cordova(原PhoneGap,Adobe)()
- 小程序,快应用
比较
解决方案 | ReactNative | Weex | Flutter | Hybrid App |
---|---|---|---|---|
平台实现 | JavaScript | JavaScript | 无桥接,原生编码 | 无桥接,原生编码 |
引擎 | JSCore | JS V8 | Flutter engine | 原生渲染 |
核心语言 | React | Vue | Dart | Java/Obeject-C |
Apk大小(Release) | 7.6M | 10.6M | 8.1M | |
bundle文件大小 | 默认单一,较大 | 较小,多页面可多文件 | 不需要 | 不需要 |
上手难度(原生角度) | 较高 | 一般 | 一般 | 容易 |
框架程度 | 较重 | 较轻 | 重 | - |
特点 | 适合开发整体App | 适合单页面 | 适合开发整体App | 适合开发整体App |
社区 | 丰富,FaceBook重点维护 | 有点残念,托管apache | 刚出道小鲜肉,拥护者众多 | 丰富 |
线上动态性 | ✅ | ✅ | ❎ | ❎ |
跨平台支持 | Android、iOS | Android、iOS、Web | Android、iOS | Android、iOS |
解析:
Flutter
闲鱼、美团,饿了么、NOW直播(腾讯)、京东金融
Flutter是谷歌的最新移动UI框架。
优点
- 热重载(Hot Reload),利用Android Studio直接一个ctrl+s就可以保存并重载,模拟器立马就可以看见效果,相比原生冗长的编译过程强很多;
- 一切皆为Widget的理念,对于Flutter来说,手机应用里的所有东西都是Widget,通过可组合的空间集合、丰富的动画库以及分层课扩展的架构实现了富有感染力的灵活界面设计;
- 借助可移植的GPU加速的渲染引擎以及高性能本地代码运行时以达到跨平台设备的高质量用户体验。 简单来说就是:最终结果就是利用Flutter构建的应用在运行效率上会和原生应用差不多。
缺点
- 不支持热更新;
- 三方库很少,需要自己造轮子;
- dart语言编写,掌握该语言的开发者很少。
React Native
墨刀,京东,手机百度 ,腾讯QQ,QQ空间,Facebook及旗下应用
React Native (简称RN)是Facebook于2015年4月开源的跨平台移动应用开发框架
优点
- 效率体验接近原生应用质量,发布和开发成本低于原生App;
- 支持热更新,快速迭代;
- 社区活跃,基本坑点都能解决;
- 代码跨越双平台
缺点
- RN 的开源库质量不可靠;
- RN 运行时的初始化太慢,首次渲染时间慢(需要从 主线程 -> JS -> Yoga -> 主线程);
- 调试困难,JSCore 在 iOS / Android 上不一致 (Android 上是 RN 自己 bundle 的),很难 debug 这种坑
WEEX
淘宝,天猫,支付宝,网易考拉,网易严选
2016年4月21日,阿里巴巴在Qcon大会上宣布跨平台移动开发工具Weex。
优点
- 单页开发模式效率极高,热更新发包体积小,并且跨平台性更强
缺点
- 社区没有RN活跃,功能尚不健全,暂不适合完全使用Weex开发App
Hybrid App(Android/iOS+Html5)
微信,爱奇艺,我爱我家
Hybrid App主要以JS+Native两者相互调用为主,从开发层面实现“一次开发,多处运行”的机制,成为真正适合跨平台的开发
优点
- 兼具了Native App良好用户体验的优势;
- 提升了开发效率,h5可以多平台复用,由服务器快速迭代;
- 实现简单,且原生与h5开发人员充沛。
缺点
- 体验比不上原生,webView性能差;
- 适用部分展示页面,复杂交互仍需要原生开发;
- 需要对应平台人员配合,jsApi因需求需要调整,版本迭代并不自由.
2.4 谈谈移动端点击
参考答案:
移动端 300 ms 点击(click 事件)延迟
由于移动端会有双击缩放的这个操作,因此浏览器在 click 之后要等待 300ms,判断这次操作是不是双击。
解决方案:
- 禁用缩放:user-scalable=no
- 更改默认的视口宽度
- CSS touch-action
点击穿透问题
因为 click 事件的 300ms 延迟问题,所以有可能会在某些情况触发多次事件。
解决方案:
- 只用 touch
- 只用 click
2.5 什么是响应式开发?
参考答案:
响应式开发是前端开发工作比较常见的工作内容,随着移动互联网的发展,移动端设计越来越重要,很多项目都是移动端项目先开发,而后是PC端的开发,为了降低运营成本和开发成本,同一个网站要能兼容PC端和移动端显示呼之欲出,进而响应式开发成了前端开发人员必备的技能,所以响应式开发的技术必须掌握。
什么是响应式
顾名思义,同一个网站兼容不同的大小的设备。如PC端、移动端(平板、横屏、竖排)的显示风格。
需要用到的技术
- Media Query(媒体查询)
用于查询设备是否符合某一特定条件,这些特定条件包括屏幕尺寸,是否可触摸,屏幕精度,横屏竖屏等信息。
- 使用em或rem做尺寸单位
用于文字大小的响应和弹性布局。
- 禁止页面缩放
<meta name="viewport" content="initial-scale=1, width=device-width, maximum-scale=1, user-scalable=no" /> 复制代码
- 屏幕尺寸响应
a) 固定布局:页面居中,两边留白,他能适应大于某个值一定范围的宽度,但是如果太宽就会有很多留白,太窄会出现滚动条,在PC页面上很常见。
b) 流动布局:屏幕尺寸在一定范围内变化时,不改变模块布局,只改变模块尺寸比例。比固定布局更具响应能力,两边不留白,但是也只能适应有限的宽度变化范围,否则模块会被挤(拉)得不成样子。
c) 自定义布局:上面几种布局方式都无法跨域大尺寸变化,所以适当时候我们需要改变模块的位置排列或者隐藏一些次要元素。
d) 栅格布局:这种布局方式使得模块之间非常容易对齐,易于模块位置的改变用于辅助自定义布局。
响应式设计注意事项
1.宽度不固定,可以使用百分比
#head{width:100%;} #content{width:50%;}
- 图片处理
图片的宽度和高度设置等比缩放,可以设置图片的width为百分比,height:auto;
背景图片可以使用background-size 指定背景图片的大小。
3.性能
3.1 前端性能优化手段
参考答案:
前端性能优化手段从以下几个方面入手:加载优化、执行优化、渲染优化、样式优化、脚本优化
加载优化:减少HTTP请求、缓存资源、压缩代码、无阻塞、首屏加载、按需加载、预加载、压缩图像、减少Cookie、避免重定向、异步加载第三方资源
执行优化:CSS写在头部,JS写在尾部并异步、避免img、iframe等的src为空、尽量避免重置图像大小、图像尽量避免使用DataURL
渲染优化:设置viewport、减少DOM节点、优化动画、优化高频事件、GPU加速
样式优化:避免在HTML中书写style、避免CSS表达式、移除CSS空规则、正确使用display:display
、不滥用float等
脚本优化:减少重绘和回流、缓存DOM选择与计算、缓存.length的值、尽量使用事件代理、尽量使用id选择器、touch事件优化
解析:
加载优化
减少HTTP请求:尽量减少页面的请求数(首次加载同时请求数不能超过4个),移动设备浏览器同时响应请求为4个请求(
Android
支持4个,iOS5+
支持6个)- 合并CSS和JS
- 使用CSS精灵图
缓存资源:使用缓存可减少向服务器的请求数,节省加载时间,所有静态资源都要在服务器端设置缓存,并且尽量使用长缓存(使用时间戳更新缓存)
- 缓存一切可缓存的资源
- 使用长缓存
- 使用外联的样式和脚本
压缩代码:减少资源大小可加快网页显示速度,对代码进行压缩,并在服务器端设置
GZip
- 压缩代码(多余的缩进、空格和换行符)
- 启用Gzip
无阻塞:头部内联的样式和脚本会阻塞页面的渲染,样式放在头部并使用
link
方式引入,脚本放在尾部并使用异步方式加载首屏加载:首屏快速显示可大大提升用户对页面速度的感知,应尽量针对首屏的快速显示做优化
按需加载:将不影响首屏的资源和当前屏幕不用的资源放到用户需要时才加载,可大大提升显示速度和降低总体流量(按需加载会导致大量重绘,影响渲染性能)
- 懒加载
- 滚屏加载
- Media Query加载
预加载:大型资源页面可使用
Loading
,资源加载完成后再显示页面,但加载时间过长,会造成用户流失- 可感知Loading:进入页面时
Loading
- 不可感知Loading:提前加载下一页
- 可感知Loading:进入页面时
压缩图像:使用图像时选择最合适的格式和大小,然后使用工具压缩,同时在代码中用
srcset
来按需显示(过度压缩图像大小影响图像显示效果)
减少Cookie:
Cookie
会影响加载速度,静态资源域名不使用Cookie
避免重定向:重定向会影响加载速度,在服务器正确设置避免重定向
异步加载第三方资源:第三方资源不可控会影响页面的加载和显示,要异步加载第三方资源
执行优化
- CSS写在头部,JS写在尾部并异步
- 避免img、iframe等的src为空:空
src
会重新加载当前页面,影响速度和效率 - 尽量避免重置图像大小:多次重置图像大小会引发图像的多次重绘,影响性能
- 图像尽量避免使用DataURL:
DataURL
图像没有使用图像的压缩算法,文件会变大,并且要解码后再渲染,加载慢耗时长
渲染优化
设置viewport:HTML的
viewport
可加速页面的渲染<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, minimum-scale=1, maximum-scale=1"> 复制代码
减少DOM节点:DOM节点太多影响页面的渲染,尽量减少DOM节点
优化动画
- 尽量使用CSS3动画
- 合理使用requestAnimationFrame动画代替setTimeout
- 适当使用Canvas动画:5个元素以内使用
CSS动画
,5个元素以上使用Canvas动画
,iOS8+
可使用WebGL动画
优化高频事件:
scroll
、touchmove
等事件可导致多次渲染- 函数节流
- 函数防抖
- 使用requestAnimationFrame监听帧变化:使得在正确的时间进行渲染
- 增加响应变化的时间间隔:减少重绘次数
GPU加速:使用某些HTML5标签和CSS3属性会触发
GPU渲染
,请合理使用(过渡使用会引发手机耗电量增加)- HTML标签:
video
、canvas
、webgl
- CSS属性:
opacity
、transform
、transition
- HTML标签:
样式优化
- 避免在HTML中书写style
- 避免CSS表达式:CSS表达式的执行需跳出CSS树的渲染
- 移除CSS空规则:CSS空规则增加了css文件的大小,影响CSS树的执行
- 正确使用
display:display
会影响页面的渲染display:inline
后不应该再使用float
、margin
、padding
、width
和height
display:inline-block
后不应该再使用float
display:block
后不应该再使用vertical-align
display:table-*
后不应该再使用float
和margin
- 不滥用float:
float
在渲染时计算量比较大,尽量减少使用 - 不滥用Web字体:Web字体需要下载、解析、重绘当前页面,尽量减少使用
- 不声明过多的font-size:过多的
font-size
影响CSS树的效率 - 值为0时不需要任何单位:为了浏览器的兼容性和性能,值为
0
时不要带单位 - 标准化各种浏览器前缀
- 无前缀属性应放在最后
- CSS动画属性只用-webkit-、无前缀两种
- 其它前缀为-webkit-、-moz-、-ms-、无前缀四种:
Opera
改用blink
内核,-o-
已淘汰
- 避免让选择符看起来像正则表达式:高级选择符执行耗时长且不易读懂,避免使用
脚本优化
- 减少重绘和回流
- 避免不必要的DOM操作
- 避免使用document.write
- 减少drawImage
- 尽量改变class而不是style,使用classList代替className
- 缓存DOM选择与计算:每次DOM选择都要计算和缓存
- 缓存.length的值:每次
.length
计算用一个变量保存值 - 尽量使用事件代理:避免批量绑定事件
- 尽量使用id选择器:
id
选择器选择元素是最快的 - touch事件优化:使用
tap
(touchstart
和touchend
)代替click
(注意touch
响应过快,易引发误操作)
常用规则
雅虎军规
- 内容
- Make Fewer HTTP Requests:减少
HTTP
请求数 - Reduce DNS Lookups:减少
DNS
查询 - Avoid Redirects:避免重定向
- Make Ajax Cacheable:缓存
AJAX请求
- Postload Components:延迟加载资源
- Preload Components:预加载资源
- Reduce The Number Of DOM Elements:减少
DOM
元素数量 - Split Components Across Domains:跨域拆分资源
- Minimize The Number Of Iframes:减少
iframe
数量 - No 404s:消除
404
错误
- Make Fewer HTTP Requests:减少
- 样式
- Put Stylesheets At The Top:置顶样式
- Avoid CSS Expressions:避免
CSS
表达式 - Choose Over @import:选择``代替
@import
- Avoid Filters:避免滤镜
- 脚本
- Put Scripts At The Bottom:置底脚本
- Make JavaScript And CSS External:使用外部
JS
和CSS
- Minify JavaScript And CSS:压缩
JS
和CSS
- Remove Duplicate Scripts:删除重复脚本
- Minimize DOM Access:减少
DOM
操作 - Develop Smart Event Handlers:开发高效的事件处理
- 图像
- Optimize images:优化图片
- Optimize CSS Sprites:优化
CSS精灵图
- Don't Scale images In HTML:不在
HTML
中缩放图片 - Make Favicon.ico Small And Cacheable:使用小体积可缓存的
favicon
- 缓存
- Reduce Cookie Size:减少
Cookie
大小 - Use Cookie-Free Domains For Components:使用无
Cookie
域名的资源
- Reduce Cookie Size:减少
- 移动端
- Keep Components Under 25kb:保持资源小于
25kb
- Pack Components Into A Multipart Document:打包资源到多部分文档中
- Keep Components Under 25kb:保持资源小于
- 服务器
- Use A Content Delivery Network:使用
CDN
- Add An Expires Or A Cache-Control Header:响应头添加
Expires
或Cache-Control
- Gzip Components:
Gzip
资源 - Configure ETags:配置
ETags
- Flush The Buffer Early:尽早输出缓冲
- Use Get For AJAX Requests:
AJAX请求
时使用get
- Avoid Empty Image Src:避免图片空链接
- Use A Content Delivery Network:使用
2-5-8原则
在前端开发中,此规则作为一种开发指导思路,针对浏览器页面的性能优化。
- 用户在
2秒内
得到响应,会感觉页面的响应速度很快 Fast - 用户在
2~5秒间
得到响应,会感觉页面的响应速度还行 Medium - 用户在
5~8秒间
得到响应,会感觉页面的响应速度很慢,但还可以接受 Slow - 用户在
8秒后
仍然无法得到响应,会感觉页面的响应速度垃圾死了
3.2 一个官网怎么优化,有哪些性能指标,如何量化
参考答案:
对首屏加载速度影响最大的还是资源大小,请求数量,请求速度等。代码方面,前端一般很难写出严重影响速度的代码。减小资源大小,可以用各种压缩,懒加载,预加载,异步加载等方法。减少请求数量可以使用雪碧图,搭建node中台将多个请求合并成一个等。对于官网这种项目,最好使用服务端渲染,首屏快之外,也有利于SEO。
检测方案:
使用lighthouse进行性能检测,并对lighthouse提出的建议进行优化
具体优化方案:
通过静态化、图片懒加载、图片压缩、异步加载(js和css)、优化代码等方式,以下是具体方法
静态化
服务端渲染,“直出”页面,具有较好的SEO和首屏加载速度。主要还有以下的优点:
- 使用jsp模板语法(百度后发现是用Velocity模板语法)渲染页面,减少了js文件体积
- 减少了请求数量
- 因为不用等待大量接口返回,加快了首屏时间
可以尝试Vue的服务端渲染。首页目前有部分是用接口读取数据,然后用jq进行渲染,性能上应该不如Virtual DOM,不过内容不多。
图片懒加载
这是一个很重要的优化项。因为官网上有很多图片,而且编辑们上传文章图片的时候一般没有压缩,但是很多图片的体积都很大。还有一个轮播图,20张图标,最小的几十K,最大的两百多K。对于图片来源不可控的页面,懒加载是个很实用的操作,直接将首屏加载的资源大小加少了十几M。
图片压缩
对于来源可控,小图标等图片可以用雪碧图,base64等方法进行优化。目前只是用工具压缩了图片大小,后续可以考虑在webpack打包的时候生成雪碧图。
异步加载js
通过标签引入的js文件,可以设置defer
,async
属性让其异步加载,而不会阻塞渲染。defer
和async
的区别在于async
加载完就立即执行,没有考虑依赖,标签顺序等。而defer
加载完后会等它前面引入的文件执行完再执行。一般defer
用的比较多,async
只能用在那些跟别的文件没有联系的孤儿脚本上。
异步加载css
没想到css也能异步加载,但这是lighthouse给出的建议。找了一下发现有以下两种方法:
一是通过js脚本在文档中插入标签
二是通过``的media
属性
media属性是媒体查询用的,用于在不同情况下加载不同的css。这里是将其设置为一个不适配当前浏览器环境的值,甚至是不能识别的值,浏览器会认为这个样式文件优先级低,会在不阻塞的情况加载。加载完成后再将
media`设置为正常值,让浏览器解析css。
<link rel="stylesheet" href="//example.css" media="none" onload="this.media='all'">
这里用的是第二种方法。但是webpack注入到html中的外链css还没找到异步加载的方法。
preconnent
lighthouse建议对于接下来会访问的地址可以提前建立连接。一般有一下几种方式。
dns-prefetch
域名预解析
<link rel="dns-prefetch" href="//example.com">
preconnet
预连接
<link rel="preconnect" href="//example.com"> <link rel="preconnect" href="//cdn.example.com" crossorigin>
prefetch
预加载
<link rel="prefetch" href="//example.com/next-page.html" as="html" crossorigin="use-credentials"> <link rel="prefetch" href="library.js" as="script">
prerender
预渲染
<link rel="prerender" href="//example.com/next-page.html">
这四种层层递进,但是不要连接不需要的资源,反而损耗性能。我在页面上对某些资源用了preconnect
,但并没有明显的效果。应该对于在线小说,在线漫画这种场景预加载会更适用。
代码优化
lighthouse上显示主线程耗时最多的是样式和布局,所以对这部分进行优化。主要有一下几点:
- 去掉页面上用于布局的table,table本身性能较低,且维护性差,是一种过时的布局方案。
- 在去掉table布局的同时减少一些无意义的DOM元素,减少DOM元素的数量和嵌套。
- 减少css选择器的嵌套。用sass,less这种css预处理器很容易造成多层嵌套。优化前代码里最多的有七八层嵌套,对性能有一定影响。重构后不超过三层。
通过上面的重构后,样式布局和渲染时间从lighthouse上看大概减少了300ms。但样式和布局的时间还是最长的,感觉还有优化空间。
接下来是js代码的优化和重构。因为移除Vue框架,以及用服务端端直出,现在js代码已经减少了大部分。主要有以下几部分:
- 拆分函数,将功能复杂的函数拆分成小函数,让每个函数只做一件事。
- 优化分支结构,用对象
Object
,代替if...else
和switch...case
如下面这段代码,优化后变得更加简洁,也便于维护。
// 优化前 var getState = function (state) { switch (state) { case 1: return 'up'; case 0: return 'stay'; case 2: return 'down'; } } // 优化后 var getState = function(state) { var stateMap = { 1: 'up', 0: 'stay', 2: 'down' } return stateMap[state] }
- 优化DOM操作
DOM操作如改变样式,改变内容可能会引起页面的重绘重排,是比较消耗性能的。网上也有很多优化jq操作的方法。
如将查询到的DOM使用变量存起来,避免重复查询。以及将多次DOM操作变成一次等。这里重点讲一下第二种。
常见的需求是渲染一个列表,如果直接在for循环里面append到父元素中,性能是非常差的。幸好原来的操作是将所有DOM用字符串拼接起来,再用html()
方法一次性添加到页面中。
还有另一种方法是使用文档碎片(fragment
)。通过document.createDocumentFragment()
可以新建一个fragment。向fragment
中appendChild
元素的时候是不会阻塞渲染进程的。最后将fragment
替换掉页面上的元素。将fragment
元素用appendChild
的方法添加到页面上时,实际上添加上去的是它内部的元素,也就是它的子元素。
var fragment = document.createDocumentFragment() for (var i = 0; i < data.length; i++) { var str = '<div>' + i + '</div>' fragment.appendChild($(str)[0]) } $('.container').append(fragment)
经过测试,在当前的场景下,使用fragment
的速度和html()
是差不多的,都是10ms左右。区别在于最后将fragment
添加到页面上$('.container').append(fragment)
这行代码仅仅花费1ms。也就是说,将fragment
插入页面时不会引起页面重绘重排,不会引起阻塞。
3.3 尾调用优化
参考答案:
尾调用是指某个函数的最后一步是调用另一个函数。
函数调用会在内存形成一个“调用记录”,又称“调用帧”,保存调用位置和内存变量等信息。如果在函数A
的内部调用函数B
,那么在A
的调用帧上方,还会形成一个B
的调用帧。等到B
运行结束,将结果返回到A
,B
的调用帧才会消失。如果函数B
内部还调用函数C
,那就还有一个C
的调用帧,依次类推。所有的调用帧,就形成一个“调用栈”。
尾调用由于是函数的最后一步操作,所有不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。
如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”。注意,只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。
尾调用案例:
function addOne(a) { var one = 1; function inner(b) { return b + one; } return inner(a); }
4. 安全
4.1 如何提高网站的安全性?
参考答案:
前端常见安全问题的7个方面:
- iframe
- opener
- CSRF(跨站请求伪造)
- XSS(跨站脚本攻击)
- ClickJacking(点击劫持)
- HSTS(HTTP严格传输安全)
- CND劫持
解析:
iframe
a.如何让自己的网站不被其他网站的 iframe 引用?
// 检测当前网站是否被第三方iframe引用 // 若相等证明没有被第三方引用,若不等证明被第三方引用。当发现被引用时强制跳转百度。 if(top.location != self.location){ top.location.href = 'http://www.baidu.com' }
b.如何禁用,被使用的 iframe 对当前网站某些操作?
sandbox是html5的新属性,主要是提高iframe安全系数。iframe因安全问题而臭名昭著,这主要是因为iframe常被用于嵌入到第三方中,然后执行某些恶意操作。
现在有一场景:我的网站需要 iframe 引用某网站,但是不想被该网站操作DOM、不想加载某些js(广告、弹框等)、当前窗口被强行跳转链接等,我们可以设置 sandbox 属性。如使用多项用空格分隔。
- allow-same-origin:允许被视为同源,即可操作父级DOM或cookie等
- allow-top-navigation:允许当前iframe的引用网页通过url跳转链接或加载
- allow-forms:允许表单提交
- allow-scripts:允许执行脚本文件
- allow-popups:允许浏览器打开新窗口进行跳转
- “”:设置为空时上面所有允许全部禁止
- opener
如果在项目中需要 打开新标签 进行跳转一般会有两种方式
// 1) HTML -> <a target='_blank' href='http://www.baidu.com'> // 2) JS -> 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 target="_blank" href="" rel="noopener noreferrer nofollow">a标签跳转url</a> <!-- 通过 rel 属性进行控制: noopener:会将 window.opener 置空,从而源标签页不会进行跳转(存在浏览器兼容问题) noreferrer:兼容老浏览器/火狐。禁用HTTP头部Referer属性(后端方式)。 nofollow:SEO权重优化,详情见 https://blog.csdn.net/qq_33981438/article/details/80909881 -->
b.window.open()
<button onclick='openurl("http://www.baidu.com")'>click跳转</button> function openurl(url) { var newTab = window.open(); newTab.opener = null; newTab.location = url; }
- CSRF / XSRF(跨站请求伪造)
你可以这么理解 CSRF 攻击:攻击者盗用了你的身份,以你的名义进行恶意请求。它能做的事情有很多包括:以你的名义发送邮件、发信息、盗取账号、购买商品、虚拟货币转账等。总结起来就是:个人隐私暴露及财产安全问题。
/* * 阐述 CSRF 攻击思想:(核心2和3) * 1、浏览并登录信任网站(举例:淘宝) * 2、登录成功后在浏览器产生信息存储(举例:cookie) * 3、用户在没有登出淘宝的情况下,访问危险网站 * 4、危险网站中存在恶意代码,代码为发送一个恶意请求(举例:购买商品/余额转账) * 5、携带刚刚在浏览器产生的信息进行恶意请求 * 6、淘宝验证请求为合法请求(区分不出是否是该用户发送) * 7、达到了恶意目标 */
防御措施(推荐添加token / HTTP头自定义属性)
- 涉及到数据修改操作严格使用 post 请求而不是 get 请求
- HTTP 协议中使用 Referer 属性来确定请求来源进行过滤(禁止外域)
- 请求地址添加 token ,使黑客无法伪造用户请求
- HTTP 头自定义属性验证(类似上一条)
- 显示验证方式:添加验证码、密码等
- XSS/CSS(跨站脚本攻击)
XSS又叫CSS(Cross Site Script),跨站脚本攻击:攻击者在目标网站植入恶意脚本(js / html),用户在浏览器上运行时可以获取用户敏感信息(cookie / session)、修改web页面以欺骗用户、与其他漏洞相结合形成蠕虫等。
浏览器遇到 html 中的 script 标签时,会解析并执行其中的js代码
针对这种情况,我们对特殊字符进行转译就好了(vue/react等主流框架已经避免类似问题,vue举例:不能在template中写script标签,无法在js中通过ref或append等方式动态改变或添加script标签)
XSS类型:
- 持久型XSS:将脚本植入到服务器上,从而导致每个访问的用户都会执行
- 非持久型XSS:对个体用户某url的参数进行攻击
防御措施(对用户输入内容和服务端返回内容进行过滤和转译)
- 现代大部分浏览器都自带 XSS 筛选器,vue / react 等成熟框架也对 XSS 进行一些防护
- 即便如此,我们在开发时也要注意和小心
- 对用户输入内容和服务端返回内容进行过滤和转译
- 重要内容加密传输
- 合理使用get/post等请求方式
- 对于URL携带参数谨慎使用
- 我们无法做到彻底阻止,但是能增加黑客攻击成本,当成本与利益不符时自然会降低风险
- ClickJacking(点击劫持)
ClickJacking 翻译过来被称为点击劫持。一般会利用透明 iframe 覆盖原网页诱导用户进行某些操作达成目的。
防御措施
- 在HTTP投中加入 X-FRAME-OPTIONS 属性,此属性控制页面是否可被嵌入 iframe 中【DENY:不能被所有网站嵌套或加载;SAMEORIGIN:只能被同域网站嵌套或加载;ALLOW-FROM URL:可以被指定网站嵌套或加载。】
- 判断当前网页是否被 iframe 嵌套(详情在第一条 firame 中)
- HSTS(HTTP Strict Transport Security:HTTP严格传输安全)
网站接受从 HTTP 请求跳转到 HTTPS 请求的做法,例如我们输入“http://www.baidu.com”或“www.baidu.com”最终都会被302重定向到“[https://www.baidu.com](https://link.zhihu.com/?target=https%3A//www.baidu.com)”。这就存在安全风险,当我们第一次通过 HTTP 或域名进行访问时,302重定向有可能会被劫持,篡改成一个恶意或钓鱼网站。
HSTS:通知浏览器此网站禁止使用 HTTP 方式加载,浏览器应该自动把所有尝试使用 HTTP 的请求自动替换为 HTTPS 进行请求。用户首次访问时并不受 HSTS 保护,因为第一次还未形成链接。我们可以通过 浏览器预置HSTS域名列表 或 将HSTS信息加入到域名系统记录中,来解决第一次访问的问题。
- CDN劫持
出于性能考虑,前端应用通常会把一些静态资源存放到CDN(Content Delivery Networks)上面,例如 js 脚本和 style 文件。这么做可以显著提高前端应用的访问速度,但与此同时却也隐含了一个新的安全风险。如果攻击者劫持了CDN,或者对CDN中的资源进行了污染,攻击者可以肆意篡改我们的前端页面,对用户实施攻击。
现在的CDN以支持SRI为荣,script 和 link 标签有了新的属性 integrity,这个属性是为了防止校验资源完整性来判断是否被篡改。它通过 验证获取文件的哈希值是否和你提供的哈希值一样来判断资源是否被篡改。
使用 SRI 需要两个条件:一是要保证 资源同域 或开启跨域,二是在<script>中 提供签名 以供校验。
integrity 属性分为两个部分,第一部分是指定哈希值的生成算法(例:sha384),第二部分是经过编码的实际哈希值,两者之前用一个短横(-)来分隔
4.2 前端安全相关-XSS和CSRF
参考答案:
XSS(Cross-site scripting),指的是跨站脚本攻击,攻击者通过向页面A注入代码,达到窃取信息等目的,本质是数据被当作程序执行。XSS危害是很大的,一般XSS可以做到以下的事情:
- 获取页面的数据,包括dom、cookies、localStorage等
- 劫持前端逻辑
- 发送请求
CSRF(Cross Site Request Frogy)指的是跨站请求伪造。与XSS不同的是,XSS是攻击者直接对我们的网站A进行注入攻击,CSRF是通过网站B对我们的网站A进行伪造请求。
举个例子,你登录购物网站A之后点击一个恶意链接B,B请求了网站A的下单接口,结果是你在网站A的帐号真的会生成一个订单。其背后的原理是:网站B通过表单、get请求来伪造网站A的请求,这时候请求会带上网站A的cookies,若登录态是保存在cookies中,则实现了伪造攻击。
解析:
XSS
XSS的类型
- 反射型(非持久):通过URL参数直接注入
- 存储型(持久):存储到数据库后读取时注入
- 基于DOM:被执行的恶意脚本会修改页面脚本结构
XSS的注入点
- HTML的节点内容或属性
- javascript代码
- 富文本
XSS的防御
3.1 浏览器的防御
防御和“X-XSS-Protection”有关,默认值为1,即默认打开XSS防御,可以防御反射型的XSS,不过作用有限,只能防御注入到HTML的节点内容或属性的XSS,例如URL参数中包含script标签。不建议只依赖此防御手段。
3.2 防御HTML节点内容
存在风险的代码:
<template> <p>{{username}}</p> </template> <script> username = "<script>alert('xss')</script>" </script>
编译后的代码:
<p> <script>alert('xss')</script> </p>
以上例子是采用vue语法,但其实在vue这样的框架中,{{username}}中的内容是经过字符串化的,所以是不会被浏览器执行的,若换其他模板语言例如jade,则可能存在风险。下同。
防御代码:
通过转义
<
为<
以及>
为>
来实现防御HTML节点内容。<template> <p>{{username}}</p> </template> <script> escape = function(str){ return str.replace(/</g, '<').replace(/>/g, '>') } username = escape("<script>alert('xss')</script>") </script>
3.3 防御HTML属性
<template> <img :src="image" /> </template> <script> image = 'www.a.com/c.png" onload="alert(1)' </script>
编译后代码:
<img src="www.a.com/c.png" onload="alert(1)" />
防御代码:
通过转义
"
为&quto;
、'
为'
来实现防御,一般不转义空格,但是这要求属性必须带引号!<template> <img :src="image" /> </template> <script> escape = function(str){ return str.replace(/"/g, '&quto;').replace(/'/g, ''').replace(/ /g, ' ') } image = escape('www.a.com/c.png" onload="alert(1)') </script>
3.4 防御javaScript代码
假设访问页面地址为
www.a.com?id=1";alert(1);"
风险代码:
var id = getQuery('id')
编译后代码:
var id = "1";alert(1);""
防御代码:
通过将数据进行JSON序列化
escape = function(str){ return JSON.stringify(str) }
3.5 防御富文本
风险代码:
<template> <p v-html="richTxt"></p> </template> <script> richTxt = '<a onmouseover=alert(document.cookie)>点击</a>' </script>
上面的这段代码中,当鼠标移动到“点击”上面时,就会触发alert弹窗!这在vue中是会发生的。
防御富文本是比较复杂的工程,因为富文本可以包含HTML和script,这些难以预测与防御,建议是通过白名单的方式来过滤允许的HTML标签和标签的属性来进行防御,大概的实现方式是:
将HTML代码段转成树级结构的数据
遍历树的每一个节点,过滤节点的类型和属性,或进行特殊处理
处理完成后,将树级结构转化成HTML代码
当然,也可以通过开源的第三方库来实现,类似的有js-xss
3.6 CSP 内容安全策略
CSP(content security policy),是一个额外的安全层,用于检测并削弱某些特定类型的攻击,包括跨站脚本 (XSS) 和数据注入攻击等。
CSP可以通过HTTP头部(Content-Security-Policy)或``元素配置页面的内容安全策略,以控制浏览器可以为该页面获取哪些资源。比如一个可以上传文件和显示图片页面,应该允许图片来自任何地方,但限制表单的action属性只可以赋值为指定的端点。一个经过恰当设计的内容安全策略应该可以有效的保护页面免受跨站脚本攻击。
4.3 url的加密解密
参考答案:
JavaScript中有三个可以对字符串编码的函数,分别是: escape,encodeURI,encodeURIComponent,相应3个解码函数:unescape,decodeURI,decodeURIComponent 。
三种方式的特点:escape()除了 ASCII 字母、数字和特定的符号外,对传进来的字符串全部进行转义编码,因此如果想对URL编码,最好不要使用此方法。而encodeURI() 用于编码整个URI,因为URI中的合法字符都不会被编码转换。encodeURIComponent方法在编码单个URIComponent(指请求参数)应当是最常用的,它可以讲参数中的中文、特殊字符进行转义,而不会影响整个URL。
解析:
1.escape()函数
定义和用法
escape() 函数可对字符串进行编码,这样就可以在所有的计算机上读取该字符串。
语法
escape(string)
参数 描述
string 必需。要被转义或编码的字符串。
返回值
已编码的 string 的副本。其中某些字符被替换成了十六进制的转义序列。
说明
该方法不会对 ASCII 字母和数字进行编码,也不会对下面这些 ASCII 标点符号进行编码: - _ . ! ~ * ' ( ) 。其他所有的字符都会被转义序列替换。
*2.encodeURI()函数
*定义和用法
encodeURI() 函数可把字符串作为 URI 进行编码。
语法
encodeURI(URIstring)
参数 描述
URIstring 必需。一个字符串,含有 URI 或其他要编码的文本。
返回值
URIstring 的副本,其中的某些字符将被十六进制的转义序列进行替换。
说明
该方法不会对 ASCII 字母和数字进行编码,也不会对这些 ASCII 标点符号进行编码: - _ . ! ~ * ' ( ) 。
该方法的目的是对 URI 进行完整的编码,因此对以下在 URI 中具有特殊含义的 ASCII 标点符号,encodeURI() 函数是不会进行转义的:;/?:@&=+$,#
3.encodeURIComponent() 函数
定义和用法
encodeURIComponent() 函数可把字符串作为 URI 组件进行编码。
语法
encodeURIComponent(URIstring)
参数 描述
URIstring 必需。一个字符串,含有 URI 组件或其他要编码的文本。
返回值
URIstring 的副本,其中的某些字符将被十六进制的转义序列进行替换。
说明
该方法不会对 ASCII 字母和数字进行编码,也不会对这些 ASCII 标点符号进行编码: - _ . ! ~ * ' ( ) 。
其他字符(比如 :;/?:@&=+$,# 这些用于分隔 URI 组件的标点符号),都是由一个或多个十六进制的转义序列替换的。
提示和注释
提示:请注意 encodeURIComponent() 函数 与 encodeURI() 函数的区别之处,前者假定它的参数是 URI 的一部分(比如协议、主机名、路径或查询字符串)。因此 encodeURIComponent() 函数将转义用于分隔 URI 各个部分的标点符号。
4.4 前端安全问题
参考答案:
1. XSS:跨站脚本攻击
就是攻击者想尽一切办法将可以执行的代码注入到网页中。
存储型(server端):
- 场景:见于带有用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等。
- 攻击步骤:
- i)攻击者将恶意代码提交到目标网站的数据库中
- ii)用户打开目标网站时,服务端将恶意代码从数据库中取出来,拼接在HTML中返回给浏览器
- iii)用户浏览器在收到响应后解析执行,混在其中的恶意代码也同时被执行
- iv)恶意代码窃取用户数据,并发送到指定攻击者的网站,或者冒充用户行为,调用目标网站的接口,执行恶意操作
反射型(Server端)
与存储型的区别在于,存储型的恶意代码存储在数据库中,反射型的恶意代码在URL上
- 场景:通过 URL 传递参数的功能,如网站搜索、跳转等。
- 攻击步骤:
- i)攻击者构造出特殊的 URL,其中包含恶意代码。
- ii)用户打开带有恶意代码的 URL 时,网站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器。
- iii)用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
- iv)恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
Dom 型(浏览器端)
DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞,而其他两种 XSS 都属于服务端的安全漏洞。
- 场景:通过 URL 传递参数的功能,如网站搜索、跳转等。
- 攻击步骤:
- i)攻击者构造出特殊的 URL,其中包含恶意代码。
- ii)用户打开带有恶意代码的 URL。
- iii)用户浏览器接收到响应后解析执行,前端 JavaScript 取出 URL 中的恶意代码并执行。
- iv)恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
预防方案:(防止攻击者提交恶意代码,防止浏览器执行恶意代码)
- i)对数据进行严格的输出编码:如HTML元素的编码,JS编码,CSS编码,URL编码等等
- 避免拼接 HTML;Vue/React 技术栈,避免使用 v-html / dangerouslySetInnerHTML
- ii)CSP HTTP Header,即 Content-Security-Policy、X-XSS-Protection
- 增加攻击难度,配置CSP(本质是建立白名单,由浏览器进行拦截)
Content-Security-Policy: default-src 'self'
-所有内容均来自站点的同一个源(不包括其子域名)Content-Security-Policy: default-src 'self' *.trusted.com
-允许内容来自信任的域名及其子域名 (域名不必须与CSP设置所在的域名相同)Content-Security-Policy: default-src https://yideng.com
-该服务器仅允许通过HTTPS方式并仅从yideng.com域名来访问文档
- iii)输入验证:比如一些常见的数字、URL、电话号码、邮箱地址等等做校验判断
- iv)开启浏览器XSS防御:Http Only cookie,禁止 JavaScript 读取某些敏感 Cookie,攻击者完成 XSS 注入后也无法窃取此 Cookie。
- v)验证码
2. CSRF:跨站请求伪造
攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。
攻击流程举例
- i)受害者登录 a.com,并保留了登录凭证(Cookie)
- ii)攻击者引诱受害者访问了b.com
- iii)b.com 向 a.com 发送了一个请求:a.com/act=xx浏览器会默认携带a.com的Cookie
- iv)a.com接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求
- v)a.com以受害者的名义执行了act=xx
- vi)攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让a.com执行了自己定义的操作
攻击类型
- i)GET型:如在页面的某个 img 中发起一个 get 请求
- ii)POST型:通过自动提交表单到恶意网站
- iii)链接型:需要诱导用户点击链接
预防方案:
CSRF通常从第三方网站发起,被攻击的网站无法防止攻击发生,只能通过增强自己网站针对CSRF的防护能力来提升安全性。)
- i)同源检测:通过Header中的Origin Header 、Referer Header 确定,但不同浏览器可能会有不一样的实现,不能完全保证
- ii)CSRF Token 校验:将CSRF Token输出到页面中(通常保存在Session中),页面提交的请求携带这个Token,服务器验证Token是否
正确 - iii)双重cookie验证:
- 流程:
- 步骤1:在用户访问网站页面时,向请求域名注入一个Cookie,内容为随机字符串(例如csrfcookie=v8g9e4ksfhw)
- 步骤2:在前端向后端发起请求时,取出Cookie,并添加到URL的参数中(接上例POST https://www.a.com/comment?csrfcookie=v8g9e4ksfhw)
- 步骤3:后端接口验证Cookie中的字段与URL参数中的字段是否一致,不一致则拒绝。
- 优点:
- 无需使用Session,适用面更广,易于实施。
- Token储存于客户端中,不会给服务器带来压力。
- 相对于Token,实施成本更低,可以在前后端统一拦截校验,而不需要一个个接口和页面添加。
- 缺点:
-Cookie中增加了额外的字段。
-如果有其他漏洞(例如XSS),攻击者可以注入Cookie,那么该防御方式失效。
-难以做到子域名的隔离。
-为了确保Cookie传输安全,采用这种防御方式的最好确保用整站HTTPS的方式,如果还没切HTTPS的使用这种方式也会有风险。
- 流程:
- iv)Samesite Cookie属性:Google起草了一份草案来改进HTTP协议,那就是为Set-Cookie响应头新增Samesite属性,它用来标明这个 Cookie是个“同站 Cookie”,同站Cookie只能作为第一方Cookie,不能作为第三方Cookie,Samesite 有两个属性值,Strict 为任何情况下都不可以作为第三方 Cookie ,Lax 为可以作为第三方 Cookie , 但必须是Get请求
3. iframe 安全
说明:
- i)嵌入第三方 iframe 会有很多不可控的问题,同时当第三方 iframe 出现问题或是被劫持之后,也会诱发安全性问题
- ii)点击劫持
- 攻击者将目标网站通过 iframe 嵌套的方式嵌入自己的网页中,并将 iframe 设置为透明,诱导用户点击。
- iii)禁止自己的 iframe 中的链接外部网站的JS
预防方案:
- i)为 iframe 设置 sandbox 属性,通过它可以对iframe的行为进行各种限制,充分实现“最小权限“原则
- ii)服务端设置 X-Frame-Options Header头,拒绝页面被嵌套,X-Frame-Options 是HTTP 响应头中用来告诉浏览器一个页面是否可以嵌入 >iframe< 中
- eg.
X-Frame-Options: SAMEORIGIN
- SAMEORIGIN: iframe 页面的地址只能为同源域名下的页面
- ALLOW-FROM: 可以嵌套在指定来源的 iframe 里
- DENY: 当前页面不能被嵌套在 iframe 里
- eg.
- iii)设置 CSP 即 Content-Security-Policy 请求头
- iv)减少对 iframe 的使用
4. 错误的内容推断
说明:
文件上传类型校验失败后,导致恶意的JS文件上传后,浏览器 Content-Type Header 的默认解析为可执行的 JS 文件
预防方案:
设置 X-Content-Type-Options 头
5. 第三方依赖包
减少对第三方依赖包的使用,如之前 npm 的包如:event-stream 被爆出恶意攻击数字货币;
6.HTTPS
描述:
黑客可以利用SSL Stripping这种攻击手段,强制让HTTPS降级回HTTP,从而继续进行中间人攻击。
预防方案:
使用HSTS(HTTP Strict Transport Security),它通过下面这个HTTP Header以及一个预加载的清单,来告知浏览器和网站进行通信的时候强制性的使用HTTPS,而不是通过明文的HTTP进行通信。这里的“强制性”表现为浏览器无论在何种情况下都直接向务器端发起HTTPS请求,而不再像以往那样从HTTP跳转到HTTPS。另外,当遇到证书或者链接不安全的时候,则首先警告用户,并且不再
用户选择是否继续进行不安全的通信。
7.本地存储数据
避免重要的用户信息存在浏览器缓存中
8.静态资源完整性校验
描述
使用 内容分发网络 (CDNs) 在多个站点之间共享脚本和样式表等文件可以提高站点性能并节省带宽。然而,使用CDN也存在风险,如果攻击者获得对 CDN 的控制权,则可以将任意恶意内容注入到 CDN 上的文件中 (或完全替换掉文件),因此可能潜在地攻击所有从该 CDN 获取文件的站点。
预防方案
将使用 base64 编码过后的文件哈希值写入你所引用的 <script> 或 标签的 integrity 属性值中即可启用子资源完整性能。
9.网络劫持
描述:
- DNS劫持(涉嫌违法):修改运行商的 DNS 记录,重定向到其他网站。DNS 劫持是违法的行为,目前 DNS 劫持已被监管,现在很少见 DNS 劫持
- HTTP劫持:前提有 HTTP 请求。因 HTTP 是明文传输,运营商便可借机修改 HTTP 响应内容(如加广告)。
预防方案
全站 HTTPS
10.中间人攻击:
中间人攻击(Man-in-the-middle attack, MITM),指攻击者与通讯的两端分别创建独立的联系,并交换其所收到的数据,使通讯的两端认为他们正在通过一个私密的连接与对方直接对话,但事实上整个会话都被攻击者窃听、篡改甚至完全控制。没有进行严格的证书校验是中间人攻击着手点。目前大多数加密协议都提供了一些特殊认证方法以阻止中间人攻击。如 SSL (安全套接字层)协议可以验证参与通讯的用户的证书是否有权威、受信任的数字证书认证机构颁发,并且能执行双向身份认证。攻击场景如用户在一个未加密的 WiFi下访问网站。在中间人攻击中,攻击者可以拦截通讯双方的通话并插入新的内容。
场景
- i)在一个未加密的Wi-Fi 无线接入点的接受范围内的中间人攻击者,可以将自己作为一个中间人插入这个网络
- ii)Fiddler / Charles (花瓶)代理工具
- iii)12306 之前的自己证书
过程
- i)客户端发送请求到服务端,请求被中间人截获
- ii)服务器向客户端发送公钥
- iii)中间人截获公钥,保留在自己手上。然后自己生成一个【伪造的】公钥,发给客户端
- iv)客户端收到伪造的公钥后,生成加密hash值发给服务器
- v)中间人获得加密hash值,用自己的私钥解密获得真秘钥,同时生成假的加密hash值,发给服务器
- vi)服务器用私钥解密获得假密钥,然后加密数据传输给客户端
使用抓包工具fiddle来进行举例说明
- 首先通过一些途径在客户端安装证书
- 然后客户端发送连接请求,fiddle在中间截取请求,并返回自己伪造的证书
- 客户端已经安装了攻击者的根证书,所以验证通过
- 客户端就会正常和fiddle进行通信,把fiddle当作正确的服务器
- 同时fiddle会跟原有的服务器进行通信,获取数据以及加密的密钥,去解密密钥
常见攻击方式
- 嗅探:嗅探是一种用来捕获流进和流出的网络数据包的技术,就好像是监听电话一样。比如:抓包工具
- 数据包注入:在这种,攻击者会将恶意数据包注入到常规数据中的,因为这些恶意数据包是在正常的数据包里面的,用户和系统都很难发现这个内容。
- 会话劫持:当我们进行一个网站的登录的时候到退出登录这个时候,会产生一个会话,这个会话是攻击者用来攻击的首要目标,因为这个会话,包含了用户大量的数据和私密信息。
- SSL剥离:HTTPS是通过SSL/TLS进行加密过的,在SSL剥离攻击中,会使SSL/TLS连接断开,让受保护的HTTPS,变成不受
保护的HTTP(这对于网站非常致命) - DNS欺骗,攻击者往往通过入侵到DNS服务器,或者篡改用户本地hosts文件,然后去劫持用户发送的请求,然后转发到攻击者想要转发到的服务器
- ARP欺骗,ARP(address resolution protocol)地址解析协议,攻击者利用APR的漏洞,用当前局域网之间的一台服务器,来冒充客户端想要请求的服务端,向客户端发送自己的MAC地址,客户端无从得到真正的主机的MAC地址,所以,他会把这个地址当作真正
的主机来进行通信,将MAC存入ARP缓存表。 - 代理服务器
预防方案:
- i)用可信的第三方CA厂商
- ii)不下载未知来源的证书,不要去下载一些不安全的文件
- iii)确认你访问的URL是HTTPS的,确保网站使用了SSL,确保禁用一些不安全的SSL,只开启:TLS1.1,TLS1.2
- iv)不要使用公用网络发送一些敏感的信息
- v)不要去点击一些不安全的连接或者恶意链接或邮件信息
11.sql 注入
描述
就是通过把SQL命令插入到Web表单递交或输入域名或页面请求的查询字符串,最终达到欺骗数据库服务器执行恶意的SQL命令,从而达到和服务器
进行直接的交互
预防方案:
- i)后台进行输入验证,对敏感字符过滤。
- ii)使用参数化查询,能避免拼接SQL,就不要拼接SQL语句。
12.前端数据安全:
描述
反爬虫。如猫眼电影、天眼查等等,以数据内容为核心资产的企业
预防方案:
- i)font-face拼接方式:猫眼电影、天眼查
- ii)background 拼接:美团
- iii)伪元素隐藏:汽车之家
- iv)元素定位覆盖式:去哪儿
- v)iframe 异步加载:网易云音乐
13.其他建议
- i)定期请第三方机构做安全性测试,漏洞扫描
- ii)使用第三方开源库做上线前的安全测试,可以考虑融合到CI中
- iii)code review 保证代码质量
- iv)默认项目中设置对应的 Header 请求头,如 X-XSS-Protection、 X-Content-Type-Options 、X-Frame-Options Header、Content-Security-Policy 等等
- v)对第三方包和库做检测:NSP(Node Security Platform),Snyk
5.网络传输
5.1 跨域是什么?如何解决跨域?
参考答案:
1.什么是同源策略及其限制内容?
同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSRF等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。
同源策略限制内容有:
- Cookie、LocalStorage、IndexedDB 等存储性内容
- DOM 节点
- AJAX 请求发送后,结果被浏览器拦截了
但是有三个标签是允许跨域加载资源:
<img src='xxx'>
<link href='xxx'>
<script src='xxx'>
跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。
跨域解决方案
解决方案有jsonp、cors、postMessage、websocket、Node中间件代理(两次跨域)、nginx反向代理、window.name + iframe、location.hash + iframe、document.domain + iframe,CORS支持所有类型的HTTP请求,是跨域HTTP请求的根本解决方案,JSONP只支持GET请求,JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。不管是Node中间件代理还是nginx反向代理,主要是通过同源策略对服务器不加限制。日常工作中,用得比较多的跨域方案是cors和nginx反向代理
解析:
1.jsonp
1) JSONP原理
利用 <script>
标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。JSONP请求一定 需要对方的服务器做支持才可以。
2) JSONP优缺点
JSONP优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持get方法具有局限性, 不安全可能会遭受XSS攻击。
3) JSONP的实现流程
声明一个回调函数,其函数名(如show)当做参数值,要传递给跨域请求数据的服务器,函数形参为要获取目 标数据(服务器返回的data)。
创建一个``标签,把那个跨域的API数据接口地址,赋值给script的src,还要在这个地址中向服务器传递该函数 名(可以通过问号传参:?callback=show)。
服务器接收到请求后,需要进行特殊的处理:把传递进来的函数名和它需要给你的数据拼接成一个字符串,例 如:传递进去的函数名是show,它准备好的数据是show('我不爱你')
。
最后服务器把准备的数据通过HTTP协议返回给客户端,客户端再调用执行之前声明的回调函数(show),对 返回的数据进行操作。
cors
CORS 需要浏览器和后端同时支持。IE 8 和 9 需要通过 XDomainRequest 来实现。
浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了 CORS,就实现了跨域。
服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。
postMessage
postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:
- 页面和其打开的新窗口的数据传递
- 多窗口之间消息传递
- 页面与嵌套的iframe消息传递
- 上面三个场景的跨域数据传递
postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。
websocket
Websocket是HTML5的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。WebSocket和HTTP都是应用层协议,都基于 TCP 协议。但是 WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。同时,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了。
原生WebSocket API使用起来不太方便,我们使用
Socket.io
,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容Node中间件代理(两次跨域)
实现原理:同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略。 代理服务器,需要做以下几个步骤:
- 接受客户端请求 。
- 将请求 转发给服务器。
- 拿到服务器 响应 数据。
- 将 响应 转发给客户端。
nginx反向代理
实现原理类似于Node中间件代理,需要你搭建一个中转nginx服务器,用于转发请求。
使用nginx反向代理实现跨域,是最简单的跨域方式。只需要修改nginx的配置即可解决跨域问题,支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能。
实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。
window.name + iframe
window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。
总结:通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。
location.hash + iframe
实现原理: a.html欲与c.html跨域相互通信,通过中间页b.html来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。
具体实现步骤:一开始a.html给c.html传一个hash值,然后c.html收到hash值后,再把hash值传递给b.html,最后b.html将结果放到a.html的hash值中。 同样的,a.html和b.html是同域的,都是
http://localhost:3000
;而c.html是http://localhost:4000
document.domain + iframe
该方式只能用于二级域名相同的情况下,比如
a.test.com
和b.test.com
适用于该方式。 只需要给页面添加document.domain ='test.com'
表示二级域名都相同就可以实现跨域。实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。
5.2 jsonp原理
参考答案:
利用 <script>
标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。JSONP请求一定 需要对方的服务器做支持才可以。
解析:
JSONP和AJAX对比
JSONP和AJAX相同,都是客户端向服务器端发送请求,从服务器端获取数据的方式。但AJAX属于同源策略,JSONP属于非同源策略(跨域请求)
JSONP优缺点
JSONP优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持get方法具有局限性,不安全可能会遭受XSS攻击。
JSONP的实现流程
声明一个回调函数,其函数名(如show)当做参数值,要传递给跨域请求数据的服务器,函数形参为要获取目标数据(服务器返回的data)。
创建一个``标签,把那个跨域的API数据接口地址,赋值给script的src,还要在这个地址中向服务器传递该函数名(可以通过问号传参:?callback=show)。
服务器接收到请求后,需要进行特殊的处理:把传递进来的函数名和它需要给你的数据拼接成一个字符串,例如:传递进去的函数名是show,它准备好的数据是
show('我不爱你')
。最后服务器把准备的数据通过HTTP协议返回给客户端,客户端再调用执行之前声明的回调函数(show),对返回的数据进行操作。
在开发中可能会遇到多个 JSONP 请求的回调函数名是相同的,这时候就需要自己封装一个 JSONP函数。
// index.html function jsonp({ url, params, callback }) { return new Promise((resolve, reject) => { let script = document.createElement('script') window[callback] = function(data) { resolve(data) document.body.removeChild(script) } params = { ...params, callback } // wd=b&callback=show let arrs = [] for (let key in params) { arrs.push(`${key}=${params[key]}`) } script.src = `${url}?${arrs.join('&')}` document.body.appendChild(script) }) } jsonp({ url: 'http://localhost:3000/say', params: { wd: 'Iloveyou' }, callback: 'show' }).then(data => { console.log(data) }) 复制代码
上面这段代码相当于向http://localhost:3000/say?wd=Iloveyou&callback=show
这个地址请求数据,然后后台返回show('我不爱你')
,最后会运行show()这个函数,打印出'我不爱你'
// server.js let express = require('express') let app = express() app.get('/say', function(req, res) { let { wd, callback } = req.query console.log(wd) // Iloveyou console.log(callback) // show res.end(`${callback}('我不爱你')`) }) app.listen(3000) 复制代码
jQuery的jsonp形式
JSONP都是GET和异步请求的,不存在其他的请求方式和同步请求,且jQuery默认就会给JSONP的请求清除缓存。
$.ajax({ url:"http://crossdomain.com/jsonServerResponse", dataType:"jsonp", type:"get",//可以省略 jsonpCallback:"show",//->自定义传递给服务器的函数名,而不是使用jQuery自动生成的,可省略 jsonp:"callback",//->把传递函数名的那个形参callback,可省略 success:function (data){ console.log(data);} });
5.3 解决跨域问题,websocket的原理
参考答案:
Websocket是HTML5的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。WebSocket和HTTP都是应用层协议,都基于 TCP 协议。但是 WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。同时,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了。
原生WebSocket API使用起来不太方便,我们使用Socket.io
,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
我们先来看个例子:本地文件socket.html向localhost:3000
发生数据和接受数据
// socket.html <script> let socket = new WebSocket('ws://localhost:3000'); socket.onopen = function () { socket.send('我爱你');//向服务器发送数据 } socket.onmessage = function (e) { console.log(e.data);//接收服务器返回的数据 } </script>
// server.js let express = require('express'); let app = express(); let WebSocket = require('ws');//记得安装ws let wss = new WebSocket.Server({port:3000}); wss.on('connection',function(ws) { ws.on('message', function (data) { console.log(data); ws.send('我不爱你') }); })
5.4 有什么方法可以保持前后端实时通信
参考答案:
实现保持前后端实时通信的方式有以下几种
- WebSocket: IE10以上才支持,Chrome16, FireFox11,Safari7以及Opera12以上完全支持,移动端形势大
- event-source: IE完全不支持(注意是任何版本都不支持),Edge76,Chrome6,Firefox6,Safari5和Opera以上支持, 移动端形势大好
- AJAX轮询: 用于兼容低版本的浏览器
- 永久帧( forever iframe)可用于兼容低版本的浏览器
- flash socket 可用于兼容低版本的浏览器
这几种方式的优缺点
1.WebSocket
- 优点:WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议,可从HTTP升级而来,浏览器和服务器只需要一次握手,就可以进行持续的,双向的数据传输,因此能显著节约资源和带宽
- 缺点:1. 兼容性问题:不支持较低版本的IE浏览器(IE9及以下)2.不支持断线重连,需要手写心跳连接的逻辑 3.通信机制相对复杂
2. server-sent-event(event-source)
- 优点:(1)只需一次请求,便可以stream的方式多次传送数据,节约资源和带宽 (2)相对WebSocket来说简单易用 (3)内置断线重连功能(retry)
- 缺点: (1)是单向的,只支持服务端->客户端的数据传送,客户端到服务端的通信仍然依靠AJAX,没有”一家人整整齐齐“的感觉(2)兼容性令人担忧,IE浏览器完全不支持
3. AJAX轮询
- 优点:兼容性良好,对标低版本IE
- 缺点:请求中有大半是无用的请求,浪费资源
4.Flash Socket
- 缺点:(1)浏览器开启时flash需要用户确认,(2)加载时间长,用户体验较差 (3)大多数移动端浏览器不支持flash,为重灾区
- 优点: 兼容低版本浏览器
5. 永久帧( forever iframe)
- 缺点: iframe会产生进度条一直存在的问题,用户体验差
- 优点:兼容低版本IE浏览器
综上,综合兼容性和用户体验的问题,我在项目中选用了WebSocket ->server-sent-event -> AJAX轮询这三种方式做从上到下的兼容
5.5 常见http status
参考答案:
1XX系列:指定客户端应相应的某些动作,代表请求已被接受,需要继续处理。由于 HTTP/1.0 协议中没有定义任何 1xx 状态码,所以除非在某些试验条件下,服务器禁止向此类客户端发送 1xx 响应。
2XX系列:代表请求已成功被服务器接收、理解、并接受。这系列中最常见的有200、201状态码。
3XX系列:代表需要客户端采取进一步的操作才能完成请求,这些状态码用来重定向,后续的请求地址(重定向目标)在本次响应的 Location 域中指明。这系列中最常见的有301、302状态码。
4XX系列:表示请求错误。代表了客户端看起来可能发生了错误,妨碍了服务器的处理。常见有:401、404状态码。
5xx系列:代表了服务器在处理请求的过程中有错误或者异常状态发生,也有可能是服务器意识到以当前的软硬件资源无法完成对请求的处理。常见有500、503状态码。
2开头 (请求成功)表示成功处理了请求的状态代码。
- 200 (成功) 服务器已成功处理了请求。 通常,这表示服务器提供了请求的网页。
- 201 (已创建) 请求成功并且服务器创建了新的资源。
- 202 (已接受) 服务器已接受请求,但尚未处理。
- 203 (非授权信息) 服务器已成功处理了请求,但返回的信息可能来自另一来源。
- 204 (无内容) 服务器成功处理了请求,但没有返回任何内容。
- 205 (重置内容) 服务器成功处理了请求,但没有返回任何内容。
- 206 (部分内容) 服务器成功处理了部分 GET 请求。
3开头 (请求被重定向)表示要完成请求,需要进一步操作。 通常,这些状态代码用来重定向。
- 300 (多种选择) 针对请求,服务器可执行多种操作。 服务器可根据请求者 (user agent) 选择一项操作,或提供操作列表供请求者选择。
- 301 (永久移动) 请求的网页已永久移动到新位置。 服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置。
- 302 (临时移动) 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。
- 303 (查看其他位置) 请求者应当对不同的位置使用单独的 GET 请求来检索响应时,服务器返回此代码。
- 304 (未修改) 自从上次请求后,请求的网页未修改过。 服务器返回此响应时,不会返回网页内容。
- 305 (使用代理) 请求者只能使用代理访问请求的网页。 如果服务器返回此响应,还表示请求者应使用代理。
- 307 (临时重定向) 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。
4开头 (请求错误)这些状态代码表示请求可能出错,妨碍了服务器的处理。
- 400 (错误请求) 服务器不理解请求的语法。
- 401 (未授权) 请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应。
- 403 (禁止) 服务器拒绝请求。
- 404 (未找到) 服务器找不到请求的网页。
- 405 (方法禁用) 禁用请求中指定的方法。
- 406 (不接受) 无法使用请求的内容特性响应请求的网页。
- 407 (需要代理授权) 此状态代码与 401(未授权)类似,但指定请求者应当授权使用代理。
- 408 (请求超时) 服务器等候请求时发生超时。
- 409 (冲突) 服务器在完成请求时发生冲突。 服务器必须在响应中包含有关冲突的信息。
- 410 (已删除) 如果请求的资源已永久删除,服务器就会返回此响应。
- 411 (需要有效长度) 服务器不接受不含有效内容长度标头字段的请求。
- 412 (未满足前提条件) 服务器未满足请求者在请求中设置的其中一个前提条件。
- 413 (请求实体过大) 服务器无法处理请求,因为请求实体过大,超出服务器的处理能力。
- 414 (请求的 URI 过长) 请求的 URI(通常为网址)过长,服务器无法处理。
- 415 (不支持的媒体类型) 请求的格式不受请求页面的支持。
- 416 (请求范围不符合要求) 如果页面无法提供请求的范围,则服务器会返回此状态代码。
- 417 (未满足期望值) 服务器未满足"期望"请求标头字段的要求。
5开头(服务器错误)这些状态代码表示服务器在尝试处理请求时发生内部错误。 这些错误可能是服务器本身的错误,而不是请求出错。
- 500 (服务器内部错误) 服务器遇到错误,无法完成请求。
- 501 (尚未实施) 服务器不具备完成请求的功能。 例如,服务器无法识别请求方法时可能会返回此代码。
- 502 (错误网关) 服务器作为网关或代理,从上游服务器收到无效响应。
- 503 (服务不可用) 服务器目前无法使用(由于超载或停机维护)。 通常,这只是暂时状态。
- 504 (网关超时) 服务器作为网关或代理,但是没有及时从上游服务器收到请求。
- 505 (HTTP 版本不受支持) 服务器不支持请求中所用的 HTTP 协议版本。
5.6 http和https
参考答案:
1. HTTP和HTTPS的基本概念
HTTP:是互联网上应用最为广泛的一种网络协议,是一个客户端和服务器端请求和应答的标准(TCP),用于从WWW服务器传输超文本到本地浏览器的传输协议,它可以使浏览器更加高效,使网络传输减少。
HTTPS:是以安全为目标的HTTP通道,简单讲是HTTP的安全版,即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。
HTTPS协议的主要作用可以分为两种:一种是建立一个信息安全通道,来保证数据传输的安全;另一种就是确认网站的真实性。
2.HTTP与HTTPS有什么区别?
HTTP协议传输的数据都是未加密的,也就是明文的,因此使用HTTP协议传输隐私信息非常不安全,为了保证 这些隐私数据能加密传输,于是网景公司设计了SSL(Secure Sockets Layer)协议用于对HTTP协议传输的数据进行加密,从而就诞生了HTTPS。简单来说,HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全。
HTTPS和HTTP的区别主要如下:
1、https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。
2、http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
3、http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
4、http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
5.7 http1.x 和http2.x区别
参考答案:
http1.x 和http2.x主要有以下4个区别:
HTTP2使用的是二进制传送,HTTP1.X是文本(字符串)传送。
二进制传送的单位是帧和流。帧组成了流,同时流还有流ID标示
HTTP2支持多路复用
因为有流ID,所以通过同一个http请求实现多个http请求传输变成了可能,可以通过流ID来标示究竟是哪个流从而定位到是哪个http请求
HTTP2头部压缩
HTTP2通过gzip和compress压缩头部然后再发送,同时客户端和服务器端同时维护一张头信息表,所有字段都记录在这张表中,这样后面每次传输只需要传输表里面的索引Id就行,通过索引ID查询表头的值
HTTP2支持服务器推送
HTTP2支持在未经客户端许可的情况下,主动向客户端推送内容
5.8 http请求方式
参考答案:
http请求方式有以下8种,其中get和post是最常用的:
1、OPTIONS
返回服务器针对特定资源所支持的HTTP请求方法,也可以利用向web服务器发送‘*’的请求来测试服务器的功能性
2、HEAD
向服务器索与GET请求相一致的响应,只不过响应体将不会被返回。这一方法可以再不必传输整个响应内容的情况下,就可以获取包含在响应小消息头中的元信息。
3、GET
向特定的资源发出请求。注意:GET方法不应当被用于产生“副作用”的操作中,例如在Web Application中,其中一个原因是GET可能会被网络蜘蛛等随意访问。Loadrunner中对应get请求函数:web_link和web_url
4、POST
向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改。 Loadrunner中对应POST请求函数:web_submit_data,web_submit_form
5、PUT
向指定资源位置上传其最新内容
6、DELETE
请求服务器删除Request-URL所标识的资源
7、TRACE
回显服务器收到的请求,主要用于测试或诊断
8、CONNECT
HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。
5.9 HTTPS如何保证安全
参考答案:
HTTPS(全称:Hypertext Transfer Protocol over Secure Socket Layer),是以安全为目标的HTTP通道,简单讲是HTTP的安全版。HTTPS = HTTP + SSL/TLS,如今 SSL 已废弃,所以现在只关注 HTTP + TLS。为了解决 HTTP 协议的问题,HTTPS 引入了数据加密和身份验证机制。在开始传输数据之前,通过安全可靠的 TLS 协议进行加密,从而保证后续加密传输数据的安全性。
TLS 协议:传输层安全性协议(Transport Layer Security,TLS)及其前身安全套接层(Secure Sockets Layer,SSL)是一种安全协议,目的是为了保证网络通信安全和数据完整性。
受 TLS 协议保护的通信过程:先对传输的数据进行了加密(使用对称加密算法)。并且对称加密的密钥是为每一个连接唯一生成的(基于 TLS 握手阶段协商的加密算法和共享密钥),然后发送的每条消息都会通过消息验证码(Message authentication code, MAC),来进行消息完整性检查,最后还可以使用公钥对通信双方进行身份验证
Https的作用
- 内容加密 建立一个信息安全通道,来保证数据传输的安全;
- 身份认证 确认网站的真实性
- 数据完整性 防止内容被第三方冒充或者篡改
5.10 非对称加密和对称加密,具体怎么实现的
参考答案:
对称加密:在对称加密算法中,加密使用的密钥和解密使用的密钥是相同的。也就是说,加密和解密都是使用的同一个密钥。
非对称加密:指加密和解密使用不同密钥的加密算法。非对称加密算法需要两个密钥:公钥(publickey)私钥(privatekey)。
公钥与私钥是一对存在,如果用公钥对数据进行加密,只有用对应的私钥才能解密;如果用密钥对数据进行加密,那么只有用对应的公钥才能解密。因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。
解析:
1、对称加密算法的缺点
1、要求提供一条安全的渠道使通讯双方在首次通讯时协商一个共同的密钥。直接的面对面协商可能是不现实而且难于实施的,所以双方可能需要借助于邮件和电话等其它相对不够安全的手段来进行协商;
2、密钥的数目难于管理。因为对于每一个合作者都需要使用不同的密钥,很难适应开放社会中大量的信息交流;
3、对称加密算法一般不能提供信息完整性的鉴别。它无法验证发送者和接受者的身份;
4、对称密钥的管理和分发工作是一件具有潜在危险的和烦琐的过程。对称加密是基于共同保守秘密来实现的,采用对称加密技术的贸易双方必须保证采用的是相同的密钥,保证彼此密钥的交换是安全可靠的,同时还要设定防止密钥泄密和更改密钥的程序。
2、两种加密体制的特点
非对称密码体制
的特点:算法强度复杂、安全性依赖于算法与密钥但是由于其算法复杂,而使得加密解密速度没有对称加密解密的速度快。
对称密码体制
中只有一种密钥,并且是非公开的,如果要解密就得让对方知道密钥,所以保证其安全性就是保证密钥的安全。而非对称密钥体制有两种密钥,其中一个是公开的,这样就可以不需要像对称密码那样传输对方的密钥了。这样安全性就大了很多。
假设两个用户要加密交换数据,双方交换公钥,使用时一方用对方的公钥加密,另一方即可用自己的私钥解密。
如果企业中有n个用户,企业需要生成n对密钥,并分发n个公钥。由于公钥是可以公开的,用户只要保管好自己的私钥即可,因此加密密钥的分发将变得 十分简单。同时,由于每个用户的私钥是唯一的,其他用户除了可以通过"信息发送者的公钥"来验证信息的来源是否真实,还可以确保发送者无法否认曾发送过该信息。非对称加密的缺点是加解密速度要远远慢于对称加密,在某些极端情况下,甚至能比对称加密慢上1000倍。
非对称的好处显而易见,非对称加密体系不要求通信双方事先传递密钥或有任何约定就能完成保密通信,并且密钥管理方便,可实现防止假冒和抵赖,因此,更适合网络通信中的保密通信要求。
3、什么是数字证书
3.1 数字证书就是互联网通讯中标志通讯各方身份信息的一串数字,提供了一种在Internet上验证通信实体身份的方式,数字证书不是数字身份证,而是身份认证机构盖在数字身份证上的一个章或印(或者说加在数字身份证上的一个签名)。
3.2 它是由权威机构——CA机构,又称为证书授权(Certificate Authority)中心发行的,人们可以在网上用它来识别对方的身份。
3.3 数字证书绑定了公钥及其持有者的真实身份,它类似于现实生活中的居民身份证,所不同的是数字证书不再是纸质的证照,而是一段含有证书持有者身份信息并经过认证中心审核签发的电子数据,广泛用在电子商务和移动互联网中。
4、什么是数字签名
4.1 数字签名是将摘要信息用发送者的私钥加密,与原文一起传送给接收者。接收者只有用发送者的公钥才能解密被加密的摘要信息,然后用HASH函数对收到的原文产生一个摘要信息,与解密的摘要信息对比。
如果相同,则说明收到的信息是完整的,在传输过程中没有被修改;
否则说明信息被修改过,因此数字签名能够验证信息的完整性。
如果中途数据被纂改或者丢失。那么对方就可以根据数字签名来辨别是否是来自对方的第一手信息数据。
4.2 数字签名是个加密的过程,数字签名验证是个解密的过程。
4.3 数字签名用来,保证信息传输的完整性、发送者的身份认证、防止交易中的抵赖发生。
非对称加密算法实现机密信息交换的基本过程是:甲方生成一对密钥并将其中的一把作为公用密钥向其它方公开;得到该公用密钥的乙方使用该密钥对机密信息进行加密后再发送给甲方;甲方再用自己保存的另一把专用密钥对加密后的信息进行解密。
5、非对称加密和对称加密在HTTPS协议中的应用
5.1 浏览器向服务器发出请求,询问对方支持的对称加密算法和非对称加密算法;服务器回应自己支持的算法。
5.2 浏览器选择双方都支持的加密算法,并请求服务器出示自己的证书;服务器回应自己的证书。
5.3 浏览器随机产生一个用于本次会话的对称加密的钥匙,并使用服务器证书中附带的公钥对该钥匙进行加密后传递给服务器;服务器为本次会话保持该对称加密的钥匙。第三方不知道服务器的私钥,即使截获了数据也无法解密。非对称加密让任何浏览器都可以与服务器进行加密会话。
5.4 浏览器使用对称加密的钥匙对请求消息加密后传送给服务器,服务器使用该对称加密的钥匙进行解密;服务器使用对称加密的钥匙对响应消息加密后传送给浏览器,浏览器使用该对称加密的钥匙进行解密。第三方不知道对称加密的钥匙,即使截获了数据也无法解密。对称加密提高了加密速度 。
6、完整的非对称加密过程
假如现在 你向支付宝 转账(术语数据信息),为了保证信息传送的保密性、真实性、完整性和不可否认性,需要对传送的信息进行数字加密和签名,其传送过程为:
- 首先你要确认是否是支付宝的数字证书,如果确认为支付宝身份后,则对方真实可信。可以向对方传送信息
- 你准备好要传送的数字信息(明文)计算要转的多少钱,对方支付宝账号等;
- 你对数字信息进行哈希运算,得到一个信息摘要(客户端主要职责);
- 你用自己的私钥对信息摘要进行加密得到 你 的数字签名,并将其附在数字信息上;
- 你随机产生一个加密密钥,并用此密码对要发送的信息进行加密(密文);
- 你用支付宝的公钥对刚才随机产生的加密密钥进行加密,将加密后的 DES 密钥连同密文一起传送给支付宝
- 支付宝收到 你 传送来的密文和加密过的 DES 密钥,先用自己的私钥对加密的 DES 密钥进行解密,得到 你随机产生的加密密钥;
- 支付宝 然后用随机密钥对收到的密文进行解密,得到明文的数字信息,然后将随机密钥抛弃;
- 支付宝 用你 的公钥对 你的的数字签名进行解密,得到信息摘要;
- 支付宝用相同的哈希算法对收到的明文再进行一次哈希运算,得到一个新的信息摘要;
- 支付宝将收到的信息摘要和新产生的信息摘要进行比较,如果一致,说明收到的信息没有被修改过;
- 确定收到信息,然后进行向对方进行付款交易,一次非对称密过程结束。
5.11 除了post get 请求还有没有别的请求方式,例如option 方式,具体讲解一下
参考答案:
除了post和get还有6种请求方式分别是:OPTIONS、HEAD、PUT、DELETE、TRACE、CONNECT
HTTP 的 OPTIONS 方法
用于获取目的资源所支持的通信选项。客户端可以对特定的 URL 使用 OPTIONS 方法,也可以对整站(通过将 URL 设置为“*”)使用该方法
作用:
检测服务器所支持的请求方法
可以使用 OPTIONS 方法对服务器发起请求,以检测服务器支持哪些 HTTP 方法:
curl -X OPTIONS http://example.org -i
CORS 中的预检请求
在 CORS 中,可以使用 OPTIONS 方法发起一个预检请求,以检测实际请求是否可以被服务器所接受。预检请求报文中的
Access-Control-Request-Method
首部字段告知服务器实际请求所使用的 HTTP 方法;Access-Control-Request-Headers
首部字段告知服务器实际请求所携带的自定义首部字段。服务器基于从预检请求获得的信息来判断,是否接受接下来的实际请求。
5.12 ajax原理,为什么要用ajax?
参考答案:
为什么要用ajax:
Ajax是一种异步请求数据的web开发技术,对于改善用户的体验和页面性能很有帮助。简单地说,在不需要重新刷新页面的情况下,Ajax 通过异步请求加载后台数据,并在网页上呈现出来。常见运用场景有表单验证是否登入成功、百度搜索下拉框提示和快递单号查询等等。Ajax的目的是提高用户体验,较少网络数据的传输量。同时,由于AJAX请求获取的是数据而不是HTML文档,因此它也节省了网络带宽,让互联网用户的网络冲浪体验变得更加顺畅
ajax原理:
Ajax的工作原理相当于在用户和服务器之间加了—个中间层(AJAX引擎),使用户操作与服务器响应异步化。并不是所有的用户请求都提交给服务器,像—些数据验证和数据处理等都交给Ajax引擎自己来做, 只有确定需要从服务器读取新数据时再由Ajax引擎代为向服务器提交请求。Ajax其核心有JavaScript、XMLHTTPRequest、DOM对象组成,通过XmlHttpRequest对象来向服务器发异步请求,从服务器获得数据,然后用JavaScript来操作DOM而更新页面
5.13 什么是同源策略,为什么需要同源策略
参考答案:
同源策略:
同源策略(Same Origin Policy)是一种约定,它是浏览器最核心最基本的安全功能。所谓的同源是指域名、协议、端口相同。不同源的客户端脚本在没有明确授权的情况下是不允许读写其他网站的资源
同源策略的限制:
- Cookie、LocalStorage 和 IndexDB 无法读取。
- DOM 无法获得。
- AJAX 请求不能发送。
同源策略作用:
防止恶意网页可以获取其他网站的本地数据。
防止恶意网站iframe其他网站的时候,获取数据。
防止恶意网站在自已网站有访问其他网站的权利,以免通过cookie免登,拿到数据。
5.14 tcp三次握手,为什么需要三次
参考答案:
两个目的:
确保建立可靠连接
避免资源浪费
三次握手的目的是“为了防止已经失效的连接请求报文段突然又传到服务端,因而产生错误”,这种情况是:一端(client)A发出去的第一个连接请求报文并没有丢失,而是因为某些未知的原因在某个网络节点上发生滞留,导致延迟到连接释放以后的某个时间才到达另一端(server)B。本来这是一个早已失效的报文段,但是B收到此失效的报文之后,会误认为是A再次发出的一个新的连接请求,于是B端就向A又发出确认报文,表示同意建立连接。如果不采用“三次握手”,那么只要B端发出确认报文就会认为新的连接已经建立了,但是A端并没有发出建立连接的请求,因此不会去向B端发送数据,B端没有收到数据就会一直等待,这样B端就会白白浪费掉很多资源。如果采用“三次握手”的话就不会出现这种情况,B端收到一个过时失效的报文段之后,向A端发出确认,此时A并没有要求建立连接,所以就不会向B端发送确认,这个时候B端也能够知道连接没有建立
5.15 https加密解密流程
参考答案:
https加密解密流程分成以下8个步骤:
客户端发起
HTTPS
请求 这个没什么好说的,就是用户在浏览器里输入一个HTTPS
网址,然后连接到服务端的443端口。服务端的配置 采用
HTTPS
协议的服务器必须要有一套数字证书,可以自己制作,也可以向组织申请。区别就是自己颁发的证书需要客户端验证通过,才可以继续访问,而使用受信任的公司申请的证书则不会弹出提示页面。这套证书其实就是一对公钥和私钥。如果对公钥不太理解,可以想象成一把钥匙和一个锁头,只是世界上只有你一个人有这把钥匙,你可以把锁头给别人,别人可以用这个锁把重要的东西锁起来,然后发给你,因为只有你一个人有这把钥匙,所以只有你才能看到被这把锁锁起来的东西。传送证书 这个证书其实就是公钥,只是包含了很多信息,如证书的颁发机构,过期时间等等。
客户端解析证书 这部分工作是由客户端的SSL/TLS来完成的,首先会验证公钥是否有效,比如颁发机构,过期时间等等,如果发现异常,则会弹出一个警示框,提示证书存在的问题。如果证书没有问题,那么就生成一个随机值。然后用证书(也就是公钥)对这个随机值进行加密。就好像上面说的,把随机值用锁头锁起来,这样除非有钥匙,不然看不到被锁住的内容。
传送加密信息 这部分传送的是用证书加密后的随机值,目的是让服务端得到这个随机值,以后客户端和服务端的通信就可以通过这个随机值来进行加密解密了。
服务端解密信息 服务端用私钥解密后,得到了客户端传过来的随机值,然后把内容通过该随机值进行对称加密,将信息和私钥通过某种算法混合在一起,这样除非知道私钥,不然无法获取内容,而正好客户端和服务端都知道这个私钥,所以只要加密算法够彪悍,私钥够复杂,数据就够安全。
传输加密后的信息 这部分信息就是服务端用私钥加密后的信息,可以在客户端用随机值解密还原。
客户端解密信息 客户端用之前生产的私钥解密服务端传过来的信息,于是获取了解密后的内容。整个过程第三方即使监听到了数据,也束手无策。
5.16 TCP vs UDP
参考答案:
TCP(传输控制协议)和UDP(用户数据报协议)
- TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议,是专门为了在不可靠的网络中提供一个可靠的端对端字节流而设计的,面向字节流。
- UDP(用户数据报协议)是iso参考模型中一种无连接的传输层协议,提供简单不可靠的非连接传输层服务,面向报文
区别:
- TCP是面向连接的,可靠性高;UDP是基于非连接的,可靠性低
- 由于TCP是连接的通信,需要有三次握手、重新确认等连接过程,会有延时,实时性差,同时过程复杂,也使其易于攻击;UDP没有建立连接的过程,因而实时性较强,也稍安全
- 在传输相同大小的数据时,TCP首部开销20字节;UDP首部开销8字节,TCP报头比UDP复杂,故实际包含的用户数据较少。TCP在IP协议的基础上添加了序号机制、确认机制、超时重传机制等,保证了传输的可靠性,不会出现丢包或乱序,而UDP有丢包,故TCP开销大,UDP开销较小
- 每条TCP连接只能时点到点的;UDP支持一对一、一对多、多对一、多对多的交互通信
应用场景选择
- 对实时性要求高和高速传输的场合下使用UDP;在可靠性要求低,追求效率的情况下使用UDP;
- 需要传输大量数据且对可靠性要求高的情况下使用TCP
5.17 FTP DNS 基于什么协议
参考答案:
DNS (Domain Name Service 域名服务) 协议基于 UDP协议
FTP (File Transfer Protocol 文件传输协议) 基于 TCP协议
DNS和FTP都是应用层协议
5.18 URL 路径包含什么, URI 是什么
参考答案:
URL 路径包含什么
一个完整的url分为4部分:
- 协议 例 Http(超文本传输协议) 、Https、
- 域名 例www.baidu.com为网站名字。 baidu.com为一级域名,www是服务
- 端口 不填写的话默认走的是80端口号
- 路径 http://www.baidu.com/路径1/路径1.2。/表示根目录
- 查询参数 http://www.baidu.com/路径1/路径1.2?name="man"(可有可无)
URI 是什么
URI是一个用于标识互联网资源名称的字符串。 该种标识允许用户对网络中(一般指万维网)的资源通过特定的协议进行交互操作。URI的最常见的形式是统一资源定位符(URL),经常指定为非正式的网址。更罕见的用法是统一资源名称(URN),其目的是通过提供一种途径。用于在特定的命名空间资源的标识,以补充网址。
扩展:
URL和URN是URI的子集,URI属于URL更高层次的抽象,一种字符串文本标准。
5.19 代码题:url GET参数写代码获取
参考答案:
方法一:采用正则表达式获取地址栏参数 (代码简洁,重点正则)
function getQueryString(name) { let reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");// 正则语句 let r = window.location.search.substr(1).match(reg);// 获取url的参数部分,用正则匹配 if (r != null) { return decodeURIComponent(r[2]); // 解码得到的参数 }; return null; }
方法二:split拆分法 (代码较复杂,较易理解)
function GetRequest() { const url = location.search; //获取url中"?"符后的字串 let theRequest = new Object(); if (url.indexOf("?") != -1) { // 判断是否是正确的参数部分 let str = url.substr(1); // 截取参数部分 strs = str.split("&"); // 以‘&’为分割符获取参数数组 for(let i = 0; i < strs.length; i ++) { theRequest[strs[i].split("=")[0]]=unescape(strs[i].split("=")[1]); } } return theRequest; }
方法三:split拆分法(易于理解,代码中规)
function getQueryVariable(variable){ let query = window.location.search.substring(1); // 获取url的参数部分 let vars = query.split("&"); // 以‘&’为分割符获取参数数组 for (let i=0;i<vars.length;i++) { // 遍历数组获取参数 let pair = vars[i].split("="); if(pair[0] == variable){return pair[1];} } return(false); }
5.20 301和302的含义
参考答案:
301和302都是重定向的状态码,重定向(Redirect)是指通过各种方法将客户端的网络请求重新定义或指定一个新方向转到其他位置(重定向包括网页重定向、域名重定向)。
301 redirect: 301 代表永久性转移(Permanently Moved)
302 redirect: 302 代表暂时性转移(Temporarily Moved )
相同点:都表示重定向,就是说浏览器在拿到服务器返回的这个状态码后会自动跳转到一个新的URL地址,这个地址可以从响应的Location首部中获取(用户看到的效果就是他输入的地址A瞬间变成了另一个地址B)
不同点:
301表示旧地址A的资源已经被永久地移除了(这个资源不可访问了),搜索引擎在抓取新内容的同时也将旧的网址交换为重定向之后的网址;
302表示旧地址A的资源还在(仍然可以访问),这个重定向只是临时地从旧地址A跳转到地址B,搜索引擎会抓取新的内容而保存旧的网址。
302会出现“网址劫持”现象,从A网址302重定向到B网址,由于部分搜索引擎无法总是抓取到目标网址,或者B网址对用户展示不够友好,因此浏览器会仍旧显示A网址,但是所用的网页内容却是B网址上的内容。
应用场景
301:域名需要切换、协议从http变成https;
302:未登录时访问已登录页时跳转到登录页面、404后跳转首页
5.21 手写jsonp
参考答案:
实现步骤:
- 创建script元素,设置src属性,并插入文档中,同时触发AJAX请求。
- 返回Promise对象,then函数才行继续,回调函数中进行数据处理
- script元素删除清理
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <script> /** * 手写jsonp并返回Promise对象 * 参数url,data:json对象,callback函数 */ function jsonp(url, data = {}, callback = 'callback') { // 处理json对象,拼接url data.callback = callback let params = [] for (let key in data) { params.push(key + '=' + data[key]) } console.log(params.join('&')) // 创建script元素 let script = document.createElement('script') script.src = url + '?' + params.join('&') document.body.appendChild(script) // 返回promise return new Promise((resolve, reject) => { window[callback] = (data) => { try { resolve(data) } catch (e) { reject(e) } finally { // 移除script元素 script.parentNode.removeChild(script) console.log(script) } } }) } jsonp('http://photo.sina.cn/aj/index', { page: 1, cate: 'recommend' }, 'jsoncallback').then(data => { console.log(data) }) </script> </body> </html>
5.22 DNS是什么
参考答案:
DNS(Domain Name Server,域名服务器)是进行域名(domain name)和与之相对应的IP地址 (IP address)转换的服务器。DNS中保存了一张域名(domain name)和与之相对应的IP地址 (IP address)的表,以解析消息的域名。 域名是Internet上某一台计算机或计算机组的名称,用于在数据传输时标识计算机的电子方位(有时也指地理位置)。域名是由一串用点分隔的名字组成的,通常包含组织名,而且始终包括两到三个字母的后缀,以指明组织的类型或该域所在的国家或地区。
5.23 OSI的应用层
参考答案:
应用层(Application layer)是最接近用户的,应用层是给各种应用程序提供对应的网络服务的,而网络服务是由各种应用层的协议支持的。网络服务和对应的协议举几个例子:文件传输是由FTP协议支持的,浏览器打开网页是由HTTP或者HTTPS协议支持的,电子邮件是由SMTP协议支持的,远程终端登录是由Telnet协议(远程登录协议)支持的。
5.24 域名解析原理
参考答案:
DNS是应用层协议,事实上他是为其他应用层协议工作的,包括不限于HTTP和SMTP以及FTP,用于将用户提供的主机名解析为ip地址。
具体过程如下:
- 客户机提出域名解析请求 , 并将该请求发送给本地的域名服务器 ;
- 当本地的域名服务器收到请求后 , 就先查询本地的缓存 , 如果有该纪录项 , 则本地的域名服务器就直接把查询的结果返回 ;
- 如果本地的缓存中没有该纪录 , 则本地域名服务器就直接把请求发给根域名服务器 , 然后根域名服务器再返回给本地域名服务器一个所查询域 (根的子域) 的主域名服务器的地址 ;
- 本地服务器再向上一步返回的域名服务器发送请求 , 然后接受请求的服务器查询自己的缓存 , 如果没有该纪录 , 则返回相关的下级的域名服务器的地址 ;
- 重复第四步 , 直到找到正确的纪录 ;
- 本地域名服务器把返回的结果保存到缓存 , 以备下一次使用 , 同时还将结果返回给客户机 ;
5.25 http缓存有几种?
参考答案:
http缓存的分类:
根据是否需要重新向服务器发起请求来分类,可分为(强制缓存,协商缓存) 根据是否可以被单个或者多个用户使用来分类,可分为(私有缓存,共享缓存) 强制缓存如果生效,不需要再和服务器发生交互,而协商缓存不管是否生效,都需要与服务端发生交互。下面是强制缓存和协商缓存的一些对比:
1.1、强制缓存
强制缓存在缓存数据未失效的情况下(即Cache-Control的max-age没有过期或者Expires的缓存时间没有过期),那么就会直接使用浏览器的缓存数据,不会再向服务器发送任何请求。强制缓存生效时,http状态码为200。这种方式页面的加载速度是最快的,性能也是很好的,但是在这期间,如果服务器端的资源修改了,页面上是拿不到的,因为它不会再向服务器发请求了。这种情况就是我们在开发种经常遇到的,比如你修改了页面上的某个样式,在页面上刷新了但没有生效,因为走的是强缓存,所以Ctrl + F5一顿操作之后就好了。 跟强制缓存相关的header头属性有(Pragma/Cache-Control/Expires), Pragma和Cache-control共存时,Pragma的优先级是比Cache-Control高的。
1.2、协商缓存
当第一次请求时服务器返回的响应头中没有Cache-Control和Expires或者Cache-Control和Expires过期还或者它的属性设置为no-cache时(即不走强缓存),那么浏览器第二次请求时就会与服务器进行协商,与服务器端对比判断资源是否进行了修改更新。如果服务器端的资源没有修改,那么就会返回304状态码,告诉浏览器可以使用缓存中的数据,这样就减少了服务器的数据传输压力。如果数据有更新就会返回200状态码,服务器就会返回更新后的资源并且将缓存信息一起返回。跟协商缓存相关的header头属性有(ETag/If-Not-Match 、Last-Modified/If-Modified-Since)请求头和响应头需要成对出现
1.3、私有缓存(浏览器级缓存)
私有缓存只能用于单独的用户:Cache-Control: Private
1.4、共享缓存(代理级缓存)
共享缓存可以被多个用户使用: Cache-Control: Public
5.26 协商缓存原理,谁跟谁协商,如何协商?
参考答案:
协商缓存: 向服务器发送请求,服务器会根据这个请求的request header的一些参数来判断是否命中协商缓存,如果命中,则返回304状态码并带上新的response header通知浏览器从缓存中读取资源;
服务器和请求协商,根据请求头携带的参数进行协商
5.27 GET和POST区别
参考答案:
- get用来获取数据,post用来提交数据
- get参数有长度限制(受限于url长度,具体的数值取决于浏览器和服务器的限制,最长2048字节),而post无限制
- get请求的数据会附加在url之 ,以 " ? "分割url和传输数据,多个参数用 "&"连接,而post请求会把请求的数据放在http请求体中。
- get是明文传输,post是放在请求体中,但是开发者可以通过抓包工具看到,也相当于是明文的。
- get请求会保存在浏览器历史记录中,还可能保存在web服务器的日志中
5.28 实现ajax的封装
参考答案:
/* * ajax * type === GET: data格式 name=baukh&age=29 * type === POST: data格式 { name: 'baukh', age:29 } * 与 jquery 不同的是,[success, error, complete]返回的第二个参数, 并不是返回错误信息, 而是错误码 * */ var extend = require('./extend'); var utilities = require('./utilities'); function ajax(options) { var defaults = { url: null,// 请求地址 type: 'GET',// 请求类型 data: null,// 传递数据 headers: {},// 请求头信息 async: true,// 是否异步执行 beforeSend: utilities.noop,// 请求发送前执行事件 complete: utilities.noop,// 请求发送后执行事件 success: utilities.noop,// 请求成功后执行事件 error: utilities.noop// 请求失败后执行事件 }; options = extend(defaults, options); if (!options.url) { utilities.error('jTool ajax: url不能为空'); return; } var xhr = new XMLHttpRequest(); var formData = ''; if (utilities.type(options.data) === 'object') { utilities.each(options.data, function (key, value) { if(formData !== '') { formData += '&'; } formData += key + '=' + value; }); }else { formData = options.data; } if(options.type === 'GET' && formData) { options.url = options.url + (options.url.indexOf('?') === -1 ? '?' : '&') + formData; formData = null; } xhr.open(options.type, options.url, options.async); for (var key in options.headers) { xhr.setRequestHeader(key, options.headers[key]); } // xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded"); // 执行发送前事件 options.beforeSend(xhr); // 监听onload并执行完成事件 xhr.onload = function() { // jquery complete(XHR, TS) options.complete(xhr, xhr.status); }; // 监听onreadystatechange并执行成功失败事件 xhr.onreadystatechange = function() { if (xhr.readyState !== 4) { return; } if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) { // jquery success(XHR, TS) options.success(xhr.response, xhr.status); } else { // jquery error(XHR, TS, statusText) options.error(xhr, xhr.status, xhr.statusText); } }; xhr.send(formData); } function post(url, data, callback) { ajax({ url: url, type: 'POST', data: data, success: callback }); } function get(url, data, callback) { ajax({ url: url, type: 'GET', data: data, success: callback }); } module.exports = { ajax: ajax, post: post, get: get };
5.29 OSI七层协议
参考答案:
OSI(Open System Interconnect),即开放式系统互联。 一般都叫OSI参考模型,是ISO(国际标准化组织)组织在1985年研究的网络互连模型。ISO为了更好的使网络应用更为普及,推出了OSI参考模型。其含义就是推荐所有公司使用这个规范来控制网络。这样所有公司都有相同的规范,就能互联了。
OSI定义了网络互连的七层框架(物理层、数据链路层、网络层、传输层、会话层、表示层、应用层),即ISO开放互连系统参考模型。
1.应用层
作用:它是与其他计算机进行通信的应用,它是对应应用程序的通信服务的。各种应用软件,包括web应用。
协议:DNS、FTP、HTTP、SMTP、TELNET、IRC、WHOIS
2.表示层
作用:这一层的主要作用是定义数据格式和加密。
3.会话层
作用:控制应用程序的会话能力,它定义了一段会话的开始、控制和结束,包括对多个双向消息的控制和管理,以便在只完成一部分消息时可以通知应用。
PS:其实在应用层、表示层、会话层这三层中,协议是可以共用的
4.传输层
作用:对差错恢复协议和无差错恢复协议的选择,对同一主机上不同数据流的输入进行复用,对数据包进行重新排序。是最关键的一层,是唯一负责整体的数据传输和数据控制的。对上三层提供可靠的传输服务,对网络层提供可靠的目的地信息。在这一层数据的单位被称为数据段。
协议:TCP、UDP等
5.网络层
作用:主要负责寻找地址和路由选择,网络层还可以实现阻塞控制、网际互联等。
协议:IP、IPX、RIP、OSPF等
6.数据链路层
作用:负责物理层面上的互联的、节点间的通信传输;该层的作用包括:物理地址寻址、数据的成帧、流量控制、数据的检错、重发等。在这一层,数据的单位称为帧(frame)
协议:ARP、RARP、SDLC、HDLC、PPP、STP、帧中继等
7.物理层
作用:负责0、1 比特流(0/1序列)与电压的高低、逛的闪灭之间的转换 规定了激活、维持、关闭通信端点之间的机械特性、电气特性、功能特性以及过程特性;该层为上层协议提供了一个传输数据的物理媒体。在这一层,数据的单位称为比特(bit)。
典型规范:EIA/TIA RS-232、EIA/TIA RS-449、V.35、RJ-45、fddi令牌环网等
5.30 怎么用UDP实现可靠传输,两条连接
参考答案:
最简单的方式是在应用层模仿传输层TCP的可靠性传输。下面不考虑拥塞处理,可靠UDP的简单设计。
- 1、添加seq/ack机制,确保数据发送到对端
- 2、添加发送和接收缓冲区,主要是用户超时重传。
- 3、添加超时重传机制。
详细说明:送端发送数据时,生成一个随机seq=x,然后每一片按照数据大小分配seq。数据到达接收端后接收端放入缓存,并发送一个ack=x的包,表示对方已经收到了数据。发送端收到了ack包后,删除缓冲区对应的数据。时间到后,定时任务检查是否需要重传数据。
目前有如下开源程序利用udp实现了可靠的数据传输。分别为RUDP、RTP、UDT\。
1、RUDP(Reliable User Datagram Protocol)
RUDP 提供一组数据服务质量增强机制,如拥塞控制的改进、重发机制及淡化服务器算法等\,从而在包丢失和网络拥塞的情况下, RTP 客户机(实时位置)面前呈现的就是一个高质量的 RTP 流。在不干扰协议的实时特性的同时,可靠 UDP 的拥塞控制机制允许 TCP 方式下的流控制行为。
2、RTP(Real Time Protocol)
RTP为数据提供了具有实时特征的端对端传送服务\,如在组播或单播网络服务下的交互式视频音频或模拟数据。
应用程序通常在 UDP 上运行 RTP 以便使用其多路结点和校验服务;这两种协议都提供了传输层协议的功能。但是 RTP 可以与其它适合的底层网络或传输协议一起使用。如果底层网络提供组播方式,那么 RTP 可以使用该组播表传输数据到多个目的地。
RTP 本身并没有提供按时发送机制或其它服务质量(QoS)保证,它依赖于底层服务去实现这一过程。 RTP 并不保证传送或防止无序传送,也不确定底层网络的可靠性。 RTP 实行有序传送, RTP 中的序列号允许接收方重组发送方的包序列,同时序列号也能用于决定适当的包位置,例如:在视频解码中,就不需要顺序解码。
3、UDT(UDP-based Data Transfer Protocol)
基于UDP的数据传输协议(UDP-basedData Transfer Protocol,简称UDT)是一种互联网数据传输协议。UDT的主要目的是支持高速广域网上的海量数据传输\,而互联网上的标准数据传输协议TCP在高带宽长距离网络上性能很差。
顾名思义,UDT建于UDP之上,并引入新的拥塞控制和数据可靠性控制机制。UDT是面向连接的双向的应用层协议。它同时支持可靠的数据流传输和部分可靠的数据报传输。由于UDT完全在UDP上实现,它也可以应用在除了高速数据传输之外的其它应用领域,例如点到点技术(P2P),防火墙穿透,多媒体数据传输等等。
5.31 数据量很大的时候UDP怎么可靠传输
参考答案:
基于UDP的数据传输协议(UDP-basedData Transfer Protocol,简称UDT)是一种互联网数据传输协议。UDT的主要目的是支持高速广域网上的海量数据传输\,而互联网上的标准数据传输协议TCP在高带宽长距离网络上性能很差。
顾名思义,UDT建于UDP之上,并引入新的拥塞控制和数据可靠性控制机制。UDT是面向连接的双向的应用层协议。它同时支持可靠的数据流传输和部分可靠的数据报传输。由于UDT完全在UDP上实现,它也可以应用在除了高速数据传输之外的其它应用领域,例如点到点技术(P2P),防火墙穿透,多媒体数据传输等等。
5.32 TCP断点重传怎么实现的
参考答案:
断点续传的关键是断点,所以在制定传输协议的时候要设计好,如下图,我自定义了一个交互协议,每次下载请求都会带上下载的起始点,这样就可以支持从断点下载了,其实HTTP里的断点续传也是这个原理,在HTTP的头里有个可选的字段RANGE,表示下载的范围
5.33 http多个tcp连接怎么实现的?
参考答案:
某些服务器对 Connection: keep-alive 的 Header 进行了支持。意思是说,完成这个 HTTP 请求之后,不要断开 HTTP 请求使用的 TCP 连接。这样的好处是连接可以被重新使用,之后发送 HTTP 请求的时候不需要重新建立 TCP 连接,以及如果维持连接,那么 SSL 的开销也可以避免
5.34 keep-alive是什么?
参考答案:
什么是KeepAlive
- KeepAlive可以简单理解为一种状态保持或重用机制,比如当一条连接建立后,我们不想它立刻被关闭,如果实现了KeepAlive机制,就可以通过它来实现连接的保持
- HTTP的KeepAlive在HTTP 1.0版本默认是关闭的,但在HTTP1.1是默认开启的;操作系统里TCP的KeepAlive默认也是关闭,但一般应用都会修改设置来开启。因此网上TCP流量中基于KeepAlive的是主流
- HTTP的KeepAlive和TCP的KeepAlive有一定的依赖关系,名称又一样,因此经常被混淆,但其实是不同的东西,下面具体分析一下
TCP为什么要做KeepAlive
我们都知道TCP的三次握手和四次挥手。当两端通过三次握手建立TCP连接后,就可以传输数据了,数据传输完毕,连接并不会自动关闭,而是一直保持。只有两端分别通过发送各自的
FIN
报文时,才会关闭自己侧的连接。这个关闭机制看起来简单明了,但实际网络环境千变万化,衍生出了各种问题。假设因为实现缺陷、突然崩溃、恶意攻击或网络丢包等原因,一方一直没有发送
FIN
报文,则连接会一直保持并消耗着资源,为了防止这种情况,一般接收方都会主动中断一段时间没有数据传输的TCP连接,比如LVS会默认中断90秒内没有数据传输的TCP连接,F5会中断5分钟内没有数据传输的TCP连接但有的时候我们的确不希望中断空闲的TCP连接,因为建立一次TCP连接需要经过一到两次的网络交互,且由于TCP的
slow start
机制,新的TCP连接开始数据传输速度是比较慢的,我们希望通过连接池模式,保持一部分空闲连接,当需要传输数据时,可以从连接池中直接拿一个空闲的TCP连接来全速使用,这样对性能有很大提升为了支持这种情况,TCP实现了KeepAlive机制。KeepAlive机制并不是TCP规范的一部分,但无论Linux和Windows都实现实现了该机制。TCP实现里KeepAlive默认都是关闭的,且是每个连接单独设置的,而不是全局设置
另外有一个特殊情况就是,当某应用进程关闭后,如果还有该进程相关的TCP连接,一般来说操作系统会自动关闭这些连接
5.35 tcp/ip协议栈、网络模型
参考答案:
TCP/IP 协议栈是一系列网络协议的总和,是构成网络通信的核心骨架,它定义了电子设备如何连入因特网,以及数据如何在它们之间进行传输。TCP/IP 协议采用4层结构,分别是应用层、传输层、网络层和链路层,
- 链路层:对0和1进行分组,定义数据帧,确认主机的物理地址,传输数据;
- 网络层:定义IP地址,确认主机所在的网络位置,并通过IP进行MAC寻址,对外网数据包进行路由转发;
- 传输层:定义端口,确认主机上应用程序的身份,并将数据包交给对应的应用程序;
- 应用层:定义数据格式,并按照对应的格式解读数据。
5.36 504 如何排查
参考答案:
排查步骤:
- 检查500/502/504错误截图,判断是负载均衡问题,高防/安全网络配置问题,还是后端ECS配置问题。
- 如果有高防/安全网络,请确认高防/安全网络的七层转发配置正确。
- 请确认是所有客户端都有问题,还仅仅是部分客户端有问题。如果仅仅是部分客户端问题,排查该客户端是否被云盾阻挡,或者负载均衡域名或者IP是否被ISP运营商拦截。
- 检查负载均衡状态,是否有后端ECS健康检查失败的情况,如果有健康检查失败,解决健康检查失败问题。
- 在客户端用hosts文件将负载均衡的服务地址绑定到后端服务器的IP地址上,确认是否是后端问题。如果5XX错误间断发生,很可能是后端某一台ECS服务器的配置问题。
- 尝试将七层负载均衡切换为四层负载均衡,查看问题是否会复现。
- 检查后端ECS服务器是否存在CPU、内存、磁盘或网络等性能瓶颈。
- 如果确认是后端服务器问题,请检查后端ECS Web服务器日志是否有相关错误,Web服务是否正常运行,确认Web访问逻辑是否有问题,卸载服务器上杀毒软件重启测试。
- 检查后端ECS Linux操作系统的TCP内核参数是否配置正确。
5.37 tcp 是如何确保有效传输的,拥塞控制
参考答案:
通过以下7种方式确保有效传输
- 校验和
- 序列号
- 确认应答
- 超时重传
- 连接管理
- 流量控制
- 拥塞控制
TCP 拥塞控制
TCP不仅可以可以控制端到端的数据传输,还可以对网络上的传输进行监控。这使得TCP非常强大智能,它会根据网络情况来调整自己的收发速度。网络顺畅时就可以发的快,拥塞时就发的相对慢一些。拥塞控制算法主要有四种:慢启动,拥塞避免,快速重传,快速恢复。
慢启动和拥塞避免
慢启动和拥塞避免算法必须被TCP发送端用来控制正在向网络输送的数据量。为了
实现这些算法,必须向TCP每连接状态加入两个参量。拥塞窗口(cwnd)是对发送端收到确
认(ACK)之前能向网络传送的最大数据量的一个发送端限制,接收端通知窗口(rwnd)是对
未完成数据量的接收端限制。cwnd和rwnd的最小值决定了数据传送。
另一个状态参量,慢启动阀值(ssthresh),被用来确定是用慢启动还是用拥塞避免
算法来控制数据传送。
在不清楚环境的情况下向网络传送数据,要求TCP缓慢地探测网络以确定可用流量,避免突然传送大量数据而使网络拥塞。在开始慢启动时cwnd为1,每收到一个用于确认新数据的ACK至多增加SMSS(SENDER MAXIMUM SEGMENT SIZE)字节。
慢启动算法在cwndssthresh时使用。当cwnd和ssthresh相等时,发送端既可以使用慢启动也可以使用拥塞避免。
当拥塞发生时,ssthresh被设置为当前窗口大小的一半(cwnd和接收方通告窗口大小的最小值,但最少为2个报文段)。如果是超时重传,cwnd被设置为1个报文段(这就是慢启动,其实慢启动也不慢,它是指数性增长,只是它的起始比较低)当达到ssthresh时,进入拥塞避免算法(拥塞避免是线性增长)。快速重传和快速恢复
当接收端收到一个顺序混乱的数据,它应该立刻回复一个重复的ACK。这个ACK的目的是通知发送端收到了一个顺序紊乱的数据段,以及期望的序列号。发送端收到这个重复的ACK可能有多种原因,可能丢失或者是网络对数据重新排序等。在收到三个重复ACK之后(包含第一次收到的一共四个同样的ACK),TCP不等重传定时器超时就重传看起来已经丢失(可能数据绕路并没有丢失)的数据段。因为这个在网络上并没有超时重传那么恶劣,所以不会进入慢启动,而进入快速恢复。快速恢复首先会把ssthresh减半(一般还会四舍五入到数据段的倍数),然后cwnd=ssthresh+收到重复ACK报文段累计的大小。
5.38 CDN
参考答案:
CDN的全称是Content Delivery Network,即内容分发网络。其目的是通过在现有的internet中增加一层新的网络架构,将网站的内容发布到最接近用户的网络边缘,使用户可以就近取得所需的内容,提高用户访问网站的响应速度。CDN有别于镜像,因为它比镜像更智能,或者可以做这样一个比喻:CDN=更智能的镜像+缓存+流量导流。因而,CDN可以明显提高Internet网络中信息流动的效率。从技术上全面解决由于网络带宽小、用户访问量大、网点分布不均等问题,提高用户访问网站的响应速度。
5.39 xhr 的 readyState
参考答案:
readyState是XMLHttpRequest对象的一个属性,用来标识当前XMLHttpRequest对象处于什么状态。
readyState总共有5个状态值,分别为0~4,每个值代表了不同的含义
0:初始化,XMLHttpRequest对象还没有完成初始化
1:载入,XMLHttpRequest对象开始发送请求
2:载入完成,XMLHttpRequest对象的请求发送完成
3:解析,XMLHttpRequest对象开始读取服务器的响应
4:完成,XMLHttpRequest对象读取服务器响应结束
5.40 CORS 的 Expose-Headers
参考答案:
Access-Control-Expose-Headers
作用:允许浏览器端能够获取相应的header值
如果服务端接口设置了响应头字段res.setHeader('serve-header','from->express');
但是CORS中对应的字段Access-Control-Expose-Headers并没有处理,此时通过请求响应后的header结果如下:
可以看到虽然响应头里面有serve-header
字段,但是却获取不到, 如果设置了 Access-Control-Allow-Headers: serve-header
再来看下结果
此时则可以拿到服务端设置的响应头里面的serve-header
字段了
5.41 axios的拦截器原理及应用
参考答案:
应用场景
请求拦截器用于在接口请求之前做的处理,比如为每个请求带上相应的参数(token,时间戳等)。
返回拦截器用于在接口返回之后做的处理,比如对返回的状态进行判断(token是否过期)。
拦截器的使用
- 在src目录下建立api文件夹
- 文件夹内建立axios.js文件,进行接口请求的初始化配置
import axios from 'axios' let instance = axios.create({ baseURL: "http://localhost:3000/", headers: { 'content-type': 'application/x-www-form-urlencoded' } }) //请求拦截器 instance.interceptors.request.use(config => { //拦截请求,做统一处理 const token = "asdasdk" //在每个http header都加上token config.headers.authorization = token return config }, err => {//失败 return Promise.reject(err) }) //响应拦截器 instance.interceptors.response.use(response => { //拦截响应,做统一处理 if (response.data.code) { switch (response.data.code) { case 200: console.log("1111") } } return response }, err => { //无响应时的处理 return Promise.reject(err.response.status) }) export default instance
- 在main.js中引入,并将其绑定到Vue原型上,设为全局,不用在每个页面重新引入
import instance from './api/axios' Vue.prototype.$http = instance
- 页面使用
this.$http.get(url).then(r => console.log(r)).catch(err => console.log(err)) this.$http.post(url, params).then(r => console.log(r)).catch(err => console.log(err))
- 效果展示
axios拦截器实现原理剖析
axios接口请求内部流程
axios原理简化
function Axios(){ this.interceptors = { //两个拦截器 request: new interceptorsManner(), response: new interceptorsManner() } } //真正的请求 Axios.prototype.request = function(){ let chain = [dispatchRequest, undefined];//这儿的undefined是为了补位,因为拦截器的返回有两个 let promise = Promise.resolve(); //将两个拦截器中的回调加入到chain数组中 this.interceptors.request.handler.forEach((interceptor)=>{ chain.unshift(interceptor.fulfilled, interceptor.rejected); }) this.interceptors.response.handler.forEach((interceptor)=>{ chain.push(interceptor.fulfilled, interceptor.rejected); }) while(chain.length){ //promise.then的链式调用,下一个then中的chain为上一个中的返回值,每次会减去两个 //这样就实现了在请求的时候,先去调用请求拦截器的内容,再去请求接口,返回之后再去执行响应拦截器的内容 promise = promise.then(chain.shift(),chain.shift()); } } function interceptorsManner(){ this.handler = []; } interceptorsManner.prototype.use = function(fulfilled,rejected){ //将成功与失败的回调push到handler中 this.handler.push({ fulfilled: fulfilled, rejected: rejected }) } //类似方法批量注册,实现多种请求 util.forEach(["get","post","delete"],(methods)=>{ Axios.prototype[methods] = function(url,config){ return this.request(util.merge(config||{},{//合并 method: methods, url: url })) } })
5.42 介绍下 HTTPS 中间人攻击
参考答案:
https 协议由 http + ssl 协议构成。
中间人攻击过程如下:
- 服务器向客户端发送公钥;
- 攻击者截获公钥,保留在自己手上;
- 然后攻击者自己生成一个【伪造的】公钥,发给客户端;
- 客户端收到伪造的公钥后,生成加密 hash(秘钥) 值发给服务器;
- 攻击者获得加密 hash 值,用自己的私钥解密获得真秘钥;
- 同时生成假的加密 hash 值,发给服务器;
- 服务器用私钥解密获得假秘钥;
- 服务器用假秘钥加密传输信息;
防范方法:
服务器在发送浏览器的公钥中加入 CA 证书,浏览器可以验证 CA 证书的有效性;(现有 HTTPS 很难被劫持,除非信任了劫持者的 CA 证书)。
5.43 SSL 连接断开后如何恢复?
参考答案:
Session ID
每一次的会话都有一个编号,当对话中断后,下一次重新连接时,只要客户端给出这个编号,服务器如果有这个编号的记录,那么双方就可以继续使用以前的密钥,而不用重新生成一把。
Session Ticket
session ticket 是服务器在上一次对话中发送给客户的,这个 ticket 是加密的,只有服务器可能够解密,里面包含了本次会话的信息,比如对话密钥和加密方法等。这样不管我们的请求是否转移到其他的服务器上,当服务器将 ticket 解密以后,就能够获取上次对话的信息,就不用重新生成对话秘钥了。
5.44 hosts 文件是什么?
参考答案:
hosts 文件是个没有扩展名的系统文件,其作用就是将网址域名和其对应的 IP 地址建立一个关联“数据库”,当用户在浏览器中输入一个 url 时,系统会首先自动从 hosts 文件中寻找对应的 IP 地址。
5.45 同域请求的并发数限制的原因
参考答案:
浏览器的并发请求数目限制是针对同一域名的,同一时间针对同一域名下的请求有一定数量限制,超过限制数目的请求会被阻塞(chorme和firefox的限制请求数都是6个)。
限制其数量的原因是:基于浏览器端口的限制和线程切换开销的考虑,浏览器为了保护自己不可能无限量的并发请求,如果一次性将所有请求发送到服务器,也会造成服务器的负载上升。
5.46 cdn加速原理
参考答案:
当用户点击网站页面上的url时,经过本地dns系统解析,dns系统会将域名的解析权给交cname指向的cdn专用dns服务器。
cdn的dns服务器将cdn的全局负载均衡设备ip地址返回给用户。
用户向cdn的全局负载均衡设备发起内容url访问请求。
cdn全局负载均衡设备根据用户ip,以及用户请求的内容url,选择一台用户所属区域的区域负载均衡设备
区域负载均衡设备会为用户选择一台合适的缓存服务器提供服务,选择的依据包括:根据用户IP地址,判断哪一台服务器距用户最近;根据用户所请求的URL中携带的内容名称,判断哪一台服务器上有用户所需内容;查询各个服务器当前的负载情况,判断哪一台服务器尚有服务能力。基于以上这些条件的综合分析之后,区域负载均衡设备会向全局负载均衡设备返回一台缓存服务器的IP地址全局负载均衡设备把服务器的IP地址返回给用户。
用户向缓存服务器发起请求,缓存服务器响应用户请求,将用户所需内容传送到用户终端。如果这台缓存服务器上没有用户想要的内容,而区域均衡设备依然将它分配给了用户,那么这台服务器 就要向它的上一级缓存服务器发起请求内容,直至追溯到网站的源服务器将内容拉回给用户。
5.47 创建ajax过程
参考答案:
- 创建XMLHttpRequest对象,也就是创建一个异步调用对象.
- 创建一个新的HTTP请求,并指定该HTTP请求的方法、URL及验证信息.
- 设置响应HTTP请求状态变化的函数.
- 发送HTTP请求.
- 获取异步调用返回的数据.
- 使用JavaScript和DOM实现局部刷新.
5.48 常用的http请求头以及响应头详
参考答案:
一、常用的http请求头
1.Accept
- Accept: text/html 浏览器可以接受服务器回发的类型为 text/html。
- Accept: */* 代表浏览器可以处理所有类型,(一般浏览器发给服务器都是发这个)。
2.Accept-Encoding
- Accept-Encoding: gzip, deflate 浏览器申明自己接收的编码方法,通常指定压缩方法,是否支持压缩,支持什么压缩方法(gzip,deflate),(注意:这不是只字符编码)。
3.Accept-Language
- Accept-Language:zh-CN,zh;q=0.9 浏览器申明自己接收的语言。
4.Connection
- Connection: keep-alive 当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接。
- Connection: close 代表一个Request完成后,客户端和服务器之间用于传输HTTP数据的TCP连接会关闭, 当客户端再次发送Request,需要重新建立TCP连接。
5.Host(发送请求时,该报头域是必需的)
- Host: 请求报头域主要用于指定被请求资源的Internet主机和端口号,它通常从HTTP URL中提取出来的。
6.Referer
- Referer: 当浏览器向web服务器发送请求的时候,一般会带上Referer,告诉服务器我是从哪个页面链接过来的,服务器籍此可以获得一些信息用于处理。
7.User-Agent
- User-Agent:Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36 告诉HTTP服务器, 客户端使用的操作系统和浏览器的名称和版本。
8.Cache-Control
- Cache-Control:private 默认为private 响应只能够作为私有的缓存,不能再用户间共享
- `\Cache-Control:public** `**响应会被缓存,并且在多用户间共享。正常情况, 如果要求HTTP认证,响应会自动设置为 private.
- Cache-Control:must-revalidate 响应在特定条件下会被重用,以满足接下来的请求,但是它必须到服务器端去验证它是不是仍然是最新的。
- Cache-Control:no-cache 响应不会被缓存,而是实时向服务器端请求资源。
- Cache-Control:max-age=10 设置缓存最大的有效时间,但是这个参数定义的是时间大小(比如:60)而不是确定的时间点。单位是[秒 seconds]。
Cache-Control:no-store
在任何条件下,响应都不会被缓存,并且不会被写入到客户端的磁盘里,这也是基于安全考虑的某些敏感的响应才会使用这个。
9.Cookie
Cookie是用来存储一些用户信息以便让服务器辨别用户身份的(大多数需要登录的网站上面会比较常见),比如cookie会存储一些用户的用户名和密码,当用户登录后就会在客户端产生一个cookie来存储相关信息,这样浏览器通过读取cookie的信息去服务器上验证并通过后会判定你是合法用户,从而允许查看相应网页。当然cookie里面的数据不仅仅是上述范围,还有很多信息可以存储是cookie里面,比如sessionid等。
10.Range(用于断点续传)
- Range:bytes=0-5 指定第一个字节的位置和最后一个字节的位置。用于告诉服务器自己想取对象的哪部分。
二、常用的http响应头
1.Cache-Control(对应请求中的Cache-Control)
- Cache-Control:private 默认为private 响应只能够作为私有的缓存,不能再用户间共享
- \Cache-Control:public** 浏览器和缓存服务器都可以缓存页面信息。
- Cache-Control:must-revalidate 对于客户机的每次请求,代理服务器必须想服务器验证缓存是否过时。
- Cache-Control:no-cache 浏览器和缓存服务器都不应该缓存页面信息。
- Cache-Control:max-age=10 是通知浏览器10秒之内不要烦我,自己从缓冲区中刷新。
- Cache-Control:no-store 请求和响应的信息都不应该被存储在对方的磁盘系统中。
2.Content-Type
- Content-Type:text/html;charset=UTF-8 告诉客户端,资源文件的类型,还有字符编码,客户端通过utf-8对资源进行解码,然后对资源进行html解析。通常我们会看到有些网站是乱码的,往往就是服务器端没有返回正确的编码。
3.Content-Encoding
- Content-Encoding:gzip 告诉客户端,服务端发送的资源是采用gzip编码的,客户端看到这个信息后,应该采用gzip对资源进行解码。
4.Date
- Date: Tue, 03 Apr 2018 03:52:28 GMT 这个是服务端发送资源时的服务器时间,GMT是格林尼治所在地的标准时间。http协议中发送的时间都是GMT的,这主要是解决在互联网上,不同时区在相互请求资源的时候,时间混乱问题。
5.Server
- Server:Tengine/1.4.6 这个是服务器和相对应的版本,只是告诉客户端服务器信息。
6.Transfer-Encoding
- Transfer-Encoding:chunked 这个响应头告诉客户端,服务器发送的资源的方式是分块发送的。一般分块发送的资源都是服务器动态生成的,在发送时还不知道发送资源的大小,所以采用分块发送,每一块都是独立的,独立的块都能标示自己的长度,最后一块是0长度的,当客户端读到这个0长度的块时,就可以确定资源已经传输完了。
7.Expires
- Expires:Sun, 1 Jan 2000 01:00:00 GMT 这个响应头也是跟缓存有关的,告诉客户端在这个时间前,可以直接访问缓存副本,很显然这个值会存在问题,因为客户端和服务器的时间不一定会都是相同的,如果时间不同就会导致问题。所以这个响应头是没有Cache-Control:max-age=*这个响应头准确的,因为max-age=date中的date是个相对时间,不仅更好理解,也更准确。
8.Last-Modified
- Last-Modified: Dec, 26 Dec 2015 17:30:00 GMT 所请求的对象的最后修改日期(按照 RFC 7231 中定义的“超文本传输协议日期”格式来表示)
9.Connection
- Connection:keep-alive 这个字段作为回应客户端的Connection:keep-alive,告诉客户端服务器的tcp连接也是一个长连接,客户端可以继续使用这个tcp连接发送http请求。
10.Etag
- ETag: "737060cd8c284d8af7ad3082f209582d" 就是一个对象(比如URL)的标志值,就一个对象而言,比如一个html文件,如果被修改了,其Etag也会别修改,所以,ETag的作用跟Last-Modified的作用差不多,主要供WEB服务器判断一个对象是否改变了。比如前一次请求某个html文件时,获得了其 ETag,当这次又请求这个文件时,浏览器就会把先前获得ETag值发送给WEB服务器,然后WEB服务器会把这个ETag跟该文件的当前ETag进行对比,然后就知道这个文件有没有改变了。
11.Refresh
- *Refresh: * 用于重定向,或者当一个新的资源被创建时。默认会在5秒后刷新重定向。
12.Access-Control-Allow-Origin
- Access-Control-Allow-Origin: * 号代表所有网站可以跨域资源共享,如果当前字段为那么Access-Control-Allow-Credentials就不能为true
- Access-Control-Allow-Origin: www.baidu.com 指定哪些网站可以跨域资源共享
13.Access-Control-Allow-Methods
- Access-Control-Allow-Methods:GET,POST,PUT,DELETE 允许哪些方法来访问
14.Access-Control-Allow-Credentials
- Access-Control-Allow-Credentials: true 是否允许发送cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。如果access-control-allow-origin为*,当前字段就不能为true
15.Content-Range
- Content-Range: bytes 0-5/7877 指定整个实体中的一部分的插入位置,他也指示了整个实体的长度。在服务器向客户返回一个部分响应,它必须描述响应覆盖的范围和整个实体长度。
5.49 fetch 请求方式
参考答案:
fetch
Fetch API 是近年来被提及将要取代XHR
的技术新标准,是一个 HTML5 的 API。
Fetch 并不是XHR
的升级版本,而是从一个全新的角度来思考的一种设计。Fetch 是基于 Promise 语法结构,而且它的设计足够低阶,这表示它可以在实际需求中进行更多的弹性设计。对于XHR所提供的能力来说,Fetch 已经足够取代XHR
,并且提供了更多拓展的可能性。
基本用法
// 获取 some.json 资源 fetch('some.json') .then(function(response) { return response.json(); }) .then(function(data) { console.log('data', data); }) .catch(function(error) { console.log('Fetch Error: ', error); }); // 采用ES2016的 async/await 语法 async function() { try { const response = await fetch('some.json'); const data = response.json(); console.log('data', data); } catch (error) { console.log('Fetch Error: ', error) } }
fetch.Post请求
fetch('https://www.api.com/api/xxx', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, body: 'a=1&b=2', }).then(resp => resp.json()).then(resp => { console.log(resp) });
fetch.Get请求
fetch('https://www.api.com/api/xxx?location=北京&key=bc08513d63c749aab3761f77d74fe820',{ method:'GET' }) // 返回一个Promise对象 .then((res)=>{ return res.json(); }) .then((res)=>{ console.log(res) // res是最终的结果 })
fetch请求网页
fetch('https://www.api.com/api/xxx') .then(response => response.text()) .then(data => console.log(data));
自定义header
var headers = new Headers({ "Content-Type": "text/plain", "X-Custom-Header": "aaabbbccc", }); var formData = new FormData(); formData.append('name', 'lxa'); formData.append('file', someFile); var config = { credentials: 'include', // 支持cookie headers: headers, // 自定义头部 method: 'POST', // post方式请求 body: formData // post请求携带的内容 }; fetch('https://www.api.com/api/xxx', config) .then(response => response.json()) .then(data => console.log(data)); // 或者这样添加头部 var content = "Hello World"; var myHeaders = new Headers(); myHeaders.append("Content-Type", "text/plain"); myHeaders.append("Content-Length", content.length.toString()); myHeaders.append("X-Custom-Header", "ProcessThisImmediately");
fetch其他参数
- method: 请求的方法,例如:
GET
,POST
。 - headers: 请求头部信息,可以是一个简单的对象,也可以是 Headers 类实例化的一个对象。
- body: 需要发送的信息内容,可以是
Blob
,BufferSource
,FormData
,URLSearchParams
或者USVString
。注意,GET
,HEAD
方法不能包含body。 - mode: 请求模式,分别有
cors
,no-cors
,same-origin
,navigate
这几个可选值。- cors: 允许跨域,要求响应中
Acess-Control-Allow-Origin
这样的头部表示允许跨域。 - no-cors: 只允许使用
HEAD
,GET
,POST
方法。 - same-origin: 只允许同源请求,否则直接报错。
- navigate: 支持页面导航。
- cors: 允许跨域,要求响应中
- credentials: 表示是否发送
cookie
,有三个选项- omit: 不发送
cookie
。 - same-origin: 仅在同源时发送
cookie
。 - include: 发送
cookie
。
- omit: 不发送
- cache: 表示处理缓存的策略。
- redirect: 表示发生重定向时,有三个选项
- follow: 跟随。
- error: 发生错误。
- manual: 需要用户手动跟随。
- integrity: 包含一个用于验证资资源完整性的字符串
var URL = 'https://www.api.com/api/xxx'; // 实例化 Headers var headers = new Headers({ "Content-Type": "text/plain", "Content-Length": content.length.toString(), "X-Custom-Header": "ProcessThisImmediately", }); var getReq = new Request(URL, {method: 'GET', headers: headers }); fetch(getReq).then(function(response) { return response.json(); }).catch(function(error) { console.log('Fetch Error: ', error); });
5.50 http 缓存策略
参考答案:
http 缓存策略
浏览器每次发起请求时,先在本地缓存中查找结果以及缓存标识,根据缓存标识来判断是否使用本地缓存。如果缓存有效,则使用本地缓存;否则,则向服务器发起请求并携带缓存标识。根据是否需向服务器发起HTTP请求,将缓存过程划分为两个部分:
强制缓存和协商缓存,强缓优先于协商缓存。- 强缓存,服务器通知浏览器一个缓存时间,在缓存时间内,下次请求,直接用缓存,不在时间内,执行比较缓存策略。
- 协商缓存,让客户端与服务器之间能实现缓存文件是否更新的验证、提升缓存的复用率,将缓存信息中的Etag和Last-Modified
通过请求发送给服务器,由服务器校验,返回304状态码时,浏览器直接使用缓存。
HTTP缓存都是从第二次请求开始的:
第一次请求资源时,服务器返回资源,并在response header中回传资源的缓存策略;
第二次请求时,浏览器判断这些请求参数,击中强缓存就直接200,否则就把请求参数加到request header头中传给服务器,看是否击中协商缓存,击中则返回304,否则服务器会返回新的资源。这是缓存运作的一个整体流程图:
强缓存
- 强缓存命中则直接读取浏览器本地的资源,在network中显示的是from memory或者from disk
- 控制强制缓存的字段有:Cache-Control(http1.1)和Expires(http1.0)
- Cache-control是一个相对时间,用以表达自上次请求正确的资源之后的多少秒的时间段内缓存有效。
- Expires是一个绝对时间。用以表达在这个时间点之前发起请求可以直接从浏览器中读取数据,而无需发起请求
- Cache-Control的优先级比Expires的优先级高。前者的出现是为了解决Expires在浏览器时间被手动更改导致缓存判断错误的问题。
如果同时存在则使用Cache-control。
强缓存-expires
- 该字段是服务器响应消息头字段,告诉浏览器在过期时间之前可以直接从浏览器缓存中存取数据。
- Expires 是 HTTP 1.0 的字段,表示缓存到期时间,是一个绝对的时间 (当前时间+缓存时间)。在响应消息头中,设置这个字段之后,就可以告诉浏览器,在未过期之前不需要再次请求。
- 由于是绝对时间,用户可能会将客户端本地的时间进行修改,而导致浏览器判断缓存失效,重新请求该资源。此外,即使不考虑修改,时差或者误差等因素也可能造成客户端与服务端的时间不一致,致使缓存失效。
- 优势特点
- HTTP 1.0 产物,可以在HTTP 1.0和1.1中使用,简单易用。
- 以时刻标识失效时间。
- 劣势问题
- 时间是由服务器发送的(UTC),如果服务器时间和客户端时间存在不一致,可能会出现问题。
- 存在版本问题,到期之前的修改客户端是不可知的。
强缓存-cache-control
- 已知Expires的缺点之后,在HTTP/1.1中,增加了一个字段Cache-control,该字段表示资源缓存的最大有效时间,在该时间内,客户端不需要向服务器发送请求。
- 这两者的区别就是前者是绝对时间,而后者是相对时间。下面列举一些
Cache-control
字段常用的值:(完整的列表可以查看MDN)max-age
:即最大有效时间。must-revalidate
:如果超过了max-age
的时间,浏览器必须向服务器发送请求,验证资源是否还有效。no-cache
:不使用强缓存,需要与服务器验证缓存是否新鲜。no-store
: 真正意义上的“不要缓存”。所有内容都不走缓存,包括强制和对比。public
:所有的内容都可以被缓存 (包括客户端和代理服务器, 如 CDN)private
:所有的内容只有客户端才可以缓存,代理服务器不能缓存。默认值。
- Cache-control 的优先级高于 Expires,为了兼容 HTTP/1.0 和 HTTP/1.1,实际项目中两个字段都可以设置。
- 该字段可以在请求头或者响应头设置,可组合使用多种指令:
- 可缓存性
- public:浏览器和缓存服务器都可以缓存页面信息
- private:default,代理服务器不可缓存,只能被单个用户缓存
- no-cache:浏览器器和服务器都不应该缓存页面信息,但仍可缓存,只是在缓存前需要向服务器确认资源是否被更改。可配合private,
过期时间设置为过去时间。 - only-if-cache:客户端只接受已缓存的响应
- 到期
- max-age=:缓存存储的最大周期,超过这个周期被认为过期。
- s-maxage=:设置共享缓存,比如can。会覆盖max-age和expires。
- max-stale[=]:客户端愿意接收一个已经过期的资源
- min-fresh=:客户端希望在指定的时间内获取最新的响应
- stale-while-revalidate=:客户端愿意接收陈旧的响应,并且在后台一部检查新的响应。时间代表客户端愿意接收陈旧响应
的时间长度。 - stale-if-error=:如新的检测失败,客户端则愿意接收陈旧的响应,时间代表等待时间。
- 重新验证和重新加载
- must-revalidate:如页面过期,则去服务器进行获取。
- proxy-revalidate:用于共享缓存。
- immutable:响应正文不随时间改变。
- 其他
- no-store:绝对禁止缓存
- no-transform:不得对资源进行转换和转变。例如,不得对图像格式进行转换。
- 可缓存性
- 优势特点
- HTTP 1.1 产物,以时间间隔标识失效时间,解决了Expires服务器和客户端相对时间的问题。
- 比Expires多了很多选项设置。
- 劣势问题
- 存在版本问题,到期之前的修改客户端是不可知的。
协商缓存
- 协商缓存的状态码由服务器决策返回200或者304
- 当浏览器的强缓存失效的时候或者请求头中设置了不走强缓存,并且在请求头中设置了If-Modified-Since 或者 If-None-Match 的时候,会将这两个属性值到服务端去验证是否命中协商缓存,如果命中了协商缓存,会返回 304 状态,加载浏览器缓存,并且响应头会设置 Last-Modified 或者 ETag 属性。
- 对比缓存在请求数上和没有缓存是一致的,但如果是 304 的话,返回的仅仅是一个状态码而已,并没有实际的文件内容,因此 在响应体体积上的节省是它的优化点。
- 协商缓存有 2 组字段(不是两个),控制协商缓存的字段有:Last-Modified/If-Modified-since(http1.0)和Etag/If-None-match(http1.1)
- Last-Modified/If-Modified-since表示的是服务器的资源最后一次修改的时间;Etag/If-None-match表示的是服务器资源的唯一标
识,只要资源变化,Etag就会重新生成。 - Etag/If-None-match的优先级比Last-Modified/If-Modified-since高。
协商缓存-协商缓存-Last-Modified/If-Modified-since
- 服务器通过
Last-Modified
字段告知客户端,资源最后一次被修改的时间,例如Last-Modified: Mon, 10 Nov 2018 09:10:11 GMT
- 浏览器将这个值和内容一起记录在缓存数据库中。
- 下一次请求相同资源时时,浏览器从自己的缓存中找出“不确定是否过期的”缓存。因此在请求头中将上次的
Last-Modified
的值写入到请求头的If-Modified-Since
字段 - 服务器会将
If-Modified-Since
的值与Last-Modified
字段进行对比。如果相等,则表示未修改,响应 304;反之,则表示修改了,响应 200 状态码,并返回数据。 - 优势特点
- 不存在版本问题,每次请求都会去服务器进行校验。服务器对比最后修改时间如果相同则返回304,不同返回200以及资源内容。
- 劣势问题
- 只要资源修改,无论内容是否发生实质性的变化,都会将该资源返回客户端。例如周期性重写,这种情况下该资源包含的数据实际上一样的。
- 以时刻作为标识,无法识别一秒内进行多次修改的情况。 如果资源更新的速度是秒以下单位,那么该缓存是不能被使用的,因为它的时间单位最低是秒。
- 某些服务器不能精确的得到文件的最后修改时间。
- 如果文件是通过服务器动态生成的,那么该方法的更新时间永远是生成的时间,尽管文件可能没有变化,所以起不到缓存的作用。
- 服务器通过
协商缓存-Etag/If-None-match
- 为了解决上述问题,出现了一组新的字段
Etag
和If-None-Match
Etag
存储的是文件的特殊标识(一般都是 hash 生成的),服务器存储着文件的Etag
字段。之后的流程和Last-Modified
一致,只是Last-Modified
字段和它所表示的更新时间改变成了Etag
字段和它所表示的文件 hash,把If-Modified-Since
变成了If-None-Match
。服务器同样进行比较,命中返回 304, 不命中返回新资源和 200。- 浏览器在发起请求时,服务器返回在Response header中返回请求资源的唯一标识。在下一次请求时,会将上一次返回的Etag值赋值给If-No-Matched并添加在Request Header中。服务器将浏览器传来的if-no-matched跟自己的本地的资源的ETag做对比,如果匹配,则返回304通知浏览器读取本地缓存,否则返回200和更新后的资源。
- Etag 的优先级高于 Last-Modified。
- 优势特点
- 可以更加精确的判断资源是否被修改,可以识别一秒内多次修改的情况。
- 不存在版本问题,每次请求都回去服务器进行校验。
- 劣势问题
- 计算ETag值需要性能损耗。
- 分布式服务器存储的情况下,计算ETag的算法如果不一样,会导致浏览器从一台服务器上获得页面内容后到另外一台服务器上进行验证时现ETag不匹配的情况。
- 为了解决上述问题,出现了一组新的字段
5.51 no-store 和 no-cache 的区别
参考答案:
no-cache 和 no-store 都是 HTTP 协议头 Cache-Control 的值。
区别是:
no-store
彻底禁用缓冲,所有内容都不会被缓存到缓存或临时文件中。
no-cache
在浏览器使用缓存前,会往返对比 ETag,如果 ETag 没变,返回 304,则使用缓存。
5.52 Cache-Control和expires区别是什么,哪个优先级高
参考答案:
Cache-Control和expires区别:
Cache-Control设置时间长度
Expires 设置时间点
优先级:
强缓存expires和cache-control同时存在时,则cache-control会覆盖expires,expires无论有没有过期,都无效。 即:cache-control优先级 > expires优先级。
5.53 什么是粘包问题,如何解决?
参考答案:
默认情况下,TCP 连接会采用延迟传送算法(Nagle 算法),在数据发送之前缓存他们。如果短时间有多个数据发送,会缓冲到一起作一次发送(缓冲大小是 socket.bufferSize
),这样可以减少 IO 消耗提高性能。(TCP 会出现这个问题,HTTP 协议解决了这个问题)
解决方法
- 多次发送之前间隔一个等待时间:处理简单,但是影响传输效率;
- 关闭 Nagle 算法:消耗资源高,整体性能下降;
- 封包/拆包:使用一些有标识来进行封包拆包(类似 HTTP 协议头尾);
6.node
6.1 node中的模块化导入导出和ES6的区别
参考答案:
node commonjs规范模块化
module对象为模块运行时生成的标识对象,提供模块信息;
exports为模块导出引用数据类型时,modulex.exports与exports指向的是同一对象,require导入的是module.exports导出的对象;
同一模块导入后存在模块缓存,后续多次导入从缓存中加载;
源模块的引用与导入该模块的引用是同一对象;
最好不要同时使用module.exports与exports,导出对象使用module.exports,导出变量使用exports。
es6规范模块化
es6通过export和import实现导出导入功能;
es6 export支持导出多个变量,export default是export形式的语法糖,表示导出default接口;
import * as xx from 'xx.js'
导入的是Module对象,包含default接口和其他变量接口;多个模块导入多次,只会执行一次;
导出引用数据类型时,导出对象与导入对象指向同一个变量,修改导出变量对象,源对象也会发生改变。
导出单个变量建议使用export default,导出多个变量使用export。
6.2 Node 中间件
参考答案:
中间件概念
在NodeJS中,中间件主要是指封装所有Http请求细节处理的方法。一次Http请求通常包含很多工作,如记录日志、ip过滤、查询字符串、请求体解析、Cookie处理、权限验证、参数验证、异常处理等,但对于Web应用而言,并不希望接触到这么多细节性的处理,因此引入中间件来简化和隔离这些基础设施与业务逻辑之间的细节,让开发者能够关注在业务的开发上,以达到提升开发效率的目的。
中间件的行为比较类似Java中过滤器的工作原理,就是在进入具体的业务处理之前,先让过滤器处理。它的工作模型下图所示。
中间件工作模型
中间件机制核心实现
中间件是从Http请求发起到响应结束过程中的处理方法,通常需要对请求和响应进行处理,因此一个基本的中间件的形式如下:
const middleware = (req, res, next) => { // TODO next() }
以下通过两种方式的中间件机制的实现来理解中间件是如何工作的。
方式一
如下定义三个简单的中间件:
const middleware1 = (req, res, next) => { console.log('middleware1 start') next() } const middleware2 = (req, res, next) => { console.log('middleware2 start') next() } const middleware3 = (req, res, next) => { console.log('middleware3 start') next() }
通过递归的形式,将后续中间件的执行方法传递给当前中间件,在当前中间件执行结束,通过调用next()
方法执行后续中间件的调用。
// 中间件数组 const middlewares = [middleware1, middleware2, middleware3] function run (req, res) { const next = () => { // 获取中间件数组中第一个中间件 const middleware = middlewares.shift() if (middleware) { middleware(req, res, next) } } next() } run() // 模拟一次请求发起
执行以上代码,可以看到如下结果:
middleware1 start middleware2 start middleware3 start
如果中间件中有异步操作,需要在异步操作的流程结束后再调用next()
方法,否则中间件不能按顺序执行。改写middleware2中间件:
const middleware2 = (req, res, next) => { console.log('middleware2 start') new Promise(resolve => { setTimeout(() => resolve(), 1000) }).then(() => { next() }) }
执行结果与之前一致,不过middleware3会在middleware2异步完成后执行。
执行结果
方式二
有些中间件不止需要在业务处理前执行,还需要在业务处理后执行,比如统计时间的日志中间件。在方式一情况下,无法在next()
为异步操作时再将当前中间件的其他代码作为回调执行。因此可以将next()
方法的后续操作封装成一个Promise
对象,中间件内部就可以使用next.then()
形式完成业务处理结束后的回调。改写run()
方法如下:
function run (req, res) { const next = () => { const middleware = middlewares.shift() if (middleware) { // 将middleware(req, res, next)包装为Promise对象 return Promise.resolve(middleware(req, res, next)) } } next() }
中间件的调用方式需改写为:
const middleware1 = (req, res, next) => { console.log('middleware1 start') // 所有的中间件都应返回一个Promise对象 // Promise.resolve()方法接收中间件返回的Promise对象,供下层中间件异步控制 return next().then(() => { console.log('middleware1 end') }) }
得益于async
函数的自动异步流程控制,中间件也可以用如下方式来实现:
// async函数自动返回Promise对象 const middleware2 = async (req, res, next) => { console.log('middleware2 start') await new Promise(resolve => { setTimeout(() => resolve(), 1000) }) await next() console.log('middleware2 end') } const middleware3 = async (req, res, next) => { console.log('middleware3 start') await next() console.log('middleware3 end') }
执行结果如下:
执行结果
以上描述了中间件机制中多个异步中间件的调用流程,实际中间件机制的实现还需要考虑异常处理、路由等。
在express
框架中,中间件的实现方式为方式一,并且全局中间件和内置路由中间件中根据请求路径定义的中间件共同作用,不过无法在业务处理结束后再调用当前中间件中的代码。koa2
框架中中间件的实现方式为方式二,将next()
方法返回值封装成一个Promise
,便于后续中间件的异步流程控制,实现了koa2
框架提出的洋葱圈模型,即每一层中间件相当于一个球面,当贯穿整个模型时,实际上每一个球面会穿透两次。
koa2中间件洋葱圈模型
koa2
框架的中间件机制实现得非常简洁和优雅,这里学习一下框架中组合多个中间件的核心代码。
function compose (middleware) { if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!') for (const fn of middleware) { if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!') } return function (context, next) { let index = -1 return dispatch(0) function dispatch (i) { // index会在next()方法调用后累加,防止next()方法重复调用 if (i <= index) return Promise.reject(new Error('next() called multiple times')) index = i let fn = middleware[i] if (i === middleware.length) fn = next if (!fn) return Promise.resolve() try { // 核心代码 // 包装next()方法返回值为Promise对象 return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { // 遇到异常中断后续中间件的调用 return Promise.reject(err) } } } }
6.3 node.js 中 require('xxx') 是从哪里导入的
参考答案:
require函数可以导入模块、JSON文件、本地文件。模块可以通过一个相对路径从node_modules、本地模块、JSON文件中导出,该路径将针对__dirname变量(如果已定义)或者当前工作目录。
6.4 node.js 中 事件循环 和 浏览器事件循环的区别
参考答案:
- 任务队列
浏览器环境
浏览器环境下的 异步任务 分为 宏任务(macroTask) 和 微任务(microTask):
- 宏任务(macroTask):script 中代码、setTimeout、setInterval、I/O、UI render;
- 微任务(microTask): Promise、Object.observe、MutationObserver。
当满足执行条件时,宏任务(macroTask) 和 微任务(microTask) 会各自被放入对应的队列:宏队列(Macrotask Queue) 和 微队列(Microtask Queue) 中等待执行。
Node 环境
在 Node 环境中 任务类型 相对就比浏览器环境下要复杂一些:
- microTask:微任务;
- nextTick:
process.nextTick
; - timers:执行满足条件的 setTimeout 、setInterval 回调;
- I/O callbacks:是否有已完成的 I/O 操作的回调函数,来自上一轮的 poll 残留;
- poll:等待还没完成的 I/O 事件,会因 timers 和超时时间等结束等待;
- check:执行 setImmediate 的回调;
- close callbacks:关闭所有的 closing handles ,一些 onclose 事件;
- idle/prepare 等等:可忽略。
因此,也就产生了执行事件循环相应的任务队列 Timers Queue、I/O Queue、Check Queue 和 Close Queue。
2.执行过程
浏览器环境
先执行``中的同步任务,然后所有微任务,一个宏任务,所有微任务,一个宏任务......
- 执行完主执行线程中的任务;
- 取出 Microtask Queue 中任务执行直到清空;
- 取出 Macrotask Queue 中一个任务执行;
- 重复 2 和 3 。
需要 注意 的是:
- 在浏览器页面中可以认为初始执行线程中没有代码,每一个
中的代码是一个独立的 **task** ,即会执行完前面的
中创建的 microTask 再执行后面的``中的同步代码; - 如果 microTask 一直被添加,则会继续执行 microTask ,“卡死” macroTask;
- 部分版本浏览器有执行顺序与上述不符的情况,可能是不符合标准或 js 与 html 部分标准冲突;
- Promise 的
then
和catch
才是 microTask ,本身的内部代码不是; - 个别浏览器独有API未列出。
Node 环境
循环之前
在进入第一次循环之前,会先进行如下操作:
- 同步任务;
- 发出异步请求;
- 规划定时器生效的时间;
- 执行
process.nextTick()
。
开始循环
循环中进行的操作:
- 清空当前循环内的 Timers Queue,清空 NextTick Queue,清空 Microtask Queue;
- 清空当前循环内的 I/O Queue,清空 NextTick Queue,清空 Microtask Queue;
- 清空当前循环内的 Check Queue,清空 NextTick Queue,清空 Microtask Queue;
- 清空当前循环内的 Close Queue,清空 NextTick Queue,清空 Microtask Queue;
- 进入下轮循环。
可以看出,nextTick 优先级比 Promise 等 microTask 高,setTimeout
和setInterval
优先级比setImmediate
高。
注意
在整个过程中,需要 注意 的是:
- 如果在 timers 阶段执行时创建了
setImmediate
则会在此轮循环的 check 阶段执行,如果在 timers 阶段创建了setTimeout
,由于 timers 已取出完毕,则会进入下轮循环,check 阶段创建 timers 任务同理; setTimeout
优先级比setImmediate
高,但是由于setTimeout(fn,0)
的真正延迟不可能完全为 0 秒,可能出现先创建的setTimeout(fn,0)
而比setImmediate
的回调后执行的情况。
6.5 node.js 中 多进程如何创建子进程
参考答案:
node.js 中 多进程如何创建子进程
hild_process 模块赋予了node可以随意创建子进程的能力 ,它提供了4个方法用于创建子进程:
spawn()
: 启动一个子进程来执行命令 exec
: 启动一个子进程来执行命令,与spawn
不同的是其接口不同,他有一个回掉函数来获知子进程的状况。 execFile()
:启动一个子进程来执行可执行文件。 fork()
:与spawn()
类似,不同点在于它创建Node的子进程只需指定要执行的JavaScript文件模块即可。
spawn()与exec()、execFile()的不同是:
后两者创建时可以指定timeout属性设置超时时间,一旦创建的进程运行超过设定的时间将会
被杀死
exec()与execFile()不同的是,exec()适合执行已有的命令
,execFile()适合执行文件
。这里我们以一个寻常命令为例,node worker.js分别用上述4种方法实现,如下所示
var cp = require('child_process'); //spawn cp.spawn('node', ['worker.js']); //exec cp.exec('node worker.js', function (err, stdout, stderr) { // some code }); //execFile cp.execFile('worker.js', function (err, stdout, stderr) { // some code }); //fork cp.fork('./worker.js');
如果是JavaScript文件通过execFile()运行,它的首行内容必须添加如下代码
#!/usr/bin/env node
尽管4种创建子进程的方式有些差别,但事实上后面3种方法都是spawn()的延伸应用
6.6 请介绍一下 require 的模块加载机制
参考答案:
- 计算模块绝对路径;
- 如果缓存中有该模块,则从缓存中取出该模块;
- 按优先级依次寻找并编译执行模块,将模块推入缓存(require.cache)中;
- 输出模块的
exports
属性;
6.7 请介绍一下 Node 中的内存泄露问题和解决方案
参考答案:
内存泄露原因
- 全局变量:全局变量挂在 root 对象上,不会被清除掉;
- 闭包:如果闭包未释放,就会导致内存泄露;
- 事件监听:对同一个事件重复监听,忘记移除(removeListener),将造成内存泄露。
解决方案
最容易出现也是最难排查的就是事件监听造成的内存泄露,所以事件监听这块需要格外注意小心使用。
如果出现了内存泄露问题,需要检测内存使用情况,对内存泄露的位置进行定位,然后对对应的内存泄露代码进行修复。
6.8 简单介绍一下 IPC
参考答案:
IPC(Inner-Process Communication)又称进程间通信技术,是用于 Node 内部父子进程之间进行通信的方法。
Node 的 IPC 是通过不同平台的管道技术实现的,特点是本地网络通信,速度快,效率高。
Node 在启动子进程的时候,主进程先建立 IPC 通道,然后将 IPC 通道的 fd(文件描述符)通过环境变量(NODE_CHANNEL_FD)的方式传递给子进程,然后子进程通过 fd 与 父进程建立 IPC 连接。
6.9 什么是守护进程?Node 如何实现守护进程?
参考答案:
守护进程是不依赖终端(tty)的进程,不会因为用户退出终端而停止运行的进程。
Node 实现守护进程的思路:
- 创建一个进程 A;
- 在进程 A 中创建进程 B,可以使用
child_process.fork
或者其他方法; - 启动子进程时,设置
detached
属性为 true,保证子进程在父进程退出后继续运行; - 进程 A 退出,进程 B 由 init 进程接管。此时进程 B 为守护进程。
6.10 简单介绍一下 Buffer
参考答案:
Buffer 是 Node 中用于处理二进制数据的类,其中与 IO 相关的操作(网络/文件等)均基于 Buffer。Buffer 类的实例非常类似于整数数组,但其大小是固定不变的,并且其内存在 V8 堆栈外分配原始内存空间。Buffer 类的实例创建之后,其所占用的内存大小就不能再进行调整。