Express 教程第四部分:路由和控制器

在本教程中,我們將為 LocalLibrary 網站中最終需要的所有資源端點設定路由(URL 處理程式碼)和“虛擬”處理函式。完成後,我們將為路由處理程式碼提供一個模組化結構,我們可以在後續文章中用實際的處理函式來擴充套件它。我們還將對如何使用 Express 建立模組化路由有一個很好的理解!

預備知識 閱讀 Express/Node 簡介。完成之前的教程主題(包括 Express 教程第三部分:使用資料庫(Mongoose))。
目標 瞭解如何建立簡單路由。設定我們所有的 URL 端點。

概述

上一篇教程文章中,我們定義了 Mongoose 模型來與資料庫互動,並使用一個(獨立的)指令碼來建立一些初始的庫記錄。現在我們可以編寫程式碼來向用戶展示這些資訊。我們需要做的第一件事是確定我們希望能夠在頁面中顯示哪些資訊,然後定義用於返回這些資源的相應 URL。然後我們需要建立路由(URL 處理程式)和檢視(模板)來顯示這些頁面。

下圖提供了 HTTP 請求/響應處理時需要實現的資料流和主要事項的提醒。除了檢視和路由之外,該圖還顯示了“控制器”——將路由請求的程式碼與實際處理請求的程式碼分離開來的函式。

由於我們已經建立了模型,因此我們主要需要建立的是

  • “路由”將支援的請求(以及請求 URL 中編碼的任何資訊)轉發到適當的控制器函式。
  • 控制器函式,用於從模型中獲取請求的資料,建立顯示資料的 HTML 頁面,並將其返回給使用者在瀏覽器中檢視。
  • 控制器用於渲染資料的檢視(模板)。

Main data flow diagram of an MVC express server: 'Routes' receive the HTTP requests sent to the Express server and forward them to the appropriate 'controller' function. The controller reads and writes data from the models. Models are connected to the database to provide data access to the server. Controllers use 'views', also called templates, to render the data. The Controller sends the HTML HTTP response back to the client as an HTTP response.

最終我們可能會有頁面來顯示圖書、型別、作者和圖書例項的列表和詳細資訊,以及建立、更新和刪除記錄的頁面。在一篇文章中記錄這麼多內容是很困難的。因此,本文大部分內容將集中在設定我們的路由和控制器以返回“虛擬”內容上。我們將在後續文章中擴充套件控制器方法以處理模型資料。

下面的第一部分簡要介紹瞭如何使用 Express 的 Router 中介軟體。然後我們將在以下部分設定 LocalLibrary 路由時使用這些知識。

路由入門

路由是 Express 程式碼的一個部分,它將 HTTP 動詞(GETPOSTPUTDELETE 等)、URL 路徑/模式以及一個用於處理該模式的函式關聯起來。

有多種方法可以建立路由。在本教程中,我們將使用 express.Router 中介軟體,因為它允許我們將網站特定部分的路由處理程式分組在一起,並使用一個通用的路由字首來訪問它們。我們將所有與庫相關的路由儲存在“目錄”模組中,如果新增處理使用者帳戶或其他功能的路由,我們可以將它們單獨分組。

注意:我們在Express 簡介 > 建立路由處理程式中簡要討論了 Express 應用程式路由。除了為模組化提供更好的支援(如下面第一小節所述)之外,使用 Router 與直接在 Express 應用程式物件上定義路由非常相似。

本節的其餘部分概述瞭如何使用 Router 定義路由。

定義和使用單獨的路由模組

以下程式碼提供了一個具體的示例,說明我們如何建立路由模組,然後將其用於 Express 應用程式。

首先,我們建立了一個名為 wiki.js 的模組中的 wiki 路由。程式碼首先匯入 Express 應用程式物件,然後使用它獲取一個 Router 物件,並使用 get() 方法向其添加了幾個路由。最後,該模組匯出了 Router 物件。

js
// wiki.js - Wiki route module.

const express = require("express");

const router = express.Router();

