Vue CLI 插件开发完全指南:从原理到实战

47 阅读6分钟

本文深入探讨 Vue CLI 插件开发的全过程,包含详细的工作原理、开发流程、实际案例和最佳实践,帮助您掌握自定义 Vue CLI 插件的核心技能。

1. Vue CLI 插件概述

1.1 什么是 Vue CLI 插件?

Vue CLI 插件是一个 npm 包,可以为 Vue CLI 创建的项目添加额外功能。它可以:

  • 添加新的 CLI 命令
  • 修改 webpack 配置
  • 添加新的 UI 界面
  • 注入额外的依赖
  • 修改项目文件结构
  • 集成第三方工具和服务

1.2 插件的作用和价值

解决的问题:

  • 重复配置的自动化
  • 团队工具链的统一
  • 项目最佳实践的标准化
  • 复杂功能的模块化封装

典型应用场景:

  • UI 组件库集成(如 Element UI、Vant)
  • 状态管理方案(如 Vuex、Pinia)
  • 测试框架配置(如 Jest、Cypress)
  • 微前端架构支持
  • 部署和 DevOps 集成

2. Vue CLI 插件架构与工作原理

2.1 插件系统架构

graph TB
    A[Vue CLI Service] --> B[Plugin API]
    B --> C[Generator API]
    B --> D[Service Plugin API]
    B --> E[Prompt API]
    
    C --> F[文件操作]
    C --> G[依赖管理]
    C --> H[模板渲染]
    
    D --> I[Webpack 配置]
    D --> J[ChainWebpack]
    D --> K[环境变量]
    
    E --> L[用户交互]
    E --> M[条件逻辑]
    
    F --> N[项目文件修改]
    I --> O[构建配置]

2.2 核心概念解析

Service Plugin:

  • 运行时插件
  • 修改 webpack 配置
  • 添加环境变量
  • 注册 CLI 命令

Generator:

  • 项目创建时执行
  • 修改项目文件结构
  • 注入依赖包
  • 渲染模板文件

Prompts:

  • 用户交互收集信息
  • 条件性功能启用
  • 动态配置生成

3. 插件开发环境搭建

3.1 创建插件项目结构

# 创建插件目录
mkdir vue-cli-plugin-my-plugin
cd vue-cli-plugin-my-plugin

# 初始化 package.json
npm init -y

项目结构:

vue-cli-plugin-my-plugin/
├── package.json
├── index.js                 # Service Plugin
├── generator.js            # Generator API
├── prompts.js              # 用户提示
├── ui.js                   # Vue CLI UI 集成
├── templates/              # 模板文件
│   ├── src/
│   │   ├── plugins/
│   │   │   └── my-plugin.js
│   │   └── views/
│   │       └── About.vue
│   └── tests/
│       └── e2e/
│           └── my-plugin.spec.js
└── README.md

3.2 基础 package.json 配置

{
  "name": "vue-cli-plugin-my-plugin",
  "version": "1.0.0",
  "description": "A custom Vue CLI plugin for demo purposes",
  "main": "index.js",
  "keywords": [
    "vue",
    "vue-cli",
    "plugin"
  ],
  "author": "Your Name",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/your-username/vue-cli-plugin-my-plugin"
  },
  "engines": {
    "node": ">=8.9",
    "npm": ">=5.0.0"
  },
  "peerDependencies": {
    "@vue/cli-service": "^3.0.0 || ^4.0.0 || ^5.0.0"
  },
  "devDependencies": {
    "@vue/cli-service": "^5.0.0"
  }
}

4. Service Plugin 开发

4.1 基础 Service Plugin 结构

