我理解的loader原理

672 阅读2分钟

之前了解到webpack的打包和编译原理,今天又看到了webpack中loader的部分原理,借此机会分享给大家

loader是做什么的?

webpack中loader是用来转换代码的,即将源代码从一种代码转换为另一种

2020-01-13-10-39-24.png

所以loader的写法也就呼之欲出,即如下代码:

module.exports = function loader(code) {
    // some transfrom operation... and return resultCode
    return resultCode;
}

loader基本配置

下面展示的是webpack中loader的基本配置之一,若不了解配置则需要去看webpack中loader的配置规则


module.exports = {
    ...,
    module: {
        rules: [
            {
                test: /\.css/, // 正则表达式作为loader的匹配规则
                use: [
                    {
                        loader: "", // loader路径,和require的规则类似
                        options: { // loader参数
                            ...,
                        }
                    }
                ]
            }
        ]
    }
}

loader打包执行顺序

我们都知道了loader是干嘛的,所以loader对于源代码的操作是有序的,具体怎么个有序法呢?我们可以来测试一下 如下代码

// webpack.config.js
const path = require("path");

const outputPath = path.join(__dirname, "dist");
module.exports = {
    mode: "development",
    entry: {
        index: path.join(__dirname, "src", "index.js")
    },
    output: {
        path: outputPath,
        filename: "[name].js",
        clean: true
    },
    module: {
        rules: [
            {
                test: /index.js$/,
                use: [
                    "./loaders/loader1",
                    "./loaders/loader2",
                ]
            },
            {
                test: /.js$/,
                use: [
                    "./loaders/loader3",
                    "./loaders/loader4",
                ]
            }
        ]
    }
}

// loaders/loader1.js
module.exports = function (code) {
    console.log("loader1");
    return code;
}

// loaders/loader2.js
module.exports = function (code) {
    console.log("loader2");
    return code;
}

// loaders/loader3.js
module.exports = function (code) {
    console.log("loader3");
    return code;
}

// loaders/loader4.js
module.exports = function (code) {
    console.log("loader4");
    return code;
}

这段代码执行命令npx webpack之后,执行结果如下:

image.png

由此可见loader执行的时候是从后往前执行的

这个时候我们在src/index,js增加如下内容,并在src目录下新增a.js,路径为src/a.js,不用填入内容

// src/index.js
require("./a");

此时我们再执行npx webpack,webpack的loader输出结果是什么呢?

image.png

所以webpack的loader执行时,首先对入口文件进行loader方法的依次执行,然后再对其依赖文件进行loader方法的依次执行,而且对于其依赖也会执行loader的匹配规则进行匹配。

loader打包和AST语法树建立先后顺序

修改src/index.js为如下代码

// src/index.js
require("./a");
变量 a = 1;

再次执行npx webpack,发现webpack不能打包,且报错。

接下来我们修改loader/loader4.js代码,如下:

// loaders/loader4.js
module.exports = function (code) {
    console.log("loader4");
    const { changeVar } = this.getOptions();
    const reg = new RegExp(changeVar, "g");
    return code.replace(reg, "var");
}

修改webpack.config.js代码中对于loader4的loader配置如下:

module.exports = {
    ...,
    module: {
        rules: [
            ...,
            {
                test: /.js$/,
                use: [
                    "./loaders/loader3",
                    {
                        loader: "./loaders/loader4", // loader:将源代码转换成另外的源代码,运行于构建AST树之前
                        options: {
                            changeVar: "变量"
                        }
                    },
                ]
            }
        ]
    }
}

接下来再执行npx webpack,执行结果如下:

image.png

如上我们得到了输出,并且不报错。说明loader打包的执行顺序比AST语法树的建立要早,所以有如下图: webpack打包流程:

2020-01-13-09-35-44.png

一些例子

简易的css-loader

新增loader/style-loader.js

