Bỏ qua điều hướng

npm là gì? Trình quản lý gói mặc định của Node.js

npm là trình quản lý gói của Node.js, nơi package.json, semver và package-lock.json phối hợp để mỗi lần cài đặt đều cho ra cùng một cây phụ thuộc.

Tuan Tran Van
17 phút đọc
Mục lục (10 phần)
  1. npm là gì?
  2. package.json: bản khai của một dự án
  3. Semver: dấu ^ và ~ quyết định bạn nhận bản nào
  4. npm install làm gì với thư mục node_modules?
  5. package-lock.json và npm ci: vì sao cài lại luôn giống nhau
  6. npm scripts: chạy tác vụ dự án bằng một câu lệnh
  7. Install script và chuỗi cung ứng: npm v12 đổi mặc định
  8. npm, Yarn hay pnpm: khác nhau ở đâu?
  9. Nên bắt đầu với npm từ đâu?
  10. Tài liệu tham khảo

npm là trình quản lý gói (package manager) mặc định cho Node.js, cho phép bạn cài đặt, chia sẻ và quản lý các thư viện mã nguồn (dependencies) một cách nhất quán giữa những môi trường phát triển khác nhau.

Trả lời thẳng câu hỏi npm là gì: đây là hạ tầng để tái sử dụng mã nguồn và quản lý những cây phụ thuộc phức tạp trong một dự án JavaScript hiện đại.

Hệ thống npm gồm ba thành phần riêng biệt nhưng vận hành thống nhất: trang web (npmjs.com), giao diện dòng lệnh (CLI) và kho lưu trữ (Registry). Trang web là nơi bạn tìm kiếm gói, thiết lập hồ sơ cá nhân và quản lý các tổ chức để điều phối quyền truy cập mã nguồn. Registry là cơ sở dữ liệu khổng lồ chứa hàng triệu gói mã nguồn JavaScript cùng thông tin meta đi kèm. CLI là công cụ chính bạn dùng qua terminal để tương tác với hai thành phần còn lại.

Tính đến tháng 9/2022, npm Registry đã ghi nhận hơn 2,1 triệu gói phần mềm, giữ vị thế kho lưu trữ mã nguồn cho một ngôn ngữ duy nhất lớn nhất thế giới. Nó phục vụ mọi nhu cầu từ dự án cá nhân nhỏ lẻ đến hệ thống backend doanh nghiệp và các framework frontend phức tạp.

Với một kỹ sư, npm không dừng ở việc tải thư viện: đây là nơi bạn khoá phiên bản, kiểm tra bảo mật và tự động hoá các tác vụ build.

Nắm vững cách npm vận hành là điều kiện tiên quyết để dựng được build pipeline ổn định trong môi trường CI/CD chuyên nghiệp.

npm là trình quản lý gói của Node.js, hình ảnh chủ đề của bài viết

npm là gì?

npm (Node Package Manager) hoạt động dựa trên sự phối hợp giữa ba thành phần chính. Thành phần đầu tiên là Registry, thực chất là một cơ sở dữ liệu công khai lưu trữ các tập tin nén (tarball) của mã nguồn và siêu dữ liệu (metadata) của hàng triệu gói JavaScript. Registry này vận hành trên nền tảng cơ sở dữ liệu CouchDB, cho phép kỹ sư khắp thế giới truy xuất mã nguồn gần như tức thời qua mạng lưới phân phối nội dung (CDN).

Ba thành phần của npm: trang web, giao diện dòng lệnh và kho lưu trữ Registry

Thành phần thứ hai là giao diện dòng lệnh (CLI), công cụ được cài đặt tự động khi bạn cài Node.js. CLI là cầu nối trực tiếp giữa máy tính cục bộ và Registry. Qua CLI, bạn khởi tạo dự án với npm init, cài thư viện với npm install, hoặc xuất bản gói mã nguồn của riêng mình. CLI cung cấp các thuật toán giải quyết xung đột phiên bản và tối ưu hoá cây phụ thuộc trong thư mục dự án.

Thành phần thứ ba là trang web npmjs.com, đóng vai trò cổng thông tin và quản trị. Tại đây bạn tìm kiếm thư viện dựa trên độ phổ biến, chất lượng mã nguồn hoặc mức độ bảo trì, đồng thời quản lý các tổ chức (Organizations) và nhóm (Teams). Điều này quan trọng với doanh nghiệp cần quản lý các gói riêng tư và phân quyền truy cập cho từng bộ phận phát triển.