// index.js
module.exports = (api, options) => {
  // 检查 Vue CLI 版本
  if (api.version && parseInt(api.version.split('.')[0]) < 3) {
    throw new Error(
      `vue-cli-plugin-my-plugin 需要 Vue CLI 3 或更高版本。当前版本: ${api.version}`
    );
  }

  // 注册 CLI 命令
  api.registerCommand(
    'my-command',
    {
      description: '自定义插件命令演示',
      usage: 'vue-cli-service my-command [options]',
      options: {
        '--mode': '指定环境模式 (默认: development)',
        '--debug': '启用调试模式'
      }
    },
    (args) => {
      // 命令实现
      require('./commands/my-command')(args, api, options);
    }
  );

  // 修改 webpack 配置
  api.chainWebpack((webpackConfig) => {
    // 根据环境进行配置
    if (process.env.NODE_ENV === 'production') {
      configureForProduction(webpackConfig, options);
    } else {
      configureForDevelopment(webpackConfig, options);
    }
  });

  // 配置开发服务器
  api.configureDevServer((app, server) => {
    // 添加自定义中间件
    app.use('/api/my-plugin', require('./dev-server-middleware'));
  });

  // 监听文件变化
  api.registerCommandHooks((hooks) => {
    hooks.afterBuild.tap('my-plugin', (stats) => {
      console.log('构建完成,执行插件后处理...');
    });
  });
};

// 生产环境配置
function configureForProduction(webpackConfig, options) {
  webpackConfig
    .plugin('my-plugin-banner')
    .use(require('webpack').BannerPlugin, [
      {
        banner: `My Plugin v${require('./package.json').version}\nBuild time: ${new Date().toISOString()}`,
      },
    ]);
}

// 开发环境配置
function configureForDevelopment(webpackConfig, options) {
  webpackConfig
    .devServer
    .set('before', (app) => {
      app.get('/__my-plugin__/status', (req, res) => {
        res.json({ status: 'ok', timestamp: Date.now() });
      });
    });
}

4.2 自定义命令实现

// commands/my-command.js
module.exports = (args, api, options) => {
  const chalk = require('chalk');
  const { log, error, warn } = require('@vue/cli-shared-utils');
  
  const fs = require('fs');
  const path = require('path');

  log(`${chalk.cyan('My Plugin Command')} - 开始执行...`);

  try {
    const projectRoot = api.resolve('.');
    const packageJsonPath = path.join(projectRoot, 'package.json');
    
    if (!fs.existsSync(packageJsonPath)) {
      error('未找到 package.json 文件');
      process.exit(1);
    }

    const packageJson = require(packageJsonPath);
    
    // 显示项目信息
    log(`项目名称: ${chalk.green(packageJson.name)}`);
    log(`项目版本: ${chalk.green(packageJson.version)}`);
    log(`Vue CLI 版本: ${chalk.green(api.version)}`);
    
    // 根据参数执行不同操作
    if (args.debug) {
      log(`${chalk.yellow('调试模式已启用')}`);
      log('参数:', args);
      log('插件选项:', options.pluginOptions || {});
    }

    // 执行插件特定逻辑
    if (args.mode) {
      log(`运行模式: ${chalk.blue(args.mode)}`);
    }

    log(`${chalk.green('✓')} 命令执行完成`);

  } catch (err) {
    error(`命令执行失败: ${err.message}`);
    process.exit(1);
  }
};

5. Generator API 开发

5.1 Generator 核心实现

