• webpack loader原理


    webpack loader原理

    概念

    帮助 webpack 将不同类型的文件转换为 webpack 可识别的模块。

    执行顺序分类

    • pre:前置loader
    • normal:普通loader
    • inline:内联loader
    • post:后置loader

    执行顺序

    • 4类loader的执行顺序**pre > normal > inline > post**

    • 相同优先级的loader执行顺序:从右到左,从上到下

    【例如】

    // 此时loader执行顺序:loader3 - loader2 - loader1
    module: {
      rules: [
        {
          test: /\.js$/,
          loader: "loader1",
        },
        {
          test: /\.js$/,
          loader: "loader2",
        },
        {
          test: /\.js$/,
          loader: "loader3",
        },
      ],
    },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    我们可以使用enforce配置标记loader的类别,再webpack配置文件中可知标记的类别:

    • pre —— 前置
    • normal —— 普通
    • post —— 后置
    // 此时loader执行顺序:loader1 - loader2 - loader3
    module: {
      rules: [
        {
          enforce: "pre",
          test: /\.js$/,
          loader: "loader1",
        },
        {
          // 没有enforce就是normal
          test: /\.js$/,
          loader: "loader2",
        },
        {
          enforce: "post",
          test: /\.js$/,
          loader: "loader3",
        },
      ],
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    使用方式

    • 配置方式:在webpack.config.js文件中指定loader(pre、normal、post loader)

    • 内联方式:在每个import语句中显示指定loader(inline loader)

      // 使用 css-loader 和 style-loader 处理 styles.css 文件
      import Styles from 'style-loader!css-loader?modules!./styles.css';
      // 等同于以下配置
      //use: [
      //    { loader: 'style-loader' },
      //    {
       //       loader: 'css-loader',
      //        options: {
      //            modules: true
      //        }
      //}
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 使用 ! 将资源中的 loader 分开,可以对应覆盖到配置中的任意 loader

      • 选项可以传递查询参数,例如 ?key=value&foo=bar,或者一个 JSON 对象,例如 ?{"key":"value","foo":"bar"}

      • inline loader可以添加不同前缀,跳过其他类型loader

        • !跳过normal loader

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

        • -!跳过 pre 和 normal loader。

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

        • !! 跳过 pre、 normal 和 post loader。

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

      尽可能使用 module.rules,因为这样可以减少源码中的代码量,并且可以在出错时,更快地调试和定位 loader 中的问题。

    • CLI:在shell命令中指定

      webpack --module-bind jade-loader --module-bind 'css=style-loader!css-loader'
      
      • 1

      这会对 .jade 文件使用 jade-loader,对 .css 文件使用 style-loadercss-loader

    特性

    • loader 支持链式传递。能够对资源使用流水线(pipeline)。一组链式的 loader 将按照相反的顺序执行。loader 链中的第一个 loader 返回值给下一个 loader。在最后一个 loader,返回 webpack 所预期的 JavaScript
    • loader 可以是同步的,也可以是异步的
    • loader 运行在 Node.js 中,并且能够执行任何可能的操作
    • loader 接收查询参数。用于对 loader 传递配置
    • loader 也能够使用 options 对象进行配置
    • 除了使用 package.json 常见的 main 属性,还可以将普通的 npm 模块导出为 loader,做法是在 package.json 里定义一个 loader 字段
    • 插件(plugin)可以为 loader 带来更多特性
    • loader 能够产生额外的任意文件

    最简单的loader

    module.exports = function loader1(content) {
      console.log("hello loader");
      return content;
    };
    
    • 1
    • 2
    • 3
    • 4

    它接受要处理的源码作为参数,输出转换后的 js 代码

    loader接受的参数

    • content 源文件的内容

    • map SourceMap 数据

    • meta 其他loader传递的数据

    loader API

    方法名含义用法
    this.async异步回调 loader。返回 this.callbackconst callback = this.async()
    this.callback可以同步或者异步调用的并返回多个结果的函数this.callback(err, content, sourceMap?, meta?)
    this.getOptions(schema)获取 loader 的 optionsthis.getOptions(schema)
    this.emitFile产生一个文件this.emitFile(name, content, sourceMap)
    this.utils.contextify返回一个相对路径this.utils.contextify(context, request)
    this.utils.absolutify返回一个绝对路径this.utils.absolutify(context, request)

    更多文档,请查阅 webpack 官方 loader api 文档

    loader分类

    同步loader

    利用return可直接返回转换后结果

    module.exports = function (content, map, meta) {
      return content;
    };
    
    • 1
    • 2
    • 3

    多层loader,将处理完的文件内容传递给下一个loader

    this.callback方法则更灵活,因为它允许传递多个参数,而不仅仅是 content

    module.exports = function (content, map, meta) {
    /*
    	第一个参数:err 代表是否错误
    	第二个参数:content 处理后的内容
    	第三个参数:source-map继续传递source-map
    	第四个参数:meta给下一个loader传递参数
    */
      this.callback(null, content, map, meta);
      return; // 当调用 callback() 函数时,总是返回 undefined,一般使用callback,不会再return
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    注意:不用在同步loader中使用异步方法,否则传递给下一个组件的源文件内容为undefined,会造成错误

    异步loader

    同步loader只适合于计算量小,速度快的场景,但是对于计量量大、耗时比较长的场景(例如网络请求),使用同步loader会阻塞整个构建过程,导致构建速度变慢,采用异步loader即可避免该问题。

    对于异步loader,使用this.async()可以获取到callback函数,该函数参数和同步loader中this.callback参数一致。

    module.exports = function(content, map, meta) {
        // 获取callback函数
        const callback = this.async();
        // 用setTimeout模拟该异步过程
        setTimeout(() => {
            // 处理后获得的结果output
        const output = dealOperation(source);
            callback(null, output, map, meta);
        }, 100)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    虽然loader是异步的,但是只有调用callback函数,才会执行一一个loader

    Raw Loader

    默认情况下,资源文件会被转化为 UTF-8 字符串,然后传给 loader

    通过设置 raw 为 true,loader 可以接收原始的 Buffer(二进制数据)

    module.exports = function (content) {
      // content是一个Buffer数据
      return content;
    };
    module.exports.raw = true; // 开启 Raw Loader
    
    • 1
    • 2
    • 3
    • 4
    • 5

    使用场景:对于图片这样的文件经过转化是二进制格式的内容,为了让loader支持接受二进制资源

    Pitching Loader

    module.exports = function (content) {
      return content;
    };
    module.exports.pitch = function (remainingRequest, precedingRequest, data) {
      console.log("do somethings");
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    执行顺序

    webpack 会先从左到右执行 loader 链中的每个 loader 上的 pitch 方法(如果有),然后再从右到左执行 loader 链中的每个 loader 上的普通 loader 方法

    loader1.pitch > loader2.pitch > loader3.pitch > loader3 > loader2 > loader1

    在这个过程中如果任何 pitch 有返回值,则 loader 链被阻断。webpack 会跳过后面所有的的 pitch 和 loader,直接进入上一个 loader 。

    自定义loader

    clean-log-loader —— 删除console.log

    作用:用来清理 js 代码中的console.log

    // loaders/clean-log-loader.js
    module.exports = function cleanLogLoader(content) {
      // 将console.log替换为空
      return content.replace(/console\.log\(.*\);?/g, "");
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    banner-loader —— 添加文本注释

    作用:给 js 代码添加文本注释

    const schema = require("./schema.json");
    
    module.exports = function (content) {
      // 获取loader的options,同时对options内容进行校验
      // schema是options的校验规则(符合 JSON schema 规则)
      const options = this.getOptions(schema);
    
      const prefix = `
        /*
        * Author: ${options.author}
        */
      `;
    
      return `${prefix} \n ${content}`;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    {
      "type": "object", 
      "properties": {
        "author": {
          "type": "string"
        }
      },
      "additionalProperties": false
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • type —— options的类型
    • properties —— 标注字段名和类型
    • additionalProperties —— 是否可追加属性

    babel-loader ES6+语法编译成 ES5-语法

    • 下载依赖

      npm i @babel/core @babel/preset-env -D
      
      • 1
    • loaders/babel-loader/index.js

      const schema = require("./schema.json");
      const babel = require("@babel/core");
      
      module.exports = function (content) {
          const options = this.getOptions(schema);
          // 使用异步loader
          const callback = this.async();
          // 使用babel对js代码进行编译
          babel.transform(content, options, function (err, result) {
              if(err) callback(err);
              else callback(err, result.code);
          });
      };
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
    • loaders/banner-loader/schema.json

      {
        "type": "object",
        "properties": {
          "presets": {
            "type": "array"
          }
        },
        "additionalProperties": true
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9

    file-loader 文件输出

    作用:将文件原封不动输出出去

    • 下载包

      npm i loader-utils -D
      
      • 1
    • loaders/file-loader.js

      // 1. 根据文件内容生成代由hash值文件名
      // 2. 将文件输出出来
      // 3. 返回:module.exports = “文件路径(文件名)”
      const loaderUtils = require("loader-utils");
      
      function fileLoader(content) {
        // 根据文件内容生产一个新的文件名称
        const filename = loaderUtils.interpolateName(this, "[hash].[ext]", {
          content,
        });
        // 输出文件
        this.emitFile(filename, content); // (文件名称,文件内容)
        // 暴露出去,给js引用。
        // 记得加上''
        return `module.exports = '${filename}'`;
      }
      
      // loader 解决的是二进制的内容
      // 图片是 Buffer 数据
      fileLoader.raw = true;
      
      module.exports = fileLoader;
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22

      loaderUtils.interpolateName参数

      const interpolatedName = loaderUtils.interpolateName(
        loaderContext,
        name,
        options
      );
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • loaderContext —— 该loader的上下文,即为this
      • name —— 文件名,我们一般使用插值就进行定义具体请看文档
      • options —— 文件内容,因为其根据文件内容生成文件名
    • loader配置

      {
        test: /\.(png|jpe?g|gif)$/,
        loader: "./loaders/file-loader.js",
        type: "javascript/auto", // 阻止webpack默认处理图片资源,只是用该loader
      },
      
      • 1
      • 2
      • 3
      • 4
      • 5

    style-loader 插入js的样式代码

    作用:动态创建 style 标签,插入 js 中的样式代码,使样式生效

    • 直接使用style-loader,只能处理样式,不能处理样式中引入的其他资源

      module.exports = function(content) {
          const script = `
          	const styleEl = document.createElement('style')
          	style.innerHTML = ${JSON.stringfy(content)}
          	document.head.appendChild(styleEl)
          `
          return script
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
    • 借用css-loader解决样式中引入的其他资源的问题

      // "./loader/style-loader" 为手写的style-loader
      use:["./loader/style-loader","css-loader"]
      
      • 1
      • 2

      问题是:css-loader暴露了一段js代码,style-loader需要执行js代码,得到返回值,在动态创建style标签,插入到页面上不好操作

    解决方法

    const styleLoader = () => {};
    
    styleLoader.pitch = function (remainingRequest) {
        /*
        remainingRequest: C:\Users\86176\Desktop\source\node_modules\css-loader\dist\cjs.js!C:\Users\86176\Desktop\source\src\css\index.css
          这里是inline loader用法,前面代表等待处理的loade,后面代表等待处理d
    
        最终我们需要将remainingRequest中的路径转化成相对路径,webpack才能处理
          希望得到:../../node_modules/css-loader/dist/cjs.js!./index.css
    
        所以:需要将绝对路径转化成相对路径
        要求:
          1. 必须是相对路径
          2. 相对路径必须以 ./ 或 ../ 开头
          3. 相对路径的路径分隔符必须是 / ,不能是 \
      */
        const relativeRequest = remainingRequest
        .split("!")
        .map((part) => {
            // 根据上下文,将路径转化为相对路径
            const relativePath = this.utils.contextify(this.context, part);
            return relativePath;
        })
        .join("!");
    
        /*
        !!${relativeRequest} 
          relativeRequest:../../node_modules/css-loader/dist/cjs.js!./index.css
          relativeRequest是inline loader用法,代表要处理的index.css资源, 使用css-loader处理
          !!代表禁用所有配置的loader,只使用inline loader。(也就是外面我们style-loader和css-loader),它们被禁用了,只是用我们指定的inline loader,也就是css-loader
    
        import style from "!!${relativeRequest}"
          引入css-loader处理后的css文件
          为什么需要css-loader处理css文件,不是我们直接读取css文件使用呢?
          因为可能存在@import导入css语法,这些语法就要通过css-loader解析才能变成一个css文件,否则我们引入的css资源会缺少
        const styleEl = document.createElement('style')
          动态创建style标签
        styleEl.innerHTML = style
          将style标签内容设置为处理后的css代码
        document.head.appendChild(styleEl)
          添加到head中生效
      */
        const script = `
        import style from "!!${relativeRequest}"
        const styleEl = document.createElement('style')
        styleEl.innerHTML = style
        document.head.appendChild(styleEl)
      `;
    
        // style-loader是第一个loader, 由于return导致熔断,所以其他loader不执行了(不管是normal还是pitch)
        return script;
    };
    
    module.exports = styleLoader;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
  • 相关阅读:
    【用户画像】将数据迁移到ClickHouse(源码实现)、位图的介绍(bitmap)、位图在用户分群中的应用、位图的使用
    设计模式:策略模式和工厂模式混合使用
    vue项目读取全局配置
    储油罐北斗监测方案
    gin实现event stream
    Solidity 小白教程:20. 发送 ETH
    卓岚联网模块连接三菱FX系列PLC应用实例
    Linux ARM平台开发系列讲解(PCIE) 2.13.1 从软件的角度去理解PCIE
    kotlin基础教程:<3>函数的高级用法和字符串的基础操作
    【操作系统】第三章 —— 如何管理物理内存
  • 原文地址:https://blog.csdn.net/qq_56303170/article/details/126532664