Bạn dùng npm cho nhiều việc hơn là tải mã nguồn. Nó tải các công cụ chạy độc lập phục vụ quá trình phát triển, và chạy gói trực tiếp mà không cần cài đặt vĩnh viễn thông qua npx. npx giúp tránh làm ô nhiễm môi trường biến toàn cục và đảm bảo bạn luôn dùng phiên bản mới nhất của một công cụ trong các tác vụ build một lần.

package.json: bản khai của một dự án

Tệp package.json là bản kế hoạch (manifest) của bất kỳ dự án Node.js nào. Nó định danh dự án qua tên, phiên bản và các mô tả kỹ thuật, đồng thời liệt kê toàn bộ thư viện phụ thuộc cần thiết. Khi bạn chuyển dự án giữa các máy tính hoặc máy chủ, bạn không cần mang theo hàng nghìn tệp trong thư mục thư viện — chỉ cần tệp package.json là đủ để tái tạo lại toàn bộ môi trường bằng npm install.

Các loại phụ thuộc trong package.json: dependencies, devDependencies, optionalDependencies và peerDependencies

Trong cấu trúc của package.json, các phụ thuộc được phân loại theo mục đích dùng lúc chạy hay lúc phát triển. dependencies là các gói bắt buộc để ứng dụng hoạt động thực tế, ví dụ framework Express. devDependencies là các gói chỉ phục vụ quá trình code, test hoặc build, như Jest hoặc Webpack. Về mặt kỹ thuật, cờ -S (hoặc --save) đưa gói vào dependencies, còn -D (hoặc --save-dev) đưa vào devDependencies. Từ npm 5, -S đã thành mặc định, nhưng nhiều kỹ sư vẫn viết ra để tường minh hoá ý định trong tài liệu hướng dẫn.

optionalDependencies là phần quan trọng nhưng thường bị bỏ qua. Loại này cho phép cài các gói mà nếu gặp lỗi lúc build — thường do không tương thích hệ điều hành hoặc thiếu trình biên dịch C++ — npm vẫn tiếp tục quy trình cài đặt thay vì dừng lại. Đổi lại, bạn phải tự viết mã xử lý khi các thư viện này không tồn tại. Một loại khác là peerDependencies, thường thấy khi phát triển plugin hoặc thư viện con, yêu cầu dự án chính phải có sẵn một phiên bản cụ thể của thư viện nền tảng, ví dụ React.

Đây là cấu trúc một tệp package.json tiêu chuẩn:

json
{
  "name": "enterprise-backend-service",
  "version": "1.2.4",
  "description": "Hệ thống quản lý logic nghiệp vụ",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "test": "jest"
  },
  "dependencies": {
    "express": "^4.18.2",
    "lodash": "^4.17.21"
  },
  "devDependencies": {
    "jest": "^29.3.1",
    "webpack": "^5.75.0"
  },
  "optionalDependencies": {
    "fsevents": "^2.3.2"
  }
}

Semver: dấu ^ và ~ quyết định bạn nhận bản nào

npm quản lý phiên bản theo tiêu chuẩn Semantic Versioning (Semver), với định dạng Major.Minor.Patch. Major thay đổi khi có API mới phá vỡ tương thích ngược. Minor thay đổi khi thêm tính năng mới theo cách tương thích ngược. Patch dành cho các bản vá lỗi. Hiểu cách npm giải quyết các dải phiên bản là kỹ năng cốt lõi để tránh lỗi runtime bất ngờ khi cập nhật thư viện.

Dấu caret cho phép cập nhật cả minor và patch, còn dấu tilde chỉ cho phép patch

npm dùng hai tiền tố phổ biến: Tilde (~) và Caret (^). Dấu Tilde rất thận trọng, chỉ cho phép cập nhật các bản Patch. Ví dụ, ~1.2.3 chấp nhận phiên bản từ 1.2.3 đến dưới 1.3.0. Dấu Caret linh hoạt hơn và là mặc định của npm hiện nay, cho phép cập nhật cả Minor lẫn Patch nhưng tuyệt đối không chạm vào Major: ^1.2.3 cho phép cập nhật đến dưới 2.0.0. Nó dựa trên giả định rằng tác giả thư viện tuân thủ nghiêm ngặt việc không đưa breaking changes vào bản Minor.