// generator.js
module.exports = (api, options, rootOptions) => {
  // 插件选项
  const pluginOptions = options.pluginOptions && options.pluginOptions.myPlugin 
    ? options.pluginOptions.myPlugin 
    : {};

  // 添加依赖
  api.extendPackage({
    dependencies: {
      'axios': '^1.0.0',
      'lodash': '^4.17.21'
    },
    devDependencies: {
      '@types/lodash': '^4.14.182'
    },
    scripts: {
      'my-plugin:analyze': 'vue-cli-service my-command --analyze',
      'my-plugin:build': 'vue-cli-service my-command --mode production'
    },
    // 添加 ESLint 配置
    eslintConfig: {
      rules: {
        'my-plugin/custom-rule': 'warn'
      }
    }
  });

  // 渲染模板文件
  api.render('./templates', {
    ...options,
    pluginOptions,
    hasTypeScript: api.hasPlugin('typescript'),
    hasRouter: api.hasPlugin('router'),
    hasVuex: api.hasPlugin('vuex')
  });

  // 修改 main.js
  api.injectImports(api.entryFile, `import './plugins/my-plugin'`);

  // 条件性文件操作
  if (pluginOptions.addExampleComponent) {
    api.render({
      './src/components/ExampleComponent.vue': './templates/src/components/ExampleComponent.vue'
    });
  }

  // 项目创建完成后的回调
  api.onCreateComplete(() => {
    const fs = require('fs');
    const path = require('path');
    
    const eslintrcPath = api.resolve('.eslintrc.js');
    if (fs.existsSync(eslintrcPath)) {
      // 修改 ESLint 配置
      const content = fs.readFileSync(eslintrcPath, 'utf-8');
      const updatedContent = content.replace(
        'rules: {}',
        `rules: {
          'my-plugin/custom-rule': 'warn'
        }`
      );
      fs.writeFileSync(eslintrcPath, updatedContent);
    }
  });
};

5.2 模板文件示例

插件入口文件:

// templates/src/plugins/my-plugin.js
import axios from 'axios';
import _ from 'lodash';

class MyPlugin {
  constructor(options = {}) {
    this.options = {
      baseURL: process.env.VUE_APP_API_BASE_URL || '/api',
      timeout: 10000,
      ...options
    };
    
    this.axiosInstance = axios.create({
      baseURL: this.options.baseURL,
      timeout: this.options.timeout
    });
    
    this.setupInterceptors();
    this.installHelpers();
  }

  setupInterceptors() {
    // 请求拦截器
    this.axiosInstance.interceptors.request.use(
      (config) => {
        console.log(`[My Plugin] 发送请求: ${config.method?.toUpperCase()} ${config.url}`);
        return config;
      },
      (error) => {
        console.error('[My Plugin] 请求错误:', error);
        return Promise.reject(error);
      }
    );

    // 响应拦截器
    this.axiosInstance.interceptors.response.use(
      (response) => {
        console.log(`[My Plugin] 收到响应: ${response.status} ${response.config.url}`);
        return response;
      },
      (error) => {
        console.error('[My Plugin] 响应错误:', error);
        return Promise.reject(error);
      }
    );
  }

  installHelpers() {
    // 添加全局工具方法
    if (window && !window.$myPlugin) {
      window.$myPlugin = {
        deepClone: (obj) => _.cloneDeep(obj),
        debounce: (func, wait) => _.debounce(func, wait),
        request: this.axiosInstance
      };
    }
  }

  install(Vue) {
    // 添加 Vue 实例方法
    Vue.prototype.$myPlugin = this;
    
    // 添加全局混入
    Vue.mixin({
      created() {
        if (this.$options.myPluginOptions) {
          console.log('[My Plugin] 组件创建:', this.$options.name);
        }
      }
    });

    // 添加全局组件
    // Vue.component('MyPluginComponent', ...);
  }
}

// 创建插件实例
const myPlugin = new MyPlugin();

export default myPlugin;

示例组件模板:

<!-- templates/src/components/ExampleComponent.vue -->
<template>
  <div class="example-component">
    <h3>{{ title }}</h3>
    <div class="content">
      <p>这是一个通过 My Plugin 自动生成的示例组件</p>
      <button @click="handleClick" class="demo-button">
        点击次数: {{ count }}
      </button>
      <div v-if="data" class="data-display">
        <h4>示例数据:</h4>
        <pre>{{ formattedData }}</pre>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'ExampleComponent',
  data() {
    return {
      title: 'My Plugin 示例组件',
      count: 0,
      data: null
    }
  },
  computed: {
    formattedData() {
      return this.data ? JSON.stringify(this.data, null, 2) : '暂无数据';
    }
  },
  mounted() {
    this.fetchData();
  },
  methods: {
    handleClick() {
      this.count++;
      this.$emit('button-click', this.count);
    },
    async fetchData() {
      try {
        if (this.$myPlugin) {
          const response = await this.$myPlugin.request.get('/example');
          this.data = response.data;
        }
      } catch (error) {
        console.error('获取数据失败:', error);
      }
    }
  }
}
</script>

