什么是 Loader、手写 Webpack Loader

目录

1. 什么是 Loader

1.1 Loader 工作原理

1.2 Loader 执行顺序

1.3 内联 Loader 前缀​​​​​​​

2. 如何开发 Loader

2.1 Loader 长什么样子

2.2 配置本地 Loader 的四种方法

2.2.1 在配置 rules 时,指定 Loader 的绝对路径

2.2.2 在 resolveLoader 里,配置 alias 别名属性

2.2.3 在 resolveLoader 里,配置 modules 属性

2.2.4 使用 npm link 引用 Loader

2.2.5 配置完成后,测试下 2.1 中的简单 Loader

2.3 带 pitch 的 Loader —— 阻断 Loader 链

2.4 手写 mini style-loader

2.4.1 style-loader 需求描述

2.4.2 mini style-loader 设计思路

2.4.3 mini style-loader 代码编写

2.4.4 使用 mini style-loader

2.4.5 在 Vue 项目中使用 Loader

3. 开发 Loader 必备工具包

4. 开发异步 Loader

5. Loader raw 设为 true,用于支持 二进制格式资源

6. file-loader 基本原理、输出文件

7. Loader 开发约定

8. 参考文章


1. 什么是 Loader

1.1 Loader 工作原理

Webpack 只能直接处理 JavaScript 代码

任何非 JavaScript 文件,都必须被预先处理为 JavaScript 代码,才可以参与打包

Loader(加载器)就是这样一个代码转换器:

  • 它由 Webpack 的 loader runner 执行调用,接收原始资源数据作为参数
  • 当多个加载器联合使用时,上一个 Loader 的结果,会传入下一个 Loader
  • 最终输出 JavaScript 代码(和可选的 source map)给 Webpack 做进一步编译

1.2 Loader 执行顺序

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

执行优先级为:pre > normal > inline > post

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

举个栗子~

use: ['loader1', 'loader2', 'loader3'],执行顺序为 loader3 → loader2 → loader1

1.3 内联 Loader 前缀

内联 Loader 可以通过添加不同前缀,跳过其他类型 Loader

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

1.4 常用的 Loader

名称作用
style-loader用于将 css 编译完成的样式,挂载到页面 style 标签上
css-loader用于识别 .css文件, 须配合 style-loader 共同使用
sass-loader/less-loadercss 预处理器
postcss-loader用于补充 css 样式各种浏览器内核前缀
url-loader处理图片类型资源,可以转 base64
vue-loader用于编译 .vue 文件
worker-loader通过内联 loader 的方式,使用 web worker 功能
style-resources-loader全局引用对应的 css,避免页面再分别引入

2. 如何开发 Loader

2.1 Loader 长什么样子

Loader 本质是一个 Node 模块,该模块导出一个函数

函数接收 source (源文件),返回处理后的source

比如下面的 Loader,接收源码,打印文字,原样返回源码

// loaders/simple-loader.js

// Loader 本质是一个 Node 模块,该模块导出一个函数
module.exports = function loader (source) {
  console.log('Lyrelion simple-loader is working');
  return source;
}

2.2 配置本地 Loader 的四种方法

为了测试 2.1 中的 Loader,需要在 webpack.config.js 里,配置 Loader

Webpack 默认会去 node_modules 里找所有第三方模块

通过 npm 或者 yarn 安装的 Loader,配置时只需直接使用 Loader 的名字,不用关心 Loader 的路径(因为他们都会安装在 node_modules 目录下)

如果使用本地自己开发的 Loader,也就是他们不在 node_modules 里,就需要告诉 Webpack Loader 的位置

在 Webpack4.0 里,有四种方法配置本地 Loader

2.2.1 在配置 rules 时,指定 Loader 的绝对路径

module.exports = {
  // xxx
  module: {
    rules: [
      {
        test: /\.js$/,
        // 在这里配置绝对路径
       use: path.resolve(__dirname, 'loaders/myLoader.js')
      }
    ]
  }
}

2.2.2 在 resolveLoader 里,配置 alias 别名属性

