你真的了解webpack中的loader么?

709 阅读3分钟

什么是Loader

webpack官网原话:

webpack only understands JavaScript and JSON files. Loaders allow webpack to process other types of files and convert them into valid modules that can be consumed by your application and added to the dependency graph.(简单说就是webpack只支持js和json,对于不支持的其类型文件的处理就需要依赖各种Loader来转化文件为webpack支持的模块了。嗯,Loader 字面意思不就是加载器么)

曾经了解的loader

为不同资源类型配置正确的loader很重要!X 3(重要的事说三遍),使用webpack打包项目时需要为js(x), less等配置对应的loader,还有其他资源对应的loader,参照配置的格式,选用对的loader就可以,唯一需要注意的是loader的顺序是从右到左的。so easy,参考官网哐哐哐就可以配置可以使用的loader了。

//webpack.config.js
    ...
    module: {
        rules: [
            {
                test: /\.jsx?$/,
                use: [
                    {
                        loader: 'thread-loader',
                        options: {
                            workers: 3
                        }
                    },
                    'babel-loader?cacheDirectory=true'
                ]
            },
            {
                test: /\.less$/,
                use: [
                    'style-loader',
                    'css-loader',
                    'less-loader',
                    'postcss-loader',
                    {
                        loader: 'px2rem-loader',
                        options: {
                            remUnit: 75,
                            percisson: 8
                        }
                    }
                ]
            }
            ...
        ]
    },

揭开loader的神秘面纱

loader其实就是一个函数,接收一个字符串,返回一个字符串(注意格式,需要是个模块格式)。webpack中loader runner会调用这个loader(函数),并且把前一个loader的返回作为入参传入当前loader,loader 函数内的会被webpack和loader runner注入上下文,有很多有用的 API,可以使用this返回,如this.callback, this.async等。

/**
 *
 * @param {string|Buffer} content Content of the resource file
 * @param {object} [map] SourceMap data consumable by https://github.com/mozilla/source-map
 * @param {any} [meta] Meta data, could be anything
 */
function webpackLoader(content, map, meta) {
  // 转换逻辑
  return `export default ${JSON.stringify(content)}`
}

同步loader 和异步loader

同步的loader如果只有一个返回结果,可以使用return或者this.callback的方式,如果有多个返回则只能使用this.callback。⚠️使用this.callback之后需要返回undefined (return;)

// 主要关注 err 和 content
this.callback(
  err: Error | null, // 用于报错
  content: string | Buffer,// 返回内容
  sourceMap?: SourceMap, // sourcemap
  meta?: any // meta
);

function webpackLoader(content, map, meta) {
  // 转换逻辑
  this.callback(null, content, map, meta);
  return; //always return undefined when calling callback()
}

异步loader的返回则需要先调用this.async()(通知loader-runner需要异步调用)并返回this .callback。

function webpackLoader(content, map, meta) {
    const callback = this.async();
    // 转换逻辑
    // 异步操作  
    someAsyncOperation(content, function (err, result) {
    if (err) return callback(err);
    callback(null, result, map, meta);
  });
  
}

如何生成文件

使用loader的场景往往需要生成文件,比如对babel-loader对jsx的处理转换,less-loader对less文件处理都需要生成文件,我们可以直接使用this.emitFile生成文件,是由webpack注入的方法。

emitFile(name: string, content: Buffer|string, sourceMap: {...})
// name 文件的路径
// content 保存的内容

loader-runner 可用于调试

runLoaders的两个参数,第一个是配置options,另一个是回调函数,用于接收loader的返回和异常信息。

const loaderRunner = require('loader-runner');
const path = require('path');

// 注意使用runLoaders, loader中无法使用this.emitFile生成文件,需要用fs.writeFile
loaderRunner.runLoaders({
    resource: path.join(__dirname, 'source.txt'), // 目标文件绝对路径
    loaders: [
        {
            loader: path.join(__dirname, 'index.js'),// loader绝对路径
            options: {name: 'hello'}// 传入的参数,loader中可以使用loader-utils获取
        }
     ],
     context: { minimize: true },// 支持注入上下文, loader中可以通过this.minimize 获取
}, (err, result) => {
    // err loader中的异常信息
    err && console.log(err);
    console.log('result', result);
})

options参数中的context支持传入上下文对象作为loader的上下文,webpack源码中也是通过该方式注入

