# webpack loader 开发入门

# webpack loader 简介 & 基本原理

通过 webpack loaders (opens new window) 中可以了解到,webpack 默认只能识别 JavaScript 以及 JSON 文件。

如果要让 webpack 识别其它类型的文件,那么就要使用对应的 loader 来将文件转化为 webpackmodules (opens new window)。在 Supported Module Types (opens new window) 中了解到 webpackmodule 可以是 ES模块、 CommonJS 模块等等。

再结合 raw-loader (opens new window),该插件能让 webpack 加载文件转化为 String(现在不推荐使用了,因为 webpack5 内置 asset-modules (opens new window))。从它的源码片段中可见,其核心就是读取文件之后,将文件内容包裹、返回 ES 或者 CommonJS 模块。

export default function rawLoader(source) {
  ...,

  const json = JSON.stringify(source)
    .replace(/\u2028/g, '\\u2028')
    .replace(/\u2029/g, '\\u2029');

  ...,

  return `${esModule ? 'export default' : 'module.exports ='} ${json};`;
}

经过上面的简单分析可以得出一个 webpack loader 有以下特点:

  • loader 中能够获取到一个字符串 source,该字符串就是文件的内容。
  • loader 中需要返回一个 webpack 所能识别的 module

知道这几点之后,经常使用的 css-loadervue-loader,似乎也不再那么神秘。我们能够自由改变、组装 source,在日后开发中又多了一个能够解决问题的方向了。

# loader 基本骨架

在开发中经常需要根据开发、生产等不同环境注入、替换不同的配置,简单来说就是字符串替换。当然社区中已有成熟的方案了,比如 DefinePlugin (opens new window)string-replace-loader (opens new window)。下面就实现一个简单的字符串替换 loader

webpack loaders api (opens new window) 中,可知一个 loader 就是一个函数,如下。

/**
 * @param {string|Buffer} content 源码文件内容
 * @param {object} [map] SourceMap 数据
 * @param {any} [meta] 其它自定义的元数据
 */
function webpackLoader(content, map, meta) {
  // code of your webpack loader
}

新建 src/replace.js,作为 loader 根入口,内容如下。

module.exports = function replaceLoader(source) {
  console.log('replaceLoader')
  return source
}

# 使用 loader

webpack.config.js 中使用 loader

const path = require('path')
const resolve = (...args) => path.resolve(process.cwd(), ...args)

module.exports = {
  entry: resolve('test', 'file-for-test.js'),
  output: {
    filename: 'main.js',
    path: resolve('dist')
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: resolve('src/replace.js')
          }
        ]
      }
    ]
  }
}

执行 webpack 构建,可以看见控制台输出了 replaceLoader 字样,说明 loader 调用成功。

# 完善 loader 逻辑

# 参数设计

loader 其实就是调用 JavaScriptreplace 方法,所以参数的基本属性会有两个,分别是:

  • search: 替换源目标,可以是字符串、也可以是一个正则。
  • replace:替换结果,可以是一个字符串,也可以是一个函数。

因为可能会需要多组匹配、替换,那么应该是一个对象数组。 loader 的参数可以是下方这么一个对象。

const options = {
  rules: [
    {
      search: 'webpack',
      replace: 'replaced'
    },
    {
      search: /webpack1/g,
      replace: () => 'replaced'
    }
  ]
}

# 获取参数

loader-utils (opens new window) 中包含了许多在编写 loader 时实用的工具方法。 其中的 getOptions 方法,能够获取调用 loader 时候传入的参数。 webpack5 已经内置了 this.getOptions(schema) (opens new window) 方法,但是目前为了更好的兼容性还是使用 loader-utils

  • 安装 loader-utils

    npm i loader-utils --save
    
  • 新建 /src/getOptions.js,该模块专门用来获取、处理参数。

    const { getOptions } = require('loader-utils')
    
    module.exports = (ctx) => {
      const options = getOptions(ctx)
      return options
    }
    

# 替换 source

  • 在主入口 /src/replace.js,中获取参数,进行字符串替换。

    const getOptions = require('./getOptions')
    
    module.exports = function replaceLoader(source) {
      const { rules } = getOptions(this)
      // 替换
      rules.forEach((rule) => {
        const { search, replace } = rule
        source = source.replace(search, replace)
      })
      return source
    }
    

