Stream.Writable: Bí kíp làm chủ 'Đầu Ra' dữ liệu của Gen Z coder!
Nodejs

Stream.Writable: Bí kíp làm chủ 'Đầu Ra' dữ liệu của Gen Z coder!

Author

Admin System

@root

Ngày xuất bản

21 Mar, 2026

Lượt xem

2 Lượt

"stream.Writable"

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ứ? Writable giú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ì Writable sẽ '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.

Illustration

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ọi callback(error) để báo hiệu cho stream biết. Điều này giúp stream phát ra sự kiện error và 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 khi Writable bắ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ối Readable stream với Writable stream, hãy dùng pipe(). Nó không chỉ tự động chuyển dữ liệu mà còn tự động quản lý backpressure và xử lý lỗi cho bạn. 'Nhàn tênh' luôn!
  • _final_destroy cho '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 Stream là '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ư zlib trong Node.js cũng sử dụng Writable 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, res object trong Express/Koa cũng là một Writable Stream đấy! Nó cho phép bạn res.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 ReadableTransform 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 backpressure có 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é!

#tech #cyberpunk #laravel
Chỉnh sửa bài viết

Bình luận (0)

Vui lòng Đăng Nhập để Bình luận

Hỗ trợ Markdown cơ bản
Nguyễn Văn A
1 ngày trước

Tính năng này đỉnh quá ad ơi, chờ mãi mới thấy một blog Tiếng Việt có UI/UX xịn như vầy!