深入理解webpack中的loader

57 阅读4分钟

为什么会有Loader?

默认情况下,即在不安装其它Loader时,webpack仅支持处理JS和JSON这两种文件类型,没办法处理其它类型的文件比如css文件、png图片等。因此,在打包时,需要一种工具来处理此类文件,loader就产生了。webpack就是通过Loader去支持其他文件类型并且把他们转化成有效的模块,并且可以添加到依赖图中。

给Loader下个定义?

定义:Loader是一个导出为函数的JS模块,接受源代码,返回处理后的代码。

也就是说Loader本身是一个函数,将源文件作为参数,返回转换后的文件,比如将TS的代码转成JS代码,将图像转成base64的字符串 。

以下就是一个最简单的loader,接受源码返回源码

module.exports = function(source) {
    return source
}

Loader的执行顺序是怎样的?

多个Loader串行执行,顺序是从后到前

从后到前,从下到上,从右到左,这些说的都是从数组的尾loader执行到首loader

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
    publicPath: 'dist/'
  },
  module: {
    rules: [
      {
        test: /.css$/, // test 指定匹配规则
        use: [ // use 指定使用的loader名称
          'style-loader',
          'css-loader',
          'less-loader'
        ]
      }
    ]
  }
}

以上webpack的配置文件,会先执行less-loader,然后将执行的结果传给css-loader,执行css-loader,然后将执行结果传递给style-loader,然后执行style-loader。处理less文件,为什么需要这么loader呢,这是因为webpack提倡保持loader的功能单一,loader应该只做单一的任务,不仅使loader已维护,也方便在更多场景中进行链式调用。

您可能会问怎样设计这样的顺序?既然每一个loader本质上是一个函数,那么多个loader就是函数的组合。函数的组合可以参考以下这种形式(webpack采取的就是这种)

compose(f, g) => (...args) => f(g(...args))

常用的loader

名称描述
babel-loader转换ES6/ES7等JS新特性语法
css-loader支持.css文件的加载和解析
less-loader将less文件转换成css
ts-loader将ts转换成JS
file-loader进行图片、字体等的打包
raw-loader将文件以字符串的形式导入
thread-loader多进程打包JS和CSS

loader-runner

loader-runner 模块提供了方法,让我们方便的运行和测试loader,它允许您在不安装webpack的情况下运行loader。

loader-runner也作为webpack的依赖,webpack中就是使用它执行了loader,我们也可以使用loader-running进行loader的开发和调试。

loader-runner的使用

import { runLoaders } from "loader-runner";

runLoaders({
	resource: "/abs/path/to/file.txt?query",
    // 字符串:资源的绝对路径(查询字符串可选)

	loaders: ["/abs/path/to/loader.js?query"],
    // 字符串数组:loader的绝对路径(查询字符串可选)
	// {loader, options}[]: 带有options对象的加载器的绝对路径

	context: { minimize: true },
	// 除了基本的上下文,添加额外的loader上下文

	processResource: (loaderContext, resourcePath, callback) => { ... },
    // 可选:处理资源的函数
	// Must have signature  必须是具名函数 有方法名,有参数 function(context, path, function(err, buffer))
	// By default readResource is used and the resource is added a fileDependency

	readResource: fs.readFile.bind(fs)
    // 可选:读取资源的函数
	// 仅当processResource没有被提供时使用
	// Must have signature function(path, function(err, buffer))
	// 默认是s.readFile
}, function(err, result) {
	// err: Error?

	// result.result: Buffer | String
	// The result
	// only available when no error occurred

	// result.resourceBuffer: Buffer
	// The raw resource as Buffer (useful for SourceMaps)
	// only available when no error occurred

	// result.cacheable: Bool
	// Is the result cacheable or do it require reexecution?

	// result.fileDependencies: String[]
	// An array of paths (existing files) on which the result depends on

	// result.missingDependencies: String[]
	// An array of paths (not existing files) on which the result depends on

	// result.contextDependencies: String[]
	// An array of paths (directories) on which the result depends on
})

以开发测试一个raw-loader为例

raw-loader.js

module.exports = function(source) {
    const json = JSON.stringify(source)
        .replace('foo', '')
        .replace(/\u2028/g, '\\u2028')
        .replace(/\u2029/g, '\\u2029');
    return `export default ${json}`; // loader的返回值
}

使用run-loader.js测试

const { runLoaders } = require('loader-runner');
const fs = require('fs');
const path = require('path');
runLoaders({
    resource: path.join(__dirname, './src/demo.txt'), // 源文件
    loaders: [
        {
            loader: path.join(__dirname, './src/raw-loader.js'), // 指定loader位置
            options: { // 传递给loader携带的额外参数
                name: 'test'
            }
        }
    ],
    context: {
        emitFile: () => {}
    },
    readResource: fs.readFile.bind(fs)
}, (err, result) => {
    err ? console.log(err) : console.log(result);
});

运行run-loader得到结果

