webpack只能处理 js 、json 模块,对于css、img 等其他非js模块是无法默认处理的。因此,就需要loader了,它能将非js模块的资源打包成js文件,进行后续处理。

Loader源码

它是在 doBuild方法中执行的,具体可看webpack源码03中的第二部分。
内部主要两部分:

  1. 创建loader上下文对象
  2. 通过loader对源代码进行编译
    1. doBuild(options, compilation, resolver, fs, callback) {
    2. // 创建loader上下文对象
    3. const loaderContext = this.createLoaderContext(
    4. resolver,
    5. options,
    6. compilation,
    7. fs
    8. );
    9. // 通过loader进行编译
    10. runLoaders(
    11. {
    12. resource: this.resource,
    13. loaders: this.loaders,
    14. context: loaderContext,
    15. readResource: fs.readFile.bind(fs)
    16. },
    17. // 此时loader已经加载结束了
    18. // result中有一个result数组,包含了经过loader处理后的源代码
    19. (err, result) => {
    20. ...
    21. }
    22. )
    23. }

01 => loader上下文对象

  1. createLoaderContext(resolver, options, compilation, fs) {
  2. ...
  3. const loaderContext = {
  4. version: 2,
  5. ...
  6. resolve(context, request, callback) {...},
  7. getResolve(options) {...},
  8. emitFile: (name, content, sourceMap, assetInfo) => {...},
  9. rootContext: options.context,
  10. webpack: true,
  11. sourceMap: !!this.useSourceMap,
  12. mode: options.mode || "production",
  13. _module: this,
  14. _compilation: compilation,
  15. _compiler: compilation.compiler,
  16. fs: fs
  17. }
  18. // 调用Tapable的normalModuleLoader钩子
  19. compilation.hooks.normalModuleLoader.call(loaderContext, this);
  20. // 和传入的loader进行合并
  21. if (options.loader) {
  22. Object.assign(loaderContext, options.loader);
  23. }
  24. return loaderContext;
  25. }

02 => 编译loader

内部会调用 iteratePitchingLoaders 函数,当该函数出发回调时,loader流程便结束了

  1. function runLoaders(options, callback) {
  2. // 设置属性
  3. ...
  4. // 内部对loader进行处理
  5. iteratePitchingLoaders(processOptions, loaderContext, function(err, result) {
  6. if(err) {...}
  7. // 最终返回一些属性
  8. // result => loader编译后的源代码
  9. callback(null, {
  10. result: result,
  11. resourceBuffer: processOptions.resourceBuffer,
  12. cacheable: requestCacheable,
  13. fileDependencies: fileDependencies,
  14. contextDependencies: contextDependencies
  15. });
  16. });
  17. }
  18. // 当loader进入到了最后一个时候,会进入 processResource 函数
  19. function iteratePitchingLoaders(options, loaderContext, callback) {
  20. // loader传递的是一个数组
  21. // loaderContext.loaderIndex => 当前loader的长度,初始为0
  22. // 意味着即使只有一个loader,也会重新进入 iteratePitchingLoaders 函数
  23. if(loaderContext.loaderIndex >= loaderContext.loaders.length)
  24. return processResource(options, loaderContext, callback);
  25. // 获取当前项的loader
  26. var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
  27. // currentLoaderObject.pitchExecuted初始为false,后续就将其改为true
  28. if(currentLoaderObject.pitchExecuted) {
  29. loaderContext.loaderIndex++;
  30. // 重新进入
  31. return iteratePitchingLoaders(options, loaderContext, callback);
  32. }
  33. // loadLoader => 加载对应的模块
  34. // var module = require(loader.path)
  35. // 执行第二个参数
  36. loadLoader(currentLoaderObject, function(err) {
  37. if (err) {...}
  38. var fn = currentLoaderObject.pitch;
  39. // 将currentLoaderObject.pitchExecuted设为true
  40. currentLoaderObject.pitchExecuted = true;
  41. // 没有设置pitch时候为true
  42. if(!fn) return iteratePitchingLoaders(options, loaderContext, callback);
  43. // 如果设置了pitch,会执行fn并传入参数
  44. ...
  45. var result = (function LOADER_EXECUTION() {
  46. return fn.apply(
  47. context,
  48. [loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}]
  49. );
  50. }());
  51. ...
  52. }
  53. }
  54. // 加载对应的loader
  55. // 最终进入 iterateNormalLoaders 函数
  56. function processResource() {
  57. // 设置loaderIndex为最后一个下标
  58. loaderContext.loaderIndex = loaderContext.loaders.length - 1;
  59. // 获取资源路径
  60. var resourcePath = loaderContext.resourcePath;
  61. // 不论怎样,都会执行 iterateNormalLoaders 函数
  62. if(resourcePath) {
  63. // 往loader中增加依赖
  64. loaderContext.addDependency(resourcePath);
  65. // 读取资源
  66. // options.readResource => fs.readFile
  67. options.readResource(resourcePath, function(err, buffer) {
  68. if(err) return callback(err);
  69. options.resourceBuffer = buffer;
  70. iterateNormalLoaders(options, loaderContext, [buffer], callback);
  71. });
  72. } else {
  73. iterateNormalLoaders(options, loaderContext, [null], callback);
  74. }
  75. }
  76. function iterateNormalLoaders(options, loaderContext, args, callback) {
  77. // 在 processResource 函数中,loaderContext.loaderIndex被设置成了loaders数组的最后一项
  78. // 后续会递减,因此,loader是从后往前执行的
  79. if(loaderContext.loaderIndex < 0) return callback(null, args);
  80. // 获取当前项loader
  81. var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
  82. // currentLoaderObject.normalExecuted初始为false
  83. // 将loaderContext.loaderIndex-- ,并重新执行自身
  84. if(currentLoaderObject.normalExecuted) {
  85. loaderContext.loaderIndex--;
  86. return iterateNormalLoaders(options, loaderContext, args, callback);
  87. }
  88. var fn = currentLoaderObject.normal;
  89. // 将currentLoaderObject.normalExecuted设置true
  90. currentLoaderObject.normalExecuted = true;
  91. // 将args从Buffer转换成utf-8格式
  92. convertArgs(args, currentLoaderObject.raw);
  93. // 之后,执行loader
  94. ...
  95. // 内部,会通过babel.transform对source进行编译
  96. // 然后会执行callback,重新进入 processResource 函数
  97. // 最后,执行 processResource 函数中的callback,返回loader处理后的结果
  98. loader.call(this, source, inputSourceMap, overrides)
  99. .then(args => callback(null, ...args), err => callback(err));
  100. }

