kd-cloud-web / Blog

一群人, 关于前端, 做一些有趣的事儿
13 stars 1 forks source link

webpack-loader #73

Open zzkkui opened 2 years ago

zzkkui commented 2 years ago

什么是 loader

loader 的作用是将不同类型的文件转换为 webpack 可识别的模块。任何非 js 文件都必须被预先处理转换为 js 代码,才可以参与打包,loader 就是这样一个代码转换器。

loader 本质就是导出一个函数的 node 模块,loader runner 会调用这个函数,然后把上一个 loader产生的结果或者资源传进去。函数的 this 上下文是由 webpack 提供的。

function Loader(source, sourceMap?, data?) {
  // source 为 loader 的输入,可能是文件内容,也可能是上一个 loader 处理结果
  return source;
};

module.exports = Loader

loader 链式调用

可以在处理某种文件的时候配置多个 loader

以处理 less 为例

module.exports = {
  module: {
    rules: [
      {
        test: /\.less/,
        // less 文件的处理顺序为先 less-loader 再 css-loader 再 style-loader
        use: [
          'style-loader',
          {
            loader:'css-loader',
            // 给 css-loader 传入配置项
            options:{
              minimize:true, 
            }
          },
          'less-loader'],
      },
    ]
  },
};

image1

可以看出来这是一个链式的调用。但是他们会以相反的顺序执行,从下到上或者从右到左

loader 使用

Webpack 中,loader 可以被分为 4 类:pre 前置、post 后置、normal 普通和 inline 行内。其中 pre 和 post loader 可以通过 rule 对象的 enforce 属性来指定

inline loader

// 使用 ! 将资源中的 loader 分开。
import Styles from 'style-loader!css-loader?modules!./styles.css';

通过为内联 import 语句添加前缀,可以覆盖配置中的所有 loader

import Styles from '!style-loader!css-loader?modules!./styles.css';
import Styles from '!!style-loader!css-loader?modules!./styles.css';
import Styles from '-!style-loader!css-loader?modules!./styles.css';

可以给 inline loader 传参,?key=value&foo=bar 或者 ?{"key":"value","foo":"bar"},上面的例子中的 modules 就是参数

pre Loader, normal loader, post Loader

顾名思义,指定了 enforce 属性的 loader 在执行时不是默认的从下到上(或从右到左)的顺序。

// webpack.config.js
// 这里就是先执行 a-loader 再执行 b-loader 最后执行 c-loader
const path = require("path");

module.exports = {
  module: {
    rules: [
      {
        test: /\.txt$/i,
        use: ["a-loader"],
        enforce: "pre", // pre loader
      },
      {
        test: /\.txt$/i,
        use: ["b-loader"], // normal loader
      },
      {
        test: /\.txt$/i,
        use: ["c-loader"],
        enforce: "post", // post loader
      },
    ],
  },
};

raw模式

content 可以是 string 或者 Buffer,在我们处理图片,音频,视频等这些文件的时候,我们就需要用到 Buffer,这时我们需要将 row 设为 true

module.exports = function (content) {
  assert(content instanceof Buffer);
  return someSyncOperation(content);
};
module.exports.raw = true

pitch

首先我们的 loader 通常是到处一个函数

function Loader(source, sourceMap?, data?) {
  // source 为 loader 的输入,可能是文件内容,也可能是上一个 loader 处理结果
  return source;
};

module.exports = Loader

同时我们可以在 loader 函数上添加一个 pitch 属性,它的值也是一个函数。它会比 loader 更早执行

/**
 * @remainingRequest 剩余请求
 * @precedingRequest 前置请求
 * @data 数据对象
 */
Loader.pitch =  function (remainingRequest, precedingRequest, data) {
 //
};

可以通过 data 参数来进行数据传递,在 Normal loader 中就可以通过 this.data 的方式读取数据。

loader 执行链的执行过程分为三个阶段,pitch、解析资源、loader执行。

// a-loader
module.exports = function(content) {
  return content;
};

module.exports.pitch = function(remainingRequest) {
  //
};
// b-loader
module.exports = function(content) {
  return content;
};

module.exports.pitch = function(remainingRequest) {
  //
};
// c-loader
module.exports = function(content) {
  return content;
};

module.exports.pitch = function(remainingRequest) {
  //
};
module: {
    rules: [
      {
        test: /\.txt$/i,
        use: ["a-loader","b-loader", "c-loader"],
      },
    ],
  },

上面的例子的 loader 的执行顺序是 image2

