1. 簡介
1.1 執行環境
-
瀏覽器是 js 的前端執行環境
-
Node.js 是 js 的後端執行環境
-
Node.js 中無法呼叫 DOM 和 BOM 等瀏覽器內建 API
1.2 Node.js 可以做什麼
-
基於 Express 框架可以快速構建 Web 應用
-
基於 Electron 框架可以快速構建跨平臺的桌面應用
-
基於 restify 框架可以快速構建 API 介面專案
-
讀取和運算元據庫,建立實用的命令列工具輔助前端開發
-
...
1.3 安裝與執行
-
下載穩定版node.js
-
安裝完檢視 node.js 的版本
node -v
- 建立測試檔案,透過命令列執行(需要切換到檔案所在目錄)
node test.js
2. fs 檔案系統模組
fs 模組是 Node.js 官方提供的用來操作檔案的模組,提供了一系列的方法和屬性,用來滿足使用者對檔案的操作需求
- 如果要在 js 程式碼中使用 fs 模組來操作檔案,則需要先匯入
const fs = require("fs");
2.1 讀取指定檔案中的內容
- 使用
fs.readFile()
讀取指定檔案中的內容
fs.readFile(path[, options), callback)
-
引數解讀
-
path
:必選,讀取的檔案路徑(字串) -
options
:可選,以什麼編碼格式來讀取檔案,預設指定utf8
-
callback
:必選,檔案讀取完成後,透過回撥函式拿到讀取的失敗和成功的結果,err 和 dataObj
-
-
示例:
const fs = require("fs");
fs.readFile("./files/1.txt", "utf-8", function (err, dataObj) {
// 讀取成功,err為null,否則為錯誤物件。因此能以此進行判斷
if (err) {
return console.log("檔案讀取失敗!" + err.message);
}
// 讀取成功的結果,失敗則為undefined
console.log("檔案讀取成功,內容是:" + dataObj);
});
2.2 向指定檔案中寫入內容
- 使用
fs.writeFile()
向指定檔案寫入內容
fs.writeFile(file, data[, options], callback)
-
引數解讀
-
file
:必選,檔案存放的路徑(字串) -
data
:必選,要寫入的內容 -
options
:可選,以什麼格式寫入檔案內容,預設utf8
-
callback
:必選,檔案寫入完成後的回撥函式
-
-
示例
const fs = require("fs");
fs.writeFile("F:/files/2.txt", "hello world", function (err) {
// 寫入成功,err為null,否則為錯誤物件
if (err) {
return console.log("寫入檔案失敗!" + err.message);
}
console.log("檔案寫入成功!");
});
2.3 小練習
-
需求:整理
成績.txt
中的資料,並寫入成績-ok.txt
-
源資料與期望格式資料如下:
- 程式碼實現
const fs = require("fs");
fs.readFile("./files/成績.txt", function (err, dataObj) {
if (err) {
return console.log("檔案讀取失敗!" + err.message);
}
let dataStr = dataObj.toString();
dataStr = dataStr.replaceAll("=", ":");
dataStr = dataStr.replaceAll(" ", "\n");
fs.writeFile("./files/成績-ok.txt", dataStr, function (err) {
if (err) {
return console.log("檔案寫入失敗!" + err.message);
}
});
});
2.4 路徑動態拼接的問題
-
在使用 fs 模組操作檔案時,如果使用相對路徑,很容易出現動態路徑拼接錯誤的問題
-
原因:程式碼在執行時,會以執行 node 命令所處的目錄,動態拼接出被操作檔案的完整路徑
-
解決
-
提供完整路徑:移植性差,不利於維護
-
使用
__dirname_ + '/files/data.txt'
:__dirname
表示當前檔案所在的目錄
-
-
使用相對路徑,並在檔案所在目錄上一級執行命令
- 最佳化後的程式碼
const fs = require("fs");
fs.readFile(__dirname + "/files/data.txt", function (err, dataObj) {
if (err) {
return console.log("檔案讀取失敗!" + err.message);
}
console.log(dataObj.toString());
});
3. Path 路徑模組
path 模組是 Node.js 官方提供的用來處理路徑的模組。它提供了一系列的方法和屬性,用來滿足使用者對路徑的處理需求
- 如果要在 js 程式碼中使用 path 模組來處理路徑,則需要先匯入
const path = require("path");
3.1 路徑拼接
- 使用
path.join()
把多個路徑片段拼接為完整的路徑字串
path.join([...paths]);
-
引數解讀
-
...paths<string>
:路徑片段的序列 -
返回值:
<string>
-
-
示例
const fs = require("fs");
const path = require("path");
// ../ 會抵消一級路徑
const pathStr = path.join("/a", "/b/c", "../", "./d", "e");
console.log(pathStr);
fs.readFile(path.join(__dirname, "/files/data.txt"), function (err, dataObj) {
if (err) {
return console.log("檔案讀取失敗!" + err.message);
}
console.log(dataObj.toString());
});
- 注:以後涉及路徑拼接的操作,都要用
path.join()
進行處理,如果直接使用+
進行拼接,可能會有問題,如下圖所示
3.2 獲取路徑中的檔名
- 使用
path.basename()
方法獲取路徑中的最後一部分,經常用它獲取路徑中的檔名
path.basename(path[, ext])
-
引數解讀
-
path
:必選,表示一個路徑的字串 -
ext
:可選,表示副檔名 -
返回值:表示路徑中的最後一部分
-
-
示例
const path = require("path");
// 不加第二個引數,會連副檔名一起輸出
const fileName = path.basename("/a/b/c/index.html", ".html");
console.log(fileName);
3.3 獲取路徑中的副檔名
- 使用
path.extname()
獲取路徑中的副檔名
const path = require("path");
const extName = path.extname("/a/b/c/index.html");
console.log(extName);
-
引數解讀
-
path
:必選,表示路徑字串 -
返回值:副檔名字串
-
-
示例
const path = require("path");
const extName = path.extname("/a/b/c/index.html");
console.log(extName);
3.4 小練習
-
需求:將
Clock.html
拆分為三個檔案,clock/index.html
、clock/index.js
、clock/index.css
,並引入 css、js 檔案(找一個含 html、css、js 的檔案進行練習即可) -
思路
-
設定正規表示式匹配
<style></style>
和<script></script>
中的內容 -
使用 fs 模組讀取
Clock.html
檔案 -
編寫三個方法處理 css、js、html 內容寫入檔案中
-
-
目錄結構
- 程式碼實現
const fs = require("fs");
const path = require("path");
// 先設定正規表示式,提取<style></style>和<script></script>的內容
const regStyle = /<style>[\s\S]*<\/style>/;
const regScript = /<script>[\s\S]*<\/script>/;
// 讀取html檔案
fs.readFile(path.join(__dirname, "../clockHtml/Clock.html"), function (err, dataObj) {
if (err) return console.log("檔案讀取失敗!" + err.message);
// 讀取檔案成功,呼叫三個方法將內容拆分成三個檔案
resolveCss(dataObj.toString());
resolveJs(dataObj.toString());
resolveHtml(dataObj.toString());
});
// 處理css
function resolveCss(htmlStr) {
const cssStr = regStyle.exec(htmlStr);
cssStr[0] = cssStr[0].replace("<style>", "").replace("</style>", "");
fs.writeFile(path.join(__dirname, "./clock/index.css"), cssStr[0], function (err) {
if (err) return console.log("檔案寫入失敗!" + cssStr);
});
console.log("css檔案寫入成功!");
}
// 處理js
function resolveJs(htmlStr) {
const jsStr = regScript.exec(htmlStr);
jsStr[0] = jsStr[0].replace("<script>", "").replace("</script>", "");
fs.writeFile(path.join(__dirname, "./clock/index.js"), jsStr[0], function (err) {
if (err) return console.log("檔案寫入失敗!" + jsStr);
});
console.log("js檔案寫入成功!");
}
// 處理html
function resolveHtml(htmlStr) {
const newStr = htmlStr
.replace(regStyle, '<link rel="stylesheet" href="index.css">')
.replace(regScript, '<script src="index.js"></script>');
fs.writeFile(path.join(__dirname, "./clock/index.html"), newStr, function (err) {
if (err) console.log("檔案寫入失敗!" + err.message);
console.log("html檔案寫入成功!");
});
}
-
兩個注意點
-
fs.writeFile()
只能用來建立檔案,不能用來建立路徑 -
重複呼叫
fs.writeFile()
寫入同一個檔案,新寫入的內容會覆蓋之前的內容
-
4. http 模組
4.1 簡介
http 模組是 Node.js 官方提供的用來建立 web 伺服器的模組
-
客戶端:在網路節點中,負責消費資源的電腦
-
伺服器:負責對外提供網路資源的電腦
-
伺服器和普通電腦的區別在於:伺服器上安裝了 web 伺服器軟體,如 IIS、Apache 等,透過安裝這些伺服器軟體,就能把一臺普通的電腦變成一臺 web 伺服器
-
在 Node.js 中不需要使用 IIS、Apache 等第三方 web 伺服器軟體,可以基於 Node.js 提供的 http 模組輕鬆手寫一個伺服器軟體
4.2 建立最基本的 web 伺服器
- 匯入
const http = require("http");
- 呼叫
http.createServer()
建立 web 伺服器例項
const server = http.createServer();
- 為伺服器例項繫結
request
事件,監聽客戶端的請求
server.on("request", (req, res) => {
// 只要有客戶端來請求伺服器,就會觸發request事件,從而呼叫這個事件處理函式
console.log("Someone visit our web server.");
});
- 呼叫
listen()
啟動當前 web 伺服器例項
server.listen("8080", () => {
console.log("http server running at http://127.0.0.1:8080");
});
- 執行之後用瀏覽器訪問該地址
4.3 req 請求物件
-
只要伺服器收到了客戶端的請求,就會呼叫透過
server.on()
為伺服器繫結的request
事件處理函式 -
req
是請求物件,包含了與客戶端相關的資料和屬性-
req.url
:客戶端請求的 url 地址 -
req.method
:客戶端的 method 請求型別
-
-
示例
const http = require("http");
const server = http.createServer();
server.on("request", (req, res) => {
console.log(`Your request url is ${req.url}, and request method is ${req.method}`);
});
server.listen("8080", () => {
console.log("http server running at http://127.0.0.1:8080");
});
4.4 res 響應物件
-
res
是響應物件,包含與伺服器相關的資料和屬性res.end()
:向客戶端傳送指定的內容,並結束本次請求的處理過程
-
示例
const http = require("http");
const server = http.createServer();
server.on("request", (req, res) => {
const str = `Your request url is ${req.url}, and request method is ${req.method}`;
res.end(str);
});
server.listen("8080", () => {
console.log("http server running at http://127.0.0.1:8080");
});
- 透過一些介面測試軟體測試一下其他請求方式,此處使用
Apifox
4.5 解決中文亂碼問題
- 當呼叫
res.end()
向客戶端傳送中文內容時,會出現亂碼,此時需要手動設定內容的編碼格式
res.setHeader("Content-Type", "text-html; charset=utf-8");
- 示例
const http = require("http");
const server = http.createServer();
server.on("request", (req, res) => {
const str = `您的請求地址是:${req.url},請求方式是:${req.method}`;
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.end(str);
});
server.listen("8080", () => {
console.log("http server running at http://127.0.0.1:8080");
});
4.6 小練習
4.6.1 根據不同的 url 響應不同的 html 內容
-
實現步驟
-
獲取請求的 url
-
路徑為
/
或/index.html
,訪問的是首頁 -
路徑為
/about.html
,訪問的是關於頁面 -
其他則顯示
404 Not Found
-
設定
Content-Type
響應頭,防止中文亂碼 -
使用
res.end()
響應給客戶端
-
-
程式碼實現
const http = require("http");
const server = http.createServer();
server.on("request", (req, res) => {
let content = "<h1>404 Not Found</h1>";
console.log(req.url);
if (req.url === "/" || req.url === "/index.html") {
content = "<h1>首頁</h1>";
} else if (req.url === "/about.html") {
content = "<h1>關於</h1>";
}
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.end(content);
});
server.listen("8080", () => {
console.log("http server running at http://127.0.0.1:8080");
});
4.6.2 實現時鐘的 web 伺服器
- 思路:把檔案的實際存放路徑,作為每個資源的請求 url 地址
- 程式碼實現
const http = require("http");
const fs = require("fs");
const path = require("path");
const server = http.createServer();
server.on("request", (req, res) => {
if (req.url !== "/favicon.ico") {
fs.readFile(path.join(__dirname, req.url), function (err, dataObj) {
if (err) {
return res.end(`<h1>404 Not Found</h1>`);
}
res.end(dataObj.toString());
});
}
});
server.listen("8080", () => {
console.log("http server running at http://127.0.0.1:8080");
});
-
最佳化資源請求路徑
-
訪問
/
時預設也訪問/clock/index.html
-
簡化路徑輸入
/clock/index.html
-->/index.html
-
const http = require("http");
const fs = require("fs");
const path = require("path");
const server = http.createServer();
server.on("request", (req, res) => {
// 最佳化資源請求路徑
let fpath = "";
if (req.url === "/") {
fpath = path.join(__dirname, "./clock/index.html");
} else {
fpath = path.join(__dirname, "/clock", req.url);
}
if (req.url !== "/favicon.ico") {
fs.readFile(fpath, function (err, dataObj) {
if (err) {
return res.end(`<h1>404 Not Found</h1>`);
}
res.end(dataObj.toString());
});
}
});
server.listen("8080", () => {
console.log("http server running at http://127.0.0.1:8080");
});
5. js 模組化規範
5.1 模組化概述
5.1.1 什麼是模組化
-
將程式⽂件依據⼀定規則拆分成多個⽂件,這種編碼⽅式就是模組化的編碼方式
-
拆分出來每個⽂件就是⼀個模組,模組中的資料都是私有的,模組之間互相隔離
-
同時也能透過一些手段,可以把模組內的指定資料“交出去”,供其他模組使用
5.1.2 為什麼需要模組化
-
隨著應用的複雜度越來越高,其程式碼量和檔案數量都會急劇增加,會逐漸引發以下問題:
-
全域性汙染問題
-
依賴混亂問題
-
資料安全問題
-
-
好處
-
複用性
-
可維護性
-
可實現按需載入
-
5.2 有哪些模組化規範
-
CommonJS——服務端應用廣泛
-
AMD(瞭解)
-
CMD(瞭解)
-
ES6 模組化——瀏覽器端應用廣泛
5.3 匯入和匯出的概念
模組化的核心思想就是:模組之間是隔離的,透過匯入和匯出進行資料和功能的共享
-
匯出(暴露):模組公開其內部的⼀部分(如變數、函式等),使這些內容可以被其他模組使用
-
匯入(引入):模組引入和使用其他模組匯出的內容,以重用程式碼和功能
5.4 Node.js 中的模組化
5.4.1 分類
-
根據來源的不同,分為三大類
-
內建模組:如 fs、path、http 等
-
自定義模組:使用者建立的每個
.js
檔案都是自定義模組 -
第三方模組:由第三方開發出來的模組,使用前需要提前下載
-
5.4.2 載入模組
// 1、載入內建的fs模組
const fs = require("fs");
// 2、載入自定義模組,.js字尾可省略
const custom = require("./custom.js");
// 3、載入第三方模組
const moment = require("moment");
5.4.3 模組作用域與 module 物件
-
模組作用域:只能在當前模組內被訪問
-
好處:防止全域性變數汙染問題
-
module 物件:每個
.js
自定義模組中都有一個module
物件,裡面儲存了和當前模組有關的資訊
5.5 CommonJS 規範
Node.js 遵循了 CommonJS 模組化規範,CommonJS 規定了模組的特性和各模組之間如何相互依賴
-
CommonJS 規定
-
每個模組內部,module 變數代表當前模組
-
module 變數是一個物件,其
exports
屬性(即module.exports
)是對外的介面 -
載入某個模組,其實就是載入該模組的
module.exports
屬性,require()
方法用於載入模組
-
5.5.1 初步體驗
- school.js
const name = "尚矽谷";
const slogan = "讓天下沒有難學的技術!";
function getTel() {
return "010-56253825";
}
function getCities() {
return ["北京", "上海", "深圳", "成都", "武漢", "西安"];
}
// 透過給exports物件新增屬性的方式,來匯出資料
// 此處不匯出getCities
exports.name = name;
exports.slogan = slogan;
exports.getTel = getTel;
- student.js
const name = "張三";
const motto = "相信明天會更好!";
function getTel() {
return "13877889900";
}
function getHobby() {
return ["抽菸", "喝酒", "燙頭"];
}
// 透過給exports物件新增屬性的方式,來匯出資料
// 此處不匯出getHobby
exports.name = name;
exports.motto = motto;
exports.getTel = getTel;
- index.js
// 引入school模組暴露的所有內容
const school = require("./school.js");
// 引入student模組暴露的所有內容
const student = require("./student.js");
console.log(school);
console.log(student);
5.5.2 匯出資料
-
在
CommonJS
標準中,匯出資料有兩種方式:-
第一種方式:
module.exports = value
-
第二種方式:
exports.name = value
-
-
注:
- 每個模組內部的:
this
、exports
、modules.exports
在初始時,都指向同一個空物件,該空物件就是當前模組匯出的資料,如下圖:
-
無論如何修改匯出物件,最終匯出的都是
module.exports
的值 -
exports
是對module.exports
的初始引用,僅為了方便給匯出新增屬性,所以不能用exports={}
的形式匯出資料,但是可以用module.exports={}
匯出資料 -
注:為了防止混亂,建議不要在同一模組中同時使用
exports
和module.exports
- 每個模組內部的:
-
school.js
const name = "尚矽谷";
const slogan = "讓天下沒有難學的技術!";
function getTel() {
return "010-56253825";
}
function getCities() {
return ["北京", "上海", "深圳", "成都", "武漢", "西安"];
}
module.exports = { name, slogan, getTel };
// this.c =789
// exports = {a:1}
// exports.b = 2
// module.exports.c = 3
// module.exports = {d:4} // 最終匯出成功的是這個
// console.log(this)
// console.log(exports)
// console.log(module.exports)
// console.log(this === exports && exports === module.exports)
exports.name = name;
exports.slogan = slogan;
exports.getTel = getTel;
-
解釋
-
一開始
module.exports
和exports
指向同一個空物件 -
exports = {a:1}
:exports
就指向了{a:1}
這個新物件,module.exports
仍指向空物件 -
exports.b = 2
:向exports
指向的物件新增屬性b
-
module.exports.c = 3
:向module.exports
指向的物件新增屬性c
-
module.exports = {d:4}
:module.exports
指向了新物件{d:4}
-
無論如何修改匯出物件,最終匯出的都是
module.exports
的值
-
5.5.3 匯入資料
在 CJS 模組化標準中,使用內建的 require 函式進行匯入資料
//直接引入模組
const school = require("./school.js");
//引入同時解構出要用的資料
const { name, slogan, getTel } = require("./school.js");
//引入同時解構+重新命名
const { name: stuName, motto, getTel: stuTel } = require("./student.js");
5.5.4 擴充套件理解
- 一個 JS 模組在執行時,是被包裹在一個內建函式中執行的,所以每個模組都有自己的作用域,可以透過如下方式驗證這一說法:
console.log(arguments);
console.log(arguments.callee.toString());
- 內建函式的大致形式如下:
function (exports, require, module, __filename, __dirname){
/**************************/
}
5.5.5 瀏覽器端執行
-
Node.js 預設是支援 CommonJS 規範的,但瀏覽器端不支援,所以需要經過編譯,步驟如下:
- 第一步:全域性安裝 browserify
npm i browserify -g
- 第二步:編譯
browserify index.js -o build.js
-
注:index.js 是原始檔,build.js 是輸出的目標檔案
-
第三步:頁面中引入使用
<script type="text/javascript" src="./build.js"></script>
5.6 ES6 模組化規範
ES6 模組化規範是一個官方標準的規範,它是在語言標準的層面上實現了模組化功能,是目前最流行的模組化規範,且瀏覽器與服務端均支援該規範
5.6.1 初步體驗
- school.js
// 匯出name
export const name = "尚矽谷";
// 匯出slogan
export const slogan = "讓天下沒有難學的技術!";
// 匯出getTel
export function getTel() {
return "010-56253825";
}
function getCities() {
return ["北京", "上海", "深圳", "成都", "武漢", "西安"];
}
- student.js
export const name = "張三";
export const motto = "相信明天會更好!";
export function getTel() {
return "13877889900";
}
function getHobby() {
return ["抽菸", "喝酒", "燙頭"];
}
- index.js
// 引入school模組暴露的所有內容
import * as school from "./school.js";
// 引入student模組暴露的所有內容
import * as student from "./student.js";
- 頁面中引入 index.js
<script type="module" src="./index.js"></script>
5.6.2 Node 中執行 ES6 模組
-
Node.js 中執行 ES6 模組程式碼有兩種方式:
-
方式一:將 JavaScript 檔案字尾從
.js
改為.mjs
,Node 則會自動識別 ES6 模組 -
方式二:在
package.json
中設定type
屬性值為module
-
5.6.3 匯出資料
ES6 模組化提供 3 種匯出方式:① 分別匯出、② 統一匯出、③ 預設匯出
- 分別匯出
// 匯出name
export const name = "尚矽谷";
// 匯出slogan
export const slogan = "讓天下沒有難學的技術!";
// 匯出getTel
export function getTel() {
return "010-56253825";
}
- 統一匯出
const name = "尚矽谷";
const slogan = "讓天下沒有難學的技術!";
function getTel() {
return "010-56253825";
}
function getCities() {
return ["北京", "上海", "深圳", "成都", "武漢", "西安"];
}
// 統一匯出了:name、slogan、getTel
export { name, slogan, getTel };
- 預設匯出
const name = "尚矽谷";
const slogan = "讓天下沒有難學的技術!";
function getTel() {
return "010-56253825";
}
function getCities() {
return ["北京", "上海", "深圳", "成都", "武漢", "西安"];
}
//預設匯出了:name、slogan、getTel
export default { name, slogan, getTel };
- 注:上述多種匯出方式,可以同時使用
// 匯出name —— 分別匯出
export const name = "尚矽谷";
const slogan = "讓天下沒有難學的技術!";
function getTel() {
return "010-56253825";
}
function getCities() {
return ["北京", "上海", "深圳", "成都", "武漢", "西安"];
}
// 匯出slogan —— 統一匯出
export { slogan };
// 匯出getTel —— 預設匯出
export default getTel;
5.6.4 匯入資料
對於 ES6 模組化來說,使用何種匯入方式,要根據匯出方式決定
🛠️ 匯入全部(通用)
- 可以將模組中的所有匯出內容整合到一個物件中
import * as school from "./school.js";
🛠️ 命名匯入(對應到處方式:分別匯出、統一匯出)
- 匯出資料的模組
// 分別匯出
export const name = "尚矽谷";
// 分別匯出
export const slogan = "讓天下沒有難學的技術!";
function getTel() {
return "010-56253825";
}
function getCities() {
return ["北京", "上海", "深圳", "成都", "武漢", "西安"];
}
// 統一匯出
export { getTel };
- 命名匯入
import { name, slogan, getTel } from "./school.js";
- 透過
as
重新命名
import { name as myName, slogan, getTel } from "./school.js";
🛠️ 預設匯出(對應匯出方式:預設匯出)
- 匯出資料的模組
const name = "尚矽谷";
const slogan = "讓天下沒有難學的技術!";
function getTel() {
return "010-56253825";
}
function getCities() {
return ["北京", "上海", "深圳", "成都", "武漢", "西安"];
}
// 預設匯出了:name、slogan、getTel
export default { name, slogan, getTel };
- 預設匯入
import school from "./school.js"; // 預設匯出的名字可以修改,不是必須為school
🛠️ 命名匯入與預設匯入可以混合使用
- 匯出資料的模組
// 分別匯出
export const name = "尚矽谷";
// 分別匯出
export const slogan = "讓天下沒有難學的技術!";
function getTel() {
return "010-56253825";
}
function getCities() {
return ["北京", "上海", "深圳", "成都", "武漢", "西安"];
}
// 預設匯出
export default getTel;
- 命名匯入與預設匯入混合使用,且預設匯入的內容必須放在前方
import getTel, { name, slogan } from "./school.js";
🛠️ 動態匯入(通用)
- 允許在執行時按需載入模組,返回值是一個 Promise
const school = await import("./school.js");
console.log(school);
🛠️import 可以不接收任何資料
- 例如只是讓 mock.js 參與執行
import "./mock.js";
5.6.5 資料引用問題
- 思考1:如下程式碼的輸出結果是什麼?
function count() {
let sum = 1;
function increment() {
sum += 1;
}
return { sum, increment };
}
const { sum, increment } = count();
console.log(sum); // 1
increment();
increment();
console.log(sum); // 1
-
思考2:使用 CommnJS 規範,編寫如下程式碼,輸出結果是什麼?
-
count.js
let sum = 1;
function increment() {
sum += 1;
}
module.exports = { sum, increment };
- index.js
const { sum, increment } = require("./count.js");
console.log(sum); // 1
increment();
increment();
console.log(sum); // 1
-
說明:cjs 匯入的變數是複製品,無論呼叫的函式怎麼修改,改的還是模組內部的變數
-
思考3:使用 ES6 模組化規範,編寫如下程式碼,輸出結果是什麼?
-
count.js
let sum = 1;
function increment() {
sum += 1;
}
export { sum, increment };
- index.js
import { sum, increment } from "./count.js";
console.log(sum); // 1
increment();
increment();
console.log(sum); // 3
-
說明:es6 匯入的變數和模組中的變數公用同一塊記憶體,因此會修改變數的值
-
使用原則:匯出的常量,務必使用
const
定義
6. 包與 npm
6.1 簡介
-
包:Node.js 中的第三方模組
-
包的來源:由第三方個人或團隊開發出來的,免費供所有人使用(免費開源)
-
為什麼需要包
-
Node.js 的內建模組僅提供一些底層的 API,在基於內建模組進行專案開發時效率較低
-
包是基於內建模組封裝出來的,提供了更高階、更方便的 API,極大提高了開發效率
-
-
從哪下載包
-
搜尋需要的包:npmjs
-
從https://registry.npmjs.org/伺服器上下載自己需要的包
-
-
如何下載
- 包管理工具 npm:Node Package Manager
6.2 安裝包
# 完整寫法,預設下載最新版的包
npm install 包名
# 簡寫
npm i 包名
# 安裝指定版本的包
npm i 包名@2.22.2
-
安裝完後,檢視文件學習該模組的使用方法
-
示例:安裝
moment
對時間進行格式化
const moment = require("moment");
const datetime = moment().format("YYYY-MM-DD HH:MM:SS");
console.log(datetime);
-
初次裝包完成後,專案資料夾下多了
node_modules
資料夾和package-lock.json
的配置檔案 -
其中
-
node_modules
資料夾用來存放所有已安裝到專案中的包,require()
就是從這個目錄中查詢並載入包 -
package-lock.json
配置檔案用來記錄node_modules
目錄下的每一個包的下載資訊,如包名、版本號、下載地址等
-
-
注:不要手動修改
node_modules
或package-lock.js
檔案中的任何程式碼,npm 包管理工具會自動維護它們
6.3 包的語義化版本規範
-
包的版本號是以“點分十進位制”形式進行定義的,總共三位數字,例如:2.24.0
-
其中每一位數字所代表的含義如下:
-
第 1 位數字:大版本,當發生了底層重構時,大版本+1
-
第 2 位數字:功能版本,當新增了一些功能時,功能版本+1
-
第 3 位數字:Bug 修復版本,對 bug 進行修復後,bug 修復版本+1
-
-
版本號提升規則:只要前面的版本號增長了,則後面的版本號歸零
6.4 包管理配置檔案
npm 規定,在專案根目錄中,必須提供名為
package.json
的包管理配置檔案,用來記錄與專案有關的一些配置資訊,如:
專案名稱、版本號、描述等
專案中都用到了哪些包
哪些包只在開發期間會用到
哪些包在開發和部署時都需要用到
-
多人協作的問題
-
整個專案的體積是 30.4M,第三方包的體積是 28.8M,專案原始碼的體積 1.6M
-
問題:第三方包體積過大,不方便團隊成員之間共享專案原始碼
-
解決:共享時剔除
node_modules
-
-
如何記錄專案中安裝了哪些包
-
在專案根目錄中,建立
package.json
配置檔案,即可用來記錄專案中安裝了哪些包,從而方便剔除node_modules
目錄後,在團隊成員之間共享專案的原始碼- 注:在專案開發中,一定要把
node_modules
資料夾新增到.gitignore
忽略檔案中
- 注:在專案開發中,一定要把
-
-
快速建立
package.json
npm init -y
-
說明
-
在執行命令所處的目錄中,快速新建
package.json
檔案 -
還未寫任何程式碼前先建立該檔案
-
該命令只能在英文的目錄下成功執行,不能含中文、空格
-
執行
npm install 包名
時,npm 包管理工具會自動把包的名稱和版本號記錄到package.json
中
-
6.4.1 dependencies 節點
-
package.json
檔案中有一個dependencies
節點,專門用來記錄用npm install
安裝了哪些包 -
一次性安裝所有包
-
當拿到一個剔除了
node_modules
的專案後,需要先把所有的包下載到專案中,專案才能執行起來 -
執行
npm install
命令時,npm 包管理工具會先讀取package.json
中的dependencies
節點 -
讀取到記錄的所有依賴包名稱和版本號後,npm 包管理工具會把這些包一次性下載到專案中
-
npm install
npm i
6.4.2 devDependencies 節點
-
如果某些包只在專案開發階段會用到,在專案上線之後不會用到,則建議把這些包記錄到
devDependencies
節點中 -
如果在開發和專案上線之後都需要用到,則建議把這些包記錄到
dependencies
節點中 -
使用如下命令安裝指定包,並記錄到
devDependencies
節點中
# 簡寫
npm i 包名 -D
# 完整寫法
npm install 包名 --save-dev
6.5 解除安裝包
npm uninstall 包名
- 注:
npm uninstall
執行成功後,會把解除安裝的包自動從package.json
的dependencies
中移除
6.6 解決下包速度慢的問題
-
為什麼下載速度慢
- 在使用 npm 下包時,預設從國外的伺服器進行下載,此時,網路資料的傳輸需要經過漫長的海底光纜,因此下包速度會很慢
-
npm 映象伺服器
-
淘寶在國內搭建了一個伺服器,專門把國外官方伺服器上的包同步到國內的伺服器,並在國內提供下包的服務,從而提高了下包的速度
-
擴充套件:映象是一種檔案儲存形式,一個磁碟上的資料在另一個磁碟上存在一個完全相同的副本即為映象
-
- 切換 npm 的下包映象源
# 檢視當前的下包映象源
npm config get registry
# 切換映象源,選擇一個即可
npm config set registry https://registry.npmmirror.com # 淘寶
npm config set registry https://npm.aliyun.com # 阿里雲
npm config set registry http://mirrors.cloud.tencent.com/npm/ # 騰訊雲
npm config set registry https://mirrors.huaweicloud.com/repository/npm/ # 華為雲
# 檢查映象源是否切換成功
npm config get registry
-
nrm
- 為了更方便的切換下包的映象源,可以安裝
nrm
工具,利用其提供的終端命令,可以快速檢視和切換下包的映象源
- 為了更方便的切換下包的映象源,可以安裝
# 將nrm安裝為全域性可用的工具
npm i nrm -g
# 檢視所有可用的映象源
nrm ls
# 將下包的映象源切換為淘寶映象
nrm use taobao
6.7 包的分類
-
分為兩大類
-
專案包
-
開發依賴包:被記錄到
devDependencies
節點中的包,只在開發期間會用到 -
核心依賴包:被記錄到
dependencies
節點中的包,在開發期間和專案上線之後都會用到
-
-
全域性包
-
執行
npm install
使用了-g
引數 -
全域性包會被安裝到
C:\User\使用者目錄\AppData\Roaming\npm\node_modules
目錄下
npm i 包名 -g # 全域性安裝指定的包 npm uninstall 包名 -g # 解除安裝全域性安裝的包
-
-
注:
-
只有工具性質的包才有全域性安裝的必要性,因為它們提供了好用的終端命令
-
判斷某個包是否需要全域性安裝才能使用,可以參考官方提供的使用說明
-
-
-
以
i5ting_toc
工具進行示例,它是一個可以把md
文件轉為html
頁面的小工具
# 將i5ting_toc安裝為全域性包
npm install -g i5ting_toc
# 呼叫i5ting_toc,輕鬆實現md轉html的功能
# -o是轉換成功後以預設瀏覽器開啟
i5ting_toc -f 要轉換的md檔案路徑 -o
6.8 規範的包結構
-
一個規範的包,其組成結構必須符合以下3點要求
-
包必須以單獨的目錄存在
-
包的頂級目錄下必須包含
package.json
這個包管理配置檔案 -
package.json
中必須包含name
,version
,main
這三個屬性,分別代表包的名字、版本號、包的入口
-
-
注:以上3點要求是一個規範的包結構必須遵守的格式,關於更多約束可以參考https://classic.yarnpkg.com/en/docs/package-json
6.9 開發屬於自己的包
-
需求:
-
格式化日期
-
轉義 HTML 中的特殊字元
-
還原 HTML 中的特殊字元
-
-
初始化包的基本結構
-
新建
my-tools
資料夾,作為包的根目錄 -
在
my-tools
資料夾中,新建如下三個檔案-
package.json
:包管理配置檔案 -
index.js
:包的入口檔案 -
README.md
:包的說明文件
-
-
-
初始化
package.json
{
"name": "my-tools",
"version": "1.0.0",
"main": "index.js",
"description": "提供了格式化時間,HTMLEscape的功能",
"keywords": ["dateFormat", "escape"],
"license": "ISC"
}
-
關於更多
license
許可協議相關的內容,可參考https://www.jianshu.com/p/86251523e898 -
在
index.js
中定義格式化時間的方法
// 包的入口檔案
function dateFormat(datetime) {
const date = new Date(datetime);
const y = date.getFullYear();
const m = addZero(date.getMonth() + 1);
const d = addZero(date.getDate());
const hh = addZero(date.getHours());
const mm = addZero(date.getMinutes());
const ss = addZero(date.getSeconds());
return `${y}-${m}-${d} ${hh}:${mm}:${ss}`;
}
function addZero(n) {
return n > 9 ? n : "0" + n;
}
module.exports = { dateFormat };
test.js
測試一下模組是否可以使用
const myTools = require("./my-tools");
const datetime = myTools.dateFormat(new Date());
console.log(datetime);
- 在
index.js
中定義轉義 HTML 的方法
function HTMLEscape(htmlStr) {
return htmlStr.replace(/<|>|"|&/g, match => {
switch (match) {
case "<":
return "<";
case ">":
return ">";
case '"':
return """;
case "&":
return "&";
}
});
}
- 在
index.js
中定義還原 HTML 的方法
function htmlUnEscape(htmlStr) {
return htmlStr.replace(/<|>|"|&/g, match => {
switch (match) {
case "<":
return "<";
case ">":
return ">";
case """:
return '"';
case "&":
return "&";
}
});
}
-
將不同的功能進行模組化拆分
-
將格式化時間的功能拆分到
src/dateFormat.js
中 -
將處理 HTML 字串的功能,拆分到
src/htmlEscape.js
-
在
index.js
中,匯入兩個模組,得到需要向外共享的方法 -
在
index.js
中,使用module.exports
把對應的方法共享出去(解構)
-
-
index.js
// 包的入口檔案
const date = require("./src/dateFormat");
const htmlEscape = require("./src/htmlEscape");
module.exports = {
...date,
...htmlEscape,
};
- 測試
const myTools = require("./my-tools");
const datetime = myTools.dateFormat(new Date());
console.log(datetime);
const htmlStr = "<h1 ttile='abc'>這是h1標籤<span>123 </span></h1>";
const str = myTools.HTMLEscape(htmlStr);
const newStr = myTools.htmlUnEscape(str);
console.log(newStr);
-
編寫包的說明文件
-
能清晰地將包的作用、用法、注意事項等描述清楚即可
-
以下
README.md
包含以下內容- 安裝方式、匯入方式、格式化時間、轉義 HTML 中的特殊字元、還原 HTML 中的特殊字元、開源協議
-
# 安裝
npm i my-tools
# 匯入
const myTools = require('./my-tools')
# 格式化時間
```
// 格式:YYYY-MM-DD hh:mm:ss
const datetime = myTools.dateFormat(new Date())
console.log(datetime)
```
# 轉義 HTML 中的特殊字元
```
const htmlStr = "<h1 ttile='abc'>這是h1標籤<span>123 </span></h1>"
// 結果:<h1 ttile='abc'>這是h1標籤<span>123&nbsp;</span></h1>
const str = myTools.HTMLEscape(htmlStr)
```
# 還原 HTML 中的特殊字元
```
const htmlStr = "<h1 ttile='abc'>這是h1標籤<span>123 </span></h1>"
const str = myTools.HTMLEscape(htmlStr)
const newStr = myTools.htmlUnEscape(str)
console.log(newStr)
```
# 開源協議
ISC
6.10 釋出包
-
註冊 npm 賬號
-
登入 npm 賬號
-
在終端執行
npm login
命令 -
注意,不是在官網登入,而是在命令列
-
在執行
npm login
之前,必須先把下包的伺服器地址切換為 npm 官方伺服器,否則會導致釋出包失敗
-
-
切換到包的根目錄,執行
npm publish
,即可將包釋出到 npm 上(注:包名不能雷同) -
刪除已釋出的包
-
npm unpublish 包名 --force
-
注:
-
只能刪除 72h 以內釋出的包
-
刪除後 24h 內不允許重複釋出
-
釋出包時要謹慎,儘量不要往 npm 上釋出沒有意義的包!
-
-
6.11 模組的載入機制
6.11.1 優先從快取中載入
-
模組在第一次載入後會被快取,即多次呼叫
require()
不會導致模組的程式碼被執行多次 -
注:不論是內建模組、使用者自定義模組還是第三方模組,都會優先從快取中載入,從而提高模組的載入效率
6.11.2 內建模組的載入機制
-
內建模組的載入優先順序最高
-
如:
require('fs')
始終返回內建的 fs 模組,即使node_modules
目錄下有同名包 fs
6.11.3 自定義模組的載入機制
-
使用
require()
載入自定義模組時,必須指定以./
或../
開頭的路徑識別符號,在載入自定義模組時,如果沒有指定./
或../
這樣的路徑識別符號,node 會把它當作內建模組或第三方模組進行載入 -
在使用
require()
匯入自定義模組時,若省略了檔案的副檔名,則 Node.js 會按順序分別嘗試載入以下檔案-
按照確切的檔名進行載入
-
補全
.js
副檔名進行載入 -
補全
.json
副檔名進行載入 -
補全
.node
副檔名進行載入 -
載入失敗,終端報錯
-
6.11.4 第三方模組的載入機制
-
如果傳遞給
require()
的模組識別符號不是一個內建模組,也沒有./
或../
開頭,則 Node.js 會從當前模組的父目錄開始,嘗試從/node_modules
資料夾中載入第三方模組 -
如果沒有找到對應的第三方模組,則移動到再上一層父目錄中,進行載入,直到檔案系統的根目錄
-
例如,假設在
C:\Users\itheima\project\foo.js
檔案裡呼叫了require('tools')
,則 Node.js 會按以下順序查詢:-
C:\Users\itheima\project\node_modules\tools
-
C:\Users\itheima\node_modules\tools
-
C:\Users\node_modules\tools
-
C:\node_modules\tools
-
6.11.5 目錄作為模組
-
當把目錄作為模組識別符號傳遞給
require()
進行載入時,有三種載入方式-
在被載入的目錄下查詢一個叫
package.json
的檔案,並尋找main
屬性,作為require()
載入的入口 -
如果目錄裡沒有
package.json
檔案,或者main
入口不存在或無法解析,則 Node.js 會試圖載入目錄下的index.js
檔案 -
若以上兩步都失敗了,則 Node.js 會在終端列印錯誤訊息,報告模組的缺失:
Error:Cannot find module 'xxx'
-
7. express
7.1 簡介
7.1.1 是什麼
-
Express是基於 Node.js 平臺,快速、開放、極簡的 Web 開發框架
-
簡單理解:Express 的作用和 Node.js 內建的 http 模組類似,是專門用來建立 Web 伺服器的
-
Express 的本質:npm 上的第三方包,提供了快速建立 Web 伺服器的便捷方法
7.1.2 進一步理解
-
不使用 Express 能否建立 Web 伺服器
- 能,使用原生的 http 模組
-
有了 http 內建模組,為什麼還要用 Express
- http 模組使用較複雜,開發效率低;Express 是基於內建的 http 模組進一步封裝出來的,能夠提高開發效率
7.1.3 Express 能夠做什麼
-
對於前端程式設計師來說,最常見的兩種伺服器,分別是
-
Web 網站伺服器:專門對外提供 Web 網頁資源的伺服器
-
API 介面伺服器:專門對對外提供 API 介面的伺服器
-
-
使用 Express,可以方便、快速的建立 Web 網站伺服器或 API 介面伺服器
7.2 基本使用
7.2.1 安裝
- 在專案所處的目錄中安裝 express
npm i express
7.2.2 建立基本的 Web 伺服器
// 匯入
const express = require("express");
// 建立web伺服器
const app = express();
// 呼叫app.listen(埠號, callback),啟動伺服器
app.listen(80, () => {
console.log("express server running at http://127.0.0.1");
});
7.2.3 監聽 GET 請求
- 透過
app.get()
可以監聽客戶端的GET
請求
app.get("請求url", function (req, res) {
/* 處理函式 */
});
-
req
:請求物件,包含了與請求相關的屬性和方法 -
res
:響應物件,包含了與響應相關的屬性和方法
7.2.4 監聽 POST 請求
- 透過
app.post()
可以監聽客戶端的POST
請求
app.post("請求url", function (req, res) {
/* 處理函式 */
});
7.2.5 把內容響應給客戶端
- 透過
res.send()
方法,可以把處理好的內容傳送給客戶端
app.get("/user", function (req, res) {
// 向客戶端傳送JSON物件
res.send({ name: "zs", age: 18, gender: "男" });
});
app.post("/user", function (req, res) {
// 向客戶端傳送文字內容
res.send("請求成功!");
});
7.2.6 獲取 url 中攜帶的查詢引數
-
req.query
預設是一個空物件 -
客戶端使用
?name=zs&age=18
這種查詢字串形式傳送到伺服器,可以透過req.query
物件訪問到,如:req.query.name
和req.query.age
app.get("/", function (req, res) {
console.log(req.query);
res.send(req.query);
});
7.2.7 獲取 url 中的動態引數
-
透過
req.params
物件,可以訪問到 url 中透過:
匹配到的動態引數 -
req.params
預設是一個空物件 -
動態引數可以有多個,如:
/user/:id/:name
// 此處:id是一個動態引數
app.get("/user/:id", function (req, res) {
console.log(req.params);
res.send(req.params);
});
7.3 託管靜態資源
7.3.1 express.static()
-
透過
express.static()
可以非常方便地建立一個靜態資源伺服器 -
示例:將 clock 目錄下的檔案對外開放訪問
app.use(express.static("./clock"));
-
此時,可以訪問
clock
目錄下的所有檔案了http://127.0.0.1/index.html
-
注:Express 在指定的靜態目錄中查詢檔案,並對外提供資源的訪問路徑,存放靜態檔案的目錄名不會出現在 url 中
- 如果要託管多個靜態資源目錄,需要多次呼叫
express.static()
app.use(express.static("./clock"));
app.use(express.static("./files"));
-
注:訪問靜態資原始檔時,
express.static()
會根據目錄的新增順序查詢所需的檔案,即如果兩個資料夾中存在同名檔案,以前面的為主 -
把
./files
放前面,訪問到的就是files
中的index.html檔案
7.3.2 掛載路徑字首
- 如果希望在託管的靜態資源訪問路徑之前掛載路徑字首,可使用如下方式
app.use("/clock", express.static("./clock"));
- 注:此後訪問資源時都必須加上字首
7.4 nodemon
-
在編寫除錯 Node.js 專案時,如果修改了專案的程式碼,需要頻繁手動關閉再重啟,比較繁瑣
-
此時,可以使用
nodemon
工具,它可以監聽專案檔案的變動,當程式碼被修改時,nodemon
會自動重啟專案,方便開發和除錯 -
安裝
npm i -g nodemon
- 用
nodemon app.js
代替傳統的node app.js
啟動專案
7.5 路由
7.5.1 概念
-
在 Express 中,路由指的是客戶端的請求與伺服器處理函式之間的對映關係
-
Express 中的路由分為 3 部分組成,分別是請求的型別、請求的 url 地址、處理函式
app.method(path, handler);
- 前面使用過的
app.get()
、app.post()
便是路由
7.5.2 路由的匹配過程
-
每當一個請求到達伺服器之後,需要先經過路由的匹配,只有匹配成功之後才會呼叫對應的處理函式
-
匹配時,會按照路由的順序進行匹配,如果請求型別和請求的 url 同時匹配成功,則 Express 會將這次請求轉交給對應的 function 函式進行處理
-
路由匹配注意點
-
按照定義的先後順序進行匹配
-
請求型別和請求的 url 同時匹配成功,才會呼叫對應的處理函式
-
7.5.3 使用
-
為了方便對路由進行模組化的管理,Express 不建議將路由直接掛載到 app 上,而是推薦將路由抽離為單獨的模組
-
步驟
-
建立路由模組對應的
.js
檔案 -
呼叫
express.Router()
函式建立路由物件 -
向路由物件上掛載具體的路由
-
使用
module.exports
向外共享路由物件 -
使用
app.use()
註冊路由模組
-
-
router.js
// 匯入express,建立路由物件
const express = require("express");
const router = express.Router();
// 掛載獲取使用者列表的路由
router.get("/user/list", function (req, res) {
res.send("Get user list.");
});
// 掛載新增使用者的路由
router.post("/user/add", function (req, res) {
res.send("Add new user.");
});
// 向外匯出路由物件
module.exports = router;
test.js
const express = require("express");
const router = require("./router");
const app = express();
app.use(router);
app.listen(80, () => {
console.log("express server running at http://127.0.0.1");
});
- 注:
app.use()
的作用就是用來註冊全域性中介軟體
7.5.4 為路由模組新增字首
// 匯入路由模組
const userRouter = require("./router/user.js");
// 使用app.use()註冊路由模組,並新增統一的訪問字首api
app.use("/api", userRouter);
7.6 中介軟體
7.6.1 概念
-
中介軟體:特指業務流程的中間處理環節
-
生活中的例子
-
在處理汙水時,一般要經過三個處理環節,從而保證處理過後的廢水達到排放標準
-
處理汙水的這三個中間處理環節,可以叫做中介軟體
-
- 當一個請求到達 Express 的伺服器後,可以連續呼叫多箇中介軟體,從而對這次請求進行預處理
7.6.2 格式
- Express 的中介軟體,本質上是一個 function 處理函式,格式如下:
-
注:中介軟體函式的形參列表中必須包含
next
引數,而路由處理函式中只包含req
和res
-
next()
是實現多箇中介軟體連續呼叫的關鍵,它表示把流轉關係轉交給下一個中介軟體或路由
7.6.3 定義中介軟體
const mw = function (req, res, next) {
console.log("這是一個最簡單的中介軟體函式");
// 在當前中介軟體的業務處理完畢後,必須呼叫next()
// 表示把流轉關係轉交給下一給中介軟體或路由
next();
};
7.6.4 全域性生效的中介軟體
-
客戶端發起的任何請求到達伺服器後,都會觸發的中介軟體,叫做全域性生效的中介軟體
-
透過呼叫
app.use(中介軟體函式)
,即可定義一個全域性生效的中介軟體
const mw = function (req, res, next) {
console.log("這是一個最簡單的中介軟體函式");
next();
};
// 全域性生效的中介軟體
app.use(mw);
// 簡寫
app.use(function (req, res, next) {
console.log("這是一個最簡單的中介軟體函式");
next();
});
-
多箇中介軟體之間共享一份
req
和res
-
基於這樣的特性,可以在上游的中介軟體中,統一為
req
和res
物件新增自定義的屬性或方法,供下游的中介軟體或路由使用
const express = require("express");
const app = express();
app.use(function (req, res, next) {
req.name = "張三";
next();
});
app.use(function (req, res, next) {
res.age = 18;
next();
});
app.get("/", (req, res) => {
console.log(req.name, res.age);
res.send("Home page.");
});
app.listen(80, () => {
console.log("express server running at http://127.0.0.1");
});
7.6.5 定義多個全域性中介軟體
- 可以使用
app.use()
連續定義多個全域性中介軟體,客戶端請求到達伺服器之後,會按照中介軟體定義的順序依次進行呼叫
app.use(function (req, res, next) {
console.log("呼叫了第1個全域性中介軟體");
next();
});
app.use(function (req, res, next) {
console.log("呼叫了第2個全域性中介軟體");
next();
});
app.get("/", (req, res) => {
res.send("Home page.");
});
7.6.6 區域性生效的中介軟體
- 不使用
app.use()
定義的中介軟體,即區域性生效的中介軟體
const mw = function (req, res, next) {
console.log("這是中介軟體函式");
next();
};
app.get("/", mw, function (req, res) {
res.send("Home page.");
});
// mw這個中介軟體不會影響下面這個路由
app.get("/user", function (req, res) {
res.send("User page.");
});
7.6.7 定義多個區域性中介軟體
- 以下兩種方式都可以定義多個區域性中介軟體
app.get("/user", mw1, mw2, (req, res) => {
res.send("User page.");
});
app.get("/user", [mw1, mw2], (req, res) => {
res.send("User page.");
});
7.6.8 注意事項
-
一定要在路由之前註冊中介軟體
-
客戶端傳送過來的請求,可以連續呼叫多箇中介軟體進行處理
-
執行完中介軟體的業務程式碼後,要呼叫
next()
-
為防止程式碼邏輯混亂,呼叫
next()
後不要再寫額外程式碼 -
連續呼叫多箇中介軟體時,多箇中介軟體之間共享
req
和res
物件
7.6.9 分類
-
Express 官方把常見的中介軟體用法分成了 5 大類
-
應用級別的中介軟體
-
路由級別的中介軟體
-
錯誤級別的中介軟體
-
Express 內建的中介軟體
-
第三方的中介軟體
-
-
應用級別的中介軟體
- 透過
app.use()
、app.get()
等繫結到 app 例項上的全域性/區域性中介軟體
- 透過
-
路由級別的中介軟體
- 繫結到
express.Router()
例項上的中介軟體,其用法與應用級別的中介軟體沒有區別
- 繫結到
const app = express();
const router = express.Router();
// 路由級別的中介軟體
router.use((req, res, next) => {
console.log("Time:", Date.now());
next();
});
app.use("/", router);
-
錯誤級別的中介軟體
-
專門用來捕獲整個專案中發生的異常錯誤,從而防止專案異常崩潰的問題
-
格式:錯誤級別中介軟體的處理函式中含四個引數
function(err, req, res, next)
-
const express = require("express");
const app = express();
app.get("/", (req, res) => {
throw new Error("出錯了!");
res.send("Home page.");
});
app.use((err, req, res, next) => {
res.send(err.message);
});
app.listen(80, () => {
console.log("express server running at http://127.0.0.1");
});
-
注:錯誤級別的中介軟體必須註冊在所有路由之後,否則不生效!
-
Express 內建的中介軟體(常用的 3 個)
-
express.static()
:快速託管靜態資源(無相容性問題) -
express.json
:解析 JSON 格式的請求體資料(4.16.0+ 可用) -
express.urlencoded
:解析 URL-encoded 格式的請求體資料(4.16.0+ 可用)
-
// 配置解析application/json格式資料的內建中介軟體
app.use(express.json());
// 配置解析application/x-www-urlencoded格式資料的內建中介軟體
app.use(express.urlencoded({ extended: false }));
- 示例1:
const express = require("express");
const app = express();
app.use(express.json());
app.get("/user", (req, res) => {
// 沒配置express.json()中介軟體時,預設是undefined
// 配置之後:{ name: 'zhangsan', age: 18 }
console.log(req.body);
res.send("ok");
});
app.listen(80, () => {
console.log("express server running at http://127.0.0.1");
});
- 示例 2:
const express = require("express");
const app = express();
// 解析表單中的url-encoded格式的資料
app.use(express.urlencoded({ extended: false }));
app.post("/book", (req, res) => {
// 在伺服器中可以使用req.body來接收客戶端傳送過來的請求體資料
// 結果:[Object: null prototype] { bookname: '西遊記', count: '10' }
console.log(req.body);
res.send("ok");
});
app.listen(80, () => {
console.log("express server running at http://127.0.0.1");
});
-
第三方的中介軟體
-
由第三方開發出來的中介軟體。在專案中可以按需下載並配置第三方中介軟體,從而提高開發效率
-
此處以
body-parser
為例,該中介軟體用來解析請求體資料-
安裝:
npm i body-parser
-
匯入:
require('body-parser')
-
註冊使用:
app.use()
-
-
Express 內建的
express.urlencoded
中介軟體就是基於body-parser
進一步封裝出來的
-
const express = require("express");
const app = express();
const parser = require("body-parser");
// 解析表單中的url-encoded格式的資料
app.use(parser({ extended: false }));
app.post("/book", (req, res) => {
// 在伺服器中可以使用req.body來接收客戶端傳送過來的請求體資料
console.log(req.body);
res.send("ok");
});
app.listen(80, () => {
console.log("express server running at http://127.0.0.1");
});
7.6.10 自定義中介軟體
-
需求:模擬一個類似於
express.urlencoded
的中介軟體來解析 post 提交到伺服器的表單資料 -
實現步驟
-
定義中介軟體
-
監聽
req
的data
事件和end
事件 -
使用
querystring
模組解析請求體資料 -
將解析出來的資料物件掛載為
req.body
-
將自定義中介軟體封裝為模組
-
-
說明:
-
在中介軟體中,需要監聽
req
物件的data
事件來獲取客戶端傳送到伺服器的資料 -
如果資料量較大,無法一次性傳送完畢,則客戶端會把資料切割後分批傳送到伺服器,所以
data
事件可能會觸發多次,每次觸發data
事件時,獲取到資料只是完整資料的一部分,需要手動對接收到的資料進行拼接 -
當請求體資料接收完畢後,會自動觸發
req
的end
事件 -
因此,可以在
req
的end
事件中拿到並處理完整的請求體資料 -
Node.js 內建了
querystring
模組,專門用來處理查詢字串,透過該模組的parse()
可以將查詢字串解析成物件的格式 -
將解析出來的資料掛載為
req
的自定義屬性,命名為req.body
,供下游使用 -
最後將自定義的中介軟體封裝為獨立的模組
-
-
custom-body-parser/index.js
// 匯入querystring模組解析請求體資料
const qs = require("querystring");
const parser = (req, res, next) => {
// 儲存客戶端傳送過來的請求體資料
let str = "";
req.on("data", chunk => {
// 拼接請求體資料
str += chunk;
});
req.on("end", () => {
// 列印完整的請求體資料
console.log(str);
// 呼叫qs.parse()把查詢字串解析為物件,並掛載為req.body
req.body = qs.parse(str);
next();
});
};
module.exports = parser;
test.js
const express = require("express");
const app = express();
const parser = require("./custom-body-parser");
app.use(parser);
app.post("/book", (req, res) => {
// 在伺服器中可以使用req.body來接收客戶端傳送過來的請求體資料
console.log(req.body);
res.send("ok");
});
app.listen(80, () => {
console.log("express server running at http://127.0.0.1");
});
7.7 使用 Express 寫介面
7.7.1 建立基本的伺服器&建立 API 路由模組
test.js
const express = require("express");
const app = express();
const apiRouter = require("./apiRouter");
app.use("/api", apiRouter);
app.listen(80, () => {
console.log("express running at http://127.0.0.1");
});
apiRouter.js
const express = require("express");
const router = express.Router();
module.exports = router;
7.7.2 編寫 GET 介面
router.get("/get", (req, res) => {
// 獲取客戶端透過查詢字串傳送到伺服器的資料
const query = req.query;
// 呼叫res.send()把資料響應給客戶端
res.send({
status: 0, // 狀態:0表示成功,1表示失敗
msg: "GET請求成功!", // 狀態描述
data: query, // 需要響應給客戶端的具體資料
});
});
7.7.3 編寫 POST 介面
router.post("/post", (req, res) => {
// 獲取客戶端透過請求體傳送到伺服器的URL-encoded資料
const body = req.body;
// 呼叫res.send()方法把資料響應給客戶端
res.send({
status: 0, // 狀態:0表示成功,1表示失敗
msg: "POST請求成功!", // 狀態描述訊息
data: body, // 需要響應給客戶端的具體資料
});
});
-
注:如果要獲取
URL-encoded
格式的請求體資料,必須配置中介軟體app.use(express.urlencoded({extended: false}))
-
test.js
const express = require("express");
const app = express();
const apiRouter = require("./apiRouter");
app.use(express.urlencoded({ extended: false }));
app.use("/api", apiRouter);
app.listen(80, () => {
console.log("express running at http://127.0.0.1");
});
7.7.4 跨域問題
-
前面寫的 GET 和 POST 介面不支援跨域請求
-
當一個請求 url 的協議、域名、埠三者之間任意一個與當前頁面 url 不同即為跨域
-
解決介面跨域問題的方案主要有兩種
-
CORS:主流的解決方法,推薦使用
-
JSONP:有缺陷,只支援 GET 請求
-
7.7.5 使用 cors 中介軟體解決跨域問題
-
cors 是 Express 的第三方中介軟體,透過安裝和配置 cors 中介軟體,可以很方便地解決跨域問題
-
使用步驟
-
安裝:
npm install cors
-
匯入:
const cors = require('cors')
-
在路由之前呼叫
app.use(cors())
配置中介軟體
-
-
編寫簡單的 html 檔案測試
<body>
<button id="get">get</button>
<button id="post">post</button>
</body>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
const getBtn = document.querySelector("#get");
const postBtn = document.querySelector("#post");
getBtn.addEventListener("click", () => {
axios({
url: "/api/get",
method: "get",
query: {
name: "張三",
age: 18,
},
});
});
postBtn.addEventListener("click", () => {
axios({
url: "/api/post",
method: "get",
params: {
name: "李四",
age: 18,
},
});
});
</script>
test.js
const express = require("express");
const app = express();
const apiRouter = require("./apiRouter");
const cors = require("cors");
app.use(cors());
app.use(express.urlencoded({ extended: false }));
app.use("/api", apiRouter);
app.listen(80, () => {
console.log("express running at http://127.0.0.1");
});
7.7.6 CORS
-
CORS(Cross-Origin Resource Sharing,跨域資源共享):由一系列 HTTP 響應頭組成,這些 HTTP 響應頭決定瀏覽器是否阻止前端 js 程式碼跨域獲取資源
-
瀏覽器的同源安全策略預設會阻止網頁“跨域”獲取資源,但如果介面伺服器配置了 cors 相關的 http 響應頭,就可以解除瀏覽器端的跨域訪問限制
-
注意:
-
CORS 主要在伺服器端進行配置,客戶端瀏覽器無需做任何額外的配置,即可請求開啟了 CORS 的介面
-
CORS 在瀏覽器中有相容性,只支援
XMLHttpRequest Level2
的瀏覽器,才能正常訪問開啟了 CORS 的服務端介面(例如:IE 10+、Chrome4+、FireFox3.5+)
-
7.7.7 CORS 響應頭部
🛠️ Access-Control-Allow-Origin
- 響應頭部中可以攜帶
Access-Control-Allow-Origin
欄位,格式如下
Access-Control-Allow-Origin: <origin> | *
-
其中,origin 引數的值指定了允許訪問該資源的外域 url
-
例如,下面的欄位值只允許來自
http://itcast.cn
的請求
res.setHeader("Access-Control-Allow-Origin", "http://itcast.cn");
- 以下程式碼表示允許來自任何域的請求
res.setHeader("Access-Control-Allow-Origin", "*");
🛠️ Access-Control-Allow-Headers
-
預設情況下,CORS 僅支援客戶端向伺服器傳送如下的 9 個請求頭
Accept
、Accept-Language
、Content-Language
、DPR
、Downlink
、Save-Data
、Viewport-Width
、Width
、Content-Type
(值僅限於text/plain
、multipart/form-data
、application/x-www-form-urlencoded
三者之一)
-
如果客戶端向伺服器傳送了額外的請求體資訊,則需要在伺服器端透過
Access-Control-Allow-Headers
對額外的請求頭進行宣告,否則這次請求會失敗!
// 執行客戶端額外向伺服器傳送Content-Type請求頭和X-Custom-Header請求頭
// 注:多個請求頭之間用英文逗號隔開
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Custom-Header");
🛠️ Access-Control-Allow-Methods
-
預設情況下,CORS 僅支援客戶端發起 GET、POST、HEAD 請求
-
如果客戶端希望透過 PUT、DELETE 等方式請求伺服器的資源,則需要在伺服器端,透過
Access-Control-Allow-Methods
來指明實際請求所允許使用的 HTTP 方法
// 只允許 POST、GET、DELETE、HEAD 請求方法
res.setHeader("Access-Control-Allow-Methods", "POST, GET, DELETE, HEAD");
// 允許所有的HTTP請求方法
res.setHeader("Access-Control-Allow-Methods", "*");
7.7.8 CORS 請求的分類
-
客戶端在請求 CORS 介面時,根據請求方式和請求頭的不同,可以將 CORS 的請求分為兩大類
-
簡單請求
-
預檢請求
-
-
同時滿足以下兩大條件的請求,就屬於簡單請求
-
請求方式:GET、POST、HEAD 三者之一
-
HTTP 頭部資訊不超過以下幾種欄位:無自定義頭部欄位、
Accept
、Accept-Language
、Content-Language
、DPR
、Downlink
、Save-Data
、Viewport-Width
、Width
、Content-Type
(值僅限於text/plain
、multipart/form-data
、application/x-www-form-urlencoded
三者之一)
-
-
符合以下任何一個條件的請求,都需要進行預檢請求
-
請求方式為 GET、POST、HEAD 之外的請求 Method 型別
-
請求頭中包含自定義頭部欄位
-
向伺服器傳送了
application/json
格式的資料
-
-
在瀏覽器與伺服器正式通訊之前,瀏覽器會先傳送
OPTION
請求進行預檢,以獲知伺服器是否允許該實際請求,所以這一次的OPTION
請求成為預檢請求 -
伺服器成功響應預檢請求後,才會傳送真正的請求,並且攜帶真實資料
-
簡單請求和預檢請求的區別
-
簡單請求:客戶端與伺服器之間只發生一次請求
-
預檢請求:客戶端與伺服器之間傳送兩次請求,OPTION 預檢請求成功後,才會發起真正的請求
-
7.7.9 JSONP 介面
-
概念:瀏覽器端透過
<script>
標籤的src
屬性請求伺服器上的資料,同時伺服器返回一個函式的呼叫。這種請求資料的方式叫做JSONP
-
特點
-
JSONP
不屬於真正的Ajax
請求,因為它沒有使用XMLHttpRequest
這個物件 -
JSONP
僅支援GET
請求,不支援POST
、PUT
、DELETE
等請求
-
-
建立
JSONP
介面- 如果專案中已經配置了
CORS
跨域資源共享,為了防止衝突,必須在配置CORS
中介軟體之前宣告JSONP
的介面,否則JSONP
介面會被處理成開啟了CORS
的介面
- 如果專案中已經配置了
// 優先建立JSONP介面【這個介面不會被處理成CORS介面】
app.get("/api/jsonp", (req, res) => {});
// 再配置CORS中介軟體【後續的所有介面都會被處理為CORS介面】
app.use(cors());
// 這是一個開啟了CORS的介面
app.get("/api/get", (req, res) => {});
-
實現 JSONP 介面的步驟
-
獲取客戶端傳送過來的回撥函式的名字
-
得到要透過 JSONP 形式傳送給客戶端的資料
-
根據前兩步得到的資料,拼接出一個函式呼叫的字串
-
把上一步拼接得到的字串響應給客戶端的
<script>
標籤進行解析執行
-
app.get("/api/jsonp", (req, res) => {
// 獲取客戶端傳送過來的回撥函式的名字
const funcName = req.query.callback;
// 得到要透過 JSONP 形式傳送給客戶端的資料
const data = { name: "張三", age: 18 };
// 根據前兩步得到的資料,拼接出一個函式呼叫的字串
const str = `${funcName}(${JSON.stringify(data)})`;
// 把上一步拼接得到的字串響應給客戶端的`<script>`標籤進行解析執行
res.send(str);
});
- 由於 axios 沒有內建 jsonp,此處使用 jquery 傳送 ajax 請求
$("#jsonp").on("click", () => {
$.ajax({
method: "GET",
url: "http://127.0.0.1/api/jsonp",
dataType: "jsonp",
success: res => {
console.log(res);
},
});
});
7.8 在專案中運算元據庫
7.8.1 安裝並連線資料庫
-
安裝第三方模組:
npm i mysql
-
配置 mysql 模組,連線到 MySQL 資料庫
// 匯入
const mysql = require("mysql");
// 建立與MySQL資料庫的連線
const db = mysql.createPool({
host: "127.0.0.1", // 資料庫的ip地址
user: "root", // 登入資料庫的賬號
password: "root", // 登入資料庫的密碼
database: "test", // 指定要操作哪個資料庫
});
- 執行 SQL 語句,測試 mysql 模組是否正常工作
db.query("SELECT 1", (err, results) => {
if (err) return console.log(err.message);
// 只要能列印出[ RowDataPacket { '1': 1 } ],就證明資料庫連線正常
console.log(results);
});
7.8.2 查詢資料
- 如果執行的是
select
查詢語句,則執行的結果是陣列
// 查詢users表中的所有使用者資料
db.query("SELECT * FROM users", (err, results) => {
// 查詢失敗
if (err) return console.log(err.message);
// 查詢成功
console.log(results);
});
7.8.3 插入資料
-
如果執行的是
insert into
插入語句,則results
是一個物件 -
可以透過
affectedRows
屬性來判斷是否插入資料成功
// 要插入的資料
const user = { username: "zhangsan", password: "123456" };
// 待執行的sql語句,其中?表示佔位符
const sqlStr = "INSERT INTO users (username, password) VALUES (?, ?)";
// 使用陣列形式,依次為?佔位符指定具體的值
db.query(sqlStr, [user.username, user.password], (err, results) => {
if (err) return console.log(err.message);
if (results.affectedRows === 1) {
console.log("插入資料成功");
}
});
- 向表中新增資料時,如果資料物件的每個屬性和資料表的欄位一一對應,則可以透過以下方式快速插入資料
// 要插入的資料
const user = { username: "Tom", password: "123456" };
// 待執行的sql語句,其中?表示佔位符
const sqlStr = "INSERT INTO users SET ?";
// 直接將資料物件當作佔位符的值
db.query(sqlStr, user, (err, results) => {
if (err) return console.log(err.message);
if (results.affectedRows === 1) {
console.log("插入資料成功");
}
});
7.8.4 更新資料
- 執行
update
語句後,執行結果也是一個物件,可以透過affectedRows
判斷是否更新成功
// 要更新的資料
const user = { id: 2, username: "lisi", password: "654321" };
// 待執行的sql語句
const sqlStr = "UPDATE users SET username=?, password=? WHERE id=?";
// 使用陣列依次為佔位符指定具體的值
db.query(sqlStr, [user.username, user.password, user.id], (err, results) => {
if (err) return console.log(err.message);
if (results.affectedRows === 1) {
console.log("更新資料成功");
}
});
- 更新表資料時,如果資料物件的每個屬性和資料表的欄位一一對應,則可以透過以下方式快速更新資料
// 要更新的資料
const user = { id: 2, username: "lisi", password: "654321" };
// 待執行的sql語句
const sqlStr = "UPDATE users SET ? WHERE id=?";
// 使用陣列依次為佔位符指定具體的值
db.query(sqlStr, [user, user.id], (err, results) => {
if (err) return console.log(err.message);
if (results.affectedRows === 1) {
console.log("更新資料成功");
}
});
7.8.5 刪除資料
-
在刪除資料時,推薦根據
id
這樣的唯一標識來刪除對應的資料 -
執行
delete
語句之後,結果也是一個物件,也有affectedRows
屬性
// 要執行的sql語句
const sqlStr = "DELETE FROM users WHERE id=?";
// 注:如果sql語句中有多個佔位符,則必須使用陣列為每個佔位符指定具體的值
// 如果只有一個佔位符,則可以省略陣列
db.query(sqlStr, 7, (err, results) => {
if (err) return console.log(err.message);
if (results.affectedRows === 1) {
console.log("刪除資料成功");
}
});
-
標記刪除
-
使用
delete
語句會真正的把資料從表中刪除,為了防止誤刪,推薦使用標記刪除的形式來模擬刪除的動作 -
所謂標記刪除,就是在表中設定類似於
status
這樣的狀態欄位,來標記當前這條資料是否被刪除 -
當使用者執行了刪除的動作時,不是執行
delete
,而是update
,將這條資料對應的status
欄位標記為刪除即可
-
db.query("UPDATE users SET status=1 WHERE id=?", 6, (err, results) => {
if (err) return console.log(err.message);
if (results.affectedRows === 1) {
console.log("刪除資料成功");
}
});
8. 前後端的身份認證
8.1 Web 開發模式
-
目前主流的 Web 開發模式有兩種
-
基於伺服器渲染的傳統 Web 開發模式
-
基於前後端分離的新型 Web 開發模式
-
8.1.1 服務端渲染
- 伺服器傳送給客戶端的 HTML 頁面,是在伺服器透過字串的拼接動態生成的,因此,客戶端不需要使用 Ajax 額外請求頁面的資料
app.get("/index.html", (req, res) => {
// 要渲染的資料
const user = { name: "zs", age: 20 };
// 伺服器透過字串的拼接,動態生成HTML內容
const html = `<h1>姓名:${user.name},年齡:${user.age}</h1>`;
// 把生成好的頁面內容響應給客戶端,因此,客戶端拿到的是帶有真實資料的HTML頁面
res.send(html);
});
-
優點
-
前端耗時少:因為伺服器端負責動態生成 HTML 內容,瀏覽器只需要直接渲染頁面即可,尤其是移動端,更省電
-
有利於 SEO:因為伺服器端響應的是完整的 HTML 頁面內容,所以爬蟲更容易爬取獲得資訊,更有利於 SEO
-
-
缺點
-
佔用伺服器端資源:即伺服器端完成 HTML 頁面內容的拼接,如果請求較多,會對伺服器造成一定的訪問壓力
-
不利於前後端分離,開發效率低:使用伺服器端渲染,則無法進行分工合作,尤其對於前端複雜度高的專案,不利於專案高效開發
-
8.1.2 前後端分離
-
後端只負責提供 API 介面,前端使用 Ajax 呼叫介面的開發模式
-
優點
-
開發體驗好:前端專注於 UI 頁面的開發,後端專注於 api 的開發,且前端有更多的選擇性
-
使用者體驗好:Ajax 技術的廣泛應用,極大提高了使用者的體驗,可以輕鬆實現頁面的區域性重新整理
-
減輕了伺服器端的渲染壓力:因為頁面最終是在每個使用者的瀏覽器中生成的
-
-
缺點
-
不利於 SEO:因為完整的 HTML 頁面需要在客戶端動態拼接完成,所以爬蟲無法爬取頁面的有效資訊
-
解決:利用 Vue、React 等前端框架的 SSR(server side render)技術
-
-
SEO
- Search Engine Optimizatio(搜尋引擎最佳化),簡單來說,就是透過一系列的技術和策略,讓你的網站更容易被搜尋引擎(如 Google、Bing)收錄,並且在搜尋結果中排名靠前。
8.1.3 如何選擇?
-
不談業務場景而盲目選擇使用何種開發模式都是耍流氓
-
比如企業級網站,主要功能是展示而沒有複雜的互動,並且需要良好的 SEO,此時使用伺服器端渲染
-
類似後臺管理專案,互動性比較強,不需要考慮 SEO,則可以使用前後端分離的開發模式
-
具體使用何種開發模式並不是絕對的,為了同時兼顧首頁的渲染速度和前後端分離的開發效率,一些網站採用了首屏伺服器端渲染 + 其他頁面前後端分離的開發模式
8.2 身份認證
8.2.1 簡介
-
身份認證:又稱“身份驗證”、“鑑權”,是透過一定的手段完成對使用者身份的確認
-
日常生活中的身份認證隨處可見,如:高鐵的驗票乘車、手機的密碼或指紋解鎖等
-
在 Web 開發中,也涉及到使用者身份的認證,如:各大網站的手機驗證碼登入、郵箱密碼登入、二維碼登入等
-
不同開發模式下的身份認證
-
服務端渲染推薦使用Session 認證機制
-
前後端分離推薦使用JWT 認證機制
-
8.2.2 Session 認證機制
-
http 協議的無狀態性
- 客戶端的每次 http 請求都是獨立的,連續多個請求之間沒有直接的關係,伺服器不會主動保留每次 http 請求的狀態
-
如何突破 http 無狀態的限制
-
對於超市來說,為了方便收銀員在結算時給 VIP 使用者打折,超市可以為每個 VIP 使用者發放會員卡
-
現實生活中的會員卡身份認證方式,在 Web 開發中的專業術語叫做
Cookie
-
-
Cookie
是儲存在使用者瀏覽器中的一段不超過 4kb 的字串,它由一個名稱(Name)、一個值(Value)和其它幾個用於控制Cookie
有效期、安全性、使用範圍的可選屬性組成 -
不同域名下的
Cookie
各自獨立,每當客戶端發起請求時,會自動把當前域名下所有未過期的Cookie
一同傳送到伺服器 -
Cookie
的極大特性-
自動傳送
-
域名獨立
-
過期時限
-
4kb 限制
-
-
客戶端第一次請求伺服器時,伺服器透過響應頭的形式向客戶端傳送一個身份認證的
Cookie
,客戶端會自動將Cookie
儲存在瀏覽器中 -
隨後,當客戶端瀏覽器每次請求伺服器時,瀏覽器會自動將身份認證相關的
Cookie
透過請求頭的形式傳送給伺服器,伺服器即可驗明客戶端的身份
-
由於
Cookie
是儲存在瀏覽器中的,而且瀏覽器也提供了讀寫Cookie
的 API,因此Cookie
很容易被偽造,不具有安全性。因此不建議伺服器將重要的隱私資料透過Cookie
的形式傳送給瀏覽器 -
注:千萬不要使用
Cookie
儲存重要且隱私的資料,比如使用者的身份資訊、密碼等 -
為了防止客戶偽造會員卡,收銀員在拿到客戶出示的會員卡後,可以在收銀機上進行刷卡認證,只有收銀機確認存在的會員卡才能被正常使用
-
這種“會員卡 + 刷卡認證”的設計理念,就是
Session
認證機制的精髓
8.2.3 Session 的工作原理
8.2.4 在 Express 中使用 Session 認證
- 安裝
express-session
中介軟體
npm i express-session
- 註冊 session 中介軟體
// 匯入
const session = require("express-session");
// 配置session中介軟體
app.use(
session({
secret: "keyboard cat", // secret屬性的值可以為任意字串
resave: false, // 固定寫法
saveUninitialized: true, // 固定寫法
})
);
-
向 session 中存資料
- 當
express-session
中介軟體配置成功後,即可透過req.session
來訪問和使用session
物件,從而儲存使用者的關鍵資訊
- 當
// 登入的介面
app.post("/api/login", (req, res) => {
// 判斷使用者提交的登入資訊是否正確
if (req.body.username !== "admin" || req.body.password !== "000000") {
return res.send({ status: 1, msg: "登入失敗!" });
}
req.session.user = req.body; // 將使用者的資訊儲存到Session中
req.session.isLogin = true; // 將使用者的登入狀態儲存到session中
res.send({ status: 0, msg: "登入成功!" });
});
-
從 session 中取資料
- 直接從
req.session
物件上獲取之前儲存的資料
- 直接從
// 獲取使用者名稱的介面
app.get("/api/username", (req, res) => {
// 判斷使用者是否登入
if (!req.session.user.isLogin) {
return res.send({ status: 1, msg: "fail" });
}
res.send({ status: 0, msg: "success", username: req.session.user.username });
});
-
清空 session
- 呼叫
req.session.destory()
即可清空伺服器儲存的session
資訊
- 呼叫
// 退出登入的介面
app.post("/api/logout", (req, res) => {
// 清空當前客戶端對應的session資訊
req.session.destory();
res.send({
status: 0,
msg: "退出登入成功",
});
});
8.2.5 jwt
-
Session 認證機制需要配合 Cookie 才能實現,由於 Cookie 預設不支援跨域訪問,所以當涉及到前端跨域請求後端介面時,需要做很多額外的配置,才能實現跨域 Session 認證
-
注:
-
當前端請求後端介面不存在跨域問題時,推薦使用 Session 身份認證機制
-
當前端需要跨域請求後端介面時,不推薦使用 Session 身份認證機制,推薦使用 JWT 認證機制
-
-
JWT(JSON Web Token)是目前最流行的跨域認證解決方案
8.2.6 jwt 工作原理
- 使用者的資訊透過 Token 字串的形式儲存在客戶端瀏覽器中,伺服器透過還原 Token 字串的形式來認證使用者的身份
8.2.7 jwt 的組成部分
-
jwt 通常由三部分組成,分別是:Header(頭部)、Playload(有效荷載)、Signature(簽名)
-
三者之間使用
.
分隔
Header.Playload.Signature
- 示例
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
-
其中
-
Playload
部分才是真正的使用者資訊,它是使用者資訊經過加密之後生成的字串 -
Header
和Signature
是安全性相關的部分,只是為了保證Token
的安全性
-
-
使用方式
-
客戶端收到伺服器返回的 jwt 之後,通常會將它儲存在
localStorage
或sessionStorage
中 -
此後,客戶端每次與伺服器通訊,都要帶上這個 jwt 字串,從而進行身份認證
-
推薦的做法是把 jwt 放在 http 請求頭的
Authorization
欄位中
Authorization: Bearer <token>
-
8.2.8 在 Express 中使用 jwt
- 安裝
npm i jsonwebtoken express-jwt
-
其中
-
jsonwebtoken
用於生成 jwt 字串 -
express-jwt
用來將 jwt 字串解析還原成 JSON 物件
-
-
匯入
const jwt require('jsonwebtoken')
const { expressjwt } = require('express-jwt')
-
定義
secret
金鑰-
為了保證 jwt 字串的安全性,防止 jwt 字串在網路傳輸過程中被別人破解,需要定義一個用於加密和解密的 secret 金鑰
-
當生成 jwt 字串時,需要使用 secret 金鑰對使用者的資訊進行加密,最終得到加密好的 jwt 字串
-
當把 jwt 字串解析還原成 JSON 物件時,需要使用 secret 金鑰進行解密
// secret金鑰的本質是一個字串,任意,越複雜越好 const secretKey = "hello world";
-
-
在登入成功後生成 jwt 字串
- 呼叫
jsonwebtoken
提供的sign()
,將使用者資訊加密成 jwt 字串響應給客戶端
- 呼叫
app.post("/api/login", (req, res) => {
if (req.body.username !== "admin" || req.body.password !== "000000") {
return res.send({ status: 1, msg: "登入失敗!" });
}
// 使用者登入成功之後生成jwt字串,透過token屬性響應給客戶端
res.send({
status: 200,
message: "登入成功!",
// 呼叫jwt.sign()生成jwt字串
// 三個引數分別是:使用者資訊、加密金鑰、配置物件,可以配置當前token的有效期
token: jwt.sign({ username: req.body.username }, secretKey, { expiresIn: "30s" }),
});
});
-
將 jwt 字串還原為 JSON 物件
-
客戶端每次在訪問那些有許可權介面時,都需要主動透過請求頭中的
Authorization
欄位,將Token
字串傳送到伺服器進行身份認證 -
此時伺服器可以透過
express-jwt
這個中介軟體,自動將客戶端傳送過來的Token
解析還原成 JSON 物件 -
注:只要配置成功了 express-jwt 這個中介軟體,就可以把解析出來的使用者資訊掛載到
req.auth
屬性上
-
// expressJWT({secret: secretKey})用來解析Token
// .unless({path: [/^\/api\//]})用來指定哪些介面不需要訪問許可權
app.use(expressjwt({ secret: secretKey, algorithms: ["HS256"] }).unless({ path: [/^\/api\//] }));
-
使用
req.auth
獲取使用者資訊- 當
express-jwt
這個中介軟體配置成功後,即可在那些有許可權的介面中使用req.auth
物件,來訪問從 jwt 字串中解出來的使用者資訊了
- 當
app.get("/admin/getinfo", (req, res) => {
console.log(req.auth);
res.send({
status: 200,
message: "獲取使用者資訊成功!",
data: req.auth,
});
});
-
捕獲解析 jwt 失敗後產生的錯誤
-
當使用
express-jwt
解析Token
時,如果客戶端傳送過來的Token
過期或不合法,會產生一個解析失敗的錯誤,影響專案的正常執行 -
可以透過 Express 的錯誤中介軟體捕獲這個錯誤並進行相關的處理
-
app.use((err, req, res, next) => {
// token解析失敗導致的錯誤
if (err.name === "UnauthorizedError") {
return res.send({
status: 401,
message: "無效的token",
});
}
// 其他原因導致的錯誤
res.send({ status: 500, message: "未知錯誤" });
});