Một khái niệm cần nắm là "Major Zero" (v0.x.x). Theo đặc tả Semver, phiên bản 0 dành cho giai đoạn phát triển ban đầu và API được coi là chưa ổn định. Trong trường hợp này, npm đối xử với dấu Caret giống hệt Tilde, nghĩa là ^0.1.2 chỉ cho phép cập nhật lên đến dưới 0.2.0. Cơ chế này bảo vệ ứng dụng khỏi những thay đổi API đột ngột trong giai đoạn thư viện chưa trưởng thành. Khoảng 82% các gói trên npm Registry vẫn đang ở phiên bản 0.x.x, cho thấy sự do dự của cộng đồng trong việc cam kết ổn định API ở mức 1.0.0.

Kỹ sư kỳ cựu thường khuyến nghị đưa dự án lên phiên bản 1.0.0 ngay khi mã nguồn đã chạy thật hoặc API đã ổn định. Kẹt lại ở "Magic Zero" gây khó cho việc quản lý phụ thuộc tự động và tạo tâm lý không an tâm cho người dùng. Khi API đã định hình, nhảy qua rào cản 1.0.0 là cách tốt nhất để dùng Semver như một công cụ truyền đạt ý định kỹ thuật.

npm install làm gì với thư mục node_modules?

Khi bạn chạy npm install, CLI thực hiện một thuật toán phân giải cây phụ thuộc. Nó đọc package.json, so sánh với package-lock.json nếu có, rồi tải các tarball từ Registry về, giải nén và đặt vào thư mục node_modules. Quy trình này xử lý không chỉ các phụ thuộc trực tiếp mà cả phụ thuộc gián tiếp — phụ thuộc của phụ thuộc — tạo nên một cấu trúc cây khổng lồ.

npm v2 lồng các phụ thuộc nhiều tầng, còn npm v3 nâng chúng lên thành cấu trúc phẳng

Ở các phiên bản cũ, từ npm v2 trở về trước, cấu trúc node_modules được xây theo kiểu lồng nhau, dẫn đến trùng lặp các gói giống nhau ở nhiều cấp và vượt quá giới hạn đường dẫn trên Windows. Từ npm v3, thuật toán hoisting được áp dụng: npm cố gắng "nâng" các phụ thuộc dùng chung lên cấp cao nhất của node_modules. Cấu trúc thư mục nhờ đó phẳng hơn, tiết kiệm không gian đĩa và tránh tải lặp cùng một thư viện.

Nếu hai thư viện yêu cầu hai phiên bản không tương thích của cùng một gói — thư viện A cần Lodash v3, thư viện B cần Lodash v4 — npm thực hiện deduplication (khử trùng lặp). Phiên bản được dùng nhiều nhất được đưa ra ngoài cùng, còn phiên bản kia được giữ lồng bên trong thư mục node_modules của thư viện yêu cầu nó. Đây là cơ chế giúp npm giải quyết "dependency hell" — địa ngục phụ thuộc — mà vẫn đảm bảo tính đúng đắn lúc chạy.

Thư mục node_modules phình to rất nhanh và có thể chứa hàng chục nghìn tập tin nhỏ. Đó là lý do kỹ sư luôn đưa thư mục này vào .gitignore. Quản lý hàng trăm MB build artifacts trong hệ thống quản lý phiên bản là không hiệu quả; thay vào đó, bạn dựa vào tính nhất quán của package-lock.json để tái tạo lại đúng thư mục này trên bất kỳ môi trường nào.

package-lock.json và npm ci: vì sao cài lại luôn giống nhau

package-lock.json được tạo ra hoặc cập nhật tự động mỗi khi bạn cài một gói mới. Nó ghi lại trạng thái chính xác của toàn bộ cây phụ thuộc tại thời điểm đó. Trong khi package.json dùng các dải phiên bản linh hoạt như ^, tệp lock lưu phiên bản cụ thể kèm địa chỉ tải về và mã băm toàn vẹn (integrity hash). Chạy cài đặt trên 10 máy khác nhau sẽ cho ra kết quả giống hệt nhau.

npm install có thể cập nhật lockfile, còn npm ci xoá node_modules và cài đúng theo lockfile