<style scoped>
.example-component {
  border: 1px solid #eaeaea;
  border-radius: 8px;
  padding: 20px;
  margin: 20px 0;
  background: #f9f9f9;
}

.example-component h3 {
  color: #2c3e50;
  margin-bottom: 15px;
}

.demo-button {
  background-color: #3498db;
  color: white;
  border: none;
  padding: 10px 20px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  transition: background-color 0.3s;
}

.demo-button:hover {
  background-color: #2980b9;
}

.data-display {
  margin-top: 15px;
  padding: 10px;
  background: white;
  border-radius: 4px;
  border: 1px solid #ddd;
}

.data-display pre {
  margin: 0;
  font-size: 12px;
  white-space: pre-wrap;
}
</style>

6. Prompts 用户交互

6.1 交互式提示配置

// prompts.js
module.exports = [
  {
    name: 'features',
    type: 'checkbox',
    message: '选择要启用的功能:',
    choices: [
      {
        name: '添加示例组件',
        value: 'addExampleComponent',
        description: '添加一个演示用的 Vue 组件'
      },
      {
        name: '集成 API 服务',
        value: 'addApiService',
        description: '添加 Axios 配置和 API 服务示例'
      },
      {
        name: '添加工具函数',
        value: 'addUtils',
        description: '添加常用的工具函数库'
      },
      {
        name: '集成代码规范',
        value: 'addLinting',
        description: '添加 ESLint 规则和代码规范配置'
      }
    ],
    default: ['addExampleComponent', 'addApiService']
  },
  {
    name: 'apiBaseUrl',
    type: 'input',
    message: '请输入 API 基础 URL:',
    default: '/api',
    when: answers => answers.features.includes('addApiService'),
    validate: input => input ? true : '请输入有效的 URL'
  },
  {
    name: 'useTypeScript',
    type: 'confirm',
    message: '是否使用 TypeScript?',
    default: false,
    when: () => {
      try {
        require.resolve('typescript');
        return true;
      } catch (e) {
        return false;
      }
    }
  },
  {
    name: 'addDemoPage',
    type: 'confirm',
    message: '是否添加演示页面?',
    default: true,
    when: answers => answers.features.includes('addExampleComponent')
  },
  {
    name: 'themeColor',
    type: 'list',
    message: '选择主题颜色:',
    choices: [
      { name: '蓝色主题', value: 'blue' },
      { name: '绿色主题', value: 'green' },
      { name: '紫色主题', value: 'purple' },
      { name: '橙色主题', value: 'orange' }
    ],
    default: 'blue',
    when: answers => answers.features.includes('addExampleComponent')
  }
];

6.2 基于用户选择的逻辑处理

