插件系统实现(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);📝 总结
核心功能
- ✅ Hook 机制实现
- ✅ 插件注册和加载
- ✅ 生命周期管理
- ✅ 插件开发示例(ESLint、Prettier、Husky)
下一步
- 远程模板支持 - 支持从 GitHub、npm 等远程源下载模板