
Chào các 'con giời' của thầy Creyt! Hôm nay, chúng ta sẽ 'bung lụa' một khái niệm nghe có vẻ 'khó nhằn' nhưng lại 'bao ngầu' trong Node.js: stream.Writable. Nghe tên thôi đã thấy 'viết được' rồi đúng không? Chính xác! Đây là 'cánh cửa thần kỳ' để đẩy dữ liệu đi ra khỏi ứng dụng của chúng ta một cách 'thông minh' và 'khéo léo'.
1. stream.Writable là gì mà 'hot' vậy? (Giải thích theo Gen Z)
Hãy tưởng tượng thế này: Các bạn đang livestream game, đúng không? Dữ liệu hình ảnh, âm thanh không phải được quay xong cả trận rồi mới up lên YouTube một cục đâu. Nó được 'phát' đi từng chút một, liên tục, theo một 'dòng chảy' không ngừng nghỉ. Cái 'dòng chảy' đó trong lập trình, chúng ta gọi là Stream.
stream.Writable chính là cái 'ống thoát nước' hoặc 'cái thùng rác thông minh' của bạn trong thế giới Node.js. Thay vì 'bốc cả núi' dữ liệu lên RAM rồi 'quẳng' một phát vào file, vào database, hay gửi qua mạng, thì Writable cho phép bạn 'đổ' dữ liệu vào từ từ, từng 'gáo' một. Nó nhận dữ liệu theo từng 'chunk' (từng mẩu nhỏ) và xử lý chúng một cách tuần tự. 'Xịn xò' chưa?
Để làm gì? Đơn giản là để:
- Tiết kiệm RAM: Ai lại muốn 'ăn' hết RAM chỉ vì xử lý một file log vài GB chứ?
Writablegiúp bạn 'nhấm nháp' dữ liệu, không cần 'ngốn' cả cục. - Tăng tốc độ: Dữ liệu vừa đến là xử lý luôn, không cần chờ đợi. Giống như bạn vừa nhận được tin nhắn là trả lời luôn, chứ không phải đợi đến cuối ngày mới trả lời cả đống tin.
- Kiểm soát luồng (Backpressure): Đây mới là 'đỉnh cao' này! Nếu cái 'đầu ra' của bạn (ví dụ: ổ cứng ghi chậm, mạng yếu) không 'tiêu hóa' kịp dữ liệu, thì
Writablesẽ 'báo hiệu' cho 'đầu vào' (cái nơi đang cấp dữ liệu) 'chơi chậm lại'. Đảm bảo hệ thống không bị 'nghẽn cổ chai' hay 'tràn ngập' dữ liệu. Nghe 'đã cái nư' chưa?
2. 'Bật mí' Code Ví Dụ (Minh hoạ rõ ràng)
Để các bạn không chỉ 'nghe sáo rỗng', thầy Creyt sẽ 'show hàng' một ví dụ 'cực phẩm' về cách tạo một Writable Stream của riêng bạn. Chúng ta sẽ tạo một stream đơn giản chỉ để 'in' dữ liệu ra console, nhưng theo phong cách 'stream'!
const { Writable } = require('stream');
// Tạo một Writable Stream "tùy chỉnh"
class MyConsoleWriter extends Writable {
constructor(options) {
super(options);
this.prefix = options && options.prefix ? options.prefix : '[LOG]';
console.log(`${this.prefix} Khởi tạo MyConsoleWriter...`);
}
// Phương thức _write là "trái tim" của Writable stream
// Nó sẽ được gọi mỗi khi có dữ liệu mới "đổ" vào stream
_write(chunk, encoding, callback) {
// chunk: Dữ liệu được gửi đến (thường là Buffer hoặc string)
// encoding: Mã hóa của chunk (ví dụ: 'utf8', 'buffer')
// callback: Hàm cần gọi khi bạn đã xử lý xong chunk này.
// Gọi callback(error) nếu có lỗi.
const data = chunk.toString(encoding); // Chuyển Buffer thành string
console.log(`${this.prefix} Nhận được dữ liệu: ${data.trim()}`);
// Rất quan trọng: Gọi callback() để báo hiệu đã xử lý xong chunk này
// và sẵn sàng nhận chunk tiếp theo.
callback();
}
// Phương thức _final (tùy chọn):
// Được gọi khi không còn dữ liệu nào được "đổ" vào stream nữa
// và stream đang chuẩn bị đóng lại. Thích hợp cho các tác vụ dọn dẹp cuối cùng.
_final(callback) {
console.log(`${this.prefix} Tất cả dữ liệu đã được xử lý. Stream đã đóng.`);
callback(); // Báo hiệu đã hoàn thành tác vụ cuối cùng
}
// Phương thức _destroy (tùy chọn):
// Được gọi khi stream bị hủy bỏ (ví dụ: có lỗi xảy ra hoặc gọi .destroy()).
// Thích hợp cho việc giải phóng tài nguyên.
_destroy(error, callback) {
if (error) {
console.error(`${this.prefix} Stream bị hủy do lỗi:`, error.message);
} else {
console.log(`${this.prefix} Stream bị hủy.`);
}
callback(error);
}
}
// --- Cách sử dụng MyConsoleWriter ---
const writer1 = new MyConsoleWriter({ prefix: '[APP LOG]' });
// Ghi dữ liệu trực tiếp vào stream
writer1.write('Hello Gen Z!');
writer1.write('Node.js streams are awesome.');
writer1.write('This is another chunk.');
// Khi không còn dữ liệu để ghi, gọi .end()
// Nó sẽ kích hoạt _final() và đóng stream.
writer1.end('Cuối cùng là chunk này, tạm biệt!');
console.log('\n--- Ví dụ 2: Dùng pipe() ---');
const writer2 = new MyConsoleWriter({ prefix: '[PIPE LOG]' });
const { Readable } = require('stream');
// Tạo một Readable stream đơn giản để "đẩy" dữ liệu vào Writable stream
const myReadableStream = new Readable({
read() {
this.push('Data from Readable 1');
this.push('Data from Readable 2');
this.push('Data from Readable 3');
this.push(null); // Báo hiệu không còn dữ liệu
}
});
// Dùng pipe() để kết nối Readable stream với Writable stream
// Dữ liệu từ myReadableStream sẽ tự động "chảy" vào writer2
myReadableStream.pipe(writer2);
// Thử nghiệm lỗi (uncomment để xem)
// writer1.destroy(new Error('Có lỗi xảy ra trong quá trình ghi!'));
Đoạn code trên 'cool ngầu' chưa? Các bạn thấy đấy, chúng ta chỉ cần tập trung vào việc xử lý từng 'chunk' dữ liệu trong _write. Node.js sẽ lo phần còn lại của 'luồng chảy' dữ liệu.