Trường integrity trong tệp lock dùng chuẩn Subresource Integrity với mã băm SHA-512. Đây là một chốt chặn bảo mật: nếu kẻ tấn công thay đổi nội dung một tarball nhưng giữ nguyên số phiên bản, npm phát hiện sai khác về mã băm và từ chối cài đặt. Tệp lock cũng ghi lại quan hệ logic giữa các gói, giúp npm bỏ qua bước tính toán phân giải cây phụ thuộc và tăng tốc độ cài đặt.

Khác biệt giữa npm installnpm ci là kiến thức bắt buộc cho môi trường chuyên nghiệp. npm install có quyền cập nhật tệp lock nếu thấy phiên bản mới thoả mãn điều kiện trong package.json. Ngược lại, npm ci (Clean Install) là lệnh nghiêm khắc dành cho CI/CD: nó xoá sạch thư mục node_modules hiện tại, đọc trực tiếp từ tệp lock, và báo lỗi ngay nếu tệp lock không khớp với package.json. Dùng npm ci loại bỏ hoàn toàn các thay đổi ngoài ý muốn khi triển khai.

Cấu trúc trường integrity bảo vệ tính toàn vẹn trong package-lock.json trông như sau:

json
"node_modules/lodash": {
  "version": "4.17.21",
  "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
  "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
}

npm scripts: chạy tác vụ dự án bằng một câu lệnh

Trường scripts trong package.json cho phép bạn định nghĩa các bí danh cho những câu lệnh shell phức tạp. Thay vì bắt mọi thành viên trong nhóm nhớ các tham số dài dòng của Webpack, Docker hay bộ test, bạn cấu hình chúng một lần rồi chạy bằng npm run <task-name>. Cả đội ngũ phát triển dùng chung một quy trình.

Một cơ chế quan trọng của npm scripts là nó tự động thêm thư mục node_modules/.bin vào biến PATH khi thực thi. Điều này cho phép bạn gọi trực tiếp các công cụ dòng lệnh của thư viện — tsc, eslint, prettier — mà không cần cài chúng toàn cục trên máy. Nó giải quyết triệt để xung đột phiên bản công cụ giữa các dự án khác nhau trên cùng một máy tính.

npm cũng cung cấp các script vòng đời (lifecycle scripts) như preinstall, postinstall, pretest, posttest. Bạn chạy trực tiếp npm test hoặc npm start mà không cần từ khoá run. Các script prepost tự động chạy trước và sau lệnh chính, cho phép bạn thiết lập môi trường, dọn dẹp thư mục build hoặc chạy các bước kiểm tra một cách nhất quán.

Ví dụ về các script tự động hoá tác vụ build và test:

json
{
  "scripts": {
    "clean": "rm -rf dist",
    "prebuild": "npm run clean",
    "build": "webpack --mode production --config webpack.config.js",
    "test": "jest --coverage --verbose",
    "lint": "eslint src/**/*.ts"
  }
}

Install script và chuỗi cung ứng: npm v12 đổi mặc định

Các script vòng đời như preinstall mang lại sự tiện lợi, nhưng chúng cũng là kẽ hở lớn nhất cho các cuộc tấn công chuỗi cung ứng. Những mã độc như sâu Shai-Hulud hay Trojan Miasma RAT lợi dụng cơ chế này để thực thi mã độc ngay khi người dùng gõ npm install. Kẻ tấn công chèn các đoạn mã đánh cắp token npm, GitHub Personal Access Token hoặc cấu hình cloud ngay trong quá trình cài thư viện.

npm v12 chuyển ba hành vi cài đặt sang chế độ phải bật thủ công

Các chiến dịch hiện đại đã đạt mức độ tinh vi cao. Mã độc có thể đọc trực tiếp bộ nhớ của tiến trình runner qua /proc/<Runner.Worker>/mem trong môi trường GitHub Actions để rút OIDC token, rồi gửi thẳng về registry. Chúng còn dùng kỹ thuật đầu độc cache, ghi đè lên các bản lưu tạm của pnpm hoặc npm để chèn mã độc vào những lần build sau của nhà bảo trì dự án.

Hạ tầng điều khiển của loại mã độc này rất kiên cố với nhiều lớp dự phòng. Nếu máy chủ chính bị chặn, chúng truy vấn các cổng Ethereum RPC để tìm địa chỉ mới ghi trên smart contract, hoặc dùng relay của giao thức Nostr và mạng BitTorrent DHT để duy trì liên lạc. Một biến thể còn cài dịch vụ chạy ngầm kiểm tra token GitHub đã đánh cắp mỗi 60 giây; nếu phát hiện token bị thu hồi, nó thực thi rm -rf ~/ để xoá sạch thư mục home của nạn nhân.