{
  result: [ 'export default "bar"' ],
  resourceBuffer: <Buffer 66 6f 6f 62 61 72>,
  cacheable: true,
  fileDependencies: [
    'e:\\project\\learn-webpack\\geektime-webpack-course\\code\\chapter07\\raw-loader\\src\\demo.txt'
  ],
  contextDependencies: []
}

获取传递给loader的参数

如果需要获取传递给loader参数,如run-loader.js代码中的第10行的参数name: test,可以记住loader-utils模块中的方法获取

const loaderUtils = require('loader-utils')
module.exports = function (source) {
  const { name } = loaderUtils.getOptions(this) // 获取options中传递的name参数
  console.log(this.query.name) // 这种方式也可以获取到name参数
}

loader中的异常处理-抛出错误

loader内直接可以通过throw抛出

throw new Error('Error')

或者通过this.callback抛出

this.callback(

​ err: Error | null,

​ content: string | Buffer,

​ sourceMap? : SourceMap

​ meta?: any

)

this.callback(new Error('Error', json))
// 或者
this.callback(null, json, 2, 3, 4) // 第一个参数必须是null, 后面的参数是返回的其它参数

借助callback还有另外一个好处,可以返回多个值,而return 只能返回一项

loader中的异步处理

有些耗时的操作使得loader变成异步的loader,比如文件的读取或者图像的处理

通过this.async来返回一个异步函数,第1个参数是Error,第2个参数是处理后的结果

以读取文件的内容并返回为例

const fs = require('fs') 
const path = reqiure('path')
module.exports = function (source) {
  const callback = this.async()
  // 读取文件async.txt中的文件并返回
  fs.readFile(path.join(__dirname, './async.txt'), 'utf-8', (err, data) => {
    if (err) {
      callback(err, '')
      return
    }
    callback(null, data)
  })
}

开启loader缓存

webpack中默认开启loader缓存, 也可以关掉缓存

this.cacheable(false) // 关闭loader缓存

缓存条件:loader的结果在相同的输入下有确定的输出,也就是说做了记忆化的处理

有依赖的loadr无法使用缓存?

loader输出文件

通过this.emitFile将文件到写入到其它文件中

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

module.exports = function(source) {
    const url = loaderUtils.interpolateName(this, '[name].[ext]', source);
    console.log(url); // 获取到了源文件的名称
    this.emitFile(url, source); // 把源文件写到dist文件下以源文件名称命名的文件中
    return source;
}

Loader编写案例

让我们开发一个Loader,功能是将css中的px值转换成相对根元素以rem为单位的相对值,标准尺寸下即开发时的根元素大小,以参数unit的形式传入,类似px2rem-loader的功能

入口文件index.js

import './index.css'
const a = 1

样式文件index.css

.box {
  width: 100px;
  height: 200px;
  font-size: 40px;
  line-height: 40px;
  color: #0000ff;
  background-color: #deb8e7;
}

webpack配置文件

const path = require('path');
module.exports = {
    entry: './src/index.js',
    output: {
        path: path.join(__dirname, 'dist'),
        filename: 'main.js'
    },
    module: {
        rules: [
            {
              test: /\.css$/,
              use: [
                // px2rem-loader处理后,传递给css-loader将css的样式内容以字符串的形式拼接,并将其作为js的模块导出内容
                'css-loader', 
                {
                  loader:  path.resolve('./loaders/px2rem-loader'),
                  options: {
                    unit: 20, // 开发时,标准尺寸的根元素大小
                  }
                },
              ]
            }
        ]
    }
}

开发的px2rem-loader文件

const loaderUtils = require('loader-utils');
module.exports = function (source) {
 const pxRegExp = /\b(\d+(\.\d+)?)px\b/g; // 匹配所有px单位的正则
  const { unit } = loaderUtils.getOptions(this); // 获取传递给loader的参数 unit
  const callback = this.async(); // 异步回调,当然这里可以不用异步的形式,为了演示上面讲解的loader中的异步处理
  const url = loaderUtils.interpolateName(this, '[name].[ext]', {content: source});
  // 将px单位大小转成以rem为单位的大小
  let result = source.replace(pxRegExp, (match, $1) => {
    return `${Number($1) / unit}rem`
  })
  // 保存转换后的文件
  this.emitFile(url, result);
  // 返回处理后的文件
  callback(null, result)
}

执行npm run build命令即运行webpack打包,打包成功后会在dist目录下得到index.css,main.js文件

Index.css文件内容如下,可以看到所有px大小都转换成了rem大小

.box {
  width: 5rem;
  height: 10rem;
  font-size: 2rem;
  line-height: 2rem;
  color: #0000ff;
  background-color: #deb8e7;
}

转换之后的文件会传递给css-loader,css-loader会将css以字符串的形式放到main.js中,打开main.js可以看到如下内容

o.push([n.i,".box {\r\n  width: 5rem;\r\n  height: 10rem;\r\n  font-size: 2rem;\r\n  line-height: 2rem;\r\n  color: #0000ff;\r\n  background-color: #deb8e7;\r\n}",""])

以上是px2rem-loader的简写版,完整实现可以参考实现源码px2rem-loader

思考:

在webpack中,loader和plugin有什么区别?常见的loader有哪些?说一说loader的执行流程?