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",
    },
  ],
},

我们可以使用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",
    },
  ],
}

使用方式

  • 配置方式:在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
    //        }
    //}
    
    • 使用 ! 将资源中的 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'
    

    这会对 .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;
};

它接受要处理的源码作为参数,输出转换后的 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;
};

多层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
};

注意:不用在同步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)
}

虽然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

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

Pitching Loader

module.exports = function (content) {
  return content;
};
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
  console.log("do somethings");
};

执行顺序

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, "");
};

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}`;
};
{
  "type": "object", 
  "properties": {
    "author": {
      "type": "string"
    }
  },
  "additionalProperties": false
}
  • type —— options的类型
  • properties —— 标注字段名和类型
  • additionalProperties —— 是否可追加属性

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

  • 下载依赖

    npm i @babel/core @babel/preset-env -D
    
  • 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);
        });
    };
    
  • loaders/banner-loader/schema.json

    {
      "type": "object",
      "properties": {
        "presets": {
          "type": "array"
        }
      },
      "additionalProperties": true
    }
    

file-loader 文件输出

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

  • 下载包

    npm i loader-utils -D
    
  • 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;
    

    loaderUtils.interpolateName参数

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

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

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
    }
    
  • 借用css-loader解决样式中引入的其他资源的问题

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

    问题是: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
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值