Express 是 Node.js 中極為常用的 Web 伺服器應用程式框架。本質上,框架是一種遵循特定規則的程式碼結構,具有兩個關鍵特徵:
它封裝了API,使開發人員能夠更加專注於業務程式碼的編寫。
它已經建立了流程和標準規範。
Express框架的核心特性如下:
它可以配置中間件來回應各種HTTP請求。
它定義了用於執行不同類型的 HTTP 請求操作的路由表。
支援向模板傳遞參數,實現HTML頁面的動態渲染。
本文將透過實作一個簡單的 LikeExpress 類別來分析 Express 如何實現中間件註冊、下一個機制以及路由處理。
我們先透過兩個Express程式碼範例來探討一下它所提供的功能:
const express = require('express');
const app = express();
const port = 3000;
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});
以下是express-generator
腳手架產生的Express專案的入口檔案app.js
的程式碼:
// Handle errors caused by unmatched routes
const createError = require('http-errors');
const express = require('express');
const path = require('path');
const indexRouter = require('./routes/index');
const usersRouter = require('./routes/users');
// `app` is an Express instance
const app = express();
// View engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
// Parse JSON format data in post requests and add the `body` field to the `req` object
app.use(express.json());
// Parse the urlencoded format data in post requests and add the `body` field to the `req` object
app.use(express.urlencoded({ extended: false }));
// Static file handling
app.use(express.static(path.join(__dirname, 'public')));
// Register top-level routes
app.use('/', indexRouter);
app.use('/users', usersRouter);
// Catch 404 errors and forward them to the error handler
app.use((req, res, next) => {
next(createError(404));
});
// Error handling
app.use((err, req, res, next) => {
// Set local variables to display error messages in the development environment
res.locals.message = err.message;
// Decide whether to display the full error according to the environment variable. Display in development, hide in production.
res.locals.error = req.app.get('env') === 'development'? err : {};
// Render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
從上面兩段程式碼我們可以看出Express實例app
主要有三個核心方法:
app.use([path,] callback [, callback...])
:用來註冊中間件。當請求路徑符合設定的規則時,就會執行對應的中介軟體函數。- `path`: Specifies the path for invoking the middleware function.
- `callback`: The callback function can take various forms. It can be a single middleware function, a series of middleware functions separated by commas, an array of middleware functions, or a combination of all the above.
app.get()
和app.post()
:這些方法與use()
類似,也用於註冊中間件。但是,它們綁定到 HTTP 請求方法。只有使用對應的HTTP請求方法才會觸發相關中間件的註冊。
app.listen()
:負責建立一個 httpServer 並傳遞server.listen()
所需的參數。
透過對Express程式碼功能的分析,我們知道Express的實現重點在於三點:
中間件函數的註冊過程。
中間件功能中的核心下一個機制。
路由處理,重點是路徑匹配。
基於這些點,我們將在下面實作一個簡單的 LikeExpress 類別。
首先明確該類別需要實作的主要方法:
use()
:實現通用中間件註冊。
get()
和post()
:實作與 HTTP 請求相關的中間件註冊。
listen()
:本質上是httpServer的listen()
函式。在該類別的listen()
函數中,建立了一個httpServer,傳入參數,監聽請求,執行回呼函數(req, res) => {}
。
回顧原生Node httpServer的使用情況:
const http = require("http");
const server = http.createServer((req, res) => {
res.end("hello");
});
server.listen(3003, "127.0.0.1", () => {
console.log("node service started successfully");
});
相應地,LikeExpress類別的基本架構如下:
const http = require('http');
class LikeExpress {
constructor() {}
use() {}
get() {}
post() {}
// httpServer callback function
callback() {
return (req, res) => {
res.json = function (data) {
res.setHeader('content-type', 'application/json');
res.end(JSON.stringify(data));
};
};
}
listen(...args) {
const server = http.createServer(this.callback());
server.listen(...args);
}
}
module.exports = () => {
return new LikeExpress();
};
從app.use([path,] callback [, callback...])
中,我們可以看到中間件可以是函數陣列,也可以是單一函數。為了簡化實現,我們將中間件統一處理為函數陣列。 LikeExpress 類別中use()
、 get()
、 post()
三個方法都可以實作中間件註冊。只是由於請求方式不同,觸發的中間件有所不同。所以我們考慮:
抽像出通用的中間件註冊函數。
為這三個方法建立中間件函數陣列,用於存放不同請求對應的中間件。由於use()
是所有請求的通用中間件註冊方法,因此儲存use()
中間件的陣列是get()
和post()
陣列的並集。
中間件陣列需要放置在公共區域,以便於類別中的方法存取。因此,我們將中間件陣列放在constructor()
建構子中。
constructor() {
// List of stored middleware
this.routes = {
all: [], // General middleware
get: [], // Middleware for get requests
post: [], // Middleware for post requests
};
}
中間件註冊是指將中間件儲存到對應的中間件陣列中。中間件註冊函數需要解析傳入的參數。第一個參數可能是路由,也可能是中間件,所以需要先判斷是否為路由。如果是,則原樣輸出;否則預設為根路由,然後將剩餘的中間件參數轉為陣列。
register(path) {
const info = {};
// If the first parameter is a route
if (typeof path === "string") {
info.path = path;
// Convert to an array starting from the second parameter and store it in the middleware array
info.stack = Array.prototype.slice.call(arguments, 1);
} else {
// If the first parameter is not a route, the default is the root route, and all routes will execute
info.path = '/';
info.stack = Array.prototype.slice.call(arguments, 0);
}
return info;
}
use()
、 get()
和post()
的實現透過通用的中間件註冊函數register()
,可以輕鬆實現use()
、 get()
和post()
,只需將中間件儲存在對應的陣列中即可。
use() {
const info = this.register.apply(this, arguments);
this.routes.all.push(info);
}
get() {
const info = this.register.apply(this, arguments);
this.routes.get.push(info);
}
post() {
const info = this.register.apply(this, arguments);
this.routes.post.push(info);
}
當註冊函數的第一個參數是路由時,只有當請求路徑匹配該路由或是其子路由時,才會觸發對應的中間件函數。所以,我們需要一個路由匹配函數,根據請求方法和請求路徑提取匹配路由的中間件陣列,供後續的callback()
函數執行:
match(method, url) {
let stack = [];
// Ignore the browser's built-in icon request
if (url === "/favicon") {
return stack;
}
// Get routes
let curRoutes = [];
curRoutes = curRoutes.concat(this.routes.all);
curRoutes = curRoutes.concat(this.routes[method]);
curRoutes.forEach((route) => {
if (url.indexOf(route.path) === 0) {
stack = stack.concat(route.stack);
}
});
return stack;
}
然後,在httpServer的回呼函數callback()
中,提取出需要執行的中間件:
callback() {
return (req, res) => {
res.json = function (data) {
res.setHeader('content-type', 'application/json');
res.end(JSON.stringify(data));
};
const url = req.url;
const method = req.method.toLowerCase();
const resultList = this.match(method, url);
this.handle(req, res, resultList);
};
}
Express 中間件函數的參數是req
、 res
和next
,其中next
是一個函數。只有呼叫它,中間件函數才能依序執行,類似ES6 Generator中的next()
。在我們的實作中,我們需要寫一個next()
函數,其要求如下:
每次從中間件佇列陣列中按順序提取一個中間件。
將next()
函數傳遞到提取的中間件。由於中間件陣列是公共的,因此每次執行next()
時,都會取出陣列中的第一個中間件函數執行,從而達到中間件順序執行的效果。
// Core next mechanism
handle(req, res, stack) {
const next = () => {
const middleware = stack.shift();
if (middleware) {
middleware(req, res, next);
}
};
next();
}
const http = require('http');
const slice = Array.prototype.slice;
class LikeExpress {
constructor() {
// List of stored middleware
this.routes = {
all: [],
get: [],
post: [],
};
}
register(path) {
const info = {};
// If the first parameter is a route
if (typeof path === "string") {
info.path = path;
// Convert to an array starting from the second parameter and store it in the stack
info.stack = slice.call(arguments, 1);
} else {
// If the first parameter is not a route, the default is the root route, and all routes will execute
info.path = '/';
info.stack = slice.call(arguments, 0);
}
return info;
}
use() {
const info = this.register.apply(this, arguments);
this.routes.all.push(info);
}
get() {
const info = this.register.apply(this, arguments);
this.routes.get.push(info);
}
post() {
const info = this.register.apply(this, arguments);
this.routes.post.push(info);
}
match(method, url) {
let stack = [];
// Browser's built-in icon request
if (url === "/favicon") {
return stack;
}
// Get routes
let curRoutes = [];
curRoutes = curRoutes.concat(this.routes.all);
curRoutes = curRoutes.concat(this.routes[method]);
curRoutes.forEach((route) => {
if (url.indexOf(route.path) === 0) {
stack = stack.concat(route.stack);
}
});
return stack;
}
// Core next mechanism
handle(req, res, stack) {
const next = () => {
const middleware = stack.shift();
if (middleware) {
middleware(req, res, next);
}
};
next();
}
callback() {
return (req, res) => {
res.json = function (data) {
res.setHeader('content-type', 'application/json');
res.end(JSON.stringify(data));
};
const url = req.url;
const method = req.method.toLowerCase();
const resultList = this.match(method, url);
this.handle(req, res, resultList);
};
}
listen(...args) {
const server = http.createServer(this.callback());
server.listen(...args);
}
}
module.exports = () => {
return new LikeExpress();
};
最後介紹一個非常適合部署Express的平台: Leapcell 。
Leapcell是無伺服器平台,具有以下功能:
按量付費,無閒置費用。
範例:25 美元支援 694 萬個請求,平均回應時間為 60 毫秒。
直覺的使用者介面,輕鬆設定。
全自動 CI/CD 管道和 GitOps 整合。
即時指標和日誌記錄以獲取可行的見解。
自動縮放以輕鬆處理高並發。
零營運開銷-只需專注於建置。
Leapcell 推特: https://x.com/LeapcellHQ
原文出處:https://dev.to/leapcell/mastering-expressjs-a-deep-dive-4ef5