webpack4 - loader 的执行过程和部分常用loader原理实现

本文详细介绍了webpack4中Loader的执行过程和常用Loader的原理实现,包括Loader的匹配和用法准则,如单一独立原则、链式调用、模块化等。此外,文章深入探讨了Loader的实现,包括babel-loader、pitch机制、loader-runner的工作原理以及css-loader、style-loader、file-loader等的具体实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

webpack4 - loader 的执行过程和部分常用loader原理实现

1.loader 运行的总体流程
    1.Compiler.js中会将用户配置与默认配置合并,其中就包括了loader部分
    2.webpack就会根据配置创建两个关键的对象 ———— NormalModuleFactory 和 ContextModuleFactory , 他们相当于是两个类工厂,通过其可以创建相应的 NormalModule 和 ContextModule
    3.在工厂创建NormalModule实例之前还要通过loader的resolver来解析loader路径
    4.在NormalModule实例创建之后,则会通过其build方法来进行模块的构建,构建模块的第一步就是使用loader来加载并处理模块内容,而loader-runner这个库就是webpack中loader的运行器
    5.最后,将loader处理完毕的模块内容输出,进入后续的编译流程

开始编译 => webpack默认配置 => 创建NormalModuleFactory => 创建NormalModule[使用resolver解析loader路径] => 编译模块[loader-runner]

2.loader 匹配和用法
loader 是导出为一个函数的 node 模块,该函数在 loader 转换资源的时候调用,给定的函数将调用 loaderApi,并通过 this 上下文访问
访问方式可以是直接给 use 数组一个个对象,loader 指定一个绝对路径,而不是直接用 xxx-loader 提供,也可以是 resolveLoader 配置的 module 的查找目录(文件夹),或者使用 npm link 发布到本地 npm 的模块
还有一种方式就是配置 resolveLoader.alias 对象,配置一一 key-value 对应的关系,key 就是可以拿来使用的 loader 别名
 /** loaders/loader1 */
    // 最后到给loader1
    function loader(inputSource) {
   
    // console.log('index') // loader3 // loader2
    return inputSource + ' // loader1';
    }

    module.exports = loader;

 /** loaders/loader2 */
    // 第二个给loader2
    function loader(inputSource){
   
        // console.log('index') // loader3
        return inputSource + ' // loader2';
    }

    module.exports = loader;

 /** loaders/loader3 */
// 先给loader3
    function loader(inputSource) {
   
    // 文件内容 inputSource
    // return inputSource + ' // loader3';
    // 异步写法如下
    const callback = this.async();
    setTimeout(() => {
   
        callback(null, inputSource + ' // loader3');
    }, 1000);
    }

    module.exports = loader;


 // webpack.config.js
 module.exports = {
   
    ...
    resolveLoader: {
   
        // [制定loader查找的时候]
        // 方式2,直接定义查找模块,node_modules找不到再去loaders目录找
        modules: [path.resolve('node_modules'), path.resolve('loaders')]
        // 方式3 配置别名
        // alias: {
   
        //   'loader1': path.resolve('./loaders/loader1.js'),
        //   'loader2': path.resolve('./loaders/loader2.js'),
        //   'loader3': path.resolve('./loaders/loader3.js'),
        // }
    },
    module: {
   
        rules: [
        {
   
            test: /\.js$/,
            // 方式1
            // use: [
            //   {
   
            //     loader: path.resolve('loaders/loader1.js')
            //   }
            // ],
            use: ['loader1', 'loader2', 'loader3']
        }
        ]
    }
 }
3.用法准则
3.1 简单 [单一独立原则]
  • loader 应该只做单一任务,这不仅使每个 loader 容易维护,也可以在更多场景链式调用
3.2 链式(Chaining)
  • 利用 loader 可以链式调用的优势,写五个简单的 loader 实现五项任务,而不是一个 loader 实现五项任务
3.3 模块化(Modular)
  • 保证输出模块化,loader 生成的模块与普通模块遵循相同的设计原则。
3.4 无状态(Stateless)

确保 loader 在不同模块转换之间不保持状态,每次运行都应该独立与其他编译模块以及相同模块之间的编译结果

3.5 loader 工具库
  • loader-utils 包,它提供了许多有用的工具,但最常用的一种工具是获取传递给 loader 的选项
  • schema-utils 包,它配合 loader-utils,用于保证 loader 选项,进行与 JSON Schema 结果一致的校验.