// generator.js - 增强版本
module.exports = (api, options, rootOptions, answers = {}) => {
  const pluginOptions = {
    ...(options.pluginOptions && options.pluginOptions.myPlugin),
    ...answers
  };

  // 根据用户选择添加依赖
  const packageExtend = {
    dependencies: {},
    devDependencies: {},
    scripts: {}
  };

  // 基础依赖
  packageExtend.dependencies.axios = '^1.0.0';

  // 条件依赖
  if (pluginOptions.features && pluginOptions.features.includes('addUtils')) {
    packageExtend.dependencies.lodash = '^4.17.21';
    if (pluginOptions.useTypeScript) {
      packageExtend.devDependencies['@types/lodash'] = '^4.14.182';
    }
  }

  if (pluginOptions.features && pluginOptions.features.includes('addLinting')) {
    packageExtend.devDependencies['eslint-plugin-my-plugin'] = '^1.0.0';
    packageExtend.eslintConfig = {
      extends: ['plugin:my-plugin/recommended']
    };
  }

  api.extendPackage(packageExtend);

  // 渲染模板
  api.render('./templates', {
    ...options,
    pluginOptions,
    hasTypeScript: pluginOptions.useTypeScript || api.hasPlugin('typescript')
  });

  // 根据主题颜色修改配置
  if (pluginOptions.themeColor) {
    api.injectImports(api.entryFile, `import './styles/theme-${pluginOptions.themeColor}.css'`);
  }

  // 添加演示页面路由
  if (pluginOptions.addDemoPage && api.hasPlugin('router')) {
    api.injectImports(
      api.resolve('./src/router/index.js'),
      `import ExamplePage from '@/views/ExamplePage.vue'`
    );
    
    api.injectRootOptions(
      api.resolve('./src/router/index.js'),
      `routes: [
        // 其他路由...
        {
          path: '/example',
          name: 'ExamplePage',
          component: ExamplePage
        }
      ]`
    );
  }
};

7. UI 界面集成

7.1 Vue CLI UI 插件配置

// ui.js
const { openBrowser } = require('@vue/cli-shared-utils');

module.exports = (api, options) => {
  // 添加任务
  api.addTask({
    name: 'my-plugin-task',
    command: 'vue-cli-service my-command',
    description: '运行 My Plugin 自定义任务',
    link: 'https://github.com/your-username/vue-cli-plugin-my-plugin#readme'
  });

  // 添加配置面板
  api.addClientAddon({
    id: 'my-plugin.config',
    url: 'https://unpkg.com/vue-cli-plugin-my-plugin/dist/config.js'
  });

  // 共享数据
  api.onViewOpen((view) => {
    if (view.id === 'my-plugin-view') {
      console.log('My Plugin 视图已打开');
    }
  });

  // 项目文件变化监听
  api.onProjectFileChange((file) => {
    if (file.endsWith('my-plugin.config.js')) {
      api.emit('my-plugin-config-changed');
    }
  });
};

// 配置界面组件
module.exports.config = {
  components: {
    'my-plugin-config': {
      template: `
        <div class="my-plugin-config">
          <h3>My Plugin 配置</h3>
          <div class="config-section">
            <div class="form-group">
              <label>API 基础 URL</label>
              <input 
                v-model="config.apiBaseUrl" 
                type="text" 
                placeholder="/api"
                class="form-control"
              >
            </div>
            <div class="form-group">
              <label>
                <input 
                  v-model="config.enableDebug" 
                  type="checkbox"
                >
                启用调试模式
              </label>
            </div>
            <div class="form-group">
              <label>主题颜色</label>
              <select v-model="config.themeColor" class="form-control">
                <option value="blue">蓝色</option>
                <option value="green">绿色</option>
                <option value="purple">紫色</option>
                <option value="orange">橙色</option>
              </select>
            </div>
            <button @click="saveConfig" class="btn btn-primary">
              保存配置
            </button>
          </div>
        </div>
      `,
      data() {
        return {
          config: {
            apiBaseUrl: '/api',
            enableDebug: false,
            themeColor: 'blue'
          }
        };
      },
      methods: {
        async saveConfig() {
          try {
            await this.$callPluginAction('my-plugin/save-config', this.config);
            this.$notify({
              title: '配置保存成功',
              type: 'success'
            });
          } catch (error) {
            this.$notify({
              title: '保存失败',
              text: error.message,
              type: 'error'
            });
          }
        }
      },
      async created() {
        try {
          const config = await this.$callPluginAction('my-plugin/load-config');
          if (config) {
            this.config = { ...this.config, ...config };
          }
        } catch (error) {
          console.error('加载配置失败:', error);
        }
      }
    }
  }
};

8. 完整插件示例:Analytics Plugin

8.1 插件结构

