Skip to main content

深入理解 loader 转换器

loader 是一个转换器,用于对源代码进行转换。

loader 可以使你在 import 或"加载"模块时预处理文件。因此,loader 类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的强大方法。

loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript,或将内联图像转换为 data URL。loader 还可以允许你直接在 JavaScript 模块中 import CSS文件!

一、loader 的特性

  • loader 支持链式传递。能够对资源使用流水线(pipeline)。一组链式的 loader 将按照相反的顺序执行。loader 链中的第一个 loader 返回值给下一个 loader。在最后一个 loader,返回 webpack 所预期的 JavaScript。
  • loader 可以是同步的,也可以是异步的。
  • loader 运行在 Node.js 中,并且能够执行任何可能的操作。
  • loader 接收查询参数。用于对 loader 传递配置。
  • loader 也能够使用 options 对象进行配置。
  • 除了使用 package.json 常见的 main 属性,还可以将普通的 npm 模块导出为 loader,做法是在 package.json 里定义一个 loader 字段。
  • 插件(plugin)可以为 loader 带来更多特性。
  • loader 能够产生额外的任意文件。

二、常见的 loader

  • style-loader:将 css 添加到 DOM 的内联样式标签 style 中;
  • css-loader:允许将 css 文件通过 import/require() 的方式引入,并返回 css 代码;
  • sass-loader:处理 sass;
  • less-loader:处理 less;
  • postcss-loader:使用 postcss 来转译 css;
  • file-loader:分发文件到 output 目录并返回相对路径;
  • url-loader:和 file-loader 类似,但是当文件小于设定的 limit 时可以返回一个 Data Url;
  • babel-loader:用 babel 来转换 ES6 文件到 ES5;
  • ts-loader:用于将 TS 转为 JS。

点击查看更多 loader

三、loader 的使用

使用 loader 一般在 webpack.config.js 文件中指定配置。

先安装相对应的 loader:

npm install --save-dev css-loader
npm install --save-dev ts-loader

然后在配置文件中指示 webpack 对每个 .css 使用 css-loader,以及对所有 .ts 文件使用 ts-loader

webpack.config.js
module.exports = {
module: {
rules: [
{ test: /\.css$/, use: 'css-loader' },
{ test: /\.ts$/, use: 'ts-loader' }
]
}
}

其中,use 可以是一个数组,例如:

// ...
module: {
rules: [
{
test: /\.css$/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader',
options: {
modules: true
}
}
]
}
]
}
// ...

注意:

  • loader 要在 module.rules 中定义,而非 rules
  • 使用正则表达式匹配文件时不能添加引号。例如,/\.txt$/'/\.txt$/' 不一样。前者指示 webpack 匹配任何以 .txt 结尾的文件,后者指示 webpack 匹配具有绝对路径 '.txt' 的单个文件。

四、如何实现一个 loader

1、编写简单 loader

loader 其实是一个函数,它的参数是 webpack 传递给 loader 的文件源码,返回结果是处理后的源码。

注意:loader 中的 this 是由 webpack 提供的对象,能够获取当前 loader 所需要的各种信息,this 作为上下文会被 webpack 填充,因此不能将 loader 设为一个箭头函数。

一个基本的 loader 如下:

// 导出一个函数,source 为 webpack 传递给 loader 的文件源内容
module.exports = function (source) {
const content = doSomeThing2JsString(source)

// 如果 loader 配置了 options 对象,那么 this.query 将指向 options
const options = this.query

// 可以用作解析其他模块路径的上下文
console.log('this.context')

/*
* this.callback 参数:
* error:Error | null,当 loader 出错时向外抛出一个 error
* content:String | Buffer,经过 loader 编译后需要导出的内容
* sourceMap:为方便调试生成的编译后内容的 source map
* ast:本次编译生成的 AST 静态语法树,之后执行的 loader 可以直接使用这个 AST,进而省去重复生成 AST 的过程
*/
this.callback(null, content) // 异步
return content // 同步
}

下面是一个最简单的 loader,它什么都没做:

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

下面给 loader 加上功能,将 var 关键词替换为 const

src/loader1.js
module.exports = function (source) {
return source.replace(/var/g, 'const')
}

到这里,一个简单的 loader 就写好了,接下来配置并测试 loader:

webpack.config.js
const path = require('path')

module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: path.resolve('./src/loader1.js')
}
]
}
]
}
}

入口文件:

src/index.js
function test() {
var a = 1
var b = 2
var c = 3
console.log(a, b, c)
}

test()

运行 npm run build,得到打包文件 dist/bundle.js

/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
/******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({

/***/ "./src/index.js":
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
/***/ (() => {

eval("function test() {\n const a = 1\n const b = 2\n const c = 3\n console.log(a, b, c)\n}\n\ntest()\n\n\n//# sourceURL=webpack://webpack-loader-plugins/./src/index.js?");

/***/ })

/******/ });
/************************************************************************/
/******/
/******/ // startup
/******/ // Load entry module and return exports
/******/ // This entry module can't be inlined because the eval devtool is used.
/******/ var __webpack_exports__ = {};
/******/ __webpack_modules__["./src/index.js"]();
/******/
/******/ })()
;

可以看到,打包后的代码中 eval 输出部分的 var 已经变成了 const

2、编写异步 loader

上面实现的 loader 是一个同步 loader,在处理完源码后用 return 返回。

下面来实现一个异步 loader:

module.exports = function (source) {
const callback = this.async()

// 由于有 3 秒延迟,所以打包时需要 3+ 秒的时间
setTimeout(() => {
callback(null, `${source.replace(/;/g, '')}`)
}, 3000)
}

异步 loader 需要调用 webpack 的 async() 生成一个 callback,它的第一个参数是 error,这里可设为 null,第二个参数就是处理后的源码。当异步处理完源码后,调用 callback 即可。

调用同步 loader 的打包时间:

调用异步 loader 的打包时间:

可以看到延迟了 3 秒,说明异步 loader 生效了。