3. Mẹo 'hack não' & Best Practices (Ghi nhớ và dùng thực tế)
Giờ là lúc 'thầy Creyt' chia sẻ vài 'chiêu độc' để các bạn 'cân' đẹp stream.Writable:
- Luôn luôn gọi
callback(): Đây là 'lời thề' của bạn với Node.js rằng 'tôi đã xử lý xong chunk này rồi, gửi cái tiếp theo đi!'. Nếu quên gọi, stream của bạn sẽ 'đứng hình', không bao giờ nhận thêm dữ liệu nữa. 'Tạch' luôn! - Xử lý lỗi 'sương sương': Trong
_write, nếu có lỗi, hãy gọicallback(error)để báo hiệu cho stream biết. Điều này giúp stream phát ra sự kiệnerrorvà bạn có thể 'bắt' nó ở bên ngoài. Đừng để lỗi 'chìm nghỉm' như 'tàu Titanic' nhé. - Hiểu về
highWaterMark: Đây là 'dung tích' tối đa của bộ đệm (buffer) trước khiWritablebắt đầu 'kêu ca' vềbackpressure. Mặc định là 16KB cho object mode và 16KB cho byte mode. Điều chỉnh nó nếu bạn cần hiệu năng cao hơn hoặc muốn tiết kiệm bộ nhớ hơn. pipe()là 'tri kỷ': Khi kết nốiReadablestream vớiWritablestream, hãy dùngpipe(). Nó không chỉ tự động chuyển dữ liệu mà còn tự động quản lýbackpressurevà xử lý lỗi cho bạn. 'Nhàn tênh' luôn!_finalvà_destroycho 'sạch sẽ': Dùng_finalđể dọn dẹp khi stream kết thúc tự nhiên (ví dụ: đóng file, gửi nốt dữ liệu cuối cùng). Dùng_destroyđể giải phóng tài nguyên khi stream bị hủy đột ngột (ví dụ: lỗi mạng, người dùng cancel). 'Sạch sẽ' là 'đẳng cấp'!
4. Ứng dụng thực tế: 'Dân chơi' nào đang dùng Writable?
Không phải chỉ mấy 'ông lớn' mới dùng đâu nha, Writable có mặt ở khắp mọi nơi mà có 'luồng' dữ liệu đi ra:
- Ghi file log: Các hệ thống logging như Winston, Pino đều dùng
Writable Streamđể ghi log vào file. Tưởng tượng một server chạy 24/7, log vài GB mỗi ngày mà không có stream thì 'toang' RAM! - Upload file lên cloud: Khi bạn upload một video 4K lên YouTube hay một file lớn lên Google Drive, dữ liệu không được load hết vào RAM server rồi mới gửi đi. Nó được 'stream' từng phần một tới dịch vụ lưu trữ.
- Chuyển đổi và xử lý dữ liệu lớn (ETL): Khi bạn cần đọc hàng triệu dòng dữ liệu từ database, biến đổi chúng, rồi ghi vào một database khác,
Writable Streamlà 'người bạn thân' giúp bạn làm điều đó mà không 'sập' server. - Nén/giải nén dữ liệu: Các module như
zlibtrong Node.js cũng sử dụngWritable Streamđể nhận dữ liệu thô và xuất ra dữ liệu đã nén (hoặc ngược lại). - HTTP Responses: Khi bạn gửi một phản hồi HTTP lớn (ví dụ: một file JSON khổng lồ, một trang HTML phức tạp) về client,
resobject trong Express/Koa cũng là mộtWritable Streamđấy! Nó cho phép bạnres.write()từng phần.
5. Thử nghiệm của 'thầy Creyt' & Khi nào nên 'triển'?
Thầy Creyt đã từng 'chinh chiến' với một dự án phải xử lý hàng trăm GB dữ liệu CSV từ một hệ thống cũ. Nhiệm vụ là đọc, parse, transform rồi ghi vào database mới. Nếu không có Writable Stream (kết hợp với Readable và Transform stream), chắc thầy phải 'cắm mặt' mấy ngày để tối ưu RAM rồi. Nhưng nhờ stream, thầy chỉ cần 'dây chuyền hóa' quy trình, dữ liệu cứ thế 'chảy' từ bước này sang bước khác một cách 'mượt mà', không tốn nhiều RAM, lại còn xử lý được cả 'backpressure' khi database bị quá tải.
Khi nào nên dùng stream.Writable?
- Khi dữ liệu của bạn 'quá khổ': Lớn hơn dung lượng RAM mà bạn muốn cấp cho ứng dụng.
- Khi bạn cần xử lý dữ liệu 'real-time': Dữ liệu đến đâu xử lý đến đó, không cần chờ đợi.
- Khi bạn muốn 'kiểm soát' luồng dữ liệu: Đặc biệt là khả năng 'backpressure' để tránh 'nghẽn' hệ thống.
- Khi bạn muốn tạo các 'module' tái sử dụng: Ví dụ, một module ghi log tùy chỉnh, một module xuất dữ liệu sang định dạng cụ thể.
Khi nào không nên 'cố chấp' dùng Writable?
- Dữ liệu 'bé tí teo': Nếu dữ liệu chỉ vài KB hoặc MB, việc dùng stream có thể làm code phức tạp hơn mà không mang lại lợi ích đáng kể về hiệu suất hay bộ nhớ. Đọc/ghi cả cục một lần sẽ đơn giản hơn.
- Khi bạn không cần quản lý 'backpressure': Nếu tốc độ xử lý đầu ra luôn nhanh hơn đầu vào, thì việc tối ưu
backpressurecó thể không cần thiết.
Vậy đó, stream.Writable không chỉ là một khái niệm 'hàn lâm' mà là một 'công cụ chiến lược' giúp các bạn Gen Z 'tung hoành' trong thế giới Node.js, xử lý dữ liệu một cách 'thông minh' và 'hiệu quả'. Hãy 'quẩy' lên và thử nghiệm ngay đi nhé!
Thuộc Series: Nodejs
Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!