深入理解 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 一般在 webpack.config.js 文件中指定配置。
先安装相对应的 loader:
npm install --save-dev css-loader
npm install --save-dev ts-loader
然后在配置文件中指示 webpack 对每个 .css 使用 css-loader,以及对所有 .ts 文件使用 ts-loader:
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:
module.exports = function (source) {
return source.replace(/var/g, 'const')
}
到这里,一个简单的 loader 就写好了,接下来配置并测试 loader:
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')
}
]
}
]
}
}
入口文件:
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 生效了。