一个最简单的 loader 代码结构

定义:loader 只是一个导出为函数的 JavaScript 模块

    module.exports = function(source) { return source; };

多 Loader 时的执行顺序

多个 Loader 串行执行, 顺序从后到前

    module.exports = { 
        entry: './src/index.js', 
        output: { 
            filename: 'bundle.js', 
            path: path.resolve(__dirname, 'dist') }, 
            module: { 
                rules: [ 
                        { 
                            test: /\.less$/, 
                            use: [ 
                                'style-loader', 
                                'css-loader', 
                                ' less-loader'
                                ]
                            } 
                        ] 
                    } 
        };

函数组合的两种情况

1. Unix 中的 pipline
2. Compose(webpack采取的是这种)
    compose = (f, g) => (...args) => f(g(...args));

验证loader 的执行顺序

        a-loader.js: 
        module.exports = function(source) { 
            console.log ('loader a is executed'); 
            return source; 
        };
        b-loader.js: 
        module.exports = function(source) { 
            console.log ('loader b is executed'); 
            return source; 
        };

load_tree

package.json
    {
        "name": "loader-order",
        "version": "1.0.0",
        "description": "",
        "main": "index.js",
        "scripts": {
            "test": "echo \"Error: no test specified\" && exit 1",
            "build": "webpack"
        },
        "keywords": [],
        "author": "",
        "license": "ISC",
        "devDependencies": {
            "loader-utils": "^1.2.3",
            "webpack": "^4.39.1",
            "webpack-cli": "^3.3.6"
        }
    }
webpack.config.js
const path = require('path');

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.join(__dirname, 'dist'),
        filename: 'main.js'
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                use: [
                    path.resolve('./loaders/a-loader'),
                    path.resolve('./loaders/b-loader')
                ]
            }
        ]
    }
}
loaders/a-loader.js
const loaderUtils = require('loader-utils');

module.exports = function(source) {
    console.log('Loader a is excuted!');

    const url = loaderUtils.interpolateName(this, '[name].[ext]', source);

    console.log(url);
    this.emitFile(url, source);
    return source;
}
loaders/b-loader.js
module.exports = function(source) {
    console.log('Loader b is excuted!');
    return source;
}
src/index.js
const a = 1;
执行结果

loader_order

loader-runner

loader-runner 的介绍

定义:loader-runner 允许你在不安装 webpack 的情况下运行 loaders

    作用:
        1.作为 webpack 的依赖,webpack 中使用它执行 loader
        2.进行 loader 的开发和调试
loader-runner 的使用
    import { runLoaders } from "loader-runner"; 

    runLoaders({ 
        resource: "/abs/path/to/file.txt?query", // String: 资源的绝对路径(可以增加查询字符串) 
        loaders: ["/abs/path/to/loader.js?query"], // String[]: loader 的绝对路径(可以增加查询字符串) 
        context: { minimize: true }, // 基础上下文之外的额外 loader 上下文 
        readResource: fs.readFile.bind(fs) // 读取资源的函数 
    }, function(err, result) { 
        // err: Error? 
        // result.result: Buffer | String 
    })

开发一个raw-loader

目录结构

raw_loader

run-loader.js
    const { runLoaders } = require('loader-runner');
    const fs = require('fs');
    const path = require('path');

    runLoaders({
        resource: path.join(__dirname, './src/demo.txt'),
        loaders: [
            {
                loader: path.join(__dirname, './src/raw-loader.js'),
                options: {
                    name: 'test'
                }
            }
        ],
        context: {
            emitFile: () => {}
        },
        readResource: fs.readFile.bind(fs)
    }, (err, result) => {
        err ? console.log(err) : console.log(result);
    });
package.json
    {
        "name": "raw-loader",
        "version": "1.0.0",
        "description": "",
        "main": "index.js",
        "scripts": {
            "test": "echo \"Error: no test specified\" && exit 1"
        },
        "keywords": [],
        "author": "",
        "license": "ISC",
        "dependencies": {
            "loader-runner": "^3.0.0"
        },
        "devDependencies": {
            "loader-utils": "^1.2.3"
        }
    }
src/raw-loader.js
    const loaderUtils = require('loader-utils');
    const fs = require('fs');
    const path = require('path');

    module.exports = function(source) {
        const { name } = loaderUtils.getOptions(this);

        const url = loaderUtils.interpolateName(this, "[name].[ext]", {
            source,
        });

        console.log(url);
        this.emitFile(path.join(__dirname, url), source);
        
        // this.cacheable(false);
        // const callback = this.async();
        // console.log('name', name);

        const json = JSON.stringify(source)
           // .replace('foo', 'baishu')
            .replace(/\u2028/g, '\\u2028')
            .replace(/\u2029/g, '\\u2029');

        // fs.readFile(path.join(__dirname, './async.txt'), 'utf-8', (err, data) => {
        //     if (err) {
        //         callback(err, '');
        //     }
        //     callback(null, data);
        // });
        // 

        // throw new Error('Error');

        return `export default ${json}`;
        // this.callback(null, json, 2, 3, 4);

    }
