webpack(3) - loader

一、loader 在webpack 中的作用

loader 用于对模块的源代码进行转换。

loader 可以使你在 import 或 "load(加载)" 模块时预处理文件。

因此,loader 类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的得力方式。

loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript 或将内联图像转换为 data URL。

loader 甚至允许你直接在 JavaScript 模块中 import CSS文件!

二、loader 运行的总体流程

webpack-loader.png

1、默认配置

webpack 入口文件 webpack.js ,根据配置文件 设置配置的options

options = new WebpackOptionsDefaulter().process(options);
    compiler = new Compiler(options.context);
    compiler.options = options;

WebpackOptionsDefaulter 加载 默认配置

// WebpackOptionsDefaulter.js
 
this.set("module.defaultRules", "make", options => [
      {
        type: "javascript/auto",
        resolve: {}
      },
      {
        test: /\.mjs$/i,
        type: "javascript/esm",
        resolve: {
          mainFields:
            options.target === "web" ||
            options.target === "webworker" ||
            options.target === "electron-renderer"
              ? ["browser", "main"]
              : ["main"]
        }
      },
      {
        test: /\.json$/i,
        type: "json"
      },
      {
        test: /\.wasm$/i,
        type: "webassembly/experimental"
      }
    ]);
   //...


    this.set("optimization.splitChunks.cacheGroups.default", {
      automaticNamePrefix: "",
      reuseExistingChunk: true,
      minChunks: 2,
      priority: -20
    });
    this.set("optimization.splitChunks.cacheGroups.vendors", {
      automaticNamePrefix: "vendors",
      test: /[\\/]node_modules[\\/]/,
      priority: -10
    });


 // ...

image2020-12-28_11-8-42.png

上图可以看出一些默认配置已经加载,WebpackOptionsApply 模块主要是根据options选项的配置,设置compile的相应的插件,属性,里面写了大量的 apply(compiler);
使得模块的this指向compiler

2、创建NormalModuleFactory

处理完一些webpac.config 和一些内部配置,在一个module 构建的过程中,首先根据module 的依赖类型,调用对应的构造函数来创建对应的模块

// Compiler.js
  
  createNormalModuleFactory() {
    const normalModuleFactory = new NormalModuleFactory(
      this.options.context,
      this.resolverFactory,
      this.options.module || {}
    );
    this.hooks.normalModuleFactory.call(normalModuleFactory);
    return normalModuleFactory;
  }



newCompilationParams() {
    const params = {
      normalModuleFactory: this.createNormalModuleFactory(),
      contextModuleFactory: this.createContextModuleFactory(),
      compilationDependencies: new Set()
    };
    return params;
  }

当NormalModuleFactory 实例化完成之后,在compilation 内部调用 这个实例create 创建normalModule

image2020-12-28_11-35-30.png

3、解析loader 路径

这里需要用到上一个阶段讲到的 NormalModuleFactory 实例, NormalModuleFactory 的 create 方法是创建 NormalModule 实例的入口, 内部需要解析一写module ,其中就包含 loaders ,资源路径 resource 等等,最终将解析完毕的参数传给 NormalModule 构建函数直接实例化

在NormalModuleFactory中,创建出NormalModule实例之前会涉及到四个钩子:

  • beforeResolve
  • factory: 负责来基于resolve钩子返回值来创建NormalModule
  • resolver : 负责解析loader 模块路径 (例如css-loader这个loader的模块路径是什么)
  • afterResolve

resolve钩子上注册的方法较长,其中还包括了模块资源本身的路径解析。resolver有两种,分别是loaderResolver和normalResolver

//  NormalModuleFactory.js

this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {
  const contextInfo = data.contextInfo;
  const context = data.context;
  const request = data.request;

 const loaderResolver = this.getResolver("loader");
 const normalResolver = this.getResolver("normal", data.resolveOptions);

....
}
   

loader 路径解析也分为两种:inline loader和config文件中的loader。resolver钩子中会先处理inline loader

resolver钩子中会先处理inline loader
比如: import Styles from 'style-loader!css-loader?modules!./styles.css';

通过 上面的 request 解析出来 所需要的loader路径

//  NormalModuleFactory.js

