webpack入门到放弃-loader(笔记)

70 阅读4分钟

loader

loader 用于对模块的源代码进行转换。loader 可以使你在 import 或 "load(加载)" 模块时预处理文件。因此,loader 类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的得力方式,实际上loader就是一个处理函数,接收文件的内容,处理之后并返回

分类

通过rule.enfore指定loader类别,可选值 'pre | post', 默认为普通loader。loader的默认执行顺序是从下到上, 从右往左。如果loader指定了类别,优先级为 pre > normal > post

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

使用方式

  • 配置方式(推荐): 在 webpack.config.js 文件中指定 loader。
module.exports = {
    //... 其他配置
    module: {
        rules: [
            //loader配置
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader']
            },
            {
                test: /\.s[ac]ss$/,
                use: [
                    {
                        loader: 'sass-loader',
                        enfore: 'pre' // 指定loader的类别, pre Loader优先执行
                    },
                ]
            }
        ]
    }
}
  • 内联方式:在每个 import 语句中显式指定 loader。

通过为内联 import 语句添加前缀,可以覆盖 配置 中的所有 loader, preLoader 和 postLoader:

使用 ! 前缀,将禁用所有已配置的 normal loader(普通 loader)

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

使用 !! 前缀,将禁用所有已配置的 loader(preLoader, loader, postLoader)

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

使用 -! 前缀,将禁用所有已配置的 preLoader 和 loader,但是不禁用 postLoaders

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

loader特性

  • loader 支持链式调用。链中的每个 loader 会将转换应用在已处理过的资源上。一组链式的 loader 将按照相反的顺序执行。链中的第一个 loader 将其结果(也就是应用过转换后的资源)传递给下一个 loader,依此类推。最后,链中的最后一个 loader,返回 webpack 所期望的 JavaScript。
  • loader 可以是同步的,也可以是异步的。
  • loader 运行在 Node.js 中,并且能够执行任何操作。
  • loader 可以通过 options 对象配置(仍然支持使用 query 参数来设置选项,但是这种方式已被废弃)。
  • 除了常见的通过 package.json 的 main 来将一个 npm 模块导出为 loader,还可以在 module.rules 中使用 loader 字段直接引用一个模块。
  • 插件(plugin)可以为 loader 带来更多特性。
  • loader 能够产生额外的任意文件。

解析 loader

loader 遵循标准 模块解析 规则。多数情况下,loader 将从 模块路径 加载(通常是从 npm installnode_modules 进行加载), 也可以通过配置module.rules指定本地loader文件

工具和LoaderAPI

  • loaderAPI: 上下文中提供如getOptions, emitFile等方法
  • webpack/loader-utils:提供了一系列诸如读取配置、requestString 序列化与反序列化、计算 hash 值之类的工具函数
  • webpack/schema-utils:参数校验工具

编写loader

loader 是导出为一个函数的 node 模块。该函数在 loader 转换资源的时候调用。给定的函数将调用 Loader API,并通过 this 上下文访问。

用法准则

编写 loader 时应该遵循以下准则。它们按重要程度排序,有些仅适用于某些场景

  • 保持 简单 。
  • 使用 链式 传递。
  • 模块化 的输出。
  • 确保 无状态 。
  • 使用 loader utilities 。
  • 记录 loader 的依赖 。
  • 解析 模块依赖关系 。
  • 提取 通用代码 。
  • 避免 绝对路径 。
  • 使用 peer dependencies

简单loader

loader导出的是一个函数,当webpack解析资源时, 会调对应的loader去处理资源,loader接收内容作为参数,做相应的处理之后,并将内容返回出去

// src/loaders/console-loader.js
/**
 * @param {*} source 源文件的内容
 * @param {*} map 可以被使用的 SourceMap 数据
 * @param {*} meta meta 数据,可以是任何内容
 * @returns 
 */
module.exports = function(source, map, meta) {
    console.log(source)
    return source
}

// main.js 入口文件
console.log('123')
// webpack.config.js
module.exports = {
    // ...其他配置
    module: {
        rules: [
            {
                test: /\.js$/,
                use: './src/loaders/console-loader'
            }
        ]
    }
}

同步loader

如果是单个处理结果,可以在 同步模式 中直接返回。如果有多个处理结果,则必须调用 this.callback()

