Giới thiệu Middleware trong Yargs
Yargs là một thư viện phổ biến để phân tích các tham số dòng lệnh, giúp nhà phát triển dễ dàng định nghĩa giao diện dòng lệnh (CLI) và cung cấp các chức năng xử lý tham số, tổ chức lệnh, tự động tạo văn bản trợ giúp. Bài viết này sẽ khám phá khả năng hỗ trợ middleware của nó.
Middleware trong Yargs là một cơ chế để chèn logic tùy chỉnh vào quá trình phân tích tham số dòng lệnh. Middleware có thể được thực thi trước hoặc sau khi xác thực tham số, cho phép nhà phát triển thao tác hoặc sửa đổi đối tượng argv. Thông qua middleware, nhà phát triển có thể tiền xử lý, xác thực, chuyển đổi dữ liệu đầu vào dòng lệnh, hoặc thêm các hành động tùy chỉnh dựa trên yêu cầu nghiệp vụ.
Các ứng dụng chính của middleware bao gồm:
- Tiền xử lý tham số: Middleware có thể chạy trước khi xác thực tham số dòng lệnh, xử lý hoặc sửa đổi
argv, ví dụ như định dạng dữ liệu đầu vào. - Xử lý sau xác thực tham số: Middleware cũng có thể chạy sau khi tham số đã được xác thực, dùng để xử lý thêm dữ liệu đã phân tích hoặc tạo ra các đầu ra cụ thể.
- Middleware toàn cục: Middleware có thể áp dụng cho tất cả các lệnh hoặc tùy chọn, được gọi là middleware toàn cục. Thông qua middleware toàn cục, việc xử lý dữ liệu đầu vào của tất cả các lệnh có thể được thực hiện một cách thống nhất.
Middleware Toàn cục
API cho middleware toàn cục có dạng: .middleware(callbacks, [applyBeforeValidation])
callbacks: Có thể là một hàm hoặc một danh sách các hàm. Mỗi hàm callback sẽ nhận một tham chiếu đếnargv, một đối tượng chứa các tham số dòng lệnh.applyBeforeValidation(tùy chọn): Một giá trị boolean, mặc định làfalse. Nếu đặt làtrue, middleware sẽ được thực thi trước khi xác thực, nhưng sau khi phân tích.
Dưới đây là một số ví dụ:
Ví dụ 1: Chuỗi thực thi middleware
const mwFunc1 = argv => console.log('Tôi là một hàm middleware');
const mwFunc2 = argv => console.log('Tôi là một hàm middleware khác');
require('yargs')(process.argv.slice(2))
.command('myCommand', 'một lệnh nào đó', {}, function(argv){
console.log('Đang chạy myCommand!');
})
.middleware([mwFunc1, mwFunc2]).parse();
Trong ví dụ này, khi lệnh myCommand được gọi từ dòng lệnh, mwFunc1 sẽ được gọi trước, sau đó là mwFunc2, và cuối cùng là hàm xử lý của lệnh. Đầu ra trên bảng điều khiển sẽ là:
I'm a middleware function
I'm another middleware function
Running myCommand!
Ví dụ 2: Tích hợp biến môi trường
require('yargs/yargs')(process.argv.slice(2))
.middleware(function (argv) {
if (process.env.HOME) argv.home = process.env.HOME;
}, true) // applyBeforeValidation = true
.command('configure-home', "làm gì đó với thư mục home của người dùng",
{
'home': {
'demand': true,
'string': true
}
},
function(argv) {
console.info(`Chúng ta biết thư mục home của người dùng là ${argv.home}`);
}
)
.parse();
Trong ví dụ này, middleware được sử dụng để điền thư mục home từ biến môi trường. Vì middleware nhận một tham số argv, nó cũng có thể sửa đổi tham số này.
Middleware Lệnh
Middleware lệnh chỉ có hiệu lực cho lệnh hiện tại và buộc phải đặt tham số applyBeforeValidation thành false. Giao diện của nó như sau: .command(cmd, desc, [builder], [handler])
Middleware lệnh chỉ được thực thi khi lệnh chạy, do đó nó sẽ thực thi sau middleware toàn cục.
Ví dụ 3: Sửa đổi đối tượng người dùng
require('yargs')
.command('$0', 'chấp nhận tên người dùng', () => {}, (argv) => {
// Middleware đã được áp dụng trước khi lệnh mặc định được gọi:
console.info(argv);
})
.choices('user', ['Goofy', 'Miky'])
.middleware(argv => {
console.info('Đã đến đây');
const user = argv.user;
switch (user) {
case 'Goofy':
argv.user = {
firstName: 'Mark',
lastName: 'Pipe',
};
break;
}
return argv;
}) // applyBeforeValidation mặc định là false
.parse('--user Miky');
Triển khai Middleware nội bộ
Yargs triển khai middleware chủ yếu dựa vào tệp middleware.ts để xử lý việc thêm, áp dụng và quản lý middleware toàn cục. Nó định nghĩa các lớp và hàm liên quan đến middleware toàn cục, đồng thời cung cấp các công cụ để xử lý logic middleware trong quá trình phân tích dòng lệnh.
Liên kết mã nguồn: middleware.ts
Lớp GlobalMiddleware
GlobalMiddleware là một lớp quản lý middleware toàn cục, lưu trữ các middleware và cung cấp các thao tác liên quan như thêm, đóng băng, bỏ đóng băng và đặt lại middleware.
globalMiddleware: Middleware[] = []: Lưu trữ tất cả các middleware đã đăng ký.frozens: Array<Middleware[]> = []: Dùng để lưu trữ các nhóm middleware ở trạng thái đóng băng, hỗ trợ quay lại cấu hình middleware trước đó.
Hàm tạo
constructor(yargs: YargsInstance) {
this.yargs = yargs;
}
Hàm tạo nhận một đối tượng YargsInstance (tức là phiên bản yargs) để gọi logic phân tích dòng lệnh sau này.
Phương thức addMiddleware
addMiddleware(
callback: MiddlewareCallback | MiddlewareCallback[],
applyBeforeValidation: boolean,
global = true,
mutates = false
): YargsInstance {
// ... implementation
}
- Chức năng: Phương thức này cho phép thêm một hoặc nhiều middleware.
callback: Nhận hàm middleware hoặc mảng middleware.applyBeforeValidation: Đánh dấu liệu middleware có nên được áp dụng trước khi xác thực tham số dòng lệnh hay không.global: Xác định liệu middleware có thuộc phạm vi toàn cục hay không.mutates: Xác định liệu middleware có sửa đổiargvhay không.
Các middleware được thêm qua phương thức này sẽ được lưu trữ trong mảng globalMiddleware.
Phương thức addCoerceMiddleware
addCoerceMiddleware(
callback: MiddlewareCallback,
option: string
): YargsInstance {
// ... implementation
}
- Chức năng: Phương thức này chuyên xử lý các middleware loại
coerce, mỗi tùy chọn chỉ có thể đăng ký một middlewarecoerce. - Hoạt động: Đầu tiên lọc bỏ các middleware
coercecho cùng một tùy chọn đã đăng ký trước đó, sau đó thêm middleware mới.
Phương thức freeze và unfreeze
freeze() {
this.frozens.push([...this.globalMiddleware]);
}
unfreeze() {
const frozen = this.frozens.pop();
if (frozen !== undefined) this.globalMiddleware = frozen;
}
freeze: Lưu ảnh chụp nhanh của middleware hiện tại vào mảngfrozens.unfreeze: Lấy ảnh chụp nhanh được lưu gần nhất từfrozensvà khôi phục nó vàoglobalMiddleware.
Phương thức reset
reset() {
this.globalMiddleware = this.globalMiddleware.filter(m => m.global);
}
- Chức năng: Đặt lại middleware, chỉ giữ lại các middleware toàn cục (
global: true).
Phương thức công cụ: commandMiddlewareFactory
export function commandMiddlewareFactory(
commandMiddleware?: MiddlewareCallback[]
): Middleware[] {
// ... implementation
}
- Chức năng: Nhận mảng middleware cấp lệnh và đặt
applyBeforeValidationthànhfalse, nghĩa là các middleware này mặc định sẽ được áp dụng sau khi xác thực.
Phương thức công cụ: applyMiddleware
export function applyMiddleware(
argv: Arguments | Promise<Arguments>,
yargs: YargsInstance,
middlewares: Middleware[],
beforeValidation: boolean
) {
return middlewares.reduce<Arguments | Promise<Arguments>>(
(acc, middleware) => {
if (middleware.applyBeforeValidation !== beforeValidation) {
return acc;
}
if (middleware.mutates) {
if (middleware.applied) return acc;
middleware.applied = true;
}
if (isPromise(acc)) {
return acc
.then(initialObj =>
Promise.all([initialObj, middleware(initialObj, yargs)])
)
.then(([initialObj, middlewareObj]) =>
Object.assign(initialObj, middlewareObj)
);
} else {
const result = middleware(acc, yargs);
return isPromise(result)
? result.then(middlewareObj => Object.assign(acc, middlewareObj))
: Object.assign(acc, result);
}
},
argv
);
}
- Chức năng: Áp dụng tất cả các middleware khớp điều kiện.
argv: Đại diện cho đối tượng tham số dòng lệnh, có thể là đối tượng thông thường hoặcPromise.middlewares: Mảng middleware được truyền vào.beforeValidation: Dựa vào cờ này để quyết định chỉ áp dụng các middleware trước khi xác thực.
Hàm này là logic cốt lõi, sử dụng reduce để lặp qua và áp dụng các middleware, lần lượt sửa đổi đối tượng argv. Nếu argv hoặc giá trị trả về của middleware là Promise, nó sẽ được chuyển đổi thành xử lý logic bất đồng bộ.
Áp dụng kiến thức Middleware
Ngoài Yargs, các framework như Express, Koa cũng có middleware. Chúng ta có thể rút ra một bộ nguyên tắc chung về việc triển khai middleware từ chúng và áp dụng vào các ngữ cảnh khác khi cần thiết.
Cốt lõi của middleware là một kỹ thuật cho phép chèn các hàm xử lý vào một chuỗi logic xử lý. Nó có thể nhận đầu vào, xử lý đầu vào và truyền đầu ra cho middleware tiếp theo hoặc trả về kết quả cuối cùng.
Do đó, middleware là sự trừu tượng hóa của luồng xử lý. Middleware chịu trách nhiệm tiếp nhận sự khác biệt trong xử lý của luồng, trong khi việc gọi nó thuộc về luồng chính.
Thông qua kỹ thuật middleware, chúng ta có thể đạt được:
- Phân tách mối quan tâm (Separation of Concerns): Middleware cho phép tách biệt các module chức năng khác nhau trong ứng dụng, để mỗi module chỉ xử lý phần mình quan tâm, ví dụ như xác thực người dùng, xử lý lỗi.
- Cải thiện khả năng mở rộng của mã nguồn: Việc lộ ra các hàm xử lý thông qua middleware giúp chức năng của hệ thống dễ dàng mở rộng hơn.
- Đơn giản hóa logic phức tạp: Đối với các luồng phức tạp, chúng ta có thể chia nhỏ thành nhiều bước đơn giản, vừa tăng khả năng kiểm soát cho mỗi bước, vừa đơn giản hóa thao tác luồng. Ví dụ, khi xử lý yêu cầu HTTP, có thể chia thành: phân tích phần thân yêu cầu → kiểm tra xác thực danh tính → xử lý quyền hạn → thực thi logic nghiệp vụ chính → định dạng giá trị trả về → ghi log.
Cấu trúc cơ bản của Middleware
function middleware(input, next) {
// Xử lý input
const result = process(input);
// Gọi middleware tiếp theo
return next(result);
}
Cấu trúc chung bao gồm:
- Đầu vào (Input): Thường là một đối tượng ngữ cảnh nào đó (ví dụ:
req/res,argv, v.v.). - Đầu ra (Output): Kết quả sau khi xử lý, được truyền cho middleware tiếp theo.
next: Hàm hoặc bộ xử lý trỏ đến middleware tiếp theo.
Đăng ký và Lưu trữ Middleware
Để thêm và quản lý middleware một cách linh hoạt, thông thường cần lưu trữ chúng trong một danh sách có thứ tự để dễ dàng thực thi tuần tự.
class MiddlewareManager {
constructor() {
this.middlewares = [];
}
addMiddleware(middleware) {
this.middlewares.push(middleware);
}
getMiddlewares() {
return this.middlewares;
}
}
Kiểm soát Thực thi Middleware
Trong một số trường hợp, middleware cần có khả năng quyết định có nên dừng việc thực thi chuỗi hay không. Điều này thường được thực hiện bằng cách không gọi next.
function middleware(input, next) {
if (shouldStop(input)) {
return input; // Không gọi next, dừng việc thực thi chuỗi
}
return next(input);
}
Trong xử lý yêu cầu HTTP, có thể chấm dứt chuỗi xử lý yêu cầu và trả về phản hồi trực tiếp dựa trên một số điều kiện. Tương tự, trong công cụ dòng lệnh, có thể kết thúc sớm chuỗi middleware dựa trên một số điều kiện.
Quản lý và Đặt lại Middleware
Chuỗi middleware có thể được quản lý, đóng băng, bỏ đóng băng và đặt lại theo yêu cầu nghiệp vụ, điều này thường được sử dụng trong các tình huống cụ thể để sửa đổi middleware hoặc đặt lại hành vi của chúng.
class MiddlewareManager {
constructor() {
this.middlewares = [];
this.frozens = []; // Lưu trạng thái đóng băng
}
freeze() {
// Lưu một bản sao của middleware hiện tại
this.frozens.push([...this.middlewares]);
}
unfreeze() {
// Khôi phục trạng thái middleware từ bản lưu gần nhất
const frozen = this.frozens.pop();
if (frozen) this.middlewares = frozen;
}
reset() {
// Chỉ giữ lại middleware toàn cục
this.middlewares = this.middlewares.filter(m => m.global);
}
}
Cơ chế này cho phép lưu trạng thái hiện tại của middleware và khôi phục khi cần thiết.
Nếu cần cấu hình linh hoạt, có thể thêm các mục cấu hình tùy chọn cho middleware.
Thực thi Chuỗi Middleware
Cốt lõi là gọi tuần tự các middleware. Có thể nối các middleware lại với nhau bằng cách sử dụng reduce hoặc đệ quy. Sau khi mỗi middleware hoàn thành xử lý hiện tại, nó cần quyết định có nên chuyển quyền xử lý cho middleware tiếp theo hay không.
function executeMiddlewares(input, middlewares) {
let index = -1;
function next(currentInput) {
index++;
if (index < middlewares.length) {
// Truyền input và hàm next cho middleware tiếp theo
return middlewares[index](currentInput, next);
}
// Trả về kết quả sau khi tất cả middleware đã xử lý
return currentInput;
}
return next(input); // Bắt đầu thực thi từ middleware đầu tiên
}
Hàm next ở đây kiểm soát thứ tự thực thi của middleware, mỗi lần gọi sẽ chuyển sang middleware tiếp theo.
Hỗ trợ Middleware Bất đồng bộ
Trong ứng dụng thực tế, nhiều middleware cần xử lý các hoạt động bất đồng bộ (như truy vấn cơ sở dữ liệu, yêu cầu HTTP, v.v.). Do đó, chuỗi middleware cần hỗ trợ các hoạt động bất đồng bộ.
async function executeAsyncMiddlewares(input, middlewares) {
let index = -1;
async function next(currentInput) {
index++;
if (index < middlewares.length) {
// Chờ middleware hiện tại hoàn thành (nếu là bất đồng bộ)
const result = await middlewares[index](currentInput, next);
return result;
}
return currentInput;
}
return next(input);
}
Điều này đảm bảo rằng các middleware bất đồng bộ sẽ đợi cho đến khi Promise được giải quyết trước khi thực thi middleware tiếp theo.
Tóm lại một cách đơn giản, middleware là việc quản lý một nhóm các hàm và gọi chúng vào những thời điểm cụ thể.