module.exports = {
  // xxx
  resolveLoader: {
  // 配置 resolveLoader.alias
    alias: {
      myLoader: path.resolve(__dirname, 'loaders/myLoader.js')
    }
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'myLoader'
      }
    ]
  }
}

2.2.3 在 resolveLoader 里,配置 modules 属性

将可能放置 Loader 的目录,存放到 resolveLoader.modules 数组中,告诉 Webpack

当 Webpack 在默认目录下,找不到指定 Loader 时,会自动去 resolveLoader.modules 数组 中查找

module.exports = {
  // xxx
  resolveLoader: {
  // 配置 resolveLoader.modules
    modules: ['node_modules', path.resolve(__dirname, 'loaders']
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'myLoader'
      }
    ]
  }
}

2.2.4 使用 npm link 引用 Loader

基本步骤:

  • 把 Loader 从当前项目抽离出来,构建独立工程
  • 在 Loader 工程目录下,执行 npm link,在全局 link 中添加 loader
  • 回到原项目目录,执行 npm link xxx (xxx 为 Loader 的名称),实现 require Loader
  • 最后,在原项目中,直接使用 Loader 名称即可 (跟 npm install 的 Loader 一样用法)

关于 npm link,可以参考下面的文章:

使用 npm link 测试本地编写的 node 模块 / 引入全局安装的 node 模块_Lyrelion的博客-CSDN博客使用 npm link 测试本地编写的 node 模块 / 引入全局安装的 node 模块https://blog.csdn.net/Lyrelion/article/details/128506812

2.2.5 配置完成后,测试下 2.1 中的简单 Loader

在 webpack.config.js 中,输入以下内容:

// webpack.config.js

const path = require('path');
module.exports = {
  entry: {...},
  output: {...},
  module: {
    rules: [
      {
        test: /\.js$/,
        // 指明 Loader 的绝对路径
        use: path.resolve(__dirname, 'loaders/simple-loader')
      }
    ]
  }
}

 

执行打包命令 yarn build,输出结果如下:

 

2.3 带 pitch 的 Loader —— 阻断 Loader 链

pitch 是 Loader 上的一个方法(非必须的),它的作用 —— 阻断 Loader 链

如果有 pitch,Loader 的执行会分为两个阶段:

  • pitch 阶段 —— Webpack 会先 从左到右 执行 Loader 链中,每个 Loader 上的 pitch 方法(如果有)
  • normal execution 阶段 —— Webpack 会再 从右到左 执行 Loader 链中,每个 Loader 上的普通 Loader 方法

举个栗子~~ 假设配置了下面的 Loader

use: ['loader1', 'loader2', 'loader3']

 

Loader 执行过程:

 

在这个过程中,如果任何 pitch 有返回值,则 Loader 链被阻断

Webpack 会跳过后面所有的的 pitch 和 loader,直接进入上一个 Loader 的 normal execution 阶段

 

也就是说,假设在 loader2 的 pitch 中返回了一个字符串,此时 Loader 链发生阻断:

 

pitch 方法有三个参数:

  • remainingRequest:loader 链中排在自己后面的 loader 以及资源文件的绝对路径以!作为连接符组成的字符串
  • precedingRequest:loader 链中排在自己前面的 loader 的绝对路径以!作为连接符组成的字符串
  • data:每个 loader 中存放在上下文中的固定字段,可用于 pitch 给 loader 传递数据

2.4 手写 mini style-loader

// 将 css 内容,通过 style 标签插入到页面中

// source —— 要处理的 css 源文件
function loader(source) {
  let style = `
    let style = document.createElement('style');
    style.setAttribute("type", "text/css"); 
    style.innerHTML = ${source};
    document.head.appendChild(style)`;
  return style;
}
module.exports = loader;

 

2.4.1 style-loader 需求描述

style-loader 通常不会独自使用,而是跟 css-loader 连用

css-loader 的返回值是一个 JavaScript 模块,大致长这样:

// 打印 css-loader 的返回值

// Imports
var ___CSS_LOADER_API_IMPORT___ = require("../node_modules/css-loader/dist/runtime/api.js");
exports = ___CSS_LOADER_API_IMPORT___(false);
// Module
exports.push([module.id, "\nbody {\n    background: yellow;\n}\n", ""]);
// Exports
module.exports = exports;