function someSyncOperation(content) {// 处理接收的资源内容
    console.log('content: ', content)
    return content;
}
module.exports = function(content, map, meta) {
    // this.callback 是webpack执行loader时,填充的一个内部方法
    // 此方法接收四个参数
    // err: 报错信息, content: 资源内容, map: sourceMap数据, meta: 其他数据
    this.callback(null, someSyncOperation(content), map, meta)
}

异步loader

对于异步 loader,使用 this.async 来获取 callback 函数:

// 存在异步操作,必须要使用异步loader进行处理
module.exports = function(content, map, meta) {
    let callback = this.async()
    setTimeout(() => {
        console.log('test async...')
        // callback接收四个参数 跟this.callback参数一致
        callback(null, content, map, meta)
    } ,1000)
}

loader 最初被设计为可以在同步 loader pipelines(如 Node.js ,使用 enhanced-require),以及 在异步 pipelines(如 webpack)中运行。然而,由于同步计算过于耗时,在 Node.js 这样的单线程环境下进行此操作并不是好的方案,我们建议尽可能地使你的 loader 异步化。但如果计算量很小,同步 loader 也是可以的。

Raw loader(处理图片等资源)

默认情况下,资源文件会被转化为 UTF-8 字符串,然后传给 loader。通过设置 raw 为 true,loader 可以接收原始的 Buffer。每一个 loader 都可以用 String 或者 Buffer 的形式传递它的处理结果。complier 将会把它们在 loader 之间相互转换。

// raw-loader.js
module.exports = function (content) {
  assert(content instanceof Buffer);
  return someSyncOperation(content);
  // 返回值也可以是一个 `Buffer`
  // 即使不是 "raw",loader 也没问题
};
module.exports.raw = true;

Pitch loader

loader 总是 从右到左被调用。有些情况下,loader 只关心 request 后面的 元数据(metadata) ,并且忽略前一个 loader 的结果。在实际(从右到左)执行 loader 之前,会先 从左到右 调用 loader 上的 pitch 方法。

  • 基本定义
module.exports = function (content) {
  return someSyncOperation(content, this.data.value);
};

/*
 * remainingRequest : 当前 loader 之后的资源请求字符串
 * previousRequest : 在执行当前 loader 之前经历过的 loader 列表
 * data : 与 Loader 函数的 `data` 相同,用于传递需要在 Loader 传播的信息
*/
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
  data.value = 42;
};
  • 执行顺序
// webpack.config.js
module.exports = {
  //...
  module: {
    rules: [
      {
        //...
        use: ['a-loader', 'b-loader', 'c-loader'],
      },
    ],
  },
};

调用顺序

|- a-loader `pitch`
  |- b-loader `pitch`
    |- c-loader `pitch`
      |- requested module is picked up as a dependency
    |- c-loader normal execution
  |- b-loader normal execution
|- a-loader normal execution

其次,如果某个 loader 在 pitch 方法中给出一个结果,那么这个过程会回过身来,并跳过剩下的 loader。在我们上面的例子中,如果 b-loader 的 pitch 方法返回了一些东西

module.exports = function (content) {
  return someSyncOperation(content);
};

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

执行顺序则如下:

|- a-loader `pitch`
  |- b-loader `pitch` returns a module
|- a-loader normal execution

loader练习

clean-log-loader

清除js代码中的console.log语句

// clean-log-loader.js
module.exports = function(content) {
    // 将所有的console.log语句去除
    return content.replace(/console\.log\(.*\);?/g, '')
}
// main.js
var a = 12345
console.log('123')
var b = '11111'
// webpack.config.js
module.exports = {
  //...
  module: {
    rules: [
      {
        //...
        use: ['./src/loaders/clean-log-loader'],
      },
    ],
  },
};

编译结果,成功去除了console.log('123')

image.png

babel-loader简易版

babel-loader的作用 是将es6语法转为es5 ,将jsx语法转为js语法,简易版只模拟将es6 转为es5

babel的基础知识

  • Presets预设
    可以被看作是一组 Babel 插件和/或 options 配置的可共享模块。将对应语法进行转义
  • 核心库
    • @babel/parser 将js代码解析成抽象语法树AST
    • @babel/core 对js代码进行语法转换
    • @babel/generator 将抽象语法树转为对应的code代码
    • @babel/traverse 将抽象语法树转为js对象,并增加一些属性

前期准备,因为我们只需要做语法转换需要用到@babel/core@babel/preset-env这两个

  • 安装依赖
npm i @babel/preset-env @babel/core -D
  • 编写schema.json文件 用于支持loader中option选项的映射,
