0x01 Node.js 基礎
(1)概述
-
Node.js 官網:https://nodejs.org
-
Node.js 是一個基於 V8 引擎的 Javascript 執行環境
-
特性:
- 完全使用 Javascript 語法
- 具有超強的併發能力,實現高效能伺服器
- 開發週期短、開發成本低、學習成本低
-
Node.js 可以解析 Javascript 程式碼,提供很多系統級別的 API
-
檔案讀寫
const fs = require('fs') fs.readFile('./text.txt', 'utf-8', (err, content) => { console.log(content) })
-
程序管理
function main(argv) { console.log(argv) } main(process.argv.slice(2))
-
網路通訊
const http = require('http') http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }) res.write("Hello, Node.js") res.end() }).listen(3000)
-
(2)環境搭建
-
在官網下載最新版 Node.js 並安裝
-
使用命令
node --version
確認安裝是否成功 -
建立一個目錄,其中新建 index.js
console.log("Hello, world");
-
在該目錄下,使用命令
node .\index.js
檢視執行結果
(3)模組化
a. CommonJS 規範
-
Node.js 支援模組化開發,採用 CommonJS 規範
-
CommonJS 包括:modules、packages、system、filesystems、binary、console、cncodings、sockets 等
-
模組化是將公共的功能抽離成為一個單獨 Javascript 檔案作為一個模組,模組可以透過暴露其中的屬性或方法,從而讓外部進行訪問
-
customModule.js
const name = "John" const getName = () => { console.log(name) } // 暴露方法一 module.exports = { getName: getName, } // 暴露方法二 exports.getName = getName
-
index.js
const customModule = require('./customModule') customModule.getName()
-
b. ES 模組化寫法
-
在根目錄建立目錄 module,其中新建 mod.js
const module = {}; export default module;
-
修改 index.js,其中使用 ES 寫法引入 module/mod.js
import module from "./module/mod.js"; console.log(module);
此時執行 index.js 後會報錯,可以透過修改配置檔案解決
-
修改 package.json
{ // ... "type": "module" }
此時可以正常執行 index.js
-
修改 mod.js,實現多個暴露
const module1 = { get() { return "module1"; }, }; const module2 = { get() { return "module2"; }, }; export { module1, module2, };
-
修改 index.js
import { module1 } from "./module/mod.js"; import { module2 } from "./module/mod.js"; console.log(module1.get(), module2.get());
(4)npm & yarn
-
npm
npm init
:初始化新專案,會生成一個 package.json 檔案,其中包含專案資訊- 使用命令
npm install
時,依賴項版本號,如"^1.1.1"
,其中的特殊符號含義如下:^
:安裝同名包~
:安裝同版本,即安裝 1.1.**
:安裝最新版
- 使用命令
npm [install/uninstall/update] [package_name] -g
:全域性安裝(或解除安裝、更新)依賴npm [install/uninstall/update] [package_name] --save-dev
:區域性安裝(或解除安裝、更新)依賴npm install [package_name]@[version/latest]
:安裝指定版本(或最新版本)的依賴npm list (-g)
:列舉當前目錄(或全域性)依賴npm info [package_name] (version)
:檢視指定依賴的詳細資訊(以及版本)npm outdated
:檢查依賴是否過時
-
nrm
NRM(Npm Registry Manager)是 npm 的映象原管理工具
- 使用命令
npm install -g nrm
全域性安裝 nrm - 使用命令
nrm ls
檢視可用源,*
表示當前使用的源 - 使用命令
nrm use xxx
切換到 xxx 源 - 使用命令
nrm test
測試源的響應時間
- 使用命令
-
yarn
- 相比 npm,yarn 具有安裝速度快、保證安裝包完整安全
- 使用命令
npm install -g yarn
全域性安裝 yarn
yarn init
:初始化新專案yarn add [package_name](@version) (--dev)
:安裝指定依賴yarn upgrade [package_name]@[version]
:升級指定依賴yarn remove [package_name]
:移除指定依賴yarn install
:安裝專案所有依賴
(5)內建模組
使用命令
npm install -g nodemon
全域性安裝 nodemon,用於熱更新
a. url
-
parse
方法:將 URL 進行語法分析成物件// 匯入 url 模組 const url = require("url"); // 宣告字串 const urlString = "http://localhost:8000/home/index.html?id=1&name=John#page=10"; // 處理字串並輸出 console.log(url.parse(urlString));
-
format
方法:將物件格式化成 URLconst url = require("url"); // 宣告物件 const urlObject = { protocol: "http:", slashes: true, auth: null, host: "localhost:8000", port: "8000", hostname: "localhost", hash: "#page=10", search: "?id=1&name=John", query: "id=1&name=John", pathname: "/home/index.html", path: "/home/index.html?id=1&name=John", }; console.log(url.format(urlObject));
-
resolve
方法:const url = require("url"); // 宣告並處理字串 let a = url.resolve("/api/v1/users", "id"); // /api/v1/id let b = url.resolve("http://localhost:8000/", "/api"); // http://localhost:8000/api let c = url.resolve("http://localhost:8000/api", "/v1"); // http://localhost:8000/v1 console.log(`${a}\n${b}\n${c}`);
b. querystring
-
parse
方法:// 匯入 querystring 模組 const querystring = require("querystring"); // 宣告、處理字串並輸出處理結果 console.log(querystring.parse("name=John&age=18"));
-
stringify
方法:const querystring = require("querystring"); console.log( querystring.stringify({ name: "John", age: 18, }) );
-
escape
/unescape
方法:將特殊字元轉義const querystring = require("querystring"); console.log(querystring.escape("http://localhost:8000/name=張三&age=18"));
const querystring = require("querystring"); console.log(querystring.unescape("http%3A%2F%2Flocalhost%3A8000%2Fname%3D%E5%BC%A0%E4%B8%89%26age%3D18"));
c. http
I. 基礎
-
index.js
// 匯入 http 模組 const http = require("http"); // 匯入自定義模組 const moduleRenderHTML = require("./module/renderHTML"); const moduleRenderStatus = require("./module/renderStatus"); // 建立本地伺服器 const server = http.createServer(); // 開啟本地伺服器 server.on("request", (req, res) => { // req 瀏覽器請求, res 伺服器響應 // 新增響應頭 res.writeHead(moduleRenderStatus.renderStatus(req.url), { "Content-Type": "text/html;charset=utf-8", }); // 根據 URL 返回不同的內容 res.write(moduleRenderHTML.renderHTML(req.url)); // 結束響應 res.end( JSON.stringify({ data: "Hello, world!", }) ); }); // 執行並監聽埠 server.listen(8000, () => { console.log("Server is running on port 8000"); });
-
module/renderHTML.js
function renderHTML(url) { switch (url) { case "/": return ` <html> <h1>Node.js</h1> <p>URL: root</p> </html> `; case "/api": return ` <html> <h1>Node.js</h1> <p>URL: api</p> </html>`; default: return ` <html> <h1>Node.js</h1> <p>URL: 404 Not Found</p> </html>`; } } module.exports = { renderHTML };
-
module/renderStatus.js
function renderStatus(url) { const routes = ["/", "/api"]; return routes.includes(url) ? 200 : 404; } exports.renderStatus = renderStatus;
-
使用命令
nodemon .\index.js
執行,並訪問 http://localhost:8000/ 或 http://localhost:8000/api
II. cors
-
修改 index.js,設定服務端允許跨域請求
// 匯入內建模組 const http = require("http"); const qs = require("querystring"); const url = require("url"); const server = http.createServer(); server.on("request", (req, res) => { let data = ""; let urlObject = url.parse(req.url, true); res.writeHead(200, { "Content-Type": "application/json;charset=utf-8", "Access-Control-Allow-Origin": "*", }); req.on("data", (chunk) => { data += chunk; }); req.on("end", () => { responseResult(qs.parse(data)); }); function responseResult(data) { switch (urlObject.pathname) { case "/api/login": res.end(JSON.stringify({ msg: data })); break; default: res.end(JSON.stringify({ msg: "error" })); } } }); server.listen(8000, () => { console.log("Server is running on port 8000"); });
-
修改 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>Document</title> </head> <body> <script> fetch("http://localhost:8000/api/login") .then((res) => res.json()) .then((res) => console.log(res.msg)); </script> </body> </html>
III. https.get
JSONP
const http = require("http");
const https = require("https");
const server = http.createServer();
server.on("request", (req, res) => {
res.writeHead(200, {
"Content-Type": "application/json; charset=utf-8",
"Access-Control-Allow-Origin": "*",
});
let data = "";
// 使用 JSONP 方法利用 GET 請求進行跨域
https.get(`https://www.baidu.com/sugrec?...&wd=John`, (res) => {
res.on("data", (chunk) => {
data += chunk;
});
res.on("end", () => {
console.log(data);
});
});
});
server.listen(8000, () => {
console.log("Server is running on port 8000");
});
IV. https.post
-
修改 index.js
const http = require("http"); const https = require("https"); const server = http.createServer(); server.on("request", (req, res) => { res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Access-Control-Allow-Origin": "*", }); // POST 請求 let data = ""; let request = https.request( { hostname: "m.xiaomiyoupin.com", port: "443", path: "/mtop/market/search/placeHolder", method: "POST", headers: { "Content-Type": "application/json", }, }, (response) => { response.on("data", (chunk) => { data += chunk; }); response.on("end", () => { res.end(data); }); } ); request.write(JSON.stringify([{}, { baseParam: { ypClient: 1 } }])); request.end(); }); server.listen(8000, () => { console.log("Server is running on port 8000"); });
-
修改 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>Document</title> </head> <body> <script> fetch("http://localhost:8000/") .then((res) => res.json()) .then((res) => console.log(res)); </script> </body> </html>
d. event
// 匯入 events 模組
const EventEmitter = require("events");
// 建立自定義事件類並繼承內建模組 events
class MyEventEmitter extends EventEmitter {}
// 宣告例項
const event = new MyEventEmitter();
// 監控 output 事件並執行回撥函式
event.on("output", (name) => {
console.log(name);
});
// 觸發事件
event.emit("output", "John");
event.emit("output", "Mary");
e. fs
匯入 fs 模組:
const fs = require("fs");
I. 資料夾操作
-
建立
fs.mkdir("./newFolder", (err) => { if (err && err.code === "EEXIST") { console.log("The folder already exists"); } else { console.log("The folder has been created"); } });
-
重新命名
fs.renamedir("./newFolder", "./folder", (err) => { if (err && err.code === "ENOENT") { console.log("The folder does not exist"); } else { console.log("The folder renamed successfully"); } });
-
刪除
fs.rmdir("./folder", (err) => { if (err && err.code === "ENOENT") { console.log("The folder does not exist"); } else if (err && err.code === "ENOTEMPTY") { console.log("The folder is not empty"); } else { console.log("The folder deleted successfully"); } });
-
內容檢視
fs.readdir("./folder", (err, data) => { if (err) { console.log(err); } else { console.log(data); } });
-
屬性檢視
fs.stat("./folder", (err, data) => { console.log(data) // 判定是否為檔案型別 console.log(data.isFile()) // 判定是否為資料夾型別 console.log(data.isDirectoty()) })
II. 檔案操作
-
建立與覆寫
fs.writeFile("./folder/text.txt", "Hello, world!", (err) => { console.log(err); });
-
續寫
fs.appendFile("./folder/text.txt", "Hello, world!", (err) => { console.log(err); });
-
讀取
fs.readFile("./folder/text.txt", (err, data) => { if (err) { console.log(err); } else { console.log(data.toString("utf-8")); } });
-
刪除
fs.unlink("./folder/text.txt", (err) => { console.log(err); });
III. 同步操作
-
上述資料夾操作與檔案操作均採用非同步方式操作
-
fs 模組也提供了同步操作方法,但是如果發生錯誤則會阻塞程式進行
-
舉例:同步讀取檔案
console.log("Before reading"); console.log("Content: " + fs.readFileSync("./folder/text.txt", "utf-8")); console.log("After reading");
此時,當 folder/text.txt 不存在時,程式會在第三行停止並退出,無法繼續執行
f. stream
-
在 Node.js 中,stream 是一個物件,使用時只需響應 stream 的事件即可,以下是常見事件:
data
:stream 的資料可讀,其中每次傳遞的chunk
是 stream 的部分資料end
:stream 的資料到結尾error
:出錯
-
舉例:使用 stream 的事件讀取檔案
const fs = require("fs"); let stream = fs.createReadStream("./folder/text.txt", "utf-8"); stream.on("data", (chunk) => { console.log("Data: ", chunk); }); stream.on("end", () => { console.log("End of file"); }); stream.on("error", (err) => { console.log("Error: ", err); });
-
pipe
方法可以將兩個 stream 串起來 -
舉例:檔案複製
const fs = require("fs"); let readStream = fs.createReadStream("./folder/text.txt", "utf-8"); let writeStream = fs.createWriteStream("./folder/text-copy.txt", "utf-8"); readStream.pipe(writeStream);
g. zlib
-
用於將靜態資原始檔壓縮,從伺服器傳到瀏覽器,減少頻寬使用
-
舉例:檔案複製並壓縮,進行對比
const fs = require("fs"); const zlib = require("zlib"); let readStream = fs.createReadStream("./folder/text.txt", "utf-8"); let writeStream = fs.createWriteStream("./folder/text-copy.txt", "utf-8"); let gzip = zlib.createGzip(); readStream.pipe(gzip).pipe(writeStream);
h. crypto
-
提供通用的加密和雜湊演算法
-
舉例:
-
MD5/SHA-1
// 匯入 crypto 模組 const crypto = require("crypto"); // 設定加密演算法 const hash = crypto.createHash("md5"); // SHA-1 // const hash = crypto.createHash("sha1"); // 將字元以 UTF-8 編碼傳入 Buffer hash.update("hello world"); // 計算並輸出 console.log(hash.digest("hex"));
-
HMAC
const crypto = require("crypto"); // 設定加密演算法以及金鑰 const hmac = crypto.createHash("sha256", "secret-key"); hmac.update("hello world"); console.log(hmac.digest("hex"));
-
AES
const crypto = require("crypto"); function encrypt(key, iv, data) { // key 是加密金鑰(256位) | iv 是初始化向量(16位元組) | data 是明文(二進位制字串) // 建立一個AES-256-CBC模式的加密器 const decipher = crypto.createCipheriv("aes-256-cbc", key, iv); // 使用加密器對資料進行加密,返回加密結果 let encrypted = decipher.update(data, "binary", "hex"); // 新增加密器的最終加密結果 encrypted += decipher.final("hex"); // 返回密文(十六進位制字串) return encrypted; } function decrypt(key, iv, crypted) { // key 是解密金鑰(長度32的Buffer物件) | iv 是初始化向量(長度16的Buffer物件) | crypted 是密文(16進位制字串) // 將加密資料從16進位制字串轉換為二進位制字串 crypted = Buffer.from(crypted, "hex").toString("binary"); // 建立解密器,使用AES-256-CBC模式 const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv); // 初始化解密過程,將解密結果轉換為UTF-8字串 let decrypted = decipher.update(crypted, "binary", "utf8"); // 結束解密過程,將剩餘的加密資料解密,併合併到解密結果中 decrypted += decipher.final("utf8"); // 返回明文(UTF-8字串) return decrypted; }
-
(6)路由
a. 基礎路由
目錄結構:
graph TB ./-->static & router.js & index.js static-->index.html & 404.html
-
編寫頁面
-
編寫路由:router.js
const fs = require("fs"); const path = require("path"); // 定義路由規則 const rules = { "/": (req, res) => { render(res, path.join(__dirname, "/static/index.html")); }, "/404": (req, res) => { render(res, path.join(__dirname, "/static/404.html")); }, }; // 渲染路由頁面 function render(res, path) { res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.write(fs.readFileSync(path, "utf8")); res.end(); } // 完成路由規則 function routes(req, res, pathname) { if (pathname in rules) { rules[pathname](req, res); } else { rules["/404"](req, res); } } module.exports = { routes, };
path.join(__dirname, "/static/404.html")
:獲取靜態資源方法
-
編寫服務端:index.js
const http = require("http"); const router = require("./router"); const server = http.createServer(); server.on("request", (req, res) => { const myURL = new URL(req.url, "http://127.0.0.1"); router.routes(req, res, myURL.pathname); res.end(); }); server.listen(8000, () => { console.log("Server is running on port 8000"); });
b. 獲取引數
I. GET
-
在頁面中建立表單併傳送請求
<body> <form> <lable>Username: <input type="text" name="username" /></lable> <lable>Password: <input type="password" name="password" /></lable> <input type="button" onclick="submit()" value="submit" /> </form> <script> let form = document.querySelector("form"); form.submit = () => { let username = form.elements.username.value; let password = form.elements.password.value; fetch(`http://localhost:8000/?username=${username}&password=${password}`) .then((res) => res.json()) .then((res) => console.log(res)); }; </script> </body>
-
修改 router.js
const fs = require("fs"); const path = require("path"); const rules = { "/": (req, res) => { const myURL = new URL(req.url, "http://127.0.0.1"); const params = myURL.searchParams; // 獲取引數 render(res, `{ "username": ${params.get("username")}, "password": ${params.get("password")}}`); }, "/404": (req, res) => { const data = fs.readFileSync(path.join(__dirname, "/static/404.html"), "utf8"); render(res, data, "text/html"); }, }; function render(res, data, type) { res.writeHead(200, { "Content-Type": `${type ? type : "application/json"}; charset=utf-8`, "Access-Control-Allow-Origin": "*", // 允許跨域請求 }); res.write(data); res.end(); } function routes(req, res, pathname) { if (pathname in rules) { rules[pathname](req, res); } else { rules["/404"](req, res); } } module.exports = { routes, };
II. POST
-
調整請求方法
<script> let form = document.querySelector("form"); form.submit = () => { let username = form.elements.username.value; let password = form.elements.password.value; fetch(`http://localhost:8000/`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ username, password, }), }) .then((res) => res.json()) .then((res) => console.log(res)); }; </script>
-
修改 router.js
"/": (req, res) => { let data = "" req.on("data", (chunk) => { data += chunk; }) req.on("end", () => { render(res, `{ "data": ${data} }`); }) },
0x02 Express
(1)概述
a. 簡介
- Express 官網連結:https://www.expressjs.com.cn/
- Express 是基於 Node.js 平臺的 Web 開發框架
- 特點:
- 極簡、靈活地建立各種 Web 和移動應用
- 具有豐富的 HTTP 實用工具和中介軟體來建立強大的 API
- 在 Node.js 基礎上擴充套件 Web 應用所需的基本功能
b. 第一個專案
-
使用命令
npm install express --save
安裝 Express -
在根目錄下建立 index.js
// 匯入 express 模組 const express = require("express"); // 建立一個 express 應用 const app = express(); // 設定應用監聽的埠 const port = 3000; // 處理根路徑的GET請求 app.get("/", (req, res) => res.send("Hello World!")); // 監聽埠並輸出日誌 app.listen(port, () => console.log(`Example app listening on port ${port}!`));
-
使用命令
node .\index.js
執行專案 -
訪問 http://localhost:3000/
c. 路由
-
基礎路由
const express = require("express"); const app = express(); app.get("/", (req, res) => res.send("Hello World!")); app.get("/html", (req, res) => res.send(`<h1>Hello World!</h1>`)); app.get("/json", (req, res) => res.send({ msg: "Hello World!" }));
-
字串模式
-
字元可選
app.get("/ab?cd", (req, res) => res.send("Hello World!"));
- http://localhost:3000/acd
- http://localhost:3000/abcd
-
引數捕獲
app.get("/abcd/:id", (req, res) => res.send("Hello World!"));
- http://localhost:3000/abcd/123
- http://localhost:3000/abcd/abc
-
重複字元
app.get("/ab+cd", (req, res) => res.send("Hello World!"));
- http://localhost:3000/abcd
- http://localhost:3000/abbbbcd
-
任意字元
app.get("/ab*cd", (req, res) => res.send("Hello World!"));
- http://localhost:3000/abcd
- http://localhost:3000/ab1234cd
-
-
正規表示式
-
含指定字元
app.get(/a/, (req, res) => res.send("Hello World!"));
- http://localhost:3000/ab
- http://localhost:3000/12ab
-
指定字元(串)結尾
app.get(/.*a$/, (req, res) => res.send("Hello World!"));
- http://localhost:3000/bcda
- http://localhost:3000/123a
-
-
回撥函式
-
多個回撥函式
app.get( "/", (req, res, next) => { console.log("Next"); next(); }, (req, res) => { res.send("Hello World!"); } );
-
回撥函式陣列
let callback_1 = function (req, res, next) { console.log("Callback 1"); next(); }; let callback_2 = function (req, res, next) { console.log("Callback 2"); next(); }; let callback_3 = function (req, res) { res.send("Hello World!"); }; app.get("/", [callback_1, callback_2, callback_3]);
-
(2)中介軟體
- Express 是由路由和中介軟體構成的 Web 開發框架,本質上,Express 應用在呼叫各種中介軟體
- 中介軟體(Middleware)是一個函式,功能包括:
- 執行邏輯
- 修改請求和響應物件
- 終結請求-響應迴圈
- 呼叫堆疊中下一個中介軟體
- 如果當前中介軟體未終結請求-響應迴圈,則需要透過
next()
方法將控制權傳遞到下一個中介軟體,否則請求會被掛起
a. 應用級中介軟體
-
一般繫結到
express()
上,使用use()
和METHOD()
const express = require("express"); const app = express(); const port = 3000; // 中介軟體 app.use((req, res, next) => { req.text = "Hello, world!"; next(); }); app.get("/", (req, res) => { res.send(req.text); }); app.listen(port, () => console.log(`Example app listening on port ${port}!`));
- http://localhost:3000/
-
中介軟體的註冊位置需要注意
app.get("/p1", (req, res) => { res.send(req.text); }); app.use((req, res, next) => { req.text = "Hello, world!"; next(); }); app.get("/p2", (req, res) => { res.send(req.text); });
- http://localhost:3000/p1
- http://localhost:3000/p2
-
可以指定某一路由使用該中介軟體
app.use("/p2", (req, res, next) => { req.text = "Hello, world!"; next(); }); app.get("/p1", (req, res) => { res.send(req.text); }); app.get("/p2", (req, res) => { res.send(req.text); });
- http://localhost:3000/p1
- http://localhost:3000/p2
b. 路由級中介軟體
-
功能與應用級中介軟體類似,繫結到
express.Router()
上const express = require("express"); const app = express(); const router = express.Router(); const port = 3000; router.use((req, res, next) => { req.text = "Hello, world!"; next(); }) router.get("/", (req, res) => { res.send(req.text); }); app.use("/", router); app.listen(port, () => console.log(`Example app listening on port ${port}!`));
- http://localhost:3000/
c. 錯誤處理中介軟體
-
需要引入
err
引數app.get('/', (req, res) => { throw new Error("Error") }) app.use((err, req, res, next) => { console.error(err.stack); res.status(500).send('Something broke!'); });
- http://localhost:3000/
e. 內建中介軟體
-
express.static
是 Express 唯一內建的中介軟體,負責在 Express 應用中提供託管靜態資源app.use(express.static('public'));
f. 第三方中介軟體
-
安裝相應的模組並在 Express 應用中載入
const app = express(); const router = express.Router(); const xxx = require('xxx'); // 應用級載入 app.use(xxx); // 路由級載入 router.use(xxx);
(3)獲取引數
-
GET
app.get("/", (req, res) => { console.log(req.query); res.send(req.query); });
- http://localhost:3000/
- http://localhost:3000/?username=John&password=123456
-
POST
// 內建用於解析 POST 引數 app.use(express.urlencoded({ extended: false })); app.post("/", (req, res) => { console.log(req.body); res.send(req.body); });
- 使用 Postman 向 http://localhost:3000/ 傳送 POST 請求,引數配置在 Body 中,使用 x-www-form-urlencoded
(4)服務端渲染
-
服務端渲染(SSR):前端傳送請求,伺服器端從資料庫中拿出資料,透過渲染函式,把資料渲染在模板(ejs)裡,產生了HTML程式碼,之後把渲染結果發給了前端,整個過程只有一次互動
- 客戶端渲染(BSR/CSR):在服務端放了一個 HTML 頁面,客戶端發起請求,服務端把頁面傳送過去,客戶端從上到下依次解析,如果在解析的過程中,發現 Ajax 請求,再次向伺服器傳送新的請求,客戶端拿到 Ajax 響應的結果並渲染在頁面上,這個過程中至少和服務端互動了兩次
SSR 與 BSR 說明參考:《客戶端渲染(BSR:Browser Side Render)、服務端渲染(SSR:Server Side Render)、搜尋引擎最佳化、SEO(Search Engine Optimization) | CSDN-Ensoleile 2021》
-
設定使用 Express 渲染模板檔案
-
使用命令
npm install ejs
安裝模板引擎 -
在根目錄建立目錄 views
- views 中包含模板檔案,如 index.ejs 等
-
修改 index.js
app.set("views", "./views"); app.set("view engine", "ejs");
-
修改
app.set("view engine", "ejs");
,使其可以直接渲染 HTML 檔案app.set("view engine", "html"); app.engine("html", require("ejs").renderFile);
-
-
-
標籤語法
<% %>
:流程控制標籤<%= %>
:輸出 HTML 標籤<%- %>
:輸出解析 HTML 標籤<%# %>
:註釋標籤<%- include('/', {key: value}) %>
:匯入公共模板
(5)生成器
-
使用命令
npm install -g express-generatore
全域性安裝 Express 生成器 -
使用命令
express myapp --view=ejs
生成 Express 專案- 上述命令不可用時,可以使用命令
npx express-generator --view=ejs myapp
- 上述命令不可用時,可以使用命令
-
使用命令
cd myapp
進入專案目錄 -
使用命令
npm install
安裝相關依賴 -
修改 package.json,使用熱更新
{ "scripts": { "start": "nodemon ./bin/www" }, }
-
使用命令
npm start
啟動專案 -
訪問 http://localhost:3000/
0x03 MongoDB
(1)概述
詳見 《MongoDB | 部落格園-SRIGT》
(2)使用 Node.js 操作
-
在根目錄新建 config 目錄,其中新建 db.config.js,用於連線資料庫
const mongoose = require("mongoose"); mongoose.connect("mongodb://127.0.0.1:27017/node_project");
-
在 bin/www 中匯入 db.config.js,用於匯入資料庫配置
require("../config/db.config");
-
在根目錄新建 model 目錄,其中新建 UserModel.js,用於建立 User 模型
const mongoose = require("mongoose"); const UserType = { name: String, username: String, password: String, }; const UserModel = mongoose.model("user", new mongoose.Schema(UserType)); module.exports = UserModel;
-
修改 routes/users.js,增加資料
const UserModel = require("../model/UserModel"); router.get("/", function (req, res, next) { const { name, username, password } = req.body; UserModel.create({ name, username, password }).then((data) => { console.log(data); }); res.send("respond with a resource"); });
-
查詢資料
UserModel.find( { name: "John" }, ["username", "password"].sort({ _id: -1 }).skip(10).limit(10) );
-
更新資料(單條)
const { name, username, password } = req.body; UserModel.updateOne({ _id }, {username, password});
-
刪除資料(單條)
UserModel.deleteOne({ _id });
0x04 介面規範與業務分層
(1)介面規範
-
採用 RESTful 介面規範
-
REST 風格 API 舉例:
GET http://localhost:8080/api/user (查詢使用者) POST http://localhost:8080/api/user (新增使用者) PUT http://localhost:8080/api/user/{id} (更新使用者) DELETE http://localhost:8080/api/user (刪除使用者)
-
使用通用欄位實現資訊過濾
?limit=10
:指定返回記錄的數量?offset=10
:指定返回記錄的開始位置?page=1&per_page=10
:指定第幾頁以及每頁的記錄數?sortby=id&order=asc
:指定排序欄位以及排序方式?state=close
:指定篩選條件
(2)業務分層
-
採用 MVC 架構
graph TB index.js-->router.js-->controller-->view & model- index.js 是伺服器入口檔案,負責接收客戶端請求
- router.js 是路由,負責將入口檔案的請求分發給控制層
- controller 是控制器(C),負責處理業務邏輯
- view 是檢視(V),負責頁面渲染與展示
- model 是模型(M),負責資料的增刪改查
(3)實際應用
-
修改 myapp 專案
目錄結構:
graph TB myapp-->config & controller & model & routes & services & ... config-->db.config.js controller-->UserController.js model-->UserModel.js routes-->index.js & users.js services-->UserService.js-
UserModel.js
const mongoose = require("mongoose"); const UserType = { name: String, username: String, password: String, }; const UserModel = mongoose.model("user", new mongoose.Schema(UserType)); module.exports = UserModel;
-
UserService.js
const UserModel = require("../models/UserModel"); const UserService = { addUser: (name, username, password) => { return UserModel.create({ name, username, password }).then((data) => { console.log(data); }); }, updateUser: (name, username, password) => { return UserModel.updateOne( { _id: req.params.id }, { name, username, password } ); }, deleteUser: (_id) => { return UserModel.deleteOne({ _id }); }, getUser: (page, limit) => { return UserModel.find({}, ["name", "username"]) .sort({ _id: -1 }) .skip((page - 1) * limit) .limit(limit); }, }; module.exports = UserService;
-
UserController.js
const UserController = { addUser: async (req, res) => { const { name, username, password } = req.body; await UserService.addUser(name, username, password); res.send({ message: "User created successfully", }); }, updateUser: async (req, res) => { const { name, username, password } = req.body; await UserService.updateUser(req.params.id, name, username, password); res.send({ message: "User updated successfully", }); }, deleteUser: async (req, res) => { await UserService.deleteUser(req.params.id); res.send({ message: "User deleted successfully", }); }, getUsers: async (req, res) => { const { page, limit } = req.query; const users = await UserService.getUsers(page, limit); res.send(users); }, }; module.exports = UserController;
-
users.js
router.post("/user", UserController.addUser); router.put("/user/:id", UserController.updateUser); router.delete("/user/:id", UserController.deleteUser); router.get("/user", UserController.getUsers);
-
0x05 登入鑑權
(1)Cookie 與 Session
-
時序圖
sequenceDiagram 瀏覽器->>服務端:POST賬號密碼 服務端->>庫(User):校驗賬號密碼 庫(User)-->>服務端:校驗成功 服務端->>庫(Session):存Session 服務端-->>瀏覽器:Set-Cookie:sessionId 瀏覽器->>服務端:請求介面(Cookie:sessionId) 服務端->>庫(Session):查Session 庫(Session)-->>服務端:校驗成功 服務端->>服務端:介面處理 服務端-->>瀏覽器:介面返回
-
修改 app.js,其中引入 Session 與 Cookie 的配置
// 引入express-session中介軟體和connect-mongo儲存模組 const session = require("express-session"); const MongoStore = require("connect-mongo"); // 配置session app.use( session({ secret: "secret-key", // session加密金鑰 resave: true, // 是否在每次請求時重新儲存session,即使它沒有變化 saveUninitialized: true, // 是否儲存未初始化的session(即僅設定了cookie但未設定session資料的情況) cookie: { maxAge: 1000 * 60 * 10, // cookie的過期時間(毫秒) secure: false, // 是否僅在https下傳送cookie }, rolling: true, // 是否在每次請求時重置cookie的過期時間 store: MongoStore.create({ mongoUrl: "mongodb://127.0.0.1:27017/node_project_session", ttl: 1000 * 60 * 10, // session在資料庫中的存活時間(毫秒) }), }) );
-
修改 UserController.js,其中設定 Session 物件
const UserController = { // ... login: async (req, res) => { const { username, password } = req.body; const data = await UserService.login(username, password); if (data.length === 0) { res.send({ message: "Invalid username or password", }); } else { req.session.user = data[0]; // 設定 Session 物件 res.send({ message: "Login successful", data, }); } } }; module.exports = UserController;
-
修改 routes/index.js,其中判斷 Session
router.get("/", function (req, res, next) { if (req.session.user) { res.render("index", { title: "Express" }); } else { res.render("/login"); } });
此時,如果 Session 儲存在記憶體中,當伺服器重啟後,Session 就會丟失;而儲存在資料庫中就不會丟失
-
修改 app.js,透過應用級中介軟體,將 Session 校驗配置到全域性
app.use((req, res, next) => { if (req.url.includes("login")) { next(); return; } if (req.session.user) { next(); } else { req.url.includes("api") ? res.send({ code: 401, message: "Please login first" }) : res.redirect("/login"); } });
-
修改 routes/users.js
router.post("/login", UserController.login); router.get("/logout", (req, res) => { req.session.destroy(() => { res.send({ message: "Logout successful", }); }); });
(2)JWT
a. 概述
-
JWT 全稱 JSON Web Token
-
優勢:
- 不需要儲存 Session ID,節約儲存空間
- 具有加密簽名,校驗結果高效準確
- 預防 CSRF 攻擊
-
缺點:
- 頻寬佔用多,開銷大
- 無法在服務端登出,有被劫持的問題
- 效能消耗大,不利於效能要求嚴格的 Web 應用
b. 實現
僅限於前後端分離專案使用
-
使用命令
npm install -S jsonwebtoken
安裝 JWT -
根目錄下新建 util 目錄,其中新建 jwt.js,用於封裝 JWT
const jsonwebtoken = require("jsonwebtoken"); const secret = "secret-key"; const JWT = { generate(value, exprires) { return jsonwebtoken.sign(value, secret, { expriresIn: exprires }); }, verify(token) { try { return jsonwebtoken.verify(token, secret); } catch (err) { return false; } }, }; module.exports = JWT;
-
修改 UserController.js,設定 JWT 物件
const JWT = require("../util/jwt"); const UserController = { // ... login: async (req, res) => { // ... if (data.length === 0) { res.send({ message: "Invalid username or password", }); } else { const token = JWT.generate( { _id: data._id, username: data[0].username, }, "1h" ); // 設定 JWT 物件 res.header("Authorization", token); // 將 token 放入響應頭 // ... } }, }; module.exports = UserController;
-
在前端使用 Axios 的攔截器,將 token 儲存在瀏覽器本地儲存
-
修改 app.js,使用中介軟體校驗
app.use((req, res, next) => { if (req.url.includes("login")) { next(); return; } const token = req.headers["authorization"]?.split(" ")[1]; if (token) { const payload = JWT.verify(token); if (payload) { // token 重新計時 const newToken = JWT.generate( { _id: payload._id, username: payload.username, }, "1h" ); res.headers("Authorization", newToken); next(); } else { res.send({ code: 401, message: "Token has expired" }); } } else { next(); } });
0x06 apiDoc
-
apiDoc 是一個簡單的 RESTful API 文件生成工具
- 透過程式碼註釋提取特定格式的內容生成文件
- 支援 Go、Java、C++、Rust 等大部分開發語言,可透過命令
apidoc lang
檢視所有支援的列表
-
特點:
- 跨平臺支援
- 多程式語言相容
- 輸出模板自定義
- 根據文件生成 mock 資料
-
使用命令
npm install -g apidoc
全域性安裝 apiDoc -
apiDoc 註釋格式:
/** * @api {get} /user/:id Request User information * @apiName GetUser * @apiGroup User * * @apiParam {Number} id Users unique ID. * * @apiSuccess {String} firstname Firstname of the User. * @apiSuccess {String} lastname Lastname of the User. */
-
修改 routes\users.js
// ... /** * * @api {post} /api/user user * @apiName addUser * @apiGroup user * @apiVersion 1.0.0 * * * @apiParam {String} name 姓名 * @apiParam {String} username 使用者名稱 * @apiParam {String} password 密碼 * * @apiSuccess (200) {String} message 描述 * * @apiParamExample {application/json} Request-Example: * { * name : "張三", * username : "法外狂徒", * password : "333" * } * * * @apiSuccessExample {application/json} Success-Response: * { * message : "User created successfully" * } * * */ router.post("/user", UserController.addUser); // ...
-
-
使用命令
apidoc -i src/ -o doc/
從 src 目錄生成 API 文件到 doc 目錄- 在專案根目錄下,使用命令
apidoc -i .\routes\ -o .\doc
- 在專案根目錄下,使用命令
-
在 VSCode 中,使用 ApiDoc Snippets 外掛可以輔助生成相應的註釋
-End-