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 <style>
    // tags it will allow on a page
    if (!options.singleton && typeof options.singleton !== 'boolean') {
        options.singleton = isOldIE();
    }
    
    var styles = listToStyles(list, options);
    addStylesToDom(styles, options);
    return function update(newList) {
        // ...
    };
};

主要的 是 modulesToDom 和addStyle


function addStyle(obj, options) {
  var style;
  var update;
  var remove;

  if (options.singleton) {
    var styleIndex = singletonCounter++;
    style = singleton || (singleton = insertStyleElement(options));
    update = applyToSingletonTag.bind(null, style, styleIndex, false);
    remove = applyToSingletonTag.bind(null, style, styleIndex, true);
  } else {
    style = insertStyleElement(options);
    update = applyToTag.bind(null, style, options);

    remove = function remove() {
      removeStyleElement(style);
    };
  }

  update(obj);
  return function updateStyle(newObj) {
    if (newObj) {
      if (newObj.css === obj.css && newObj.media === obj.media && newObj.sourceMap === obj.sourceMap) {
        return;
      }

      update(obj = newObj);
    } else {
      remove();
    }
  };
}
function insertStyleElement(options) {
  var style = document.createElement('style');
  var attributes = options.attributes || {};

  if (typeof attributes.nonce === 'undefined') {
    var nonce = typeof __webpack_nonce__ !== 'undefined' ? __webpack_nonce__ : null;

    if (nonce) {
      attributes.nonce = nonce;
    }
  }

  Object.keys(attributes).forEach(function (key) {
    style.setAttribute(key, attributes[key]);
  });

  if (typeof options.insert === 'function') {
    options.insert(style);
  } else {
    var target = getTarget(options.insert || 'head');

    if (!target) {
      throw new Error("Couldn't find a style target. This probably means that the value for the 'insert' parameter is invalid.");
    }

    target.appendChild(style);
  }

  return style;
}

style-loader会返回一个字符串,而在浏览器中调用时,会将创建一个style标签,将其加入head中,并将css的内容放入style中,同时每次该文件更新也会相应的更新Style结构,如果该css文件内容被删除,则style的内容也会被相应的删除,总体来说,style-loader做了一件非常简单的事:在 DOM 里插入一个 <style> 标签,并且将 CSS 写入这个标签内

而style-loader的pitch方法里面调用了require('!!.../x.css'),这就会把require的css文件当作新的入口文件,重新链式调用剩余的loader函数进行处理。(值得注意的是'!!'是一个标志,表示不会再重复递归调用style-loader,而只会调用css-loader处理了)

file-loader 和url-loader

file-loader和url-loader都可以辅助webpack将图片打包为js文件

  • file-loader会将图片打包成一个单独的js文件
  • url-loader会将图片打包成一个base64格式的图片字符串,并将这个图片字符串放到bundle.js里,可以减少http请求,但是如果图片大小非常大那么打包后的bundle.js会非常大加载的时候会非常慢,甚至会有空白,所以可以做一个限制,limit:2048 limit的单位是字节,在小于这个单位的时候就使用者个loader,否则就不使用这loader
  • 由于url-loader包含了file-loader所以,file-loader内的option在url-loader中均能使用
  • url-loader 会将引入的文件进行编码,生成 DataURL,相当于把文件翻译成了一串字符串,再把这个字符串打包到 JavaScript

file-loader:

function loader(content) {
  const options = (0, _loaderUtils.getOptions)(this);
  (0, _schemaUtils.validate)(_options.default, options, {
    name: 'File Loader',
    baseDataPath: 'options'
  });
  const context = options.context || this.rootContext;
  const name = options.name || '[contenthash].[ext]';
  const url = (0, _loaderUtils.interpolateName)(this, name, {
    context,
    content,
    regExp: options.regExp
  });
  let outputPath = url;
....
}

处理outpath:

 if (options.outputPath) {
    if (typeof options.outputPath === 'function') {
      outputPath = options.outputPath(url, this.resourcePath, context);
    } else {
      outputPath = _path.default.posix.join(options.outputPath, url);
    }
  }
image.png

处理publicpath:·