image.png 重点来说一下回调函数中的两个参数,err和result。err是loader的异常,如果loader中直接throw new Error('error') 或者this.callback(new Error('error')),那么将会传到err中。 第二个参数result是包含一些返回结果相关的信息,主要看result.result,存放的是返回的结果,来个简单的demo吧

// 需要安装loader-runner 和loader-utils
//source.txt
hello world

//demo-loader.js
const loaderUtils = require('loader-utils');
// loader 格式 (source: string) => string
module.exports = function(source) {
    const { name } = loaderUtils.getOptions(this);
    console.log('name', name)
    console.log('loader is working, but do nothing');
    return `export default ${JSON.stringify(source)}`;
}

// test.js
const loaderRunner = require('loader-runner');
const path = require('path');

// 使用runLoaders 无法使用emitFile生成文件
loaderRunner.runLoaders({
    resource: path.join(__dirname, 'source.txt'),
    loaders: [{loader: path.join(__dirname, 'index.js'), options: {name: 'hello'}}]
}, (err, result) => {
    err && console.log(err);
    console.log('result', result);
})

image.png

以下是copy来的备注

	// err: Error?

	// result.result: Buffer | String 此处是个数组
	// The result
	// only available when no error occured

	// result.resourceBuffer: Buffer
	// The raw resource as Buffer (useful for SourceMaps)
	// only available when no error occured

	// result.cacheable: Bool
	// Is the result cacheable or do it require reexecution?

	// result.fileDependencies: String[]
	// An array of paths (existing files) on which the result depends on

	// result.missingDependencies: String[]
	// An array of paths (not existing files) on which the result depends on

	// result.contextDependencies: String[]
	// An array of paths (directories) on which the result depends on

使用 loader-utils 开发loader

loade-utils有很多有用的方法可以在开发loader时提供便利,介绍几个常用的方法

  1. getOptions可以用于获取传入的options,如果你使用了webpack5,也可以直接使用this.getOptions(schema)。
//loader config
loaders: [
    {
        loader: 'xxx-loader',
        options: {
            //any option
        }
    }
 ]
// your-loader
const options = loaderUtils.getOptions(this);
  1. parseQuery用于解析loader的this.resourceQuery(?a&b&c)
//loader config
loaders: ['xxx-loader?param=hellowrold']
// your-loader
const { param } = loaderUtils.parseQuery(this.resourceQuery);
console.log(param);// helloworld

解析规则如下

?                            -> {}
?flag                        -> { flag: true }
?+flag                       -> { flag: true }
?-flag                       -> { flag: false }
?xyz=test                    -> { xyz: "test" }
?xyz=1                       -> { xyz: "1" } // numbers are NOT parsed
?xyz[]=a                     -> { xyz: ["a"] }
?flag1&flag2                 -> { flag1: true, flag2: true }
?+flag1,-flag2               -> { flag1: true, flag2: false }
?xyz[]=a,xyz[]=b             -> { xyz: ["a", "b"] }
?a%2C%26b=c%2C%26d           -> { "a,&b": "c,&d" }
?{data:{a:1},isJSON5:true}   -> { data: { a: 1 }, isJSON5: true }
  1. interpolateName 是个功能强大的函数,用于文件名插值,在生成文件时很有用。
const interpolatedName = loaderUtils.interpolateName(loaderContext, name, options);
// loaderContext 传入loader的上下文(this)
// name 文件名模板,支持插值
// options 传入 {content: 'your content'}

重点介绍第二个参数name,支持多种插值,可以获取的当前处理的文件的文件名,扩展名,路径,内容哈希等。

[ext] 文件扩展名
[name] 文件名
[path] 文件路径
[folder] 项目文件夹
[query]  获取到this.resourceQuery 如?foo=bar
[emoji] 随机的emoji表情(基于文件内容),手动666
[emoji:<length>] 还能支持插入多个表情,只要配置好length
[contenthash] 返回options.content的hash
[hash] 返回options.content的hash,目前和[contenthash]一样,推荐使用[contenthash]
[<hashType>:(conten)thash:<digestType>:<length>] (content)hash支持更复杂的配置
. hashTypes, i. e. sha1, md4, md5, sha256, sha512 (默认为md4)
. digestTypes, i. e. hex, base26, base32, base36, base49, base52, base58, base62, base64 (默认使用hex)
. length 截取hash长度,例如在[contenthash:8]最后截取了前8位hash值

updating...