HTML和浏览器
JS是解释型语言还是编译型语言
- 解释型语言和编译型语言的区别
- 编译型语言,如 Java,编译器在代码运行前将其从编程语言转换成机器语言,所以编译后的文件可以直接运行
- 解释型语言,编译器在运行时将代码从编程语言转换成机器语言,所以运行环境中要安装解释器
JavaScript
根据以上的定义来看,JavaScript 算是一门解释型语言, 因为 JavaScript 代码需要在机器(node 或者浏览器)上安装一个工具( JS 引擎)才能执行。这是解释型语言需要的
但是同时 JavaScript 又带有一个编译型语言的特性。编译型语言的优点是它有足够的时间在运行之前被编译,在编译的同时做足够的词法分析、极致的优化工作。但是解释型语言要在执行后一瞬间就开始,JavaScript 引擎没有时间做优化。比如:
for(i=0; i<1000; i++){
sum += i;
}
在编译型语言中,循环体内的部分在运行时早已被编译器编译成机器码,所以直接运行 1000 次就好;而在解释型语言中,JavaScript 引擎不得不对循环体内相同的代码解释 1000 次,这会造成很大的性能浪费。所以 Google 和 Mozilla 的开发者给 JavaScript 引擎加上了 JIT (即时编译)的功能
JIT主要的作用是对多次使用的代码进行缓存,避免重复解释,在代码运行之前,会先放在 JIT 中进行编译,它具体这样编译:如果同一段代码运行超过一次,就成为 warm,如果一个函数开始 warm,JIT 就把这段代码缓存起来,下一次就可以不用解释这一段代码。如果代码被重复使用的次数越来越多,也就是变得 hot 或者 hotter,JIT 会进行更多优化并缓存
User-Agent
User-Agent 是一个特征字符串,用来标识发起请求的用户代理软件的应用类型、操作系统、软件开发商和版本号的信息
语法为:
User-Agent: <product> / <product-version> <comment>
浏览器通常使用的格式为:
User-Agent: Mozilla/<version> (<system-information>) <platform> (<platform-details>) <extensions>
以下为一段使用Chrome发出的User-Agent:
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.116 Safari/537.36
性能优化
浏览器缓存
浏览器的缓存规则分为两大块:强制缓存和协商缓存。对于某个文件来说,具体是采用哪种缓存方式,由HTTP Response Headers设置,当然也可以通过 meta 标签,但是现在越来越多浏览器忽略设置缓存的 meta 标签,所以还是推荐通过 HTTP Response Headers 设置
强制缓存
首先客户端会检查本地缓存中是否有所要请求的数据,如果有,就直接从缓存中获取数据;如果没有,就从服务器获取数据
HTTP Response Headers 中 Cache-Control 和 Expires 字段都表示对本地资源启动强制缓存,其中 Cache-Control 是 http1.1 标准中的字段,而 Expires 是 http1.0 的字段,Cache-Control 优先级更高,使用更广泛。
Cache-Control 的值的单位为秒,关键字 max-age 表示可以被缓存多长时间,当 Cache-Control 值设为 max-age=300 时,则代表在这个请求正确返回时间(浏览器也会记录下来)的 5 分钟内再次加载资源,就会命中强缓存
- cache-control 常用值
- max-age:表示可以被缓存多长时间
- -no-cache:不使用本地缓存。需要使用缓存协商,先与服务器确认返回的响应是否被更改,如果之前的响应中存在ETag,那么请求的时候会与服务端验证,如果资源未被更改,则可以避免重新下载
- -no-store:直接禁止浏览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源
- -public:可以被所有的用户缓存,包括终端用户和 CDN 等中间代理服务器
- -private:只能被终端用户的浏览器缓存,不允许 CDN 等中继缓存服务器对其缓存
Expires 字段规定了过期时间,浏览器再次加载资源时,如果在这个过期时间内,则命中强缓存
协商缓存
客户端先从本地缓存中获得一个缓存标识,带着这个标识向服务器发送请求进行验证,如果证明缓存没有失效就会返回 304,此时直接从本地缓存中获取数据;如果证明缓存失效,就从服务端获取数据。
协商缓存也有 2 个标识头:Last-Modified 和 Etag
- Last-Modified
资源被服务器返回时,HTTP Response Headers 中的 Last-Modified 返回头标识了此资源在服务器上的最后修改时间。浏览器再次请求服务器时,会将上次 Last-Modified 的值作为 if-Modified-Since 头的值发送,服务器收到请求后,查看最后修改时间以后资源是否被修改过,如果没有被修改过,就返回 304,从缓存读取;修改过,返回状态 200 以及整个资源
- Etag
至于 Etag 头,资源被服务器返回时,服务器通过算法生成针对这个资源的唯一标识作为 Etag 的值返回给浏览器。下次浏览器再次请求时,浏览器将上次 Etag 的值作为这个表示头的值发送给服务器。通过唯一标识来检测资源是否被修改过,如果没有被修改过,就返回 304,从缓存读取。修改过,返回状态 200 以及整个资源
- Last-Modified和Etag对比
Etag 要优于 Last-Modified,Last-Modified的单位是秒,如果数据在1秒内多次改变,则使用Last-Modified不能体现出变化,在优先级上,服务器校验优先考虑Etag
在性能上,Etag 要逊于 Last-Modified,毕竟 Last-Modified 只需要记录时间,而 Etag 需要服务器通过算法来计算出一个 hash 值
缓存保存的位置
缓存保存的位置按优先级从高到低分别是
- Service Worker
Service Worker 运行在 JavaScript 主线程之外,虽然由于脱离了浏览器窗体无法直接访问 DOM,但是它可以完成离线缓存、消息推送、网络代理等功能
- Memory Cache
Memory Cache 就是内存缓存,它的效率最快,但是存活时间最短,关掉浏览器后 Memory Cache 里的文件会被清空
- Disk Cache
Cache 资源被存储在硬盘上,存活时间比 Memory Cache 要持久很多
总结
浏览器缓存最重要的是能区分开强制缓存和协商缓存:完全不向服务器发送请求的是强制缓存,向服务器发送请求的是协商缓存,涉及到 304 的都是协商缓存
浏览器缓存的全过程
- 浏览器第一次加载资源,服务器返回 200,浏览器从服务器下载资源文件,并缓存住资源文件与 response header 以供下次加载时对比使用;
- 下一次加载资源时,由于强制缓存优先级较高,先比较当前时间与上一次返回 200 时的时间差,如果没有超过 cache-control 设置的 max-age,则没有过期,并命中强缓存,直接从本地读取资源。如果浏览器不支持HTTP1.1,则使用 expires 头判断是否过期;
- 如果资源已过期,则表明强制缓存没有被命中,则开始协商缓存,向服务器发送带有 If-None-Match 和 If-Modified-Since 的请求;
- 服务器收到请求后,优先根据 Etag 的值判断被请求的文件有没有做修改,Etag 值一致则没有修改,命中协商缓存,返回 304;如果不一致则有改动,直接返回新的资源文件带上新的 Etag 值并返回 200;
- 如果服务器收到的请求没有 Etag 值,则将 If-Modified-Since 和被请求文件的最后修改时间做比对,一致则命中协商缓存,返回 304;不一致则返回新的 last-modified 和文件并返回 200;
扩展
- 点击刷新按钮或者按 F5
浏览器直接对本地的缓存文件过期,但是会带上If-Modifed-Since,If-None-Match,这就意味着服务器会对文件检查新鲜度,返回结果可能是 304,也有可能是 200
- 用户按 Ctrl+F5(强制刷新)
浏览器不仅会对本地文件过期,而且不会带上 If-Modifed-Since,If-None-Match,相当于之前从来没有请求过,返回结果是 200
- 地址栏回车
浏览器发起请求,按照正常流程,本地检查是否过期,然后服务器检查新鲜度,最后返回内容
网页性能检查
- 通过浏览器开发者工具或浏览器插件、Fiddler、Charles 等查看页面加载情况
- 在页面中引入额外的代码钩子,来记录时间等相关数据
- 使用第三方的工具如 Chrome UX 报告等
- Performance,它是 W3C 推出的专门用来测试性能的接口,示例
let t0 = window.performance.now();
doSomething();
let t1 = window.performance.now();
console.log("doSomething函数执行了" + (t1 - t0) + "毫秒.")
// Performance 接口比 JavaScript 中 Date 对象好的地方在于,使用 JavaSciprt 中的 Date 对象精度不够、功能有限
// 而 Performance 接口不仅提高了精度,而且还可以获得一些后台事件的时间进度,包括页面载入、DOM 交互、DNS 解析、服务器响应、页面下载、TCP 和 SSL 耗时等
常用的性能优化方法
服务端
- 使用 CDN,CDN 可以通过 DNS 负载均衡技术将用户的请求转移到就近的 cache 服务器上,提高请求返回速度
CDN 的英文全拼是:Content Delivery Network,即内容分发网络。它的原理是通过在现有的 Internet 中增加一层新的网络架构,将网站的内容发布到最接近用户的 cache 服务器内,通过 DNS 负载均衡的技术,判断用户来源就近访问 cache 服务器取得所需的内容。比如,深圳用户访问遥远的美国服务器,当然不理想了,把静态内容分布到 CDN 可以减少用户响应时间 20% 或更多
- 利用静态资源缓存,给返回头加上 Cache-Control 或者 Etag 头
网络
- 通过雪碧图、合并请求等方法减少请求数
- 减少文件大小:压缩 CSS、JS 和图片,在服务器(Nginx)中开启 Gzip:也就是先在服务端进行压缩,再在客户端进行解压
客户端
防抖和节流
防抖(debounce)
当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时
function debounce(fn, wait) {
// 使用闭包保存持久变量
var timeout = null;
return function() {
if(timeout !== null) {
// 如果持续触发,就把前面的定时器取消掉,这样来保证只有不触发事件时才开始计时
clearTimeout(timeout);
}
timeout = setTimeout(fn, wait);
}
}
// 处理函数
function handle() {
console.log(Math.random());
}
// 滚动事件
window.addEventListener('scroll', debounce(handle, 1000));
节流(throttle)
当持续触发事件时,保证一定时间段内只调用一次事件处理函数
const throttle = function (func, delay) {
var timer = null;
return function inner () {
// 此处的this为返回的inner函数的调用方,与外层的函数无关,赋值是为了在setTimeout内部调用
var context = this;
// 此处的this也是inner函数的参数,赋值是为了在setTimeout中进行传递
var args = arguments;
if (!timer) {
timer = setTimeout(function () {
func.apply(context, args);
timer = null;
}, delay);
}
}
}
function handle() {
console.log(Math.random());
}
window.addEventListener('scroll', throttle(handle, 1000));
防抖和节流的区别
- 函数防抖是某一段时间内只执行一次
- 而函数节流是间隔时间执行
Http/Https协议
GET与POST区别
语义上的区别
- GET 方法表达的是一种幂等的,只读的,纯粹的操作,它本身不会对服务器产生副作用(资源不会发生修改),这意味着 GET 请求时可以被缓存的,因此大多数的 GET 请求不需要真正到达服务器去读取资源,只需要读取上一次的缓存结果就行了
- POST 所表达的语义是非幂等的,有副作用的操作,有副作用的操作是不能被缓存的,因为请求必须真的到达服务器副作用才能发生,所以 POST 请求必须交由 web 服务器处理
幂等:一个请求原封不动地发送 N 次和 M 次 (N 不等于 M,N 和 M 都大于 1),服务器上资源的状态最终都是一致的
用法上的区别
- 带参数时,按照标准,GET 方法的参数要放在 url 中,而 POST 方法的参数要放在 body 中
// GET方法简化版报文
GET /index?name=qiming.c&age=22 HTTP/1.1
Host: localhost
// POST方法简化版报文
POST /index.php HTTP/1.1
Host: localhost
Content-Type: application/x-www-form-urlencoded
name=qiming.c&age=22
- 以上传参的方式只是约定格式,实际上在 POST 请求中把参数放在 url 中也是完全可以的,只要服务器能够配合解析
在浏览器中的表现
GET 请求可以直接输入在地址栏中来请求,POST 请求不行;GET 请求会被记录在浏览器历史记录中,而 POST 请求不会
常见请求方法
- GET: 无副作用,幂等
- PUT: 副作用,幂等
- POST: 副作用,非幂等
- DELETE: 副作用,幂等
扩展
- 所有请求都设计成GET
这意味着,所有的类似请求都可以放在地址栏中请求,并且都会被记录在浏览器历史记录中,而且都有可能被爬虫请求,这样做是非常危险的,很可能出现数据库被删光都不知道发生了什么的情况下
- 所有请求都设计成POST
这意味着所有的请求都默认是有副作用的, 所以所有的请求都要直接到达服务器,这意味着无法使用 CDN 缓存,对于服务器资源来说是巨大的浪费
常见HTTP状态码
状态码分类
- 1xx : 表示请求已经接受了,继续处理
- 2xx : 表示请求已经处理掉了
- 3xx : 重定向
- 4xx : 一般表示客户端有错误,请求无法实现
- 5xx : 一般为服务器端的错误
常见状态码
- 200 OK 客户端请求成功。
- 301 Moved Permanently 请求永久重定向。
- 302 Moved Temporarily 请求临时重定向。
- 304 Not Modified 文件未修改,可以直接使用缓存的文件。
- 400 Bad Request 由于客户端请求有语法错误,不能被服务器所理解。
- 401 Unauthorized 请求未经授权,无法访问。
- 403 Forbidden 服务器收到请求,但是拒绝提供服务。服务器通常会在响应正文中给出不提供服务的原因。
- 404 Not Found 请求的资源不存在,比如输入了错误的URL。
- 500 Internal Server Error 服务器发生不可预期的错误,导致无法完成客户端的请求。
- 503 Service Unavailable 服务器当前不能够处理客户端的请求,在一段时间之后,服务器可能会恢复正常
HTTP常用头部信息
请求头
- Authorization:验证信息
- Accept:浏览器能够接收的内容类型,如 text/html
- Accept-Encoding:浏览器能解码的编码类型,如gzip, deflate
- Connection:是否需要持久连接,HTTP1.1默认值为keep-alive
- Cookie:会话追踪
- User-Agent:用户代理信息
- Content-Type:用于指定发送内容的MIME类型,GET请求无该字段,POST/PUT请求常见值有:application/x-www-form-urlencoded, multipart/form-data
响应头
- Last-Modified:资源在服务器的最后修改时间
- Expires:相应过期的时间
- Set-Cookie:设置Cookie
- Content-Encoding:文档的编码方法
- Content-Length:内容的长度
- Content-Type:文档的MIME类型
Cookie, localStorage 和 sessionStorage
localStorage 和 sessionStorage 的出现并不是为了取代 Cookie 的,它们有不同的应用场景,Cookie中的数据是为了给服务端读取的,而 Storage 中的数据是为了给客户端读取的,具体有以下区别:
- 生命周期
- Cookie 一般由服务器生成,可设置失效时间。如果在浏览器端生成 Cookie,默认是关闭浏览器后失效
- localStorage,除非被清除,否则永久保存
- sessionStorage,其仅在当前会话下有效,关闭页面或浏览器后被清除
- 大小
- Cookie 最大是 4K
- Storage是5M
- 与服务器通信时
- Cookie 每次都会携带在 HTTP 头中,所以如果使用 cookie 保存过多数据会带来性能问题
- Storage 仅在客户端(即浏览器)中保存,不参与和服务器的通信
- 使用场景
- Cookie 需要尽量精简,只携带与服务器通信所需要的验证信息,如用户 token
- Storage 存储需要浏览器记住的本地数据,如购物车中的内容
HTTPS
HTTPS 是一种计算机网络进行安全通信的传输协议,实际利用 HTTP 进行通信,但是用 SSL/TLS 来加密数据包。它的出现主要是为了保护交互数据的隐私与完整性
简单来说就是,HTTP 不是安全的,攻击者可以通过监听和中间人攻击等手段,获取网站帐户和敏感信息等。 HTTPS 的设计可以防止前述攻击,在正确配置时是安全的
HTTP面临的风险
HTTP面临的主要风险是:公钥被中间人拿走后并进行篡改,这样接受者就会接收到假信息
所以要解决的核心问题是:防止公钥被掉包,收到公钥的时候要确认这个公钥确实是指定的发送者发送的,而不是别人的
HTTPS的原理
HTTPS 使用 证书+数字签名 的方法来解决上述问题,步骤如下:
- 服务端申请SSL证书
- 客户端与服务端建立连接时,服务端会向客户端发送SSL证书
- 浏览器开始查找操作系统中已内置的受信任的证书发布机构 CA,与服务器发来的证书中的颁发者 CA 比对,用于校验证书是否为合法机构颁发
- 如果找到,那么浏览器就会从操作系统中取出 颁发者 CA 的公钥,然后对服务器发来的证书里面的签名进行解密
- 浏览器使用相同的 hash 算法计算出服务器发来的证书的 hash 值,将这个计算的 hash 值与证书中签名做对比
- 对比结果一致,则证明服务器发来的证书合法,没有被冒充
- 此时浏览器就可以读取证书中的公钥,用于后续加密了
HTTP2
HTTP1.1 的问题
- 线程阻塞:TCP 连接只能发送一个请求,前面的请求未完成前,后续的请求都在排队等待
- 为了加快速度,过度依赖于多个 TCP 连接并发请求,但是建立 TCP 连接成本很高
- 头部冗余,格式是文本格式
- 客户端需要主动请求
HTTP2 的新特性
- 新的二进制格式(Binary Format),HTTP1.x 的解析是基于文本。基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认 0 和 1 的组合。基于这种考虑 HTTP2.0 的协议解析决定采用二进制格式,实现方便且健壮
- 多路复用(MultiPlexing),即连接共享,即每一个 request 都是是用作连接共享机制的。一个 request 对应一个 id,这样一个连接上可以有多个 request,每个连接的 request 可以随机的混杂在一起,接收方可以根据 request 的 id 将 request 再归属到各自不同的服务端请求里面
- header压缩,如上文中所言,对前面提到过 HTTP1.x 的 header 带有大量信息,而且每次都要重复发送, HTTP2.0 使用 encoder 来减少需要传输的 header 大小,通讯双方各自 cache 一份 header fields 表,既避免了重复 header 的传输,又减小了需要传输的大小
- 服务端推送(server push),同 SPDY 一样,HTTP2.0 也具有 server push 功能。目前,有大多数网站已经启用 HTTP2.0
代理和Nginx
正向代理和反向代理
正向代理
客户端想获得一个服务器的数据,但是因为种种原因无法直接获取。于是客户端设置了一个代理服务器,并且指定目标服务器,之后代理服务器向目标服务器转交请求并将获得的内容发送给客户端。这样本质上起到了对真实服务器隐藏真实客户端的目的。实现正向代理需要修改客户端,比如修改浏览器配置
反向代理
服务器为了能够将工作负载分布到多个服务器来提高网站性能 (负载均衡) 等目的,当其受到请求后,会首先根据转发规则来确定请求应该被转发到哪个服务器上,然后将请求转发到对应的真实服务器上。这样本质上起到了对客户端隐藏真实服务器的作用
一般使用反向代理后,需要通过修改 DNS 让域名解析到代理服务器 IP,这时浏览器无法察觉到真正服务器的存在,当然也就不需要修改配置了
正向代理和反向代理的区别
大多数用来访问外网的代理都是正向代理;反向代理大多用来做负载均衡
正向代理和反向代理的结构是一样的,都是 client-proxy-server 的结构,它们主要的区别就在于中间这个 proxy 是哪一方设置的。在正向代理中,proxy 是 client 设置的,用来隐藏 client;而在反向代理中,proxy 是 server 设置的,用来隐藏 server
Nginx
Nginx 是一款轻量级的 Web 服务器,也可以用于反向代理、负载均衡和 HTTP 缓存等。Nginx 使用异步事件驱动的方法来处理请求,是一款面向性能设计的 HTTP 服务器
传统的 Web 服务器如 Apache 是 process-based 模型的,而 Nginx 是基于 event-driven 模型的。正是这个主要的区别带给了 Nginx 在性能上的优势
Nginx 架构的最顶层是一个 master process,这个 master process 用于产生其他的 worker process,这一点和 Apache 非常像,但是 Nginx 的 worker process 可以同时处理大量的 HTTP 请求,而每个 Apache process 只能处理一个
关于Nginx的更多内容可参照另一篇博文
客户端安全
完整内容参照这里
- XSS 的攻击
通过在受害者打开的目标网页上执行恶意脚本来窃取用户的 cookie 值(前提是浏览器已经存在目标网页的cookie,即已经登陆),黑客使用 cookie 值在自己的电脑上冒充用户来进行隐私操作
- CSRF 的攻击
黑客不窃取用户的任何信息,黑客通过诱使受害者点击恶意链接来向目标网站发送请求
- 二者的区别
XSS 攻击着重于窃取 cookie,黑客需要在自己的电脑上用 cookie 来进行恶意操作;而在 CSRF 攻击不窃取任何东西,所有的恶意操作都发生在受害者自己的电脑上