vue-cli-plugin-analytics/
├── package.json
├── index.js
├── generator.js
├── prompts.js
├── commands/
│   └── analytics.js
├── templates/
│   ├── src/
│   │   ├── plugins/
│   │   │   └── analytics.js
│   │   └── components/
│   │       └── AnalyticsDashboard.vue
│   └── tests/
│       └── e2e/
│           └── analytics.spec.js
└── README.md

8.2 Service Plugin 实现

// index.js
module.exports = (api, options) => {
  const analyticsOptions = options.pluginOptions && options.pluginOptions.analytics 
    ? options.pluginOptions.analytics 
    : {};

  // 注册分析命令
  api.registerCommand(
    'analytics',
    {
      description: '生成和分析项目数据',
      usage: 'vue-cli-service analytics [options]',
      options: {
        '--report': '生成详细报告',
        '--export [format]': '导出数据 (json|csv)',
        '--watch': '监听文件变化'
      }
    },
    (args) => {
      require('./commands/analytics')(args, api, options);
    }
  );

  // 添加环境变量
  if (analyticsOptions.trackingId) {
    process.env.VUE_APP_ANALYTICS_TRACKING_ID = analyticsOptions.trackingId;
  }

  // 修改 webpack 配置
  api.chainWebpack((webpackConfig) => {
    // 添加分析插件
    if (process.env.NODE_ENV === 'production' && analyticsOptions.bundleAnalyzer) {
      const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
      webpackConfig
        .plugin('bundle-analyzer')
        .use(BundleAnalyzerPlugin, [{
          analyzerMode: 'static',
          openAnalyzer: false
        }]);
    }

    // 添加自定义 loader 用于分析
    webpackConfig.module
      .rule('analytics')
      .test(/\.vue$/)
      .pre()
      .use('analytics')
      .loader(require.resolve('./loaders/analytics-loader'))
      .options(analyticsOptions);
  });

  // 开发服务器配置
  api.configureDevServer((app) => {
    app.get('/_analytics/data', (req, res) => {
      res.json({
        project: {
          name: options.projectName,
          version: require(api.resolve('package.json')).version
        },
        components: getComponentStats(api),
        dependencies: getDependencyStats(api)
      });
    });
  });
};

function getComponentStats(api) {
  const fs = require('fs');
  const path = require('path');
  
  const componentsDir = api.resolve('./src/components');
  let components = [];
  
  if (fs.existsSync(componentsDir)) {
    components = fs.readdirSync(componentsDir)
      .filter(file => file.endsWith('.vue'))
      .map(file => {
        const filePath = path.join(componentsDir, file);
        const stats = fs.statSync(filePath);
        const content = fs.readFileSync(filePath, 'utf-8');
        
        return {
          name: file.replace('.vue', ''),
          size: stats.size,
          lines: content.split('\n').length,
          hasScript: content.includes('<script'),
          hasStyle: content.includes('<style')
        };
      });
  }
  
  return components;
}

function getDependencyStats(api) {
  const packageJson = require(api.resolve('package.json'));
  
  return {
    dependencies: Object.keys(packageJson.dependencies || {}).length,
    devDependencies: Object.keys(packageJson.devDependencies || {}).length,
    total: Object.keys(packageJson.dependencies || {}).length + 
           Object.keys(packageJson.devDependencies || {}).length
  };
}

8.3 分析命令实现

// commands/analytics.js
const chalk = require('chalk');
const { log, error, warn, info } = require('@vue/cli-shared-utils');
const fs = require('fs');
const path = require('path');

module.exports = (args, api, options) => {
  const projectRoot = api.resolve('.');
  
  log(`${chalk.cyan('Vue CLI Analytics')} - 项目分析工具\n`);

  try {
    // 收集项目数据
    const projectData = collectProjectData(projectRoot);
    
    // 显示基础信息
    displayBasicInfo(projectData);
    
    // 生成报告
    if (args.report) {
      generateReport(projectData, args);
    }
    
    // 导出数据
    if (args.export) {
      exportData(projectData, args.export);
    }

  } catch (err) {
    error(`分析失败: ${err.message}`);
    process.exit(1);
  }
};

