DrillLab
第 01 / 09 节LESSON 01 / 09约 12 分钟~12 min

Node.js、npm、node_modules 和 lockfileNode.js, npm, node_modules and the lockfile

为什么装个 React 项目会多出几万个文件,以及为什么那个 lock 文件不能随便删。Why installing a React project adds tens of thousands of files, and why you must not delete that lock file.

2 个练习2 exercises地基 · 第 1 部分Foundations · Part 1
这一页有什么On this page6
学完这节你会After this lesson you can
  • 说清 Node.js 和浏览器里的 JavaScript 是什么关系Explain how Node.js relates to the JavaScript that runs in a browser
  • 知道 npm install 到底做了什么,node_modules 从哪来Know what npm install actually does, and where node_modules comes from
  • 知道 lockfile 是什么、为什么不能随便删或换成别的包管理器Know what a lockfile is, and why you should not delete it or switch to another package manager
  • 知道 dependencies 和 devDependencies 的区别在哪里体现Know where the difference between dependencies and devDependencies shows up
这在考试里考什么What the exam does with this

两个 assessment 的第一步都是 npm install。装不上、装错版本、或者手滑生成了第二个 lockfile,后面全都跑不起来 —— 这时候不是你 React 写得不好,是根本没进考场。The first step of both exams is npm install. If it fails, installs the wrong versions, or accidentally creates a second lockfile, nothing after it will run. The problem then is not your React code. You have not started the exam at all.