pitch 的执行顺序是和 loader 执行顺序相反,可以看成是顺序入栈倒序出栈

同时 pitch 还有一个重要功能:阻断。

还是上面的例子,只不过现在我们 b-loader 的 pitch 函数返回一个。

// b-loader
module.exports = function(content) {
  return content;
};

module.exports.pitch = function(remainingRequest) {
  return '123';
};

现在的执行顺序就是

image3

pitch 有返回(非 undefined),就会跳过后续 pitch 的执行,接着只执行之前执行了 pitch 相关联的 loader,而且 b-loader 的返回值也是 pitch 返回的值

可以看出 pitch 提供了一个可以阻断loader链执行的方式。style-loader 就用到了 pitch,在 style-loader 中,是没有loader内容的,只有 loader.pitch

// style-loader
const loaderAPI = () => {};

loaderAPI.pitch = function loader(request) {
    // 
return someCode
}

这里就很奇怪了,为什么style-loader阻断了loaderpitch 有返回值)执行,但是我们写的样式依旧可以生效!

这是因为在 style-loader 的返回值里面返回了 inline loader。然后执行 inline loader返回的css字符串直接写进的style标签,然后再插入 dom

// style-loader 返回内容中很重要的一段
// !! 禁用所有已配置的 loader
import content, * as namedExport from "!!css-loader-path!less-loader-path!./index.less";

所以我们使用 less 时,实际的loader执行顺序是: 第一次执行 image4

第二次是inline loader执行

image5

并且第一次执行的时候是在webpack编译阶段,但是第二次执行则是在浏览器在加载 js 的时候执行。

为什么 style-loader 要这么设计

事实上 style-loader 这么设计是有原因的: style-loader 会去操作 dom 元素(会给dom添加 style 标签)。但是在webpack编译阶段执行 loader 的过程中,是在 nodejs 环境,没有 dom 对象。所以才通过返回 inline loader执行,执行结果在通过 style-loader 处理添加到 dom 中。

loader开发

本地开发调试

通过在 rule 对象设置 path.resolve 指向这个本地 loader

{
  test: /\.js$/
  use: [
    {
      loader: path.resolve('path/to/loader.js'),
      options: {/* ... */}
    }
  ]
}

匹配多个 loaders, 可以使用 resolveLoader.modules 配置

resolveLoader: {
  modules: [
    'node_modules',
    // 本地 loaders 目录
    path.resolve(__dirname, 'loaders')
  ]
}

或者通过 npm link 来关联到需要测试的项目

loader API

this.getOptions

webpack 5 开始,getOptions可以在loader上下文中使用。替代loader-utils 中的 getOptions

获取loader参数

this.getOptions()

this.data

pitch 共享的数据

this.callback

loader 是可以通过 return 来返回处理结果,但是如果需要返回多个结果时,需要使用到 callback API

this.callback(
  err: Error | null,
  content: string | Buffer,
  sourceMap?: SourceMap,
  meta?: any // webpack 会忽略,loader 之间可以传输信息
);

this.async

异步loader

async function Loader(source) {
  // 1. 获取异步回调函数
  const callback = this.async();

  let res;

  try {
    // 2. 调用less 将模块内容转译为 css
    res = await SomeAsync();
  } catch (error) {
    // ...
  }
  callback(null, res);
}

this.cachable

默认情况下,loader的结果都是被缓存的,传递 false 会让 loader 结果不缓存。

this.cacheable(flag = true: boolean)

this.addDependency

添加依赖来监听,依赖项变化时,会重新编译生成缓存

this.addDependency(file: string)

更多

实现一个 txt-loader

// txt-loader.js
module.exports = function loader(source) {
    return `module.exports = '${source}'`;
}
// index.js
import Data from "./data.txt"

const msgElement = document.querySelector("#message");
msgElement.innerText = Data;

实现极简 babel-loader

// simple-babel-loader.js
const core = require('@babel/core');

module.exports = function loader(source) {
    const options = this.getOptions() || {};
    const callback = this.async();
    core.transform(source, options, function (err, result) {
        if (err) {
            callback(err);
        } else {
            callback(null, result.code);
        }
    });
}

工具

注意事项

当 loader 用来解析非 js 文件到 js 文件时,最后返回的代码是需要添加module.exports 或者 export default字符串,这样外界在导入模块的时候就可以接收到这个HTML字符串。

 return `module.exports = ${JSON.stringify(someString)}`
 // or
 // 有 callback 就不需要 return
 this.callback(null, `module.exports = ${JSON.stringify(someString)}`)

代码实现

参考