function collectProjectData(projectRoot) {
  const packageJson = require(path.join(projectRoot, 'package.json'));
  
  // 收集组件信息
  const components = collectComponents(projectRoot);
  
  // 收集路由信息
  const routes = collectRoutes(projectRoot);
  
  // 收集依赖信息
  const dependencies = collectDependencies(packageJson);
  
  return {
    project: {
      name: packageJson.name,
      version: packageJson.version,
      description: packageJson.description
    },
    components,
    routes,
    dependencies,
    stats: {
      totalComponents: components.length,
      totalRoutes: routes.length,
      totalDependencies: dependencies.total
    }
  };
}

function collectComponents(projectRoot) {
  const componentsDir = path.join(projectRoot, 'src/components');
  const components = [];
  
  if (fs.existsSync(componentsDir)) {
    const walk = (dir) => {
      const files = fs.readdirSync(dir);
      
      files.forEach(file => {
        const filePath = path.join(dir, file);
        const stat = fs.statSync(filePath);
        
        if (stat.isDirectory()) {
          walk(filePath);
        } else if (file.endsWith('.vue')) {
          const content = fs.readFileSync(filePath, 'utf-8');
          const lines = content.split('\n').length;
          
          components.push({
            name: file.replace('.vue', ''),
            path: path.relative(projectRoot, filePath),
            size: stat.size,
            lines,
            hasScript: content.includes('<script'),
            hasStyle: content.includes('<style'),
            hasTemplate: content.includes('<template')
          });
        }
      });
    };
    
    walk(componentsDir);
  }
  
  return components;
}

function displayBasicInfo(data) {
  info(`项目: ${chalk.green(data.project.name)} v${data.project.version}`);
  info(`组件数量: ${chalk.blue(data.stats.totalComponents)}`);
  info(`路由数量: ${chalk.blue(data.stats.totalRoutes)}`);
  info(`依赖数量: ${chalk.blue(data.stats.totalDependencies)}`);
  
  // 显示最大的组件
  const largestComponent = data.components
    .sort((a, b) => b.size - a.size)[0];
  
  if (largestComponent) {
    warn(`最大的组件: ${chalk.yellow(largestComponent.name)} (${(largestComponent.size / 1024).toFixed(2)} KB)`);
  }
}

9. 插件测试和调试

9.1 本地测试配置

// package.json - 测试脚本
{
  "scripts": {
    "test:unit": "jest",
    "test:e2e": "cypress run",
    "test:integration": "node test/integration.js",
    "dev": "node test/dev-server.js",
    "link:local": "npm link && cd test-project && npm link vue-cli-plugin-my-plugin"
  },
  "devDependencies": {
    "jest": "^27.0.0",
    "cypress": "^9.0.0",
    "@vue/test-utils": "^2.0.0"
  }
}

9.2 集成测试示例

// test/integration.test.js
const { runCLI } = require('@vue/cli-test-utils');
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');

describe('vue-cli-plugin-my-plugin', () => {
  const projectName = 'test-project';
  const projectPath = path.join(__dirname, '..', projectName);
  
  beforeAll(async () => {
    // 创建测试项目
    await runCLI(['create', projectName, '--preset', 'default'], {
      cwd: path.join(__dirname, '..')
    });
  });
  
  afterAll(() => {
    // 清理测试项目
    if (fs.existsSync(projectPath)) {
      fs.rmSync(projectPath, { recursive: true });
    }
  });
  
  test('插件安装成功', async () => {
    // 安装插件
    await runCLI(['add', 'vue-cli-plugin-my-plugin'], {
      cwd: projectPath
    });
    
    // 检查文件是否生成
    const pluginFile = path.join(projectPath, 'src/plugins/my-plugin.js');
    expect(fs.existsSync(pluginFile)).toBe(true);
    
    // 检查 package.json 是否更新
    const packageJson = require(path.join(projectPath, 'package.json'));
    expect(packageJson.dependencies).toHaveProperty('axios');
  });
  
  test('自定义命令工作正常', async () => {
    const result = await runCLI(['my-command'], {
      cwd: projectPath
    });
    
    expect(result.stdout).toContain('My Plugin Command');
  });
});