// Home page route.
router.get("/", (req, res) => {
  res.send("Wiki home page");
});

// About page route.
router.get("/about", (req, res) => {
  res.send("About this wiki");
});

module.exports = router;

注意:上面我們直接在路由函式中定義了路由處理程式回撥。在 LocalLibrary 中,我們將在一個單獨的控制器模組中定義這些回撥。

要在我們的主應用程式檔案中使用路由模組,我們首先 require() 路由模組(wiki.js)。然後我們呼叫 Express 應用程式上的 use() 來將 Router 新增到中介軟體處理路徑,指定 URL 路徑為 'wiki'。

js
const wiki = require("./wiki.js");

// …
app.use("/wiki", wiki);

然後,我們 wiki 路由模組中定義的兩個路由將可透過 /wiki//wiki/about/ 訪問。

路由函式

我們上面的模組定義了幾個典型的路由函式。“about”路由(複製如下)是使用 Router.get() 方法定義的,該方法僅響應 HTTP GET 請求。此方法的第一個引數是 URL 路徑,第二個引數是接收到帶有該路徑的 HTTP GET 請求時將呼叫的回撥函式。

js
router.get("/about", (req, res) => {
  res.send("About this wiki");
});

回撥函式接受三個引數(通常按所示命名:reqresnext),它們將包含 HTTP 請求物件、HTTP 響應以及中介軟體鏈中的 next 函式。

注意:路由函式是Express 中介軟體,這意味著它們必須完成(響應)請求或呼叫鏈中的 next 函式。在上面的情況下,我們使用 send() 完成請求,因此 next 引數未使用(我們選擇不指定它)。

上面的路由函式只接受一個回撥,但您可以指定任意數量的回撥引數,或者一個回撥函式陣列。每個函式都是中介軟體鏈的一部分,並將按照新增到鏈中的順序被呼叫(除非前面的函式完成了請求)。

此處的 callback 函式在響應上呼叫 send(),以在接收到路徑為 (/about) 的 GET 請求時返回字串“About this wiki”。還有許多其他響應方法用於結束請求/響應週期。例如,您可以呼叫 res.json() 傳送 JSON 響應,或呼叫 res.sendFile() 傳送檔案。在構建庫時,我們將最常使用的響應方法是 render(),它使用模板和資料建立並返回 HTML 檔案——我們將在後續文章中詳細討論這一點!

HTTP 動詞

上面的示例路由使用 Router.get() 方法來響應具有特定路徑的 HTTP GET 請求。

Router 還為所有其他 HTTP 動詞提供了路由方法,它們的使用方式基本相同:post()put()delete()options()trace()copy()lock()mkcol()move()purge()propfind()proppatch()unlock()report()mkactivity()checkout()merge()m-search()notify()subscribe()unsubscribe()patch()search()connect()

例如,下面的程式碼與之前的 /about 路由行為相同,但只響應 HTTP POST 請求。

js
router.post("/about", (req, res) => {
  res.send("About this wiki");
});

路由路徑

路由路徑定義了可以發出請求的端點。我們目前看到的示例只是字串,並按原樣使用:'/'、'/about'、'/book'、'/any-random.path'。

路由路徑也可以是字串模式。字串模式使用正則表示式語法的一種形式來定義將匹配的端點“模式”。LocalLibrary 的大多數路由將使用字串而不是正則表示式。我們還將使用下一節中討論的路由引數。

路由引數

路由引數是命名的 URL 段,用於捕獲 URL 中特定位置的值。命名的段以冒號開頭,後跟名稱(例如,/:your_parameter_name/)。捕獲的值儲存在 req.params 物件中,使用引數名稱作為鍵(例如,req.params.your_parameter_name)。

因此,例如,考慮一個編碼包含使用者和圖書資訊的 URL:https://:3000/users/34/books/8989。我們可以如下所示提取此資訊,其中包含 userIdbookId 路徑引數

js
app.get("/users/:userId/books/:bookId", (req, res) => {
  // Access userId via: req.params.userId
  // Access bookId via: req.params.bookId
  res.send(req.params);
});

