Cài đặt RxJS
Thiết lập RxJS với npm, TypeScript và môi trường thực hành.
Cài được package chưa có nghĩa là môi trường đã sẵn sàng để học RxJS. Bạn còn cần biết TypeScript có resolve đúng kiểu dữ liệu không, lệnh nào thật sự chạy file, và vì sao một chương trình dùng interval có thể không chịu thoát.
Trang này dựng một playground Node.js nhỏ, chạy một stream từ đầu đến cuối, rồi dùng chính output để đọc subscribe, complete và teardown. Nếu bạn đã có project TypeScript hoặc framework, hãy xem phần Đưa RxJS vào project hiện có thay vì tạo project mới.
Phạm vi phiên bản
Các lệnh và API trong bài nhắm đến RxJS 7.8.2, bản ổn định thuộc nhánh RxJS 7 tại thời điểm kiểm chứng. Ví dụ chỉ dùng public import từ rxjs và không giả định API của RxJS 8.
Mục lục
- Bạn sẽ dựng gì
- Mental model: cài đặt rồi chạy
- Chuẩn bị
- Tạo playground TypeScript
- Đọc lifecycle từ output
- Teardown khi hủy thủ công
- Ví dụ thực tế: dừng worker bằng Ctrl+C
- Đưa RxJS vào project hiện có
- Pitfalls thường gặp
- Xử lý lỗi cài đặt
- Tiếp tục lộ trình
- Nguồn tham khảo
Bạn sẽ dựng gì
Sau các bước bên dưới, thư mục thực hành có dạng:
rxjs-playground/
├── node_modules/
├── src/
│ └── index.ts
├── package-lock.json
├── package.json
└── tsconfig.jsonBạn sẽ có:
rxjstrongdependencies, vì code chạy thật sự cần runtime của thư viện;typescript,tsxvà@types/nodetrongdevDependencies, vì chúng chỉ phục vụ việc viết, kiểm tra và chạy source TypeScript;- một lệnh type-check riêng và một lệnh chạy riêng;
- một stream hữu hạn để chương trình tự complete, cùng một stream sống lâu để luyện teardown.
Mình khuyên tạo playground riêng thay vì cài thêm package vào repo tài liệu hoặc project đang làm việc. Một môi trường nhỏ giúp bạn nhìn rõ lỗi thuộc về RxJS, TypeScript hay cấu hình project.
Mental model: cài đặt rồi chạy
npm install chỉ đặt runtime JavaScript và declaration TypeScript của RxJS vào node_modules. Nó không tự tạo Observable, không chạy operator và cũng không tự subscribe.
npm install
│
▼
node_modules/rxjs
(runtime JavaScript + type declarations)
│
├──► tsc đọc declarations để kiểm tra kiểu
│
└──► tsx nạp runtime để chạy src/index.ts
│
▼
subscribe()
│
source ─► operators ─► Observer
│
▼
complete / error / unsubscribe
│
▼
teardownHãy tách ba việc trong đầu:
- Cài package làm cho module
rxjscó mặt trong dependency graph. - Type-check xác nhận import và kiểu dữ liệu hợp lệ, nhưng không chạy stream.
- Thực thi chạy file; với phần lớn cold Observable, source chỉ bắt đầu khi có
subscribe().
Cách tách này đặc biệt hữu ích khi debug: tsx chạy được chưa chắc code đã qua type-check, còn tsc --noEmit thành công chưa chứng minh pipeline có output đúng.
Chuẩn bị
Bạn cần một bản Node.js còn được hỗ trợ và npm đi kèm. Playground trong bài dùng Node.js 20 trở lên để có môi trường hiện đại, nhưng đây là lựa chọn cho bài học, không phải tuyên bố về phiên bản Node.js tối thiểu của mọi ứng dụng RxJS.
Kiểm tra công cụ trước khi tạo project:
node --version
npm --versionNếu terminal không nhận ra node hoặc npm, hãy cài Node.js trước rồi mở terminal mới. Đừng dùng sudo npm install để chữa lỗi quyền truy cập; cách đó dễ tạo file do root sở hữu trong project. Nên sửa cách cài Node.js hoặc dùng một version manager phù hợp với hệ điều hành.
Tạo playground TypeScript
Khởi tạo project và cài package
Chạy các lệnh sau trong thư mục dành cho bài tập, không phải trong repo tài liệu này:
mkdir rxjs-playground
cd rxjs-playground
npm init -y
npm install rxjs@7.8.2
npm install --save-dev typescript tsx @types/node
npm pkg set type=module
mkdir srcViệc pin rxjs@7.8.2 giúp output của bài không thay đổi vì một major release trong tương lai. npm cũng tạo package-lock.json; hãy commit lockfile trong ứng dụng để các máy cài cùng dependency tree.
Xác nhận package thực sự được resolve từ project hiện tại:
npm ls rxjsKết quả mong đợi có dòng tương tự:
rxjs-playground@1.0.0
└── rxjs@7.8.2Nếu npm ls báo (empty), thường là bạn đã chạy npm install ở thư mục khác. Kiểm tra pwd hoặc vị trí hiện tại của terminal trước khi cài lại.
Cấu hình ESM và TypeScript
Tạo tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noEmit": true,
"types": ["node"],
"skipLibCheck": true
},
"include": ["src/**/*.ts"]
}"type": "module" trong package.json và cặp NodeNext trong tsconfig.json cùng mô tả cách Node.js resolve ESM. Giữ chúng nhất quán sẽ tránh tình trạng TypeScript hiểu file theo một module system còn runtime hiểu theo hệ khác.
noEmit có nghĩa tsc chỉ kiểm tra kiểu. tsx sẽ transpile và chạy file trực tiếp trong lúc học, nên bạn chưa cần thư mục dist hay một build pipeline riêng.
tsx không thay thế type-check
tsx ưu tiên chạy nhanh và không thực hiện đầy đủ vai trò kiểm tra kiểu của tsc. Hãy chạy cả npx tsc --noEmit và npx tsx src/index.ts; hai lệnh trả lời hai câu hỏi khác nhau.
Viết stream đầu tiên
Tạo src/index.ts với import đầy đủ từ public entry point rxjs:
import { finalize, interval, map, take } from 'rxjs';
const statuses = ['đã nhận', 'đang xử lý', 'hoàn tất'] as const;
const orderStatus$ = interval(200).pipe(
take(statuses.length),
map((index) => statuses[index]),
finalize(() => console.log('teardown: đã dọn timer')),
);
const subscription = orderStatus$.subscribe({
next: (status) => console.log('next:', status),
error: (error: unknown) => console.error('error:', error),
complete: () => console.log('complete'),
});
console.log('ngay sau subscribe:', subscription.closed);
setTimeout(() => {
console.log('sau khi kết thúc:', subscription.closed);
}, 750);Tên biến có hậu tố $ là convention để báo rằng biến chứa Observable; đây không phải cú pháp bắt buộc của RxJS. Ví dụ mô phỏng trạng thái một đơn hàng, nhưng timer chỉ để bạn quan sát lifecycle rõ ràng, không phải cách lưu trạng thái đơn hàng production.
Type-check và chạy
Trước hết, kiểm tra kiểu mà không sinh file JavaScript:
npx tsc --noEmitNếu lệnh không in lỗi và trả exit code 0, chạy chương trình:
npx tsx src/index.tsOutput xuất hiện theo thời gian như sau:
ngay sau subscribe: false
next: đã nhận
next: đang xử lý
next: hoàn tất
complete
teardown: đã dọn timer
sau khi kết thúc: trueinterval(200) không phát 0 ngay lập tức; emission đầu tiên đến sau một period. Thời điểm thực tế có thể lệch đôi chút do event loop, nhưng thứ tự các dòng trên không đổi trong ví dụ này.
Đọc lifecycle từ output
Khi khai báo orderStatus$, bạn mới tạo một công thức. Timer bắt đầu khi subscribe() nối Observer vào pipeline.
subscribe
│
▼
interval: 0 ─────► 1 ─────► 2 ─────► ...
│ │ │
└──────────────┴─────────┴──► take(3)
│
├──► map: trạng thái
├──► Observer.complete()
└──► unsubscribe upstream
│
▼
finalizeLifecycle cụ thể là:
subscribe()trả về mộtSubscription; lúc nàyclosedlàfalse.intervalphát các số tăng dần.mapđổi chỉ số thành trạng thái để Observer nhận quanext.- Sau ba giá trị,
take(3)gửicompletexuống Observer và hủy subscription phía upstream. - Teardown của
intervaldọn tác vụ định kỳ; callback trongfinalizechạy khi chuỗi kết thúc. - Lần kiểm tra sau cùng thấy
subscription.closedlàtrue.
complete là notification từ pipeline đến Observer. Teardown là quá trình giải phóng tài nguyên của execution. Hai ý liên quan nhưng không đồng nghĩa: một subscription cũng có thể teardown do error hoặc do consumer tự gọi unsubscribe().
Teardown khi hủy thủ công
Bây giờ thay nội dung src/index.ts bằng một stream không tự complete:
import { finalize, interval } from 'rxjs';
const heartbeat$ = interval(300).pipe(
finalize(() => console.log('finalize: đã dọn heartbeat')),
);
const subscription = heartbeat$.subscribe({
next: (tick) => console.log('tick:', tick),
complete: () => console.log('complete'),
});
setTimeout(() => {
console.log('hủy subscription');
subscription.unsubscribe();
console.log('closed:', subscription.closed);
}, 750);Output mong đợi:
tick: 0
tick: 1
hủy subscription
finalize: đã dọn heartbeat
closed: trueBạn sẽ không thấy dòng complete. Gọi unsubscribe() dừng execution và chạy teardown, nhưng không biến thao tác hủy của consumer thành notification complete. Vì thế, đừng đặt cleanup bắt buộc chỉ trong callback complete; finalize hoặc teardown của source mới bao phủ cả complete, error và hủy chủ động.
Nếu bỏ take(...) ở ví dụ trước và cũng không gọi unsubscribe(), timer tiếp tục giữ Node.js process sống. Đây là lý do một bài thử RxJS đôi khi “chạy xong output” nhưng terminal vẫn không trả prompt.
Đọc kỹ hơn về quyền sở hữu subscription tại Subscription và teardown.
Ví dụ thực tế: dừng worker bằng Ctrl+C
Một worker thường có công việc định kỳ và cần dừng gọn khi người vận hành nhấn Ctrl+C. Bạn có thể biểu diễn cả heartbeat lẫn tín hiệu SIGINT thành Observable, rồi để takeUntil nối hai lifecycle lại:
import { finalize, fromEvent, interval, takeUntil, tap } from 'rxjs';
const shutdown$ = fromEvent(process, 'SIGINT');
const worker$ = interval(1_000).pipe(
tap((tick) => console.log(`heartbeat ${tick}`)),
takeUntil(shutdown$),
finalize(() => console.log('teardown: đã dọn timer và listener')),
);
worker$.subscribe({
complete: () => console.log('worker đã dừng'),
error: (error: unknown) => console.error('worker lỗi:', error),
});Chạy file rồi nhấn Ctrl+C sau vài giây. Một phiên minh họa có thể in:
heartbeat 0
heartbeat 1
^Cworker đã dừng
teardown: đã dọn timer và listenerfromEvent đăng ký listener cho SIGINT khi có subscription. Khi listener phát giá trị đầu tiên, takeUntil complete worker$; quá trình teardown gỡ cả timer của interval và listener của shutdown$, nên process có thể thoát. Ví dụ này cho thấy lợi ích thực tế của RxJS: thay vì tự giữ hai handler rồi nhớ dọn từng cái, bạn mô tả quan hệ “chạy cho đến khi có tín hiệu dừng” trong một pipeline.
Giới hạn của ví dụ
Đây là graceful shutdown tối thiểu. Worker production còn phải ngừng nhận việc mới, chờ tác vụ đang chạy, đặt timeout cưỡng bức và trả exit code phù hợp. Xem thêm bối cảnh server tại RxJS với Node.js.
Đưa RxJS vào project hiện có
Project TypeScript hoặc bundler
Nếu project đã có TypeScript, Vite, Next.js hoặc bundler khác, bạn thường chỉ cần:
npm install rxjs@7.8.2Không cài lại typescript, tsx hay chép nguyên tsconfig.json của playground nếu project đã có toolchain. Cấu hình NodeNext ở trên dành cho việc chạy trực tiếp bằng Node.js; bundler của ứng dụng có thể cần moduleResolution khác.
Với RxJS 7.8.x, hãy ưu tiên public import từ package root:
import { fromEvent, map } from 'rxjs';Đừng import từ rxjs/internal/.... Đường dẫn internal là chi tiết triển khai, không phải API contract dành cho ứng dụng. Các entry point chuyên biệt như rxjs/ajax, rxjs/testing hoặc rxjs/webSocket chỉ nên dùng khi bạn thật sự cần module tương ứng.
Project Angular
Angular project thường đã có rxjs trong dependency tree. Trước khi cài thêm, kiểm tra:
npm ls rxjsHãy giữ phiên bản trong range mà phiên bản Angular của project hỗ trợ. Đừng ép rxjs@7.8.2 chỉ để giống playground nếu việc đó xung đột với peer dependency của framework; ở đây compatibility của ứng dụng quan trọng hơn việc khớp tuyệt đối phiên bản bài học. Phần tích hợp framework nằm tại RxJS với Angular.
Chọn range phiên bản
Ba cách cài phục vụ ba mục tiêu khác nhau:
| Lệnh | Ý nghĩa | Khi nên dùng |
|---|---|---|
npm install rxjs@7.8.2 | Pin đúng một phiên bản | Tutorial, workshop hoặc lúc cần tái lập output chính xác. |
npm install rxjs@^7.8.2 | Cho phép các bản tương thích theo semver trong cùng major | Ứng dụng muốn nhận patch/minor có kiểm soát qua lockfile và CI. |
npm install rxjs@7 | Lấy bản mới nhất trong major 7 tại thời điểm cài | Thử nghiệm nhánh 7, khi không cần khóa minor ban đầu. |
Mặc định cho bài này, mình chọn pin 7.8.2. Với ứng dụng thật, mình thường để range phù hợp chính sách cập nhật của team và dựa vào lockfile cùng CI để kiểm soát thay đổi. Tránh dùng tag prerelease nếu mục tiêu chỉ là học API ổn định của RxJS 7.
Pitfalls thường gặp
- Chạy file nhưng không có output: tạo Observable hoặc gọi
pipe(...)chưa khởi động cold source. Kiểm tra xem code đãsubscribe()chưa. - Node.js process không thoát: một
interval, event listener hoặc resource khác vẫn còn subscription sống. Dùng operator kết thúc phù hợp nhưtake,takeUntil, hoặc gọiunsubscribe()ở lifecycle owner. - Chỉ chạy
tsxrồi tưởng type-safe:tsxcó thể thực thi trong khitscvẫn tìm thấy lỗi kiểu. Giữ bướcnpx tsc --noEmittrong workflow. - Import từ đường dẫn internal: autocomplete có thể gợi ý đường dẫn sâu, nhưng chúng làm code phụ thuộc vào cấu trúc bên trong thư viện. Chỉ import từ public entry point.
- Cài RxJS cả global lẫn local: code của project resolve dependency local.
npm install --global rxjskhông thay thế cho dependency trongpackage.json. - Tạo nhiều subscription ngoài ý muốn: với cold Observable, mỗi lần
subscribe()có thể tạo timer, listener hoặc request riêng. Cài đúng package không tự động chia sẻ execution. - Tin rằng unsubscribe sẽ gọi complete: hủy chủ động chạy teardown nhưng callback
completecủa Observer không chạy.
Các khái niệm phía sau những lỗi này được mở rộng tại RxJS là gì?, Subscribe và Observer và Playground và debug.
Xử lý lỗi cài đặt
| Triệu chứng | Nguyên nhân thường gặp | Cách kiểm tra và sửa |
|---|---|---|
Cannot find module 'rxjs' | Package chưa được cài trong project hiện tại | Chạy npm ls rxjs, kiểm tra thư mục hiện tại, rồi chạy lại npm install rxjs@7.8.2. |
TypeScript không hiểu process | Thiếu Node.js declarations hoặc types không chứa node | Cài @types/node ở devDependencies và dùng "types": ["node"] cho playground Node.js. |
| Lỗi lẫn lộn ESM và CommonJS | package.json, module và runtime không cùng module system | Với playground này, giữ "type": "module", module: "NodeNext" và moduleResolution: "NodeNext". |
tsx chạy nhưng tsc báo lỗi | Execution và type-check là hai bước độc lập | Sửa lỗi từ npx tsc --noEmit; đừng lấy việc chạy được làm bằng chứng kiểu dữ liệu đúng. |
| Có nhiều phiên bản RxJS | Các dependency yêu cầu range khác nhau | Đọc toàn bộ cây từ npm ls rxjs; ưu tiên range framework hỗ trợ thay vì ép dedupe mù quáng. |
| Terminal không trả prompt | Stream hoặc resource vẫn sống | Thêm finalize để quan sát teardown, rồi xác định nơi cần take, takeUntil hay unsubscribe(). |
Nếu lỗi xuất hiện trong một project lớn nhưng không tái hiện ở playground, khác biệt thường nằm ở module resolution, framework plugin hoặc dependency tree của project đó. So sánh package.json, tsconfig.json và kết quả npm ls rxjs trước khi thay operator trong code.
Tiếp tục lộ trình
Môi trường đã ổn khi cả type-check lẫn hai ví dụ lifecycle chạy đúng. Bước tiếp theo không phải học thuộc hàng chục operator; hãy nắm chắc ai tạo execution, ai nhận notification và ai chịu trách nhiệm teardown.
Subscribe và Observer
Đọc ba callback next, error, complete và cách subscribe khởi động execution.
Pipe và operator
Ghép các phép biến đổi thành pipeline có thể đọc và tái sử dụng.
Playground và debug
Quan sát giá trị, thời gian và lỗi khi thử nghiệm stream.
Subscription và teardown
Quản lý tài nguyên cho stream sống lâu và các đường kết thúc khác nhau.
Nguồn tham khảo
- RxJS — Installation Instructions — lệnh cài package
rxjsqua npm và cách package cung cấp module cho consumer. - Gói
rxjs7.8.2 trên npm — phiên bản phát hành và metadata của package. - RxJS 7.8.2 —
package.json— public exports, runtime entry points và TypeScript declarations. - RxJS 7.8.2 — source của
interval— emission đầu tiên sau một period và chuỗi số tăng dần. - RxJS 7.8.2 — source của
take— phát tối đa số lượng giá trị đã chọn rồi complete. - RxJS 7.8.2 — source của
finalize— callback chạy khi complete, error hoặc unsubscribe chủ động.