一、计算机发展的 7 个阶段
二、计算机之间的通信
1. OSI 七层参考模型
2. TCP/IP 的分层模型
3. TCP/IP 的数据传输
4. 地址栏 -> URL -> 敲回车 -> 见到页面 的流程
-1). URL 解析
当用户在浏览器地址栏中输入地址并敲下回车之后, 浏览器首先会对用户输入的地址进行校验, 对不符合 URL 规则的整体的部分进行 URL 编码。 JavaScript 中通过
encodeURI、decodeURI、encodeURIComponent、decodeURLComponent、escape、unescape对 URL 进行编码与解码。
escape/unescape 与 其他函数的区别 :
=> escape/unescape 是非标准的, MDN 推荐我们使用 codeURI 与 codeURIComponent 来替代。
=> escape/unescape 在处理 0xFF 之外的字符的时候与 codeURI 和 codeURIComponent 也不同。
=> 后端没有指定的解析规则或者方式来对此类编码的 URL 进行对应的解析
encodeURI/decodeURI 与 encodeURIComponent/decodeURIComponent 的区别 :
=> encodeURI/decodeURI 是针对整个 url 进行编码/解码的, encodeURIComponent/decodeURIComponent 是针对 URL的个别部分进行编码的, 这一点从 encodeURIComponent 中的 Component【组件】 可以看出来。
=> encodeURI/decodeURI 仅把需要进行编码的部分编码而不需要的部分诸如 /、#、&、?等不进行编码。encodeURIComponent/decodeURIComponent 如果对整个 URL 编码的话会将所有需要编码的地方全部编码(包括 /、#、&、?等)
=> 针对于以上特点, 可以 encodeURI/decodeURI 与 encodeURIComponent/decodeURIComponent 结合着用, 或者说如果吃不准, 那干脆就都用 encodeURI/decodeURI 来处理即可。
- 编码:
let url = "https://www.cnblogs.com/shuiyi/p/5277233.html?name=chunlei&age=22&&cn=大仙儿#50a1";
console.log(encodeURI(url)); // https://www.cnblogs.com/shuiyi/p/5277233.html?name=chunlei&age=22&&cn=%E5%A4%A7%E4%BB%99%E5%84%BF#50a1【可直接访问】
// 对整个 url 编码
console.log(encodeURIComponent(url)); // https%3A%2F%2Fwww.cnblogs.com%2Fshuiyi%2Fp%2F5277233.html%3Fname%3Dchunlei%26age%3D22%26%26cn%3D%E5%A4%A7%E4%BB%99%E5%84%BF%2350a1【不可直接访问】
console.log(escape(url)); // https%3A//www.cnblogs.com/shuiyi/p/5277233.html%3Fname%3Dchunlei%26age%3D22%26%26cn%3D%u5927%u4ED9%u513F%2350a1【不可直接访问】
// 对局部 url 编码
console.log(`https://www.cnblogs.com/shuiyi/p/5277233.html?name=chunlei&age=22&&cn=${ encodeURIComponent("大仙儿") }#50a1`); // https://www.cnblogs.com/shuiyi/p/5277233.html?name=chunlei&age=22&&cn=%E5%A4%A7%E4%BB%99%E5%84%BF#50a1 // 可直接访问
解码(解码后得到的 url 字符串均与原来相同均可直接访问) :
console.log(decodeURI(encodeURI(url))); // https://www.cnblogs.com/shuiyi/p/5277233.html?name=chunlei&age=22&&cn=大仙儿#50a1
console.log(decodeURI(decodeURIComponent(encodeURIComponent(url)))); // https://www.cnblogs.com/shuiyi/p/5277233.html?name=chunlei&age=22&&cn=大仙儿#50a1
console.log(decodeURI(unescape(escape(url)))); // https://www.cnblogs.com/shuiyi/p/5277233.html?name=chunlei&age=22&&cn=大仙儿#50a1
-2). 在缓存中查找
- 浏览器根据地址先在自身缓存中查找 DNS 中的解析记录。如果
存在对应域名的 ip 则判断当前请求是 GET 请求还是 POST 请求, 如果是 POST 请求则不走缓存策略直接拿着当前 ip 重新请求, 如果是 GET 请求则继续走缓存策略, 判断该资源是否过期, 如果没过期直接返回该资源, 如果已过期则直接返回对应的 ip。
=> GET 请求缓存示意图
-1). ETag 的值实际上是后端服务器文件通过 hash 算法算出来的 hash 值(可以理解为 MD5 摘要)而下次请求带着的这个 if-None-Match 属性的属性值就是当前生成的 hash 值, 所以下次请求就比对俩 hash 值, 不一样就说明文件已更新, 就需要重新获取资源、一样就说明文件未更新就不需要重新获取资源。
-2). Last-Modified 的值实际上是后端服务器文件最近一次修改后的时间,而下次请求带着的这个 if-Modified-Since 属性的属性值就是文件最近一次修改后的时间, 所以下次请求就比对俩 时间, 不一样就说明文件已更新, 就需要重新获取资源、一样就说明文件未更新就不需要重新获取资源。
-3). ETag/if-None-Match 优先于 Last-Modified/if-Modified-Since 。 -4). 有 3 个响应首部属性来控制强缓存 :
-> 按优先级()排列依次为:
Pragma>Cache-Control>ExpiresPragma :
属性值只有一个
no-cache【不使用强缓存】。Cache-Control :
| 指令 | 作用 |
|---|---|
| max-age | 单位是秒; 超过间隔的秒数则缓存失效 |
| no-store | 禁止使用缓存(包括协商缓存) |
| private | 只能个人缓存、中间代理与 CDN 不能缓存该响应结果 |
| public | 响应结果可以被 中间代理与 CDN 缓存 |
| no-cache | 不使用强缓存, 下次会发起请求验证资源是否过期 |
| max-stale | 指定秒数内即使缓存过期也使用该缓存 |
| min-fresh | 希望在指定秒数内获得最新响应 |
**Expires** Expires 值是一个 http 日期, 在浏览器发起请求时, 会根据系统时间和 Expires 值对比, 如果系统时间超过了 Expires 值则缓存失效, 由于是是系统时间参与比较可能会有误差【浏览器本地时间与服务器时间不一致的时候】。
-3). DNS 域名解析
- 当浏览器根据地址先在自身缓存中查找 DNS 中的解析记录。如果
不存在对应域名的 ip 则继续查找本机的 hosts 文件是否有该域名的 DNS 的解析记录, 如果存在则返回对应的 ip, 如果不存在则查找本地的 DNS 服务器, 如果找到则返回, 未找到就继续查找根域名服务器 -> 顶级域名服务器 -> 权威域名服务器 直到找到对应的 ip。 - DNS 查询/解析 就是查找指定域名对应的指定 ip地址, 基于这个 ip 地址就可以继续去访问目标服务器去拿资源了。
-4). 三次握手
-5). 四次挥手
=> 关于四次挥手补充 :
- 当浏览器与服务器四次挥手之后并不会马上关闭连接, 而是会等一会儿, 这一会儿所处的状态就是 time_wait 状态,
- 为啥要有这个 time_wait 状态 :
- 保证当最后一个 ack 丢失后, 能接收到对端重传的 fin 包。
- 保证重传的 fin 包【重传也需要花时间的】, 不会影响下一个连接。
- 四次挥手也不总是四次挥手, 有时候可能也三次挥手【略过 FIN_WAIT_2 状态】。
-6). 页面渲染
=> 当 html、css、js ... 等资源已经被加载回来的时候。
- 浏览器的 GUI 渲染线程, 解析 html 结构为 DOM 树。
- 浏览器的 GUI 渲染线程, 解析 css 样式表为 CSSOM 树。
- GUI 渲染线程将二者合二为一生成(Render Tree) 渲染树,并计算它们在设备视口内的确切位置和大小【计算阶段被称为(reflow)回流】。
- 根据渲染树以及得到的几何信息, 得到节点的绝对像素, 将渲染树绘制(painting)到页面上。
=> 针对上述流程的优化方案【渲染流程上的优化】 : 针对 css :
- 使用标签语义化和。
- CSS 选择器渲染是从右到左所以要避免深层次嵌套。
- 利用 http 请求的并发机制尽早的将 CSS 下载。
针对 js :
- 将 script 标签放到页面底部, 避免 js 阻塞 html 的解析与渲染。
- 如果非要将 script 放到 head 首部: 解决方案 :
- script 不带 src 属性使用 window.addEventListener('load', function() { console.log("页面所有资源加载完后我执行"); }, false);
- script 不带 src 属性使用 window.addEventListener("DOMContentLoaded", function() { console.log("页面 dom 结构解析完后我就执行"); }, false);
- script 带着 src 属性使用
async属性。 - script 带着 src 属性使用
defer属性。- async 属性与 defer 属性的联系与区别 :
- 联系: 都是异步去请求 js 脚本。
- 区别: 执行时机不同, async 是取回来立马的执行(不拖泥带水, 但此时有可能 html 解析还没有完成)。defer 是取回来等着 html 解析完毕后再执行。所以在使用 async 修饰的脚本中尽量不要操作 DOM , 有可能因 DOM 未解析完成获取不到而抛异常, async 中尽量放置一些计算相关与 DOM 操作无关的代码。defer 就可以放心的使用啦 ~ 。
- async 属性与 defer 属性的联系与区别 :
减少 DOM 回流与重绘 :
- 回流与重绘的定义 :
- 重绘: 元素的样式改变【宽高、大小、位置 不变】
- 回流: 元素的大小和位置发生了变化、浏览器窗口尺寸发生变化, 都会触发回流。
- 回流一定触发重绘, 重绘不一定触发回流。
- 优化方案 :
- 放弃操作 DOM 转而操作 数据。
- 分离读写【针对现代浏览器的渲染队列机制】。
- 样式集中改变。
- 缓存布局信息。
- 元素批量修改【文档碎片 createDocumentFragment】。
- 动画效果应用到 position 属性为 absolute 或 fixed 元素上(脱离文档流不会影响其他元素)。
- CSS3 硬件加速(GPU 加速)【opacity、filter、transform ...】
- 牺牲平滑度换取速度
- 避免 table 布局和在 css 中使用 js 表达式
=> 针对请求资源上的优化方案【网络交互层面上的优化】 :
- DNS 方面(每次 DNS 解析时间约为 20ms ~ 120ms) :
- 减少 DNS 请求次数。
- 使用 DNS 预获取
<link rel="dns-prefetch" href="//static.360buying.com">。
- 减少 http 请求次数与请求资源大小
- 资源合并压缩
- 字体图标
- Base64
- GZIP 压缩技术
- 图片懒加载
- 数据延迟加载与分批加载【分页】
- CDN
- 应用缓存
- 缓存位置 :
- Service Worker 浏览器独立现成进行缓存。
- Memory Cache 内存缓存
- Disk Cache 硬盘缓存
- Push Cache 推送缓存
- 查找机制 :
- 打开网址敲回车 -> 是否有缓存 -> 有就使用缓存没有就请求资源。
- 普通刷新: F5 , 当前网页未关闭, 当前进程依然存在其占用的内存资源也依然存在所以此时 Memory Cache 是可用的。
- 强制刷新: ctrl + F5 -> 浏览器不使用缓存, 直接请求资源。
- 浏览器缓存种类 :
- 强缓存
- 协商缓存
强缓存与协商缓存具体内容参见上面
4. 地址栏 -> URL -> 敲回车 -> 见到页面 的流程 中的 -2). 中的内容
- 缓存位置 :
三、Ajax 与 axios
1. Ajax 释义 :
简单说是网页局部刷新技术, 无需刷新整个页面即可与服务器进行少量的数据交换以达到页面的局部刷新。
2. 常用的数据格式 :
- json
{
"name": "FruitJ"
}
- xml
<?xml version="1.0" encoding="UTF-8"?>
<data>
<name>FruitJ</name>
<age>22</age>
</data>
- stream
3. Ajax 核心四步走
- 创建 XMLHttpRquest 实例
let xhr = new XMLHttpRequest();
- 打开网络请求通道
xhr.open([method], [url], [async]);
- 设置回调监听
xhr.addEventListener('readystatechange', function() {
if(xhr.readyState === 4) {
if(/^[23]\d{2}$/.test(xhr.status)) {
// do something ...
}
}
}, false);
- 发送请求
xhr.send(null);
=> 如果是 post 请求 :
- 在 xhr.send 之前要设置 Content-Type 请求头
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
- xhr.send(params); post 请求需要将请求参数通过 send 放到请求体哦中。
=> 常见的 Content-Type :
-1). application/x-www-form-urlencoded 原生的 form 表单的参数传递形式。
-2). multipart/form-data 文件上传
-3). application/json 提交 json 数据
-4). text/xml 提交 xml 格式数据
=> 更多 Content-Type :
http Content-type 常用对照表 - 开源中国
4. http 请求方式(request method) :
| 序号 | 方法 | 描述 |
|---|---|---|
| 1 | GET | 从服务器获取数据 |
| 2 | DELETE | 从服务器删除数据 |
| 3 | HEAD | 从服务器获取响应头信息 |
| 4 | POST | 向服务器存储数据【会导致新的资源的建立/已有资源的修改】 |
| 5 | PUT | 从服务器修改数据 |
| 6 | OPTIONS | 客户端查看服务器性能 |
| 7 | TRACE | 回显服务器收到的请求, 用于测试或诊断 |
| 8 | PEACH | 是对 PUT 方法的补充, 用于对已知资源进行局部更新 |
=> GET 请求与 POST 请求区别 :
| 区别 | GET | POST |
|---|---|---|
| 传递信息的方式 | 基于 URL 传参 | 基于 http 请求体传参 |
| 安全性 | GET 请求基本明文传参 | POST 请求的参数都在请求体中外界看不到 |
| 缓存 | GET 请求会产生缓存 | POST 请求不会产生缓存 |
| 数据长度 | 2048个字符 | 理论上无限制 |
| 数据类型 | 只允许 ASCII 字符 | 无限制, 也允许 2 进制数据 |
5. http 响应状态码(status code)
| 状态码 | 类别 | 原因短语 |
|---|---|---|
| 1XX | Informational(信息性状态码) | 接收的请求正在处理 |
| 2XX | Success(成功状态码) | 请求正常处理完毕 |
| 3XX | Redirection(重定向) | 需要进行附加操作以完成请求 |
| 4XX | Client Error(客户端错误) | 这个请求包含语法错误或者不能使用, 服务器无法处理请求 |
| 5XX | Server(服务器错误) | 服务器处理请求出错 |
=> 细化(常用状态码) :
- 1xx
- 100 : 【Continue】初始的请求已经接受, 客户应当继续发送请求的其余部分。
- 101 : 【Switching Protocols】 服务器遵循客户请求转换到另外一种协议。
- 2xx
- 200 【OK】成功
- 202 【Accepted】已经接受请求但处理尚未完成
- 3xx
- 301 【Moved Permanently】永久重定向
- 302 【Found】临时重定向,但是会在重定向的时候将 POST 请求改为 GET 请求。也即只能重定向 GET 请求。
- 304 【Not Modified】服务器资源未发生变化, 可以使用缓存
- 307 【Temporary Redirect】临时重定向, 与 302 不同的是, 重定向的时候不会更改用户请求。也即可以重定向 GET/POST 请求
- 4xx
- 403 【Forbidden】资源不可用, 服务器理解客户端请求但拒绝处理它
- 404 【Not Found】无法找到指定位置的资源(可能是地址错误也可能服务器压根就没有此文件)
- 405 【Method Not Allowed】如果未按照服务器指定的请求方法来请求资源, 那么服务器可能会选择返回一个 405 响应码。
- 5xx
- 500 【Internal Server Error】服务器内部错误(例如 Java 后台程序如果抛出一个未捕获的异常就会导致服务器宕机从而报 500)
6. Ajax 状态码 :
- 0: 【UNSENT】=> 刚创建 XMLHttpRequest 实例的时候
- 1: 【OPENED】=> 调用 open 方法后
- 2: 【HEADERS_RECEIVED】=> 已收到响应头信息
- 3: 【LOADING】=> 等待响应体回来
- 4: 【DONE】=> 已收到响应体
7. XMLHttpRequest 实例可以调用的全部属性与方法和事件
-
属性 :
- readyState
- response/responseText/responseXML
- responseType/responseURL
- status/statusText
- timeout
- withCredentials
-
方法 :
- abort
- getAllResponseHeaders
- getResponseHeader
- open
- overrideMimeType
- send
- setRequestHeader
- upload
-
事件 :
- onabort
- onerror
- onreadystatechange
- onload/onloadstart/onloadend
- onprogress
- ontimeout
8. axios 的基础使用
- axios 官网 api 传送
- axios 看云中文文档传送
- 测试代码 : 由于 Live Server 插件不支持 post 请求, 那就用 node 在本地起个服务来解决此事。
server.js
let http = require("http");
let fs = require("fs");
let qs = require("qs");
let Url = require("url");
let mime = require("mime");
const INDEX = `/html/axios.html`,
ICO = `/favicon.ico`,
GET_PATH = `/query`,
POST_PATH=`/push`;
let static = function static(url, res) {
res.setHeader("Content-Type", `${mime.getType(url)};charset=utf-8`);
fs.readFile(`.${url}`, (err, data) => {
res.write(data);
res.end();
});
};
let send = function send(res) {
res.write(`{"status": "ok"}`);
res.end();
};
let callback = function callback(req, res) {
// 随便干点什么事儿 ...
let url = req.url,
paramObj = Url.parse(url, true),
pathname = paramObj.pathname;
if (url === "/") {
static(INDEX, res);
} else {
if (pathname === POST_PATH) {
let allData = ``;
req.on('data', (chunk) => {
allData += chunk;
});
req.on('end', () => {
console.log(qs.parse(allData.toString()));
send(res);
});
} else if (pathname === GET_PATH) {
console.log(paramObj.query.req);
send(res);
} else {
pathname === ICO ? null : static(pathname, res);
}
}
};
let server = http.createServer(callback);
server.listen(3000, 'localhost');
=> 先把 node 服务跑起来然后测试代码即可【node ./js/server.js/nodemon ./js/server.js】。
axios.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>测试 axios</title>
</head>
<body>
<script src="../node_modules/axios/dist/axios.js"></script>
<script src="../node_modules/qs/dist/qs.js"></script>
<script>
let baseURL = "http://localhost:3000/";
void(() => {
function fn() { // axios 原始方式
axios({
method: "POST",
url: `${baseURL}push`,
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
data: {
req: "data",
},
transformRequest: [(data, headers) => {
return Qs.stringify(data);
}],
}).then((res) => {
console.log(res.data);
/*
res 对象中的属性 :
- config: 发送时我们自己配置的参数信息。
- data: 响应体信息
- headers: 响应头信息
- request: 原始 xhr 实例
*/
}).catch((err) => {
console.log(err.message);
});
}
// fn();
})();
void(() => {
function fn() { // get 快捷方式
axios.get(`${baseURL}query`, {
params: {
req: "source",
},
}).then((res) => {
console.log(res.data);
});
}
// fn();
})();
void(() => {
function fn() { // 并发请求
function getData() {
return axios.get(`${baseURL}query`, {
req: "source",
});
}
function pushData() {
return axios.post(`${baseURL}push`, {
req: "data",
}, {
headers: {
"Cache-Control": "no=cache"
}
});
}
axios.all([getData(), pushData()]).then(axios.spread((getRes, pushRes) => {
console.log(getRes.data);
console.log(pushRes.data);
}));
}
// fn();
})();
void(() => {
function fn() { // 默认配置
axios.defaults.baseURL = baseURL;
axios.defaults.timeout = 6000;
axios.defaults.headers["Cache-Control"] = "no-cache";
axios.post("push", {
req: "data",
}).then((res) => {
console.log(res.data);
});
}
// fn();
})();
void(() => {
function fn() { // 拦截器
axios.interceptors.request.use((config) => { // 请求拦截器
console.log(config);
config.headers["Cache-Control"] = "no-cache";
return config;
}, (err) => {
console.log(err);
return Promise.reject(error)
});
axios.interceptors.response.use((res) => { // 响应拦截器
// 如仅想得到响应数据的值的集合 ->
return Object.values(res.data);
}, (err) => {
console.log(err);
return Promise.reject(error);
});
axios.get(`${baseURL}query`, {
params: {
req: "source",
},
}).then((res) => {
console.log(res); // ["ok"]
});
}
fn();
})();
</script>
</body>
</html>
=> 项目目录结构 :
四、 Cookie 与 Session机制
简单的说一下 cookie , 剩下的关于具体的 cookie 与 session 机制请移步笔者以前的博文(JavaEE)JavaWeb中的session与Cookie机制(案例-cookie验证) cookie :
- 不是专门用来存储数据的。
- 只要作用是服务器为了跟踪用户会话而向客户端浏览器颁发的一个 session 凭证, 每次客户端浏览器向服务器发送请求时都会携带 cookie 信息, 服务器通过解析 cookie 信息集合来寻找有没有上一次颁发过的 cookie, 如果有 ok , 根据 cookie 中的信息判定该用户直接登录【这就是用户二次登录免输密码的原理】
- cookie 可以存在内存中也可以存在硬盘上, 对于后端服务器版发的 cookie 通常都是 SESSION_ID 当然也可以自定义内容。服务器版发的 cookie 基本上都存在硬盘上。
- cookie 其实是不太安全的, 当你刚访问银行再去访问个黑站可能对方获取你的 cookie 就可以免密登录你的银行账户了。所以 cookie 中如果包含重要信息例如用户名或者密码之类必须使用 base64 编一下码, 最起码不能太过明显。
- 主要用途 :
- 二次登录免输密码直接登录
- 服务器通过 cookie 识别对应的客户端加载对应的购物车资源
- 加载对应个性化设置 ...
五、当今的浏览器的本地存储方式
-
cookie: 4 kb 左右
- 获取:
document.cookie - 设置:
document.cookie = "key=valye; expires=2089-02-03";- path=path【如果没有定义,默认为当前文档位置的路径】
- domain=domain 【如果没有定义,默认为当前文档位置的路径的域名部分。如果指定了一个域,那么子域也包含在内。】
- max-age=max-age-in-seconds
- expires=date-in-GMTString-format【如果没有定义,cookie会在会话结束时过期】
- secure 【cookie只通过https协议传输】
- 删除:
document.cookie = "key=valye; expires=-1";
- 获取:
-
localStotage : localStorage 与 sessionStorage 用法一样, 不同的是生命周期不同, localStorage 是持久化保存, sessionStorage 是指在当前页面生存期内有效
- setItem(key, value)
- getItem(key)
- removeItem(key)
- clear()
-
SessionStorage :
- setItem(key, value)
- getItem(key)
- removeItem(key)
- clear()
-
IndexDB : 据说是更像一种 no sql 类型的数据库, 没试验过 ...
-
WEBSQL : WEB SQL 我总觉得跟 android 中的 sqlite 数据库极像。 以前试水的 web sql 文章 (JavaScript - 原生)WEB SQL
六、以上参考图除了浏览器渲染模块的的那张图是复制过来的以外其他的均为笔者绘制, 绘图工具: draw.io
七、 参考链接
- IP地址,何为同一网段?何为不同网段? - 百度知道
- 计算机网络基础_2_网络层的作用(上) --IP地址、子网掩码和路由器的作用 - 51CTO
- 如何理解网络层取得源IP地址和目的IP地址的过程 - csdn
- 三次握手及四次挥手在TCP/ip模型的哪一层进行的?最好有理由~ - 百度知道
- 访问Web,tcp传输全过程(三次握手、请求、数据传输、四次挥手) - csdn
- TCP/IP四层模型与OSI参考模型 - 博客园
- ARP (地址解析协议)- 百度百科
- IP寻址详细过程详解 - 百家号
- 请举出OSI七层模型在实际应用中的实例 - 百度知道
- OSI七层通信的简易理解-举例说明 - csdn
- OSI第六层:表示层功能作用 - csdn
- OSI第五层:会话层功能及作用 - csdn
- 会话层 - 百度百科
- OSI数据链路层 - 百度百科
- OSI模型之数据链路层 - csdn
- OSI体系结构的物理层 - 百度知道
- 数据帧、数据包、数据报以及数据段 - csdn
- 网络中的物理层传输的是比特流,数据链路层传输的是帧,谁可以帮我解释下比特流和这个帧吗?谢谢 - 百度知道
- 数据链路层的主要功能 - 680.com
- ACK - 百度百科
- SYN - 百度百科
- TIME_WAIT状态: - 博客园
- 四次挥手中TIME_WAIT,CLOSE_WAIT,FIN_WAIT_1,FIN_WAIT_2状态浅析 - csdn
- fin wait1 是什么意思 - 百度知道
- 从输入url到页面加载完成发生了什么?——前端角度 - 简书
- 图解 HTTP 缓存 - 掘金