注意:URL /book/create 將由諸如 /book/:bookId 的路由匹配(因為 :bookId任何 字串的佔位符,因此 create 匹配)。將使用與傳入 URL 匹配的第一個路由,因此如果您想專門處理 /book/create URL,則其路由處理程式必須在您的 /book/:bookId 路由之前定義。

路由引數名稱(例如上面的 bookId)可以是任何以字母、_$ 開頭的有效 JavaScript 識別符號。您可以在第一個字元之後包含數字,但不能包含連字元和空格。您還可以使用不是有效 JavaScript 識別符號的名稱,包括空格、連字元、表情符號或任何其他字元,但您需要使用帶引號的字串定義它們,並使用括號表示法訪問它們。例如

js
app.get('/users/:"user id"/books/:"book-id"', (req, res) => {
  // Access quoted param using bracket notation
  const user = req.params["user id"];
  const book = req.params["book-id"];
  res.send({ user, book });
});

萬用字元

萬用字元引數匹配多個段中的一個或多個字元,將每個段作為陣列中的一個值返回。它們的定義方式與常規引數相同,但以星號為字首。

因此,例如,考慮 URL https://:3000/users/34/books/8989,我們可以使用 example 萬用字元提取 users/ 之後的所有資訊

js
app.get("/users/*example", (req, res) => {
  // req.params would contain { "example": ["34", "books", "8989"]}
  res.send(req.params);
});

可選部分

花括號可用於定義路徑中的可選部分。例如,下面我們匹配帶有任何副檔名(或沒有副檔名)的檔名。

js
app.get("/file/:filename{.:ext}", (req, res) => {
  // Given URL: https://:3000/file/somefile.md`
  // req.params would contain { "filename": "somefile", "ext": "md"}
  res.send(req.params);
});

保留字元

以下字元是保留的:(()[]?+!)。如果您想使用它們,必須用反斜槓 (\) 對它們進行轉義。

您也不能在正則表示式中使用管道符 (|)。

這就是開始使用路由所需的全部內容。如果需要,您可以在 Express 文件中找到更多資訊:基本路由路由指南。以下部分將展示我們如何為 LocalLibrary 設定路由和控制器。

處理路由函式中的錯誤和異常

前面顯示的路由函式都帶有 reqres 引數,分別代表請求和響應。路由函式還會傳遞第三個引數 next,其中包含一個回撥函式,可以呼叫該函式將任何錯誤或異常傳遞給 Express 中介軟體鏈,最終它們將傳播到您的全域性錯誤處理程式碼。

從 Express 5 開始,如果路由處理程式返回的 Promise 隨後被拒絕,則會自動使用拒絕值呼叫 next;因此,在使用 Promise 時,路由函式中不需要錯誤處理程式碼。在使用非同步基於 Promise 的 API 時,這會導致非常緊湊的程式碼,尤其是在使用 asyncawait 時。

例如,以下程式碼使用 find() 方法查詢資料庫,然後渲染結果。

js
exports.get("/about", async (req, res, next) => {
  const successfulResult = await About.find({}).exec();
  res.render("about_view", { title: "About", list: successfulResult });
});

以下程式碼展示了使用 Promise 鏈的相同示例。請注意,如果您願意,您可以 catch() 錯誤並實現自己的自定義處理。

js
exports.get(
  "/about",
  // Removed 'async'
  (req, res, next) =>
    About.find({})
      .exec()
      .then((successfulResult) => {
        res.render("about_view", { title: "About", list: successfulResult });
      })
      .catch((err) => {
        next(err);
      }),
);

注意:大多數現代 API 都是非同步且基於 Promise 的,因此錯誤處理通常就是這麼簡單。當然,這只是您在本教程中真正需要瞭解的錯誤處理!

Express 5 自動捕獲並轉發在同步程式碼中丟擲的異常

js
app.get("/", (req, res) => {
  // Express will catch this
  throw new Error("SynchronousException");
});

