目录
- [1. Webpack 基础概念](#1. Webpack 基础概念 "#1-webpack-%E5%9F%BA%E7%A1%80%E6%A6%82%E5%BF%B5")
- [2. 模块化支持 (CJS/ESM)](#2. 模块化支持 (CJS/ESM) "#2-%E6%A8%A1%E5%9D%97%E5%8C%96%E6%94%AF%E6%8C%81-cjsesm")
- [3. 自定义 Loader 开发](#3. 自定义 Loader 开发 "#3-%E8%87%AA%E5%AE%9A%E4%B9%89-loader-%E5%BC%80%E5%8F%91")
- [4. 自定义 Plugin 开发](#4. 自定义 Plugin 开发 "#4-%E8%87%AA%E5%AE%9A%E4%B9%89-plugin-%E5%BC%80%E5%8F%91")
- [5. 实战案例](#5. 实战案例 "#5-%E5%AE%9E%E6%88%98%E6%A1%88%E4%BE%8B")
- [6. 最佳实践](#6. 最佳实践 "#6-%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5")
1. Webpack 基础概念
1.1 什么是 Webpack
Webpack 是一个现代 JavaScript 应用程序的静态模块打包工具。当 webpack 处理应用程序时,它会在内部构建一个依赖图,此依赖图对应映射到项目所需的每个模块,并生成一个或多个 bundle。
1.2 核心概念
- Entry(入口): 指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始
- Output(输出): 告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件
- Loader(加载器): 让 webpack 能够去处理那些非 JavaScript 文件
- Plugin(插件): 用于执行范围更广的任务,从打包优化和压缩,一直到重新定义环境中的变量
- Mode(模式): 通过选择 development 或 production 之中的一个,来设置 mode 参数
1.3 基本配置示例
javascript
// webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [],
};
2. 模块化支持 (CJS/ESM)
2.1 模块化概述
现代 JavaScript 开发中,模块化是构建大型应用的基础。Webpack 支持两种主要的模块化方式:
- CommonJS (CJS): Node.js 传统的模块化方式
- ES Modules (ESM): ECMAScript 标准的模块化方式
2.2 CommonJS (CJS) 方式
2.2.1 基本语法
javascript
// 导出模块
module.exports = {
name: 'MyModule',
version: '1.0.0',
sayHello: function() {
console.log('Hello from CJS module');
}
};
// 或者导出单个函数
module.exports = function() {
console.log('Hello from CJS function');
};
// 导出多个内容
exports.name = 'MyModule';
exports.version = '1.0.0';
exports.sayHello = function() {
console.log('Hello from CJS exports');
};
2.2.2 导入模块
javascript
// 导入整个模块
const myModule = require('./my-module');
// 导入特定属性
const { name, version } = require('./my-module');
// 导入函数
const myFunction = require('./my-function');
2.2.3 CJS Loader 示例
javascript
// cjs-loader.js
module.exports = function(source) {
const options = this.getOptions();
// 处理 CommonJS 模块
let result = source;
// 添加模块信息
if (options.addModuleInfo) {
result = `
// 模块信息 (由 cjs-loader 生成)
// 文件路径: ${this.resourcePath}
// 处理时间: ${new Date().toISOString()}
${result}
`;
}
// 转换 require 为 import (可选)
if (options.convertToESM) {
result = result.replace(
/const\s+(\w+)\s*=\s*require\(['"]([^'"]+)['"]\)/g,
'import $1 from "$2"'
);
result = result.replace(
/module\.exports\s*=/g,
'export default'
);
}
return result;
};
2.3 ES Modules (ESM) 方式
2.3.1 基本语法
javascript
// 默认导出
export default {
name: 'MyModule',
version: '1.0.0',
sayHello: function() {
console.log('Hello from ESM module');
}
};
// 命名导出
export const name = 'MyModule';
export const version = '1.0.0';
export function sayHello() {
console.log('Hello from ESM function');
}
// 混合导出
const defaultExport = {
name: 'MyModule',
version: '1.0.0'
};
export { defaultExport as default, name, version };
2.3.2 导入模块
javascript
// 默认导入
import myModule from './my-module';
// 命名导入
import { name, version, sayHello } from './my-module';
// 混合导入
import myModule, { name, version } from './my-module';
// 重命名导入
import { name as moduleName } from './my-module';
// 整体导入
import * as myModule from './my-module';
2.3.3 ESM Loader 示例
javascript
// esm-loader.js
module.exports = function(source) {
const options = this.getOptions();
// 处理 ES Modules
let result = source;
// 添加模块信息
if (options.addModuleInfo) {
result = `
// 模块信息 (由 esm-loader 生成)
// 文件路径: ${this.resourcePath}
// 处理时间: ${new Date().toISOString()}
${result}
`;
}
// 转换 import 为 require (可选)
if (options.convertToCJS) {
// 处理默认导入
result = result.replace(
/import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g,
'const $1 = require("$2")'
);
// 处理命名导入
result = result.replace(
/import\s*{\s*([^}]+)\s*}\s+from\s+['"]([^'"]+)['"]/g,
(match, imports, modulePath) => {
const importList = imports.split(',').map(imp => imp.trim());
const destructure = importList.join(', ');
return `const { ${destructure} } = require("${modulePath}")`;
}
);
// 处理默认导出
result = result.replace(
/export\s+default\s+/g,
'module.exports = '
);
}
return result;
};
2.4 双模块化支持
2.4.1 package.json 配置
json
{
"name": "my-webpack-project",
"version": "1.0.0",
"main": "dist/index.cjs",
"module": "dist/index.esm.js",
"exports": {
".": {
"import": "./dist/index.esm.js",
"require": "./dist/index.cjs"
}
},
"type": "module",
"scripts": {
"build": "webpack --config webpack.config.js",
"build:cjs": "webpack --config webpack.cjs.config.js",
"build:esm": "webpack --config webpack.esm.config.js"
}
}
2.4.2 双模块化 Loader
javascript
// dual-module-loader.js
module.exports = function(source) {
const options = this.getOptions();
const moduleType = options.moduleType || 'auto';
// 自动检测模块类型
if (moduleType === 'auto') {
if (source.includes('import') || source.includes('export')) {
return this.processESM(source, options);
} else if (source.includes('require') || source.includes('module.exports')) {
return this.processCJS(source, options);
}
}
// 根据配置处理
if (moduleType === 'esm') {
return this.processESM(source, options);
} else if (moduleType === 'cjs') {
return this.processCJS(source, options);
}
return source;
};
// 处理 ESM 模块
module.exports.processESM = function(source, options) {
let result = source;
// 添加 ESM 特性
if (options.addTreeShaking) {
result = `
// Tree Shaking 优化标记
/*#__PURE__*/
${result}
`;
}
return result;
};
// 处理 CJS 模块
module.exports.processCJS = function(source, options) {
let result = source;
// 添加 CJS 特性
if (options.addStrictMode) {
result = `'use strict';\n\n${result}`;
}
return result;
};
2.5 模块化转换工具
2.5.1 CJS 转 ESM Loader
javascript
// cjs-to-esm-loader.js
module.exports = function(source) {
const options = this.getOptions();
let result = source;
// 转换 require 为 import
result = result.replace(
/const\s+(\w+)\s*=\s*require\(['"]([^'"]+)['"]\)/g,
'import $1 from "$2"'
);
result = result.replace(
/var\s+(\w+)\s*=\s*require\(['"]([^'"]+)['"]\)/g,
'import $1 from "$2"'
);
result = result.replace(
/let\s+(\w+)\s*=\s*require\(['"]([^'"]+)['"]\)/g,
'import $1 from "$2"'
);
// 转换 module.exports 为 export default
result = result.replace(
/module\.exports\s*=\s*/g,
'export default '
);
// 转换 exports.xxx 为 export const
result = result.replace(
/exports\.(\w+)\s*=\s*/g,
'export const $1 = '
);
// 添加严格模式
if (options.addStrictMode) {
result = `'use strict';\n\n${result}`;
}
return result;
};
2.5.2 ESM 转 CJS Loader
javascript
// esm-to-cjs-loader.js
module.exports = function(source) {
const options = this.getOptions();
let result = source;
// 转换 import 为 require
result = result.replace(
/import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g,
'const $1 = require("$2")'
);
// 转换命名导入
result = result.replace(
/import\s*{\s*([^}]+)\s*}\s+from\s+['"]([^'"]+)['"]/g,
(match, imports, modulePath) => {
const importList = imports.split(',').map(imp => imp.trim());
const destructure = importList.join(', ');
return `const { ${destructure} } = require("${modulePath}")`;
}
);
// 转换默认导出
result = result.replace(
/export\s+default\s+/g,
'module.exports = '
);
// 转换命名导出
result = result.replace(
/export\s+const\s+(\w+)/g,
'exports.$1'
);
result = result.replace(
/export\s+function\s+(\w+)/g,
'exports.$1 = function'
);
// 添加严格模式
if (options.addStrictMode) {
result = `'use strict';\n\n${result}`;
}
return result;
};
2.6 Webpack 配置支持
2.6.1 基础配置
javascript
// webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
library: {
type: 'umd', // 支持 CJS 和 ESM
name: 'MyLibrary'
}
},
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: path.resolve(__dirname, 'loaders/dual-module-loader.js'),
options: {
moduleType: 'auto',
addModuleInfo: true
}
}
]
}
]
}
};
2.6.2 多输出配置
javascript
// webpack.multi.config.js
const path = require('path');
// CJS 配置
const cjsConfig = {
mode: 'development',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index.cjs',
library: {
type: 'commonjs2'
}
},
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: path.resolve(__dirname, 'loaders/esm-to-cjs-loader.js'),
options: {
addStrictMode: true
}
}
]
}
]
}
};
// ESM 配置
const esmConfig = {
mode: 'development',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index.esm.js',
library: {
type: 'module'
}
},
experiments: {
outputModule: true
},
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: path.resolve(__dirname, 'loaders/cjs-to-esm-loader.js'),
options: {
addTreeShaking: true
}
}
]
}
]
}
};
module.exports = [cjsConfig, esmConfig];
2.7 最佳实践
2.7.1 模块化选择建议
- 新项目: 推荐使用 ESM,它是 JavaScript 的未来标准
- Node.js 项目: 可以使用 CJS 或 ESM(Node.js 12+ 支持)
- 浏览器项目: 推荐使用 ESM,支持 Tree Shaking
- 库开发: 建议同时支持 CJS 和 ESM
2.7.2 兼容性处理
javascript
// 兼容性 Loader
module.exports = function(source) {
const options = this.getOptions();
// 检测运行环境
const isNode = typeof process !== 'undefined' && process.versions && process.versions.node;
const isBrowser = typeof window !== 'undefined';
let result = source;
if (isNode && options.nodePolyfills) {
// 添加 Node.js polyfills
result = `
// Node.js polyfills
const path = require('path');
const fs = require('fs');
${result}
`;
}
if (isBrowser && options.browserPolyfills) {
// 添加浏览器 polyfills
result = `
// Browser polyfills
if (typeof global === 'undefined') {
window.global = window;
}
${result}
`;
}
return result;
};
3. 自定义 Loader 开发
3.1 Loader 基础概念
Loader 是 webpack 的核心概念之一,它允许 webpack 处理除了 JavaScript 之外的其他类型的文件。Loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块。
3.2 Loader 的特点
- 链式调用: Loader 可以链式调用,从右到左(或从下到上)执行
- 单一职责: 每个 Loader 只负责一个转换功能
- 无状态: Loader 应该是无状态的,不依赖于之前的转换结果
- 可配置: Loader 可以通过 options 进行配置
3.3 创建自定义 Loader
3.3.1 基本 Loader 结构
javascript
// my-loader.js
module.exports = function(source) {
// source 是文件内容
// 返回转换后的代码
return source;
};
3.3.2 带选项的 Loader
javascript
// my-loader.js
module.exports = function(source) {
const options = this.getOptions();
// 使用选项进行转换
const transformedSource = source.replace(
/console\.log\(/g,
`console.log('[${options.prefix || 'DEBUG'}]: `
);
return transformedSource;
};
3.3.3 异步 Loader
javascript
// async-loader.js
module.exports = function(source) {
const callback = this.async();
// 异步处理
setTimeout(() => {
const result = source.replace(/const/g, 'let');
callback(null, result);
}, 100);
};
3.4 实用 Loader 示例
3.4.1 注释移除 Loader
javascript
// comment-remover-loader.js
module.exports = function(source) {
// 移除单行注释
let result = source.replace(/\/\/.*$/gm, '');
// 移除多行注释
result = result.replace(/\/\*[\s\S]*?\*\//g, '');
// 移除多余的空行
result = result.replace(/\n\s*\n/g, '\n');
return result;
};
3.4.2 国际化 Loader
javascript
// i18n-loader.js
module.exports = function(source) {
const options = this.getOptions();
const locale = options.locale || 'zh-CN';
// 简单的国际化替换
const translations = {
'zh-CN': {
'Hello': '你好',
'World': '世界',
'Welcome': '欢迎'
},
'en-US': {
'Hello': 'Hello',
'World': 'World',
'Welcome': 'Welcome'
}
};
let result = source;
const currentTranslations = translations[locale] || translations['zh-CN'];
Object.keys(currentTranslations).forEach(key => {
const regex = new RegExp(`'${key}'|"${key}"`, 'g');
result = result.replace(regex, `'${currentTranslations[key]}'`);
});
return result;
};
3.4.3 代码统计 Loader
javascript
// code-stats-loader.js
module.exports = function(source) {
const options = this.getOptions();
// 统计代码行数
const lines = source.split('\n').length;
const characters = source.length;
const words = source.split(/\s+/).length;
// 生成统计信息
const stats = `
// 代码统计信息 (由 code-stats-loader 生成)
// 行数: ${lines}
// 字符数: ${characters}
// 单词数: ${words}
// 生成时间: ${new Date().toISOString()}
${source}
`;
if (options.verbose) {
console.log(`📊 文件统计: ${this.resourcePath}`);
console.log(` 行数: ${lines}`);
console.log(` 字符数: ${characters}`);
console.log(` 单词数: ${words}`);
}
return stats;
};
3.5 Loader 使用准则
3.5.1 简单性原则
- 每个 Loader 只做一件事
- 保持转换逻辑简单明了
3.5.2 链式调用
javascript
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [
'comment-remover-loader',
'i18n-loader?locale=zh-CN',
'code-stats-loader?verbose=true'
]
}
]
}
};
3.5.3 模块化
javascript
// 将复杂的 Loader 拆分为多个简单 Loader
// 1. 语法解析 Loader
// 2. 转换 Loader
// 3. 输出格式化 Loader
4. 自定义 Plugin 开发
4.1 Plugin 基础概念
Plugin 是 webpack 的支柱功能,用于执行范围更广的任务。Plugin 的目的在于解决 Loader 无法实现的其他功能。
4.2 Plugin 的特点
- 事件驱动: Plugin 通过监听 webpack 构建过程中的事件来工作
- 生命周期: 可以在构建的不同阶段执行不同的逻辑
- 配置化: 支持通过构造函数参数进行配置
- 异步支持: 支持异步操作和 Promise
4.3 创建自定义 Plugin
4.3.1 基本 Plugin 结构
javascript
// my-plugin.js
class MyPlugin {
constructor(options = {}) {
this.options = options;
}
apply(compiler) {
// 在 compiler 上注册钩子
compiler.hooks.done.tap('MyPlugin', (stats) => {
console.log('构建完成!');
});
}
}
module.exports = MyPlugin;
4.3.2 带配置的 Plugin
javascript
// configurable-plugin.js
class ConfigurablePlugin {
constructor(options = {}) {
this.options = {
name: 'Default Plugin',
outputFile: 'build-info.json',
...options
};
}
apply(compiler) {
const { name, outputFile } = this.options;
compiler.hooks.done.tap(name, (stats) => {
console.log(`${name} 执行完成`);
// 生成构建信息
const buildInfo = {
timestamp: new Date().toISOString(),
duration: stats.endTime - stats.startTime,
assets: Object.keys(stats.compilation.assets),
chunks: stats.compilation.chunks.length
};
// 这里可以写入文件或发送到服务器
console.log('构建信息:', buildInfo);
});
}
}
module.exports = ConfigurablePlugin;
4.4 实用 Plugin 示例
4.4.1 构建时间统计 Plugin
javascript
// build-time-plugin.js
class BuildTimePlugin {
constructor(options = {}) {
this.options = {
showDetails: true,
format: 'human', // 'human' | 'ms'
...options
};
}
apply(compiler) {
const startTime = Date.now();
compiler.hooks.done.tap('BuildTimePlugin', (stats) => {
const endTime = Date.now();
const duration = endTime - startTime;
let formattedDuration;
if (this.options.format === 'human') {
formattedDuration = this.formatDuration(duration);
} else {
formattedDuration = `${duration}ms`;
}
console.log(`⏱️ 构建耗时: ${formattedDuration}`);
if (this.options.showDetails) {
console.log(`📦 输出文件数: ${Object.keys(stats.compilation.assets).length}`);
console.log(`🔧 模块数: ${stats.compilation.modules.size}`);
}
});
}
formatDuration(ms) {
if (ms < 1000) {
return `${ms}ms`;
} else if (ms < 60000) {
return `${(ms / 1000).toFixed(2)}s`;
} else {
const minutes = Math.floor(ms / 60000);
const seconds = ((ms % 60000) / 1000).toFixed(2);
return `${minutes}m ${seconds}s`;
}
}
}
module.exports = BuildTimePlugin;
4.4.2 文件列表生成 Plugin
javascript
// file-list-plugin.js
const { RawSource } = require('webpack-sources');
class FileListPlugin {
constructor(options = {}) {
this.options = {
outputFile: 'file-list.md',
title: '# 构建文件列表',
...options
};
}
apply(compiler) {
const pluginName = 'FileListPlugin';
compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
compilation.hooks.processAssets.tap(
{
name: pluginName,
stage: compilation.PROCESS_ASSETS_STAGE_SUMMARIZE,
},
(assets) => {
// 生成文件列表内容
const content = this.generateFileList(assets);
// 添加新资源到构建输出
compilation.emitAsset(
this.options.outputFile,
new RawSource(content)
);
}
);
});
}
generateFileList(assets) {
const { title } = this.options;
let content = `${title}\n\n`;
content += `生成时间: ${new Date().toLocaleString()}\n\n`;
content += '## 文件列表\n\n';
Object.keys(assets).forEach(filename => {
const size = assets[filename].size();
const sizeFormatted = this.formatSize(size);
content += `- **${filename}** (${sizeFormatted})\n`;
});
return content;
}
formatSize(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];
}
}
module.exports = FileListPlugin;
4.4.3 环境变量注入 Plugin
javascript
// env-inject-plugin.js
const { RawSource } = require('webpack-sources');
class EnvInjectPlugin {
constructor(options = {}) {
this.options = {
envFile: '.env',
prefix: 'APP_',
...options
};
}
apply(compiler) {
const pluginName = 'EnvInjectPlugin';
compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
compilation.hooks.processAssets.tap(
{
name: pluginName,
stage: compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
},
(assets) => {
// 读取环境变量
const envVars = this.readEnvVars();
// 生成环境变量文件
const envContent = this.generateEnvFile(envVars);
compilation.emitAsset(
'env-config.js',
new RawSource(envContent)
);
}
);
});
}
readEnvVars() {
const envVars = {};
const prefix = this.options.prefix;
// 读取 process.env 中的环境变量
Object.keys(process.env).forEach(key => {
if (key.startsWith(prefix)) {
envVars[key] = process.env[key];
}
});
return envVars;
}
generateEnvFile(envVars) {
let content = '// 自动生成的环境变量配置\n';
content += 'window.ENV_CONFIG = {\n';
Object.keys(envVars).forEach(key => {
content += ` '${key}': '${envVars[key]}',\n`;
});
content += '};\n';
content += 'export default window.ENV_CONFIG;\n';
return content;
}
}
module.exports = EnvInjectPlugin;
4.4.4 异步编译 Plugin
javascript
// async-compilation-plugin.js
class AsyncCompilationPlugin {
constructor(options = {}) {
this.options = {
delay: 1000,
message: '异步任务完成',
...options
};
}
apply(compiler) {
// 使用 tapAsync 处理异步操作
compiler.hooks.emit.tapAsync(
'AsyncCompilationPlugin',
(compilation, callback) => {
console.log('开始异步任务...');
setTimeout(() => {
console.log(this.options.message);
// 添加自定义资源
compilation.emitAsset(
'async-task-complete.txt',
new (require('webpack-sources').RawSource)(
`异步任务完成时间: ${new Date().toISOString()}`
)
);
callback();
}, this.options.delay);
}
);
}
}
module.exports = AsyncCompilationPlugin;
4.4.5 模块化分析 Plugin
javascript
// module-analyzer-plugin.js
const { RawSource } = require('webpack-sources');
class ModuleAnalyzerPlugin {
constructor(options = {}) {
this.options = {
outputFile: 'module-analysis.json',
analyzeImports: true,
analyzeExports: true,
...options
};
}
apply(compiler) {
const pluginName = 'ModuleAnalyzerPlugin';
compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
compilation.hooks.processAssets.tap(
{
name: pluginName,
stage: compilation.PROCESS_ASSETS_STAGE_SUMMARIZE,
},
(assets) => {
// 分析模块依赖关系
const moduleAnalysis = this.analyzeModules(compilation);
// 生成分析报告
const analysisContent = JSON.stringify(moduleAnalysis, null, 2);
compilation.emitAsset(
this.options.outputFile,
new RawSource(analysisContent)
);
}
);
});
}
analyzeModules(compilation) {
const analysis = {
timestamp: new Date().toISOString(),
totalModules: compilation.modules.size,
modules: [],
dependencies: [],
moduleTypes: {
cjs: 0,
esm: 0,
mixed: 0
}
};
compilation.modules.forEach(module => {
if (module.resource) {
const moduleInfo = this.analyzeModule(module);
analysis.modules.push(moduleInfo);
// 统计模块类型
if (moduleInfo.hasImport && moduleInfo.hasRequire) {
analysis.moduleTypes.mixed++;
} else if (moduleInfo.hasImport) {
analysis.moduleTypes.esm++;
} else if (moduleInfo.hasRequire) {
analysis.moduleTypes.cjs++;
}
}
});
return analysis;
}
analyzeModule(module) {
const source = module._source ? module._source.source() : '';
return {
path: module.resource,
size: source.length,
hasImport: /import\s+/.test(source),
hasExport: /export\s+/.test(source),
hasRequire: /require\s*\(/.test(source),
hasModuleExports: /module\.exports/.test(source),
importCount: (source.match(/import\s+/g) || []).length,
exportCount: (source.match(/export\s+/g) || []).length,
requireCount: (source.match(/require\s*\(/g) || []).length
};
}
}
module.exports = ModuleAnalyzerPlugin;
4.4.6 模块化转换 Plugin
javascript
// module-transformer-plugin.js
const { RawSource } = require('webpack-sources');
class ModuleTransformerPlugin {
constructor(options = {}) {
this.options = {
targetModuleType: 'esm', // 'esm' | 'cjs'
transformImports: true,
transformExports: true,
...options
};
}
apply(compiler) {
const pluginName = 'ModuleTransformerPlugin';
compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
compilation.hooks.processAssets.tap(
{
name: pluginName,
stage: compilation.PROCESS_ASSETS_STAGE_OPTIMIZE,
},
(assets) => {
// 转换模块格式
Object.keys(assets).forEach(filename => {
if (filename.endsWith('.js')) {
const asset = assets[filename];
const source = asset.source();
const transformedSource = this.transformModule(source);
compilation.emitAsset(
filename,
new RawSource(transformedSource)
);
}
});
}
);
});
}
transformModule(source) {
let result = source;
if (this.options.targetModuleType === 'esm') {
result = this.transformToESM(result);
} else if (this.options.targetModuleType === 'cjs') {
result = this.transformToCJS(result);
}
return result;
}
transformToESM(source) {
let result = source;
if (this.options.transformImports) {
// 转换 require 为 import
result = result.replace(
/const\s+(\w+)\s*=\s*require\(['"]([^'"]+)['"]\)/g,
'import $1 from "$2"'
);
}
if (this.options.transformExports) {
// 转换 module.exports 为 export default
result = result.replace(
/module\.exports\s*=\s*/g,
'export default '
);
}
return result;
}
transformToCJS(source) {
let result = source;
if (this.options.transformImports) {
// 转换 import 为 require
result = result.replace(
/import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g,
'const $1 = require("$2")'
);
}
if (this.options.transformExports) {
// 转换 export default 为 module.exports
result = result.replace(
/export\s+default\s+/g,
'module.exports = '
);
}
return result;
}
}
module.exports = ModuleTransformerPlugin;
4.5 Plugin 钩子类型
4.5.1 同步钩子 (SyncHook)
javascript
class SyncHookPlugin {
apply(compiler) {
// 同步钩子,直接使用 tap
compiler.hooks.done.tap('SyncHookPlugin', (stats) => {
console.log('同步钩子执行完成');
});
}
}
4.5.2 异步钩子 (AsyncSeriesHook)
javascript
class AsyncHookPlugin {
apply(compiler) {
// 异步钩子,使用 tapAsync
compiler.hooks.emit.tapAsync('AsyncHookPlugin', (compilation, callback) => {
// 异步操作
setTimeout(() => {
console.log('异步钩子执行完成');
callback();
}, 1000);
});
// 或者使用 tapPromise
compiler.hooks.emit.tapPromise('AsyncHookPlugin', (compilation) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log('Promise 钩子执行完成');
resolve();
}, 1000);
});
});
}
}
4.5.3 瀑布钩子 (WaterfallHook)
javascript
class WaterfallHookPlugin {
apply(compiler) {
// 瀑布钩子,前一个返回值作为后一个的输入
compiler.hooks.optimizeChunkModules.tap(
'WaterfallHookPlugin',
(chunks, modules) => {
console.log('优化前的模块数:', modules.length);
return modules.filter(module => module.size > 1000); // 过滤大模块
}
);
}
}
5. 实战案例
5.1 完整的项目配置
javascript
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const BuildTimePlugin = require('./plugins/build-time-plugin');
const FileListPlugin = require('./plugins/file-list-plugin');
const EnvInjectPlugin = require('./plugins/env-inject-plugin');
const ModuleAnalyzerPlugin = require('./plugins/module-analyzer-plugin');
const ModuleTransformerPlugin = require('./plugins/module-transformer-plugin');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
clean: true,
library: {
type: 'umd', // 支持 CJS 和 ESM
name: 'MyLibrary'
}
},
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: path.resolve(__dirname, 'loaders/dual-module-loader.js'),
options: {
moduleType: 'auto',
addModuleInfo: true
}
},
{
loader: path.resolve(__dirname, 'loaders/comment-remover-loader.js'),
},
{
loader: path.resolve(__dirname, 'loaders/code-stats-loader.js'),
options: {
verbose: true,
},
},
],
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
}),
new BuildTimePlugin({
showDetails: true,
format: 'human',
}),
new FileListPlugin({
outputFile: 'build-files.md',
title: '# 项目构建文件列表',
}),
new EnvInjectPlugin({
prefix: 'APP_',
}),
new ModuleAnalyzerPlugin({
outputFile: 'module-analysis.json',
analyzeImports: true,
analyzeExports: true
}),
new ModuleTransformerPlugin({
targetModuleType: 'esm',
transformImports: true,
transformExports: true
}),
],
devServer: {
static: './dist',
hot: true,
},
};
5.2 测试文件
javascript
// src/index.js
// 这是一个测试文件,用于演示 Loader 和 Plugin 的功能
// 支持 CJS 和 ESM 两种模块化方式
// ESM 导入
import { formatDate } from './utils/date.js';
import { logger } from './utils/logger.js';
// CJS 导入
const path = require('path');
const fs = require('fs');
const message = 'Hello World'; // 这个注释会被移除
const welcome = 'Welcome to our app'; // 另一个注释
console.log(message);
console.log(welcome);
// 多行注释示例
/*
* 这是一个多行注释
* 它会被完全移除
*/
// 使用导入的模块
console.log('当前时间:', formatDate(new Date()));
logger.info('应用启动成功');
// ESM 导出
export default {
message,
welcome,
formatDate,
logger
};
// 命名导出
export const VERSION = '1.0.0';
export const AUTHOR = 'Webpack Learner';
// CJS 导出
module.exports.config = {
name: 'webpack-demo',
version: VERSION
};
javascript
// src/utils/date.js (ESM 模块)
export function formatDate(date) {
return date.toLocaleString('zh-CN');
}
export function getCurrentYear() {
return new Date().getFullYear();
}
export default {
formatDate,
getCurrentYear
};
javascript
// src/utils/logger.js (CJS 模块)
const colors = require('colors');
function info(message) {
console.log(colors.green(`[INFO] ${message}`));
}
function error(message) {
console.log(colors.red(`[ERROR] ${message}`));
}
function warn(message) {
console.log(colors.yellow(`[WARN] ${message}`));
}
module.exports = {
info,
error,
warn
};
html
<!-- src/index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Webpack 学习示例</title>
</head>
<body>
<div id="app">
<h1>Webpack 自定义 Loader 和 Plugin 示例</h1>
<p>打开控制台查看构建信息</p>
</div>
</body>
</html>
5.3 运行和测试
bash
# 安装依赖
npm install webpack webpack-cli html-webpack-plugin style-loader css-loader --save-dev
# 构建项目
npx webpack
# 启动开发服务器
npx webpack serve
6. 最佳实践
6.1 Loader 最佳实践
- 保持简单: 每个 Loader 只做一件事
- 链式调用: 利用 Loader 的链式特性
- 缓存友好: 确保转换结果可以被缓存
- 错误处理: 提供清晰的错误信息
- 文档化: 为 Loader 提供详细的使用文档
6.2 Plugin 最佳实践
- 事件驱动: 合理使用 webpack 的钩子系统
- 异步处理: 对于耗时操作使用异步钩子
- 资源管理: 正确管理构建资源
- 配置验证: 验证插件配置参数
- 性能优化: 避免在插件中执行耗时操作
6.3 调试技巧
javascript
// 调试 Loader
module.exports = function(source) {
console.log('Loader 输入:', source.substring(0, 100) + '...');
const result = source.replace(/console\.log/g, 'console.warn');
console.log('Loader 输出:', result.substring(0, 100) + '...');
return result;
};
// 调试 Plugin
class DebugPlugin {
apply(compiler) {
compiler.hooks.compilation.tap('DebugPlugin', (compilation) => {
console.log('编译开始');
compilation.hooks.optimize.tap('DebugPlugin', () => {
console.log('优化阶段');
});
compilation.hooks.afterOptimize.tap('DebugPlugin', () => {
console.log('优化完成');
});
});
}
}
6.4 性能优化
- 并行处理 : 使用
thread-loader
进行并行处理 - 缓存: 合理使用 webpack 的缓存机制
- 代码分割 : 使用
splitChunks
进行代码分割 - Tree Shaking: 启用 Tree Shaking 移除无用代码
- 压缩 : 使用
terser-webpack-plugin
进行代码压缩
6.5 常见问题解决
6.5.1 Loader 顺序问题
javascript
// 正确的 Loader 顺序
{
test: /\.scss$/,
use: [
'style-loader', // 最后执行
'css-loader', // 中间执行
'sass-loader' // 最先执行
]
}
6.5.2 Plugin 执行时机
javascript
// 选择合适的钩子时机
compiler.hooks.beforeCompile.tap('MyPlugin', () => {
// 编译前执行
});
compiler.hooks.afterCompile.tap('MyPlugin', (compilation) => {
// 编译后执行
});
compiler.hooks.emit.tap('MyPlugin', (compilation) => {
// 输出前执行
});
总结
通过本文的学习,你应该已经掌握了:
- Webpack 基础概念和核心原理
- 模块化支持 (CJS/ESM) 的实现方式和转换工具
- 自定义 Loader 开发的方法和最佳实践
- 自定义 Plugin 开发的技巧和钩子使用
- 实战案例的完整实现
- 性能优化和调试技巧
Webpack 的 Loader 和 Plugin 系统提供了强大的扩展能力,特别是对模块化的支持,让开发者可以灵活地在 CommonJS 和 ES Modules 之间进行转换和兼容。通过合理使用这些功能,可以大大提高开发效率和构建质量。记住要遵循单一职责原则,保持代码的简洁性和可维护性。
模块化支持要点
- CJS (CommonJS): 适用于 Node.js 环境和传统项目
- ESM (ES Modules): 现代 JavaScript 标准,支持 Tree Shaking
- 双模块化支持: 通过 Loader 和 Plugin 实现自动转换
- 兼容性处理: 确保在不同环境下的正常运行