插件系统实现(Plugin System)

实现插件机制,允许脚手架通过插件扩展功能,类似 Vite 和 Webpack 的插件体系,包括 Hook 机制、插件注册、生命周期管理等。


📋 目录


1. 插件架构设计

1.1 插件系统架构

Scaffold Core
    ↓
Plugin System
    ├── Hook Registry
    ├── Plugin Loader
    └── Lifecycle Manager
    ↓
Plugins
    ├── ESLint Plugin
    ├── Prettier Plugin
    ├── Husky Plugin
    └── Custom Plugin

1.2 核心概念

  • Hook(钩子):在特定时机执行的函数
  • Plugin(插件):包含多个 Hook 的模块
  • Lifecycle(生命周期):插件执行的各个阶段

2. Hook 机制实现

2.1 Hook 类型定义

// src/core/hooks.ts
export type HookFunction = (...args: any[]) => Promise<void> | void;
 
export interface Hook {
  name: string;
  fn: HookFunction;
  priority?: number; // 优先级,数字越小越先执行
}
 
export class HookRegistry {
  private hooks: Map<string, Hook[]> = new Map();
 
  // 注册 Hook
  register(hookName: string, fn: HookFunction, priority: number = 100) {
    if (!this.hooks.has(hookName)) {
      this.hooks.set(hookName, []);
    }
 
    const hooks = this.hooks.get(hookName)!;
    hooks.push({ name: hookName, fn, priority });
    
    // 按优先级排序
    hooks.sort((a, b) => (a.priority || 100) - (b.priority || 100));
  }
 
  // 执行 Hook
  async execute(hookName: string, ...args: any[]): Promise<void> {
    const hooks = this.hooks.get(hookName) || [];
    
    for (const hook of hooks) {
      await hook.fn(...args);
    }
  }
 
  // 获取所有 Hook 名称
  getHookNames(): string[] {
    return Array.from(this.hooks.keys());
  }
}

2.2 预定义 Hook

// src/core/lifecycle.ts
export enum LifecycleHooks {
  // 创建前
  BEFORE_CREATE = 'beforeCreate',
  
  // 创建后
  AFTER_CREATE = 'afterCreate',
  
  // 模板处理前
  BEFORE_RENDER_TEMPLATE = 'beforeRenderTemplate',
  
  // 模板处理后
  AFTER_RENDER_TEMPLATE = 'afterRenderTemplate',
  
  // 安装依赖前
  BEFORE_INSTALL = 'beforeInstall',
  
  // 安装依赖后
  AFTER_INSTALL = 'afterInstall',
  
  // Git 初始化前
  BEFORE_INIT_GIT = 'beforeInitGit',
  
  // Git 初始化后
  AFTER_INIT_GIT = 'afterInitGit',
  
  // 完成前
  BEFORE_COMPLETE = 'beforeComplete',
  
  // 完成后
  AFTER_COMPLETE = 'afterComplete'
}

3. 插件注册和加载

3.1 插件接口定义

// src/core/plugin.ts
import { HookRegistry } from './hooks';
import { LifecycleHooks } from './lifecycle';
 
export interface Plugin {
  name: string;
  version?: string;
  description?: string;
  
  // 插件安装函数
  install(registry: HookRegistry, options?: any): void;
  
  // 插件卸载函数(可选)
  uninstall?(): void;
}
 
export class PluginManager {
  private registry: HookRegistry;
  private plugins: Map<string, Plugin> = new Map();
 
  constructor() {
    this.registry = new HookRegistry();
  }
 
  // 注册插件
  register(plugin: Plugin, options?: any): void {
    if (this.plugins.has(plugin.name)) {
      throw new Error(`Plugin ${plugin.name} is already registered`);
    }
 
    plugin.install(this.registry, options);
    this.plugins.set(plugin.name, plugin);
  }
 
  // 卸载插件
  unregister(pluginName: string): void {
    const plugin = this.plugins.get(pluginName);
    if (plugin && plugin.uninstall) {
      plugin.uninstall();
    }
    this.plugins.delete(pluginName);
  }
 
  // 获取 Hook Registry
  getRegistry(): HookRegistry {
    return this.registry;
  }
 
  // 获取所有插件
  getPlugins(): Plugin[] {
    return Array.from(this.plugins.values());
  }
}

3.2 插件加载器

// src/core/loader.ts
import path from 'path';
import fs from 'fs-extra';
import { Plugin, PluginManager } from './plugin';
 
export class PluginLoader {
  // 从本地路径加载插件
  async loadFromPath(pluginPath: string): Promise<Plugin> {
    const fullPath = path.resolve(pluginPath);
    
    if (!(await fs.pathExists(fullPath))) {
      throw new Error(`Plugin not found: ${pluginPath}`);
    }
 
    // 尝试加载插件
    const pluginModule = require(fullPath);
    
    // 支持默认导出或命名导出
    const plugin = pluginModule.default || pluginModule;
    
    if (!plugin || typeof plugin.install !== 'function') {
      throw new Error(`Invalid plugin: ${pluginPath}`);
    }
 
    return plugin;
  }
 