但是,您必須 catch() 路由處理程式或中介軟體呼叫的非同步程式碼中發生的異常。這些將不會被預設程式碼捕獲

js
app.get("/", (req, res, next) => {
  setTimeout(() => {
    try {
      // You must catch and propagate this error yourself
      throw new Error("AsynchronousException");
    } catch (err) {
      next(err);
    }
  }, 100);
});

最後,如果您使用的是舊式的非同步方法,這些方法在回撥函式中返回錯誤或結果,那麼您需要自己傳播錯誤。以下示例展示瞭如何操作。

js
router.get("/about", (req, res, next) => {
  About.find({}).exec((err, queryResults) => {
    if (err) {
      // Propagate the error
      return next(err);
    }
    // Successful, so render
    res.render("about_view", { title: "About", list: queryResults });
  });
});

更多資訊請參見錯誤處理

LocalLibrary 所需的路由

我們將最終用於頁面的 URL 列在下面,其中 object 替換為每個模型(書、圖書例項、型別、作者)的名稱,objectsobject 的複數形式,而 id 是每個 Mongoose 模型例項預設賦予的唯一例項欄位 (_id)。

  • catalog/ — 主頁/索引頁。
  • catalog/<objects>/ — 所有書籍、圖書例項、型別或作者的列表(例如,/catalog/books/、/catalog/genres/ 等)
  • catalog/<object>/<id> — 具有給定 _id 欄位值的特定圖書、圖書例項、型別或作者的詳細資訊頁面(例如,/catalog/book/584493c1f4887f06c0e67d37)
  • catalog/<object>/create — 建立新圖書、圖書例項、型別或作者的表單(例如,/catalog/book/create)
  • catalog/<object>/<id>/update — 用於更新具有給定 _id 欄位值的特定圖書、圖書例項、型別或作者的表單(例如,/catalog/book/584493c1f4887f06c0e67d37/update)
  • catalog/<object>/<id>/delete — 用於刪除具有給定 _id 欄位值的特定圖書、圖書例項、型別或作者的表單(例如,/catalog/book/584493c1f4887f06c0e67d37/delete)

第一個主頁和列表頁不編碼任何額外資訊。雖然返回的結果將取決於模型型別和資料庫中的內容,但執行獲取資訊的查詢將始終相同(同樣,用於物件建立的程式碼也將始終相似)。

相比之下,其他 URL 用於對特定文件/模型例項進行操作——這些 URL 將專案的標識編碼在 URL 中(如上所示為 <id>)。我們將使用路徑引數提取編碼資訊並將其傳遞給路由處理程式(在後面的文章中,我們將使用它來動態確定從資料庫中獲取哪些資訊)。透過將資訊編碼在 URL 中,我們只需要一個路由來處理特定型別資源的每個例項(例如,一個路由來處理每個圖書專案的顯示)。

注意:Express 允許您以任何方式構建 URL — 您可以將資訊編碼在 URL 的主體中,如上所示,或使用 URL GET 引數(例如,/book/?id=6)。無論您使用哪種方法,URL 都應保持簡潔、邏輯和可讀性(請在此處檢視 W3C 的建議)。

接下來,我們將為上述所有 URL 建立路由處理程式回撥函式和路由程式碼。

建立路由處理回撥函式

在定義路由之前,我們首先建立所有將呼叫的虛擬/骨架回調函式。這些回撥將儲存在 BookBookInstanceGenreAuthor 的獨立“控制器”模組中(您可以使用任何檔案/模組結構,但這似乎是本專案適當的粒度)。

首先,在專案根目錄(/controllers)中為控制器建立一個資料夾,然後為每個模型建立單獨的控制器檔案/模組

/express-locallibrary-tutorial  # the project root
  /controllers
    authorController.js
    bookController.js
    bookinstanceController.js
    genreController.js

作者控制器

開啟 /controllers/authorController.js 檔案並輸入以下程式碼

js
const Author = require("../models/author");

// Display list of all Authors.
exports.author_list = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Author list");
};

