Daotin

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

Vue

📖 概述

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

🎯 检测目标

  • ✅ 未使用的文件和组件
  • ✅ 未使用的依赖包(dependencies & devDependencies)
  • ✅ 未列出的依赖(missing dependencies)
  • ✅ 未使用的导出函数、类型、接口
  • ✅ 重复的导出和函数
  • ✅ 未使用的类成员和枚举成员
  • ✅ 无法解析的 import 路径

🛠️ 推荐工具

主要工具:Knip(首选)

为什么选择 Knip?

  • 🔥 活跃维护,功能最全面
  • 🎯 支持 Vue 3 + TypeScript 项目
  • 🔌 内置 65+插件,自动识别主流框架
  • 📊 提供多种报告格式
  • 🚀 支持 monorepo 和工作区
  • ⚡ 性能优秀,支持大型项目

对比其他工具:

功能Knipdepcheckts-unused-exportsESLint
未使用文件✅❌❌❌
未使用依赖✅✅❌❌
未使用导出✅❌✅部分
Vue 支持✅部分❌✅
TypeScript 支持✅部分✅✅
维护状态活跃停止维护维护中活跃

📦 安装和配置

1. 安装 Knip

bash
# 使用npm
npm install -D knip

# 使用yarn
yarn add -D knip

# 使用pnpm
pnpm add -D knip

2. 创建配置文件

在项目根目录创建 knip.json:

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. 高级配置选项

json
{
    "$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"]
        }
    }
}

🚀 使用方法

基础命令

bash
# 运行完整检测
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

高级用法

bash
# 检测单个工作区
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重复导出保留一个导出位置

示例报告

plain text
✂️ 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 标记来控制检测行为:

typescript
/**
 * 公共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 脚本

json
{
    "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 配置

yaml
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. 预提交钩子

bash
# 安装husky和lint-staged
npm install -D husky lint-staged

# 配置package.json
{
  "lint-staged": {
    "src/**/*.{vue,ts,js}": [
      "knip --include exports,dependencies"
    ]
  }
}

📁 项目结构建议

推荐的目录结构

plain text
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/               # 样式文件

模块导出建议

typescript
// utils/index.ts - 统一导出入口
export * from './http'
export * from './format'
export * from './validation'

// 避免直接导出
export { default as HttpClient } from './http'

🔧 常见问题与解决方案

1. Vue 组件未被检测到

原因:Knip 可能无法识别 Vue 组件的动态引用

解决方案:

json
{
    "vue": {
        "entry": ["src/**/*.vue", "src/components/**/*.vue"]
    }
}

2. 路径别名无法解析

解决方案:

json
{
    "paths": {
        "@/*": ["./src/*"],
        "~/": ["./"]
    }
}

3. 第三方库误报

解决方案:

json
{
    "ignoreDependencies": ["vue-demi", "@vueuse/core"]
}

4. 动态导入被误报

解决方案:

typescript
// 使用注释标记
/* @public */
export const dynamicComponent = () => import('./Component.vue')

📈 最佳实践

1. 渐进式清理

bash
# 第一步:仅检查明显问题
npx knip --include files,dependencies

# 第二步:检查导出问题
npx knip --include exports,types

# 第三步:全面检查
npx knip

2. 团队协作规范

  • 🎯 每个 PR 运行死代码检测
  • 📊 定期生成检测报告
  • 🔄 建立清理计划和时间表
  • 📝 文档化特殊情况的处理方式

3. 性能优化

bash
# 大型项目性能优化
npx knip --no-gitignore           # 忽略.gitignore提升性能
npx knip --workspace frontend     # 单独检测工作区

4. 代码组织建议

typescript
// ✅ 推荐:明确的导出
export interface User {
    id: string
    name: string
}

export function createUser(data: Partial<User>): User {
    // implementation
}

// ❌ 避免:批量导出未使用的内容
export * from './helpers'

🔮 高级配置

自定义报告器

typescript
// 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}`)
    })
}

预处理器

typescript
// 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
}

📚 参考资源

Related posts