每天一个高级前端知识 - Day 18
今日主题:前端工程化进阶 - 自定义构建工具与Babel插件开发
核心概念:工程化是将"不确定性"转化为"确定性"的过程
前端工程化不仅仅是Webpack配置,而是可复用的抽象能力和自动化思维。
🔧 自定义构建工具(基于ESBuild)
// build.js - 轻量级构建工具
const esbuild = require('esbuild');
const fs = require('fs-extra');
const path = require('path');
const chokidar = require('chokidar');
class CustomBuilder {
constructor(config) {
this.config = {
entry: './src/index.ts',
outdir: './dist',
format: 'esm', // esm, cjs, iife
target: 'es2020',
minify: process.env.NODE_ENV === 'production',
sourcemap: true,
bundle: true,
plugins: [],
...config
};
this.watcher = null;
this.buildCount = 0;
}
async build() {
const startTime = Date.now();
try {
// 清理输出目录
await fs.emptyDir(this.config.outdir);
// 执行构建
const result = await esbuild.build({
...this.config,
metafile: true,
write: true
});
// 生成分析报告
await this.generateReport(result.metafile);
const duration = Date.now() - startTime;
console.log(`✅ 构建完成 (${duration}ms) | 文件数: ${Object.keys(result.metafile.outputs).length}`);
return result;
} catch (error) {
console.error('❌ 构建失败:', error);
throw error;
}
}
async generateReport(metafile) {
const report = {
totalSize: 0,
assets: [],
chunks: [],
dependencies: {}
};
for (const [file, info] of Object.entries(metafile.outputs)) {
const size = info.bytes;
report.totalSize += size;
report.assets.push({
file: path.basename(file),
size,
sizeFormatted: this.formatBytes(size)
});
}
// 排序并保存
report.assets.sort((a, b) => b.size - a.size);
await fs.writeJSON(path.join(this.config.outdir, 'build-report.json'), report, { spaces: 2 });
// 输出摘要
console.log(`📦 总大小: ${this.formatBytes(report.totalSize)}`);
console.log(`📊 最大文件: ${report.assets[0]?.file} (${report.assets[0]?.sizeFormatted})`);
}
formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
async watch() {
console.log('👀 启动监听模式...');
this.watcher = chokidar.watch(['./src/**/*'], {
ignored: /(^|[\/\\])\../,
persistent: true
});
let debounceTimer;
this.watcher.on('all', (event, file) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
console.log(`📝 检测到变更: ${file}`);
await this.build();
this.buildCount++;
}, 100);
});
// 首次构建
await this.build();
}
async serve(port = 3000) {
await this.build();
const express = require('express');
const app = express();
app.use(express.static(this.config.outdir));
app.use(express.static('./public'));
// 热更新中间件
const clients = new Set();
app.get('/__reload', (req, res) => {
clients.add(res);
req.on('close', () => clients.delete(res));
});
this.watcher = chokidar.watch('./src/**/*');
this.watcher.on('change', async () => {
await this.build();
clients.forEach(client => {
client.write('data: reload\n\n');
});
});
app.listen(port, () => {
console.log(`🚀 开发服务器启动: http://localhost:${port}`);
});
}
stop() {
if (this.watcher) {
this.watcher.close();
}
}
}
// 使用示例
const builder = new CustomBuilder({
entry: './src/index.tsx',
outdir: './dist',
format: 'esm',
minify: true,
external: ['react', 'react-dom']
});
// 开发模式
if (process.env.NODE_ENV === 'development') {
await builder.watch();
} else {
await builder.build();
}
🔌 Babel插件开发
// ============ 1. 基础Babel插件结构 ============
module.exports = function() {
return {
name: 'babel-plugin-custom',
visitor: {
// 访问者模式
}
};
};
// ============ 2. 实际插件:自动导入组件 ============
// babel-plugin-auto-import.js
module.exports = function({ types: t }) {
return {
name: 'babel-plugin-auto-import',
visitor: {
Program: {
enter(path, state) {
const { opts } = state;
const { components, from } = opts;
if (!components) return;
// 检查是否已存在导入
const existingImports = new Set();
path.node.body.forEach(node => {
if (t.isImportDeclaration(node) && node.source.value === from) {
node.specifiers.forEach(spec => {
if (t.isImportSpecifier(spec)) {
existingImports.add(spec.imported.name);
}
});
}
});
// 添加缺失的导入
const missingComponents = components.filter(c => !existingImports.has(c));
if (missingComponents.length > 0) {
const importDeclaration = t.importDeclaration(
missingComponents.map(comp =>
t.importSpecifier(t.identifier(comp), t.identifier(comp))
),
t.stringLiteral(from)
);
// 插入到文件开头
path.node.body.unshift(importDeclaration);
}
}
}
}
};
};
// ============ 3. 插件:console.log自动移除 ============
// babel-plugin-remove-console.js
module.exports = function({ types: t }) {
return {
name: 'babel-plugin-remove-console',
visitor: {
CallExpression(path, state) {
const { opts } = state;
const methods = opts.methods || ['log', 'info', 'warn', 'error', 'debug'];
if (t.isMemberExpression(path.node.callee)) {
const object = path.node.callee.object;
const property = path.node.callee.property;
if (
t.isIdentifier(object, { name: 'console' }) &&
t.isIdentifier(property) &&
methods.includes(property.name)
) {
// 生产环境移除
if (process.env.NODE_ENV === 'production') {
path.remove();
}
// 开发环境替换为noop
else if (opts.replaceWithNoop) {
path.replaceWith(t.identifier('noop'));
}
}
}
}
}
};
};
// ============ 4. 插件:组件props类型自动注入 ============
// babel-plugin-inject-proptypes.js
module.exports = function({ types: t }) {
return {
name: 'babel-plugin-inject-proptypes',
visitor: {
ExportDefaultDeclaration(path, state) {
const declaration = path.node.declaration;
if (!t.isFunctionDeclaration(declaration) && !t.isArrowFunctionExpression(declaration)) {
return;
}
const componentName = declaration.id?.name || 'Component';
const props = this.extractPropsFromUsage(declaration);
if (props.length > 0) {
const propTypes = this.generatePropTypes(props);
// 插入PropTypes声明
path.insertAfter(
t.expressionStatement(
t.assignmentExpression(
'=',
t.memberExpression(
t.identifier(componentName),
t.identifier('propTypes')
),
propTypes
)
)
);
}
}
},
extractPropsFromUsage(fn) {
const props = new Set();
// 遍历函数体,找出使用的props
fn.body.body.forEach(node => {
if (t.isExpressionStatement(node)) {
this.traverseProps(node.expression, props);
}
});
return Array.from(props);
},
traverseProps(node, props) {
if (!node) return;
if (t.isMemberExpression(node)) {
if (t.isIdentifier(node.object, { name: 'props' })) {
if (t.isIdentifier(node.property)) {
props.add(node.property.name);
}
}
this.traverseProps(node.object, props);
}
if (t.isLogicalExpression(node)) {
this.traverseProps(node.left, props);
this.traverseProps(node.right, props);
}
},
generatePropTypes(props) {
const properties = {};
props.forEach(prop => {
properties[prop] = t.memberExpression(
t.memberExpression(t.identifier('PropTypes'), t.identifier('any')),
t.identifier('isRequired')
);
});
return t.objectExpression(
Object.entries(properties).map(([key, value]) =>
t.objectProperty(t.identifier(key), value)
)
);
}
};
};
// ============ 5. 插件:国际化自动提取 ============
// babel-plugin-i18n-extract.js
const fs = require('fs');
const path = require('path');
module.exports = function({ types: t }) {
const messages = new Map();
return {
name: 'babel-plugin-i18n-extract',
visitor: {
CallExpression(path, state) {
const { opts } = state;
const { funcName = 't', outputPath = './locales/zh-CN.json' } = opts;
if (t.isIdentifier(path.node.callee, { name: funcName })) {
const arg = path.node.arguments[0];
if (t.isStringLiteral(arg)) {
const key = arg.value;
const defaultMessage = key;
if (!messages.has(key)) {
messages.set(key, defaultMessage);
}
// 开发环境下添加注释
if (process.env.NODE_ENV === 'development') {
path.addComment('leading', ` i18n: ${key} `);
}
}
}
}
},
post() {
// 生成语言文件
const locales = {};
for (const [key, value] of messages) {
locales[key] = value;
}
const outputPath = path.resolve(process.cwd(), './locales/zh-CN.json');
fs.writeFileSync(outputPath, JSON.stringify(locales, null, 2));
console.log(`✅ 提取了 ${messages.size} 条国际化消息到 ${outputPath}`);
}
};
};
⚡ 自定义ESLint规则
// eslint-plugin-custom/rules/no-hooks-in-loop.js
module.exports = {
meta: {
type: 'problem',
docs: {
description: '禁止在循环中使用React Hooks',
category: 'Best Practices',
recommended: true
},
fixable: 'code',
schema: []
},
create(context) {
let inLoop = false;
const loopNodes = new Set();
return {
// 检测循环
ForStatement(node) {
loopNodes.add(node);
inLoop = true;
},
'ForStatement:exit'(node) {
loopNodes.delete(node);
inLoop = loopNodes.size > 0;
},
WhileStatement(node) {
loopNodes.add(node);
inLoop = true;
},
'WhileStatement:exit'(node) {
loopNodes.delete(node);
inLoop = loopNodes.size > 0;
},
DoWhileStatement(node) {
loopNodes.add(node);
inLoop = true;
},
'DoWhileStatement:exit'(node) {
loopNodes.delete(node);
inLoop = loopNodes.size > 0;
},
// 检测Hook调用
CallExpression(node) {
if (!inLoop) return;
const callee = node.callee;
let hookName = null;
if (t.isIdentifier(callee)) {
hookName = callee.name;
} else if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
hookName = callee.property.name;
}
if (hookName && hookName.startsWith('use')) {
context.report({
node,
message: `不要在循环中使用 Hook "${hookName}",这会导致性能问题或状态错误`,
fix(fixer) {
// 提供修复建议:将Hook移到循环外
return fixer.insertTextBefore(node, '/* 警告: Hook不应该在循环中 */ ');
}
});
}
}
};
}
};
// 使用配置 .eslintrc.js
module.exports = {
plugins: ['custom'],
rules: {
'custom/no-hooks-in-loop': 'error',
'custom/no-console-in-production': process.env.NODE_ENV === 'production' ? 'error' : 'off'
}
};
📦 自动化发布工具
// scripts/release.js
const semver = require('semver');
const inquirer = require('inquirer');
const execa = require('execa');
const chalk = require('chalk');
const fs = require('fs-extra');
class ReleaseManager {
constructor(options) {
this.packagePath = './package.json';
this.pkg = require(this.packagePath);
this.currentVersion = this.pkg.version;
}
async run() {
console.log(chalk.cyan(`\n📦 当前版本: ${this.currentVersion}`));
// 1. 选择版本类型
const { versionType } = await inquirer.prompt([
{
type: 'list',
name: 'versionType',
message: '选择版本增量类型:',
choices: [
{ name: `补丁 (patch) ${chalk.gray('bug修复')}`, value: 'patch' },
{ name: `次要 (minor) ${chalk.gray('新功能,向后兼容')}`, value: 'minor' },
{ name: `主要 (major) ${chalk.red('破坏性变更')}`, value: 'major' },
{ name: '自定义版本', value: 'custom' }
]
}
]);
let newVersion;
if (versionType === 'custom') {
const { version } = await inquirer.prompt([
{ type: 'input', name: 'version', message: '输入新版本号:', validate: v => !!semver.valid(v) }
]);
newVersion = version;
} else {
newVersion = semver.inc(this.currentVersion, versionType);
}
// 2. 确认版本
const { confirmed } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirmed',
message: `确定将版本从 ${this.currentVersion} 升级到 ${chalk.green(newVersion)}?`,
default: true
}
]);
if (!confirmed) return;
// 3. 更新版本号
await this.updateVersion(newVersion);
// 4. 生成CHANGELOG
await this.generateChangelog();
// 5. 运行测试
await this.runTests();
// 6. 构建
await this.build();
// 7. 提交代码
await this.commit(newVersion);
// 8. 创建Git标签
await this.createTag(newVersion);
// 9. 发布到NPM
await this.publish();
// 10. 推送到远程
await this.push();
console.log(chalk.green(`\n✅ 发布成功!版本 ${newVersion} 已发布\n`));
}
async updateVersion(version) {
this.pkg.version = version;
await fs.writeJson(this.packagePath, this.pkg, { spaces: 2 });
console.log(chalk.green(`✅ 版本更新: ${this.currentVersion} → ${version}`));
}
async generateChangelog() {
try {
await execa('conventional-changelog', ['-p', 'angular', '-i', 'CHANGELOG.md', '-s', '-r', '0']);
console.log(chalk.green('✅ CHANGELOG 生成完成'));
} catch (error) {
console.log(chalk.yellow('⚠️ CHANGELOG 生成失败,跳过'));
}
}
async runTests() {
console.log(chalk.cyan('\n🧪 运行测试...'));
try {
await execa('npm', ['test']);
console.log(chalk.green('✅ 测试通过'));
} catch (error) {
console.log(chalk.red('❌ 测试失败'));
process.exit(1);
}
}
async build() {
console.log(chalk.cyan('\n🔨 构建项目...'));
try {
await execa('npm', ['run', 'build']);
console.log(chalk.green('✅ 构建成功'));
} catch (error) {
console.log(chalk.red('❌ 构建失败'));
process.exit(1);
}
}
async commit(version) {
console.log(chalk.cyan('\n📝 提交代码...'));
try {
await execa('git', ['add', '.']);
await execa('git', ['commit', '-m', `chore(release): ${version}`]);
console.log(chalk.green('✅ 提交成功'));
} catch (error) {
console.log(chalk.red('❌ 提交失败'));
}
}
async createTag(version) {
try {
await execa('git', ['tag', `v${version}`]);
console.log(chalk.green(`✅ 标签创建: v${version}`));
} catch (error) {
console.log(chalk.red('❌ 标签创建失败'));
}
}
async publish() {
console.log(chalk.cyan('\n📤 发布到NPM...'));
const { otp } = await inquirer.prompt([
{ type: 'input', name: 'otp', message: '输入NPM OTP (如果需要):' }
]);
try {
const args = ['publish'];
if (otp) args.push('--otp', otp);
await execa('npm', args);
console.log(chalk.green('✅ 发布成功'));
} catch (error) {
console.log(chalk.red('❌ 发布失败'));
process.exit(1);
}
}
async push() {
console.log(chalk.cyan('\n📡 推送到远程仓库...'));
try {
await execa('git', ['push', 'origin', 'main', '--tags']);
console.log(chalk.green('✅ 推送成功'));
} catch (error) {
console.log(chalk.red('❌ 推送失败'));
}
}
}
// 执行发布
const release = new ReleaseManager();
release.run().catch(console.error);
🎯 今日挑战
实现自定义脚手架工具,要求:
- 支持
create-xxx命令创建项目 - 可选的模板(React/Vue/Node)
- 支持TypeScript配置选项
- 自动安装依赖
- 初始化Git仓库
- 生成配置文件(ESLint/Prettier/tsconfig)
# 使用示例
npx create-my-app my-project --template react --typescript --eslint
# 输出
✅ 创建项目文件夹
✅ 克隆模板
✅ 安装依赖
✅ 配置TypeScript
✅ 配置ESLint
✅ Git初始化成功
🚀 项目创建完成!cd my-project && npm start
明日预告:Web Worker 深度应用 - 多线程前端计算与高性能数据处理
💡 工程化箴言:"自动化一切重复劳动"——工程师的时间应该花在解决问题,而不是重复配置!