// Display detail page for a specific Author.
exports.author_detail = async (req, res, next) => {
  res.send(`NOT IMPLEMENTED: Author detail: ${req.params.id}`);
};

// Display Author create form on GET.
exports.author_create_get = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Author create GET");
};

// Handle Author create on POST.
exports.author_create_post = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Author create POST");
};

// Display Author delete form on GET.
exports.author_delete_get = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Author delete GET");
};

// Handle Author delete on POST.
exports.author_delete_post = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Author delete POST");
};

// Display Author update form on GET.
exports.author_update_get = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Author update GET");
};

// Handle Author update on POST.
exports.author_update_post = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Author update POST");
};

該模組首先需要我們將用於訪問和更新資料的 Author 模型。然後它匯出了我們將要處理的每個 URL 的函式。請注意,建立、更新和刪除操作使用表單,因此也有用於處理表單 POST 請求的附加方法——我們將在後面的“表單文章”中討論這些方法。

這些函式返回一個字串,指示相關頁面尚未建立。如果控制器函式預計會接收路徑引數,這些引數將輸出到訊息字串中(參見上方的 req.params.id)。

圖書例項控制器

開啟 /controllers/bookinstanceController.js 檔案並複製以下程式碼(這與 Author 控制器模組遵循相同的模式)

js
const BookInstance = require("../models/bookinstance");

// Display list of all BookInstances.
exports.bookinstance_list = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: BookInstance list");
};

// Display detail page for a specific BookInstance.
exports.bookinstance_detail = async (req, res, next) => {
  res.send(`NOT IMPLEMENTED: BookInstance detail: ${req.params.id}`);
};

// Display BookInstance create form on GET.
exports.bookinstance_create_get = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: BookInstance create GET");
};

// Handle BookInstance create on POST.
exports.bookinstance_create_post = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: BookInstance create POST");
};

// Display BookInstance delete form on GET.
exports.bookinstance_delete_get = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: BookInstance delete GET");
};

// Handle BookInstance delete on POST.
exports.bookinstance_delete_post = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: BookInstance delete POST");
};

// Display BookInstance update form on GET.
exports.bookinstance_update_get = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: BookInstance update GET");
};

// Handle bookinstance update on POST.
exports.bookinstance_update_post = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: BookInstance update POST");
};

型別控制器

開啟 /controllers/genreController.js 檔案並複製以下文字(這與 AuthorBookInstance 檔案遵循相同的模式)

js
const Genre = require("../models/genre");

// Display list of all Genre.
exports.genre_list = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Genre list");
};

// Display detail page for a specific Genre.
exports.genre_detail = async (req, res, next) => {
  res.send(`NOT IMPLEMENTED: Genre detail: ${req.params.id}`);
};

// Display Genre create form on GET.
exports.genre_create_get = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Genre create GET");
};

// Handle Genre create on POST.
exports.genre_create_post = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Genre create POST");
};

// Display Genre delete form on GET.
exports.genre_delete_get = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Genre delete GET");
};

// Handle Genre delete on POST.
exports.genre_delete_post = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Genre delete POST");
};

// Display Genre update form on GET.
exports.genre_update_get = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Genre update GET");
};

// Handle Genre update on POST.
exports.genre_update_post = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Genre update POST");
};

圖書控制器

開啟 /controllers/bookController.js 檔案並複製以下程式碼。這與其他的控制器模組遵循相同的模式,但額外有一個用於顯示站點歡迎頁面的 index() 函式

js
const Book = require("../models/book");

exports.index = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Site Home Page");
};

// Display list of all books.
exports.book_list = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Book list");
};

// Display detail page for a specific book.
exports.book_detail = async (req, res, next) => {
  res.send(`NOT IMPLEMENTED: Book detail: ${req.params.id}`);
};

// Display book create form on GET.
exports.book_create_get = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Book create GET");
};

// Handle book create on POST.
exports.book_create_post = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Book create POST");
};

// Display book delete form on GET.
exports.book_delete_get = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Book delete GET");
};

