Day8 - 再續 Express.js

再續

前言

前面我們介紹了 Express 以及 Middleware 的概念,這一篇我們將會繼續介紹 Express.js。

路由

前面我們用了以下範例來示範如何使用 Express:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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 範例吧?!

前面我們有簡單的寫了一下路由範例:

1
2
3
4
5
6
7
8
9
10
11
//...略過其他程式碼

app.get('/todos', (req, res) => {
//...略過其他程式碼
});

app.post('/todos', (req, res) => {
//...略過其他程式碼
});

//...略過其他程式碼

在這個範例的路由其實只有一個,就是 /todos,而這個路由其實就是我們的 API 路由,也就是說當我們輸入 http://localhost:3000/todos 時,就會進入到這個路由,只是因為我們依據了 HTTP 的方法來區分不同的行為,因此我們才會有 app.get 以及 app.post

如果你想要實現動態路由,那麼就可以這樣寫:

1
2
3
4
app.get('/todos/:id', (req, res) => {
const { id } = req.params; // 取得動態路由的參數
//...略過其他程式碼
});

那查詢字串(Query String)呢?

1
2
3
4
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
如果你有多個查詢字串,那麼你可以透過 & 來串接;通常查詢字串是由一個 ? 開頭,後面接著查詢字串的參數,而每個參數之間則是用 & 來串接,因此會有 ?&= 這三個符號。

那麼關於查詢字串這邊有件事情要特別提醒一下,如果查詢字串中包含特殊字符、空格或非英數字元的話,你就必須額外處理,否則會造成錯誤。

什麼意思呢?我們知道一個查詢字串的組合會是這樣子的:

1
?page=1&keyword=Express

因此,如果你的 keyword 預期要傳入「ray&array」的話,那麼你的查詢字串就會變成這樣子:

1
?page=1&keyword=ray&array

這樣在解析的時候就會造成錯誤,因此你必須要將特殊字符、空格或非英數字元轉換成 URL 編碼,例如像是:

1
?page=1&keyword=ray%26array

Note
URL 編碼(又稱百分號編碼、URL 轉譯)其實就是將特殊字符、空格或非英數字元轉換成 % 加上十六進位的編碼

這邊也稍微提一下前端怎麼做,通常我們會透過 encodeURIComponent 來處理,例如像是:

1
2
3
4
5
const keyword = 'ray&array';

const encodedKeyword = encodeURIComponent(keyword);

console.log(encodedKeyword); // ray%26array

對於查詢字串常見的雷點有一點概念後,我們就回來 Express.js 吧!

其實實戰開發來講,我們通常會將路由寫在不同的檔案中,例如像是:

1
2
3
4
5
6
7
8
9
10
11
12
13
// routes/todos.js
const express = require('express');
const router = express.Router();

router.get('/', (req, res) => {
//...略過其他程式碼
});

router.post('/', (req, res) => {
//...略過其他程式碼
});

module.exports = router;
1
2
3
4
5
6
7
8
// 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

public 資料夾大部分是拿來放靜態資源的,例如像是圖片、CSS、JavaScript 等等,而這些資源通常是不需要經過處理的,因此我們可以直接將這些資源放在 public 資料夾中,這樣的好處是我們可以直接透過 URL 來取得這些資源,例如像是 https://israynotarray.com/images/logo.png

Note
雖然 Public 主要是放靜態資源,但請不要把敏感資源放在這邊,例如像是密碼、金鑰等等,因為這些資源是可以直接透過 URL 取得的,因此如果你把這些資源放在 Public 資料夾中的話,那麼就會造成資安問題。

utils

utils 全名是 utilities,主要常見放置一些工具類型的通用程式,例如…

時間處理工具:

1
2
3
4
5
6
// utils/dateUtils.js
function formatDate(date, format) {
// 日期格式轉換的邏輯
}

module.exports = formatDate;

又或者是 Email 驗證工具:

1
2
3
4
5
6
// utils/emailUtils.js
function validateEmail(email) {
// 驗證 Email 的邏輯
}

module.exports = validateEmail;

這些比較通用類型的程式碼通常會放在 utils 資料夾中,這樣的好處是我們可以直接透過 require 來引入,例如像是:

1
2
3
4
5
const formatDate = require('./utils/dateUtils');

const date = new Date();

const formattedDate = formatDate(date, 'yyyy-MM-dd');

config

其實這個資料夾就比較簡單一點,通常常見會放一些跟專案有關的設定檔案,例如像是:

  • 資料庫連線設定
  • 環境變數設定
  • Log 設定
  • 網站設定

等等,當然還有很多,這邊就不一一列舉了。

services

services 通常會放一些跟商業邏輯有關的程式碼,例如像是部落格相關的邏輯:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 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…