1.loader #

1.1 loader 运行的总体流程 #

webpackflowloader

1.2 loader-runner #

loaderrunner

1.2.1 runner.js #

// 引入 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);
});

1.2.2 pre2-loader.js #

loaders-chain\pre2-loader.js

function loader(source) {
    console.log("pre2");
    return source + "//pre2";
}
loader.pitch = function () {
    console.log("pitch-pre2");
};
module.exports = loader;

1.2.3 pre1-loader.js #

loaders-chain\pre1-loader.js

function loader(source) {
    console.log("pre1");
    return source + "//pre1";
}
loader.pitch = function () {
    console.log("pitch-pre1");
};
module.exports = loader;

1.2.4 normal2-loader.js #

loaders-chain\normal2-loader.js

function loader(source) {
    console.log("normal2");
    return source + "//normal2";
}
loader.pitch = function () {
    console.log("pitch-normal2");
};
module.exports = loader;

1.2.5 normal1-loader.js #

loaders-chain\normal1-loader.js

function loader(source) {
    console.log("normal1");
    return source + "//normal1";
}
loader.pitch = function () {
    console.log("pitch-normal1");
};
module.exports = loader;

1.2.6 inline2-loader.js #

loaders-chain\inline2-loader.js

function loader(source) {
    console.log("inline2");
    return source + "//inline2";
}
loader.pitch = function () {
    console.log("pitch-inline2");
};
module.exports = loader;

1.2.7 inline1-loader.js #

loaders-chain\inline1-loader.js

function loader(source) {
    console.log("inline1");
    return source + "//inline1";
}
loader.pitch = function () {
    console.log("pitch-inline1");
};
module.exports = loader;

1.2.8 post2-loader.js #

loaders-chain\post2-loader.js

function loader(source) {
    console.log("post2");
    return source + "//post2";
}
loader.pitch = function () {
    console.log("pitch-post2");
};
module.exports = loader;

1.2.9 post1-loader.js #

loaders-chain\post1-loader.js

function loader(source) {
    console.log("post1");
    return source + "//post1";
}
loader.pitch = function () {
    console.log("pitch-post1");
};
module.exports = loader;

pitchloaderexec

1.3 pitch #

|- 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

loaderrunner2

1.4 特殊配置 #

符号 变量 含义
-! 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);
});

1.5 实现loader-runner #

1.5.1 runner.js #

// 引入 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);
});

1.5.2 loader-runner.js #

// 引入 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;

2.babel-loader #

属性
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
 */

1.使用css-loader #

1.1 安装 #

npm install webpack webpack-cli html-webpack-plugin css-loader style-loader --save

1.2 package.json #

package.json

{
    "scripts": {
    "build": "webpack"
  },
}

1.3 webpack.config.js #

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' }),
    ]
}

1.4 src\index.js #

src\index.js

const indexCss = require("./index.css");
console.log(indexCss);

1.5 src\index.html #

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>

1.6 src\index.css #

src\index.css

body {
    color: red;
}

1.7 dist\main.js #

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);
``

2.实现css-loader #

2.1 webpack.config.js #

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' }),
    ]
}

2.2 css-loader\index.js #

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;

2.3 utils.js #

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;

2.4 api.js #

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;
};

2.5 noSourceMaps.js #

loaders\css-loader\runtime\noSourceMaps.js

// 导出一个函数,输入参数为 i
module.exports = (i) => {
  // 返回 i 数组的第二个元素(下标为 1)
  return i[1];
};

3.使用style-loader #

3.1 webpack.config.js #

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' }),
    ]
}

3.2 style-loader\index.js #

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;

4.url #

4.1 postcss-value-parser #

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'
        }
    ],
}
 */

4.2 webpack.config.js #

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' }),
    ]
}

4.3 src\index.css #

src\index.css

body {
    color: red;
+        background-image: url(./images/logo.png);
+        background-repeat: no-repeat;
}

4.4 css-loader\index.js #

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;

4.5 utils.js #

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;

4.6 postcss-url-parser.js #

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;

4.7 getUrl.js #

loaders\css-loader\runtime\getUrl.js

module.exports = (url) => {
    return url;
};

5.import #

5.1 src\index.css #

src\index.css

+@import "basic.css";
body {
    color: red;
        background-image: url(./images/logo.png);
        background-repeat: no-repeat;
}

5.2 src\basic.css #

src\basic.css

body{
    background-color: green;
}

5.3 webpack.config.js #

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' }),
    ]
}

5.4 css-loader\index.js #

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;

5.5 utils.js #

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;

5.6 postcss-import-parser.js #

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;

6.modules #

6.1 postcss-selector-parser #

6.1.1 root.json #

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)"
        }
    ]
}

6.1.2 selector.json #

{
    "nodes": [
        {
            "nodes": [
                {
                    "value": ":local",
                    "nodes": [
                        {
                            "nodes": [
                                {
                                    "value": "background",
                                    "type": "class"
                                }
                            ],
                            "type": "selector"
                        }
                    ],
                    "type": "pseudo"
                }
            ],
            "type": "selector"
        }
    ],
    "type": "root"
}

6.1.3 postcss-selector-parser.js #

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;
}
 */

6.2 icss-utils #

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);

6.3 src\index.js #

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);

6.4 src\index.css #

src\index.css

:local(.background){
    width:100px;
    height:100px;
  background-color: green;
}

6.5 webpack.config.js #

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' }),
    ]
}

6.6 style-loader\index.js #

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;

6.7 css-loader\index.js #

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;

6.8 utils.js #

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;

6.9 postcss-modules-scope.js #

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;

6.10 postcss-icss-parser.js #

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;