this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {
  const contextInfo = data.contextInfo;
  const context = data.context;
  const request = data.request;  

// 假如 import Styles from 'style-loader!css-loader?modules!./styles.css'
// 这时候 request 就是

 const loaderResolver = this.getResolver("loader");
 const normalResolver = this.getResolver("normal", data.resolveOptions);

 let matchResource = undefined;
 let requestWithoutMatchResource = request;
...
   // 是否忽略perLoader 
const noPreAutoLoaders = requestWithoutMatchResource.startsWith("-!");
      // 是否忽略normalLoader
const noAutoLoaders =
        noPreAutoLoaders || requestWithoutMatchResource.startsWith("!");
        // 忽略所有的 perLoader、normalLoader、postLoader
 const noPrePostAutoLoaders = requestWithoutMatchResource.startsWith("!!");
      // 首先解析出所需要的 loader,这种loader 为内联loader
 let elements = requestWithoutMatchResource
                .replace(/^-?!+/, "")
                .replace(/!!+/g, "!")
                .split("!");
let resource = elements.pop();  // 获取资源路径
      // 获取每个loader对应的options 配置
elements = elements.map(identToLoaderRequest);  
....
}

从上面可知 request :style-loader!css-loader?modules!./styles.css

let elements = requestWithoutMatchResource
    .replace(/^-?!+/, "")
    .replace(/!!+/g, "!")
    .split("!");

从style-loader!css-loader?modules!./styles.css中可以取出
两个loader:style-loader和css-loader

image.png

webpack首先解析了inline loader的绝对路径与配置。接下来则是解析config文件中的loader (source code),

//  NormalModuleFactory.js
const result = this.ruleSet.exec({
    resource: resourcePath,
    realResource:
        matchResource !== undefined
            ? resource.replace(/\?.*/, "")
            : resourcePath,
    resourceQuery,
    issuer: contextInfo.issuer,
    compiler: contextInfo.compiler
});

ruleSet: 它可以根据模块路径名,匹配出模块所需的loader
返回的result就是当前模块匹配出的config中的loader

image.png
webpack-loader.png
const settings = {};
const useLoadersPost = [];  // post loader
const useLoaders = [];  //  normal loader
const useLoadersPre = [];  // pre loader
for (const r of result) {
    if (r.type === "use") {
        if (r.enforce === "post" && !noPrePostAutoLoaders) {
           useLoadersPost.push(r.value);
          } else if (
              r.enforce === "pre" 
             &&!noPreAutoLoaders
             &&!noPrePostAutoLoaders) {
             useLoadersPre.push(r.value);   // perLoader
            } else if (!r.enforce &&!noAutoLoaders &&!noPrePostAutoLoaders) {
            useLoaders.push(r.value);   // normal loader
     }
  } else if (
       typeof r.value === "object" &&r.value !== null &&settings[r.type] !== null
  ) {
     settings[r.type] = cachedCleverMerge(settings[r.type], r.value);
  } else {
     settings[r.type] = r.value;
   }
 }

最后,使用neo-aysnc来并行解析三类loader数组