  // 从 npm 包加载插件
  async loadFromNpm(packageName: string): Promise<Plugin> {
    // 这里需要先安装 npm 包
    // 然后加载插件
    const pluginPath = path.join(
      process.cwd(),
      'node_modules',
      packageName
    );
    
    return this.loadFromPath(pluginPath);
  }
}

4. 插件生命周期

4.1 集成到创建流程

// src/commands/create.ts
import { PluginManager } from '../core/plugin';
import { LifecycleHooks } from '../core/lifecycle';
 
export async function createCommand(
  projectName: string,
  options: any
) {
  const pluginManager = new PluginManager();
  const registry = pluginManager.getRegistry();
 
  try {
    // 1. 加载插件
    await loadPlugins(pluginManager, options);
 
    // 2. beforeCreate Hook
    await registry.execute(LifecycleHooks.BEFORE_CREATE, projectName, options);
 
    // 3. 创建项目
    const targetDir = path.resolve(process.cwd(), projectName);
    await createProject(targetDir, options);
 
    // 4. afterCreate Hook
    await registry.execute(LifecycleHooks.AFTER_CREATE, targetDir, options);
 
    // 5. beforeRenderTemplate Hook
    await registry.execute(
      LifecycleHooks.BEFORE_RENDER_TEMPLATE,
      targetDir,
      options
    );
 
    // 6. 渲染模板
    await renderTemplate(targetDir, options);
 
    // 7. afterRenderTemplate Hook
    await registry.execute(
      LifecycleHooks.AFTER_RENDER_TEMPLATE,
      targetDir,
      options
    );
 
    // 8. 安装依赖
    if (options.installDeps) {
      await registry.execute(
        LifecycleHooks.BEFORE_INSTALL,
        targetDir,
        options
      );
 
      await installDependencies(targetDir, options.packageManager);
 
      await registry.execute(
        LifecycleHooks.AFTER_INSTALL,
        targetDir,
        options
      );
    }
 
    // 9. 初始化 Git
    if (options.initGit) {
      await registry.execute(
        LifecycleHooks.BEFORE_INIT_GIT,
        targetDir,
        options
      );
 
      await initGit(targetDir);
 
      await registry.execute(
        LifecycleHooks.AFTER_INIT_GIT,
        targetDir,
        options
      );
    }
 
    // 10. beforeComplete Hook
    await registry.execute(
      LifecycleHooks.BEFORE_COMPLETE,
      targetDir,
      options
    );
 
    // 11. afterComplete Hook
    await registry.execute(
      LifecycleHooks.AFTER_COMPLETE,
      targetDir,
      options
    );
 
    logger.success(`项目 ${projectName} 创建成功!`);
  } catch (error) {
    logger.error(`创建项目失败: ${error.message}`);
    throw error;
  }
}
 
async function loadPlugins(
  pluginManager: PluginManager,
  options: any
): Promise<void> {
  // 从配置或选项加载插件
  const plugins = options.plugins || [];
 
  for (const pluginConfig of plugins) {
    if (typeof pluginConfig === 'string') {
      // 插件名称
      const loader = new PluginLoader();
      const plugin = await loader.loadFromNpm(pluginConfig);
      pluginManager.register(plugin);
    } else if (typeof pluginConfig === 'object') {
      // 插件配置对象
      const loader = new PluginLoader();
      const plugin = await loader.loadFromPath(pluginConfig.path);
      pluginManager.register(plugin, pluginConfig.options);
    }
  }
}

5. 插件开发示例

5.1 ESLint 插件

// plugins/eslint-plugin/index.ts
import { Plugin } from '../../src/core/plugin';
import { HookRegistry } from '../../src/core/hooks';
import { LifecycleHooks } from '../../src/core/lifecycle';
import fs from 'fs-extra';
import path from 'path';
 
const eslintPlugin: Plugin = {
  name: 'eslint-plugin',
  version: '1.0.0',
  description: '自动添加 ESLint 配置',
 
  install(registry: HookRegistry, options?: any) {
    // 在 afterCreate 时添加 ESLint 配置
    registry.register(
      LifecycleHooks.AFTER_CREATE,
      async (targetDir: string, createOptions: any) => {
        if (createOptions.features?.includes('eslint')) {
          await addESLintConfig(targetDir, createOptions);
        }
      },
      50 // 优先级
    );
  }
};
 
async function addESLintConfig(targetDir: string, options: any) {
  const eslintConfig = {
    extends: options.useTypeScript
      ? ['eslint:recommended', '@typescript-eslint/recommended']
      : ['eslint:recommended'],
    parser: options.useTypeScript ? '@typescript-eslint/parser' : undefined,
    parserOptions: {
      ecmaVersion: 2020,
      sourceType: 'module',
      ...(options.useTypeScript && {
        project: './tsconfig.json'
      })
    },
    env: {
      browser: true,
      es2020: true
    }
  };
 
  const configPath = path.join(targetDir, '.eslintrc.json');
  await fs.writeJson(configPath, eslintConfig, { spaces: 2 });
 
  // 更新 package.json
  const packageJsonPath = path.join(targetDir, 'package.json');
  const packageJson = await fs.readJson(packageJsonPath);
 
  packageJson.devDependencies = {
    ...packageJson.devDependencies,
    eslint: '^8.0.0',
    ...(options.useTypeScript && {
      '@typescript-eslint/eslint-plugin': '^5.0.0',
      '@typescript-eslint/parser': '^5.0.0'
    })
  };
 
  packageJson.scripts = {
    ...packageJson.scripts,
    lint: 'eslint src --ext .js,.jsx,.ts,.tsx',
    'lint:fix': 'eslint src --ext .js,.jsx,.ts,.tsx --fix'
  };
 
  await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
}
 