// Handle book delete on POST.
exports.book_delete_post = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Book delete POST");
};

// Display book update form on GET.
exports.book_update_get = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Book update GET");
};

// Handle book update on POST.
exports.book_update_post = async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Book update POST");
};

建立目錄路由模組

接下來,我們為 LocalLibrary 網站所需的所有 URL 建立路由,這些路由將呼叫我們在前幾節中定義的控制器函式。

骨架已經有一個 ./routes 資料夾,其中包含 indexusers 的路由。在此資料夾內建立另一個路由檔案 — catalog.js — 如下圖所示。

/express-locallibrary-tutorial # the project root
  /routes
    index.js
    users.js
    catalog.js

開啟 /routes/catalog.js 並複製以下程式碼

js
const express = require("express");

// Require controller modules.
const book_controller = require("../controllers/bookController");
const author_controller = require("../controllers/authorController");
const genre_controller = require("../controllers/genreController");
const book_instance_controller = require("../controllers/bookinstanceController");

const router = express.Router();

/// BOOK ROUTES ///

// GET catalog home page.
router.get("/", book_controller.index);

// GET request for creating a Book. NOTE This must come before routes that display Book (uses id).
router.get("/book/create", book_controller.book_create_get);

// POST request for creating Book.
router.post("/book/create", book_controller.book_create_post);

// GET request to delete Book.
router.get("/book/:id/delete", book_controller.book_delete_get);

// POST request to delete Book.
router.post("/book/:id/delete", book_controller.book_delete_post);

// GET request to update Book.
router.get("/book/:id/update", book_controller.book_update_get);

// POST request to update Book.
router.post("/book/:id/update", book_controller.book_update_post);

// GET request for one Book.
router.get("/book/:id", book_controller.book_detail);

// GET request for list of all Book items.
router.get("/books", book_controller.book_list);

/// AUTHOR ROUTES ///

// GET request for creating Author. NOTE This must come before route for id (i.e. display author).
router.get("/author/create", author_controller.author_create_get);

// POST request for creating Author.
router.post("/author/create", author_controller.author_create_post);

// GET request to delete Author.
router.get("/author/:id/delete", author_controller.author_delete_get);

// POST request to delete Author.
router.post("/author/:id/delete", author_controller.author_delete_post);

// GET request to update Author.
router.get("/author/:id/update", author_controller.author_update_get);

// POST request to update Author.
router.post("/author/:id/update", author_controller.author_update_post);

// GET request for one Author.
router.get("/author/:id", author_controller.author_detail);

// GET request for list of all Authors.
router.get("/authors", author_controller.author_list);

/// GENRE ROUTES ///

// GET request for creating a Genre. NOTE This must come before route that displays Genre (uses id).
router.get("/genre/create", genre_controller.genre_create_get);

// POST request for creating Genre.
router.post("/genre/create", genre_controller.genre_create_post);

// GET request to delete Genre.
router.get("/genre/:id/delete", genre_controller.genre_delete_get);

// POST request to delete Genre.
router.post("/genre/:id/delete", genre_controller.genre_delete_post);

// GET request to update Genre.
router.get("/genre/:id/update", genre_controller.genre_update_get);

// POST request to update Genre.
router.post("/genre/:id/update", genre_controller.genre_update_post);

// GET request for one Genre.
router.get("/genre/:id", genre_controller.genre_detail);

// GET request for list of all Genre.
router.get("/genres", genre_controller.genre_list);

/// BOOKINSTANCE ROUTES ///

// GET request for creating a BookInstance. NOTE This must come before route that displays BookInstance (uses id).
router.get(
  "/bookinstance/create",
  book_instance_controller.bookinstance_create_get,
);

// POST request for creating BookInstance.
router.post(
  "/bookinstance/create",
  book_instance_controller.bookinstance_create_post,
);

// GET request to delete BookInstance.
router.get(
  "/bookinstance/:id/delete",
  book_instance_controller.bookinstance_delete_get,
);