asyncLib.parallel(
[
 this.resolveRequestArray.bind(
  this,
  contextInfo,
  this.context,
  useLoadersPost,
  loaderResolver,
),
 this.resolveRequestArray.bind(
  this,
  contextInfo,
  this.context,
  useLoaders,
  loaderResolver,
 ),
this.resolveRequestArray.bind(
  this,
  contextInfo,
  this.context,
  useLoadersPre,
  loaderResolver,
),                      
]

不同类型 loader 上的 pitch 方法执行的顺序为:
postLoader.pitch -> inlineLoader.pitch -> normalLoader.pitch -> preLoader.pitch
最终 loader 所执行的顺序对应为:
preLoader -> normalLoader -> inlineLoader -> postLoader

loader是从右至左执行的,真实的loader执行顺序是倒过来的,因此inlineLoader是整体后于config中normal loader执行的

经过上面一下步骤,loader 解析工作基本完成。

补充: 前面提到 RuleSet - 会将resourcePath应用于所有的module.rules规则,从而筛选出所需的loader

  • RuleSet - 含有类静态方法.normalizeRule() 和实例方法.exec()
  • 通过其上的静态方法.normalizeRule()将配置值转换为标准化的test对象,会存储一个this.references属性是一个map类型的存储,key是loader在配置中的类型和位置。例如,ref-2表示loader配置数组中的第三个。
  • this.ruleSet.exec()中传入源码模块路径,返回的result就是当前模块匹配出的config中的loader
image.png

4、loader的运行

loader的绝对路径解析完毕后,在NormalModuleFactory的factory钩子中会创建当前模块的NormalModule对象

在创建完 NormalModule 实例之后会调用 build 方法继续进行内部的构建。我们熟悉的 loaders 将会在这里开始应用

4.1、 loader-runner - loader的执行库

loader-runner分为了两个部分:loadLoader.js与LoaderRunner.js。

  • module 开始构建的过程中,会先创建一个 loaderContext 对象。
    所有的 loader 会共享这个 loaderContext 对象,每个loader 执行的时候,上下文 是这个对象
doBuild(options, compilation, resolver, fs, callback) {
    const loaderContext = this.createLoaderContext(
        resolver,
            options,
        compilation,
        fs
    );
 runLoaders(
  {
   resource: this.resource,
   loaders: this.loaders,
   context: loaderContext, // loaderContext 上下文
   readResource: fs.readFile.bind(fs)  // 读取文件的
  },
....
  )
}
  • 初始化 loaderContext ,完成之后,开始调用runLoader 方法,到了loaders 执行。
// 
exports.runLoaders = function runLoaders(options, callback) {
  // read options
  var resource = options.resource || "";  // 模块路径
  var loaders = options.loaders || [];   // 所需要的loader
  var loaderContext = options.context || {};   // 在 normalModule 里创建的loaderContext
  var readResource = options.readResource || readFile;


  //
  var splittedResource = resource && splitQuery(resource);
  var resourcePath = splittedResource ? splittedResource[0] : undefined;  // 模块实际路径
  var resourceQuery = splittedResource ? splittedResource[1] : undefined; // 模块路径 query 参数
  var contextDirectory = resourcePath ? dirname(resourcePath) : null;  // 

  // execution state
  var requestCacheable = true;
  var fileDependencies = [];
  var contextDependencies = [];

  // prepare loader objects
  loaders = loaders.map(createLoaderObject);  // 处理loader

  loaderContext.context = contextDirectory;
  loaderContext.loaderIndex = 0;
  loaderContext.loaders = loaders;
  loaderContext.resourcePath = resourcePath;
  loaderContext.resourceQuery = resourceQuery;
  loaderContext.async = null;
  loaderContext.callback = null;
  loaderContext.cacheable = function cacheable(flag) {
    if(flag === false) {
      requestCacheable = false;
    }
  };
  loaderContext.dependency = loaderContext.addDependency = function addDependency(file) {
    fileDependencies.push(file);
  };
  loaderContext.addContextDependency = function addContextDependency(context) {
    contextDependencies.push(context);
  };
  loaderContext.getDependencies = function getDependencies() {
    return fileDependencies.slice();
  };
  loaderContext.getContextDependencies = function getContextDependencies() {
    return contextDependencies.slice();
  };
  loaderContext.clearDependencies = function clearDependencies() {
    fileDependencies.length = 0;
    contextDependencies.length = 0;
    requestCacheable = true;
  };
  
  // 被构建的模块 路径, loaderContext.resource -> getter/setter
  Object.defineProperty(loaderContext, "resource", {
    enumerable: true,
    get: function() {
      if(loaderContext.resourcePath === undefined)
        return undefined;
      return loaderContext.resourcePath + loaderContext.resourceQuery;
    },
    set: function(value) {
      var splittedResource = value && splitQuery(value);
      loaderContext.resourcePath = splittedResource ? splittedResource[0] : undefined;
      loaderContext.resourceQuery = splittedResource ? splittedResource[1] : undefined;
    }
  });

  // 所有的loader以及 这个模块的resource 所组成request 字符串
  Object.defineProperty(loaderContext, "request", {
    enumerable: true,
    get: function() {
      return loaderContext.loaders.map(function(o) {
        return o.request;
      }).concat(loaderContext.resource || "").join("!");
    }
  });
  // 执行loader 提供的 pitch 函数阶段传入函数,未被调用的 loader.pitch 组成的request 字符串
  Object.defineProperty(loaderContext, "remainingRequest", {
    enumerable: true,
    get: function() {
      if(loaderContext.loaderIndex >= loaderContext.loaders.length - 1 && !loaderContext.resource)
        return "";
      return loaderContext.loaders.slice(loaderContext.loaderIndex + 1).map(function(o) {
        return o.request;
      }).concat(loaderContext.resource || "").join("!");
    }
  });
  
  Object.defineProperty(loaderContext, "currentRequest", {
    enumerable: true,
    get: function() {
      return loaderContext.loaders.slice(loaderContext.loaderIndex).map(function(o) {
        return o.request;
      }).concat(loaderContext.resource || "").join("!");
    }
  });
  // 包含已经执行的loader.pitch 所组成的request 字符串
  Object.defineProperty(loaderContext, "previousRequest", {
    enumerable: true,
    get: function() {
      return loaderContext.loaders.slice(0, loaderContext.loaderIndex).map(function(o) {
        return o.request;
      }).join("!");
    }
  });
  
  // 获取当前正在执行loader 的query参数
  // 
  Object.defineProperty(loaderContext, "query", {
    enumerable: true,
    get: function() {
      var entry = loaderContext.loaders[loaderContext.loaderIndex];
      return entry.options && typeof entry.options === "object" ? entry.options : entry.query;
    }
  });
  
  // 每个loader 在pitch 阶段和正常执行都可以共享的 data 数据
  Object.defineProperty(loaderContext, "data", {
    enumerable: true,
    get: function() {
      return loaderContext.loaders[loaderContext.loaderIndex].data;
    }
  });

  // finish loader context
  if(Object.preventExtensions) {
    Object.preventExtensions(loaderContext);
  }

  var processOptions = {
    resourceBuffer: null,
    readResource: readResource
  };
  
  // 开始执行每个loader 上的pitch 函数
  iteratePitchingLoaders(processOptions, loaderContext, function(err, result) {
    if(err) {
      return callback(err, {
        cacheable: requestCacheable,
        fileDependencies: fileDependencies,
        contextDependencies: contextDependencies
      });
    }
    callback(null, {
      result: result,
      resourceBuffer: processOptions.resourceBuffer,
      cacheable: requestCacheable,
      fileDependencies: fileDependencies,
      contextDependencies: contextDependencies
    });
  });
};
  • 从上面 runLoader 方法来看,loader-runner中对应的
    iteratePitchingLoaders()和iterateNormalLoaders()两个方法
  • iteratePitchingLoaders()会递归执行,并记录loader的pitch状态与当前执行到的loaderIndex(loaderIndex++)
    从下图可以知道 iteratePitchingLoaders 中 通过pitchExecuted 属性来判断 是否执行过pitch
image.png
  • 当loaderContext.loaderIndex值达到整体loader数组长度时,表明所有pitch都被执行完毕。所有pitch都被执行完毕(执行到了最后的loader),这时会调用processResource()来处理模块资源
    if(loaderContext.loaderIndex >= loaderContext.loaders.length)
    return processResource(options, loaderContext, callback);
image.png
  • processResource 会递归执行iterateNormalLoaders()并进行loaderIndex--操作,因此loader会“反向”执行。
function processResource(options, loaderContext, callback) {
    // set loader index to last loader
    loaderContext.loaderIndex = loaderContext.loaders.length - 1;

    var resourcePath = loaderContext.resourcePath;
    if(resourcePath) {
        loaderContext.addDependency(resourcePath);
        options.readResource(resourcePath, function(err, buffer) {
            if(err) return callback(err);
            options.resourceBuffer = buffer;
            iterateNormalLoaders(options, loaderContext, [buffer], callback);
        });
    } else {
        iterateNormalLoaders(options, loaderContext, [null], callback);
    }
}
function iterateNormalLoaders(options, loaderContext, args, callback) {
...
    if(currentLoaderObject.normalExecuted) {
        loaderContext.loaderIndex--;
        return iterateNormalLoaders(options, loaderContext, args, callback);
    }
...
}
  • loader 中的this其实是一个叫loaderContext的对象,那么this.data的实现其实就是loaderContext.data
  // 每个loader 在pitch 阶段和正常执行都可以共享的 data 数据
    Object.defineProperty(loaderContext, "data", {
        enumerable: true,
        get: function() {
            return loaderContext.loaders[loaderContext.loaderIndex].data;
        }
    });

调用this.data时,不同的normal loader由于loaderIndex不同,会得到不同的值;而pitch方法的形参data也是不同的loader下的data

4.2、 pitch 和 normal 执行 都是在 runSyncOrAsync
function runSyncOrAsync(fn, context, args, callback) {
    var isSync = true;  // 是否为同步
    var isDone = false;
    var isError = false; // internal error
  var reportedError = false;
  // 给loaderContext 上下文赋值 async 函数,用来将loader 异步并返回 异步回调
    context.async = function async() {
        if(isDone) {
            if(reportedError) return; // ignore
            throw new Error("async(): The callback was already called.");
        }
        isSync = false;
        return innerCallback;
  };
  // callback 这种形式,可以向下一个loader 传递多个参数
    var innerCallback = context.callback = function() {
        if(isDone) {
            if(reportedError) return; // ignore
            throw new Error("callback(): The callback was already called.");
        }
        isDone = true;
        isSync = false;
        try {
            callback.apply(null, arguments);
        } catch(e) {
            isError = true;
            throw e;
        }
    };
    try {
        var result = (function LOADER_EXECUTION() {  // 开始执行loader
            return fn.apply(context, args);
        }());
        if(isSync) {
            isDone = true;
            if(result === undefined)  // 如果没有返回值,执行callback 开始下一个loader 执行
                return callback();
            if(result && typeof result === "object" && typeof result.then === "function") {
                return result.then(function(r) {  // loader 返回一个promise 实例,待实例被resolve 或者reject后执行下一个loader
                    callback(null, r);
                }, callback);
            }
            return callback(null, result);
        }
    } catch(e) {
        if(isError) throw e;
        if(isDone) {
            // loader is already "done", so we cannot use the callback function
            // for better debugging we print the error on the console
            if(typeof e === "object" && e.stack) console.error(e.stack);
            else console.error(e);
            return;
        }
        isDone = true;
        reportedError = true;
        callback(e);
    }

}

pitch:

// runLoader.js
// 开始执行 pitch 函数
        runSyncOrAsync(
            fn,
            loaderContext, [loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}],
            function(err) {
                if(err) return callback(err);
                var args = Array.prototype.slice.call(arguments, 1);
                if(args.length > 0) {
                    loaderContext.loaderIndex--;
                    iterateNormalLoaders(options, loaderContext, args, callback);
                } else {
                    iteratePitchingLoaders(options, loaderContext, callback);
                }
            }
        );