// "additionalProperties": true // 是否允许配置额外的属性 
// 如果设置为false  则options里只能有presets一个属性,其他情况会出现报错
{
    "type": "object",
    "properties": {
        "presets": {
            "type": "array"
        }
    },
    "additionalProperties": true
}
  • babel-loader.js
const babel= require("@babel/core");
const schema = require('./schema.json')

module.exports = function(content) {
    const options = this.getOptions(schema) // 提取给定的 loader 选项,接受一个可选的 JSON schema 作为参数
    const callback = this.async() // 有异步操作 需要使用异步loader
    // 使用babel进行代码编译
    babel.transform(content, options, function(err, result) {
        if(err) callback(err)
        else callback(null, result.code)
    });
}
  • webpack.config.js
module.exports = {
  //...
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
            loader: './src/babel-loader',
            options: {
                presets: ['@babel/preset-env']
            }
        }
      }
    ],
  },
};
  • main.js
const sum = (...args) => {
    return args.reduce((p,c) => p+c, 0)
}
  • 编译打包的结果

image.png

file-loader简易版

webpack5中已经内置了file-loader,url-loader,通过rule中指定type:'asset'来告知webpack资源的类型

  • file-loader.js
// https://github.com/webpack/loader-utils
const loaderUtils = require('loader-utils')

module.exports = function(content) {
    console.log('content: ', loaderUtils)
    // 1.根据文件内容生成对应hash值的文件名
    const interpolatedName = loaderUtils.interpolateName(
        this,
        '[hash].[ext][query]',
        {
            content
        }
    );
    console.log(interpolatedName)
    // 2.将文件输出到输出目录
    // loaderAPI地址: https://webpack.docschina.org/api/loaders/
    this.emitFile(interpolatedName, content)

    // 3. 返回module.exports = '文件路径(文件名)'
    return `module.exports = '${interpolatedName}'`
}
//因为处理的是图片资源  需要使用raw-loader
module.exports.raw = true
  • main.js
import './main.css'
  • main.css
.box1 {
   width: 500px;
   height: 300px;
   background: url('./assets/bg.png');
   background-size: cover;
}
  • webpack.config.js
module.exports = {
  //...
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      },
      {
        test: /\.(png|jpeg|gif)$/,
        loader: './src/file-loader',
        type: 'javascript/auto'
      },
    ],
  },
};
  • index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div class="box1"></div>
    <script src="bundle.js"></script>
</body>
</html>
  • 打包结果
    |- dist
        |- bundle.js
        |- ee6e171b64f6ffb89d31.png
        |- index.html    
  • 显示结果

image.png

style-loader简易版

把 CSS 插入到 DOM 中, 并不会去处理Css中引入的资源(图片、字体等),需要配合css-loader进行资源解析

  • 基础版

动态创建style标签,并拼接到head中,不会处理内部的资源

// style-loader.js
/**
* style-loader不处理样式中的图片资源 ,导致图片或者字体图标引用不到
*/
module.exports = function(content) {
    console.log(typeof content)
    const script = `
        let styleEle = document.createElement('style')
        styleEle.innerHTML = ${JSON.stringify(content)}
        document.head.appendChild(styleEle)
    `
    return script
}

打包之后效果

image.png

  • 进阶版,引入css-loader

引入css-loader之后,style-loader接收到的是一堆js代码,没办法执行获取结果,则需要通过pitch loader,获取剩下需要执行的loader和文件,通过内联loader的方式引入资源文件,并中断后续操作的方法实现

// style-loader.js
module.exports.pitch = function(remainingRequest) {
    // remainingRequest 剩下还需要处理的loader 中间用!分割
    // console.log(remainingRequest) // C:\Users\Lenovo\Desktop\sj\gis\amap\webpack-loader\node_modules\css-loader\dist\cjs.js!C:\Users\Lenovo\Desktop\sj\gis\amap\webpack-loader\src\main.css
    // 将绝对路径转为相对路径 使用loaderAPI中的utils工具
    const relativePath = remainingRequest.split('!').map(absolutepath => {
        return this.utils.contextify(this.context, absolutepath);
    }).join('!')
    console.log(relativePath) //../node_modules/css-loader/dist/cjs.js!./main.css
    // 通过内联方式使用loader
    // !! 使用 !! 前缀,将禁用所有已配置的 loader
    const script = `
        import Styles from '!!${relativePath}';
        let styleEle = document.createElement('style')
        styleEle.innerHTML = Styles
        document.head.appendChild(styleEle)
    `
    // pitch方法return之后 会中断后续的loader
    return script
}