let publicPath = `__webpack_public_path__ + ${JSON.stringify(outputPath)}`;

  if (options.publicPath) {
    if (typeof options.publicPath === 'function') {
      publicPath = options.publicPath(url, this.resourcePath, context);
    } else {
      publicPath = `${options.publicPath.endsWith('/') ? options.publicPath : `${options.publicPath}/`}${url}`;
    }

    publicPath = JSON.stringify(publicPath);
  }

  if (options.postTransformPublicPath) {
    publicPath = options.postTransformPublicPath(publicPath);
  }

emitFile 是由 loader 上下文提供的方法,用于输出一个文件

assetInfo.sourceFilename = (0, _utils.normalizePath)(_path.default.relative(this.rootContext, this.resourcePath));
    // 把文件输出到指定的outputPath路径
this.emitFile(outputPath, content, null, assetInfo);
image.png
image.png

最终输出路径:

return module.exports = ${publicPath};;

            emitFile: (name, content, sourceMap, assetInfo) => {
                if (!this.buildInfo.assets) {
                    this.buildInfo.assets = Object.create(null);
                    this.buildInfo.assetsInfo = new Map();
                }
                this.buildInfo.assets[name] = this.createSourceForAsset(
                    name,
                    content,
                    sourceMap
                );
                this.buildInfo.assetsInfo.set(name, assetInfo);
            },
            rootContext: options.context,  // 项目的根路径
            webpack: true,
            sourceMap: !!this.useSourceMap,
            mode: options.mode || "production",
            _module: this,
            _compilation: compilation,
            _compiler: compilation.compiler,
            fs: fs
        };

四、修改代码

根据上面 loader 的介绍分析,可以看出很多loader 都用到了loader-utils,
loader-utils是一个webpack工具类,通过一些方法配合loader处理文件

  • getOptions(检索被调用loader的配置选项)
  • stringifyRequest(将一个请求转化为非绝对路径的可被require或import的字符串;)
  • urlToRequest(将url转换成适合webpack环境的模块请求)
  • getHashDigest(通过限制字符长度获取文件部分哈希值)
  • isUrlRequest(判断是否是路径)
  • getCurrentRequest(获取当前请求)

4.1、实现一个loader 替换某一个类名

  • webpack.config.js 引入自己的loader
module:{
rule:[
...
    {
        test: /\.js?$/,
        exclude: /node_modules/,
        use: [
          'babel-loader',
          {
            // 因为该loader还没有上传的npm,所以要指定该loader的路径,才能使用
            loader: 'test-loader',
            options: {
              text: !isProd?'wechat-test-wrap':'wechat-wrap'
            },
          }
        ]
      },
]
},
 resolveLoader: {
    modules: ['node_modules', './src/loader']
},

  • test-loader
// src/loader/test-loader.js

const loaderUtils = require('loader-utils');
module.exports = function(source) {
  console.log('dasdasd',source,'test')    // 'import less from './index.less';\n\nconsole.log('index.js file load');'
  const options = loaderUtils.getOptions(this); // 此时 this 就是 context
  console.log('options',options,options.text)
return source.replace(/wrap/g, options.text);
};

4.2 异步loader (简单实现一个less-loader)

  • webpack.config.js
     {
        test: /\.(le|c)ss$/,
        use: [
          { loader: 'style-loader' },
          {
            loader: 'css-loader',
            options: {
              // modules: true   启用或者禁用css 模块
            }
          },
          {
             loader:"test-less-loader",
             options:{
               sourceMap:false
             }
          },    
        ]
      },
  • test-less-loader
const less = require('less');
// webpack 提供的模块
const loaderUtils = require('loader-utils');

// source 为源代码
module.exports = function (source) {
  console.log('--------- begin handle less file:');
  console.log(source);
  // 获取 webpack.config.js 中配置的 options
  const options = loaderUtils.getOptions(this);
  console.log('--------- options: ');
  console.log(options);

  // loader 的异步回调
  const callback = this.async();
  // less 转换函数
  less.render(source).then(({css, map, imports}) => {
    console.log('less render finished, css: ');
    console.log(css);
    callback(null, css);
  });
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容