阿川私房教材:
學 JavaScript 前端,帶作品集去面試!

63 個專案實戰,寫出作品集,讓面試官眼前一亮!

立即開始免費試讀!

圖片說明

Express 是 Node.js 中極為常用的 Web 伺服器應用程式框架。本質上,框架是一種遵循特定規則的程式碼結構,具有兩個關鍵特徵:

  • 它封裝了API,使開發人員能夠更加專注於業務程式碼的編寫。

  • 它已經建立了流程和標準規範。

Express框架的核心特性如下:

  • 它可以配置中間件來回應各種HTTP請求。

  • 它定義了用於執行不同類型的 HTTP 請求操作的路由表。

  • 支援向模板傳遞參數,實現HTML頁面的動態渲染。

本文將透過實作一個簡單的 LikeExpress 類別來分析 Express 如何實現中間件註冊、下一個機制以及路由處理。

快速分析

我們先透過兩個Express程式碼範例來探討一下它所提供的功能:

Express 官網 Hello World 範例

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}`);
});

入口檔app.js分析

以下是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主要有三個核心方法:

  1. 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.
  1. app.get()app.post() :這些方法與use()類似,也用於註冊中間件。但是,它們綁定到 HTTP 請求方法。只有使用對應的HTTP請求方法才會觸發相關中間件的註冊。

  2. app.listen() :負責建立一個 httpServer 並傳遞server.listen()所需的參數。

程式碼實現

透過對Express程式碼功能的分析,我們知道Express的實現重點在於三點:

  • 中間件函數的註冊過程。

  • 中間件功能中的核心下一個機制。

  • 路由處理,重點是路徑匹配。

基於這些點,我們將在下面實作一個簡單的 LikeExpress 類別。

1. 類別的基本結構

首先明確該類別需要實作的主要方法:

  • 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();
};

2. 中介軟體註冊

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);
}

3. 路由匹配處理

當註冊函數的第一個參數是路由時,只有當請求路徑匹配該路由或是其子路由時,才會觸發對應的中間件函數。所以,我們需要一個路由匹配函數,根據請求方法和請求路徑提取匹配路由的中間件陣列,供後續的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 中間件函數的參數是reqresnext ,其中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();
};

Leapcell:用於 Web 託管、非同步任務和 Redis 的下一代無伺服器平台

圖片說明

最後介紹一個非常適合部署Express的平台: Leapcell

Leapcell是無伺服器平台,具有以下功能:

1. 多語言支持

  • 使用 JavaScript、Python、Go 或 Rust 進行開發。

2.免費部署無限個專案

  • 只需按使用量付費 — 無請求,不收費。

3. 無與倫比的成本效益

  • 按量付費,無閒置費用。

  • 範例:25 美元支援 694 萬個請求,平均回應時間為 60 毫秒。

4.簡化的開發者體驗

  • 直覺的使用者介面,輕鬆設定。

  • 全自動 CI/CD 管道和 GitOps 整合。

  • 即時指標和日誌記錄以獲取可行的見解。

5. 輕鬆的可擴充性和高效能

  • 自動縮放以輕鬆處理高並發。

  • 零營運開銷-只需專注於建置。

在文件中探索更多內容!

圖片說明

Leapcell 推特: https://x.com/LeapcellHQ


原文出處:https://dev.to/leapcell/mastering-expressjs-a-deep-dive-4ef5


共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。

阿川私房教材:
學 JavaScript 前端,帶作品集去面試!

63 個專案實戰,寫出作品集,讓面試官眼前一亮!

立即開始免費試讀!