3.6 loader 依赖(Loader Dependencies)
  • 如果一个 loader 使用外部资源(例如从文件系统读取),必须声明它,这些信息用于使缓存 loaders 无效,以及在观察模式(watch mode)下重新编译
3.7 模块依赖(Module Dependencies)
  • 根据模块类型,可能会有不同的模式制定依赖关系,例如在 CSS 中,使用@import 和 url(…)语句来声明依赖,这些依赖关系应该由模块系统解析
3.8 绝对路径(Absolute Paths)
  • 不要在模块代码中插入绝对路径,因为当项目根路径变化时,文件绝对路径也会发生变化,loader-utils 中的 stringifyRequest 方法,可以讲绝对路径转化程相对路径。
3.9 同等依赖(Peer Dependencies)
  • 如果你的 loader 简单包裹另外一个包,你应该把这个包作为一个 peerDependency 引入
  • 这种方式允许应用程序开发者在必要情况下,在 package.json 中指定所需的确定版本。
4.loader 的实现
4.1 babel-loader 实现
// loaders/babel-loader.js
const babel = require('@babel/core');
const loaderUtils = require('loader-utils');
const path = require('path');
/** loader只是一个函数 */
module.exports = function loader(inputSource) {
   
  // 获取配置参数options的数据
  const options = loaderUtils.getOptions(this);
  // 默认配置
  const baseOptions = {
   
    ...options, // 合并配置
    // presets: ['@babel/preset-env'],
    sourceMaps: true, // 告诉babel我要生成sourceMao
    filename: path.basename(this.resourcePath)
  };
  // 代码 map文件  ast语法树
  const {
    code, map, ast } = babel.transform(inputSource, baseOptions);
  // 我们可以把source-map ast 都传递给webpack,这样webpack就不需要自己把源代码转语法树,也不需要自己生成source-map
  return this.callback(null, code, map, ast);
};
// webpack.config.js
module.exports = {
   
  // ...
  module: {
   
    rules: [
      {
   
        test: /\.js$/,
        use: [
          {
   
            loader: 'babel-loader',
            options: {
   
              // webpack loader的选项配置
              presets: ['@babel/preset-env']
            }
          }
        ]
      }
    ]
  }
  // ...
};
4.2 pitch 实现 (上面实现的 babel-loader,babel1-3 是普通函数,不是 pitch 函数)

pitch function 是先执行的,从左往右执行,执行完毕后执行normal function,从右往左回去,不过执行条件如下

  • 比如 a!b!c!module,正常调用顺序应该是 c、b、a,但是真正调用顺序是 a(pitch)、b(pitch)、c(pitch)、c、b、a,如果其中任何一个 pitching loader 返回了值,就相当于在它以及它右边的 loader 已经执行完毕
  • 比如如果 b 返回了字符串’result b’,接下来只有 a 会被系统执行,且 a 的 loader 收到的参数是’result b’
  • loader 根据返回值可以分成两种,一种是返回 s 代码(一个 module 的代码,含有类似 module export 语句)的 loader,还有不能作为最左边 loader 的其他 loader
  • 有时候我们想把两个第一种 loader chain 起来,比如 style-loader!css-loader,问题是 css-loader 的返回值是一串代码,如果按正常方式写 style-loader 的参数就是一串代码字符串
  • 为了解决这种问题,我们需要在 style-loader 里执行 require(css-loader!resources)
/** loaders/loader1 */
// 最后到给loader1
function loader(inputSource) {
   
  // console.log('index') // loader3 // loader2
  console.log('loader1');
  return inputSource + ' // loader1';
}
// pitch function
loader.pitch = function(remindingRequest, previousRequest, data) {
   
  console.log('pitch1');
};

module.exports = loader;

/** loaders/loader2 */
// 第二个给loader2
function loader(inputSource) {
   
  // console.log('index') // loader3
  console.log('loader2');
  return inputSource + ' // loader2';
}
// pitch function
loader.pitch = function(remindingRequest, previousRequest, data) {
   
  console.log('pitch2');
};

module.exports = loader;

/** loaders/loader3 */
// 先给loader3
function loader(inputSource) {
   
  // 文件内容 inputSource
  // return inputSource + ' // loader3';
  // 异步写法如下
  const callback = this.async();
  setTimeout(() => {
   
    console.log('loader3');
    callback(null, inputSource + ' // loader3');
  }, 1000);
}
// pitch function
loader.pitch = function(remindingRequest, previousRequest, data) {
   
  console.log(
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值