1

现代前端在开发过程中时常都会使用到webpack,用来对代码进行模块化打包。通常情况下,我们都是直接使用webpack的配置和别人写好的loader。如果自己要实现一个loader,需要怎么做?

1. 什么是loader?

webpack是基于node的模块化打包工具,它本身也只能处理JS和JSON文件,没有处理CSS、图片等其他格式文件的能力。loader就相当于是一个翻译机,将这些文件翻译成webpack能处理的格式。换句话说,loader赋予了webpack处理其他文件的能力。

2. loader的使用

module.exports = {
  //...
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: ['style-loader', 'css-loader', {
            loader: 'sass-loader',
            options: {
                //...
            }
        }]
      }
    ]
  }
};
  • 职责单一: 每个loader只完成一种转换,如sass-loader只讲sass转换为css。
  • 链式调用: 第一个loader接收到的是资源文件的内容,后续loader都是接收到的上一个loader返回的处理结果。
  • 调用顺序: 在loader中存在一个pitch属性,调用顺序是style-loader(pitch)、css-loader(pitch)、sass-loader(pitch)、sass-loader、css-loader、style-loader。如果其中一个pitch返回了值,则停止后续调用。简单点说就是先从左往右执行pitch,然后从右往左执行loader。

3. 一个基础的loader

上面写了一堆乱七糟八的东西,看起来着实枯燥。现在就写一个一讲自定义loader就会写的例子,就和写代码必先写hello world一样的东西。

一个用于替换的loader。替换原来JS代码中的NAME。
webpack.config.js

const path = require('path');
// 先看看怎么用
module.exports = {
    //...
    module: {
        rules: [
            { 
                test: /\.js$/, 
                use: [{ 
                    // 本地引用loader
                    loader: path.resolve('./replace-loader'),
                    options: {
                        // 通过配置传入words来替换NAME为wei
                        words: 'wei'
                    }
                }]
            }
        ]
    }
};

replace-loader.js

// loader-utils是专门用于自定义loader时的一些工具函数
const { getOptions } = require('loader-utils');

module.exports = function(source) {
    const options = getOptions(this); // getOptions用于获取配置

    return source.replace(/NAME/g,     options.words);
}

如上,一个简单的loader就完成了。

3. mini版的style-loader

只写一个replace-loader好像啥也没有写一样,不写个todo list怎么能体现出我能自己写loader了。

const { stringifyRequest, getOptions } = require('loader-utils');

function loader(source) {}

// 这里使用pitch是因为按照正常顺序在css-loader调用之后调用style-loader的话,style-loader接收到的就是一堆代码字符串
// 为了避免这种问题,所以需要在css-loader执行之前执行style-loader
loader.pitch = function(remainingRequest, precedingRequest, data) {
    const options = getOptions(this);

    // 使用JSON字符串化属性
    // 作为变量来拼接字符串
    const attributes = JSON.stringify(options.attributes || {});
    const insert = options.insert === undefined 
                    ? '"head"' 
                    : typeof options.insert === 'string' ? JSON.stringify(options.insert) : options.insert.toString();

    // 标准化路径
    const request = stringifyRequest(this, '!!' + remainingRequest);

    return `
        var style = document.createElement('style');
        var content = require(${ request }); // 相当于是require(css-loader!resource),返回的就是css-loader处理之后的内容
        var attributes = ${ attributes };
        var insert = ${ insert };
        // 遍历设置属性
        for (var key in attributes) {
            style.setAttribute(key, attributes[key]);
        }
        content = content.__esModule ? content.default : content;
        style.innerHTML = content;
        var insertElement;
        if (typeof insert === 'string') {
            var insertElement = document.querySelector(insert);
            insertElement && insertElement.appendChild(style);
        } else {
            insert(style);
        }
    `;
}

module.exports = loader;

4. mini版的sass-loader

const sass = require('node-sass');
const { getOptions } = require('loader-utils');

module.exports = function(source) {
    const options = getOptions(this);

    const sassOptions = options.sassOptions || {};

    const result = sass.renderSync({
        data: source,
        ...sassOptions
    });

    return result.css;
};

loader相关的知识还有很多,例如异步(this.async())处理二进制数据(loader.raw = true)禁用缓存(this.cacheable(false))等等。在实际开发的时候可以再查阅相关的文档。

更多:


WillemWei
491 声望37 粉丝