自定义Loader

// webpack.config.js
const path = require('path');
module.exports = {
    ...
    module: {
        rules: [{
            test: /\.js/,
            use: [
                {
                    loader: path.resolve(__dirname, './myLoader.js'),
                    options: {
                        name: 'oh'
                    }
                }   
            ]
        }]
    }
}

// myLoader.js
const loaderUtils = require('loader-utils');
module.exports = function(source) {
    // getOptions可以拿到webpack.config.js中的option
    const options = loaderUtils.getOptions(this);
      // 替换文件中所有 "test" 的字符串为 "oh"
    const result = source.replace('test', options.name);
      // 因为使用的function不是箭头函数,因此this指向是正确的
    this.callback(null, result);
}

Plugin源码

严格来说,plugin并没有源码,只是在一开始进入的时候,调用了apply方法,因此,核心就是绑定Tapable的钩子,之后在编译的过程中适当时候触发即可。
compiler对象身上绑定了webpack的几乎所有属性,因此,插件能做的时候很多

// 在webpack.js入口文件,就挂载了plugins
// 会调用内部的apply方法,并传入compiler
// compiler中有许多 Tapable 的钩子,在自定义插件时,挂载 Tabpable 的钩子,等待适时的触发即可
if (options.plugins && Array.isArray(options.plugins)) {
  for (const plugin of options.plugins) {
    // 如果是函数类型,则将this指向compiler,并传参compiler
    if (typeof plugin === "function") {
      plugin.call(compiler, compiler);
    } else {
      // 调用apply方法并传参compiler
      // 此处也是为什么,写插件的时候需要提供一个apply方法
      plugin.apply(compiler);
    }
  }
}

自定义Plugin

根据传入的compiler对象,传入Tapable钩子即可

// MyPlugin.js
module.exports = class MyPlugin {
    apply(compiler) {
        compiler.hooks.emit.tap('emit', () => {
            console.log('这是我的emit')
        })
    }
}

// webpack.config.js
const MyPlugin = require('./util');  
module.exports = {
    ...
  plugins: [
    new MyPlugin()
  ]
}