这节课要看的真实文件Real files this lesson looks at3 项 · 3 个可以展开看原文3 items · 3 can be opened
react-notes-app/package.jsonReact 考试的依赖清单The dependency list for the React exam
JSONpackage.json源项目From source
1{
2 "name": "react-notes-app",
3 "private": true,
4 "version": "1.0.0",
5 "type": "module",
6 "scripts": {
7 "dev": "vite",
8 "build": "tsc && vite build",
9 "q2": "tsx q2/demo.ts"
10 },
11 "dependencies": {
12 "react": "^18.3.1",
13 "react-dom": "^18.3.1"
14 },
15 "devDependencies": {
16 "@testing-library/jest-dom": "^7.0.0",
17 "@testing-library/react": "^16.3.2",
18 "@testing-library/user-event": "^14.6.1",
19 "@types/react": "^18.3.3",
20 "@types/react-dom": "^18.3.0",
21 "@vitejs/plugin-react": "^4.3.1",
22 "jsdom": "^29.1.1",
23 "tsx": "^4.16.2",
24 "typescript": "^5.5.3",
25 "vite": "^5.4.0",
26 "vitest": "^4.1.10"
27 }
28}
Source: react-notes-app/package.json
react-notes-app/package-lock.json锁定确切版本(139 KB)Pins the exact versions (139 KB)
JSONpackage-lock.json源项目From source
1{
2 "name": "react-notes-app",
3 "version": "1.0.0",
4 "lockfileVersion": 3,
5 "requires": true,
6 "packages": {
7 "": {
8 "name": "react-notes-app",
9 "version": "1.0.0",
10 "dependencies": {
11 "react": "^18.3.1",
12 "react-dom": "^18.3.1"
13 },
14 "devDependencies": {
15 "@testing-library/jest-dom": "^7.0.0",
16 "@testing-library/react": "^16.3.2",
17 "@testing-library/user-event": "^14.6.1",
18 "@types/react": "^18.3.3",
19 "@types/react-dom": "^18.3.0",
20 "@vitejs/plugin-react": "^4.3.1",
21 "jsdom": "^29.1.1",
22 "tsx": "^4.16.2",
23 "typescript": "^5.5.3",
24 "vite": "^5.4.0",
25 "vitest": "^4.1.10"
26 }
27 },
28 "node_modules/@adobe/css-tools": {
29 "version": "4.5.0",
30 "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz",
31 "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==",
32 "dev": true,
33 "license": "MIT"
34 },
35 "node_modules/@asamuzakjp/css-color": {
36 "version": "5.1.11",
37 "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz",
38 "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==",
39 "dev": true,
40 "license": "MIT",
41 "dependencies": {
42 "@asamuzakjp/generational-cache": "^1.0.1",
43 "@csstools/css-calc": "^3.2.0",
44 "@csstools/css-color-parser": "^4.1.0",
45 "@csstools/css-parser-algorithms": "^4.0.0",
46 "@csstools/css-tokenizer": "^4.0.0"
47 },
48 "engines": {
49 "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
50 }
51 },
52 "node_modules/@asamuzakjp/dom-selector": {
53 "version": "7.1.1",
54 "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz",
55 "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==",
56 "dev": true,
57 "license": "MIT",
58 "dependencies": {
59 "@asamuzakjp/generational-cache": "^1.0.1",
60 "@asamuzakjp/nwsapi": "^2.3.9",
61 "bidi-js": "^1.0.3",
62 "css-tree": "^3.2.1",
63 "is-potential-custom-element-name": "^1.0.1"
64 },
65 "engines": {
66 "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
67 }
68 },
69 "node_modules/@asamuzakjp/generational-cache": {
70 "version": "1.0.1",
71 "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz",
72 "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==",
73 "dev": true,
74 "license": "MIT",
75 "engines": {
76 "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
77 }
78 },
79 "node_modules/@asamuzakjp/nwsapi": {
80 "version": "2.3.9",
81 "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
82 "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
83 "dev": true,
84 "license": "MIT"
85 },
86 "node_modules/@babel/code-frame": {
87 "version": "7.29.7",
88 "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
89 "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
90 "dev": true,
91 "license": "MIT",
92 "dependencies": {
93 "@babel/helper-validator-identifier": "^7.29.7",
94 "js-tokens": "^4.0.0",
95 "picocolors": "^1.1.1"
96 },
97 "engines": {
98 "node": ">=6.9.0"
99 }
100 },
101 "node_modules/@babel/compat-data": {
102 "version": "7.29.7",
103 "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
104 "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
105 "dev": true,
106 "license": "MIT",
107 "engines": {
108 "node": ">=6.9.0"
109 }
110 },
111 "node_modules/@babel/core": {
112 "version": "7.29.7",
113 "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
114 "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
115 "dev": true,
116 "license": "MIT",
117 "dependencies": {
118 "@babel/code-frame": "^7.29.7",
119 "@babel/generator": "^7.29.7",
120 "@babel/helper-compilation-targets": "^7.29.7",
Source: react-notes-app/package-lock.json

只显示前 120 行,整个文件共 4181 行 —— 其余在本机打开看。First 120 of 4181 lines — open the file locally for the rest.

graphql-federation-practice/node-subgraph/package.jsonsubgraph 的依赖清单The dependency list for the subgraph
JSONpackage.json源项目From source
1{
2 "name": "order-subgraph",
3 "version": "1.0.0",
4 "description": "GraphQL Federation Subgraph for Order Management",
5 "main": "src/index.js",
6 "type": "module",
7 "scripts": {
8 "start": "node src/index.js",
9 "test": "NODE_OPTIONS=--experimental-vm-modules jest",
10 "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch"
11 },
12 "dependencies": {
13 "@apollo/server": "^4.10.0",
14 "@apollo/subgraph": "^2.7.0",
15 "graphql": "^16.8.1",
16 "graphql-tag": "^2.12.6",
17 "dataloader": "^2.2.2"
18 },
19 "devDependencies": {
20 "jest": "^29.7.0",
21 "@jest/globals": "^29.7.0"
22 },
23 "jest": {
24 "testEnvironment": "node",
25 "transform": {},
26 "testMatch": ["**/__tests__/**/*.test.js"]
27 }
28}
Source: graphql-federation-practice/node-subgraph/package.json
§01

Node.js:让 JavaScript 离开浏览器Node.js: running JavaScript outside the browser

JavaScript 最早只能在网页里跑。Node.js 把它搬到了你的终端里。At first JavaScript could only run inside a web page. Node.js lets it run in your terminal.

你在浏览器控制台里写 alert("hi"),能弹窗,是因为浏览器给 JavaScript 提供了 windowdocument 这些东西。 JavaScript 本身并不知道什么叫「网页」。

Node.js 做的事情是:把浏览器里的那台 JavaScript 引擎(V8)单独拿出来, 再配上「读文件」「起服务器」「读环境变量」这类本机能力。于是 JavaScript 可以在终端里直接运行:

这就是为什么两个 assessment 都需要 Node —— React 项目要靠 Node 跑构建工具 (Vite)和测试(Vitest);GraphQL subgraph 本身就是一个跑在 Node 上的服务器。

When you type alert("hi") in the browser console and a box pops up, that is the browser handing JavaScript things like window and document. JavaScript itself has no idea what a “web page” even is.

What Node.js does is take the JavaScript engine out of the browser (V8) on its own, then bolt on machine-level abilities: read a file, start a server, read an environment variable. Now JavaScript runs straight from your terminal:

That is why both assessments need Node — the React project leans on Node to run its build tool (Vite) and its tests (Vitest); the GraphQL subgraph is itself a server running on Node.

Terminal已跑通Verified
1node -v
2# v22.21.1 ← 本机实测版本
3
4node -e "console.log(1 + 1)"
5# 2
1node -v
2# v22.21.1 ← the version measured on this machine
3
4node -e "console.log(1 + 1)"
5# 2
npm 一般跟着 Node.js 一起装,所以装完 Node 就有 npm 了。npm is normally installed together with Node.js, so once Node is installed you already have npm.
§02

npm:替你去把别人写好的代码搬回来npm: it fetches the code other people already wrote

npm 是 package manager(包管理器)。它管的是「这个项目需要哪些别人写的代码」。npm is a package manager. It keeps track of which code written by other people this project needs.

React 是别人写的。Vite 是别人写的。Apollo Server 是别人写的。 你不需要自己实现它们,只需要在 package.json 里声明「我要用这些」, 然后让 npm 去下载。每一个这样的第三方包,叫一个依赖(dependency)

npm install 做三件事:读 package.json 里的依赖清单 → 把这些包(以及这些包自己的依赖,再以及那些包的依赖……)全部下载下来 → 统统摊在 node_modules/ 这个文件夹里。

所以 node_modules 动辄几万个文件是正常的。它是下载产物, 不是你的代码 —— 这也是为什么它几乎永远出现在 .gitignore 里: 别人拿到你的 package.json,自己 npm install就能装出一份一样的,不需要你把它传上去。

React was written by other people. So was Vite. So was Apollo Server. You do not have to build any of them — you declare “I want these” in package.json and let npm go download them. Every third-party package like that is called a dependency.

npm install does three things: read the dependency list in package.json → download all of those packages (plus their own dependencies, plus those packages’ dependencies...) → and spread the lot into a folder called node_modules/.

So tens of thousands of files in node_modules is normal. It is a download artifact, not your code — which is also why it shows up in .gitignore nearly every time: hand someone your package.json, they run npm install, and they get the same thing. No need to ship it.

Terminal已跑通Verified
1cd graphql-federation-practice/node-subgraph
2npm install
3# added 424 packages in 11s ← 本机实测输出
1cd graphql-federation-practice/node-subgraph
2npm install
3# added 424 packages in 11s ← measured on this machine
5 个直接依赖(@apollo/server、@apollo/subgraph、graphql、graphql-tag、dataloader)+ 2 个开发依赖,最后装出 424 个包 —— 中间那些都是依赖的依赖。5 direct dependencies (@apollo/server, @apollo/subgraph, graphql, graphql-tag, dataloader) plus 2 dev dependencies end up installing 424 packages. Everything in between is a dependency of a dependency.
§03

lockfile:把「大概哪个版本」钉成「就是这个版本」The lockfile: it turns a version range into one exact version

package.json 写的是范围,lockfile 记的是事实。package.json states a range. The lockfile records what was actually installed.

react-notes-app/package.json 里的这行:"react": "^18.3.1"。 那个 ^ 的意思是「18.3.1 或者更新的 18.x 都行」。 今天装是 18.3.1,半年后装可能变成 18.3.9。

这对考试是灾难:同一份代码,你机器上跑得过,判卷机器上因为版本不同挂了。 所以 npm 在第一次安装时会生成 lockfilepackage-lock.json),把「实际装的到底是哪个版本、从哪下的、 校验和是多少」一条条记下来。下一次 npm install, 只要 lockfile 在,就照它装,不再重新解析版本范围。

由此得出三条实操规矩:

  • 别删 lockfile。删了就等于放弃版本锁定。
  • 别混用包管理器。项目里已经有 package-lock.json(npm 的),就不要再跑 pnpm installyarn —— 那会生成第二个 lockfile,两份互相矛盾的事实。
  • 装不上就先看错误,别先删 node_modules。「删了重装」偶尔有用,但它会掩盖真正的问题。

Look at this line in react-notes-app/package.json: "react": "^18.3.1". That ^ means “18.3.1, or any newer 18.x, is fine”. Install today and you get 18.3.1; install in six months and you might get 18.3.9.

For an exam that is a disaster: the same code passes on your machine and fails on the grading machine because a version differs. So on the first install npm writes a lockfile (package-lock.json) that records, line by line, which version actually landed, where it was downloaded from, and its checksum. The next npm install follows the lockfile as long as it is there, and never resolves the version ranges again.

Three practical rules fall out of that:

  • Do not delete the lockfile. Deleting it means giving up version pinning.
  • Do not mix package managers. If the project already has package-lock.json (npm’s), do not go run pnpm install or yarn — that writes a second lockfile, and now you have two contradicting sets of facts.
  • When install fails, read the error before deleting node_modules. Delete-and-reinstall helps once in a while, but it buries the real problem.
§04

dependencies 和 devDependencies 差在哪Where dependencies and devDependencies differ

区别只有一句话:产品跑起来之后还需要的,放 dependencies; 只在开发和构建时需要的,放 devDependencies。

react-notes-app 举例。reactreact-dom在 dependencies 里 —— 用户打开页面时,这些代码要在浏览器里跑。 而 vitetypescriptvitest@testing-library/react 全在 devDependencies 里 —— 它们负责把代码打包、检查类型、跑测试,打包完成后就没它们的事了。

@types/react 这种 @types/ 开头的包也在 devDependencies:它们只包含类型信息,给 TypeScript 编译器看, 编译完就消失,一行都不会进到浏览器里。

One sentence covers it: whatever the product still needs once it is running goes in dependencies; whatever is needed only while developing and building goes in devDependencies.

Take react-notes-app. react and react-dom sit in dependencies — that code has to run in the browser when a user opens the page. But vite, typescript, vitest and @testing-library/react all sit in devDependencies — they bundle the code, check the types and run the tests, and once the bundle is out the door they have no further job.

Packages that start with @types/, such as @types/react, live in devDependencies too: they carry nothing but type information for the TypeScript compiler, they vanish when it compiles, and not one line reaches the browser.

练习Practice

动手做Get your hands on it

填空只是过渡。真正掌握的标准,是在没有答案的时候从头写出来 —— 所以做完 L2 之后一定要往 L3、L4 走。Filling blanks is a stepping stone. The real bar is writing it from nothing, so once L2 is comfortable, push on to L3 and L4.

L1认出来Spot it这个包该放哪边?Which side does this package go on?

下面是 react-notes-app 真实的依赖之一。它在真实的package.json 里被放在哪个字段下?

Below is one of the real dependencies of react-notes-app. Which field of the real package.json is it under?

JSON示意Illustrative
1"vitest": "^4.1.10"
先选一个选项Pick an option first
L1认出来Spot itlockfile 该怎么对待How to treat the lockfile

项目里已经有 package-lock.json,你要安装依赖。下面哪些做法是对的?(多选)

The project already has a package-lock.json and you need to install the dependencies. Which of these are correct? (more than one)

这题是多选。More than one answer is correct.

先选一个选项Pick an option first
迁移Transfer

换一道题也能用Works on other problems too

考试不会原题重考。真正能带走的是「看到这种信号 → 伸手去拿这个解法」。The exam will not reuse the same question. What you take away is the reflex: see this signal, reach for that solution.

拿到一个新项目,不知道从哪开始A new project, and you do not know where to start
先看 package.json,再 npm installRead package.json first, then run npm install
「我这里跑得过,他那里跑不过」It runs on my machine but not on theirs
先比 Node 版本和 lockfileCompare the Node version and the lockfile first
看到 node_modules 很大node_modules is very large
正常,它是下载产物,不进版本库Normal. It is downloaded output, and is not committed
这节的要点What to take away
  1. Node.js = 能在终端里跑 JavaScript 的运行时;npm 一般随它一起装。Node.js is the runtime that runs JavaScript in a terminal. npm is usually installed with it.
  2. npm install 读 package.json,把依赖(以及依赖的依赖)下载到 node_modules。npm install reads package.json and downloads the dependencies, and their dependencies, into node_modules.
  3. package.json 里的 ^18.3.1 是范围,lockfile 才是「实际装了哪个版本」的事实。In package.json, ^18.3.1 is a range. Only the lockfile records which version was actually installed.
  4. 别删 lockfile,别在有 package-lock.json 的项目里跑 pnpm/yarn。Do not delete the lockfile, and do not run pnpm or yarn in a project that has a package-lock.json.
  5. dependencies = 产品运行时要用;devDependencies = 只在开发/构建/测试时用。dependencies are needed while the product runs. devDependencies are needed only for development, building and testing.

接下来What next

  1. 把这一节的练习做掉Do this lesson’s exercises2 个,就在这一页上面 —— 别攒着最后一起做2 of them, further up this page — do not save them for later
    回到练习 ↑Back up to them ↑
  2. 接着看下一节Continue to the next lessonpackage.json 逐字段读一遍package.json, field by field
    下一节Next lesson
读完并且做过上面的练习了吗?Read it and worked through the exercises above?