// POST request to delete BookInstance.
router.post(
  "/bookinstance/:id/delete",
  book_instance_controller.bookinstance_delete_post,
);

// GET request to update BookInstance.
router.get(
  "/bookinstance/:id/update",
  book_instance_controller.bookinstance_update_get,
);

// POST request to update BookInstance.
router.post(
  "/bookinstance/:id/update",
  book_instance_controller.bookinstance_update_post,
);

// GET request for one BookInstance.
router.get("/bookinstance/:id", book_instance_controller.bookinstance_detail);

// GET request for list of all BookInstance.
router.get("/bookinstances", book_instance_controller.bookinstance_list);

module.exports = router;

該模組需要 Express,然後使用它來建立一個 Router 物件。所有路由都在路由器上設定,然後匯出。

路由使用路由器物件的 .get().post() 方法定義。所有路徑都使用字串定義(我們不使用字串模式或正則表示式)。處理特定資源(例如書籍)的路由使用路徑引數從 URL 獲取物件 ID。

處理函式都從我們在上一節中建立的控制器模組中匯入。

更新索引路由模組

我們已經設定了所有新路由,但仍然有一個指向原始頁面的路由。現在,讓我們將它重定向到我們在路徑 /catalog 建立的新索引頁面。

開啟 /routes/index.js 並用下面的函式替換現有路由。

js
// GET home page.
router.get("/", (req, res) => {
  res.redirect("/catalog");
});

注意:這是我們第一次使用 redirect() 響應方法。此方法重定向到指定的頁面,預設情況下發送 HTTP 狀態碼“302 Found”。如果需要,您可以更改返回的狀態碼,並提供絕對或相對路徑。

更新 app.js

最後一步是將路由新增到中介軟體鏈中。我們在 app.js 中完成此操作。

開啟 app.js 並在其他路由下方引入目錄路由(新增下面顯示的第三行,在檔案中已有的另外兩行下方)

js
const indexRouter = require("./routes/index");
const usersRouter = require("./routes/users");
const catalogRouter = require("./routes/catalog"); // Import routes for "catalog" area of site

接下來,將目錄路由新增到其他路由下方的中介軟體堆疊中(新增下面顯示的第三行,在檔案中已有的另外兩行下方)

js
app.use("/", indexRouter);
app.use("/users", usersRouter);
app.use("/catalog", catalogRouter); // Add catalog routes to middleware chain.

注意:我們已在路徑 /catalog 處添加了我們的目錄模組。此路徑將新增到目錄模組中定義的所有路徑的前面。因此,例如,要訪問圖書列表,URL 將是:/catalog/books/

就這樣。現在,我們應該已經為 LocalLibrary 網站最終將支援的所有 URL 啟用了路由和骨架函式。

測試路由

要測試路由,首先使用您通常的方法啟動網站

  • 預設方法

    bash
    # Windows
    SET DEBUG=express-locallibrary-tutorial:* & npm start
    
    # macOS or Linux
    DEBUG=express-locallibrary-tutorial:* npm start
    
  • 如果您之前設定了 nodemon,則可以使用

    bash
    npm run serverstart
    

然後導航到一些 LocalLibrary URL,並驗證您沒有收到錯誤頁面 (HTTP 404)。下面列出了一些 URL,供您方便使用

  • https://:3000/
  • https://:3000/catalog
  • https://:3000/catalog/books
  • https://:3000/catalog/bookinstances/
  • https://:3000/catalog/authors/
  • https://:3000/catalog/genres/
  • https://:3000/catalog/book/5846437593935e2f8c2aa226
  • https://:3000/catalog/book/create

總結

現在我們已經為我們的網站建立了所有路由,以及可在後續文章中填充完整實現的虛擬控制器函式。在此過程中,我們學習了大量關於 Express 路由、異常處理以及構建路由和控制器的一些方法的​​基本資訊。

在下一篇文章中,我們將使用檢視(模板)和模型中儲存的資訊為網站建立一個合適的歡迎頁面。

另見