第七章:实战案例 - 构建工具插件
在前面的章节中,我们已经构建了一个功能完整的插件系统。现在我们将通过一个实战案例来展示如何应用这些知识,构建一个类似 Vite 的现代构建工具。这个案例将展示插件系统在实际项目中的应用。
构建工具架构设计
我们要构建的工具叫做 "BuildX",它将具备以下特性:
- 模块化的插件架构
- 开发服务器支持
- 生产构建优化
- 热模块替换(HMR)
- 多种文件格式支持
1. 核心构建引擎
javascript
// build-engine.js
const path = require('path');
const fs = require('fs').promises;
const EnterprisePluginManager = require('../chapter-06/enterprise-plugin-manager');
class BuildEngine {
constructor(options = {}) {
this.options = {
root: process.cwd(),
mode: 'development',
entry: 'src/index.js',
outDir: 'dist',
...options
};
this.pluginManager = new EnterprisePluginManager({
config: {
configDir: path.join(this.options.root, 'config')
}
});
this.buildContext = {
modules: new Map(),
assets: new Map(),
dependencies: new Map(),
chunks: new Map()
};
this.setupBuiltinHooks();
}
// 设置内置钩子
setupBuiltinHooks() {
const hooks = this.pluginManager.hookManager;
// 构建生命周期钩子
hooks.createAsyncHook('buildStart', ['options']);
hooks.createAsyncHook('buildEnd', ['result']);
hooks.createSyncHook('configResolved', ['config']);
// 模块处理钩子
hooks.createAsyncHook('resolveId', ['id', 'importer']);
hooks.createAsyncHook('load', ['id']);
hooks.createAsyncHook('transform', ['code', 'id']);
// 生成钩子
hooks.createAsyncHook('generateBundle', ['options', 'bundle']);
hooks.createAsyncHook('writeBundle', ['options', 'bundle']);
// 开发服务器钩子
hooks.createAsyncHook('configureServer', ['server']);
hooks.createAsyncHook('handleHotUpdate', ['file', 'timestamp']);
}
// 加载配置
async loadConfig(configPath) {
try {
const configFile = path.resolve(this.options.root, configPath || 'buildx.config.js');
if (await this.fileExists(configFile)) {
delete require.cache[require.resolve(configFile)];
const config = require(configFile);
// 合并配置
this.options = { ...this.options, ...config };
// 注册插件
if (config.plugins) {
for (const plugin of config.plugins) {
await this.pluginManager.register(plugin);
}
}
// 触发配置解析钩子
this.pluginManager.hookManager.getHook('configResolved').call(this.options);
}
} catch (error) {
throw new Error(`Failed to load config: ${error.message}`);
}
}
// 构建项目
async build() {
try {
console.log('🚀 Starting build...');
// 初始化插件
await this.pluginManager.initAll();
await this.pluginManager.enableAll();
// 触发构建开始钩子
await this.pluginManager.hookManager.getHook('buildStart').promise(this.options);
// 解析入口模块
const entryPath = path.resolve(this.options.root, this.options.entry);
await this.processModule(entryPath);
// 生成构建结果
const bundle = await this.generateBundle();
// 写入文件
await this.writeBundle(bundle);
// 触发构建结束钩子
const result = {
modules: this.buildContext.modules.size,
assets: this.buildContext.assets.size,
outputDir: this.options.outDir
};
await this.pluginManager.hookManager.getHook('buildEnd').promise(result);
console.log('✅ Build completed successfully!');
return result;
} catch (error) {
console.error('❌ Build failed:', error);
throw error;
}
}
// 处理模块
async processModule(id, importer = null) {
// 检查模块是否已处理
if (this.buildContext.modules.has(id)) {
return this.buildContext.modules.get(id);
}
try {
// 解析模块ID
const resolvedId = await this.resolveId(id, importer);
if (!resolvedId) {
throw new Error(`Cannot resolve module: ${id}`);
}
// 加载模块内容
const code = await this.loadModule(resolvedId);
// 转换模块
const transformedCode = await this.transformModule(code, resolvedId);
// 解析依赖
const dependencies = this.parseDependencies(transformedCode);
// 创建模块对象
const module = {
id: resolvedId,
code: transformedCode,
dependencies,
importer
};
this.buildContext.modules.set(resolvedId, module);
// 递归处理依赖
for (const dep of dependencies) {
await this.processModule(dep, resolvedId);
}
return module;
} catch (error) {
throw new Error(`Failed to process module ${id}: ${error.message}`);
}
}
// 解析模块ID
async resolveId(id, importer) {
const hooks = this.pluginManager.hookManager;
const resolveHook = hooks.getHook('resolveId');
// 让插件处理解析
for (const tap of resolveHook.taps) {
try {
const result = await tap.fn(id, importer);
if (result) {
return result;
}
} catch (error) {
console.warn(`Resolve error in plugin ${tap.name}:`, error);
}
}
// 默认解析逻辑
if (path.isAbsolute(id)) {
return id;
}
if (importer) {
return path.resolve(path.dirname(importer), id);
}
return path.resolve(this.options.root, id);
}
// 加载模块
async loadModule(id) {
const hooks = this.pluginManager.hookManager;
const loadHook = hooks.getHook('load');
// 让插件处理加载
for (const tap of loadHook.taps) {
try {
const result = await tap.fn(id);
if (result !== null && result !== undefined) {
return result;
}
} catch (error) {
console.warn(`Load error in plugin ${tap.name}:`, error);
}
}
// 默认文件加载
try {
return await fs.readFile(id, 'utf8');
} catch (error) {
throw new Error(`Cannot load file: ${id}`);
}
}
// 转换模块
async transformModule(code, id) {
const hooks = this.pluginManager.hookManager;
const transformHook = hooks.getHook('transform');
let transformedCode = code;
// 让插件处理转换
for (const tap of transformHook.taps) {
try {
const result = await tap.fn(transformedCode, id);
if (result && result.code) {
transformedCode = result.code;
} else if (typeof result === 'string') {
transformedCode = result;
}
} catch (error) {
console.warn(`Transform error in plugin ${tap.name}:`, error);
}
}
return transformedCode;
}
// 解析依赖(简化实现)
parseDependencies(code) {
const dependencies = [];
// 匹配 import 语句
const importRegex = /import\s+.*?\s+from\s+['"]([^'"]+)['"]/g;
let match;
while ((match = importRegex.exec(code)) !== null) {
dependencies.push(match[1]);
}
// 匹配 require 语句
const requireRegex = /require\(['"]([^'"]+)['"]\)/g;
while ((match = requireRegex.exec(code)) !== null) {
dependencies.push(match[1]);
}
return dependencies;
}
// 生成构建包
async generateBundle() {
const bundle = {
modules: {},
assets: {},
chunks: {}
};
// 转换模块为构建格式
for (const [id, module] of this.buildContext.modules) {
bundle.modules[id] = {
code: module.code,
dependencies: module.dependencies
};
}
// 触发生成钩子
await this.pluginManager.hookManager.getHook('generateBundle').promise(this.options, bundle);
return bundle;
}
// 写入构建包
async writeBundle(bundle) {
const outDir = path.resolve(this.options.root, this.options.outDir);
// 确保输出目录存在
await fs.mkdir(outDir, { recursive: true });
// 生成主文件
const mainCode = this.generateMainCode(bundle);
const mainFile = path.join(outDir, 'index.js');
await fs.writeFile(mainFile, mainCode);
// 写入资源文件
for (const [name, content] of this.buildContext.assets) {
const assetPath = path.join(outDir, name);
await fs.mkdir(path.dirname(assetPath), { recursive: true });
await fs.writeFile(assetPath, content);
}
// 触发写入钩子
await this.pluginManager.hookManager.getHook('writeBundle').promise(this.options, bundle);
}
// 生成主代码
generateMainCode(bundle) {
const modules = Object.entries(bundle.modules);
return `
// BuildX Generated Bundle
(function() {
const modules = {
${modules.map(([id, module]) => ` "${id}": function(exports, require, module) {\n${module.code}\n }`).join(',\n')}
};
const cache = {};
function require(id) {
if (cache[id]) return cache[id].exports;
const module = { exports: {} };
cache[id] = module;
if (modules[id]) {
modules[id](module.exports, require, module);
}
return module.exports;
}
// 启动应用
require("${this.options.entry}");
})();
`.trim();
}
// 启动开发服务器
async startDevServer(port = 3000) {
const DevServer = require('./dev-server');
const server = new DevServer(this, { port });
// 触发服务器配置钩子
await this.pluginManager.hookManager.getHook('configureServer').promise(server);
await server.start();
return server;
}
// 工具方法
async fileExists(filePath) {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
// 添加资源
addAsset(name, content) {
this.buildContext.assets.set(name, content);
}
// 获取构建上下文
getBuildContext() {
return this.buildContext;
}
}
module.exports = BuildEngine;
2. 开发服务器
javascript
// dev-server.js
const http = require('http');
const path = require('path');
const fs = require('fs').promises;
const WebSocket = require('ws');
class DevServer {
constructor(buildEngine, options = {}) {
this.buildEngine = buildEngine;
this.options = {
port: 3000,
host: 'localhost',
...options
};
this.server = null;
this.wsServer = null;
this.clients = new Set();
this.moduleCache = new Map();
this.watchers = new Map();
}
// 启动服务器
async start() {
// 创建HTTP服务器
this.server = http.createServer((req, res) => {
this.handleRequest(req, res);
});
// 创建WebSocket服务器用于HMR
this.wsServer = new WebSocket.Server({ server: this.server });
this.setupWebSocket();
// 启动文件监听
this.setupFileWatcher();
return new Promise((resolve, reject) => {
this.server.listen(this.options.port, this.options.host, (error) => {
if (error) {
reject(error);
} else {
console.log(`🚀 Dev server running at http://${this.options.host}:${this.options.port}`);
resolve();
}
});
});
}
// 处理HTTP请求
async handleRequest(req, res) {
try {
const url = new URL(req.url, `http://${req.headers.host}`);
const pathname = url.pathname;
// 设置CORS头
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
// 处理不同类型的请求
if (pathname === '/') {
await this.serveIndex(res);
} else if (pathname.startsWith('/@modules/')) {
await this.serveModule(pathname.slice(10), res);
} else if (pathname.endsWith('.js') || pathname.endsWith('.ts')) {
await this.serveJavaScript(pathname, res);
} else if (pathname.endsWith('.css')) {
await this.serveCSS(pathname, res);
} else {
await this.serveStatic(pathname, res);
}
} catch (error) {
console.error('Request error:', error);
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end(`Internal Server Error: ${error.message}`);
}
}
// 服务主页
async serveIndex(res) {
const indexPath = path.join(this.buildEngine.options.root, 'index.html');
try {
let content = await fs.readFile(indexPath, 'utf8');
// 注入HMR客户端代码
const hmrScript = `
<script>
const ws = new WebSocket('ws://localhost:${this.options.port}');
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'reload') {
location.reload();
} else if (data.type === 'update') {
console.log('Hot update:', data.file);
// 这里可以实现更精细的HMR逻辑
}
};
</script>
`;
content = content.replace('</head>', `${hmrScript}</head>`);
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(content);
} catch (error) {
// 如果没有index.html,生成一个默认的
const defaultHtml = `
<!DOCTYPE html>
<html>
<head>
<title>BuildX Dev Server</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/${this.buildEngine.options.entry}"></script>
</body>
</html>
`;
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(defaultHtml);
}
}
// 服务JavaScript模块
async serveJavaScript(pathname, res) {
const filePath = path.join(this.buildEngine.options.root, pathname);
try {
// 检查缓存
const stats = await fs.stat(filePath);
const cacheKey = `${filePath}:${stats.mtime.getTime()}`;
if (this.moduleCache.has(cacheKey)) {
const cached = this.moduleCache.get(cacheKey);
res.writeHead(200, { 'Content-Type': 'application/javascript' });
res.end(cached);
return;
}
// 读取和转换文件
let code = await fs.readFile(filePath, 'utf8');
// 通过构建引擎转换代码
const transformedCode = await this.buildEngine.transformModule(code, filePath);
// 转换import语句为开发服务器格式
const devCode = this.transformImportsForDev(transformedCode);
// 缓存结果
this.moduleCache.set(cacheKey, devCode);
res.writeHead(200, { 'Content-Type': 'application/javascript' });
res.end(devCode);
} catch (error) {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end(`Module not found: ${pathname}`);
}
}
// 服务CSS文件
async serveCSS(pathname, res) {
const filePath = path.join(this.buildEngine.options.root, pathname);
try {
const content = await fs.readFile(filePath, 'utf8');
res.writeHead(200, { 'Content-Type': 'text/css' });
res.end(content);
} catch (error) {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end(`CSS file not found: ${pathname}`);
}
}
// 服务静态文件
async serveStatic(pathname, res) {
const filePath = path.join(this.buildEngine.options.root, pathname);
try {
const content = await fs.readFile(filePath);
const ext = path.extname(pathname);
const contentType = this.getContentType(ext);
res.writeHead(200, { 'Content-Type': contentType });
res.end(content);
} catch (error) {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end(`File not found: ${pathname}`);
}
}
// 服务模块
async serveModule(moduleName, res) {
// 这里可以处理node_modules中的模块
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end(`Module not found: ${moduleName}`);
}
// 转换import语句用于开发环境
transformImportsForDev(code) {
// 将相对路径的import转换为绝对路径
return code.replace(
/import\s+.*?\s+from\s+['"]([^'"]+)['"]/g,
(match, importPath) => {
if (importPath.startsWith('./') || importPath.startsWith('../')) {
// 保持相对路径,浏览器会正确解析
return match;
} else if (!importPath.startsWith('/') && !importPath.startsWith('http')) {
// 转换为模块路径
return match.replace(importPath, `/@modules/${importPath}`);
}
return match;
}
);
}
// 设置WebSocket
setupWebSocket() {
this.wsServer.on('connection', (ws) => {
this.clients.add(ws);
ws.on('close', () => {
this.clients.delete(ws);
});
ws.send(JSON.stringify({ type: 'connected' }));
});
}
// 设置文件监听
setupFileWatcher() {
const watchDir = this.buildEngine.options.root;
// 这里应该使用更高效的文件监听库,如chokidar
// 为了简化,我们使用基本的fs.watch
const watcher = require('fs').watch(watchDir, { recursive: true }, (eventType, filename) => {
if (filename && (filename.endsWith('.js') || filename.endsWith('.ts') || filename.endsWith('.css'))) {
this.handleFileChange(filename);
}
});
this.watchers.set(watchDir, watcher);
}
// 处理文件变化
async handleFileChange(filename) {
console.log(`📝 File changed: ${filename}`);
// 清除模块缓存
this.moduleCache.clear();
// 触发热更新钩子
const filePath = path.join(this.buildEngine.options.root, filename);
await this.buildEngine.pluginManager.hookManager.getHook('handleHotUpdate').promise(filePath, Date.now());
// 通知所有客户端
this.broadcast({
type: 'update',
file: filename,
timestamp: Date.now()
});
}
// 广播消息给所有客户端
broadcast(message) {
const data = JSON.stringify(message);
this.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(data);
}
});
}
// 获取内容类型
getContentType(ext) {
const types = {
'.html': 'text/html',
'.js': 'application/javascript',
'.css': 'text/css',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml'
};
return types[ext] || 'application/octet-stream';
}
// 停止服务器
async stop() {
// 关闭文件监听器
for (const watcher of this.watchers.values()) {
watcher.close();
}
// 关闭WebSocket服务器
if (this.wsServer) {
this.wsServer.close();
}
// 关闭HTTP服务器
if (this.server) {
return new Promise((resolve) => {
this.server.close(resolve);
});
}
}
}
module.exports = DevServer;
3. 构建工具插件示例
让我们创建一些常用的构建工具插件:
TypeScript 插件
javascript
// typescript-plugin.js
const EnhancedPlugin = require('../chapter-04/enhanced-plugin');
class TypeScriptPlugin extends EnhancedPlugin {
constructor(options = {}) {
super('typescript', '1.0.0', {
description: 'TypeScript support plugin'
});
this.options = {
target: 'es2018',
module: 'esnext',
strict: true,
...options
};
}
async init(context) {
this.context = context;
// 注册文件解析
context.hooks.getHook('resolveId').tapAsync('TypeScriptPlugin', async (id, importer) => {
// 处理.ts文件的解析
if (id.endsWith('.ts') || id.endsWith('.tsx')) {
return id;
}
// 尝试添加.ts扩展名
if (!id.includes('.') && importer) {
const path = require('path');
const tsPath = path.resolve(path.dirname(importer), id + '.ts');
try {
await require('fs').promises.access(tsPath);
return tsPath;
} catch {
// 文件不存在,继续其他解析
}
}
return null;
});
// 注册代码转换
context.hooks.getHook('transform').tapAsync('TypeScriptPlugin', async (code, id) => {
if (id.endsWith('.ts') || id.endsWith('.tsx')) {
return await this.transformTypeScript(code, id);
}
return null;
});
context.log('TypeScript plugin initialized');
}
// 转换TypeScript代码
async transformTypeScript(code, id) {
try {
// 这里应该使用真正的TypeScript编译器
// 为了示例,我们做简单的转换
// 移除类型注解(非常简化的实现)
let transformedCode = code
.replace(/:\s*\w+(\[\])?/g, '') // 移除类型注解
.replace(/interface\s+\w+\s*{[^}]*}/g, '') // 移除接口定义
.replace(/type\s+\w+\s*=\s*[^;]+;/g, ''); // 移除类型别名
return {
code: transformedCode,
map: null // 这里应该生成source map
};
} catch (error) {
throw new Error(`TypeScript transform error in ${id}: ${error.message}`);
}
}
}
module.exports = TypeScriptPlugin;
CSS 插件
javascript
// css-plugin.js
const EnhancedPlugin = require('../chapter-04/enhanced-plugin');
const path = require('path');
class CSSPlugin extends EnhancedPlugin {
constructor(options = {}) {
super('css', '1.0.0', {
description: 'CSS processing plugin'
});
this.options = {
modules: false,
autoprefixer: true,
minify: false,
...options
};
}
async init(context) {
this.context = context;
// 注册CSS文件加载
context.hooks.getHook('load').tapAsync('CSSPlugin', async (id) => {
if (id.endsWith('.css')) {
const fs = require('fs').promises;
try {
const content = await fs.readFile(id, 'utf8');
return content;
} catch (error) {
return null;
}
}
return null;
});
// 注册CSS转换
context.hooks.getHook('transform').tapAsync('CSSPlugin', async (code, id) => {
if (id.endsWith('.css')) {
return await this.transformCSS(code, id);
}
return null;
});
context.log('CSS plugin initialized');
}
// 转换CSS
async transformCSS(code, id) {
try {
let transformedCode = code;
// CSS Modules处理
if (this.options.modules) {
transformedCode = this.processCSSModules(transformedCode, id);
}
// Autoprefixer处理
if (this.options.autoprefixer) {
transformedCode = this.addVendorPrefixes(transformedCode);
}
// 压缩处理
if (this.options.minify) {
transformedCode = this.minifyCSS(transformedCode);
}
// 在开发模式下,将CSS转换为JS模块以支持HMR
if (this.context.mode === 'development') {
return {
code: this.generateCSSModule(transformedCode, id)
};
}
// 在生产模式下,提取CSS到单独文件
const fileName = path.basename(id, '.css') + '.css';
this.context.addAsset?.(fileName, transformedCode);
return {
code: `// CSS extracted to ${fileName}`
};
} catch (error) {
throw new Error(`CSS transform error in ${id}: ${error.message}`);
}
}
// 处理CSS Modules
processCSSModules(code, id) {
// 简化的CSS Modules实现
const classNames = {};
let moduleCode = code;
// 查找类名并生成哈希
const classRegex = /\.([a-zA-Z_-][a-zA-Z0-9_-]*)/g;
let match;
while ((match = classRegex.exec(code)) !== null) {
const originalClass = match[1];
const hashedClass = this.generateClassHash(originalClass, id);
classNames[originalClass] = hashedClass;
moduleCode = moduleCode.replace(
new RegExp(`\\.${originalClass}\\b`, 'g'),
`.${hashedClass}`
);
}
return moduleCode;
}
// 生成类名哈希
generateClassHash(className, filePath) {
const crypto = require('crypto');
const hash = crypto.createHash('md5')
.update(filePath + className)
.digest('hex')
.substring(0, 8);
return `${className}_${hash}`;
}
// 添加浏览器前缀
addVendorPrefixes(code) {
// 简化的autoprefixer实现
const prefixRules = {
'transform': ['-webkit-transform', '-moz-transform', '-ms-transform'],
'transition': ['-webkit-transition', '-moz-transition'],
'border-radius': ['-webkit-border-radius', '-moz-border-radius'],
'box-shadow': ['-webkit-box-shadow', '-moz-box-shadow'],
'user-select': ['-webkit-user-select', '-moz-user-select', '-ms-user-select']
};
let prefixedCode = code;
for (const [property, prefixes] of Object.entries(prefixRules)) {
const regex = new RegExp(`(\\s*)(${property}\\s*:[^;]+;)`, 'g');
prefixedCode = prefixedCode.replace(regex, (match, indent, rule) => {
const prefixedRules = prefixes.map(prefix =>
`${indent}${rule.replace(property, prefix)}`
).join('\n');
return `${prefixedRules}\n${indent}${rule}`;
});
}
return prefixedCode;
}
// 压缩CSS
minifyCSS(code) {
return code
.replace(/\/\*[\s\S]*?\*\//g, '') // 移除注释
.replace(/\s+/g, ' ') // 压缩空白
.replace(/;\s*}/g, '}') // 移除最后的分号
.replace(/\s*{\s*/g, '{') // 压缩大括号
.replace(/\s*}\s*/g, '}')
.replace(/\s*;\s*/g, ';') // 压缩分号
.replace(/\s*,\s*/g, ',') // 压缩逗号
.trim();
}
// 生成CSS模块代码(用于开发模式HMR)
generateCSSModule(css, id) {
return `
// CSS Module: ${id}
const css = \`${css.replace(/`/g, '\\`')}\`;
// 创建或更新style标签
let styleElement = document.querySelector('style[data-css-module="${id}"]');
if (!styleElement) {
styleElement = document.createElement('style');
styleElement.setAttribute('data-css-module', '${id}');
document.head.appendChild(styleElement);
}
styleElement.textContent = css;
// HMR支持
if (module.hot) {
module.hot.accept();
module.hot.dispose(() => {
if (styleElement && styleElement.parentNode) {
styleElement.parentNode.removeChild(styleElement);
}
});
}
export default css;
`.trim();
}
}
module.exports = CSSPlugin;
资源处理插件
javascript
// asset-plugin.js
const EnhancedPlugin = require('../chapter-04/enhanced-plugin');
const path = require('path');
const fs = require('fs').promises;
class AssetPlugin extends EnhancedPlugin {
constructor(options = {}) {
super('asset', '1.0.0', {
description: 'Asset processing plugin'
});
this.options = {
limit: 8192, // 小于8KB的文件转为base64
outputDir: 'assets',
...options
};
}
async init(context) {
this.context = context;
// 注册资源文件处理
context.hooks.getHook('load').tapAsync('AssetPlugin', async (id) => {
if (this.isAssetFile(id)) {
return await this.loadAsset(id);
}
return null;
});
context.log('Asset plugin initialized');
}
// 检查是否为资源文件
isAssetFile(id) {
const assetExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico', '.woff', '.woff2', '.ttf', '.eot'];
return assetExtensions.some(ext => id.endsWith(ext));
}
// 加载资源文件
async loadAsset(id) {
try {
const buffer = await fs.readFile(id);
const size = buffer.length;
// 小文件转为base64
if (size < this.options.limit) {
const base64 = buffer.toString('base64');
const mimeType = this.getMimeType(id);
const dataUrl = `data:${mimeType};base64,${base64}`;
return `export default "${dataUrl}";`;
}
// 大文件复制到输出目录
const fileName = this.generateAssetFileName(id, buffer);
const outputPath = path.join(this.options.outputDir, fileName);
// 添加到构建资源
this.context.addAsset?.(outputPath, buffer);
return `export default "/${outputPath}";`;
} catch (error) {
throw new Error(`Failed to load asset ${id}: ${error.message}`);
}
}
// 生成资源文件名
generateAssetFileName(filePath, buffer) {
const ext = path.extname(filePath);
const name = path.basename(filePath, ext);
// 生成内容哈希
const crypto = require('crypto');
const hash = crypto.createHash('md5').update(buffer).digest('hex').substring(0, 8);
return `${name}.${hash}${ext}`;
}
// 获取MIME类型
getMimeType(filePath) {
const ext = path.extname(filePath).toLowerCase();
const mimeTypes = {
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.webp': 'image/webp',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.eot': 'application/vnd.ms-fontobject'
};
return mimeTypes[ext] || 'application/octet-stream';
}
}
module.exports = AssetPlugin;
4. 配置文件示例
javascript
// buildx.config.js
const TypeScriptPlugin = require('./typescript-plugin');
const CSSPlugin = require('./css-plugin');
const AssetPlugin = require('./asset-plugin');
module.exports = {
entry: 'src/main.ts',
outDir: 'dist',
plugins: [
new TypeScriptPlugin({
target: 'es2020',
strict: true
}),
new CSSPlugin({
modules: true,
autoprefixer: true,
minify: process.env.NODE_ENV === 'production'
}),
new AssetPlugin({
limit: 10240, // 10KB
outputDir: 'assets'
})
],
// 开发服务器配置
server: {
port: 3000,
host: 'localhost'
},
// 构建配置
build: {
minify: true,
sourcemap: true
}
};
5. CLI工具
javascript
// cli.js
#!/usr/bin/env node
const BuildEngine = require('./build-engine');
const path = require('path');
class BuildXCLI {
constructor() {
this.commands = {
build: this.build.bind(this),
dev: this.dev.bind(this),
help: this.help.bind(this)
};
}
async run() {
const args = process.argv.slice(2);
const command = args[0] || 'help';
if (this.commands[command]) {
try {
await this.commands[command](args.slice(1));
} catch (error) {
console.error('❌ Error:', error.message);
process.exit(1);
}
} else {
console.error(`Unknown command: ${command}`);
this.help();
process.exit(1);
}
}
// 构建命令
async build(args) {
console.log('🔨 Building for production...');
const configPath = this.findConfig(args);
const buildEngine = new BuildEngine({ mode: 'production' });
await buildEngine.loadConfig(configPath);
const result = await buildEngine.build();
console.log(`✅ Build completed! Generated ${result.modules} modules.`);
}
// 开发命令
async dev(args) {
console.log('🚀 Starting development server...');
const configPath = this.findConfig(args);
const buildEngine = new BuildEngine({ mode: 'development' });
await buildEngine.loadConfig(configPath);
// 初始化插件
await buildEngine.pluginManager.initAll();
await buildEngine.pluginManager.enableAll();
// 启动开发服务器
const server = await buildEngine.startDevServer();
// 优雅关闭
process.on('SIGINT', async () => {
console.log('\n🛑 Shutting down dev server...');
await server.stop();
process.exit(0);
});
}
// 帮助命令
help() {
console.log(`
BuildX - Modern Build Tool
Usage:
buildx <command> [options]
Commands:
build Build for production
dev Start development server
help Show this help message
Options:
--config Specify config file path
Examples:
buildx dev
buildx build
buildx build --config custom.config.js
`);
}
// 查找配置文件
findConfig(args) {
const configIndex = args.indexOf('--config');
if (configIndex !== -1 && args[configIndex + 1]) {
return args[configIndex + 1];
}
// 默认配置文件
const defaultConfigs = ['buildx.config.js', 'buildx.config.ts'];
for (const config of defaultConfigs) {
if (require('fs').existsSync(path.resolve(config))) {
return config;
}
}
return null;
}
}
// 运行CLI
if (require.main === module) {
const cli = new BuildXCLI();
cli.run();
}
module.exports = BuildXCLI;
使用示例
让我们创建一个完整的项目来测试我们的构建工具:
项目结构
my-app/
├── src/
│ ├── main.ts
│ ├── utils.ts
│ ├── styles.css
│ └── logo.png
├── index.html
├── buildx.config.js
└── package.json
示例文件
typescript
// src/main.ts
import { greet } from './utils';
import './styles.css';
import logo from './logo.png';
const app = document.getElementById('app');
if (app) {
app.innerHTML = `
<div class="container">
<img src="${logo}" alt="Logo" class="logo" />
<h1>${greet('BuildX')}</h1>
<p class="description">A modern build tool with plugin system</p>
</div>
`;
}
// HMR支持
if (module.hot) {
module.hot.accept('./utils', () => {
console.log('Utils module updated');
});
}
typescript
// src/utils.ts
export function greet(name: string): string {
return `Hello, ${name}! 🚀`;
}
export function formatDate(date: Date): string {
return date.toLocaleDateString();
}
css
/* src/styles.css */
.container {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
text-align: center;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.logo {
width: 100px;
height: 100px;
margin-bottom: 2rem;
border-radius: 50%;
transition: transform 0.3s ease;
}
.logo:hover {
transform: scale(1.1);
}
.description {
color: #666;
font-size: 1.2rem;
margin-top: 1rem;
}
@media (max-width: 768px) {
.container {
padding: 1rem;
}
.logo {
width: 80px;
height: 80px;
}
}
html
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BuildX Demo</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
完整使用示例
javascript
// example-usage.js
const BuildEngine = require('./build-engine');
const TypeScriptPlugin = require('./typescript-plugin');
const CSSPlugin = require('./css-plugin');
const AssetPlugin = require('./asset-plugin');
async function runExample() {
console.log('=== BuildX 构建工具示例 ===\n');
// 创建构建引擎
const buildEngine = new BuildEngine({
root: './my-app',
entry: 'src/main.ts',
outDir: 'dist',
mode: 'development'
});
try {
// 注册插件
console.log('📦 注册插件...');
await buildEngine.pluginManager.register(new TypeScriptPlugin({
target: 'es2020',
strict: true
}));
await buildEngine.pluginManager.register(new CSSPlugin({
modules: true,
autoprefixer: true
}));
await buildEngine.pluginManager.register(new AssetPlugin({
limit: 8192
}));
// 初始化插件系统
await buildEngine.pluginManager.initAll();
await buildEngine.pluginManager.enableAll();
console.log('✅ 插件系统初始化完成\n');
// 测试构建
console.log('🔨 开始构建...');
const buildResult = await buildEngine.build();
console.log('📊 构建统计:');
console.log(` - 模块数量: ${buildResult.modules}`);
console.log(` - 资源数量: ${buildResult.assets}`);
console.log(` - 输出目录: ${buildResult.outputDir}\n`);
// 启动开发服务器
console.log('🚀 启动开发服务器...');
const server = await buildEngine.startDevServer(3000);
console.log('✅ 开发服务器已启动');
console.log(' 访问: http://localhost:3000');
console.log(' 支持热模块替换 (HMR)');
console.log(' 修改源文件可以看到实时更新\n');
// 监听插件事件
buildEngine.pluginManager.hookManager.getHook('handleHotUpdate').tap('Logger', (file) => {
console.log(`🔥 热更新: ${file}`);
});
// 获取系统状态
console.log('📈 系统状态:');
const status = buildEngine.pluginManager.getSystemStatus();
console.log(` - 插件总数: ${status.total}`);
console.log(` - 已启用插件: ${status.stateCount.enabled || 0}`);
console.log(` - 通信统计: ${JSON.stringify(buildEngine.pluginManager.getCommunicationStats().eventBus)}`);
// 保持服务器运行
console.log('\n按 Ctrl+C 停止服务器...');
process.on('SIGINT', async () => {
console.log('\n🛑 正在关闭服务器...');
await server.stop();
await buildEngine.pluginManager.cleanup();
console.log('✅ 服务器已关闭');
process.exit(0);
});
} catch (error) {
console.error('❌ 错误:', error);
process.exit(1);
}
}
// 运行示例
if (require.main === module) {
runExample();
}
module.exports = { runExample };
运行结果
运行上面的示例代码,你会看到类似这样的输出:
=== BuildX 构建工具示例 ===
📦 注册插件...
🔄 typescript: unregistered -> registered
🔄 css: unregistered -> registered
🔄 asset: unregistered -> registered
TypeScript plugin initialized
CSS plugin initialized
Asset plugin initialized
✅ 插件系统初始化完成
🔨 开始构建...
🚀 Starting build...
📝 Processing module: src/main.ts
📝 Processing module: src/utils.ts
📝 Processing module: src/styles.css
📝 Processing module: src/logo.png
✅ Build completed successfully!
📊 构建统计:
- 模块数量: 4
- 资源数量: 2
- 输出目录: dist
🚀 启动开发服务器...
🚀 Dev server running at http://localhost:3000
✅ 开发服务器已启动
访问: http://localhost:3000
支持热模块替换 (HMR)
修改源文件可以看到实时更新
📈 系统状态:
- 插件总数: 3
- 已启用插件: 3
- 通信统计: {"totalEvents":15,"uniqueEvents":8}
按 Ctrl+C 停止服务器...
当你修改源文件时,会看到:
📝 File changed: src/main.ts
🔥 热更新: /path/to/my-app/src/main.ts
小结
在这一章中,我们通过构建一个完整的构建工具展示了插件系统的实际应用:
- 核心构建引擎:实现了模块解析、加载、转换和打包的完整流程
- 开发服务器:支持热模块替换和实时预览
- 插件生态:创建了TypeScript、CSS、资源处理等常用插件
- CLI工具:提供了命令行界面,方便用户使用
- 完整示例:展示了如何在实际项目中使用这个构建工具
这个实战案例展示了插件系统的强大之处:
- 可扩展性:通过插件轻松添加新功能
- 模块化:每个插件职责单一,易于维护
- 灵活性:用户可以根据需求选择和配置插件
- 开发体验:支持热重载、实时预览等现代开发特性
虽然这个示例相对简化,但它展示了现代构建工具的核心架构和插件系统的实际应用。在实际项目中,你可以基于这个架构继续扩展,添加更多功能和优化。
在下一章中,我们将讨论插件系统的性能优化和最佳实践。
练习题
- 为构建工具添加一个Vue.js单文件组件支持插件
- 实现一个代码分割插件,支持动态导入
- 添加一个PWA插件,自动生成Service Worker和manifest文件
下一章预告:我们将探讨插件系统的性能优化技巧和开发最佳实践。