10. 插件发布和维护

10.1 发布准备

// package.json - 发布配置
{
  "name": "vue-cli-plugin-my-plugin",
  "version": "1.0.0",
  "description": "A feature-rich Vue CLI plugin for analytics and project management",
  "keywords": [
    "vue",
    "vue-cli",
    "plugin",
    "analytics",
    "project-management"
  ],
  "homepage": "https://github.com/your-username/vue-cli-plugin-my-plugin#readme",
  "bugs": {
    "url": "https://github.com/your-username/vue-cli-plugin-my-plugin/issues"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/your-username/vue-cli-plugin-my-plugin.git"
  },
  "license": "MIT",
  "author": "Your Name <your.email@example.com>",
  "files": [
    "index.js",
    "generator.js",
    "prompts.js",
    "ui.js",
    "commands",
    "templates",
    "README.md"
  ],
  "peerDependencies": {
    "@vue/cli-service": "^3.0.0 || ^4.0.0 || ^5.0.0"
  },
  "devDependencies": {
    "@vue/cli-service": "^5.0.0",
    "jest": "^27.0.0"
  },
  "engines": {
    "node": ">=12.0.0"
  }
}

10.2 文档和示例

# Vue CLI Plugin My Plugin

一个功能丰富的 Vue CLI 插件,提供项目分析、工具集成和开发效率提升功能。

## 功能特性

- 📊 项目分析和统计
- 🔧 自动化工具配置
- 🎨 主题和样式管理
- 📈 性能监控和优化
- 🔌 可扩展的插件架构

## 安装

```bash
vue add my-plugin

配置

vue.config.js 中配置插件选项:

module.exports = {
  pluginOptions: {
    myPlugin: {
      apiBaseUrl: '/api',
      enableDebug: true,
      themeColor: 'blue'
    }
  }
}

使用

命令行使用

# 运行分析
vue-cli-service analytics --report

# 导出数据
vue-cli-service analytics --export json

# 自定义命令
vue-cli-service my-command --debug

在代码中使用

// 在 Vue 组件中
export default {
  mounted() {
    if (this.$myPlugin) {
      this.$myPlugin.request.get('/data')
        .then(response => {
          console.log('Data:', response.data);
        });
    }
  }
}

API 参考

Plugin Options

  • apiBaseUrl (String): API 基础 URL
  • enableDebug (Boolean): 启用调试模式
  • themeColor (String): 主题颜色

Global Methods

  • $myPlugin.request: Axios 实例
  • $myPlugin.deepClone: 深度克隆工具
  • $myPlugin.debounce: 防抖函数

许可证

MIT


## 11. 总结

通过本文的详细讲解,您应该已经掌握了:

1. **Vue CLI 插件架构**:理解 Service Plugin、Generator、Prompts 的核心概念
2. **插件开发流程**:从环境搭建到发布维护的完整流程
3. **高级功能实现**:UI 集成、自定义命令、模板渲染等
4. **实战经验**:通过完整示例学习实际开发技巧
5. **最佳实践**:测试、调试、文档和维护的最佳方法

Vue CLI 插件生态系统为 Vue.js 开发提供了强大的扩展能力,掌握插件开发技能将极大提升您的开发效率和项目质量。

---

**扩展学习资源:**
- [Vue CLI 官方文档](https://cli.vuejs.org/)
- [Plugin Development Guide](https://cli.vuejs.org/dev-guide/plugin-dev.html)
- [Webpack Configuration](https://webpack.js.org/configuration/)
- [Node.js API Documentation](https://nodejs.org/api/)

开始创建您自己的 Vue CLI 插件,为 Vue.js 生态系统贡献力量!