# 传入参数构建测试

修改 webpack.config.js,传入参数。

...,

module.exports = {
  ...,
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: resolve('src/replace.js'),
            options: {
              rules: [
                {
                  search: 'webpack',
                  replace: 'replace with string'
                },
                {
                  search: /(.+)(loader)(.+)/,
                  replace: (match, $1, $2, $3) => `${$1}replace with regex${$3}`
                }
              ]
            }
          }
        ]
      }
    ]
  }
}

执行构建,目标字符串都被成功替换。

# 参数校验

为了能够让使用者在参数传入错误时给出准确的错误提示,这里使用官方推荐的 schema-utils (opens new window) 来对传入的 options 进行校验。

  • 安装依赖。

    npm install schema-utils --save
    
  • 添加校验规则描述文件 /src/schema.json

    {
      "type": "object",
      "required": ["rules"],
      "properties": {
        "rules": {
          "type": "array",
          "items": {
            "type": "object",
            "required": ["search", "replace"],
            "additionalProperties": false,
            "properties": {
              "search": {
                "anyOf": [{ "type": "string" }, { "instanceof": "RegExp" }]
              },
              "replace": {
                "anyOf": [{ "type": "string" }, { "instanceof": "Function" }]
              }
            }
          }
        }
      },
      "additionalProperties": false
    }
    

    上方描述简单来说就是对 options 的参数进行了规则描述,比如 "type": "object",就是声明该参数是一个对象, "required": ["rules"] 声明该对象必须存在 rules 属性,"properties" 就是对该对象其它属性进行描述。

    具体用法可查看 schema-utils (opens new window) 说明,官方 README 并没有太详细的说明,那么也可以从其 单元测试用例 schema.json (opens new window) 中查看、参考更多不同的用法。

  • 调用校验方法。

    修改 getOptions.js

    const { getOptions } = require('loader-utils')
    const { validate } = require('schema-utils')
    const schema = require('./schema.json')
    
    module.exports = (ctx) => {
      const options = getOptions(ctx)
      // 参数检验
      validate(schema, options, {
        name: 'webpack replace loader'
      })
      return options
    }
    

配置完成之后,尝试传入不合法的 options,再次执行构建,可以看见控制台报错,并且输出了可读性较高的错误提示。

# 单元测试

loader 已经完成,接下来添加单元测试,来进一步保证 loader 的正确运行。

  • 安装、使用 jest (opens new window) 作为我们的测试框架。

    npm install --save-dev jest
    
  • 配置 jest,添加 jest.config.js 配置文件。

    这里声明了在 test 目录中,以 .spec.js 结尾的文件,都是单元测试文件。

    module.exports = {
      testMatch: ['**/test/**/*.spec.js']
    }
    
  • 编写单元测试。

    新增 test/index.spec.js 文件。单元测试其实就是编写函数来判断方法的输入、输出是否符合预期,所以要想办法拿到 webpack 的构建结果。可以使用 webpackNode Interface (opens new window),来达成这一目的。

    下方的两个用例都使用了 webpackNode Interface 来进行构建,在 stats (opens new window) 对象中可以获取输出的构建内容。

    const webpack = require('webpack')
    
    test('Replace correctly', async (done) => {
      webpack(require('./webpack.config.js'), (err, stats) => {
        const error = err || stats.hasErrors()
        const json = stats.toJson({ source: true })
        const output = json.modules[0].source
        expect(output).toEqual(
          expect.stringContaining('Hello replace with string replace with regex.')
        )
        done(error)
      })
    })
    
    test('Invalid options', async (done) => {
      const options = require('./webpack.config.js')
      options.module.rules[0].use[0].options.rules = 1
      webpack(options, (err, stats) => {
        const error = err || stats.hasErrors()
        expect(error).toBeTruthy()
        done()
      })
    })
    

执行 jest 测试,可以看到两个用例通过。

本文相关实践代码可从 https://github.com/xuwenchao66/webpack-loader-practice (opens new window) 中查阅。

# 参考

  1. webpack loaders api (opens new window)
  2. webpack loaders (opens new window)
  3. webpack writing-a-loader (opens new window)
上次更新: 7/3/2021, 11:12:19 AM