下图显示 : loaderContext.remainingRequest 表示剩余 loader 拼接成字符 (未被调用的 loader.pitch 组成的request 字符串)


image.png

normal:

    runSyncOrAsync(fn, loaderContext, args, function(err) {
        if(err) return callback(err);

        var args = Array.prototype.slice.call(arguments, 1);
        iterateNormalLoaders(options, loaderContext, args, callback);
    });
4.3、webpack 中loader 四种类型
  • pre (前置)
{
    test: /test\.js$/,
    loader: 'loader1',        
    enforce: 'pre'
}
 rules: [{
    test: /\.js$/,
    use: ['eslint-loader'],
    enforce: 'pre',
    exclude: /node_modules/,
  }]

Pre表示这个loader在所有的loader之前执行。配置表示在所有处理js文件模块loader之前执行eslint-loader,这样我们可以在js代码未被处理的时候就进行eslint代码规范校验

  • normal : 是普通loader ,没有配置就是普通的loader
  • inline :

import 'loader1!loader2!./test.js';

  • post:
{
    test: /test\.js$/,
    loader: 'loader5',
    enforce: 'post'
},

调用顺序:

不同类型 loader 上的 pitch 方法执行的顺序为:
postLoader.pitch -> inlineLoader.pitch -> normalLoader.pitch -> preLoader.pitch
最终 loader 所执行的顺序对应为:
preLoader -> normalLoader -> inlineLoader -> postLoader

  • 比如: a!b!c!module, 正常调用顺序应该是c、b、a,但是真正调用顺序是
    a(pitch)、b(pitch)、c(pitch)、c、b、a,比如如果b返回了字符串"result b", 接下来只有a会被系统执行,且a的loader收到的参数是result b

