寫作背景
筆者所在專案使用的前端技術比較老舊,在開發的過程中需要先啟動一個後端專案 (tomcat + mysql + redis) 來做為靜態伺服器
然後使用的是一個公司內部的類AMD模組載入工具,每次重新整理頁面都要載入1000+ 的檔案,頁面的響應時間接近20s, 導致開發的過程非常痛苦
所以決定使用 HTTP/2 來開發一個開發伺服器來加快頁面的載入速度. 目前來說效果不錯,相對於 HTTP1.1 來說載入速度提升了 50%。
對於開發環境與我們類似的專案,可以嘗試一下。
理論基礎
1. HTTP/2 的TCP連線複用
雖然我們開發的時候使用的是本地伺服器,建立連線的速度和下載速度都很快,但是瀏覽器針對同一域名的併發請求是有上限的。
當所需要的檔案數量很多時,我們每次只能請求一定數量的檔案,當前面的檔案的請求完成後才能去請求下一個檔案,這就造成了堵塞。
從圖中我們可以看到明顯的連結限制和堵塞
而 HTTP/2 可以在同一連線上進行多個併發交換,可以避免出現因為瀏覽器的併發限制而造成的堵塞
HTTP/2 通過支援標頭欄位壓縮和在同一連線上進行多個併發交換,讓應用更有效地利用網路資源,減少感知的延遲時間。具體來說,它可以對同一連線上的請求和響應訊息進行交錯傳送併為 HTTP 標頭欄位使用有效編碼。 HTTP/2 還允許為請求設定優先順序,讓更重要的請求更快速地完成,從而進一步提升效能。出臺的協議對網路更加友好,因為與 HTTP/1.x 相比,可以使用更少的 TCP 連線。 這意味著與其他流的競爭減小,並且連線的持續時間變長,這些特性反過來提高了可用網路容量的利用率。 最後,HTTP/2 還可以通過使用二進位制訊息分幀對訊息進行更高效的處理。(超文字傳輸協議版本 2,草案 17) - https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn
2. Server Push 伺服器推送
HTTP/2 中最令人期待的特性就是 Sever Push (伺服器推送)。
通過 Server Push,伺服器可以對瀏覽器的單個請求返回多個響應,而不需要等待瀏覽器發出請求再給去響應。
簡單舉個?
- 瀏覽器向伺服器傳送 a.com 請求
- 伺服器確定這個請求返回一個 index.html 檔案,同時發現這個檔案需要 style.css 和 script.js 檔案
- 伺服器向瀏覽器放回 index.html 的響應,同時告訴瀏覽器我這裡有 style.css 和 script.js 檔案你可能需要
- 瀏覽器收到 index.html 後,解析後發現需要 style.css 和 script.js,正好伺服器端說可以推送這兩個資源,瀏覽器就不需要再次傳送請求去獲取,而是直接就收伺服器的推送
結合上面的連線複用,HTTP/2 可以極大的加快資原始檔的載入速度
可以看到瀏覽器使用一個連結載入完了所有的資原始檔
Nodejs HTTP/2 模組簡單使用
這裡先簡單介紹下 Node 中 HTTP/2 的使用,下篇文章將詳細闡述如何編寫一個可以應用的 HTTP/2 開發伺服器
1. 建立 HTTPS 證照
由於 HTTP/2 需要使用 HTTPS,這裡我們需要先生成一個證照。
openssl req -x509 -newkey rsa:2048 -nodes -sha256 -subj '/CN=localhost' \
-keyout localhost-privkey.pem -out localhost-cert.pem
複製程式碼
2. 專案結構
.
├── certificate
│ ├── localhost-cert.pem
│ └── localhost-privkey.pem
├── node_modules
├── package-lock.json
├── package.json
├── src
│ └── app.js
└── www
├── index.html
├── script.js
└── styles.css
複製程式碼
<!-- www/index.html -->
<!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>HTTP2 demo</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div id="content"></div>
<script src="script.js"></script>
</body>
</html>
複製程式碼
// www/script.js
const content = document.querySelector("#content");
content.innerHTML = `<h1>Hello HTTP/2</h1>`;
複製程式碼
/* www/styles.css */
h1 {
color: cornflowerblue;
}
複製程式碼
建立如上的專案結構
3. 建立伺服器
const http2 = require("http2");
const fs = require("fs");
const path = require("path");
const server = http2.createSecureServer({
key: fs.readFileSync(
path.resolve(__dirname, "../certificate/localhost-privkey.pem")
),
cert: fs.readFileSync(
path.resolve(__dirname, "../certificate/localhost-cert.pem")
)
});
server.on("error", err => console.error(err));
server.on("stream", (stream, headers) => {
// stream is a Duplex
stream.respond({
"content-type": "text/html",
":status": 200
});
stream.end("<h1>Hello World</h1>");
});
server.listen(8443);
複製程式碼
開啟控制檯,進入 Network ,開啟 Protocol 顯示
訪問 https://localhost:8443/ ,即可看到協議變為 h2
4. 啟用伺服器端推送
const http2 = require("http2");
const fs = require("fs");
const path = require("path");
const pemPath = path.resolve(__dirname, "../certificate/localhost-privkey.pem");
const certPaht = path.resolve(__dirname, "../certificate/localhost-cert.pem");
// 獲取 HTTP2 header 常量
const { HTTP2_HEADER_PATH, HTTP2_HEADER_STATUS } = http2.constants;
// 獲取靜態目錄下的所有檔案資訊
function createFileInfoMap() {
let fileInfoMap = new Map();
const fileList = fs.readdirSync(staticPath);
const contentTypeMap = {
js: "application/javascript",
css: "text/css",
html: "text/html"
};
fileList.forEach(file => {
const fd = fs.openSync(path.resolve(staticPath, file), "r");
const contentType = contentTypeMap[file.split(".")[1]];
const stat = fs.fstatSync(fd);
const headers = {
"content-length": stat.size,
"last-modified": stat.mtime.toUTCString(),
"content-type": contentType
};
fileInfoMap.set(`/${file}`, {
fd,
headers
});
});
return fileInfoMap;
}
// 定義靜態目錄
const staticPath = path.resolve(__dirname, "../www");
const fileInfoMap = createFileInfoMap();
// 將傳入的檔案推送到瀏覽器
function push(stream, path) {
const file = fileInfoMap.get(path);
if (!file) {
return;
}
stream.pushStream({ [HTTP2_HEADER_PATH]: path }, (err, pushStream) => {
pushStream.respondWithFD(file.fd, file.headers);
});
}
const server = http2.createSecureServer({
key: fs.readFileSync(pemPath),
cert: fs.readFileSync(certPaht)
});
server.on("error", err => console.error(err));
server.on("stream", (stream, headers) => {
// 獲取請求路徑
let requestPath = headers[HTTP2_HEADER_PATH];
// 請求到 '/' 的請求返回 index.html
if (requestPath === "/") {
requestPath = "/index.html";
}
// 根據請求路徑獲取對應的檔案資訊
const fileInfo = fileInfoMap.get(requestPath);
if (!fileInfo) {
stream.respond({
[HTTP2_HEADER_STATUS]: 404
});
stream.end("Not found");
}
// 訪問首頁時同時推送其他檔案資源
if (requestPath === "/index.html") {
for (let key in fileInfoMap.keys()) {
push(stream, key);
}
}
// 推送首頁資料
stream.respondWithFD(fileInfo.fd, {
...fileInfo.headers
});
});
server.listen(8443);
複製程式碼
訪問 https://localhost:8443 就可以看到 styles.css 和 script.js 是通過 HTTP/2 推送過來的