这个模块在运行时,返回一段字符串代码 —— “\nbody {\n background: yellow;\n}\n”

style-loader 的作用:将这段 css 代码,转成 style 标签,插入到 html 的 head 中

2.4.2 mini style-loader 设计思路

style-loader 最终需返回一个 JavaScript 脚本:在脚本中创建一个 style 标签,将 css 代码赋给 style 标签,再将这个 style 标签插入 html 的 head 中

难点:获取 css 代码;因为 css-loader 的返回值只能在 运行时 的上下文中执行,而执行 Loader 是在编译阶段。换句话说,css-loader 的返回值在 style-loader 里派不上用场


曲线救国方案:使用获取 css 代码的表达式,在运行时再获取 css (类似 require('css-loader!index.css'))

在处理 css 的 loader 中又去调用 inline loader require css 文件,会产生循环执行 loader 的问题:

  • 需要利用 pitch 方法,让 style-loader 在 pitch 阶段返回脚本,跳过剩下的 loader
  • 同时还需要内联前缀 !! 的加持

注:pitch 方法有3个参数:

  • remainingRequest:loader 链中排在自己后面的 loader 以及资源文件的绝对路径以!作为连接符组成的字符串
  • precedingRequest:loader 链中排在自己前面的 loader 的绝对路径以!作为连接符组成的字符串
  • data:每个 loader 中存放在上下文中的固定字段,可用于 pitch 给 loader 传递数据

可以利用 remainingRequest 参数获取 loader 链的剩余部分

2.4.3 mini style-loader 代码编写

// loaders/simple-style-loader.js

const loaderUtils = require('loader-utils');
module.exports = function (source) {
  // do nothing
}

/**
 * @param {*} remainingRequest loader 链中排在自己后面的 loader 以及资源文件的绝对路径以!作为连接符组成的字符串
 * @returns 
 */
module.exports.pitch = function (remainingRequest) {
  console.log('Lyrelion simple-style-loader is working');
  // 在 pitch 阶段返回脚本
  return (
    `
      // 创建 style 标签
      let style = document.createElement('style');

      /**
      * 利用 remainingRequest 参数获取 loader 链的剩余部分
      * 利用 ‘!!’ 前缀跳过其他 loader 
      * 利用 loaderUtils 的 stringifyRequest 方法将模块的绝对路径转为相对路径
      * 将获取 css 的 require 表达式赋给 style 标签
      */
      style.innerHTML = require(${loaderUtils.stringifyRequest(this, '!!' + remainingRequest)});

      // 将 style 标签插入 head
      document.head.appendChild(style);
      `
  )
}

2.4.4 使用 mini style-loader

webpack.config.js

// webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
  entry: {...},
  output: {...},
  // 手动配置 loader 路径
  resolveLoader: {
    modules: [path.resolve(__dirname, 'loaders'), 'node_modules']
  },
  module: {
    rules: [
      {
        // 配置处理 css 的 loader
        test: /\.css$/,
        use: ['simple-style-loader', 'css-loader']
      }
    ]
  },
  plugins: [
    // 渲染首页
    new HtmlWebpackPlugin({
      template: './src/index.html'
    })
  ]
}

src/index.css

body {
  background: pink;
}

src/index.js

require('./index.css');
// let p = require('./nodejs.png');
// console.log(p);

console.log('----------- Lyrelion 测试 Loader -----------');

 

目录整体结构: 

打包后的页面效果展示:

 

2.4.5 在 Vue 项目中使用 Loader

在 vue.config.js 引入 Loader:

const MyStyleLoader = require('./simple-style-loader')

 

在 configureWebpack 中,添加配置:

module.exports = {
  configureWebpack: {
    module: {
      rules: [
        {
          // 对 main.css 文件使用 MyStyleLoader 处理
          test: /main.css/,
          loader: MyStyleLoader
        }
      ]
    }
  }
};

 

3. 开发 Loader 必备工具包

loader-utils

该模块中,常用的几个方法:

  • getOptions 获取 loader 的配置项
  • interpolateName 处理生成文件的名字
  • stringifyRequest 把绝对路径处理成相对根目录的相对路径

 

