// 引入 loader-runner 模块中的 runLoaders 函数
const { runLoaders } = require('loader-runner');
// 引入 path 模块
const path = require('path');
// 引入 fs 模块
const fs = require('fs');
// 获取入口文件的绝对路径
const entryFile = path.resolve(__dirname, 'src/index.js');
// 定义需要处理的请求字符串
const request = `inline1-loader!inline2-loader!${entryFile}`;
// 定义 loader 规则
const rules = [
{
test: /\.js$/,
use: ['normal1-loader', 'normal2-loader']
},
{
test: /\.js$/,
enforce: 'pre',
use: ['pre1-loader', {
loader: 'pre2-loader',
options: {
age: '18'
}
}]
},
{
test: /\.js$/,
enforce: 'post',
use: ['post1-loader', 'post2-loader']
}
];
// 将请求字符串按照感叹号分割成数组
const parts = request.split('!');
// 弹出数组最后一个元素,即资源路径
const resource = parts.pop();
// 将剩下的数组元素作为内联 loader 列表
const inlineLoaders = parts;
// 定义空数组 preLoaders、normalLoaders 和 postLoaders
const preLoaders = [], normalLoaders = [], postLoaders = [];
// 遍历 rules 数组
for (let i = 0; i < rules.length; i++) {
// 获取当前遍历到的规则
let rule = rules[i];
// 如果当前规则匹配到了资源路径
if (rule.test.test(resource)) {
// 根据 enforce 属性判断是前置 loader 还是后置 loader 还是普通 loader,将 loader 添加到对应的数组中
if (rule.enforce === 'pre') {
preLoaders.push(...rule.use);
} else if (rule.enforce === 'post') {
postLoaders.push(...rule.use);
} else {
normalLoaders.push(...rule.use);
}
}
}
// 将 preLoaders、normalLoaders、postLoaders 和 inlineLoaders 数组合并成一个总的 loaders 数组
let loaders = [...postLoaders, ...inlineLoaders, ...normalLoaders, ...preLoaders];
// 定义一个函数,用于将 loader 转换成绝对路径
function resolveLoader(loader) {
return path.resolve(__dirname, 'loaders-chain', (loader.loader ? loader.loader : loader) + '.js');
}
// 将 loaders 数组中的 loader 转换成绝对路径
let resolvedLoaders = loaders.map(resolveLoader);
// 调用 runLoaders 函数,开始执行 loader 链
runLoaders({
// 资源路径
resource,
// 需要执行的 loader 列表
loaders: resolvedLoaders,
// 上下文对象
context: {},
// 读取资源的函数,这里使用 fs.readFile 函数
readResource: fs.readFile.bind(fs)
}, (err, result) => {
// 打印错误和处理结果
console.log(err);
console.log(result);
});
loaders-chain\pre2-loader.js
function loader(source) {
console.log("pre2");
return source + "//pre2";
}
loader.pitch = function () {
console.log("pitch-pre2");
};
module.exports = loader;
loaders-chain\pre1-loader.js
function loader(source) {
console.log("pre1");
return source + "//pre1";
}
loader.pitch = function () {
console.log("pitch-pre1");
};
module.exports = loader;
loaders-chain\normal2-loader.js
function loader(source) {
console.log("normal2");
return source + "//normal2";
}
loader.pitch = function () {
console.log("pitch-normal2");
};
module.exports = loader;
loaders-chain\normal1-loader.js
function loader(source) {
console.log("normal1");
return source + "//normal1";
}
loader.pitch = function () {
console.log("pitch-normal1");
};
module.exports = loader;
loaders-chain\inline2-loader.js
function loader(source) {
console.log("inline2");
return source + "//inline2";
}
loader.pitch = function () {
console.log("pitch-inline2");
};
module.exports = loader;
loaders-chain\inline1-loader.js
function loader(source) {
console.log("inline1");
return source + "//inline1";
}
loader.pitch = function () {
console.log("pitch-inline1");
};
module.exports = loader;
loaders-chain\post2-loader.js
function loader(source) {
console.log("post2");
return source + "//post2";
}
loader.pitch = function () {
console.log("pitch-post2");
};
module.exports = loader;
loaders-chain\post1-loader.js
function loader(source) {
console.log("post1");
return source + "//post1";
}
loader.pitch = function () {
console.log("pitch-post1");
};
module.exports = 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
符号 | 变量 | 含义 | |
---|---|---|---|
-! |
noPreAutoLoaders | 不要前置和普通 loader | Prefixing with -! will disable all configured preLoaders and loaders but not postLoaders |
! |
noAutoLoaders | 不要普通 loader | Prefixing with ! will disable all configured normal loaders |
!! |
noPrePostAutoLoaders | 不要前后置和普通 loader,只要内联 loader | Prefixing with !! will disable all configured loaders (preLoaders, loaders, postLoaders) |
// 引入 loader-runner 模块中的 runLoaders 函数
const { runLoaders } = require('loader-runner');
// 引入 path 模块
const path = require('path');
// 引入 fs 模块
const fs = require('fs');
// 获取入口文件的绝对路径
const entryFile = path.resolve(__dirname, 'src/index.js');
// 定义需要处理的请求字符串
const request = `inline1-loader!inline2-loader!${entryFile}`;
// 定义 loader 规则
const rules = [
{
test: /\.js$/,
use: ['normal1-loader', 'normal2-loader']
},
{
test: /\.js$/,
enforce: 'pre',
use: ['pre1-loader', {
loader: 'pre2-loader',
options: {
age: '18'
}
}]
},
{
test: /\.js$/,
enforce: 'post',
use: ['post1-loader', 'post2-loader']
}
];
// 将请求字符串按照感叹号分割成数组
+const parts = request.replace(/^-?!+/, '').split('!');
// 弹出数组最后一个元素,即资源路径
const resource = parts.pop();
// 将剩下的数组元素作为内联 loader 列表
const inlineLoaders = parts;
// 定义空数组 preLoaders、normalLoaders 和 postLoaders
const preLoaders = [], normalLoaders = [], postLoaders = [];
// 遍历 rules 数组
for (let i = 0; i < rules.length; i++) {
// 获取当前遍历到的规则
let rule = rules[i];
// 如果当前规则匹配到了资源路径
if (rule.test.test(resource)) {
// 根据 enforce 属性判断是前置 loader 还是后置 loader 还是普通 loader,将 loader 添加到对应的数组中
if (rule.enforce === 'pre') {
preLoaders.push(...rule.use);
} else if (rule.enforce === 'post') {
postLoaders.push(...rule.use);
} else {
normalLoaders.push(...rule.use);
}
}
}
// 将 preLoaders、normalLoaders、postLoaders 和 inlineLoaders 数组合并成一个总的 loaders 数组
+let loaders = [];
+if (request.startsWith('!!')) {
+ loaders = [...inlineLoaders];
+} else if (request.startsWith('-!')) {
+ loaders = [...postLoaders, ...inlineLoaders];
+} else if (request.startsWith('!')) {
+ loaders = [...postLoaders, ...inlineLoaders, ...preLoaders];
+} else {
+ loaders = [...postLoaders, ...inlineLoaders, ...normalLoaders, ...preLoaders];
+}
// 定义一个函数,用于将 loader 转换成绝对路径
function resolveLoader(loader) {
return path.resolve(__dirname, 'loaders-chain', (loader.loader ? loader.loader : loader) + '.js');
}
// 将 loaders 数组中的 loader 转换成绝对路径
let resolvedLoaders = loaders.map(resolveLoader);
// 调用 runLoaders 函数,开始执行 loader 链
runLoaders({
// 资源路径
resource,
// 需要执行的 loader 列表
loaders: resolvedLoaders,
// 上下文对象
context: {},
// 读取资源的函数,这里使用 fs.readFile 函数
readResource: fs.readFile.bind(fs)
}, (err, result) => {
// 打印错误和处理结果
console.log(err);
console.log(result);
});
// 引入 loader-runner 模块中的 runLoaders 函数
const { runLoaders } = require('loader-runner');
// 引入 path 模块
const path = require('path');
// 引入 fs 模块
const fs = require('fs');
// 获取入口文件的绝对路径
const entryFile = path.resolve(__dirname, 'src/index.js');
// 定义需要处理的请求字符串
const request = `inline1-loader!inline2-loader!${entryFile}`;
// 定义 loader 规则
const rules = [
{
test: /\.js$/,
use: ['normal1-loader', 'normal2-loader']
},
{
test: /\.js$/,
enforce: 'pre',
use: ['pre1-loader', {
loader: 'pre2-loader',
options: {
age: '18'
}
}]
},
{
test: /\.js$/,
enforce: 'post',
use: ['post1-loader', 'post2-loader']
}
];
// 将请求字符串按照感叹号分割成数组
const parts = request.split('!');
// 弹出数组最后一个元素,即资源路径
const resource = parts.pop();
// 将剩下的数组元素作为内联 loader 列表
const inlineLoaders = parts;
// 定义空数组 preLoaders、normalLoaders 和 postLoaders
const preLoaders = [], normalLoaders = [], postLoaders = [];
// 遍历 rules 数组
for (let i = 0; i < rules.length; i++) {
// 获取当前遍历到的规则
let rule = rules[i];
// 如果当前规则匹配到了资源路径
if (rule.test.test(resource)) {
// 根据 enforce 属性判断是前置 loader 还是后置 loader 还是普通 loader,将 loader 添加到对应的数组中
if (rule.enforce === 'pre') {
preLoaders.push(...rule.use);
} else if (rule.enforce === 'post') {
postLoaders.push(...rule.use);
} else {
normalLoaders.push(...rule.use);
}
}
}
// 将 preLoaders、normalLoaders、postLoaders 和 inlineLoaders 数组合并成一个总的 loaders 数组
let loaders = [...postLoaders, ...inlineLoaders, ...normalLoaders, ...preLoaders];
// 定义一个函数,用于将 loader 转换成绝对路径
function resolveLoader(loader) {
return path.resolve(__dirname, 'loaders-chain', (loader.loader ? loader.loader : loader) + '.js');
}
// 将 loaders 数组中的 loader 转换成绝对路径
let resolvedLoaders = loaders.map(resolveLoader);
// 调用 runLoaders 函数,开始执行 loader 链
runLoaders({
// 资源路径
resource,
// 需要执行的 loader 列表
loaders: resolvedLoaders,
// 上下文对象
+ context: {
+ getCurrentLoader() {
+ return this.loaders[this.loaderIndex];
+ },
+ getOptions() {
+ const loader = this.getCurrentLoader();
+ return loader.options;
+ }
+ },
// 读取资源的函数,这里使用 fs.readFile 函数
readResource: fs.readFile.bind(fs)
}, (err, result) => {
// 打印错误和处理结果
console.log(err);
console.log(result);
});
// 引入 url 模块
const url = require('url');
// 创建 loader 对象的函数
function createLoaderObject(loader) {
// 通过 require 函数加载 loader
let normal = require(loader);
// 从 loader 中取出 pitch 函数和 raw 属性
let pitch = normal.pitch;
let raw = normal.raw || true;
// 返回一个包含 loader 相关信息的对象
const obj = {
path: null,
query: null,
normal,
pitch,
normalExecuted: false,
pitchExecuted: false,
data: {},
raw
};
Object.defineProperty(obj, "request", {
get: function () {
return obj.path + obj.query;
},
set: function (value) {
const { pathname, query } = url.parse(value);
obj.path = pathname;
obj.query = query;
}
});
obj.request = loader;
return obj;
}
// 迭代执行 normal loader 的函数
function iterateNormalLoaders(processOptions, loaderContext, args, pitchingCallback) {
// 如果已经遍历完所有的 normal loader,则调用 pitchingCallback 函数
if (loaderContext.loaderIndex < 0) {
return pitchingCallback(null, args);
}
// 取出当前要执行的 loader 对象
let currentLoader = loaderContext.loaders[loaderContext.loaderIndex];
// 如果当前 loader 已经执行过 normal 函数,则向前遍历 loader 数组
if (currentLoader.normalExecuted) {
loaderContext.loaderIndex--;
return iterateNormalLoaders(processOptions, loaderContext, args, pitchingCallback);
}
// 取出 normal 函数并标记当前 loader 已经执行过 normal 函数
let fn = currentLoader.normal;
currentLoader.normalExecuted = true;
// 根据 loader 的 raw 属性将传入的参数转换为 buffer 类型或者 string 类型
convertArgs(args, currentLoader.raw);
// 执行 normal 函数,将返回值传入下一次迭代
runSyncOrAsync(fn, loaderContext, args, (err, ...returnArgs) => {
return iterateNormalLoaders(processOptions, loaderContext, returnArgs, pitchingCallback);
});
}
// 将 loader 函数的参数转换为 buffer 类型或者 string 类型
function convertArgs(args, raw) {
if (raw && !Buffer.isBuffer(args[0])) {
args[0] = Buffer.from(args[0]);
} else if (!raw && Buffer.isBuffer(args[0])) {
args[0] = args[0].toString('utf-8');
}
}
// 处理资源文件的函数
function processResource(processOptions, loaderContext, pitchingCallback) {
// 读取资源文件的内容,存入 processOptions.resourceBuffer 中
processOptions.readResource(loaderContext.resourcePath, (err, resourceBuffer) => {
processOptions.resourceBuffer = resourceBuffer;
// 向前遍历 loader 数组,然后开始迭代执行 normal loader
loaderContext.loaderIndex--;
iterateNormalLoaders(processOptions, loaderContext, [resourceBuffer], pitchingCallback);
});
}
function iteratePitchingLoaders(processOptions, loaderContext, pitchingCallback) {
if (loaderContext.loaderIndex >= loaderContext.loaders.length) {
return processResource(processOptions, loaderContext, pitchingCallback);
}
let currentLoader = loaderContext.loaders[loaderContext.loaderIndex];
if (currentLoader.pitchExecuted) {
loaderContext.loaderIndex++;
return iteratePitchingLoaders(processOptions, loaderContext, pitchingCallback);
}
let fn = currentLoader.pitch;
currentLoader.pitchExecuted = true;
if (!fn) {
return iteratePitchingLoaders(processOptions, loaderContext, pitchingCallback);
}
runSyncOrAsync(fn, loaderContext, [loaderContext.remainingRequest, loaderContext.previousRequest, loaderContext.data], (err, ...args) => {
if (args.length > 0 && args.some(item => item)) {
loaderContext.loaderIndex--;
return iterateNormalLoaders(processOptions, loaderContext, args, pitchingCallback);
} else {
return iteratePitchingLoaders(processOptions, loaderContext, pitchingCallback);
}
});
}
function runSyncOrAsync(fn, loaderContext, args, runCallback) {
let isSync = true;
loaderContext.callback = (err, ...args) => {
runCallback(err, ...args);
};
loaderContext.async = () => {
isSync = false;
return loaderContext.callback;
};
let result = fn.apply(loaderContext, args);
if (isSync) {
runCallback(null, result);
}
}
/**
运行 Loader
@param {*} options
@param {*} finalCallback
*/
function runLoaders(options, finalCallback) {
const {
resource,
loaders = [],
context = {},
readResource = fs.readFile.bind(fs)
} = options;
let loaderContext = context;
let loaderObjects = loaders.map(createLoaderObject);
// 定义 resource 属性
Object.defineProperty(loaderContext, "resource", {
get: function () {
return loaderContext.resourcePath + loaderContext.resourceQuery;
},
set: function (value) {
var splittedResource = url.parse(value);
loaderContext.resourcePath = splittedResource.pathname;
loaderContext.resourceQuery = splittedResource.query;
}
});
loaderContext.resource = resource;
loaderContext.readResource = readResource;
loaderContext.loaders = loaderObjects;
loaderContext.loaderIndex = 0;
loaderContext.callback = null;
loaderContext.async = null;
// 定义 request、remainingRequest、currentRequest、previousRequest 和 data 属性
Object.defineProperty(loaderContext, 'request', {
get() {
return loaderContext.loaders.map(loader => loader.path).concat(resource).join('!');
}
});
Object.defineProperty(loaderContext, 'remainingRequest', {
get() {
return loaderContext.loaders.slice(loaderContext.loaderIndex + 1).map(loader => loader.path).concat(resource).join('!');
}
});
Object.defineProperty(loaderContext, 'currentRequest', {
get() {
return loaderContext.loaders.slice(loaderContext.loaderIndex).map(loader => loader.path).concat(resource).join('!');
}
});
Object.defineProperty(loaderContext, 'previousRequest', {
get() {
return loaderContext.loaders.slice(0, loaderContext.loaderIndex).map(loader => loader.path).join('!');
}
});
Object.defineProperty(loaderContext, 'data', {
get() {
return loaderContext.loaders[loaderContext.loaderIndex].data;
}
});
// 定义 processOptions 对象
let processOptions = {
resourceBuffer: null,
readResource
};
// 依次执行 pitching loader 和 normal loader
iteratePitchingLoaders(processOptions, loaderContext, (err, result) => {
finalCallback(err, {
result,
resourceBuffer: processOptions.resourceBuffer
});
});
}
exports.runLoaders = runLoaders;
sourceMaps
最后有个s
属性 | 值 |
---|---|
this.request | /loaders/babel-loader.js!/src/index.js |
this.resourcePath | /src/index.js |
$ npm install webpack webpack-cli webpack-dev-server html-webpack-plugin --save
$ npm install @babel/preset-env @babel/core -D
const babel = require('@babel/core'); // 引入Babel
function loader(sourceCode, inputSourceMap, inputAst) {
const filename = this.resourcePath; // 获取当前文件的绝对路径
const useOptions = this.getOptions(); // 获取loader选项
const options = {
filename, // 配置Babel的filename选项
inputSourceMap, // 配置Babel的inputSourceMap选项
sourceMaps: true, // 配置Babel的sourceMaps选项
sourceFileName: filename, // 配置Babel的sourceFileName选项
ast: true, // 配置Babel的ast选项
...useOptions // 将loader选项和Babel选项合并
};
const config = babel.loadPartialConfig(options); // 加载Babel的配置
if (config) {
babel.transformAsync(sourceCode, config.options, (err, result) => {// 使用Babel转换代码
this.callback(null, result.code, result.map, result.ast); // 调用Webpack提供的callback返回转换后的代码
});
return;
}
return sourceCode; // 如果没有找到Babel配置,则直接返回源代码
}
module.exports = loader; // 导出loader函数
/**
* babel-loader只是提供一个转换函数,但是它并不知道要干啥要转啥
* @babel/core 负责把源代码转成AST,然后遍历AST,然后重新生成新的代码
* 但是它并不知道如何转换语换法,它并不认识箭头函数,也不知道如何转换
* @babel/transform-arrow-functions 插件其实是一个访问器,它知道如何转换AST语法树
* 因为要转换的语法太多,插件也太多。所以可一堆插件打包大一起,成为预设preset-env
*/
webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
mode: "development",
devtool: "source-map",
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].js",
},
devServer: {
hot: false,
},
resolveLoader: {
alias: {
"babel-loader": path.resolve(__dirname, "loader/babel-loader.js"),
},
modules: [path.resolve("./loader"), "node_modules"],
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
},
},
},
{
test: /\.less$/,
exclude: /node_modules/,
use: ["style-loader", "less-loader"],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: "./src/index.html",
}),
],
};
/**
* 要想在项目中使用自定义loader
* 1.可以使用绝对路径 path.resolve(__dirname,'loader/babel-loader.js')
* 2.resolveLoader 配置alias
* 3.resolveLoader 配置modules
*/
css-loader
是一个用于处理 CSS 文件的 webpack loader。它将 CSS 代码解析为 JavaScript,使得在 webpack 打包过程中,可以将 CSS 代码捆绑到 JavaScript 模块中。当 css-loader 处理 CSS 文件时,它还会解析其中的 @import 和 url() 表达式,以便 webpack 可以正确处理这些资源。style-loader
是一个用于将 CSS 样式插入到 HTML 文档中的 webpack loader。当与 css-loader 结合使用时,style-loader 将处理由 css-loader 生成的 JavaScript 模块,并在运行时将这些样式添加到 HTML 文档的 head 部分,从而实现 CSS 样式的应用npm install webpack webpack-cli html-webpack-plugin css-loader style-loader --save
package.json
{
"scripts": {
"build": "webpack"
},
}
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
devtool: false,
module: {
rules: [
{
test: /\.css$/,
use: [
{
loader: 'css-loader',
options: {
esModule: false
}
}
],
include: path.resolve('src')
}
]
},
plugins: [
new HtmlWebpackPlugin({ template: './src/index.html' }),
]
}
src\index.js
const indexCss = require("./index.css");
console.log(indexCss);
src\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>css-loader</title>
</head>
<body>
</body>
</html>
src\index.css
body {
color: red;
}
dist\main.js
// 定义包含各个模块的对象
var webpackModules = {
// 定义 "./src/index.css" 模块
"./src/index.css": (module, unusedWebpackExports, webpackRequire) => {
// 导入 noSourceMaps 运行时
var cssLoaderApiNoSourcemapImport = webpackRequire("./node_modules/css-loader/dist/runtime/noSourceMaps.js");
// 导入 css-loader API 运行时
var cssLoaderApiImport = webpackRequire("./node_modules/css-loader/dist/runtime/api.js");
// 使用 css-loader API 运行时处理 noSourceMaps 运行时
var cssLoaderExport = cssLoaderApiImport(cssLoaderApiNoSourcemapImport);
// 将处理后的 CSS 内容添加到 cssLoaderExport
cssLoaderExport.push([module.id, "body {\r\n color: red;\r\n}"]);
// 将 cssLoaderExport 导出
module.exports = cssLoaderExport;
},
// 定义 css-loader 的 API 运行时模块
"./node_modules/css-loader/dist/runtime/api.js": module => {
"use strict";
// 导出一个函数,处理 cssWithMappingToString 并返回一个列表
module.exports = function (cssWithMappingToString) {
var list = [];
// 定义 list 的 toString 方法,将列表中的内容转换为字符串
list.toString = function toString() {
return this.map(function (item) {
var content = "";
content += cssWithMappingToString(item);
return content;
}).join("\r\n");
};
// 定义 list 的 i 方法,用于将模块添加到列表中
list.i = function i(modules) {
for (var k = 0; k < modules.length; k++) {
var item = [].concat(modules[k]);
list.push(item);
}
};
return list;
};
},
// 定义 noSourceMaps 运行时模块
"./node_modules/css-loader/dist/runtime/noSourceMaps.js": module => {
// 导出一个函数,用于处理 i 参数
module.exports = function (i) {
return i[1];
};
}
};
// 定义一个缓存对象,用于存储已加载的模块
var webpackModuleCache = {};
// 定义一个模块加载函数,根据模块 ID 加载模块
function webpackRequire(moduleId) {
// 检查缓存中是否已有该模块
var cachedModule = webpackModuleCache[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// 创建一个新的模块并将其添加到缓存中
var module = webpackModuleCache[moduleId] = {
id: moduleId,
exports: {}
};
// 执行指定模块 ID 的模块代码
webpackModules[moduleId](module, module.exports, webpackRequire);
// 返回模块的导出
return module.exports;
}
// 使用 webpackRequire 函数加载 "./src/index.css" 模块
const indexCss = webpackRequire("./src/index.css");
// 在控制台输出加载到的模块内容
console.log(indexCss);
``
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
devtool: false,
module: {
rules: [
{
test: /\.css$/,
use: [
{
//loader: 'css-loader',
+ loader: path.resolve('loaders/css-loader'),
options: {
esModule: false
}
}
],
include: path.resolve('src')
}
]
},
plugins: [
new HtmlWebpackPlugin({ template: './src/index.html' }),
]
}
loaders\css-loader\index.js
// 从 './utils' 模块中导入所需的工具函数
const { getImportCode, getModuleCode, getExportCode, stringifyRequest } = require('./utils');
// 定义 loader 函数,它接受一个参数 content(通常是源文件的内容)
function loader(content) {
// 获取 loader 的配置选项
const options = this.getOptions();
// 为异步回调创建一个回调函数
const callback = this.async();
// 定义要导入的模块及其别名
const imports = [
{
importName: "cssLoaderApiNoSourcemapImport",
url: stringifyRequest(this, require.resolve("./runtime/noSourceMaps"))
},
{
importName: "cssLoaderApiImport",
url: stringifyRequest(this, require.resolve("./runtime/api"))
}
];
// 使用工具函数生成导入代码
const importCode = getImportCode(imports, options);
// 使用工具函数生成模块代码
const moduleCode = getModuleCode({ css: content });
// 使用工具函数生成导出代码
const exportCode = getExportCode(options);
// 将生成的代码传递给异步回调函数
callback(null, `${importCode}${moduleCode}${exportCode}`);
}
// 导出 loader 函数
module.exports = loader;
loaders\css-loader\utils.js
// 导入路径相关的库
const path = require("path");
// 定义一个函数,用于生成 import 代码
function getImportCode(imports, options) {
let code = "";
// 遍历 imports 对象,生成 import 代码
for (const item of imports) {
const { importName, url } = item;
code += `var ${importName} = require(${url});\n`;
}
return code;
}
// 定义一个函数,用于生成模块代码
function getModuleCode(result) {
let code = JSON.stringify(result.css);
let beforeCode = `var cssLoaderExport = cssLoaderApiImport(cssLoaderApiNoSourcemapImport);\n`;
return `${beforeCode}cssLoaderExport.push([module.id, ${code}]);\n`;
}
// 定义一个函数,用于生成导出代码
function getExportCode(options) {
let code = "";
let finalExport = "cssLoaderExport";
code += `${options.esModule ? "export default" : "module.exports ="} ${finalExport};\n`;
return code;
}
// 定义一个函数,用于将请求字符串转换为相对路径字符串
function stringifyRequest(loaderContext, request) {
return JSON.stringify(loaderContext.utils.contextify(loaderContext.context, request));
}
// 导出相关函数
exports.getModuleCode = getModuleCode;
exports.getImportCode = getImportCode;
exports.getExportCode = getExportCode;
exports.stringifyRequest = stringifyRequest;
loaders\css-loader\runtime\api.js
// 导出一个函数,输入参数为 cssWithMappingToString 函数
module.exports = cssWithMappingToString => {
// 创建一个 list 数组用于存储 CSS 模块
const list = [];
// 为 list 定义一个 toString 方法,将 list 中的每个 CSS 模块转换为字符串,并用换行符连接
list.toString = function toString() {
return this.map(item => {
let content = "";
content += cssWithMappingToString(item);
return content;
}).join("\r\n");
};
// 为 list 定义一个 i 方法,用于将传入的模块数组添加到 list 中
list.i = function i(modules) {
for (var k = 0; k < modules.length; k++) {
// 将每个模块转换为数组,以确保它们是可迭代的
var item = [].concat(modules[k]);
// 将转换后的模块添加到 list 中
list.push(item);
}
};
// 返回 list
return list;
};
loaders\css-loader\runtime\noSourceMaps.js
// 导出一个函数,输入参数为 i
module.exports = (i) => {
// 返回 i 数组的第二个元素(下标为 1)
return i[1];
};
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
devtool: false,
module: {
rules: [
{
test: /\.css$/,
use: [
+ {
+ //loader: 'style-loader'
+ loader: path.resolve('loaders/style-loader'),
+ },
{
//loader: 'css-loader',
loader: path.resolve('loaders/css-loader'),
options: {
esModule: false
}
}
],
include: path.resolve('src')
}
]
},
plugins: [
new HtmlWebpackPlugin({ template: './src/index.html' }),
]
}
loaders\style-loader\index.js
// 引入 path 模块,用于处理文件路径
const path = require('path');
// 定义一个函数,用于将请求字符串转换为相对于 loaderContext.context 的路径
function stringifyRequest(loaderContext, request) {
return JSON.stringify(loaderContext.utils.contextify(loaderContext.context, request));
}
// 定义一个空的 loaderAPI 函数
function loaderAPI() { }
// 在 loaderAPI 上定义 pitch 方法,用于处理剩余请求
loaderAPI.pitch = function loader(remindingRequest) {
// 定义将要返回的代码字符串,用于将样式内容添加到 HTML 文档中
const contentCode = `
// 通过 require 导入样式内容
const content = require("!!"+${stringifyRequest(this, `${remindingRequest}`)});
// 创建一个 style 元素
const element = document.createElement("style");
// 将样式内容设置为 style 元素的 innerHTML
element.innerHTML = content.toString();
// 将 style 元素添加到文档的 head 部分
document.head.appendChild(element);
`;
// 返回生成的代码字符串
return contentCode;
};
// 导出 loaderAPI
module.exports = loaderAPI;
prepare
钩子函数在 PostCSS 插件中用于在处理流程开始前进行预处理操作。它在 PostCSS 解析 CSS 之前触发,因此,你可以在 prepare 钩子函数中设置一些初始数据或预处理操作。prepare 钩子函数返回一个对象,该对象可以包含多个钩子函数,用于在处理过程中的不同阶段执行特定操作Declaration
属性是一个钩子函数,它在每个 CSS 声明(例如 color: red;)被处理时触发。这意味着你可以在这个钩子函数中对每个声明执行特定操作。Declaration 钩子函数接收一个参数,即声明节点(decl),它包含了有关声明的详细信息,如属性名(prop)和属性值(value)AtRule
是 PostCSS 中的一个概念,表示 CSS 中的一种特殊规则,以 @
开头。常见的 AtRule 有 @media、@keyframes、@import、@font-face 等。AtRule 可以包含其他规则(如 @media),也可以包含声明(如 @font-face),甚至可以包含一些元信息(如 @import)Once
:这个钩子函数在 PostCSS 处理的开始阶段触发。它只会执行一次,而不是针对每个 CSS 规则或声明。Once 钩子函数接收一个参数,即根节点(root),它表示整个 CSS 树。你可以在 Once 钩子函数中遍历和修改整个 CSS 树OnceExit
:与 Once 钩子函数类似,OnceExit 也是在整个处理过程中仅执行一次。然而,OnceExit 在 PostCSS 处理的结束阶段触发,这意味着它可以用来执行在处理流程结束时需要完成的操作。OnceExit 钩子函数也接收根节点(root)作为参数postcss-value-parser
是一个 JavaScript 库,用于解析 CSS 值,例如样式规则中的声明值。它能够将 CSS 值分解为更小的部分,这样你可以方便地操作和分析它们。这个库对于编写 PostCSS 插件、CSS 预处理器和其他需要操作 CSS 值的工具非常有用const valueParser = require('postcss-value-parser');
// 解析 CSS 值
const parsedValue = valueParser('background-image: url(./images/logo.png)');
// 遍历节点树
parsedValue.walk(node => {
console.log(node);
// 过滤出所有函数节点
if (node.type === 'function') {
console.log('Function:', node.value); // 输出函数名称,例如 'url'
node.nodes[0].value = 'replacement_0'; // 修改函数参数
}
});
// 序列化节点树
const serializedValue = valueParser.stringify(parsedValue);
console.log(serializedValue); // 输出:'background-image: url(./images/logo.png)'
/**
{
type: 'word',
value: 'background-image'
}
{
type: 'function',
value: 'url',
nodes: [
{
type: 'word',
value: './images/logo.png'
}
],
}
*/
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
devtool: false,
module: {
rules: [
{
test: /\.css$/,
use: [
{
//loader: 'style-loader'
loader: path.resolve('loaders/style-loader'),
},
{
//loader: 'css-loader',
loader: path.resolve('loaders/css-loader'),
options: {
esModule: false,
url: true
}
}
],
include: path.resolve('src')
},
+ {
+ test: /\.(png|jpg|gif)$/,
+ type: 'asset/resource'
+ }
]
},
plugins: [
new HtmlWebpackPlugin({ template: './src/index.html' }),
]
}
src\index.css
body {
color: red;
+ background-image: url(./images/logo.png);
+ background-repeat: no-repeat;
}
loaders\css-loader\index.js
// 从 './utils' 模块中导入所需的工具函数
const { getImportCode, getModuleCode, getExportCode, stringifyRequest } = require('./utils');
+const postcss = require('postcss');
+const urlParser = require('./plugins/postcss-url-parser');
// 定义 loader 函数,它接受一个参数 content(通常是源文件的内容)
function loader(content) {
// 获取 loader 的配置选项
const options = this.getOptions();
// 为异步回调创建一个回调函数
const callback = this.async();
+ const plugins = [];
+ const replacements = [];
// 定义要导入的模块及其别名
+ const urlPluginImports = [];
+ if (options.url) {
+ plugins.push(urlParser({
+ imports: urlPluginImports,
+ replacements,
+ loaderContext: this,
+ urlHandler: url => stringifyRequest(this, url)
+ }));
+ }
+ postcss(plugins)
+ .process(content, { from: this.resourcePath, to: this.resourcePath })
+ .then((result) => {
+ const imports = [
+ {
+ importName: "cssLoaderApiNoSourcemapImport",
+ url: stringifyRequest(this, require.resolve("./runtime/noSourceMaps"))
+ },
+ {
+ importName: "cssLoaderApiImport",
+ url: stringifyRequest(this, require.resolve("./runtime/api"))
+ }
+ ];
+ imports.push(...urlPluginImports);
+ // 使用工具函数生成导入代码
+ const importCode = getImportCode(imports, options);
+ // 使用工具函数生成模块代码
+ const moduleCode = getModuleCode(result, replacements);
+ // 使用工具函数生成导出代码
+ const exportCode = getExportCode(options);
+ // 将生成的代码传递给异步回调函数
+ callback(null, `${importCode}${moduleCode}${exportCode}`);
+ })
}
// 导出 loader 函数
module.exports = loader;
loaders\css-loader\utils.js
// 导入路径相关的库
const path = require("path");
// 定义一个函数,用于生成 import 代码
function getImportCode(imports, options) {
let code = "";
// 遍历 imports 对象,生成 import 代码
for (const item of imports) {
const { importName, url } = item;
code += `var ${importName} = require(${url});\n`;
}
return code;
}
// 定义一个函数,用于生成模块代码
+function getModuleCode(result, replacements) {
let code = JSON.stringify(result.css);
let beforeCode = `var cssLoaderExport = cssLoaderApiImport(cssLoaderApiNoSourcemapImport);\n`;
+ for (const item of replacements) {
+ const { replacementName, importName } = item;
+ beforeCode += `var ${replacementName} = cssLoaderGetUrlImport(${importName});\n`;
+ code = code.replace(
+ new RegExp(replacementName, "g"),
+ () => `" + ${replacementName} + "`
+ );
+ }
return `${beforeCode}cssLoaderExport.push([module.id, ${code}]);\n`;
}
// 定义一个函数,用于生成导出代码
function getExportCode(options) {
let code = "";
let finalExport = "cssLoaderExport";
code += `${options.esModule ? "export default" : "module.exports ="} ${finalExport};\n`;
return code;
}
// 定义一个函数,用于将请求字符串转换为相对路径字符串
function stringifyRequest(loaderContext, request) {
return JSON.stringify(loaderContext.utils.contextify(loaderContext.context, request));
}
// 导出相关函数
exports.getModuleCode = getModuleCode;
exports.getImportCode = getImportCode;
exports.getExportCode = getExportCode;
exports.stringifyRequest = stringifyRequest;
loaders\css-loader\plugins\postcss-url-parser.js
// 导入所需模块
const valueParser = require('postcss-value-parser');
const isUrlFunc = /url/i;
const needParseDeclaration = /(?:url)\(/i;
// 定义一个名为 parseDeclaration 的函数,用于解析给定的 PostCSS 声明节点
function parseDeclaration(declaration) {
//如果声明的值不包含需要解析的函数(例如 url()),则跳过解析
if (!needParseDeclaration.test(declaration.value)) {
return [];
}
//使用 postcss-value-parser 解析声明的值,得到一个节点树。
const parsed = valueParser(declaration.value);
//创建一个空数组 parsedURLs,用于存储解析出的 URL 信息
const parsedURLs = [];
//使用 postcss-value-parser 的 walk 方法遍历节点树
parsed.walk(valueNode => {
//如果当前节点不是函数类型,跳过此节点
if (valueNode.type !== 'function') {
return;
}
//如果当前函数节点是 url() 函数,继续处理
if (isUrlFunc.test(valueNode.value)) {
//从当前函数节点中提取子节点数组
const { nodes } = valueNode;
//将子节点数组转换回字符串,得到 url 的值
let url = valueParser.stringify(nodes);
//将解析出的 URL 信息作为对象添加到 parsedURLs 数组中
parsedURLs.push({
declaration,//存储当前处理的声明节点
//获取 url() 函数内部的节点(例如 'image.png')
node: valueNode.nodes[0],
url,//存储解析出的 URL 字符串
parsed//存储解析后的节点树
});
}
});
return parsedURLs;
}
// 定义一个名为 plugin 的函数,接收一个包含 imports、replacements 和 urlHandler 的对象作为参数
const plugin = ({ imports = [], replacements = [], urlHandler }) => {
//返回一个包含 PostCSS 插件配置的对象。
return {//设置插件名称
postcssPlugin: 'postcss-url-parser',
prepare() {//定义插件的 prepare 方法,用于创建一个新的 PostCSS 会话
//创建一个空数组 parsedDeclarations,用于存储解析过的声明
const parsedDeclarations = [];
//返回一个对象,包含当前 PostCSS 会话的事件处理器
return {//定义一个处理声明节点的方法
Declaration(declaration) {
//调用 parseDeclaration 函数解析当前声明节点,并获取解析后的 URL 信息
const parsedURLs = parseDeclaration(declaration);
//将解析到的 URL 信息添加到 parsedDeclarations 数组中
parsedDeclarations.push(...parsedURLs);
},
//定义一个异步方法,用于处理 PostCSS 会话结束时的逻辑
OnceExit() {
//如果没有解析到任何 URL 信息,直接返回
if (parsedDeclarations.length === 0) {
return;
}
//将 getUrl.js 的导入信息添加到 imports 数组中
imports.push({
type: "get_url_import",//设置导入类型
importName: "cssLoaderGetUrlImport",//设置导入的名称
//使用 urlHandler 处理 getUrl.js 的路径
url: urlHandler(require.resolve("../runtime/getUrl.js")),
});
//遍历解析过的声明
for (let index = 0; index < parsedDeclarations.length; index++) {
//获取当前解析过的声明
const item = parsedDeclarations[index];
//从当前解析过的声明中提取 URL 信息
const { url, node } = item;
const importName = `url_${index}`;
//将 URL 导入信息添加到 imports 数组中
imports.push({
type: 'url',//设置导入类型
importName,//设置导入的名称
url: JSON.stringify(url)//将 URL 转换为 JSON 字符串
});
//为当前 URL 生成一个替换名称
const replacementName = `replacement_${index}`;
//将替换信息添加到 replacements 数组中
replacements.push({
replacementName,//将生成的替换名称添加到当前替换信息对象中
importName,//设置当前替换信息的导入名称为
});
node.value = replacementName;
item.declaration.value = item.parsed.toString();
}
}
};
}
};
};
// 导出插件
module.exports = plugin;
loaders\css-loader\runtime\getUrl.js
module.exports = (url) => {
return url;
};
src\index.css
+@import "basic.css";
body {
color: red;
background-image: url(./images/logo.png);
background-repeat: no-repeat;
}
src\basic.css
body{
background-color: green;
}
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
devtool: false,
module: {
rules: [
{
test: /\.css$/,
use: [
{
//loader: 'style-loader'
loader: path.resolve('loaders/style-loader'),
},
{
//loader: 'css-loader',
loader: path.resolve('loaders/css-loader'),
options: {
esModule: false,
url: true,
+ import: true,
+ importLoaders: 0,
}
}
],
include: path.resolve('src')
},
{
test: /\.(png|jpg|gif)$/,
type: 'asset/resource'
}
]
},
plugins: [
new HtmlWebpackPlugin({ template: './src/index.html' }),
]
}
loaders\css-loader\index.js
// 从 './utils' 模块中导入所需的工具函数
+const { getImportCode, getModuleCode, getExportCode, stringifyRequest, combineRequests, getPreRequester } = require('./utils');
const postcss = require('postcss');
const urlParser = require('./plugins/postcss-url-parser');
+const importParser = require('./plugins/postcss-import-parser');
// 定义 loader 函数,它接受一个参数 content(通常是源文件的内容)
function loader(content) {
// 获取 loader 的配置选项
const options = this.getOptions();
// 为异步回调创建一个回调函数
const callback = this.async();
const plugins = [];
const replacements = [];
// 定义要导入的模块及其别名
const urlPluginImports = [];
+ const importPluginImports = [];
+ const importPluginApi = [];
+ if (options.import) {
+ plugins.push(importParser({
+ imports: importPluginImports,
+ api: importPluginApi,
+ loaderContext: this,
+ urlHandler: url => stringifyRequest(this, combineRequests(getPreRequester(this, options.importLoaders), url))
+ }));
+ }
if (options.url) {
plugins.push(urlParser({
imports: urlPluginImports,
replacements,
loaderContext: this,
urlHandler: url => stringifyRequest(this, url)
}));
}
postcss(plugins)
.process(content, { from: this.resourcePath, to: this.resourcePath })
.then((result) => {
const imports = [
{
importName: "cssLoaderApiNoSourcemapImport",
url: stringifyRequest(this, require.resolve("./runtime/noSourceMaps"))
},
{
importName: "cssLoaderApiImport",
url: stringifyRequest(this, require.resolve("./runtime/api"))
}
];
+ imports.push(...importPluginImports, ...urlPluginImports,);
// 使用工具函数生成导入代码
const importCode = getImportCode(imports, options);
// 使用工具函数生成模块代码
+ const moduleCode = getModuleCode(result, importPluginApi, replacements);
// 使用工具函数生成导出代码
const exportCode = getExportCode(options);
// 将生成的代码传递给异步回调函数
callback(null, `${importCode}${moduleCode}${exportCode}`);
})
}
// 导出 loader 函数
module.exports = loader;
loaders\css-loader\utils.js
// 导入路径相关的库
const path = require("path");
// 定义一个函数,用于生成 import 代码
function getImportCode(imports, options) {
let code = "";
// 遍历 imports 对象,生成 import 代码
for (const item of imports) {
const { importName, url } = item;
code += `var ${importName} = require(${url});\n`;
}
return code;
}
// 定义一个函数,用于生成模块代码
+function getModuleCode(result, api, replacements) {
let code = JSON.stringify(result.css);
let beforeCode = `var cssLoaderExport = cssLoaderApiImport(cssLoaderApiNoSourcemapImport);\n`;
+ for (const item of api) {
+ beforeCode += `cssLoaderExport.i(${item.importName});\n`;
+ }
for (const item of replacements) {
const { replacementName, importName } = item;
beforeCode += `var ${replacementName} = cssLoaderGetUrlImport(${importName});\n`;
code = code.replace(
new RegExp(replacementName, "g"),
() => `" + ${replacementName} + "`
);
}
return `${beforeCode}cssLoaderExport.push([module.id, ${code}]);\n`;
}
// 定义一个函数,用于生成导出代码
function getExportCode(options) {
let code = "";
let finalExport = "cssLoaderExport";
code += `${options.esModule ? "export default" : "module.exports ="} ${finalExport};\n`;
return code;
}
// 定义一个函数,用于将请求字符串转换为相对路径字符串
function stringifyRequest(loaderContext, request) {
return JSON.stringify(loaderContext.utils.contextify(loaderContext.context, request));
}
+function getPreRequester({ loaders, loaderIndex }, { importLoaders = 0 }) {
+ const loadersRequest = loaders.slice(loaderIndex, loaderIndex + 1 + importLoaders).map(x => x.request).join("!");
+ return "-!" + loadersRequest + "!";
+}
+function combineRequests(preRequest, url) {
+ return preRequest + url;
+}
// 导出相关函数
exports.getModuleCode = getModuleCode;
exports.getImportCode = getImportCode;
exports.getExportCode = getExportCode;
exports.stringifyRequest = stringifyRequest;
+exports.getPreRequester = getPreRequester;
+exports.combineRequests = combineRequests;
loaders\css-loader\plugins\postcss-import-parser.js
// 引入 postcss-value-parser 模块
const valueParser = require("postcss-value-parser");
// 解析 @import 规则节点,提取 URL
function parseNode(atRule) {
const { nodes } = valueParser(atRule.params);;
return { atRule, url:nodes[0].value };
}
// 定义 postcss 插件,用于处理 @import 规则
const plugin = ({ imports = [], loaderContext, api, urlHandler }) => {
return {
postcssPlugin: "postcss-import-parser",
prepare() {
// 存储解析到的 @import 规则
const parsedAtRules = [];
return {
// 对于 AtRule 类型的节点,如果是 import 规则则处理
AtRule: {
import(atRule) {
let parsedAtRule = parseNode(atRule);
parsedAtRules.push(parsedAtRule);
}
},
// 当 postcss 处理完成后,处理解析到的 @import 规则
async OnceExit() {
if (parsedAtRules.length === 0) {
return;
}
// 使用 webpack 的解析器来解析 URL
const resolver = loaderContext.getResolve();
for (let index = 0; index <= parsedAtRules.length - 1; index++) {
const parsedAtRule = parsedAtRules[index];
const { atRule, url } = parsedAtRule;
// 删除原始的 @import 规则
atRule.remove();
// 解析 URL
const resolvedUrl = await resolver(loaderContext.context, "./" + url)
let importName = `cssLoaderAtRuleImport_${index}`;
api.push({ importName, url })
imports.push({ type: 'rule_import', importName, url: urlHandler(resolvedUrl) });
}
}
};
}
};
};
module.exports = plugin;
postcss-selector-parser
是一个用于处理和转换 CSS 选择器的 JavaScript 库。它允许你以编程方式分析和修改 CSS 选择器。这个库基于 PostCSS,一个用于处理 CSS 的强大工具postcss-selector-parser
提供了一些方法来遍历和修改选择器的 AST。例如,你可以在遍历过程中替换选择器的部分,插入新的子节点或删除不需要的部分root.json
{
"raws": {
"semicolon": false,
"after": ""
},
"type": "root",
"nodes": [
{
"raws": {
"before": "",
"between": "",
"semicolon": true,
"after": "\n"
},
"type": "rule",
"nodes": [
{
"raws": {
"before": "\n ",
"between": ": "
},
"type": "decl",
"prop": "background-color",
"value": "green"
}
],
"selector": ":local(.background)"
}
]
}
{
"nodes": [
{
"nodes": [
{
"value": ":local",
"nodes": [
{
"nodes": [
{
"value": "background",
"type": "class"
}
],
"type": "selector"
}
],
"type": "pseudo"
}
],
"type": "selector"
}
],
"type": "root"
}
postcss-selector-parser.js
// 导入所需的模块
const postcss = require('postcss');
const selectorParser = require('postcss-selector-parser');
const crypto = require("crypto");
// 定义一个生成作用域名称的函数,它接受一个类名并为其生成一个唯一的哈希
function generateScopedName(name) {
const hash = crypto.createHash('md4');
hash.update(name);
return `_${hash.digest('hex')}__${name}`;
};
// 定义一个 PostCSS 插件
const plugin = () => {
return {
postcssPlugin: "my-postcss-modules-scope",
Once(root) {
// 创建一个用于存储导出名称的对象
const exports = Object.create(null);
// 定义一个函数,该函数接受一个类名并返回其作用域名称
function exportScopedName(name) {
const scopedName = generateScopedName(name);
exports[name] = scopedName;
return scopedName;
}
// 定义一个函数,该函数会根据节点类型修改其类名
function localizeNode(node) {
switch (node.type) {
case "selector":
node.nodes = node.map(localizeNode);
return node;
case "class":
return selectorParser.className({
value: exportScopedName(node.value)
});
}
}
// 定义一个遍历函数,用于处理每个选择器节点
function traverseNode(node) {
if (node.type === "pseudo" && node.value === ":local") {
const selector = localizeNode(node.first);
node.replaceWith(selector);
return;
}
if (node.type === "root" || node.type === "selector") {
node.each(traverseNode);
}
return node;
}
// 遍历根节点下的所有规则,处理选择器
root.walkRules(rule => {
let parsedSelector = selectorParser().astSync(rule);
rule.selector = traverseNode(parsedSelector.clone()).toString();
});
}
};
};
// 输入 CSS
const input = `:local(.background){
background-color: green;
}`;
// 创建一个 PostCSS 处理管道,包含我们的插件
const pipeline = postcss([plugin()]);
// 处理输入的 CSS
const res = pipeline.process(input);
// 输出处理后的 CSS
console.log(res.css);
/**
._be351a352b127c87b54833df13617444__background{
background-color: orange;
width:100px;
height:100px;
}
:export{
background: _be351a352b127c87b54833df13617444__background;
}
*/
icss-utils
是一个 JavaScript 库,用于处理 Interoperable CSS (ICSS)
icss-utils
提供了一组实用工具,用于在 PostCSS 插件中处理这些 ICSS 特性extractICSS
函数从 PostCSS AST(抽象语法树)中提取 ICSS 导入和导出。它返回一个对象,其中包含两个属性:icssImports
和 icssExports
。这两个属性分别表示从 CSS 中提取的导入和导出规则const postcss = require('postcss');
const icssUtils = require('icss-utils');
// 输入 CSS
const input = `:export{
background: _be351a352b127c87b54833df13617444__background;
}`;
const plugin = (options = {}) => {
return {
// 设置插件名称为 "postcss-icss-parser"
postcssPlugin: "postcss-icss-parser",
// 在 PostCSS AST 遍历完成后执行这个异步方法
OnceExit(root) {
// 从 AST 根节点提取 ICSS 导出信息
const { icssExports } = icssUtils.extractICSS(root);
// 遍历提取到的所有导出名称
for (const name of Object.keys(icssExports)) {
// 获取当前导出名称对应的值
const value = icssExports[name]
// 将当前导出名称和值添加到 options.exports 数组中
options.exports.push({ name, value });
}
}
};
};
plugin.postcss = true;
let options = { exports: [] };
// 创建一个 PostCSS 处理管道,包含我们的插件
const pipeline = postcss([plugin(options)]);
// 处理输入的 CSS
const res = pipeline.process(input);
// 输出处理后的 CSS 一定要执行此行代码,否则不会进行编译
console.log(res.css);
console.log(options.exports);
src\index.js
const indexCss = require("./index.css");
console.log(indexCss);
const div = document.createElement("div");
div.className = indexCss.background;
div.innerHTML = '"div"';
document.body.appendChild(div);
src\index.css
:local(.background){
width:100px;
height:100px;
background-color: green;
}
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
devtool: false,
module: {
rules: [
{
test: /\.css$/,
use: [
/* {
loader: 'style-loader'
//loader: path.resolve('loaders/style-loader'),
}, */
{
//loader: 'css-loader',
loader: path.resolve('loaders/css-loader'),
options: {
esModule: false,
url: true,
import: true,
importLoaders: 0,
+ modules: {
+ exportOnlyLocals: false
+ }
}
}
],
include: path.resolve('src')
},
{
test: /\.(png|jpg|gif)$/,
type: 'asset/resource'
}
]
},
plugins: [
new HtmlWebpackPlugin({ template: './src/index.html' }),
]
}
loaders\style-loader\index.js
// 引入 path 模块,用于处理文件路径
const path = require('path');
// 定义一个正则表达式,用于匹配相对路径
const matchRelativePath = /^\.\.?[/\\]/;
// 定义一个函数,用于检查给定的字符串是否为相对路径
function isRelativePath(str) {
return matchRelativePath.test(str);
}
// 定义一个函数,用于将请求字符串转换为相对于 loaderContext.context 的路径
function stringifyRequest(loaderContext, request) {
// 使用感叹号分割请求字符串
const splitted = request.split("!");
// 从 loaderContext 中解构出 context
const { context } = loaderContext;
// 使用 JSON.stringify 将处理后的请求数组转换为字符串
return JSON.stringify(
// 遍历 splitted 数组
splitted
.map((singlePath) => {
// 将单个路径转换为相对于 context 的相对路径
singlePath = path.relative(context, singlePath);
// 如果路径不是相对路径,则添加 "./" 前缀
if (!isRelativePath(singlePath)) {
singlePath = `./${singlePath}`;
}
// 将路径中的反斜杠替换为正斜杠
return singlePath.replace(/\\/g, "/");
})
// 使用感叹号将处理后的请求重新连接为字符串
.join("!")
);
}
// 定义一个空的 loaderAPI 函数
function loaderAPI() { }
// 在 loaderAPI 上定义 pitch 方法,用于处理剩余请求
loaderAPI.pitch = function loader(remindingRequest) {
// 定义将要返回的代码字符串,用于将样式内容添加到 HTML 文档中
const contentCode = `
// 通过 require 导入样式内容
const content = require("!!"+${stringifyRequest(this, `${remindingRequest}`)});
// 创建一个 style 元素
const element = document.createElement("style");
// 将样式内容设置为 style 元素的 innerHTML
element.innerHTML = content.toString();
// 将 style 元素添加到文档的 head 部分
document.head.appendChild(element);
+ module.exports = content.locals?content.locals:content;
`;
// 返回生成的代码字符串
return contentCode;
};
// 导出 loaderAPI
module.exports = loaderAPI;
loaders\css-loader\index.js
// 从 './utils' 模块中导入所需的工具函数
+const { getImportCode, getModuleCode, getExportCode, stringifyRequest,
combineRequests, getPreRequester, shouldUseIcssPlugin, shouldUseModulesPlugins, getModulesPlugins } = require('./utils');
const postcss = require('postcss');
const urlParser = require('./plugins/postcss-url-parser');
const importParser = require('./plugins/postcss-import-parser');
+const icssParser = require("./plugins/postcss-icss-parser");
// 定义 loader 函数,它接受一个参数 content(通常是源文件的内容)
function loader(content) {
// 获取 loader 的配置选项
const options = this.getOptions();
// 为异步回调创建一个回调函数
const callback = this.async();
const plugins = [];
const replacements = [];
+ const exports = [];
+ if (shouldUseModulesPlugins(options)) {
+ plugins.push(...getModulesPlugins(this));
+ }
// 定义要导入的模块及其别名
const urlPluginImports = [];
const importPluginImports = [];
const importPluginApi = [];
if (options.import) {
plugins.push(importParser({
imports: importPluginImports,
api: importPluginApi,
loaderContext: this,
urlHandler: url => stringifyRequest(this, combineRequests(getPreRequester(this, options.importLoaders), url))
}));
}
if (options.url) {
plugins.push(urlParser({
imports: urlPluginImports,
replacements,
loaderContext: this,
urlHandler: url => stringifyRequest(this, url)
}));
}
+ const needToUseIcssPlugin = shouldUseIcssPlugin(options);
+ if (needToUseIcssPlugin) {
+ plugins.push(icssParser({
+ loaderContext: this,
+ exports
+ }));
+ }
postcss(plugins)
.process(content, { from: this.resourcePath, to: this.resourcePath })
.then((result) => {
const imports = [
{
importName: "cssLoaderApiNoSourcemapImport",
url: stringifyRequest(this, require.resolve("./runtime/noSourceMaps"))
},
{
importName: "cssLoaderApiImport",
url: stringifyRequest(this, require.resolve("./runtime/api"))
}
];
imports.push(...importPluginImports, ...urlPluginImports,);
// 使用工具函数生成导入代码
const importCode = getImportCode(imports, options);
// 使用工具函数生成模块代码
const moduleCode = getModuleCode(result, importPluginApi, replacements);
// 使用工具函数生成导出代码
+ const exportCode = getExportCode(exports, options);
// 将生成的代码传递给异步回调函数
callback(null, `${importCode}${moduleCode}${exportCode}`);
})
}
// 导出 loader 函数
module.exports = loader;
loaders\css-loader\utils.js
+const postcssModulesScope = require("./postcss-modules-scope");
// 导入路径相关的库
const path = require("path");
// 定义一个函数,用于生成 import 代码
function getImportCode(imports, options) {
let code = "";
// 遍历 imports 对象,生成 import 代码
for (const item of imports) {
const { importName, url } = item;
code += `var ${importName} = require(${url});\n`;
}
return code;
}
// 定义一个函数,用于生成模块代码
function getModuleCode(result, api, replacements) {
let code = JSON.stringify(result.css);
let beforeCode = `var cssLoaderExport = cssLoaderApiImport(cssLoaderApiNoSourcemapImport);\n`;
for (const item of api) {
beforeCode += `cssLoaderExport.i(${item.importName});\n`;
}
for (const item of replacements) {
const { replacementName, importName } = item;
beforeCode += `var ${replacementName} = cssLoaderGetUrlImport(${importName});\n`;
code = code.replace(
new RegExp(replacementName, "g"),
() => `" + ${replacementName} + "`
);
}
return `${beforeCode}cssLoaderExport.push([module.id, ${code}]);\n`;
}
// 定义一个函数,用于生成导出代码
function getExportCode(exports, options) {
let code = "";
+ let localsCode = "";
+ const addExportToLocalsCode = (name, value) => {
+ if (localsCode) {
+ localsCode += `,\n`;
+ }
+ localsCode += `\t${JSON.stringify(name)}: ${JSON.stringify(value)}`;
+ }
+ for (const { name, value } of exports) {
+ addExportToLocalsCode(name, value);
+ }
+ if (options.modules.exportOnlyLocals) {
+ code += `${options.esModule ? "export default" : "module.exports ="} {\n${localsCode}\n};\n`;
+ return code;
+ }
+ code += `cssLoaderExport.locals = {${`\n${localsCode}\n`}};\n`;
let finalExport = "cssLoaderExport";
code += `${options.esModule ? "export default" : "module.exports ="} ${finalExport};\n`;
return code;
}
// 定义一个函数,用于将请求字符串转换为相对路径字符串
function stringifyRequest(loaderContext, request) {
return JSON.stringify(loaderContext.utils.contextify(loaderContext.context, request));
}
function getPreRequester({ loaders, loaderIndex }, { importLoaders = 0 }) {
const loadersRequest = loaders.slice(loaderIndex, loaderIndex + 1 + importLoaders).map(x => x.request).join("!");
return "-!" + loadersRequest + "!";
}
function combineRequests(preRequest, url) {
return preRequest + url;
}
+function shouldUseModulesPlugins(options) {
+ if (typeof options.modules === "boolean" && options.modules === false) {
+ return false;
+ }
+ return true
+}
+function shouldUseIcssPlugin(options) {
+ return Boolean(options.modules);
+}
+function getModulesPlugins(loaderContext) {
+ let plugins = [postcssModulesScope({ loaderContext })];
+ return plugins;
+}
// 导出相关函数
exports.getModuleCode = getModuleCode;
exports.getImportCode = getImportCode;
exports.getExportCode = getExportCode;
exports.stringifyRequest = stringifyRequest;
exports.getPreRequester = getPreRequester;
exports.combineRequests = combineRequests;
+exports.shouldUseModulesPlugins = shouldUseModulesPlugins;
+exports.getModulesPlugins = getModulesPlugins;
+exports.shouldUseIcssPlugin = shouldUseIcssPlugin;
loaders\css-loader\postcss-modules-scope.js
// 引入 postcss-selector-parser 模块
const selectorParser = require("postcss-selector-parser");
// 引入 crypto 模块,用于生成哈希值
const crypto = require("crypto");
// 定义生成范围限定名称的函数,接收类名和加载器上下文作为参数
function generateScopedName(name, loaderContext) {
// 使用 MD4 哈希算法创建一个新哈希对象
const hash = crypto.createHash('md4')
// 使用 loader 上下文的资源路径更新哈希
hash.update(loaderContext.resourcePath);
// 返回生成的范围限定名称,包含哈希值和原始名称
return `_${hash.digest('hex')}__${name}`;
};
// 定义插件函数,接收一个包含 loaderContext 的对象作为参数
const plugin = ({ loaderContext }) => {
return {
// 设置插件名称为 "postcss-modules-scope"
postcssPlugin: "postcss-modules-scope",
// 在 PostCSS AST 的根节点执行这个方法
Once(root, { rule }) {
// 创建一个空的导出对象
const exports = Object.create(null);
// 定义导出范围限定名称的函数
function exportScopedName(name) {
// 生成范围限定名称
const scopedName = generateScopedName(name, loaderContext);
// 将生成的名称添加到导出对象中
exports[name] = scopedName;
// 返回生成的名称
return scopedName;
}
// 根据节点类型处理节点,返回处理后的节点
function localizeNode(node) {
switch (node.type) {
case "selector":
node.nodes = node.map(localizeNode);
return node;
case "class":
return selectorParser.className({
value: exportScopedName(node.value)
});
}
}
// 遍历节点,并根据节点类型进行相应处理
function traverseNode(node) {
if (node.type === "pseudo" && node.value === ":local") {
const selector = localizeNode(node.first);
node.replaceWith(selector);
return;
}
if (node.type === "root" || node.type === "selector") {
node.each(traverseNode);
}
return node;
}
// 遍历根节点的所有规则
root.walkRules(rule => {
let parsedSelector = selectorParser().astSync(rule);
rule.selector = traverseNode(parsedSelector.clone()).toString();
});
// 获取导出对象中的所有名称
const exportedNames = Object.keys(exports);
// 如果存在导出名称,则创建一个新的导出规则
if (exportedNames.length > 0) {
const exportRule = rule({
selector: ":export"
});
// 将导出名称添加到导出规则中
exportedNames.forEach(exportedName => exportRule.append({
prop: exportedName,
value: exports[exportedName],
raws: { before: "\n " }
}));
// 将导出规则添加到根节点
root.append(exportRule);
}
}
};
};
// 设置插件与 postcss兼容
plugin.postcss = true;
// 将插件导出,以便在其他模块中使用
module.exports = plugin;
loaders\css-loader\plugins\postcss-icss-parser.js
// 引入 icss-utils 模块
var icssUtils = require("icss-utils");
// 定义插件函数,接收一个默认为空对象的 options 参数
const plugin = (options = {}) => {
return {
// 设置插件名称为 "postcss-icss-parser"
postcssPlugin: "postcss-icss-parser",
// 在 PostCSS AST 遍历完成后执行这个异步方法
async OnceExit(root) {
// 从 AST 根节点提取 ICSS 导出信息
const { icssExports } = icssUtils.extractICSS(root);
// 遍历提取到的所有导出名称
for (const name of Object.keys(icssExports)) {
// 获取当前导出名称对应的值
const value = icssExports[name]
// 将当前导出名称和值添加到 options.exports 数组中
options.exports.push({ name, value });
}
}
};
};
// 设置插件与 postcss 兼容
plugin.postcss = true;
// 导出插件
module.exports = plugin;