Express — це надзвичайно популярний фреймворк для веб-серверів у Node.js. Фреймворк — це структура коду, яка відповідає певним правилам і має дві основні характеристики:
- Інкапсулює API, що дозволяє розробникам зосередитись на написанні бізнес-коду.
- Має встановлені процеси та стандартні специфікації.
Основні можливості фреймворка Express:
- Може налаштовувати проміжне програмне забезпечення (middleware) для відповіді на різні HTTP запити.
- Визначає таблицю маршрутів для виконання різних типів дій з HTTP запитами.
- Підтримує передачу параметрів у шаблони для динамічного рендерингу HTML сторінок.
Ця стаття проаналізує, як Express реалізує реєстрацію проміжного програмного забезпечення, наступний механізм та обробку маршрутів за допомогою простої реалізації класу LikeExpress.
Аналіз Express
Розглянемо спочатку функції, які він надає, через два приклади коду Express:
Офіційний приклад "Hello World" з сайту 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}`);
});
Аналіз вхідного файлу app.js
Ось код вхідного файлу app.js
проекту Express, створеного за допомогою генератора express-generator
:
// Обробка помилок, викликаних невідповідними маршрутами
const createError = require('http-errors');
const express = require('express');
const path = require('path');
const indexRouter = require('./routes/index');
const usersRouter = require('./routes/users');
// `app` — це екземпляр Express
const app = express();
// Налаштування движка для подання
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
// Парсинг JSON-даних у запитах POST і додавання поля `body` до об’єкта `req`
app.use(express.json());
// Парсинг даних у форматі urlencoded у запитах POST і додавання поля `body` до об’єкта `req`
app.use(express.urlencoded({ extended: false }));
// Обробка статичних файлів
app.use(express.static(path.join(__dirname, 'public')));
// Реєстрація основних маршрутів
app.use('/', indexRouter);
app.use('/users', usersRouter);
// Ловимо помилки 404 і передаємо їх до обробника помилок
app.use((req, res, next) => {
next(createError(404));
});
// Обробка помилок
app.use((err, req, res, next) => {
// Встановлення локальних змінних для відображення повідомлень про помилки в середовищі розробки
res.locals.message = err.message;
// Визначення, чи показувати повну помилку залежно від змінної середовища. Показуємо в розробці, приховуємо в продакшн-режимі.
res.locals.error = req.app.get('env') === 'development'? err : {};
// Рендеринг сторінки помилки
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
З цих двох фрагментів коду ми можемо побачити, що екземпляр Express app
має три основні методи:
app.use([path,] callback [, callback...])
: Використовується для реєстрації проміжного програмного забезпечення (middleware). Коли шлях запиту співпадає з встановленими правилами, виконується відповідна функція проміжного програмного забезпечення.
path
: Вказує шлях для виклику функції проміжного програмного забезпечення.callback
: Функція зворотного виклику може бути різною. Це може бути одна функція проміжного програмного забезпечення, серія функцій, розділених комами, масив функцій або комбінація всіх вищезазначених варіантів.
app.get()
таapp.post()
: Ці методи схожі наuse()
, також для реєстрації проміжного програмного забезпечення. Однак вони прив’язані до HTTP методів запиту. Лише коли використовується відповідний HTTP метод запиту, реєстрація відповідного проміжного програмного забезпечення буде ініційована.
3.
app.listen()
: Відповідає за створення httpServer і передачу параметрів, необхідних дляserver.listen()
.
Реалізація коду
Згідно з аналізом функцій коду Express, ми знаємо, що реалізація Express зосереджена на трьох аспектах:
- Процес реєстрації функцій проміжного програмного забезпечення (middleware).
- Основний механізм
next
у функціях проміжного програмного забезпечення. - Обробка маршрутів, з акцентом на збіг шляхів.
З огляду на ці моменти, ми реалізуємо простий клас LikeExpress нижче.
1. Основна структура класу
Перш за все, визначимо основні методи, які цей клас повинен реалізувати:
use()
: Реалізує загальну реєстрацію проміжного програмного забезпечення.get()
іpost()
: Реалізують реєстрацію проміжного програмного забезпечення, пов’язаного з HTTP запитами.listen()
: Насправді це функціяlisten()
для httpServer. У функціїlisten()
цього класу створюється httpServer, передаються параметри, здійснюється прослуховування запитів і виконується функція зворотного виклику(req, res) => {}
.
Розглянемо використання рідного httpServer у Node.js:
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() {}
// callback для httpServer
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()
— це загальний метод реєстрації проміжного програмного забезпечення для всіх запитів, масив, що зберігає middleware дляuse()
, є об’єднанням масивів дляget()
іpost()
.
Масив черги проміжного програмного забезпечення
Масив проміжного програмного забезпечення потрібно розмістити в публічному місці для легкого доступу до нього методами класу. Тому ми поміщаємо масив проміжного програмного забезпечення в конструктор класу.
constructor() {
// Список збережених middleware
this.routes = {
all: [], // Загальне middleware
get: [], // Middleware для get запитів
post: [], // Middleware для post запитів
};
}
Функція реєстрації проміжного програмного забезпечення
Реєстрація проміжного програмного забезпечення означає зберігання middleware у відповідному масиві. Функція реєстрації проміжного програмного забезпечення повинна обробляти вхідні параметри. Перший параметр може бути маршрутом або middleware, тому потрібно спочатку визначити, чи є це маршрутом.
Якщо це маршрут, вивести його без змін; в іншому випадку за замовчуванням використовується кореневий маршрут, і всі інші параметри middleware конвертуються в масив.
register(path) {
const info = {};
// Якщо перший параметр — це маршрут
if (typeof path === "string") {
info.path = path;
// Конвертуємо всі параметри, починаючи з другого, в масив і зберігаємо їх у масив middleware
info.stack = Array.prototype.slice.call(arguments, 1);
} else {
// Якщо перший параметр не є маршрутом, за замовчуванням використовується кореневий маршрут, і всі маршрути виконуються
info.path = '/';
info.stack = Array.prototype.slice.call(arguments, 0);
}
return info;
}
Реалізація use()
, get()
і post()
Завдяки загальній функції реєстрації проміжного програмного забезпечення register()
, легко реалізувати use()
, get()
і post()
, просто зберігаючи middleware у відповідних масивах.
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. Обробка збігу маршрутів
Коли перший параметр функції реєстрації є маршрутом, відповідна функція middleware буде викликана тільки тоді, коли шлях запиту збігається з маршрутом або є його підмаршрутом. Тому нам потрібна функція для збігу маршрутів, щоб витягти масив middleware для відповідного маршруту, з урахуванням методу запиту та шляху запиту, для подальшого виконання функцією callback()
:
match(method, url) {
let stack = [];
// Ігноруємо запит на вбудовану іконку браузера
if (url === "/favicon") {
return stack;
}
// Отримуємо маршрути
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;
}
Далі, в callback-функції callback()
для httpServer, витягуємо middleware, яке потрібно виконати:
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);
};
}
4. Реалізація механізму next
Параметри функції middleware в Express — це req
, res
і next
, де next
— це функція. Тільки викликаючи її, можна виконувати функції middleware послідовно, подібно до next()
в ES6 Generator. У нашій реалізації, ми повинні написати функцію next()
, яка повинна відповідати наступним вимогам:
- Кожного разу витягувати одну функцію middleware з масиву черги middleware.
- Передавати функцію
next()
у витягнуту функцію middleware.
Оскільки масив middleware є публічним, кожного разу при виконанніnext()
перша функція middleware з масиву буде забрана та виконана, таким чином досягається ефект послідовного виконання middleware.
// Основний механізм next
handle(req, res, stack) {
const next = () => {
const middleware = stack.shift();
if (middleware) {
middleware(req, res, next);
}
};
next();
}
Код Express
const http = require('http');
const slice = Array.prototype.slice;
class LikeExpress {
constructor() {
// Список збережених middleware
this.routes = {
all: [],
get: [],
post: [],
};
}
register(path) {
const info = {};
// Якщо перший параметр — це маршрут
if (typeof path === "string") {
info.path = path;
// Конвертуємо всі параметри, починаючи з другого, в масив і зберігаємо їх у стек
info.stack = slice.call(arguments, 1);
} else {
// Якщо перший параметр не є маршрутом, за замовчуванням використовується кореневий маршрут, і всі маршрути виконуються
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 = [];
// Запит на вбудовану іконку браузера
if (url === "/favicon") {
return stack;
}
// Отримуємо маршрути
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;
}
// Основний механізм next
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: наступне покоління серверної платформи для веб-хостингу, асинхронних завдань та Redis
Нарешті, хочу представити платформу, яка ідеально підходить для розгортання Express: Leapcell.
Leapcell — це безсерверна платформа з такими характеристиками:
1. Підтримка багатьох мов програмування
- Розробляйте на JavaScript, Python, Go або Rust.
2. Безкоштовне розгортання необмеженої кількості проєктів
- Платіть лише за використання — немає запитів — немає зборів.
3. Безпрецедентна ефективність витрат
- Оплата по мірі використання без плати за простоювання.
- Приклад: $25 підтримують 6,94 мільйона запитів при середньому часі відповіді 60 мс.
4. Спрощений досвід для розробників
- Інтуїтивно зрозумілий інтерфейс для безперешкодної налаштування.
- Повністю автоматизовані CI/CD пайплайни та інтеграція GitOps.
- Метрики в реальному часі та ведення журналів для отримання корисної інформації.
5. Легкість масштабування та висока продуктивність
- Автоматичне масштабування для обробки високої одночасної кількості запитів.
- Відсутність операційних витрат — зосередьтесь лише на розробці.
Досліджуйте більше в документації!
Leapcell Twitter: https://x.com/LeapcellHQ
Перекладено з: Mastering Express.js: A Deep Dive