loader原理
webpack本身只能识别js模块,loader能够帮助webpack将不同类型的文件转换成webpack能识别的模块。
官方文档: webpack.docschina.org/api/loaders
1、loader分类
. pre 前置loader
. normal 普通loader
. inline 内联loader
. post 后置loader
2、执行顺序
pre > normal > inline > post
相同优先级的执行顺序:从右到左、从下到上
enforce 可以指定loader类型
module: {
rules: [
// 执行顺序 loader1 > loader2 > loader3
{
enforce: 'pre', // 指定loader类型
test: /\.js$/,
use: 'loader1'
},
{
// 没有enforce 是 normal loader
test: /\.js$/,
use: 'loader2'
},
{
enforce: 'post',
test: /\.js$/,
use: 'loader3'
},
]
},
3、使用loader的方式
. 配置方式: 在webpack.config.js中指定loader(pre、normal、post loader)
. 内联方式: 在每个import语句中显示指定loader (inline loader)
4、inline loader
用法: 多个loader用 ! 连接, ? 后面是给css-loader传递的参数
用style-loader和css-loader去处理 ./style.css
import Styles from 'style-loader!css-loader?modules!./style.css
inline loader可以通过添加不同前缀,跳过其它类型的loader
1、 ! 跳过 normal loader
import Styles from '!style-loader!css-loader?modules!./style.css
2、 -! 跳过 pre和normal loader
import Styles from '-!style-loader!css-loader?modules!./style.css
3、 !! 跳过 pre、normal、post loader
import Styles from '!!style-loader!css-loader?modules!./style.css
开发一个loader
package.json
{
"name": "loader",
"version": "1.0.0",
"description": "",
"main": "webpack.config.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"html-webpack-plugin": "^5.5.0",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.0"
}
}
入口文件 src/main.js
console.log('hello main')
模板文件 public/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>loader</title>
</head>
<body>
</body>
</html>
第一个loader loaders/test-loader.js
/**
* loader就是一个函数
* 当webpack解析资源时,会调用相应的loader去处理
* loader接收到文件内容作为参数,返回内容出去
* @param {*} content 接收到的内容
* @param {*} map sourcemap相关的
* @param {*} meta 其它loader传过来的数据
* @returns 处理后的内容
*/
module.exports = function(content, map, meta) {
console.log(content, 'content')
return content;
}
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'js/[name].js',
clean: true
},
module: {
rules: [
{
test: /\.js$/,
loader: './loaders/test-loader.js'
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, './public/index.html')
}),
],
mode: 'development'
}
npx webpack执行打包命令
loader分类
同步loader
// 同步loader
// module.exports = function(content) {
// console.log(content, 'content')
// return content;
// }
// 同步laoder的第二种写法
module.exports = function(content, map, meta) {
console.log('test1')
/**
* param1: err 是否有错误
* param2: content 处理后的内容
* param3: map source-map(继续传递source-map)
* param4: meta 给先一个loader传递的参数
*/
this.callback(null, content, map, meta);
// 同步laoder中不能进行异步操作
// setTimeout(() => {
// this.callback(null, content, map, meta);
// }, 1000)
}
异步loader
loader内部的异步执行完后,再执行下一个loader
// 异步loader
module.exports = function(content, map, meta) {
const callback = this.async();
setTimeout(() => {
console.log('test2')
callback(null, content, map, meta)
}, 1000);
}
raw loader
接收到的内容是二进制数据,用来处理图片等的内容
// raw loader 接收到的content内容是buffer数据(二进制数据)
// module.exports = function(content) {
// console.log(content); // 二进制数据
// return content;
// }
// module.exports.raw = true;
// raw loader 另一种写法
function test3Loader(content) {
return content;
}
test3Loader.raw = true;
module.exports = test3Loader;
pitch loader
loader上带有pitch方法的叫pitchloader。
// pitch loader
// loader的pitch方法会在loader执行前调用
// 比如use[loader1, loader2, loader3], 3个loader上都有pitch方法
// 调用pitch方法调用顺序为 pitch1 > pitch2 > pitch3 > loader3 > loader2 > loader1
// 如果pitch方法内有return; 则后面的pitch不会执行,紧接着会执行上一个nomal loader(这种机制叫熔断机制)
// 例如 loader2的pitch方法内有return,则执行顺序为 pitch1 > pitch2 > loader1
module.exports = function(content) {
console.log('normal loader 1')
return content;
}
module.exports.pitch = function() {
console.log('pitch loader 1')
// return 'result'
}
loader api
loader是一个函数,在函数内可以通过this访问loader的api
this.async
含义:异步回调loader,返回this.callback
用法:const callback = this.async();
this.callback
含义:可以同步或异步调用的返回多个结果的函数
用法:this.callback(error, content, sourceMap?, meta?);
this.getOptions(schema)
含义: 获取loader的options
用法: this.getOptions(schema); schema是一个验证规则,如果option不符合验证规则们就会报错
this.emitFile
含义:产生一个文件
用法: this.emitFil(name, content, sourceMap)
this.utils.contextify
含义: 返回一个相对路径
用法: this.utils.contextify(context, request);
this.utils.absolutify
含义:返回一个绝对路径
用法: this.utils.absolutify(context, request);
api文档: https://webpack.docschina.org/api/loaders
自定义clean-log-loader
module.exports = function(content) {
// 清除文件中的console.log(xxx)
return content.replace(/console\.log\(.*\);?/g,'');
}
使用clean-log-loader
module: {
rules: [
{
test: /\.js$/,
// use: ['./loaders/test-loader.js']
loader: './loaders/clean-log-loader.js'
}
]
}
自定义banner-loader
banner-loader/index.js
const schema = require('./shcema.json')
module.exports = function(content) {
// 在内容前面增加上作者名字, 作者名字由options(参数)传入
// schema: 对options的验证规则
// schema符合JSON Schema的规则
const options = this.getOptions(schema);
const prefix = `
/**
* author: ${options.author}
*/
`;
return prefix + content;
}
banner-loader/schema.json
{
"type": "object", // 类型为 对象
"properties": { // 有哪些属性
"author": { // author属性
"type": "string" // 类型为 字符串
}
},
"additionalProperties": false // 是否允许追加属性
}
使用此loader
module: {
rules: [
{
test: /\.js$/,
loader: './loaders/banner-loader',
options: {
author: '老王',
// age: 18 不能新增字段,不然会报错
}
}
]
}
自定义babel-loader
我们用babel的@babel/core和@babel/preset-env
npm i @babel/core @babel/preset-env -D
babel-loader/index.js
const schema = require('./shcema.json');
const babel = require("@babel/core");
// https://babel.docschina.org/docs/en/babel-core/
module.exports = function(content) {
// 异步loader
// babel-loader
const callback = this.async();
const options = this.getOptions(schema);
// 使用babel对代码进行编译
babel.transform(content, options, function(err, result) {
if(err) callback(err);
else callback(null, result.code);
});
}
babel-loader/schema.json
{
"type": "object",
"properties": {
"presets": {
"type": "array"
}
},
"additionalProperties": true
}
使用
module: {
rules: [
{
test: /\.js$/,
loader: './loaders/babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
]
}
自定义file-loader
file-loader/index.js
npm i loader-utils - D
// 借助webpack中的loader-utils中的
const loaderUtils = require('loader-utils');
module.exports = function(content) {
// file-loader
// 把资原封不动的输出出去
// 处理的都是图片、字体等buffer数据
// 步骤
// 1:根据文件内容生成带hash值的文件名
const interpolatedName = loaderUtils.interpolateName(
this,
"[hash].[ext][query]",
{ content }
);
console.log(interpolatedName, 'interpolatedName==='); // 4e1e5120a3ecc13d.png
// 2:将文件输出出去
this.emitFile(interpolatedName, content);
// 3.返回,module.exports = "文件路径(文件名)"
return `module.exports = "${interpolatedName}"`
}
module.exports.raw = true;
使用:
{
test: /\.(png|jpe?g|gif)$/,
loader: './loaders/file-loader',
type: 'javascript/auto', // 禁止webpack默认处理这些资源,只让自己的file-loader生效
}
自定义style-loader
style-loader/index.js
module.exports = function(content) {
/**
* 1.直接使用style-loader,只能处理样式,不能处理样式中引入的其它资源,比如图片
* use: ['./loaders/style-loader']
* 2.借助css-loader解决样式中引入的其它资源的问题
* use: ['./loaders/style-loader', 'css-loader']
* 产生的问题是css暴露的是一段js代码,style-loader需要执行js代码,得到返回值,再动态创建style标签,放到页面上
* 不好操作
* 3.style-loader使用pitch loader的用法解决
*/
// const script = `
// const styleEl = document.createElement('style');
// styleEl.innerHTML = ${JSON.stringify(content)};
// document.head.appendChild(styleEl);
// `;
// return script;
}
module.exports.pitch = function(remainingRequest){
// remainingRequest剩下还需要处理的loader
console.log(remainingRequest, 'remainingRequest==='); // c:root\loader\node_modules\css-loader\dist\cjs.js!c:root\loader\src\css\index.css
// 如果拿到css-loader的处理结果,再让style-loader去处理就解决了问题
// 1.将remainingRequest中的绝对路径改成相对路径(因为后面只能通过相对路径操作)
// 希望: ..\..\node_modules\css-loader\dist\cjs.js!.\index.css
const relativePath = remainingRequest.split('!').map(absolutePath => {
// this.context当前loader的目录
// 处理成相对路径 (absolutePath相对于this.context)
return this.utils.contextify(this.context, absolutePath);
}).join('!');
console.log(relativePath, 'relativePath==='); // ../../node_modules/css-loader/dist/cjs.js!./index.css
// 2.引入css-loader处理后的资源(!! 代表终止后面loader的执行)
// 3.创建style标签,将内容插入到页面生效
const script = `
import style from "!!${relativePath}";
const styleEl = document.createElement('style');
styleEl.innerHTML = style;
document.head.appendChild(styleEl);
`;
// 终止后面loader执行
return script;
}
使用:
{
test: /\.css$/,
use: ['./loaders/style-loader', 'css-loader']
}