Vue + TypeScript 项目死代码检测最佳方案

Daotin 于 2025-09-19 发布 编辑
  1. 📖 概述
  2. 🎯 检测目标
  3. 🛠️ 推荐工具
    1. 主要工具:Knip(首选)
  4. 📦 安装和配置
    1. 1. 安装 Knip
    2. 2. 创建配置文件
    3. 3. 高级配置选项
  5. 🚀 使用方法
    1. 基础命令
    2. 高级用法
  6. 📊 报告解读
    1. 问题类型说明
    2. 示例报告
  7. 🛡️ JSDoc 标记
  8. 🔄 CI/CD 集成
    1. 1. 添加 npm 脚本
    2. 2. GitHub Actions 配置
    3. 3. 预提交钩子
  9. 📁 项目结构建议
    1. 推荐的目录结构
    2. 模块导出建议
  10. 🔧 常见问题与解决方案
    1. 1. Vue 组件未被检测到
    2. 2. 路径别名无法解析
    3. 3. 第三方库误报
    4. 4. 动态导入被误报
  11. 📈 最佳实践
    1. 1. 渐进式清理
    2. 2. 团队协作规范
    3. 3. 性能优化
    4. 4. 代码组织建议
  12. 🔮 高级配置
    1. 自定义报告器
    2. 预处理器
  13. 📚 参考资源

📖 概述

最近接手了一个陈年老旧项目,代码库中积累了大量的孤儿函数、未使用的文件和模块。本文档提供了一套完整的死代码检测和清理方案,帮助团队维护高质量的代码库。

🎯 检测目标

🛠️ 推荐工具

主要工具:Knip(首选)

为什么选择 Knip?

对比其他工具:

功能 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. 团队协作规范

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
}

📚 参考资源