npm v12 trả lời rủi ro này bằng cách đổi chính các mặc định. Ba hành vi trước đây tự chạy trong npm install giờ phải bật thủ công: allowScripts mặc định tắt, nên các script vòng đời preinstall, install, postinstall và bước build node-gyp ngầm không còn tự chạy; --allow-git mặc định là none, nên phụ thuộc lấy từ Git không còn được phân giải; và --allow-remote mặc định là none, nên phụ thuộc tải từ URL từ xa cũng vậy. Để duyệt những script bạn thực sự tin tưởng, chạy npm approve-scripts --allow-scripts-pending rồi commit danh sách cho phép sinh ra trong package.json. Cả ba hành vi này đã có sẵn kèm cảnh báo từ npm 11.16.0, nên bạn kịp chuẩn bị trước khi nâng cấp.

npm, Yarn hay pnpm: khác nhau ở đâu?

npm là tiêu chuẩn, còn Yarn và pnpm ra đời để giải quyết các bài toán cụ thể về hiệu năng và quản lý đĩa. Khác biệt lớn nhất nằm ở cách tổ chức node_modules. npm dùng cấu trúc phẳng thông qua hoisting, và điều đó dẫn tới vấn đề phụ thuộc ma (phantom dependencies): một thư viện có thể truy cập các gói mà nó không hề khai báo trong package.json, chỉ vì các gói đó được nâng lên cấp cha. Hệ quả là những lỗi rất khó hiểu khi cấu trúc cây phụ thuộc thay đổi.

npm và Yarn dùng node_modules phẳng, còn pnpm liên kết từ một kho lưu trữ chung

pnpm giải quyết triệt để vấn đề này bằng liên kết cứng (hard link) và liên kết tượng trưng (symlink). pnpm chỉ lưu duy nhất một bản sao của mỗi phiên bản gói trong một kho lưu trữ trung tâm trên đĩa, rồi tạo symlink trỏ về kho đó trong thư mục dự án. Cách tiếp cận này tiết kiệm dung lượng đĩa rất lớn và chặn hoàn toàn phụ thuộc ma, vì mã nguồn chỉ nhìn thấy đúng những gì nó đã khai báo.

Về tệp khoá phiên bản, mỗi trình quản lý có định dạng riêng: npm dùng package-lock.json, Yarn dùng yarn.lock với định dạng tuỳ chỉnh, còn pnpm dùng pnpm-lock.yaml. Định dạng khác nhau nhưng mục tiêu cuối cùng vẫn là đảm bảo tính nhất quán của cây phụ thuộc. Tốc độ cũng là một điểm khác biệt: pnpm thường dẫn đầu nhờ khả năng cài song song và cơ chế liên kết tệp hiệu quả hơn việc sao chép tarball của npm.

Một nguyên tắc quan trọng: tuyệt đối không trộn lẫn các trình quản lý gói trong cùng một dự án. Chạy cả npm install lẫn yarn install sẽ tạo ra các tệp lock xung đột, dẫn đến môi trường không nhất quán và gây khó cho việc gỡ lỗi. Hãy chọn một công cụ phù hợp với hạ tầng của đội — pnpm cho các dự án monorepo lớn, hoặc npm khi bạn cần tương thích cao nhất với các công cụ CI/CD mặc định.

Nên bắt đầu với npm từ đâu?

Bắt đầu một dự án chuẩn mực bằng npm init. Trong vận hành thực tế, hãy ưu tiên npm ci cho tất cả các pipeline tự động để đảm bảo mọi lần cài đặt đều giống nhau, và luôn commit package-lock.json vào Git để cả đội dùng chung một cây phụ thuộc.

npm vẫn là lựa chọn an toàn và phổ biến nhất nếu bạn hiểu rõ ba thứ: cơ chế khoá phiên bản, mã băm toàn vẹn, và những gì npm install được phép tự chạy trên máy bạn. Chạy npm audit định kỳ để vá các lỗ hổng đã biết trong chuỗi cung ứng.

Tài liệu tham khảo

Chia sẻ bài viết

X / TwitterFacebookLinkedIn