工具与代码质量
好工具 + 好习惯 = 高质量代码
🎯 学习目标
学完本章,你将能够:
- 能够使用 npm 命令管理项目依赖(install、uninstall、update、audit)
- 能够理解 package.json 的结构与语义化版本规则(^、~、*)
- 能够配置和使用 ESLint 进行代码风格检查与自动修复
- 能够配置 Prettier 实现代码自动格式化并与 ESLint 协同工作
- 能够使用 Vitest 编写基础单元测试并理解测试金字塔概念
你有没有想过
为什么大公司的代码风格统一?为什么改了一处另一处不会偷偷坏掉?为什么有人敢放心重构几千行代码?
答案不是"他们技术好",而是他们用了一套工具链:包管理器管依赖、ESLint 检查代码风格、Prettier 自动格式化、单元测试保证逻辑正确。
这些工具不是"锦上添花",而是专业开发的基础设施。这一章你将学会搭建这套工具链。
Part 1: npm 包管理
学会用它,才能用好别人写的轮子
项目里要装一个 Express、一个测试框架、一个代码检查工具——这些从哪来?全靠 npm 下载和管理。npm 是 Node.js 世界的"应用商店",装包、卸包、锁版本全靠它。接下来你会掌握 npm 的常用命令、package.json 的结构,以及版本号那个奇怪的 ^ 到底在干什么。
npm 常用命令
init、install、run——这三个能覆盖 90% 的日常操作
$ npm init -y # 初始化 package.json
$ npm install express # 安装生产依赖
$ npm install -D vitest # 安装开发依赖
$ npm uninstall package # 卸载
$ npm update # 更新所有依赖
$ npm outdated # 检查过时的包
$ npm audit # 安全审计
$ npm run build # 运行 scripts 中的命令
dependencies 是运行时需要的包(如 express),devDependencies 是开发时才需要的包(如测试框架)。生产部署时可以用 npm install --production 跳过开发依赖。
package.json
项目的身份证:名字、版本、依赖、脚本都在这里
{
"name": "my-app",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"test": "vitest"
},
"dependencies": {
"express": "^4.18.0"
},
"devDependencies": {
"vitest": "^1.0.0"
},
"engines": {
"node": ">=18"
}
}
语义化版本
^ 和 ~ 控制更新范围
| 写法 | 含义 | 示例 |
|---|---|---|
^1.2.3 | 兼容小版本更新 | 允许 1.x.x,不允许 2.0.0 |
~1.2.3 | 仅补丁更新 | 允许 1.2.x,不允许 1.3.0 |
1.2.3 | 精确锁定 | 只用 1.2.3 |
版本号格式:主版本.次版本.补丁(major.minor.patch)。主版本变意味着有破坏性改动。
npm vs yarn vs pnpm:pnpm 速度快、磁盘占用少、有严格的依赖隔离。新项目推荐 pnpm。
Part 2: ESLint 与 Prettier
两者配合,代码风格全团队一致
依赖装好了,代码能跑了,但新问题来了:张三用双引号、李四用单引号、王五缩进 2 格、你缩进 4 格。代码评审时光为格式吵半天。ESLint 负责找逻辑问题,Prettier 负责统一排版格式。这一节我们把这两个工具配上,让代码风格再也不是争论的焦点。
ESLint 配置
定义规则,告诉团队什么写法可以、什么不行
$ npm install -D eslint @eslint/js
$ npx eslint --init
// eslint.config.js(Flat Config,ESLint 9+)
import js from '@eslint/js';
export default [
js.configs.recommended,
{
rules: {
'no-unused-vars': 'warn', // 未使用变量警告
'no-console': 'warn', // console 警告
'eqeqeq': 'error', // 强制 ===
'curly': 'error', // 强制花括号
'prefer-const': 'error', // 优先用 const
'no-var': 'error', // 禁止 var
}
}
];
ESLint 使用
检查和自动修复,一条命令搞定
$ npx eslint src/ # 检查所有文件
$ npx eslint src/ --fix # 自动修复能修的问题
规则等级三种:off 关闭、warn 警告、error 报错。CI 中建议全设为 error,有警告就不通过。
Prettier 格式化
不需要争论缩进是 2 还是 4——Prettier 统一决定
$ npm install -D prettier
// .prettierrc
{
"semi": true,
"singleQuote": true,
"tabWidth": 4,
"trailingComma": "es5",
"printWidth": 100
}
$ npx prettier --write "src/**/*.js" # 格式化
$ npx prettier --check "src/**/*.js" # 只检查不修改
ESLint + Prettier
一个管逻辑,一个管格式,互不冲突
$ npm install -D eslint-config-prettier eslint-plugin-prettier
ESLint 和 Prettier 有些规则会冲突(比如分号、引号)。eslint-config-prettier 会关闭 ESLint 中所有和 Prettier 冲突的规则。
推荐工作流:保存时 Prettier 自动格式化 + ESLint 实时检查。在 VS Code 中安装对应扩展即可。
Part 3: JSDoc 类型注释
不用 TypeScript,也能获得类型提示和文档
ESLint 和 Prettier 搞定了代码风格,但还有一个痛点:调用函数时不知道该传什么参数,IDE 也没法提示。暂时不想上 TypeScript?JSDoc 是一个轻量级的替代方案。用注释给函数签名,编辑器就能帮你检查参数类型、生成文档。这一节看看它能做什么,又有哪些局限性。
JSDoc 注释
用注释给函数签名,编辑器就能提示类型
/**
* 计算两个数的和
* @param {number} a - 第一个数
* @param {number} b - 第二个数
* @returns {number} 两数之和
*/
function add(a, b) { return a + b; }
// 自定义类型
/**
* @typedef {Object} User
* @property {string} name - 用户名
* @property {number} age - 年龄
* @property {string} [email] - 可选邮箱
*/
/**
* @param {User} user
* @returns {Promise<boolean>}
*/
async function saveUser(user) { /* ... */ }
VS Code 原生支持 JSDoc。写了注释后,调用函数时会自动显示参数类型和说明。
JSDoc 进阶
泛型、回调、数组类型
// 数组类型
/** @type {string[]} */
let names = [];
// 泛型
/**
* @template T
* @param {T[]} arr
* @returns {T|undefined}
*/
function first(arr) { return arr[0]; }
// 回调类型
/**
* @callback FilterFn
* @param {string} item
* @param {number} index
* @returns {boolean}
*/
/** @param {FilterFn} fn */
function filter(fn) { /* ... */ }
Part 4: 单元测试
代码质量的"安全网"——改了代码跑一下测试,马上知道有没有搞坏什么
工具链的最后一块拼图:怎么确保改了一处代码,不会悄悄搞坏另一处?答案是写测试。单元测试就像给每个函数配了一个"质检员",输入什么、期望输出什么,全都写清楚。这一节用 Vitest 写几个真实的测试用例,体会"先写测试、再改代码"的安全感。
测试基础概念
describe 分组、it 写用例、expect 断言
import { describe, it, expect, beforeEach } from 'vitest';
import { add, multiply } from './math.js';
describe('Math utilities', () => {
beforeEach(() => {
// 每个测试前执行(如重置状态)
});
it('should add two numbers', () => {
expect(add(2, 3)).toBe(5);
expect(add(-1, 1)).toBe(0);
});
it('should multiply', () => {
expect(multiply(2, 3)).toBe(6);
});
it('should handle edge cases', () => {
expect(add(0, 0)).toBe(0);
expect(multiply(0, 5)).toBe(0);
});
});
常用断言
expect 的各种匹配方式
| 断言 | 用途 |
|---|---|
expect(value).toBe(expected) | 严格相等(===) |
expect(value).toEqual(expected) | 深度相等(对象/数组) |
expect(value).toBeTruthy() | 真值判断 |
expect(value).toBeFalsy() | 假值判断 |
expect(fn).toThrow() | 函数是否抛错 |
expect(array).toContain(item) | 数组是否包含 |
expect(fn).toHaveBeenCalled() | mock 函数是否被调用 |
Vitest 和 Jest 的 API 几乎完全一样。Vitest 更快,且原生支持 ESM。
常见问题
.gitignore 中加上 node_modules/。别人 clone 后 npm install 就能恢复依赖。
问题一:npm install 报错 ERESOLVE
原因:依赖版本冲突,两个包要求同一个依赖的不同版本。
修复:尝试 npm install --legacy-peer-deps,或者检查 package.json 中的版本范围。
问题二:ESLint 和 Prettier 规则冲突
原因:ESLint 的格式规则和 Prettier 的配置不一致。
修复:安装 eslint-config-prettier,它会关闭 ESLint 中所有和 Prettier 冲突的规则。
问题三:测试通过了但线上还是出 bug
原因:测试只覆盖了"正常路径",没有测试边界情况和错误路径。
修复:测试中专门写错误输入、空值、超大值等边界用例。
Git Hooks
提交前自动检查——不合规代码根本进不了仓库
$ npm install -D husky lint-staged
$ npx husky init
$ echo "npx lint-staged" > .husky/pre-commit
// package.json
{
"lint-staged": {
"*.js": ["eslint --fix", "prettier --write"],
"*.{json,md}": ["prettier --write"]
}
}
Husky 在 git commit 前自动触发 lint-staged,只检查暂存区文件,速度快。如果 ESLint 或 Prettier 报错,提交会被拦截。
用 commitlint 规范提交信息
$ npm install -D @commitlint/cli @commitlint/config-conventional
$ echo "export default { extends: ['@commitlint/config-conventional'] };" > commitlint.config.js
$ echo "npx --no -- commitlint --edit \$1" > .husky/commit-msg
提交信息必须符合 type(scope): message 格式,例如 feat(auth): 添加登录功能。type 只允许 feat / fix / docs / refactor 等约定值。
.husky/ 目录被提交到 Git,否则队友 clone 后钩子不生效。
代码覆盖率
量化测试的"覆盖面"——哪些代码被测到了,哪些没有
| 维度 | 含义 |
|---|---|
| 行覆盖率 | 被执行到的代码行占比 |
| 分支覆盖率 | if/else 分支被执行到的占比 |
| 函数覆盖率 | 被调用到的函数占比 |
# 运行覆盖率报告
$ npx vitest --coverage
// vitest.config.js
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
coverage: {
provider: 'v8', // 或 'istanbul'
reporter: ['text', 'html'], // 终端输出 + HTML 报告
include: ['src/**/*.js'],
thresholds: {
lines: 80, // 行覆盖率 ≥ 80%
branches: 70, // 分支覆盖率 ≥ 70%
functions: 80,
}
}
}
});
设置 thresholds 后,覆盖率不达标测试会直接失败,防止覆盖率悄悄下降。
试一试
练习一
任务:初始化一个项目,安装 ESLint,配置规则禁止 var 和要求使用 ===。
$ mkdir my-project && cd my-project
$ npm init -y
$ npm install -D eslint @eslint/js
参考答案
// eslint.config.js
import js from '@eslint/js';
export default [
js.configs.recommended,
{
rules: {
'no-var': 'error',
'eqeqeq': 'error',
}
}
];
练习二
任务:给下面这个函数写单元测试,覆盖正常情况和边界情况。
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
参考答案
import { describe, it, expect } from 'vitest';
describe('clamp', () => {
it('值在范围内不变', () => {
expect(clamp(5, 0, 10)).toBe(5);
});
it('小于最小值取最小值', () => {
expect(clamp(-3, 0, 10)).toBe(0);
});
it('大于最大值取最大值', () => {
expect(clamp(15, 0, 10)).toBe(10);
});
it('边界值', () => {
expect(clamp(0, 0, 10)).toBe(0);
expect(clamp(10, 0, 10)).toBe(10);
});
});
练习三
任务:为下面的 TodoList 类编写完整的单元测试,覆盖 add(item)、remove(id)、toggle(id)、filter(completed) 四个方法。要求覆盖正常路径和边界情况(如添加空字符串、删除不存在的 id)。
// todo.js
let nextId = 1;
export function createTodoList() {
const items = [];
return {
add(item) {
if (!item || typeof item !== 'string') throw new Error('无效的待办项');
const todo = { id: nextId++, text: item, completed: false };
items.push(todo);
return todo;
},
remove(id) {
const idx = items.findIndex(t => t.id === id);
if (idx === -1) return null;
return items.splice(idx, 1)[0];
},
toggle(id) {
const todo = items.find(t => t.id === id);
if (!todo) return null;
todo.completed = !todo.completed;
return todo;
},
filter(completed) {
return items.filter(t => t.completed === completed);
}
};
}
参考答案
import { describe, it, expect, beforeEach } from 'vitest';
import { createTodoList } from './todo.js';
describe('TodoList', () => {
let list;
beforeEach(() => { list = createTodoList(); });
describe('add', () => {
it('正常添加待办项', () => {
const todo = list.add('买牛奶');
expect(todo).toEqual({ id: 1, text: '买牛奶', completed: false });
});
it('添加空字符串应抛错', () => {
expect(() => list.add('')).toThrow('无效的待办项');
});
it('添加非字符串应抛错', () => {
expect(() => list.add(null)).toThrow('无效的待办项');
expect(() => list.add(123)).toThrow('无效的待办项');
});
});
describe('remove', () => {
it('正常删除', () => {
list.add('任务A');
const removed = list.remove(1);
expect(removed.text).toBe('任务A');
expect(list.filter(true).length + list.filter(false).length).toBe(0);
});
it('删除不存在的 id 返回 null', () => {
expect(list.remove(999)).toBe(null);
});
});
describe('toggle', () => {
it('切换完成状态', () => {
list.add('任务A');
const toggled = list.toggle(1);
expect(toggled.completed).toBe(true);
list.toggle(1);
expect(toggled.completed).toBe(false);
});
it('切换不存在的 id 返回 null', () => {
expect(list.toggle(999)).toBe(null);
});
});
describe('filter', () => {
it('按完成状态过滤', () => {
list.add('任务A');
list.add('任务B');
list.toggle(1);
expect(list.filter(true)).toHaveLength(1);
expect(list.filter(false)).toHaveLength(1);
});
it('空列表过滤返回空数组', () => {
expect(list.filter(false)).toEqual([]);
});
});
});
📝 要点总结
本章核心知识点
npm 包管理
init/install/run 核心命令,dependencies vs devDependencies
语义化版本
MAJOR.MINOR.PATCH,^ 允许小版本,~ 允许补丁版本
ESLint
静态代码分析,可修复规则,配置文件 .eslintrc
Prettier
代码格式化工具,与 ESLint 分工明确
测试与 Git Hooks
Vitest 单元测试,husky + lint-staged 提交前检查
下一章
工具链已经就绪。下一章学习设计模式初探——用经典模式解决代码中的常见问题。