loader
loader 用于对模块的源代码进行转换。loader 可以使你在
import或 "load(加载)" 模块时预处理文件。因此,loader 类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的得力方式,实际上loader就是一个处理函数,接收文件的内容,处理之后并返回
分类
通过
rule.enfore指定loader类别,可选值 'pre | post', 默认为普通loader。loader的默认执行顺序是从下到上, 从右往左。如果loader指定了类别,优先级为 pre > normal > post
- pre 前置loader
- normal 普通loader
- inline 内联loader
- post 后置loader
使用方式
- 配置方式(推荐): 在 webpack.config.js 文件中指定 loader。
module.exports = {
//... 其他配置
module: {
rules: [
//loader配置
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.s[ac]ss$/,
use: [
{
loader: 'sass-loader',
enfore: 'pre' // 指定loader的类别, pre Loader优先执行
},
]
}
]
}
}
- 内联方式:在每个
import语句中显式指定 loader。
通过为内联
import语句添加前缀,可以覆盖 配置 中的所有 loader, preLoader 和 postLoader:
使用 ! 前缀,将禁用所有已配置的 normal loader(普通 loader)
import Styles from '!style-loader!css-loader?modules!./styles.css';
使用 !! 前缀,将禁用所有已配置的 loader(preLoader, loader, postLoader)
import Styles from '!!style-loader!css-loader?modules!./styles.css';
使用 -! 前缀,将禁用所有已配置的 preLoader 和 loader,但是不禁用 postLoaders
import Styles from '-!style-loader!css-loader?modules!./styles.css';
loader特性
- loader 支持链式调用。链中的每个 loader 会将转换应用在已处理过的资源上。一组链式的 loader 将按照相反的顺序执行。链中的第一个 loader 将其结果(也就是应用过转换后的资源)传递给下一个 loader,依此类推。最后,链中的最后一个 loader,返回 webpack 所期望的 JavaScript。
- loader 可以是同步的,也可以是异步的。
- loader 运行在 Node.js 中,并且能够执行任何操作。
- loader 可以通过
options对象配置(仍然支持使用query参数来设置选项,但是这种方式已被废弃)。 - 除了常见的通过
package.json的main来将一个 npm 模块导出为 loader,还可以在 module.rules 中使用loader字段直接引用一个模块。 - 插件(plugin)可以为 loader 带来更多特性。
- loader 能够产生额外的任意文件。
解析 loader
loader 遵循标准 模块解析 规则。多数情况下,loader 将从 模块路径 加载(通常是从 npm install, node_modules 进行加载), 也可以通过配置module.rules指定本地loader文件
工具和LoaderAPI
- loaderAPI: 上下文中提供如getOptions, emitFile等方法
- webpack/loader-utils:提供了一系列诸如读取配置、requestString 序列化与反序列化、计算 hash 值之类的工具函数
- webpack/schema-utils:参数校验工具
编写loader
loader 是导出为一个函数的 node 模块。该函数在 loader 转换资源的时候调用。给定的函数将调用 Loader API,并通过
this上下文访问。
用法准则
编写 loader 时应该遵循以下准则。它们按重要程度排序,有些仅适用于某些场景
- 保持 简单 。
- 使用 链式 传递。
- 模块化 的输出。
- 确保 无状态 。
- 使用 loader utilities 。
- 记录 loader 的依赖 。
- 解析 模块依赖关系 。
- 提取 通用代码 。
- 避免 绝对路径 。
- 使用 peer dependencies
简单loader
loader导出的是一个函数,当webpack解析资源时, 会调对应的loader去处理资源,loader接收内容作为参数,做相应的处理之后,并将内容返回出去
// src/loaders/console-loader.js
/**
* @param {*} source 源文件的内容
* @param {*} map 可以被使用的 SourceMap 数据
* @param {*} meta meta 数据,可以是任何内容
* @returns
*/
module.exports = function(source, map, meta) {
console.log(source)
return source
}
// main.js 入口文件
console.log('123')
// webpack.config.js
module.exports = {
// ...其他配置
module: {
rules: [
{
test: /\.js$/,
use: './src/loaders/console-loader'
}
]
}
}
同步loader
如果是单个处理结果,可以在 同步模式 中直接返回。如果有多个处理结果,则必须调用
this.callback()
function someSyncOperation(content) {// 处理接收的资源内容
console.log('content: ', content)
return content;
}
module.exports = function(content, map, meta) {
// this.callback 是webpack执行loader时,填充的一个内部方法
// 此方法接收四个参数
// err: 报错信息, content: 资源内容, map: sourceMap数据, meta: 其他数据
this.callback(null, someSyncOperation(content), map, meta)
}
异步loader
对于异步 loader,使用
this.async来获取callback函数:
// 存在异步操作,必须要使用异步loader进行处理
module.exports = function(content, map, meta) {
let callback = this.async()
setTimeout(() => {
console.log('test async...')
// callback接收四个参数 跟this.callback参数一致
callback(null, content, map, meta)
} ,1000)
}
loader 最初被设计为可以在同步 loader pipelines(如 Node.js ,使用 enhanced-require),以及 在异步 pipelines(如 webpack)中运行。然而,由于同步计算过于耗时,在 Node.js 这样的单线程环境下进行此操作并不是好的方案,我们建议尽可能地使你的 loader 异步化。但如果计算量很小,同步 loader 也是可以的。
Raw loader(处理图片等资源)
默认情况下,资源文件会被转化为 UTF-8 字符串,然后传给 loader。通过设置
raw为true,loader 可以接收原始的Buffer。每一个 loader 都可以用String或者Buffer的形式传递它的处理结果。complier 将会把它们在 loader 之间相互转换。
// raw-loader.js
module.exports = function (content) {
assert(content instanceof Buffer);
return someSyncOperation(content);
// 返回值也可以是一个 `Buffer`
// 即使不是 "raw",loader 也没问题
};
module.exports.raw = true;
Pitch loader
loader 总是 从右到左被调用。有些情况下,loader 只关心 request 后面的 元数据(metadata) ,并且忽略前一个 loader 的结果。在实际(从右到左)执行 loader 之前,会先 从左到右 调用 loader 上的
pitch方法。
- 基本定义
module.exports = function (content) {
return someSyncOperation(content, this.data.value);
};
/*
* remainingRequest : 当前 loader 之后的资源请求字符串
* previousRequest : 在执行当前 loader 之前经历过的 loader 列表
* data : 与 Loader 函数的 `data` 相同,用于传递需要在 Loader 传播的信息
*/
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
data.value = 42;
};
- 执行顺序
// webpack.config.js
module.exports = {
//...
module: {
rules: [
{
//...
use: ['a-loader', 'b-loader', 'c-loader'],
},
],
},
};
调用顺序
|- a-loader `pitch`
|- b-loader `pitch`
|- c-loader `pitch`
|- requested module is picked up as a dependency
|- c-loader normal execution
|- b-loader normal execution
|- a-loader normal execution
其次,如果某个 loader 在 pitch 方法中给出一个结果,那么这个过程会回过身来,并跳过剩下的 loader。在我们上面的例子中,如果 b-loader 的 pitch 方法返回了一些东西
module.exports = function (content) {
return someSyncOperation(content);
};
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
if (someCondition()) {
return (
'module.exports = require(' +
JSON.stringify('-!' + remainingRequest) +
');'
);
}
};
执行顺序则如下:
|- a-loader `pitch`
|- b-loader `pitch` returns a module
|- a-loader normal execution
loader练习
clean-log-loader
清除js代码中的console.log语句
// clean-log-loader.js
module.exports = function(content) {
// 将所有的console.log语句去除
return content.replace(/console\.log\(.*\);?/g, '')
}
// main.js
var a = 12345
console.log('123')
var b = '11111'
// webpack.config.js
module.exports = {
//...
module: {
rules: [
{
//...
use: ['./src/loaders/clean-log-loader'],
},
],
},
};
编译结果,成功去除了console.log('123')
babel-loader简易版
babel-loader的作用 是将es6语法转为es5 ,将jsx语法转为js语法,简易版只模拟将es6 转为es5
babel的基础知识
- Presets预设
可以被看作是一组 Babel 插件和/或options配置的可共享模块。将对应语法进行转义 - 核心库
- @babel/parser 将js代码解析成抽象语法树AST
- @babel/core 对js代码进行语法转换
- @babel/generator 将抽象语法树转为对应的code代码
- @babel/traverse 将抽象语法树转为js对象,并增加一些属性
前期准备,因为我们只需要做语法转换需要用到@babel/core 和 @babel/preset-env这两个
- 安装依赖
npm i @babel/preset-env @babel/core -D
- 编写schema.json文件 用于支持loader中option选项的映射,
// "additionalProperties": true // 是否允许配置额外的属性
// 如果设置为false 则options里只能有presets一个属性,其他情况会出现报错
{
"type": "object",
"properties": {
"presets": {
"type": "array"
}
},
"additionalProperties": true
}
- babel-loader.js
const babel= require("@babel/core");
const schema = require('./schema.json')
module.exports = function(content) {
const options = this.getOptions(schema) // 提取给定的 loader 选项,接受一个可选的 JSON schema 作为参数
const callback = this.async() // 有异步操作 需要使用异步loader
// 使用babel进行代码编译
babel.transform(content, options, function(err, result) {
if(err) callback(err)
else callback(null, result.code)
});
}
- webpack.config.js
module.exports = {
//...
module: {
rules: [
{
test: /\.js$/,
use: {
loader: './src/babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
],
},
};
- main.js
const sum = (...args) => {
return args.reduce((p,c) => p+c, 0)
}
- 编译打包的结果
file-loader简易版
webpack5中已经内置了file-loader,url-loader,通过rule中指定type:'asset'来告知webpack资源的类型
- file-loader.js
// https://github.com/webpack/loader-utils
const loaderUtils = require('loader-utils')
module.exports = function(content) {
console.log('content: ', loaderUtils)
// 1.根据文件内容生成对应hash值的文件名
const interpolatedName = loaderUtils.interpolateName(
this,
'[hash].[ext][query]',
{
content
}
);
console.log(interpolatedName)
// 2.将文件输出到输出目录
// loaderAPI地址: https://webpack.docschina.org/api/loaders/
this.emitFile(interpolatedName, content)
// 3. 返回module.exports = '文件路径(文件名)'
return `module.exports = '${interpolatedName}'`
}
//因为处理的是图片资源 需要使用raw-loader
module.exports.raw = true
- main.js
import './main.css'
- main.css
.box1 {
width: 500px;
height: 300px;
background: url('./assets/bg.png');
background-size: cover;
}
- webpack.config.js
module.exports = {
//...
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.(png|jpeg|gif)$/,
loader: './src/file-loader',
type: 'javascript/auto'
},
],
},
};
- index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div class="box1"></div>
<script src="bundle.js"></script>
</body>
</html>
- 打包结果
|- dist
|- bundle.js
|- ee6e171b64f6ffb89d31.png
|- index.html
- 显示结果
style-loader简易版
把 CSS 插入到 DOM 中, 并不会去处理Css中引入的资源(图片、字体等),需要配合css-loader进行资源解析
- 基础版
动态创建style标签,并拼接到head中,不会处理内部的资源
// style-loader.js
/**
* style-loader不处理样式中的图片资源 ,导致图片或者字体图标引用不到
*/
module.exports = function(content) {
console.log(typeof content)
const script = `
let styleEle = document.createElement('style')
styleEle.innerHTML = ${JSON.stringify(content)}
document.head.appendChild(styleEle)
`
return script
}
打包之后效果
- 进阶版,引入css-loader
引入css-loader之后,style-loader接收到的是一堆js代码,没办法执行获取结果,则需要通过pitch loader,获取剩下需要执行的loader和文件,通过内联loader的方式引入资源文件,并中断后续操作的方法实现
// style-loader.js
module.exports.pitch = function(remainingRequest) {
// remainingRequest 剩下还需要处理的loader 中间用!分割
// console.log(remainingRequest) // C:\Users\Lenovo\Desktop\sj\gis\amap\webpack-loader\node_modules\css-loader\dist\cjs.js!C:\Users\Lenovo\Desktop\sj\gis\amap\webpack-loader\src\main.css
// 将绝对路径转为相对路径 使用loaderAPI中的utils工具
const relativePath = remainingRequest.split('!').map(absolutepath => {
return this.utils.contextify(this.context, absolutepath);
}).join('!')
console.log(relativePath) //../node_modules/css-loader/dist/cjs.js!./main.css
// 通过内联方式使用loader
// !! 使用 !! 前缀,将禁用所有已配置的 loader
const script = `
import Styles from '!!${relativePath}';
let styleEle = document.createElement('style')
styleEle.innerHTML = Styles
document.head.appendChild(styleEle)
`
// pitch方法return之后 会中断后续的loader
return script
}