2

什么是 Loader

webpack 打包时只能处理 js 文件,对于其他类型的文件如 jsx, css, scss, vue, png 等文件,需要专门的东西处理一下再传入 webpack,这个东西就是 loader

loader 用于对模块的源代码进行转换。loader 可以使你在 import 或"加载"模块时预处理文件。因此,loader 类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的强大方法。loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript,或将内联图像转换为 data URL。loader 甚至允许你直接在 JavaScript 模块中 import CSS文件!

Loader 的开发

在开发自己 loader 之前,我们得知道 loader 是啥

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

说白了,loader 就是一个函数,接收源模块,然后处理一番,再导出去,给下一个 loader 或者 webpack

module.exports = function(source) {
    // handle source
    ...
    return handled source
}

关于开发一个 loader 遵循的一些原则,大家可以去看文档,本文以一个处理 txt 文件的小例子来说明如何开发一个 loader。目录结构如下

image.png

// webpack.config.js
const path = require('path')
module.exports = {
  mode: 'development',
  entry:  __dirname + "/src/app.js",
  output: {
    path: __dirname + "/dist",
    filename: "bundle.js"
  },
  module: {
    rules: [
      {
        test: /\.txt$/,
        use: [
          'text-loader'
        ]
      }
    ]
  }
}

loaders 文件中存放我们的 txt-loader.js

// txt-loader.js
module.exports = function(source) {
    console.log(source)
}

源文件 name.txt

// name.txt
hello [name]!

入口文件 app.js

// app.js
const name = require('./name.txt')
console.log(name)

先执行走一波,终端执行

./node_modules/.bin/webpack

肯定会报错,因为我们的 loader 还没有写完,但是源文件内容已经打印出来了

image.png

报错信息也说,这个 loader没有返回 Buffer 或者 String

txt-loader 要做的事情就是将任何 .txt 文件中的 [name] 直接替换为我们想要的名字,然后返回包含默认导出文本的 JavaScript 模块。

需要注意的是,我们不能再 loader 里面将这个名字写死,而应该在使用 loader 的时候以配置的形式传进去,我们平时看到的 loader 一般都有个 options 选项,就是为了传些配置进去

{
    test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
    loader: 'url-loader',
    options: {
       limit: 10000,
       name: utils.assetsPath('img/[name].[hash:7].[ext]')
    }
},

我们给 txt-loader 加个配置选项

// webpack.config.js
...
{
    test: /\.txt$/,
    use: {
        loader: path.resolve(__dirname, './src/loaders/txt-loader.js'),
        options: {
            name: 'Jay'
        }
    }
}
...

那我们在 txt-loader.js 怎么接收配置呢?webpack 提供了一个 loader 工具库

// txt-loader.js
const loaderUtils = require('loader-utils')

module.exports = function(source) {
    this.cacheable && this.cacheable()
    const options = loaderUtils.getOptions(this) || {}
    console.log(options)
    
    source = source.replace(/\[name\]/g, options.name)
    console.log(source)
    
    return source
}

执行一下

image.png

发现我们期待的结果打印出来了,但是还是报错了,报错信息说还需要额外的 loader 去处理当前 loader 的结果。

有时我们处理某种类型的文件需要多个 loader,这些 loader 的执行顺序和 use 数组中 loader 书写顺序是相反的,如解析 scss 文件时

{//处理.scss文件
    test: /\.scss$/,
    use: [{
        loader: "style-loader" // creates style nodes from JS strings
    }, {
        loader: "css-loader" // translates CSS into CommonJS
    }, {
        loader: "sass-loader" // compiles Sass to CSS
    }]
},

上面例子 webpack 是先经过 sass-loader,然后将结果传入 css-loader,最后再进入 style-loader

链路中间的 loader 返回什么样的结果都行,只要下一个接收的 loader 能够正常处理就行,但是最后一个调用 loader 的结果是需要传入至 webpack 中,webpack 期望它返回 JS 代码,以及可选的source map

注意:如果是处理顺序排在最后一个的 loader,那么它的返回值将最终交给 webpack 的 require,换句话说,它一定是一段可执行的 JS 脚本 (用字符串来存储),更准确来说,是一个 node 模块的 JS 脚本。
// 处理顺序排在最后的 loader
module.exports = function (source) {
    // 这个 loader 的功能是把源模块转化为字符串交给 require 的调用方
    return `module.exports = ${JSON.stringify(source)}`
}

本例处理 txt 文件只有一个 txt-loader,最终传入至 webpack 中的是 hello Jay!,不是个可执行的 JS 脚本。最终代码如下

module.exports = function(source) {
    this.cacheable && this.cacheable()
    const options = loaderUtils.getOptions(this) || {}
    source = source.replace(/\[name\]/g, options.name)
    return `module.exports = ${JSON.stringify(source)}`
}

dist 中生成的 bundle.js 文件放入浏览器控制台中运行一下,可以看到输出 hello Jay!

image.png

我们再看一个使用多个 loader 的例子,处理 html 文件并压缩,解析 html 并使之成为 JS 可执行的脚本的任务就交给现有的 html-loader,压缩的任务就咱们自己来实现,就叫 html-optimize-loader 吧。

修改一下 webpack.config.js

// webpack.config.js
...
module: {
    rules: [
        {
            test: /\.txt$/,
            use: {
                loader: 'name-loader',
                options: {
                    name: 'Jay'
                }
            }
        },
        {
            test: /\.html$/,
            use: ['html-loader',
            {
                loader: 'html-optimize-loader',
                options: {
                    comments: false
                }
            }]
        }
    ]
},
resolveLoader: {
// html-loader 在 'node_modules'
modules: ['node_modules', path.resolve(__dirname, './src/loaders')]
},
...

这里我们改成多个 loader 配置的模式,也在我们新加的 html-optimize-loader 中加入了配置,压缩时是否保留注释。

src 中新建 test.html 文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <!-- 这里是 main 内容 -->
</body>
</html>

入口文件 app.js 改成 test.html

const html = require('./test.html')
console.log(html)

loaders 文件中新建 html-optimize-loader.js

// hmtl-optimize-loader.js
const Minimize = require('minimize')
const loaderUtils = require('loader-utils')

module.exports = function (source) {
    var callback = this.async()
    this.cacheable && this.cacheable()

    var options = loaderUtils.getOptions(this) || {} 
    var minimize = new Minimize(options)
    console.log(source)
    console.log(minimize.parse(source))
    return minimize.parse(source, callback)
}

这里 loader 我们采用异步的方式,执行一下

image.png

发现 source 和压缩后的 source 都打印出来了,这里我们直接将压缩后 source 直接传入 html-loader 中去处理了。大家可以将 options 中的 comment 设成 true,发现注释就会保留了,最终生成的 bundle 文件也可以丢进浏览器的控制台跑一下。

就这样咯,下一篇写实现一个 webpack plugin


见贤思齐
66 声望8 粉丝

写代码的