loader承担的是翻译官的职责,利用其弥补了让webpack只能理解JavaScript和JSON文件的问题,从而可以处理其它类型的文件,所以loader对webpack的重要性不言而喻,所以学习构建一个loader是学习webpack的必经之路。在学习编写一个loader之前,要明确一下loader的职责:其职责是单一的,只需要完成一种转换。下面将逐步阐述选择loader开发中的几个关键点并实现一个loader。
一、Loader分类
loader是一个CommonJs风格的函数,接收输入的source后可通过同步或异步的方式进行处理,然后将内容进行输出。
1.1 同步Loader
同步loader指的是同步的返回转换后的内容。由于是在Node.js这样的单线程环境,所以转换过程会阻塞整个构建,构建缓慢,不适用于耗时较长的环境中。对于同步loader,主要有两种方法返回转换后的内容:return和this.callback.
- return
利用return可直接返回转换后结果。
module.exports = function(source, map, meta){ // ... // output为处理后结果 return output; }
- this.callback
该方法相比于return更加灵活,其参数主要有四个:
this.callback( err: Error | null, content: string | Buffer, sourceMap?: SourceMap, meta?: any );
(1)第一个参数为无法转换原内容,Webpack会返回一个Error。
(2)第二个参数即为经过转换后的内容(为输出的内容)。
(3)指与编译后代码所映射的源代码,便于调试。为了在此loader中获取该sourceMap,则需要在创建的webpack做一下配置(以js为例,babel-loader会将基础ES6语法进行转换为ES5,通过devtool可以开启source-map):
// webpack.config.js module.exports = { // ... module: { rules: [ { test: /\.js$/, use: [ 'test-loader',// 该loader即为自己构建的loader { loader: 'babel-loader', options: { presets: [ '@babel/preset-env' ] } } ] } ] }, devtool: 'eval-source-map', }
(4)可以是任何东西,输出该参数,即可在下一个loader中获取并使用,例如通过各loader之间共享通用的AST,加速编译时间。
利用this.callback可返回传递多参数的结果。
module.exports = function(source, map, meta) { // 处理后获得的结果output const output = dealOperation(source); this.callback(null, output, map, meta); }
1.2 异步Loader
同步loader只适合于计算量小,速度快的场景,但是对于计量量大、耗时比较长的场景(例如网络请求),使用同步loader会阻塞整个构建过程,导致构建速度变慢,采用异步loader即可避免该问题。对于异步loader,使用this.async()可以获取到callback函数,该函数参数和同步loader中this.callback参数一致。
module.exports = function(content, map, meta) { // 获取callback函数 const callback = this.async(); // 用setTimeout模拟该异步过程 setTimeout(() => { // 处理后获得的结果output const output = dealOperation(source); callback(null, output, map, meta); }, 100) }
二、文件转化后类型
默认情况下,资源文件经转化后都是UTF-8格式编码的字符串,但是对于图片这样的文件经过转化后是二进制格式的内容,为了让loader支持接收二进制资源,需要设置raw(以图片资源为例进行展示)
// webpack.config.js module.exports = { // ... module: { rules: [ { test: /\.(png|jpg|gif)$/, use: [ 'url-loader', 'raw-test-loader',// 自己的loader ] } ] } }
// raw-test-loader.js module.exports = function(source, map, meta) { // 处理输入的资源 const output = dealOperation(source); return output; } // 通过该属性告诉webpack该loader是否需要二进制数据 module.exports.raw = true;
三、options选项
对于webpack配置中,loader往往有一些options参数,对于自己编写的loader中为了获取options参数,官方推荐使用loader-utils包,利用该包即可获取options中参数,然后在loader中进行处理。
const loaderUtils = require('loader-utils'); module.exports = function (source, map, meta){ // 获取options const options = loaderUtils.getOptions(this); const output = dealOperation(source); return output; }
四、是否缓存
对于转换操作需要大量的计算,非常耗时,每次重新构建会让构建过程变的非常缓慢。webpack会默认缓存所有loader的处理结果,即要处理文件和其相关依赖没发生变化就会利用其缓存(注意loader除了this.addDependency里指定的依赖外,不应该有任何外部依赖)。通过this.cacheable可控制其是否进行缓存。
module.exports = function(source, map, meta) { // 关闭缓存 this.cacheable(false); return source; }
五、实现一个loader
本节是loader实战,编写了一个用于字母大小写转换的loader,利用该loader能够实现将txt文件中字母的大小写转换,其loader内容及webpack.config.js相关配置如下所示(详细代码见github上代码)
// format-letters-loader.js const loaderUtils = require('loader-utils'); const Lowercase2Uppercase = 'L2U'; const Uppercase2Lowercase = 'U2L'; module.exports = function (source, map, meta) { let output = ''; // 获取options const options = loaderUtils.getOptions(this); const { formatType } = options; switch(formatType) { case Lowercase2Uppercase: { output = source.toUpperCase(); break; } case Uppercase2Lowercase: { output = source.toLowerCase(); break; } default: { output = source; } } this.callback(null, output, map, meta); };
// webpack.config.js module.exports = { // ... module: { rules: [ { exclude: /\.(css|js|html|png|jpg|gif)$/, use: [ { loader: 'file-loader', options: { name: '[name].[ext]', outputPath: 'asset', } }, { loader: 'format-letters-loader', options: { formatType: 'U2L' } }, ] } ] }, // 解析loader包是设置模块如何被解析 resolveLoader: { modules: ['./node_modules', './loader'],// 告诉 webpack 解析loader时应该搜索的目录。 }, }