NodeJS 與 Express

SRIGT發表於2024-06-20

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)環境搭建

  1. 官網下載最新版 Node.js 並安裝

  2. 使用命令 node --version 確認安裝是否成功

  3. 建立一個目錄,其中新建 index.js

    console.log("Hello, world");
    
  4. 在該目錄下,使用命令 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 模組化寫法

  1. 在根目錄建立目錄 module,其中新建 mod.js

    const module = {};
    
    export default module;
    
  2. 修改 index.js,其中使用 ES 寫法引入 module/mod.js

    import module from "./module/mod.js";
    
    console.log(module);
    

    此時執行 index.js 後會報錯,可以透過修改配置檔案解決

  3. 修改 package.json

    {
      // ...
      "type": "module"
    }
    

    此時可以正常執行 index.js

  4. 修改 mod.js,實現多個暴露

    const module1 = {
      get() {
        return "module1";
      },
    };
    const module2 = {
      get() {
        return "module2";
      },
    };
    
    export {
      module1,
      module2,
    };
    
  5. 修改 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 的映象原管理工具

    1. 使用命令 npm install -g nrm 全域性安裝 nrm
    2. 使用命令 nrm ls 檢視可用源,* 表示當前使用的源
    3. 使用命令 nrm use xxx 切換到 xxx 源
    4. 使用命令 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 方法:將物件格式化成 URL

    const 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. 基礎

  1. 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");
    });
    
  2. 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 };
    
  3. module/renderStatus.js

    function renderStatus(url) {
      const routes = ["/", "/api"];
      return routes.includes(url) ? 200 : 404;
    }
    
    exports.renderStatus = renderStatus;
    
  4. 使用命令 nodemon .\index.js 執行,並訪問 http://localhost:8000/http://localhost:8000/api

II. cors

  1. 修改 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");
    });
    
  2. 修改 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

  1. 修改 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");
    });
    
  2. 修改 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. 資料夾操作

  1. 建立

    fs.mkdir("./newFolder", (err) => {
      if (err && err.code === "EEXIST") {
        console.log("The folder already exists");
      } else {
        console.log("The folder has been created");
      }
    });
    
  2. 重新命名

    fs.renamedir("./newFolder", "./folder", (err) => {
      if (err && err.code === "ENOENT") {
        console.log("The folder does not exist");
      } else {
        console.log("The folder renamed successfully");
      }
    });
    
  3. 刪除

    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");
      }
    });
    
  4. 內容檢視

    fs.readdir("./folder", (err, data) => {
      if (err) {
        console.log(err);
      } else {
        console.log(data);
      }
    });
    
  5. 屬性檢視

    fs.stat("./folder", (err, data) => {
      console.log(data)
    
      // 判定是否為檔案型別
      console.log(data.isFile())
    
      // 判定是否為資料夾型別
      console.log(data.isDirectoty())
    })
    

II. 檔案操作

  1. 建立與覆寫

    fs.writeFile("./folder/text.txt", "Hello, world!", (err) => {
      console.log(err);
    });
    
  2. 續寫

    fs.appendFile("./folder/text.txt", "Hello, world!", (err) => {
      console.log(err);
    });
    
  3. 讀取

    fs.readFile("./folder/text.txt", (err, data) => {
      if (err) {
        console.log(err);
      } else {
        console.log(data.toString("utf-8"));
      }
    });
    
  4. 刪除

    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
  1. 編寫頁面

  2. 編寫路由: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"):獲取靜態資源方法
  3. 編寫服務端: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

  1. 在頁面中建立表單併傳送請求

    <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>
    
  2. 修改 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

  1. 調整請求方法

    <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>
    
  2. 修改 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. 第一個專案

  1. 使用命令 npm install express --save 安裝 Express

  2. 在根目錄下建立 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}!`));
    
  3. 使用命令 node .\index.js 執行專案

  4. 訪問 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 渲染模板檔案

    1. 使用命令 npm install ejs 安裝模板引擎

    2. 在根目錄建立目錄 views

      • views 中包含模板檔案,如 index.ejs 等
    3. 修改 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)生成器

  1. 使用命令 npm install -g express-generatore 全域性安裝 Express 生成器

  2. 使用命令 express myapp --view=ejs 生成 Express 專案

    • 上述命令不可用時,可以使用命令 npx express-generator --view=ejs myapp
  3. 使用命令 cd myapp 進入專案目錄

  4. 使用命令 npm install 安裝相關依賴

  5. 修改 package.json,使用熱更新

    {
      "scripts": {
        "start": "nodemon ./bin/www"
      },
    }
    
  6. 使用命令 npm start 啟動專案

  7. 訪問 http://localhost:3000/

0x03 MongoDB

(1)概述

詳見 《MongoDB | 部落格園-SRIGT

(2)使用 Node.js 操作

  1. 在根目錄新建 config 目錄,其中新建 db.config.js,用於連線資料庫

    const mongoose = require("mongoose");
    
    mongoose.connect("mongodb://127.0.0.1:27017/node_project");
    
  2. 在 bin/www 中匯入 db.config.js,用於匯入資料庫配置

    require("../config/db.config");
    
  3. 在根目錄新建 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;
    
  4. 修改 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");
    });
    
  5. 查詢資料

    UserModel.find(
      { name: "John" },
      ["username", "password"].sort({ _id: -1 }).skip(10).limit(10)
    );
    
  6. 更新資料(單條)

    const { name, username, password } = req.body;
    UserModel.updateOne({ _id }, {username, password});
    
  7. 刪除資料(單條)

    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
    1. 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;
      
    2. 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;
      
    3. 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;
      
    4. 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)-->>服務端:校驗成功 服務端->>服務端:介面處理 服務端-->>瀏覽器:介面返回
  1. 修改 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在資料庫中的存活時間(毫秒)
        }),
      })
    );
    
  2. 修改 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;
    
  3. 修改 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 就會丟失;而儲存在資料庫中就不會丟失

  4. 修改 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");
      }
    });
    
  5. 修改 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. 實現

僅限於前後端分離專案使用

  1. 使用命令 npm install -S jsonwebtoken 安裝 JWT

  2. 根目錄下新建 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;
    
  3. 修改 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;
    
  4. 在前端使用 Axios 的攔截器,將 token 儲存在瀏覽器本地儲存

  5. 修改 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-

相關文章