likes
comments
collection
share

一文读懂 Webpack Loader 的实现

作者站长头像
站长
· 阅读数 12

Webpack 支持使用 loader 对文件进行预处理。你可以构建包括 JavaScript 在内的任何静态资源。

Loader 是一个文件加载器,能够加载不同的资源,并对这些文件进行操作,如编译,压缩,最终打包到指定的文件。

Loader 最核心的只能是实现内容转换器 —— 将各式各样的资源转化为标准 JavaScript 内容格式

Why?

本质上是因为 Webpack 只认识符合 JavaScript 规范的文本(Webpack 5之后增加了其它 parser):在构建(make)阶段,解析模块内容时会调用 acorn 将文本转换为 AST 对象,进而分析代码结构,分析模块依赖;这一套逻辑对 image、json、Vue SFC等场景就不适用,需要通过 Loader 介入将资源转化成 Webpack 可以理解的内容形态。

常用 loader

  • babel-loader 使用 Babel 加载 ES2015+ 代码并将其转换为 ES5
  • ts-loader 像加载 JavaScript 一样加载 TypeScript 2.0+
  • html-loader 将 HTML 导出为字符串,需要传入静态资源的引用路径
  • markdown-loader 将 Markdown 编译为 HTML
  • style-loader 将模块导出的内容作为样式并添加到 DOM 中
  • css-loader 加载 CSS 文件并解析 import 的 CSS 文件,最终返回 CSS 代码
  • less-loader 加载并编译 LESS 文件
  • sass-loader 加载并编译 SASS/SCSS 文件
  • postcss-loader 使用 PostCSS 加载并转换 CSS/SSS 文件
  • vue-loader 加载并编译 Vue 组件

处理一个文件可以使用多个loader进行解析,loader的加载顺序是相反的,会从最后一个loader向上执行

认识 Loader

代码层面,Loader 通常是一个函数,结构如下:

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

Loader 函数接收三个参数,分别为:

  • source:资源输入,对于第一个执行的 loader 为资源文件的内容;后续执行的 loader 则为前一个 loader 的执行结果
  • sourceMap: 可选参数,代码的 sourcemap 结构
  • data: 可选参数,其它需要在 Loader 链中传递的信息,比如 posthtml/posthtml-loader 就会通过这个参数传递参数的 AST 对象

同步 Loader

其中 source 是最重要的参数,大多数 Loader 要做的事情就是将 source 转译为另一种形式的输入,比如

// loaders/cleanLogger.loader.js
module.exports = function (source) {
  const reg = /console.log([\s\S]*?);/g;
  source = source.replace(reg, '');
  return source;
};

// webpack.config.js
module.epxorts = {
  //...
  module: {
    rules: [
      {
        test: /.m?js$/,
        exclude: /(node_modules|bower_components)/,
        use: [
          // {
          //   loader: 'babel-loader',
          //   options: {
          //     presets: ['@babel/preset-env'],
          //   },
          // },
          {
            loader: path.join(__dirname, 'loaders/cleanLogger.loader.js'),
          },
        ],
      },
    ],
  },
}
const div = document.createElement('div');
const a = document.createElement('a');

a.innerHTML = '快点我';

class Person {
  name = '';

  constructor(name) {
    this.name = name;
  }

  run() {
    console.log(this.name, '正在奔跑中...');
  }
}

const person = new Person('张三');
person.run(1, 2);

div.appendChild(a);

div.addEventListener('click', () => {
  let i = 0;
  while (i <= 5) {
    console.log(i);
    i++;
  }
});

document.querySelector('#app').appendChild(div);

执行 webpack 命令后生产文件中,我们会发现打包文件中无法找到 console.log 的代码一文读懂 Webpack Loader 的实现

异步Loader

另外,loader 是可以返回 3 个参数的,我们可以调用 callback 进行传递,这里我们用less文件来说明

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './src/index.js',
  mode: 'development',
  // mode: 'production',
  output: {
    clean: true, // 在生成文件之前清空 output 目录
    filename: 'bundle.[hash:8].js',
    path: path.join(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        test: /.m?js$/,
        exclude: /(node_modules|bower_components)/,
        use: [
          {
            loader: path.join(__dirname, 'loaders/cleanLogger.loader.js')
          },
        ],
      },
      {
        test: /.less$/,
        use: [
          {
            loader: path.join(__dirname, 'loaders/styleLoader.js'),
          },
          {
            loader: path.join(__dirname, 'loaders/lessLoader.js'),
          },
        ],
      },
    ],
  },
};
@primaryColor: #1e80ff;

body {
  background-color: @primaryColor;
  color: #fff; 
}
import './index.less';

这里我们定义了 lessLoader 和 styleLoader,并在 index.js 文件中引入了 index.less 文件

因为 loader 的 use 规则是从右到左,所以我们先实现 lessLoader

let less = require('less');

module.exports = function (source) {
  const callback = this.async(); // 转译比较耗时,采用异步方式
  // const options = this.getOptions(); // 获取配置文件中less-loader的options

  less.render(source).then(
    (output) => {
      // 将生成的css代码传递给下一个loader
      callback(null, output.css);
    },
    (err) => {
      // handler err
    }
  );
};

