Webpack 2024 前前端架构老鸟的分享(三)
七、Webpack 自定义 Loader 的实践
7.1 Loader 基础
Loader 是一个导出为函数的 JavaScript 模块,用于对模块的源代码进行转换。 Loader 函数接受源码作为参数,可以通过 return 的方式将转换结果返回给 Webpack。 它可以实现各种功能,如代码压缩、代码转译、静态资源处理等,是 Webpack 构建过程中的重要组成部分。 通过编写自定义的 Loader,可以根据项目需求灵活地对源代码进行处理,从而满足特定的开发需求。 Loader 的执行顺序由配置中的顺序决定,可以通过配置文件或内联方式指定 Loader 的使用顺序和参数。
//basetransfer-loader.js
// 导出一个函数,该函数接收源代码作为参数
module.exports = function(source) {
// 对源码进行转换,这里使用正则表达式将换行符替换为空字符串,实现去除换行的功能
const transformedSource = source.replace(/\n/g, '');
// 返回转换后的源码
return transformedSource;
}
测试使用(下面的示例测试方式皆如此不再重复)
使用Webpack 2024 前前端架构老鸟的分享(一)的demo
中使用上面的基础自定义loader:
1、在项目的根目录下打开终端命令
2、使用 npm(Node.js 包管理器)或 yarn(另一种常用的包管理器)安装自定义 loader。假设你已经创建了一个名为 basetransfer-loader
的文件夹,并在其中包含了你提供的 loader 文件。使用以下命令安装:
npm install ./basetransfer-loader
或者使用 yarn 安装:
yarn add ./basetransfer-loader
这将会把 basetransfer-loader
安装到项目的 node_modules
文件夹中。
3、在 webpack 的配置文件中,像使用其他 loader 一样使用你的自定义 loader。假设你希望在处理 JavaScript 文件时使用这个 loader,在 webpack 配置文件中的 module.rules
部分添加如下配置:
module.exports = {
// 其他配置...
module: {
rules: [
{
test: /\.js$/, // 匹配 JavaScript 文件
use: 'basetransfer-loader', // 使用 basetransfer-loader 进行处理
enforce: 'pre' // 确保在其他 loader 之前执行
},
// 其他规则...
]
}
};
这个配置会告诉 webpack 在处理 JavaScript 文件之前先使用 basetransfer-loader 这个 loader。
4、这样,你的自定义 loader 就会被安装到本地项目中,并且可以在 webpack 构建过程中使用了。
新手小白Q&A
webpack小白提问:”自定义loader需要手动引入到配置文件吗”
老鸟回答:”对于这个简单的自定义 loader,不需要显式地引入其他模块或文件,因为它只是对源码进行简单的处理,并没有依赖于其他模块。
所以在 webpack 配置文件中,只需要指定 loader 的名称即可,webpack 会自动从 node_modules 中找到对应的 loader 文件并加载。
因此,在 webpack 配置中直接指定 basetransfer-loader 即可,不需要额外的引入。”
7.2 常用 Loader 开发实战
通过上面的简单介绍想必你对webpack 自定义loader已经有一个基本的认知了,下面跟我这我一起来动手试试,定义自己的loader。
自动 import/require 转换 Loader
// import-export-loader.js
// 定义模块导入和导出数组
const imports = [];
const exports = [];
// 正则表达式匹配模块导入和导出语句
const importRegExp = /import\s+(.+)\s+from\s+['"](.+)['"]/g;
const exportRegExp = /export\s+(.+)/g;
// 替换源代码中的模块导入语句并将匹配结果存入imports数组
source = source.replace(importRegExp, (match, names, path) => {
imports.push(`const { ${names} } = require('${path}');`);
return '';
});
// 替换源代码中的模块导出语句并将匹配结果存入exports数组
source = source.replace(exportRegExp, (match, names) => {
exports.push(`exports.${names} = ${names};`);
return '';
});
// 返回处理后的源代码,包括模块导入、源代码主体、模块导出
return `${imports.join('\n')}\n\n${source}\n\n${exports.join('\n')}`;
说明:
相信你看到上面的内容感觉明白了但是又不是很通透下面我来给你讲解清楚每一步的含义和思考。
首先这个自定义 loader 的作用是将 ES6 的模块导入导出语法转换为 CommonJS 的 require 和 exports 语法:
-
- 定义模块导入和导出数组:
const imports = []; // 用于存储模块导入语句 const exports = []; // 用于存储模块导出语句
这里创建了两个空数组,分别用来存储将要生成的模块导入和导出语句。
-
- 正则表达式匹配模块导入和导出语句:
const importRegExp = /import\s+(.+)\s+from\s+['"](.+)['"]/g; // 匹配 import 语句 const exportRegExp = /export\s+(.+)/g; // 匹配 export 语句
这里定义了两个正则表达式,用于匹配 ES6 模块导入和导出语句。
-
- 替换源代码中的模块导入语句并将匹配结果存入 imports 数组:
source = source.replace(importRegExp, (match, names, path) => { imports.push(`const { ${names} } = require('${path}');`); // 将匹配到的导入语句转换为 require 形式并存入数组 return ''; // 将源代码中的 import 语句替换为空字符串 });
这里使用
source.replace()
方法,根据正则表达式匹配源代码中的 import 语句,将其转换为 CommonJS 的 require 形式,并存入 imports 数组中。 -
- 替换源代码中的模块导出语句并将匹配结果存入 exports 数组:
source = source.replace(exportRegExp, (match, names) => { exports.push(`exports.${names} = ${names};`); // 将匹配到的导出语句存入数组 return ''; // 将源代码中的 export 语句替换为空字符串 });
这里同样使用
source.replace()
方法,根据正则表达式匹配源代码中的 export 语句,将其转换为 CommonJS 的 exports 形式,并存入 exports 数组中。 -
- 返回处理后的源代码:
return `${imports.join('\n')}\n\n${source}\n\n${exports.join('\n')}`;
最后,将生成的模块导入、源代码主体和模块导出拼接起来,返回给 webpack 处理下一个 loader。
这样,当这个自定义 loader 被应用到 webpack 构建过程中时,它会将源代码中的 ES6 模块语法转换为 CommonJS 形式,使得代码可以在不支持 ES6 模块的环境中正常运行。
小结
因为本人比较辣第一个demo 和基础部分讲解会比较详细,下面demo注释基本上是逐行注释的,希望你带着上面学习的内容去理解下面的几个自定义loader,如果暂时不是很明白没关系,多看两遍上面的内容动手去尝试你就会明白下面的这些自定义loader的思路了,😄😄,🦀🦀。
7.3 更多的示例练习
自动样式前缀 Loader
// autoprefixer-loader.js
// 自动添加浏览器前缀的加载器
const autoprefixer = require('autoprefixer');
const postcss = require('postcss');
module.exports = function(source) {
// 导入 autoprefixer 和 postcss 模块
const options = {
browsers: ['last 2 versions']
};
// 设置 autoprefixer 的选项,指定要兼容的浏览器版本为最近的两个版本
const processed = postcss([autoprefixer(options)]).process(source, { from: undefined });
// 使用 postcss 处理传入的源代码,并使用 autoprefixer 添加浏览器前缀
return processed.css;
// 返回处理后的 CSS 代码
}
自动添加 CSS Modules Loader
// css-modules-loader.js
// 导入必要的工具库
const loaderUtils = require('loader-utils');
const path = require('path');
const fs = require('fs');
// 导出 loader 函数,接受源代码 source 作为参数
module.exports = function(source) {
// 获取 loader 的选项,如果没有则使用默认空对象
const options = loaderUtils.getOptions(this) || {};
// 根据资源路径获取模块名称,默认使用文件名(不带后缀)
const moduleName = options.name || path.basename(this.resourcePath, '.css');
// 创建空对象用于存储 CSS 类名映射关系
const json = {};
// 使用正则表达式替换源代码中的 CSS 类名,并生成对应的映射关系
const replacedSource = source.replace(/.(\w+)(\s*{)/g, (match, className, brackets) => {
// 根据选项中的前缀设置 CSS 类名前缀
const classNamePrefix = options.prefix ? `${options.prefix}-${moduleName}` : moduleName;
// 生成新的唯一 CSS 类名
const newClassName = `${classNamePrefix}-${className}`;
// 将原始 CSS 类名与新生成的 CSS 类名建立映射关系
json[`.${className}`] = newClassName;
// 返回替换后的 CSS 类名
return `.${newClassName}${brackets}`;
});
// 根据模块名称生成对应的 JSON 文件路径
const jsonPath = path.resolve(this.context, `${moduleName}.css.json`);
// 将 CSS 类名映射关系写入 JSON 文件
fs.writeFileSync(jsonPath, JSON.stringify(json));
// 返回替换后的源代码
return replacedSource;
}
自动添加 Base64 Loader
// base64-loader.js
// 导入必要的工具包
const loaderUtils = require('loader-utils'); // 从 loader-utils 中导入工具函数
const mime = require('mime'); // 导入 mime 库,用于获取资源的 MIME 类型
// 导出 loader 函数,接收源代码作为参数
module.exports = function(source) {
// 获取 loader 的配置选项,若没有配置则使用空对象
const options = loaderUtils.getOptions(this) || {};
// 设置 base64 转换的字节限制,默认为 8192 字节
const limit = options.limit || 8192;
// 如果源代码长度超过了限制,直接返回源代码
if (source.length > limit) {
return source;
}
// 获取资源的 MIME 类型
const mimetype = mime.getType(this.resourcePath);
// 将源代码转换为 base64 编码格式,并拼接成 data URI
const base64 = `data:${mimetype};base64,${source.toString('base64')}`;
// 返回一个 JavaScript 模块,其中内容为 base64 编码后的资源
return `module.exports = '${base64}'`;
}
自动添加 SourceMap Loader
// sourcemap-loader.js
// 这是一个自定义的webpack loader,用于处理JavaScript文件及其对应的源映射文件
const loaderUtils = require('loader-utils'); // 引入loader-utils模块,用于处理loader的参数
const path = require('path'); // 引入path模块,用于处理文件路径
const fs = require('fs'); // 引入fs模块,用于读取文件内容
// 导出一个函数作为loader的处理函数
module.exports = function(source, sourceMap) {
// 获取loader的参数,如果没有参数则设为空对象
const options = loaderUtils.getOptions(this) || {};
// 获取当前处理的文件路径相对于webpack上下文的相对路径
const sourcePath = path.relative(this.context, this.resourcePath);
// 如果没有提供源映射,则从文件中读取源码内容,并创建一个源映射对象
if (!sourceMap) {
const sourceContent = fs.readFileSync(this.resourcePath, 'utf-8');
sourceMap = this.sourceMap || {
version: 3,
sources: [sourcePath],
sourcesContent: [sourceContent]
};
}
// 创建一个新的源映射对象,确保其中的路径使用标准化的形式
const newSourceMap = { ...sourceMap };
newSourceMap.sources = newSourceMap.sources.map(source => path.normalize(source));
// 调用webpack提供的callback函数,将处理后的源码及其源映射返回给下一个loader
this.callback(null, source, newSourceMap);
}
总结
好了各位,关于Webpack的知识我就分享到这里。
无论你们对Webpack爱与恨,我都衷心希望它能在你们的项目中发挥应有的威力。
对了,最后给你们几个使用Webpack的小贴士:
第一,配置文件别看着就头疼,其实很简单;
第二,出了错别着急,stack trace很有用;
第三,永远相信那个绿色的进度条!
至此,《Webpack 2024 前前端架构老鸟的分享(一)(二)(三)》三部曲就结束啦。很快会将三篇合并出一篇《Webpack 2024 前前端架构老鸟的分享(总篇)》方便有时间的同学通篇学习,三部曲是为了碎片学习的同学撰写。创作不易,希望看完留言点赞收藏🦀🦀。
原文链接:https://juejin.cn/post/7358136994331885609 作者:吴佳浩