三、 常用loader的执行机制分析

style-loader

style-loader 中pitch 中的作用:

我们知道css-loader最后会导出一段js字符串,里面可能包含需要动态执行的函数。按照正常的执行顺序,style-loader只能拿到这些字符串而并不能把他们转成真正的css代码

// style-loader .js
loaderApi.pitch = function loader(request) {
// // 获取webpack配置的options
 const options = _loaderUtils.default.getOptions(this);
 (0, _schemaUtils.validate)(_options.default, options, {
    name: 'Style Loader',
    baseDataPath: 'options'
  });
 // // 定义了两个变量,不难看出insert的默认值为head,injectType默认值为styleTag
  const insert = typeof options.insert === 'undefined' ? '"head"' : typeof options.insert === 'string' ? JSON.stringify(options.insert) : options.insert.toString();
  const injectType = options.injectType || 'styleTag';
  const esModule = typeof options.esModule !== 'undefined' ? options.esModule : true;
  const namedExport = esModule && options.modules && options.modules.namedExport;
  const runtimeOptions = {
    injectType: options.injectType,
    attributes: options.attributes,
    insert: options.insert,
    base: options.base
  };
switch(injectType){
    case 'linkTag':
        {
            // ...
        }
    case 'lazyStyleTag':
    case 'lazySingletonStyleTag':
        {
            // ...
        }
    case 'styleTag':
    case 'singletonStyleTag':
    default:
        {
            // ...
        }
 }
}

