Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

webpack-loader #73

Open
zzkkui opened this issue Sep 9, 2022 · 0 comments
Open

webpack-loader #73

zzkkui opened this issue Sep 9, 2022 · 0 comments

Comments

@zzkkui
Copy link

zzkkui commented Sep 9, 2022

什么是 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

  • 使用 ! 前缀,将禁用所有已配置的 normal loader
import Styles from '!style-loader!css-loader?modules!./styles.css';
  • 使用 !! 前缀,将禁用所有已配置的 loader(preLoader, normal loader, postLoader)
import Styles from '!!style-loader!css-loader?modules!./styles.css';
  • 使用 -! 前缀,将禁用所有已配置的 preLoaderloader,但是不禁用 postLoaders
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)}`)

代码实现

参考

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant