前面我們介紹了 Express 以及 Middleware 的概念,這一篇我們將會繼續介紹 Express.js。
前面我們用了以下範例來示範如何使用 Express:
const express = require('express');
const app = express();
const port = 3000;
const data = [];
app.get('/todos', (req, res) => {
res.send(data);
});
app.post('/todos', (req, res) => {
const { title, completed } = req.body;
data.push({
id: new Date().getTime(),
title: cacheBody.title,
completed: cacheBody.completed,
});
res.send(data);
});
app.listen(3000)
其中也包含了 Middleware 範例
const express = require('express');
const app = express();
const jsonParserMiddleware = (req, res, next) => {
let data = '';
// 監聽數據流事件,並將數據流串接起來
req.on('data', (chunk) => {
data += chunk.toString();
});
// 監聽數據流結束事件,並將解析後的 JSON 資料附加到請求物件的 body 屬性
req.on('end', () => {
try {
// 將解析後的 JSON 資料附加到請求物件的 body 屬性
req.body = JSON.parse(data);
// 繼續執行後續的 Middleware 或路由處理函式
next();
} catch (error) {
// JSON 解析失敗,回傳錯誤訊息
res.status(400).json({ error: 'Invalid JSON data' });
}
});
}
app.use(jsonParserMiddleware);
app.post('/', (req, res) => {
res.send(req.body);
});
app.listen(3000)
接下來這一篇將會延續 Express.js 來去介紹路由(Routing)的概念。
那麼路由其實在 Web 開發上是極其重要的一環,你要說它是把網頁連結起來的橋樑也不為過,因為它就是用來連結網頁的,因此它也是整個網站核心組成之一。
ok,廢話那麼多我們就來假設一下情境好了。
假設,你今天在瀏覽器上輸入 https://israynotarray.com/
,那麼你會看到我的部落格「首頁」(趁機會業配),那麼 https://israynotarray.com/
這個 URL 其實是對應到我們的「首頁」路由,而這個路由就是 /
。
Note
https://israynotarray.com/
就是所謂的 URL,如果你對於 URL 沒有概念的話,可以參考我先前寫的文章「(22) 試著學 Hexo - SEO 篇 - 先來聊聊 Url 對於 SEO 的影響」。
接著,當你輸入 https://israynotarray.com/links/
,就代表你進入了我的「更多資訊」連結頁面,那麼這個路由就會是 /links
。
我們的網站會依據使用者所輸入的路由來決定要回傳什麼內容,根據 URL 的路徑將使用者請求的資源導向到正確的處理函式,而這個函式就會依據我們的需求來回傳不同的內容。
當然,這只是一個簡單的例子,實際上路由的應用是非常廣泛的,例如像是:
https://israynotarray.com/
:首頁https://israynotarray.com/posts/
:文章列表https://israynotarray.com/posts/:id
:文章內容,:id
代表文章的 ID,而這又稱為動態路由https://israynotarray.com/search?keyword=Express
:搜尋頁面,?keyword=Express
代表搜尋的關鍵字,而這又稱為查詢字串。我相信你到現在應該對於路由比較有概念了,就讓我們回來前面我們所寫的 Express.js 範例吧?!
前面我們有簡單的寫了一下路由範例:
//...略過其他程式碼
app.get('/todos', (req, res) => {
//...略過其他程式碼
});
app.post('/todos', (req, res) => {
//...略過其他程式碼
});
//...略過其他程式碼
在這個範例的路由其實只有一個,就是 /todos
,而這個路由其實就是我們的 API 路由,也就是說當我們輸入 http://localhost:3000/todos
時,就會進入到這個路由,只是因為我們依據了 HTTP 的方法來區分不同的行為,因此我們才會有 app.get
以及 app.post
。
如果你想要實現動態路由,那麼就可以這樣寫:
app.get('/todos/:id', (req, res) => {
const { id } = req.params; // 取得動態路由的參數
//...略過其他程式碼
});
那查詢字串(Query String)呢?
app.get('/todos', (req, res) => {
const { keyword } = req.query; // 取得查詢字串的參數
//...略過其他程式碼
});
有趣的是查詢字串並不需要特別的設定,只要你的 URL 中有 ?
就會自動被解析成查詢字串,而且你可以透過 req.query
來取得查詢字串的參數。
查詢字串通常會用在搜尋的時候,例如像是 https://israynotarray.com/search?keyword=Express
,這個 URL 就是用來搜尋關鍵字為 Express
的文章;另外,實戰開發上也很常見用於分頁及搭配關鍵字,例如像是 https://israynotarray.com/posts?page=1&keyword=Express
,這個 URL 就是用來取得第一頁的文章,並且搜尋關鍵字為 Express
的文章。
Note
如果你有多個查詢字串,那麼你可以透過&
來串接;通常查詢字串是由一個?
開頭,後面接著查詢字串的參數,而每個參數之間則是用&
來串接,因此會有?
、&
與=
這三個符號。
那麼關於查詢字串這邊有件事情要特別提醒一下,如果查詢字串中包含特殊字符、空格或非英數字元的話,你就必須額外處理,否則會造成錯誤。
什麼意思呢?我們知道一個查詢字串的組合會是這樣子的:
?page=1&keyword=Express
因此,如果你的 keyword 預期要傳入「ray&array」的話,那麼你的查詢字串就會變成這樣子:
?page=1&keyword=ray&array
這樣在解析的時候就會造成錯誤,因此你必須要將特殊字符、空格或非英數字元轉換成 URL 編碼,例如像是:
?page=1&keyword=ray%26array
Note
URL 編碼(又稱百分號編碼、URL 轉譯)其實就是將特殊字符、空格或非英數字元轉換成%
加上十六進位的編碼
這邊也稍微提一下前端怎麼做,通常我們會透過 encodeURIComponent
來處理,例如像是:
const keyword = 'ray&array';
const encodedKeyword = encodeURIComponent(keyword);
console.log(encodedKeyword); // ray%26array
對於查詢字串常見的雷點有一點概念後,我們就回來 Express.js 吧!
其實實戰開發來講,我們通常會將路由寫在不同的檔案中,例如像是:
// routes/todos.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
//...略過其他程式碼
});
router.post('/', (req, res) => {
//...略過其他程式碼
});
module.exports = router;
// app.js
const express = require('express');
//...略過其他程式碼
const todosRouter = require('./routes/todos');
app.use('/todos', todosRouter);
//...略過其他程式碼
為什麼會這樣做呢?其主要原因是實戰開發上我們的路由不可能只有少少那幾隻,一個專案可能會有數十個路由,因此我們必須要將路由分類,而這樣的寫法就是將路由分類的一種方式。
透過這種方式我們可以使程式碼更具有組織性和可讀性,但如果你的專案如果沒有很複雜的話確實是可以不用這樣做,但如果你的專案很複雜的話,那麼這樣的寫法就是必須的。
剛有提到路由會區分資料夾,那麼這邊也來提一下專案資料夾的結構。
其實專案的資料夾並沒有很硬性的規定應該要長怎麼樣,我這邊就只列出幾個常見的資料夾:
routes
:路由controllers
:控制器models
:模型middlewares
:中介軟體public
:靜態資源views
:視圖utils
:工具tests
:測試config
:設定services
:服務雖然以上是一個常見的資料夾結構,但實際上你可以依照你的需求來做調整,例如像是我們的專案不需要測試,那麼 tests
這個資料夾就可以不用建立,又或者你的專案是屬於純 API 專案,那麼 views
這個資料夾就可以不用建立。
Note
Controllers + Models + Views 三者又稱 MVC 架構,早期沒有 MVC 架構時,程式碼通常會寫在一起,這樣的寫法會造成程式碼難以維護,因此 MVC 架構就是為了解決這個問題而生的,可詳見此篇文章「Day8-從基礎學習 ThinkPHP-MVC 模式」。
那麼這邊除了基本的 MVC 架構資料夾、前面介紹過的 middlewares 與 routes 之外,我就稍微針對其他資料夾稍微補充說明一下。
public 資料夾大部分是拿來放靜態資源的,例如像是圖片、CSS、JavaScript 等等,而這些資源通常是不需要經過處理的,因此我們可以直接將這些資源放在 public 資料夾中,這樣的好處是我們可以直接透過 URL 來取得這些資源,例如像是 https://israynotarray.com/images/logo.png
。
Note
雖然 Public 主要是放靜態資源,但請不要把敏感資源放在這邊,例如像是密碼、金鑰等等,因為這些資源是可以直接透過 URL 取得的,因此如果你把這些資源放在 Public 資料夾中的話,那麼就會造成資安問題。
utils 全名是 utilities,主要常見放置一些工具類型的通用程式,例如...
時間處理工具:
// utils/dateUtils.js
function formatDate(date, format) {
// 日期格式轉換的邏輯
}
module.exports = formatDate;
又或者是 Email 驗證工具:
// utils/emailUtils.js
function validateEmail(email) {
// 驗證 Email 的邏輯
}
module.exports = validateEmail;
這些比較通用類型的程式碼通常會放在 utils 資料夾中,這樣的好處是我們可以直接透過 require
來引入,例如像是:
const formatDate = require('./utils/dateUtils');
const date = new Date();
const formattedDate = formatDate(date, 'yyyy-MM-dd');
其實這個資料夾就比較簡單一點,通常常見會放一些跟專案有關的設定檔案,例如像是:
等等,當然還有很多,這邊就不一一列舉了。
services 通常會放一些跟商業邏輯有關的程式碼,例如像是部落格相關的邏輯:
// services/ArticleService.js
const Article = require('../models/Article');
function createArticle(title, content) {
// 新增一篇新文章
const newArticle = articleModel.create({ title, content });
return newArticle;
}
function getArticleById(articleId) {
// 根據文章 ID 取得文章
const article = articleModel.findById(articleId);
return article;
}
function getAllArticles() {
// 取得所有文章
const articles = articleModel.find();
return articles;
}
function updateArticle(articleId, newData) {
// 更新文章內容
const updatedArticle = articleModel.findByIdAndUpdate(articleId, newData, { new: true });
return updatedArticle;
}
function deleteArticle(articleId) {
// 刪除文章
const deletedArticle = articleModel.findByIdAndDelete(articleId);
return deletedArticle;
}
module.exports = {
createArticle,
getArticleById,
getAllArticles,
updateArticle,
deleteArticle
};
透過這種可以讓原本比較複雜的 Controller 變得更加簡潔,而且也可以讓程式碼更具有組織性和可讀性。
Note
當 Controller 太過複雜時,就會抽出一些商業邏輯到 Service 中,這樣的好處是可以讓 Controller 變得更加簡潔,讓 Controller 專注於處理請求,而 Service 專注於處理商業邏輯。
當然,上面這些資料夾的結構都只是一個參考而已,實際上還是會依照自己專案的需求來做調整,但如果你的專案沒有很複雜的話,那麼其實也不用太過於在意這些資料夾的結構,畢竟這些資料夾的結構主要是為了讓程式碼更具有組織性和可讀性,而不是為了硬性規定一定要長怎麼樣。
那麼這一篇也差不多了,我們下一篇見囉~
最近晚上睡到腰痠背痛的,嘗試睡前拉筋,也嘗試過運動等等,床也是硬床,但過陣子又會恢復正常 QQ...