schema-utils 

该模块用于验证 loader option 配置的合法性

使用方法:

// loaders/simple-loader-with-validate.js

const loaderUtils = require('loader-utils');
const validate = require('schema-utils');
module.exports = function(source) {
  // 获取 loader 配置项
  let options = loaderUtils.getOptions(this) || {};
  // 定义配置项结构和类型
  let schema = {
    type: 'object',
    properties: {
      name: {
        type: 'string'
      }
    }
  }
  // 验证配置项是否符合要求
  validate(schema, options);
  return source;
}

4. 开发异步 Loader

异步 Loader 的开发(例如:需要读取文件的操作),需要通过 this.async() 获取异步回调,然后手动调用它

使用方法:

// loaders/simple-async-loader.js

module.exports = function(source) {
    console.log('async loader');
    let cb = this.async();
    setTimeout(() => {
      console.log('ok');
      // 在异步回调中手动调用 cb 返回处理结果
      cb(null, source);
    }, 3000);
}

PS:异步回调 cb() 的第一个参数是 error,第二个参数是返回结果

5. Loader raw 设为 true,用于支持 二进制格式资源

Webpack 默认是以 utf-8 的格式读取文件内容给 Loader

如果是用于处理 图片、字体 等资源的 Loader,需要将 Loader 上的 raw 属性设置为 true,让 loader 支持二进制格式资源

使用方法:

// loaders/simple-raw-loader.js

module.exports = function(source) {
  // 将输出 buffer 类型的二进制数据
  console.log(source);
  // todo handle source
  let result = 'results of processing source'
  return `
    module.exports = '${result}'
  `;
}

// 告诉 webpack 这个 loader 需要接收的是二进制格式的数据
module.exports.raw = true;

 

6. file-loader 基本原理、输出文件

在开发一些处理资源文件(比如图片、字体等)的 Loader 中,需要拷贝或生成新的文件,可以使用内部的 this.emitFile() 方法

 

file-loader 基本原理:

  • Loader 读取图片内容(buffer),将其重命名
  • 调用 this.emitFile() 输出到指定目录
  • 返回一个模块,这个模块导出重命名后的图片地址
  • 最终实现了:当 require 图片的时候,就相当于 require 了一个模块,从而得到图片路径

基本用法:

// loaders/simple-file-loader.js

const loaderUtils = require('loader-utils');

module.exports = function(source) {
  // 获取 loader 的配置项
  let options = loaderUtils.getOptions(this) || {};

  // 获取用户设置的文件名或者制作新的文件名
  // 注意第三个参数,是计算 contenthash 的依据
  let url = loaderUtils.interpolateName(this, options.filename || '[contenthash].[ext]', {content: source});

  // 输出文件
  this.emitFile(url, source);

  // 返回导出文件地址的模块脚本
  return `module.exports = '${JSON.stringify(url)}'`;
}

module.exports.raw = true;

 

7. Loader 开发约定

Loader 的本质是一个 node 模块,这个模块导出一个函数,这个函数上可能还有一个 pitch 方法

了解了 Loader 的本质、Loader 链的执行机制,其实就已经具备了 Loader 开发基础了

开发 Loader 不难上手,但是要开发一款高质量的 Loader,仍需不断实践

 

在 Webpack 社区,有一份 loader 开发准则:

  • 保持简单
  • 利用多个 loader 链
  • 模块化输出
  • 确保 loader 是无状态的
  • 使用 loader-utils 包
  • 标记加载程序依赖项
  • 解析模块依赖关系
  • 提取公共代码
  • 避免绝对路径
  • 使用 peerDependency 对等依赖项

8. 参考文章

揭秘webpack loader | ChampYin's BlogLoader(加载器) 是 webpack 的核心之一。它用于将不同类型的文件转换为 webpack 可识别的模块。本文将尝试深入探索 webpack 中的 loader,揭秘它的工作原理,以及如何开发一个 loader。https://champyin.com/2020/01/28/%E6%8F%AD%E7%A7%98webpack-loader/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lyrelion

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值