这里我们调用了 this.async 这个方法,调用这个方法的意思是告诉当前 loader 为异步加载器,会挂起当前执行队列直到 callback 被触发

  • 使用this.async来告诉上下文当前loader是一个异步loader需要loader runner等待异步处理结果

  • this.asycn()方法会返回一个callback回调函数,在异步任务处理完之后调用callback并将需要传递的数据(一般是异步操作处理后的数据)通过callback函数传递给下一个loader

module.exports = (source, map, data) => {
  return `
    const style = document.createElement('style');
    style.innerHTML = ${JSON.stringify(source)};
    document.head.appendChild(style);
  `;
};

这里的 source 就是 lessLoader 编译后的内容,执行 webpack 命令,我们可以看到输出后的内容一文读懂 Webpack Loader 的实现打开打包出来的index.html后,效果如下:一文读懂 Webpack Loader 的实现

Context & Side Effect

我们在上文中调用了 this.getOptions,那么这个 this 是什么呢?这里面涉及到了 webpack 里的源码,我就不细说,这里的 this 对象由 NormolModule.createLoaderContext 函数在调用 Loader 前创建,里面的 api 有:

const loaderContext = {
  // 获取当前 Loader 的配置信息
  getOptions: schema => {},
  // 添加警告
  emitWarning: warning => {},
  // 添加错误信息,注意这不会中断 Webpack 运行
  emitError: error => {},
  // 解析资源文件的具体路径
  resolve(context, request, callback) {},
  // 直接提交文件,提交的文件不会经过后续的chunk、module处理,直接输出到 fs
  emitFile: (name, content, sourceMap, assetInfo) => {},
  // 添加额外的依赖文件
  // watch 模式下,依赖文件发生变化时会触发资源重新编译
  addDependency(dep) {},
};

其中 addDependency、emitFile 、emitError、emitWarning 都会对后续编译流程产生副作用,例如 less-loader 源码中包含下面这段代码

try {
  result = await (options.implementation || less).render(data, lessOptions);
} catch (error) {
  // ...
}

const { css, imports } = result;

imports.forEach((item) => {
  // ...
  this.addDependency(path.normalize(item));
});

代码中首先调用 less 编译文件内容,之后遍历所有 import 语句,也就是上例 result.imports 数组,一一调用 this.addDependency 函数将 import 到的其它资源都注册为依赖,之后这些其它资源文件发生变化时都会触发重新编译

Loader Pitch

Webpack 允许在这个函数上挂载名为 pitch 的函数,运行时 pitch 会比 Loader 本身更早执行

const loader = (source, map) => {
  console.log('后执行');
  return source;
};

loader.pitch = function (remainingRequest, precedingRequest, data) {
  console.log('先执行');
};

module.exports = loader;

我们看看执行结果一文读懂 Webpack Loader 的实现那么这个 pitch 函数是用来干什么的?

function pitch(remainingRequest: string, previousRequest: string, data = {}): void {
  // todo
}
  • remainingRequest : 当前 loader 之后的资源请求字符串
  • previousRequest : 在执行当前 loader 之前经历过的 loader 列表
  • data : 与 Loader 函数的 data 相同,用于传递需要在 Loader 传播的信息

这些参数不复杂,但与 requestString 紧密相关,我们看个例子加深了解:

module.exports = {
  module: {
    rules: [
      {
        test: /.less$/i,
        use: [
          "style-loader", "css-loader", "less-loader"
        ],
      },
    ],
  },
};

将会发生这些步骤:

|- style-loader `pitch`
  |- css-loader `pitch`
    |- less-loader `pitch`
      |- requested module is picked up as a dependency
    |- less-loader normal execution
  |- css-loader normal execution
|- style-loader normal execution

那么,为什么 loader 可以利用 "pitching" 阶段呢?

首先,传递给 pitch 方法的 data,在执行阶段也会暴露在 this.data 之下,并且可以用于在循环时,捕获并共享前面的信息

const loader = function (source, map, data) {
  console.log(this.data.value);

  return source;
};

loader.pitch = function (remainingRequest, precedingRequest, data) {
  data.value = 42;
};

module.exports = loader;

一文读懂 Webpack Loader 的实现

其次,如果将一个 loader 的 pitch 函数设置一个返回值,那么后面的所有 loader 将不再执行

const loader = function (source, map, data) {
  // console.log(this.data.value);

  return source;
};

loader.pitch = function (remainingRequest, precedingRequest, data) {
  return (
    'module.exports = require(' + JSON.stringify('-!' + remainingRequest) + ');'
  );
};

module.exports = loader;

比如 css-loader 中的 pitch 设置了返回值,那么 css-loader 和 style-loader 将不再执行,起到了一个阻断的作用


工具

  • loader-utils:提供了一系列诸如读取配置、requestString 序列化与反序列化、计算 hash 值之类的工具函数
  • schema-utils:参数校验工具
const schemaUtils = require('schema-utils'); // 校验options
const loaderUtils = require('loader-utils');

const schema = {
  // ...
}

// 获取配置项
const options = loaderUtils.getOptions(this);

// 校验配置项
schemaUtils.validate(schema, options, {
  name: "CSS Loader",
  baseDataPath: "options",
});
转载自:https://juejin.cn/post/7348002324495908918
评论
请登录