--- title: 'Express 教學 4: 路由與控制器' slug: Learn/Server-side/Express_Nodejs/routes translation_of: Learn/Server-side/Express_Nodejs/routes ---
在本教程中,我們將為最終在 本地圖書館 網站中需要的所有資源端點,搭配 "空殼" 處理函式來配置路由 (URL handling code) 。完成後,我們的路由處理源碼將會有模組化結構,在接下來的文章中,我們可以用真實的處理函式加以擴充。我們也會對如何使用Express 創建模組化路由,有更好的理解。
先備知識: | 閱讀 Express/Node 介紹。 完成先前教學主題 (包含 Express 教學 3: 使用資料庫 (Mongoose)). |
---|---|
目標: | 理解如何創建簡易路由配置。我們所有的URL端點。 |
在上一篇教程文章中,我們定義了Mongoose模型,以與數據庫互動,並使用(獨立)腳本創建一些初始庫記錄。現在我們可以編寫代碼,向用戶展示這些信息。我們需要做的第一件事,是確定我們希望能夠在頁面中顯示哪些信息,然後定義適當的URL,以返回這些資源。然後我們將需要創建路由(URL處理程序)和視圖(模板)來顯示這些頁面。
下圖是作為處理HTTP請求/響應時,需要實現的主要數據流和事項的提醒。除了視圖和路線之外,圖表還顯示“控制器” — 實際處理請求的函數,那些與路由請求分開的代碼。
由於我們已經創建了模型,我們需要創建的主要內容是:
最終,我們可能會有頁面顯示書籍,流派,作者和書籍的列表和詳細信息,以及用於創建,更新和刪除記錄的頁面。對一篇文章來說,這是很多的內容。因此,本文的大部分內容,都將集中在設置我們的路由和控制器,以返回“虛擬”內容。我們將在後續文章中,擴展控制器方法,以使用模型數據。
下面的第一部分,提供了關於如何使用Express Router中間件的簡要“入門”。當我們設置LocalLibrary路由時,我們將在後面的章節中使用這些知識。
路由是Express代碼的一部分,它將HTTP動詞(GET
, POST
, PUT
, DELETE
等),URL路徑/模式和被調用來處理該模式的函數,相關聯起來。
有幾種方法可以創建路線。本教程將使用express.Router
中間件,因為它允許我們將站點的特定部分的路由處理程序組合在一起,並使用通用的路由前綴訪問它們。我們會將所有與圖書館有關的路由,保存在“目錄”模塊中,如果我們添加路由來處理用戶帳戶或其他功能,我們可以將它們分開保存。
注意:我們在Express簡介>創建路由處理程序中,簡要討論了Express應用程序路由。除了為模塊化提供更好的支持之外(如下面第一小節所述),使用Router非常類似於直接在Express應用程序對像上定義路由。
本節的其餘部分,概述瞭如何使用路由器Router
來定義路由。
下面的代碼提供了一個具體示例,說明我們如何創建路由模塊,然後在Express應用程序中使用它。首先,我們在一個名為wiki.js的模塊中創建一個wiki的路由。代碼首先導入Express應用程序對象,使用它獲取一個
Router
對象,然後使用get()
方法向其添加一對路由。所有模塊的最後一個導出路由器Router
對象。
// wiki.js - Wiki route module.
var express = require('express');
var router = express.Router();
// Home page route.
router.get('/', function (req, res) {
res.send('Wiki home page');
})
// About page route.
router.get('/about', function (req, res) {
res.send('About this wiki');
})
module.exports = router;
注意:上面我們直接在路由器函數中定義路由處理程序回調。在LocalLibrary中,我們將在一個單獨的控制器模塊中,定義這些回調。
要在主應用程序文件中使用路由器模塊,我們首先require()
路由模塊(wiki.js)。然後,我們在Express應用程序上調用use()
,將路由器添加到中間件處理路徑,並指定一個'wiki'的URL路徑。
var wiki = require('./wiki.js');
// ...
app.use('/wiki', wiki);
然後可以從/wiki/
和/wiki/about/
,訪問我們的wiki路由模塊中定義的兩個路由。
我們上面的模塊,定義了幾個典型的路由功能。使用Router.get()
方法定義“about”路由(在下面),該方法僅響應HTTP GET請求。此方法的第一個參數是URL路徑,而第二個參數是一個回調函數,如果收到帶有路徑的HTTP GET請求,將會調用該函數。
router.get('/about', function (req, res) {
res.send('About this wiki');
})
回調函數接受三個參數(通常如下所示命名:req
, res
, next
),它將包含HTTP請求對象,HTTP響應,以及中間件鏈中的下一個函數。
注意:路由器功能是Express中間件,這意味著它們必須完成(響應)請求或調用鏈中的下一個功能next
。在上面的例子中,我們使用send()
完成了請求,所以下一個參數next
沒有被使用(我們選擇不指定它)。
上面的路由器函數只需要一次回調,但您可以根據需要指定任意數量的回調參數,或一組回調函數。每個函數都是中間件鏈的一部分,並且將按照添加到鏈中的順序調用(除非前面的函數完成請求)。
這裡的回調函數,在響應中調用send()
,當我們收到帶有路徑(' /about'
)的GET請求時,返回字符串“About this wiki”。有許多其他響應方法,可以結束請求/響應週期。例如,您可以調用res.json()
,來發送JSON響應,或調用res.sendFile()
來發送文件。構建庫時,我們最常使用的響應方法是render(),它使用模板和數據創建並返回HTML文件—我們將在後面的文章中,進一步討論這個問題!
上面的示例路由使用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請求。
router.post('/about', function (req, res) {
res.send('About this wiki');
})
路由路徑定義可以進行請求的端點。我們到目前為止看到的例子,都是字符串,並且完全按照字符串的寫法使用:'/','/ about','/ book','/any-random.path'。
路由路徑也可以是字符串模式。字符串模式使用正則表達式語法的子集,來定義將匹配的端點模式。下面列出了子集(請注意,連字符(-
)和點(.
)由字符串路徑字面解釋):
'/ab?cd'
的路徑路徑將匹配端點acd
或abcd
。'/ab+cd'
的路徑路徑將與端點abcd
,abbcd
,abbbcd
等匹配。'ab\*cd'
的路由路徑,將匹配端點abcd
, abXcd
, abSOMErandomTEXTcd
等。'/ab(cd)?e'
,表示以?號對(cd)進行匹配-它會匹配abe
和abcde
。(譯註:即(cd)必須為0個或1個。若為0,匹配abe
。若為1,匹配abcde
)路由路徑也可以是JavaScript正則表達式。例如,下面的路由路徑將與鯰魚catfish
和角鯊魚dogfish
相匹配,但不包括鯰魚catflap
、鯰魚頭catfishhead
等。請注意,正則表達式的路徑使用正則表達式語法(它不像以前那樣,是帶引號的字符串)。
app.get(/.*fish$/, function (req, res) {
...
})
注意: LocalLibrary的大部分路由,都只使用字符串,而不是字符串模式和正則表達式。我們還將使用下一節中討論的路由參數。
路徑參數是命名的URL段,用於捕獲在URL中的位置指定的值。命名段以冒號為前綴,然後是名稱(例如。捕獲的值,使用參數名稱作為鍵,存在對像中(例如)。/:your_parameter_name/
req.params
req.params.your_parameter_name
例如,考慮一個編碼的URL,其中包含有關用戶和書本的信息:http://localhost:3000/users/34/books/8989
。我們可以使用userId
和bookId
路徑參數,提取如下所示的信息:
app.get('/users/:userId/books/:bookId', function (req, res) {
// Access userId via: req.params.userId
// Access bookId via: req.params.bookId
res.send(req.params);
})
路由參數的名稱,必須由“單詞字符”(AZ,az,0-9和_)組成。
注意: URL /book/create將與/book/:bookId
之類的路由匹配(它將提取要創建' create
'的“bookId”值)。將使用與傳入URL匹配的第一個路由,因此,如果要單獨處理/book/create
URL,則必須在/book/:bookId
路由之前,先定義其路由處理程序。
這就是您開始使用路由所需的全部內容-如果需要,您可以在Express文檔中找到更多信息:基本路由和路由指南。以下部分顯示了我們如何為LocalLibrary設置路由和控制器。
下面列出了我們最終需要用於頁面的URL,其中object被替換為每個模型的名稱(book,bookinstance,genre,author),objects是對象的複數,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中(如上面的<id>
)。
我們將使用路徑參數,來提取編碼信息,並將其傳遞給路由處理程序(在稍後的文章中,我們將使用它來動態確定從數據庫獲取的信息)。通過對我們的URL中的信息進行編碼,我們只需要一個路由,用於特定類型的每個資源(例如,一個路由來處理每個書本項目的顯示)。
注意 : Express允許您以任何方式構建URL -您可以在URL正文中編碼信息,就像上面一樣,或使用URL GET
參數(例如/book/?id=6
)。無論您使用哪種方法,URL都應保持乾淨,合理且可讀(請在此處查看W3C建議)。
接下來,我們為所有上述URL,創建路由處理程序回調函數和路由代碼。
在我們定義路由之前,我們將首先創建它們將調用的所有虛擬/骨架回調函數。回調將存在Books,BookInstances,Genres 和Authors 的單獨“控制器” 模塊中(您可以使用任何文件/模塊結構,但這似乎是該項目的適當粒度)。
首先在項目根目錄(/controllers)中,為我們的控制器創建一個文件夾,然後創建單獨的控制器文件/模塊,來處理每個模型:
/express-locallibrary-tutorial //the project root /controllers authorController.js bookController.js bookinstanceController.js genreController.js
打開/controllers/authorController.js文件,並複制以下代碼:
var Author = require('../models/author'); // Display list of all Authors. exports.author_list = function(req, res) { res.send('NOT IMPLEMENTED: Author list'); }; // Display detail page for a specific Author. exports.author_detail = function(req, res) { res.send('NOT IMPLEMENTED: Author detail: ' + req.params.id); }; // Display Author create form on GET. exports.author_create_get = function(req, res) { res.send('NOT IMPLEMENTED: Author create GET'); }; // Handle Author create on POST. exports.author_create_post = function(req, res) { res.send('NOT IMPLEMENTED: Author create POST'); }; // Display Author delete form on GET. exports.author_delete_get = function(req, res) { res.send('NOT IMPLEMENTED: Author delete GET'); }; // Handle Author delete on POST. exports.author_delete_post = function(req, res) { res.send('NOT IMPLEMENTED: Author delete POST'); }; // Display Author update form on GET. exports.author_update_get = function(req, res) { res.send('NOT IMPLEMENTED: Author update GET'); }; // Handle Author update on POST. exports.author_update_post = function(req, res) { res.send('NOT IMPLEMENTED: Author update POST'); };
該模塊首先導入我們稍後將使用的模型,來訪問和更新我們的數據。然後它為我們希望處理的每個URL,導出函數(創建,更新和刪除操作使用表單,因此還有其他方法,來處理表單發布請求- 我們將在稍後的“表單文章” 中討論這些方法) 。
所有函數都具有Express中間件函數的標準形式,如果方法沒有完成請求週期,則會調用請求,響應和next
下一個函數的參數(在所有這些情況下,它都會執行!)。這些方法只返回一個字符串,表明尚未創建關聯的頁面。如果期望控制器函數接收路徑參數,則在消息字符串中,輸出這些參數(參見上面的req.params.id
)。
打開/controllers/bookinstanceController.js文件,並將其複製到以下代碼中(它遵循與Author
控制器模塊相同的模式):
var BookInstance = require('../models/bookinstance'); // Display list of all BookInstances. exports.bookinstance_list = function(req, res) { res.send('NOT IMPLEMENTED: BookInstance list'); }; // Display detail page for a specific BookInstance. exports.bookinstance_detail = function(req, res) { res.send('NOT IMPLEMENTED: BookInstance detail: ' + req.params.id); }; // Display BookInstance create form on GET. exports.bookinstance_create_get = function(req, res) { res.send('NOT IMPLEMENTED: BookInstance create GET'); }; // Handle BookInstance create on POST. exports.bookinstance_create_post = function(req, res) { res.send('NOT IMPLEMENTED: BookInstance create POST'); }; // Display BookInstance delete form on GET. exports.bookinstance_delete_get = function(req, res) { res.send('NOT IMPLEMENTED: BookInstance delete GET'); }; // Handle BookInstance delete on POST. exports.bookinstance_delete_post = function(req, res) { res.send('NOT IMPLEMENTED: BookInstance delete POST'); }; // Display BookInstance update form on GET. exports.bookinstance_update_get = function(req, res) { res.send('NOT IMPLEMENTED: BookInstance update GET'); }; // Handle bookinstance update on POST. exports.bookinstance_update_post = function(req, res) { res.send('NOT IMPLEMENTED: BookInstance update POST'); };
打開/controllers/genreController.js文件,並複制以下文本(這與Author
和BookInstance
文件的模式相同):
var Genre = require('../models/genre'); // Display list of all Genre. exports.genre_list = function(req, res) { res.send('NOT IMPLEMENTED: Genre list'); }; // Display detail page for a specific Genre. exports.genre_detail = function(req, res) { res.send('NOT IMPLEMENTED: Genre detail: ' + req.params.id); }; // Display Genre create form on GET. exports.genre_create_get = function(req, res) { res.send('NOT IMPLEMENTED: Genre create GET'); }; // Handle Genre create on POST. exports.genre_create_post = function(req, res) { res.send('NOT IMPLEMENTED: Genre create POST'); }; // Display Genre delete form on GET. exports.genre_delete_get = function(req, res) { res.send('NOT IMPLEMENTED: Genre delete GET'); }; // Handle Genre delete on POST. exports.genre_delete_post = function(req, res) { res.send('NOT IMPLEMENTED: Genre delete POST'); }; // Display Genre update form on GET. exports.genre_update_get = function(req, res) { res.send('NOT IMPLEMENTED: Genre update GET'); }; // Handle Genre update on POST. exports.genre_update_post = function(req, res) { res.send('NOT IMPLEMENTED: Genre update POST'); };
打開/controllers/bookController.js文件,並複制以下代碼。它遵循與其他控制器模塊相同的模式,但另外還有一個index()
函數,用於顯示站點歡迎頁面:
var Book = require('../models/book'); exports.index = function(req, res) { res.send('NOT IMPLEMENTED: Site Home Page'); }; // Display list of all books. exports.book_list = function(req, res) { res.send('NOT IMPLEMENTED: Book list'); }; // Display detail page for a specific book. exports.book_detail = function(req, res) { res.send('NOT IMPLEMENTED: Book detail: ' + req.params.id); }; // Display book create form on GET. exports.book_create_get = function(req, res) { res.send('NOT IMPLEMENTED: Book create GET'); }; // Handle book create on POST. exports.book_create_post = function(req, res) { res.send('NOT IMPLEMENTED: Book create POST'); }; // Display book delete form on GET. exports.book_delete_get = function(req, res) { res.send('NOT IMPLEMENTED: Book delete GET'); }; // Handle book delete on POST. exports.book_delete_post = function(req, res) { res.send('NOT IMPLEMENTED: Book delete POST'); }; // Display book update form on GET. exports.book_update_get = function(req, res) { res.send('NOT IMPLEMENTED: Book update GET'); }; // Handle book update on POST. exports.book_update_post = function(req, res) { res.send('NOT IMPLEMENTED: Book update POST'); };
接下來,我們為LocalLibrary 網站,創建所需全部URL 的路由,這將調用我們在上一節中定義的控制器功能。
骨架網站已經有一個./routes文件夾,其中包含索引和用戶的路由。在此文件夾中,創建另一個路徑文件— catalog.js —如下圖所示。
/express-locallibrary-tutorial //the project root /routes index.js users.js catalog.js
打開/routes/ catalog.js,複製下面的代碼:
var express = require('express'); var router = express.Router(); // Require controller modules. var book_controller = require('../controllers/bookController'); var author_controller = require('../controllers/authorController'); var genre_controller = require('../controllers/genreController'); var book_instance_controller = require('../controllers/bookinstanceController'); /// 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並使用下面的函數,替換現有路由。
// GET home page. router.get('/', function(req, res) { res.redirect('/catalog'); });
注意:這是我們第一次使用redirect()響應方法。這會重定向到指定的頁面,默認情況下會發送HTTP狀態代碼“302 Found”。您可以根據需要,更改返回的狀態代碼,並提供絕對路徑或相對路徑。
最後一步,是將路由,添加到中間件鏈。我們在app.js
這樣做。
打開app.js,並要求其他路由下方的目錄路由(添加下面顯示的第三行,在其他兩個路由下面):
var indexRouter = require('./routes/index'); var usersRouter = require('./routes/users'); var catalogRouter = require('./routes/catalog'); //Import routes for "catalog" area of site
接下來,將目錄路由,添加到其他路由下面的中間件堆棧(添加下面顯示的第三行,在其他兩行下面):
app.use('/', indexRouter); app.use('/users', usersRouter); app.use('/catalog', catalogRouter); // Add catalog routes to middleware chain.
Note: 我們已在路徑'/catalog'
中添加了目錄模塊。它預先添加到目錄模塊中定義的所有路徑。例如,要訪問書本列表,URL將為:/catalog/books/
。
就是這樣。現在應該為我們最終在LocalLibrary 網站上支持的所有URL,啟用路由和框架功能。
要測試路由,首先使用您通常的方法啟動網站
// Windows
SET DEBUG=express-locallibrary-tutorial:* & npm start
// macOS or Linux
DEBUG=express-locallibrary-tutorial:* npm start
// Windows
SET DEBUG=express-locallibrary-tutorial:* & npm run devstart
// macOS or Linux
DEBUG=express-locallibrary-tutorial:* npm run devstart
然後瀏覽一些上面的LocalLibrary URL,並驗證您沒有收到錯誤頁面(HTTP 404)。為方便起見,下面列出了一小組網址:
我們現在為網站創建了所有的路由,在稍後的教程,我們可以將實作完成的代碼,填入到空殼控制器函式。以這樣的方式,我們學到了許多關於Express 路由的基本信息,以及一些組織路由和控制器的方式。
下一篇文章,我們將使用視圖(模板) 和存在模型裡的信息,為網站創建一個合適的歡迎頁面。
{{PreviousMenuNext("Learn/Server-side/Express_Nodejs/mongoose", "Learn/Server-side/Express_Nodejs/Displaying_data", "Learn/Server-side/Express_Nodejs")}}