export default eslintPlugin;

5.2 Prettier 插件

// plugins/prettier-plugin/index.ts
import { Plugin } from '../../src/core/plugin';
import { HookRegistry } from '../../src/core/hooks';
import { LifecycleHooks } from '../../src/core/lifecycle';
import fs from 'fs-extra';
import path from 'path';
 
const prettierPlugin: Plugin = {
  name: 'prettier-plugin',
  version: '1.0.0',
  description: '自动添加 Prettier 配置',
 
  install(registry: HookRegistry, options?: any) {
    registry.register(
      LifecycleHooks.AFTER_CREATE,
      async (targetDir: string, createOptions: any) => {
        if (createOptions.features?.includes('prettier')) {
          await addPrettierConfig(targetDir);
        }
      },
      50
    );
  }
};
 
async function addPrettierConfig(targetDir: string) {
  const prettierConfig = {
    semi: true,
    singleQuote: true,
    tabWidth: 2,
    trailingComma: 'es5',
    printWidth: 80
  };
 
  const configPath = path.join(targetDir, '.prettierrc.json');
  await fs.writeJson(configPath, prettierConfig, { spaces: 2 });
 
  // 添加 .prettierignore
  const ignorePath = path.join(targetDir, '.prettierignore');
  await fs.writeFile(
    ignorePath,
    'node_modules\ndist\nbuild\ncoverage\n'
  );
 
  // 更新 package.json
  const packageJsonPath = path.join(targetDir, 'package.json');
  const packageJson = await fs.readJson(packageJsonPath);
 
  packageJson.devDependencies = {
    ...packageJson.devDependencies,
    prettier: '^2.8.0'
  };
 
  packageJson.scripts = {
    ...packageJson.scripts,
    format: 'prettier --write "src/**/*.{js,jsx,ts,tsx,json,css,md}"'
  };
 
  await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
}
 
export default prettierPlugin;

5.3 Husky 插件

// plugins/husky-plugin/index.ts
import { Plugin } from '../../src/core/plugin';
import { HookRegistry } from '../../src/core/hooks';
import { LifecycleHooks } from '../../src/core/lifecycle';
import { execSync } from 'child_process';
import fs from 'fs-extra';
import path from 'path';
 
const huskyPlugin: Plugin = {
  name: 'husky-plugin',
  version: '1.0.0',
  description: '自动添加 Husky Git Hooks',
 
  install(registry: HookRegistry, options?: any) {
    registry.register(
      LifecycleHooks.AFTER_INSTALL,
      async (targetDir: string, createOptions: any) => {
        if (createOptions.features?.includes('husky')) {
          await setupHusky(targetDir, createOptions);
        }
      },
      50
    );
  }
};
 
async function setupHusky(targetDir: string, options: any) {
  // 更新 package.json
  const packageJsonPath = path.join(targetDir, 'package.json');
  const packageJson = await fs.readJson(packageJsonPath);
 
  packageJson.devDependencies = {
    ...packageJson.devDependencies,
    husky: '^8.0.0',
    'lint-staged': '^13.0.0'
  };
 
  packageJson.scripts = {
    ...packageJson.scripts,
    prepare: 'husky install'
  };
 
  await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
 
  // 安装依赖后初始化 Husky
  execSync('npm run prepare', { cwd: targetDir, stdio: 'inherit' });
 
  // 创建 pre-commit hook
  const huskyDir = path.join(targetDir, '.husky');
  await fs.ensureDir(huskyDir);
 
  const preCommitHook = `#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
 
npx lint-staged
`;
 
  await fs.writeFile(
    path.join(huskyDir, 'pre-commit'),
    preCommitHook
  );
  await fs.chmod(path.join(huskyDir, 'pre-commit'), '755');
 
  // 添加 lint-staged 配置
  packageJson['lint-staged'] = {
    '*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'],
    '*.{json,css,md}': ['prettier --write']
  };
 
  await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
}
 
export default huskyPlugin;

5.4 使用插件

// 在创建命令中使用插件
const options = {
  template: 'react-ts',
  features: ['eslint', 'prettier', 'husky'],
  plugins: [
    'my-cli-eslint-plugin',
    'my-cli-prettier-plugin',
    'my-cli-husky-plugin'
  ]
};
 
await createCommand('my-app', options);

📝 总结

核心功能

  1. ✅ Hook 机制实现
  2. ✅ 插件注册和加载
  3. ✅ 生命周期管理
  4. ✅ 插件开发示例(ESLint、Prettier、Husky)

下一步


脚手架 前端工程化 插件系统 Hook机制 Node.js