相关文章
本篇文章涉及到webpack loader 的一些原理、api,如果不清楚的可以查看下面的文章或者 webpack 官网查看
项目目录
让我们实现一些简易的loader
,从大量的简易loader的实现过程中学习编写如何 webpack loader
├── loaders # loader目录
├── src # 业务代码
│ │── index.html
│ └── index.js
├── .gitignore
├── package.json
├── package-lock.json
└── webpack.config.js # webpack 配置文件
搭建项目
mkdir loaders
cd loaders
npm init -y
npm i -D webpack webpack-cli html-webpack-plugin webpack-dev-server loader-utils
webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
module.exports = {
mode: 'development',
plugins: [
new HtmlWebpackPlugin({
title: '自定义 webpack loader',
template: './src/index.html',
}),
],
};
src/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
</body>
</html>
src/index.js
import text from './happy-new-year.txt';
const textDom = document.createElement('p');
textDom.style.cssText = 'width: 200px;height: 200px;background-color: pink;';
textDom.innerText = text;
document.body.appendChild(textDom);
实现 my-raw-loader
src/happy-new-year.txt
🎉🎉🎆🎆🧨🧨
新年快乐!大吉大利!
🎉🎉🎆🎆🧨🧨
执行 npx webpack-dev-server
,会发现编译报错了
那么下面我们就实现 my-raw-loader
来抛砖引玉!
webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
module.exports = {
mode: 'development',
module: {
rules: [
{
test: /.txt$/,
use: [
{
loader: path.resolve(__dirname, 'loaders/my-raw-loader'),
options: {
esModule: true,
},
},
],
},
],
},
plugins: [
new HtmlWebpackPlugin({
title: '自定义 webpack loader',
template: './src/index.html',
}),
],
};
loaders/my-raw-loader.js
function myRawLoader(source) {
console.log('source', source);
}
module.exports = myRawLoader;
执行 npx webpack-dev-server
可以看到打印结果,这个参数是一个字符串
参数
修改 loaders/my-raw-loader.js
function myRawLoader(source) {
// 提取给定的 loader 选项,
// 从 webpack 5 开始,this.getOptions 可以获取到 loader 上下文对象。它用来替代来自 loader-utils 中的 getOptions 方法。
const { esModule } = this.getOptions();
console.log('esModule:', esModule);
// 这里一定要返回字符串或者 buffer
if (!esModule) {
return `module.exports = ${JSON.stringify(source)}`;
}
return `export default ${JSON.stringify(source)}`;
}
module.exports = myRawLoader;
执行 npx webpack-dev-server
可以看到通过 this.getOptions()
获取到了当前 loader 的配置,并且编译未报错,访问 http://localhost:8080/ 页面得偿所愿!成功读取并渲染了原始文本内容。
schema-utils
schema-utils
由webpack 官方提供, 它配合 loader-utils
,用于保证 loader 选项,进行与 JSON Schema 结构一致的校验
const { validate } = require('schema-utils');
const schema = {
type: 'object',
properties: {
esModule: {
type: 'boolean',
}
},
"additionalProperties": false // 是否允许不存在的选项传入
};
function myRawLoader(source) {
const options = this.getOptions();
validate(schema, options, {
name: 'my-raw-loader',
baseDataPath: 'options',
});
// 提取给定的 loader 选项,
// 从 webpack 5 开始,this.getOptions 可以获取到 loader 上下文对象。它用来替代来自 loader-utils 中的 getOptions 方法。
console.log('esModule:', options.esModule);
// 这里一定要返回字符串或者 buffer
if (!options.esModule) {
return `module.exports = ${JSON.stringify(source)}`;
}
return `export default ${JSON.stringify(source)}`;
}
module.exports = myRawLoader;
如果传入未定义的选项,则会发生编译报错
{
test: /.txt$/,
use: [
{
loader: path.resolve(__dirname, 'loaders/my-raw-loader'),
options: {
esModule2: true,
},
},
],
},
实现 tpl-loader
info.tpl
<div>
<h1>{{ name }}</h1>
<p>{{ age }}</p>
<p>{{ sex }}</p>
</div>
index.js
import infoTpl from './info.tpl';
const info = {
name: 'HuaJi',
age: 29,
sex: '男',
};
const textDom = document.createElement('p');
textDom.style.cssText = 'width: 200px;height: 200px;background-color: pink;';
textDom.innerHTML = infoTpl(info);
document.body.appendChild(textDom);
webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
module.exports = {
mode: 'development',
output: {
clean: true,
},
devServer: {
port: 9000,
hot: true,
open: true,
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, './src/index.html'),
}),
],
module: {
rules: [
{
test: /\.tpl$/i,
use: [
{
loader: path.resolve(__dirname, './loaders/tpl-loader.js'),
options: {
log: true,
},
},
],
},
],
},
};
loaders/tpl-loader.js
const schema = {
type: 'object',
properties: {
log: {
type: 'boolean',
},
},
additionalProperties: false,
};
function tplReplace(template, replaceData) {
// 正则匹配{{}}替换为值
return template.replace(/\{\{(.*?)}}/g, (word, key) => replaceData[key]);
}
function tplLoader(content, map, meta) {
const options = this.getOptions(schema);
// 去除模板中的不可见字符
const resource = content.replace(/\s+/g, '');
const logStr = options.log ? `console.log('Compiled the file from ${this.resourcePath}')` : '';
// 返回 esm export 及 函数
// 函数接收外部参数,函数内部声明一个函数,然后调用
// 调用时注意:第一个参数 `template` 应当是一个字符串,切记
return `export default (data) => {
${tplReplace.toString()}
${logStr}
return tplReplace('${resource}', data)
}`;
}
module.exports = tplLoader;
结果:
实现 file-loader
- 指定输出文件的路径——即打包后文件的存储位置。
- 生成解析文件的路径——即打包后引用文件时的URL地址。
index.js
import sunflower from './sunflower.jpg';
const img = document.createElement('img');
img.style.cssText = 'width: 200px;height: auto;';
img.src = sunflower;
document.body.appendChild(img);
webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
module.exports = {
mode: 'development',
output: {
clean: true,
},
devServer: {
port: 9000,
hot: true,
open: true,
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, './src/index.html'),
}),
],
module: {
rules: [
{
test: /\.(png|jpe?g|gif)$/i,
loader: path.resolve(__dirname, './loaders/file-loader.js'),
},
],
},
};
loaders/file-loader.js
const loaderUtils = require('loader-utils');
function fileLoader(content) {
// 1. 根据文件内容生成带 hash 值文件名
const filename = loaderUtils.interpolateName(this, '', {
content,
});
// 2. 将文件输出出去
this.emitFile(filename, content);
// 3. 返回 export default "[contenthash].[ext]"
return `export default "${filename}"`;
}
// file-loader 需要处理图片、字体等文件,它们都是buffer数据,需要使用 raw loader 才能处理
fileLoader.raw = true;
module.exports = fileLoader;
结果:
实现 style-loader
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<div class="box">
<div class="box1"></div>
<div class="box2"></div>
<div class="box3"></div>
</div>
</body>
</html>
index.js
import './index.less';
index.less
.box {
.box1, .box2, .box3 {
width: 200px;
height: 200px;
}
.box1 {
background-color: #9a6e3a;
}
.box2 {
background-color: #55a532;
}
.box3 {
background-color: #0077aa;
}
}
webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
module.exports = {
mode: 'development',
output: {
clean: true,
},
devServer: {
port: 9000,
hot: true,
open: true,
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, './src/index.html'),
}),
],
module: {
rules: [
{
test: /\.(css|less)$/i,
use: [path.resolve(__dirname, './loaders/style-loader.js'),
'css-loader',
'less-loader',
],
},
],
},
};
loaders/style-loader.js
function styleLoader() {}
// Pitching Loader:https://blog.csdn.net/qq_41887214/article/details/128605233?spm=1001.2014.3001.5501#t12
styleLoader.pitch = function (remainingRequest) {
// remainingRequest:剩下还需要处理的 loader
console.log('remainingRequest:', remainingRequest);
// remainingRequest:/Users/huaji/learning/code/webpack-demos/node_modules/css-loader/dist/cjs.js!/Users/huaji/learning/code/webpack-demos/proficient/step_6-loaders/node_modules/less-loader/dist/cjs.js!/Users/huaji/learning/code/webpack-demos/proficient/step_6-loaders/src/testStyleLoader/index.less
// 1. 将`remainingRequest`中的绝对路径转换为相对路径
// https://webpack.docschina.org/api/loaders/#thisutils
const relativePath = remainingRequest.split('!').map((absolutePath) => this.utils.contextify(this.context, absolutePath)).join('!');
console.log('relativePath:', relativePath);
// relativePath:../../../../node_modules/css-loader/dist/cjs.js!../../node_modules/less-loader/dist/cjs.js!./index.less
const script = `
// 2. 引入 less-loader、css-loader 处理后的结果
import style from "!!${relativePath}";
// 3. 动态生成\`style\`标签插入到页面
const styleDom = document.createElement('style');
styleDom.innerHTML = style;
document.head.appendChild(styleDom);
`;
return script;
};
module.exports = styleLoader;
原理
-
relativePath
:…/…/…/…/node_modules/css-loader/dist/cjs.js!../…/node_modules/less-loader/dist/cjs.js!./index.less- relativePath 是
inline loader
用法 - 代表
./index.less
需要使用less-loader
和css-loader
处理
- relativePath 是
-
import style from “!!${relativeRequest}”
- !! 表示跳过 pre、 normal 和 post loader
- 在这里是跳过后面再次执行
less-loader
和css-loader
-
inline loader
-
用法:import Styles from ‘style-loader!css-loader?modules!./styles.css’;
-
含义:
-
使用 css-loader 和 style-loader 处理 styles.css 文件
-
通过 ! 将资源中的 loader 分开
inline loader 可以通过添加不同前缀,跳过其他类型 loader。
-
!
跳过 normal loader。
import Styles from ‘!style-loader!css-loader?modules!./styles.css’; -
-!
跳过 pre 和 normal loader。
import Styles from ‘-!style-loader!css-loader?modules!./styles.css’; -
!!
跳过 pre、 normal 和 post loader。
import Styles from ‘!!style-loader!css-loader?modules!./styles.css’;
-
结果:
源码:https://gitee.com/yanhuakang/webpack-demos/tree/master/proficient/step_6-loaders