// loaders/style-loader
const path = require("path");
const loaderUtil = require("loader-utils");
module.exports = function (buffer) {
    const {embed} = this.getOptions();
    if (embed) {
        return exportEmbedStyle(this, buffer);
    }

    return exportFileStyle(this, buffer);

}

function exportFileStyle(context, buffer) {
    const {filePath, publicPath} = context.getOptions();
    const filename = getFileName(context, buffer);
    const fullPath = path.join(filePath, filename);
    const relativePath = path.relative(publicPath, fullPath).replace(/\\/g, "/");
    return `const link = document.createElement("link");
            link.rel = "stylesheet";
            link.href = "${relativePath}";
            document.head.appendChild(link);
            module.exports = "${relativePath}"`;
}

function getFileName(context, buffer) {
    const ext = path.extname(context.resourcePath);
    const _name = path.basename(context.resourcePath);
    const filename = _name.slice(0, _name.length - ext.length);
    const hash = loaderUtil.interpolateName(context, "[contenthash]", {
        content: buffer
    });
    const fullName = filename + "." + hash + ext;
    context.emitFile(fullName, buffer);

    return fullName;
}

function exportEmbedStyle(context, buffer) {
    const code = buffer.toString();
    return `const style = document.createElement("style");
            style.innerHTML = `${code}`;
            document.head.appendChild(style);
            module.exports = `${code}``;
}

module.exports.raw = true; // 让源文件以buffer的形式传入

修改webpack配置:

const outputPath = path.join(__dirname, "dist");
const publicPath = path.join(__dirname, "public");
module.exports = {
    ...,
    module: {
        rules: [
            ...,
            {
                test: /.css$/,
                use: [
                    {
                        loader: "./loaders/style-loader",
                        options: {
                            filePath: outputPath,
                            publicPath: publicPath,
                            embed: false
                        }
                    }
                ]
            }
        ]
    }
}

简易的img-loader

新增loaders/img-loader.js

// loaders/img-loader.js
const path = require("path");
const loaderUtil = require("loader-utils");

function loader(buffer) {
   console.log(this.context);
   const { limit } = this.getOptions();
   if (buffer.byteLength <= limit) {
      return exportBase64(this, buffer);
   }

   return exportFile(this, buffer);
}

function exportFile(context, buffer) {
   const filename = getFilePath(context, buffer);
   const { filePath, publicPath } = context.getOptions();
   const fullPath = path.join(filePath, filename);
   const relativePath = path.relative(publicPath, fullPath).replace(/\\/g, "/");
   return `module.exports = "${relativePath}"`;
}

function exportBase64(context, buffer) {
   const ext = path.extname(context.resourcePath).slice(1);
   const content = base64Image(buffer, ext);
   return `module.exports = `${content}``;
}

function getFilePath(context, buffer) {
   const ext = path.extname(context.resourcePath);
   const _name = path.basename(context.resourcePath);
   const filename = _name.slice(0, _name.length - ext.length);
   const hash = loaderUtil.interpolateName(context, "[contenthash]", {
      content: buffer
   });
   const fullName = filename + "." + hash + ext;
   context.emitFile(fullName, buffer);

   return fullName;
}

function base64Image(buffer, ext) {
   let base64String = buffer.toString("base64");
   return `data:image/${ext};base64,${base64String}`;
}

loader.raw = true;

module.exports = loader;

修改wenpack配置:

const outputPath = path.join(__dirname, "dist");
const publicPath = path.join(__dirname, "public");
module.exports = {
    ...,
    module: {
        rules: [
            ...,
            {
                test: /.(png|jpg|gif)$/gi,
                use: [
                    {
                        loader: "./loaders/img-loader",
                        options: {
                            limit: 1024 * 100, // 10KB以上使用路径,100KB一下使用base64
                            filePath: outputPath,
                            publicPath: publicPath
                        }
                    }
                ]
            }
        ]
    }
}

更多loader上下文见: Loader Interface | webpack

综上就是我对于laoder打包修改代码时的原理理解和部分应用,欢迎大佬指正