Quản lý dependency: lockfile, ba loại dependency, và rủi ro supply chain
Một dự án Node thực tế kéo theo hàng trăm tới hàng nghìn package gián tiếp. Quản lý chúng không chỉ là npm install — mà là kiểm soát chính xác phiên bản nào chạy ở production (lockfile), phân loại đúng dependency nào cần lúc chạy / lúc dev / do host cung cấp, và phòng các package độc hại hoặc dính lỗ hổng (supply chain). Làm ẩu ở đây dẫn tới "chạy được trên máy tôi" do lệch phiên bản, hoặc tệ hơn, một package bị chiếm quyền tiêm mã độc vào build.
Cơ chế hoạt động
package.json khai dải phiên bản chấp nhận ("^4.18.0"); lockfile (package-lock.json/yarn.lock/pnpm-lock.yaml) ghi chính xác phiên bản đã resolve cho từng package, kể cả gián tiếp. npm ci cài đúng theo lockfile — đây là lệnh dùng ở CI/production để build tái lập được.
{
"dependencies": { "express": "^4.18.0" }, // cần lúc runtime -> vào production
"devDependencies": { "jest": "^29.0.0" }, // chỉ lúc dev/test -> KHÔNG vào production
"peerDependencies":{ "react": ">=18" } // host phải cung cấp; package không tự cài
}
Ba loại khác mục đích: dependencies đi cùng app khi chạy; devDependencies chỉ cho dev (build, test, lint) và bị bỏ khi npm install --production; peerDependencies là cách một thư viện nói "tôi cần host có React 18, nhưng tôi không tự mang React để tránh hai bản React xung đột".
Vấn đề gặp trong production
Failure mode: không commit lockfile / không dùng npm ci. Nếu CI chạy npm install không có lockfile, nó resolve lại dải ^ và có thể lấy phiên bản mới hơn lúc dev — "chạy được trên máy tôi" vì máy dev có lockfile cũ còn CI thì không. Lockfile phải được commit, và CI/production dùng npm ci (cài đúng lockfile, fail nếu lệch) để mọi môi trường chạy cùng cây phụ thuộc.
Failure mode: phân loại sai dependency. Để một thư viện build (TypeScript, một bundler) trong dependencies làm image production phình và chứa thứ không cần; ngược lại để một package runtime trong devDependencies làm production thiếu module lúc chạy (Cannot find module chỉ trên production sau khi --production). Phân loại đúng theo "có cần khi app chạy không".
Failure mode: supply chain attack. Đây là rủi ro nghiêm trọng và thực tế: một package (thường là dependency gián tiếp sâu) bị chiếm tài khoản maintainer hoặc bị chèn mã độc trong một bản phát hành mới, và mọi dự án tự động nâng qua dải ^ sẽ kéo bản độc về. Phòng vệ: lockfile (không tự nhảy phiên bản), npm audit quét lỗ hổng đã biết, ghim phiên bản cho dependency nhạy cảm, và xem kỹ trước khi thêm package mới (số lượt dùng, lịch sử bảo trì, số dependency con). Càng ít dependency, bề mặt tấn công càng nhỏ.
Failure mode: dependency trùng lặp/xung đột phiên bản. Hai package cần hai phiên bản khác nhau của cùng một thư viện làm cây phình và đôi khi gây bug tinh vi (hai bản của cùng một lib không nhận nhau). npm dedupe gom bớt; với thư viện cần một bản dùng chung (như React), peerDependencies là cơ chế tránh trùng.
Cách debug và monitor
"Chạy được local, lỗi trên CI/production" với khác biệt hành vi package — kiểm tra lockfile đã commit chưa và CI có dùng npm ci không. Cannot find module chỉ trên production sau khi cài --production nghĩa là một runtime dependency bị xếp nhầm vào devDependencies. Chạy npm audit định kỳ (và trong CI) để bắt lỗ hổng đã biết; tích hợp một công cụ như Dependabot/Renovate để được cảnh báo và nâng có kiểm soát. Theo dõi kích thước cây phụ thuộc (npm ls) — tăng đột biến khi thêm một package nhỏ nghĩa là nó kéo theo nhiều dependency con, cân nhắc lại. Trước khi thêm package, kiểm tra độ phổ biến, tần suất bảo trì, và số dependency con.
Tradeoff
Dùng package mới nhất có tính năng và vá lỗi mới — đổi lại rủi ro breaking change và (với bản vừa phát hành) rủi ro supply chain chưa kịp phát hiện. Ghim phiên bản và cập nhật có kiểm soát thì ổn định và an toàn hơn, đổi lại chậm nhận tính năng/vá. Quy tắc thực tế: luôn commit lockfile và dùng npm ci ở CI/production để build tái lập; phân loại dependency đúng theo nhu cầu runtime; chạy npm audit và cập nhật theo lịch có kiểm soát (đọc changelog, test) thay vì nâng mù; giữ số dependency tối thiểu để giảm bề mặt tấn công. Cân giữa "mới" và "ổn định" theo mức độ quan trọng của hệ thống.
Câu hỏi phỏng vấn
peerDependencydùng khi nào, và vì sao phải commit lockfile và dùngnpm ci?
peerDependencies dùng khi một thư viện cần host cung cấp một package chung thay vì tự mang theo — điển hình là plugin/component library cần React: khai React là peer dependency để package không tự cài React riêng, tránh việc có hai bản React trong cây gây xung đột (hook không hoạt động, context không khớp). Phải commit lockfile vì package.json chỉ khai dải phiên bản (^4.18.0) còn lockfile ghi chính xác phiên bản đã resolve cho mọi package gồm gián tiếp; không có nó, mỗi lần cài có thể resolve ra phiên bản khác, gây "chạy được trên máy tôi". npm ci cài đúng theo lockfile và fail nếu lệch, nên dùng ở CI/production để mọi môi trường chạy cùng cây phụ thuộc, build tái lập được. Điểm ăn điểm: lockfile + npm ci cũng là phòng vệ supply chain (không tự nhảy sang bản độc qua dải ^), kết hợp npm audit, ghim phiên bản nhạy cảm, và giữ ít dependency để giảm bề mặt tấn công; phân loại sai runtime dependency vào devDependencies gây Cannot find module chỉ trên production.
Hands-on
Lấy một dự án thật, xóa node_modules và cài bằng npm install không lockfile rồi bằng npm ci có lockfile, so sánh cây phụ thuộc resolve ra để thấy vì sao lockfile + npm ci cho build tái lập. Cố tình xếp một runtime dependency vào devDependencies, chạy npm install --production, và quan sát Cannot find module lúc chạy — rồi sửa phân loại. Chạy npm audit để xem các lỗ hổng đã biết và thử nâng một package có lỗ hổng theo cách có kiểm soát (đọc changelog, test). Cuối cùng thêm một package nhỏ và dùng npm ls để thấy nó kéo theo bao nhiêu dependency con, đánh giá bề mặt tấn công tăng thêm.
Top comments (0)