Webpack 系列 - 手写webpack loader

479 阅读3分钟

上一篇文章从分析style-loader源码入手让大家了解了webpack loader 工作原理,这一篇咱们手写一个简单的webpack loader

url-loader这个loader想必大家都用过,小图片转base64。今天咱们就写一个简单版的url-loader

loader编写原则

我们先来回顾下loader编写原则

单一原则: 每个 Loader 只做一件事;
链式调用: Webpack 会按顺序链式调用每个 Loader
统一原则: 遵循 Webpack 制定的设计规则和结构,输入与输出均为字符串,各个 Loader 完全独立,即插即用;

基础点

1、缓存:提高执行效率上,如何处理利用缓存是极其重要的。 Webpack,Hot-Replace 以及 React Hot Loader 也充分地利用缓存来提高编译效率。 Webpack Loader 同样可以利用缓存来提高效率,并且只需在一个可缓存的 Loader 上加一句 this.cacheable(); 很多 Loader 都是可以缓存的,但也有例外。可以缓存的 Loader 需要具备可预见性,不变性等等。

2、异步:异步并不陌生,当一个 Loader 无依赖,可异步的时候我想都应该让它不再阻塞地去异步。在一个异步的模块中,回传时需要调用 Loader API 提供的回调方法 this.async(),使用起来也很简单

module.exports = function(source) {

    var callback = this.async();

    // 做异步的事

    doSomeAsyncOperation(content, function(err, result) {

        if(err) return callback(err);

        callback(null, result);

    });

};

3、pitching Loader : 一般 Loader 从右到左链式执行。这种说法实际说的是 Loader 中 module.exports 出来的执行方法顺序。在一些场景下,Loader 并不依赖上一个 Loader 的结果,而只关心原输入内容。这时候,从左到右执行并没有什么问题。在 Loader 的 module 中,可使用 module.exports.pitch = function(); pitch 方法在 Loader 中便是从左到右执行的,并且可以通过 data 这个变量来进行 pitch 和 normal 之间传递。具体的实践可以查看 style-loader,里面就有使用到 pitch。

module.exports.pitch = function(remaining, preceding, data) {

    if(somothingFlag()) {

        return "module.exports = require(" + JSON.stringify("-!" + remaining) + ");";

    }

    data.value = 1;

};

4、默认的情况,原文件是以 UTF-8 String 的形式传入给 Loader。module也可使用 buffer 的形式进行处理,针对这种情况,只需要设置 module.exports.raw = true; 这样内容将会以 raw Buffer 的形式传入到 loader 中了。

5、Loader 中的 this

  • data  pitch loader 中可以通过 data 让 pitch 和 normal module 进行数据共享。 
  • query  则能获取到 Loader 上附有的参数。 如 require("./somg-loader?ls"); 通过 query 就可以得到 "ls" 了。
  • emitFile  emitFile 能够让开发者更方便的输出一个 file 文件,这是 webpack 特有的方法,使用的方法也很直接

好啦,了解了上面你的基础点,我们就能来手写个简单的webpack loader了;

loader 配置

其实配置适合url-loader一样的, 匹配图片:

 {
    test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
    use: ['./my-loader/img-loader?limit=10240 '],
    include: [srcDir],
  }

编写自己的loader

这边只是给大家做个示范,就不上传到npm了。我们先在根目录创建img-loader.js文件,定义loader方法,然后导出,因为我们匹配的是图片,记住要用上面说过的 module.exports.raw = true; Buffer 的形式传入到 loader 中。

function loader(source) {
    return xxx
}

loader.raw = true; //把文件转成二进制流

module.exports = loader;

然后就开始填充我们loader的功能。首先载入工具库,为了后续使用。第一步先验证options是否符合类型,第二步获取参数,然后替换传入的资源文件字符串。按照这个思路,请看我们的代码

//获取参数的工具箱
const _loaderUtils = require("loader-utils");

const _schemaUtils = require("schema-utils");

//获取文件标识
const mime = require("mime");

const schema = {
    type: "object",
    properties: {
        limit: {
            type: ["number", "string"]
        },
    }
};

function getFilePath(buffer) {
    //生成一个新的图片, 添加到最终资源中, 并返回路径 
    //interpolateName获取文件的hash值,并插入值,生成唯一的文件名
    var filename = _loaderUtils.interpolateName(this, "[contenthash:5].[ext]", {
        content: buffer
    })
    // emitFile 能够让开发者更方便的输出一个 file 文件,这是 webpack 特有的方法,使用的方法也很直接
    //发射文件,会在dist目录下面生成一个文件
    this.emitFile(filename, buffer)
    return filename;
}
function loader(source) {
    //获取参数
    let options = (0, _loaderUtils.getOptions)(this);
    (0, _schemaUtils.validate)(schema, options, 'img-loader');
    const { limit = 0 } = options
    //获取图片的类型
    const mimetype = mime.getType(this.resourcePath);
    //如果文件大小小于limit,则使用base64
    if (source.length < limit) {
        //组装base64
        let base64 = `data:${mimetype};base64,${source.toString('base64')}`;
        //最后必须修改文件的路径
        return `module.exports = ${JSON.stringify(base64)}`;
    } else {
        //使用file-loader
        src = getFilePath.call(this, source);
        return `module.exports=\`${src}\``;
    }
}
loader.raw = true; //把文件转成二进制流
module.exports = loader;

如果获取的图片文件的大小小于limit参数,则把传入的文件流转为base64然后返回,如果不大于就生成一个新的图片, 添加到最终资源中并返回。

最后运行起来结果也是正确的,大家也可自行实践一下。

到这里基本就结束了,觉得有帮助,不妨点个,不足之处,还望斧正~