src/async.txt
    async   
src/demo.txt
    foobar
执行结果
    node run-loader.js

raw_loader

loader的参数获取

    通过 loader-utils 的 getOptions 方法获取

    const loaderUtils = require("loader-utils"); 
    module.exports = function(content) { 
        const { name } = loaderUtils.getOptions(this); 
    };

loader的异常处理

    1.loader 内直接通过 throw 抛出
    2.通过 this.callback 传递错误 
    this.callback( 
        err: Error | null, 
        content: string | Buffer, 
        sourceMap?: SourceMap, 
        meta?: any );

loader 的异步处理

    通过 this.async 来返回一个异步函数
    第一个参数是 Error,第二个参数是处理的结果
    示意代码:
    module.exports = function(input) { 
        const callback = this.async(); 
        // No callback -> return synchronous results 
        // if (callback) { ... } 
        callback(null, input + input); 
    };

在 loader 中使用缓存

    webpack 中默认开启 loader 缓存
    ·可以使用 this.cacheable(false) 关掉缓存

    缓存条件: loader 的结果在相同的输入下有确定的输出
    ·有依赖的 loader 无法使用缓存

loader 如何进行文件输出?

    通过 this.emitFile 进行文件写入
    const loaderUtils = require("loader-utils");
    module.exports = function(content) { 
        const url = loaderUtils.interpolateName(this, "[hash].[ext]", { content, 
        }); 
        this.emitFile(url, content); 
        const path = `__webpack_public_path__ + ${JSON.stringify(url)};`; return `export default ${path}`; 
    };

实战开发一个自动合成雪碧图的 loader

支持的语法
    background: url('a.png?__sprite');
    background: url('b.png?__sprite'); 
    -> background: url('sprite.png');
准备知识:如何将两张图片合成一张图片?
    使用 spritesmith: https://www.npmjs.com/package/spritesmith
    spritesmith 使用示例 
    const sprites = ['./images/1.jpg', './images/2.jpg']; 
    Spritesmith.run({src: sprites}, function handleResult (err, result) { 
        result.image; 
        result.coordinates; 
        result.properties; 
    });
目录结构

sprite-loader

run-loader.js
    const { runLoaders } = require('loader-runner');
    const fs = require('fs');
    const path = require('path');

    runLoaders({
        resource: path.join(__dirname, './loaders/index.css'),
        loaders: [path.resolve(__dirname, './loaders/sprite-loader.js')],
        readResource: fs.readFile.bind(fs)
    }, (err, result) => {
        err ? console.log(err) : console.log(result);
    });
test.js
    const Spritesmith = require('spritesmith')
    const fs = require('fs');
    const path = require('path');

    const sprites = ['./loaders/images/1.jpg', './loaders/images/2.jpg']; 
    Spritesmith.run({src: sprites}, (err, result)=>{ 
        console.log(result.image)
        console.log(result.coordinates)
        console.log(result.properties)
        fs.writeFileSync(path.join(__dirname,'dist/sprite.jpg'),result.image)
    });
package.json
    {
        "name": "raw-loader",
        "version": "1.0.0",
        "description": "",
        "main": "index.js",
        "scripts": {
            "test": "echo \"Error: no test specified\" && exit 1"
        },
        "keywords": [],
        "author": "",
        "license": "ISC",
        "dependencies": {
            "loader-runner": "^3.0.0",
            "spritesmith": "^3.4.0"
        }
    }
loaders/sprite-loader.js
    const Spritesmith = require('spritesmith')
    const fs = require('fs');
    const path = require('path');
    const loaderUtils = require('loader-utils');
    module.exports = function(source) {
        const callback = this.async();
        const imgs = source.match(/url\((\S*)\?__sprite/g);
        const matchedImgs = [];

        for(let i = 0; i < imgs.length; i++){
            const img = imgs[i].match(/url\((\S*)\?__sprite/)[1];
            matchedImgs.push(path.join(__dirname,img));
        }

        Spritesmith.run({
            src: matchedImgs
        }, (err, result)=>{ 
            fs.writeFileSync(path.join(process.cwd(),'dist/sprite.jpg'),result.image)
            source = source.replace(/url\((\S*)\?__sprite/g,(match) => {
                return `url("dist/sprite.jpg"`
            })
            fs.writeFileSync(path.join(process.cwd(),'dist/index.css'),source)
            callback(null,source)
        });
    }
loaders/index.css
    .img1{
        background: url(./images/1.jpg?__sprite);
    }
    .img2{
        background: url(./images/2.jpg?__sprite);
    }
dist/index.css
    .img1{
        background: url("dist/sprite.jpg");
    }
    .img2{
        background: url("dist/sprite.jpg");
    }