switch(){
....
case:...
..
  default:
      {
        const isSingleton = injectType === 'singletonStyleTag';
        const hmrCode = this.hot ? `
if (module.hot) {
  if (!content.locals || module.hot.invalidate) {
    var isEqualLocals = ${_isEqualLocals.default.toString()};
    var oldLocals = ${namedExport ? 'locals' : 'content.locals'};

    module.hot.accept(
      ${_loaderUtils.default.stringifyRequest(this, `!!${request}`)},
      function () {
        ${esModule ? `if (!isEqualLocals(oldLocals, ${namedExport ? 'locals' : 'content.locals'}, ${namedExport})) {
                module.hot.invalidate();

                return;
              }

              oldLocals = ${namedExport ? 'locals' : 'content.locals'};

              update(content);` : `content = require(${_loaderUtils.default.stringifyRequest(this, `!!${request}`)});

              content = content.__esModule ? content.default : content;

              if (typeof content === 'string') {
                content = [[module.id, content, '']];
              }

              if (!isEqualLocals(oldLocals, content.locals)) {
                module.hot.invalidate();

                return;
              }

              oldLocals = content.locals;

              update(content);`}
      }
    )
  }

  module.hot.dispose(function() {
    update();
  });
}` : '';
        return `${esModule ? `import api from ${_loaderUtils.default.stringifyRequest(this, `!${_path.default.join(__dirname, 'runtime/injectStylesIntoStyleTag.js')}`)};
            import content${namedExport ? ', * as locals' : ''} from ${_loaderUtils.default.stringifyRequest(this, `!!${request}`)};` : `var api = require(${_loaderUtils.default.stringifyRequest(this, `!${_path.default.join(__dirname, 'runtime/injectStylesIntoStyleTag.js')}`)});
            var content = require(${_loaderUtils.default.stringifyRequest(this, `!!${request}`)});

            content = content.__esModule ? content.default : content;`}

var options = ${JSON.stringify(runtimeOptions)};

options.insert = ${insert};
options.singleton = ${isSingleton};

var update = api(content, options);

${hmrCode}

${esModule ? namedExport ? `export * from ${_loaderUtils.default.stringifyRequest(this, `!!${request}`)};` : 'export default content.locals || {};' : 'module.exports = content.locals || {};'}`;
      }


}

简化 大概是:pitch 方法 返回一个字符串

const isSingleton = injectType === 'singletonStyleTag';
const hmrCode = this.hot ? `
        // ...
    ` : '';
return `
    // _loaderUtils.default.stringifyRequest这里就不叙述了,主要作用是将绝对路径转换为相对路径
    var content = require(${_loaderUtils.default.stringifyRequest(this, `!!${request}`)});
    if (typeof content === 'string') {
        content = [[module.id, content, '']];
    }
    var options = ${JSON.stringify(options)}
    options.insert = ${insert};
    options.singleton = ${isSingleton};
    
    var update = require(${_loaderUtils.default.stringifyRequest(this, `!${_path.default.join(__dirname, 'runtime/injectStylesIntoStyleTag.js')}`)})(content, options);
    if (content.locals) {
        module.exports = content.locals;
    }
    ${hmrCode}
`;`
  • 首先调用require方法获取css文件的内容,将其赋值给content
  • 如果content是字符串,则将content赋值为数组,即:[[module.id], content, ''],接着我们覆盖了options的insert、singleton属性
  • 又使用require方法引用了runtime/injectStyleIntoStyleTag.js,它返回一个函数,我们将content和options传递给该函数,并立即执行它:
module.exports = function (list, options) {
    options = options || {};
    options.attributes = typeof options.attributes === 'object' ? options.attributes : {}; // Force single-tag solution on IE6-9, which has a hard limit on the # of