- 📖 概述
- 🎯 检测目标
- 🛠️ 推荐工具
- 📦 安装和配置
- 🚀 使用方法
- 📊 报告解读
- 🛡️ JSDoc 标记
- 🔄 CI/CD 集成
- 📁 项目结构建议
- 🔧 常见问题与解决方案
- 📈 最佳实践
- 🔮 高级配置
- 📚 参考资源
📖 概述
最近接手了一个陈年老旧项目,代码库中积累了大量的孤儿函数、未使用的文件和模块。本文档提供了一套完整的死代码检测和清理方案,帮助团队维护高质量的代码库。
🎯 检测目标
- ✅ 未使用的文件和组件
- ✅ 未使用的依赖包(dependencies & devDependencies)
- ✅ 未列出的依赖(missing dependencies)
- ✅ 未使用的导出函数、类型、接口
- ✅ 重复的导出和函数
- ✅ 未使用的类成员和枚举成员
- ✅ 无法解析的 import 路径
🛠️ 推荐工具
主要工具:Knip(首选)
为什么选择 Knip?
- 🔥 活跃维护,功能最全面
- 🎯 支持 Vue 3 + TypeScript 项目
- 🔌 内置 65+插件,自动识别主流框架
- 📊 提供多种报告格式
- 🚀 支持 monorepo 和工作区
- ⚡ 性能优秀,支持大型项目
对比其他工具:
功能 | Knip | depcheck | ts-unused-exports | ESLint |
---|---|---|---|---|
未使用文件 | ✅ | ❌ | ❌ | ❌ |
未使用依赖 | ✅ | ✅ | ❌ | ❌ |
未使用导出 | ✅ | ❌ | ✅ | 部分 |
Vue 支持 | ✅ | 部分 | ❌ | ✅ |
TypeScript 支持 | ✅ | 部分 | ✅ | ✅ |
维护状态 | 活跃 | 停止维护 | 维护中 | 活跃 |
📦 安装和配置
1. 安装 Knip
# 使用npm
npm install -D knip
# 使用yarn
yarn add -D knip
# 使用pnpm
pnpm add -D knip
2. 创建配置文件
在项目根目录创建 knip.json
:
{
"$schema": "https://unpkg.com/knip@latest/schema.json",
"entry": ["src/main.ts", "src/router/index.ts", "src/store/index.ts"],
"project": ["src/**/*.{vue,ts,js,jsx,tsx}"],
"ignore": ["**/*.d.ts", "**/dist/**", "**/node_modules/**", "**/.nuxt/**", "**/coverage/**", "**/public/**"],
"ignoreDependencies": ["@types/*", "typescript"],
}
3. 高级配置选项
{
"$schema": "https://unpkg.com/knip@latest/schema.json",
"entry": ["src/main.ts"],
"project": ["src/**/*.{vue,ts,js}"],
// 生产模式配置
"production": {
"entry": ["src/main.ts!"],
"project": ["src/**/*.{vue,ts,js}!"]
},
// 忽略特定类型的问题
"rules": {
"files": "warn", // 文件问题显示为警告
"dependencies": "error", // 依赖问题显示为错误
"exports": "error", // 导出问题显示为错误
"types": "warn", // 类型问题显示为警告
"enumMembers": "off" // 关闭枚举成员检测
},
// 忽略在文件内部使用的导出
"ignoreExportsUsedInFile": true,
// 路径别名配置
"paths": {
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"],
"@utils/*": ["./src/utils/*"]
},
// 工作区配置(适用于monorepo)
"workspaces": {
".": {
"entry": ["src/main.ts"],
"project": ["src/**/*.{vue,ts,js}"]
},
"packages/*": {
"entry": ["src/index.ts"],
"project": ["src/**/*.ts"]
}
}
}
🚀 使用方法
基础命令
# 运行完整检测
npx knip
# 仅检测依赖问题
npx knip --dependencies
# 仅检测导出问题
npx knip --exports
# 生产模式检测
npx knip --production
# 严格模式(仅检查直接依赖)
npx knip --strict
# 指定报告格式
npx knip --reporter json
npx knip --reporter compact
npx knip --reporter codeowners
高级用法
# 检测单个工作区
npx knip --workspace packages/ui
# 包含入口文件的导出检测
npx knip --include-entry-exports
# 排除特定问题类型
npx knip --exclude files,dependencies
# 调试模式
npx knip --debug
# 性能分析
npx knip --performance
📊 报告解读
问题类型说明
问题类型 | 描述 | 建议处理 |
---|---|---|
files |
未被引用的文件 | 删除或确认是否需要 |
dependencies |
未使用的依赖 | 从 package.json 移除 |
devDependencies |
未使用的开发依赖 | 从 package.json 移除 |
unlisted |
缺失的依赖 | 添加到 package.json |
unresolved |
无法解析的导入 | 检查路径或安装依赖 |
exports |
未使用的导出 | 移除导出或确认用途 |
types |
未使用的类型导出 | 移除导出或确认用途 |
enumMembers |
未使用的枚举成员 | 移除或标记@public |
classMembers |
未使用的类成员 | 移除或标记@public |
duplicates |
重复导出 | 保留一个导出位置 |
示例报告
✂️ Knip found issues in 1 workspace:
Unused files (2)
src/components/UnusedComponent.vue
src/utils/oldHelper.ts
Unused dependencies (3)
lodash
moment
@types/lodash
Unlisted dependencies (1)
dayjs src/utils/dateHelper.ts:1:0
Unused exports (4)
calculateTotal src/utils/math.ts:15:0
UserInterface src/types/user.ts:8:0
formatDate src/utils/date.ts:23:0
API_ENDPOINT src/config/constants.ts:5:0
🛡️ JSDoc 标记
使用 JSDoc 标记来控制检测行为:
/**
* 公共API,不要报告为未使用
* @public
*/
export const publicApi = () => {}
/**
* 内部使用,生产模式下忽略
* @internal
*/
export const internalHelper = () => {}
/**
* 别名导出,不报告重复
* @alias
*/
export { default as Component } from './Component.vue'
/**
* Beta功能
* @beta
*/
export const betaFeature = () => {}
🔄 CI/CD 集成
1. 添加 npm 脚本
{
"scripts": {
"lint:dead-code": "knip",
"lint:dead-code:ci": "knip --reporter json",
"lint:dead-code:production": "knip --production",
"check:unused": "knip --dependencies"
}
}
2. GitHub Actions 配置
name: Dead Code Detection
on:
pull_request:
branches: [main, develop]
jobs:
detect-dead-code:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run dead code detection
run: npm run lint:dead-code:ci
3. 预提交钩子
# 安装husky和lint-staged
npm install -D husky lint-staged
# 配置package.json
{
"lint-staged": {
"src/**/*.{vue,ts,js}": [
"knip --include exports,dependencies"
]
}
}
📁 项目结构建议
推荐的目录结构
src/
├── main.ts # 入口文件
├── App.vue # 根组件
├── components/ # 公共组件
│ ├── common/ # 通用组件
│ └── business/ # 业务组件
├── views/ # 页面组件
├── router/ # 路由配置
├── store/ # 状态管理
├── composables/ # 组合式函数
├── utils/ # 工具函数
│ ├── index.ts # 统一导出
│ ├── http.ts # HTTP相关
│ ├── format.ts # 格式化函数
│ └── validation.ts # 验证函数
├── types/ # 类型定义
├── assets/ # 静态资源
└── styles/ # 样式文件
模块导出建议
// utils/index.ts - 统一导出入口
export * from './http'
export * from './format'
export * from './validation'
// 避免直接导出
export { default as HttpClient } from './http'
🔧 常见问题与解决方案
1. Vue 组件未被检测到
原因:Knip 可能无法识别 Vue 组件的动态引用
解决方案:
{
"vue": {
"entry": ["src/**/*.vue", "src/components/**/*.vue"]
}
}
2. 路径别名无法解析
解决方案:
{
"paths": {
"@/*": ["./src/*"],
"~/": ["./"]
}
}
3. 第三方库误报
解决方案:
{
"ignoreDependencies": ["vue-demi", "@vueuse/core"]
}
4. 动态导入被误报
解决方案:
// 使用注释标记
/* @public */
export const dynamicComponent = () => import('./Component.vue')
📈 最佳实践
1. 渐进式清理
# 第一步:仅检查明显问题
npx knip --include files,dependencies
# 第二步:检查导出问题
npx knip --include exports,types
# 第三步:全面检查
npx knip
2. 团队协作规范
- 🎯 每个 PR 运行死代码检测
- 📊 定期生成检测报告
- 🔄 建立清理计划和时间表
- 📝 文档化特殊情况的处理方式
3. 性能优化
# 大型项目性能优化
npx knip --no-gitignore # 忽略.gitignore提升性能
npx knip --workspace frontend # 单独检测工作区
4. 代码组织建议
// ✅ 推荐:明确的导出
export interface User {
id: string
name: string
}
export function createUser(data: Partial<User>): User {
// implementation
}
// ❌ 避免:批量导出未使用的内容
export * from './helpers'
🔮 高级配置
自定义报告器
// custom-reporter.js
module.exports = report => {
const issues = Object.values(report).flat()
console.log(`发现 ${issues.length} 个问题`)
// 自定义输出格式
issues.forEach(issue => {
console.log(`${issue.file}:${issue.line} - ${issue.name}`)
})
}
预处理器
// preprocessor.js
module.exports = report => {
// 过滤掉测试文件的问题
Object.keys(report).forEach(key => {
report[key] = report[key].filter(issue => !issue.file.includes('.spec.') && !issue.file.includes('.test.'))
})
return report
}