前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >6. 模块构建之loader执行:loader-runner@2.4.0源码分析

6. 模块构建之loader执行:loader-runner@2.4.0源码分析

作者头像
tinyant
发布2022-11-16 17:31:31
4180
发布2022-11-16 17:31:31
举报

通过一个demo带你深入进入webpack@4.46.0源码的世界,分析构建原理,专栏地址,共有十篇。


第四节创建完模式实例后,进入模块的构建工作,本节重点说loaders的执行。


看下a.js在执行runLoaders是控制台的日志:

看到先是执行所有的module.exports.pitch指向的函数函数,然后再执行module.exports指向的函数。实际上webpack将loader的执行分为两个阶段:pitching和normal。

再关注下在某个阶段不同类型(pre、post、normal、inline四种类型)的loader的执行顺序:

  • pitching阶段:post -> inline -> normal -> pre
  • normal阶段:pre -> normal -> inline -> post

下面分析下具体的执行过程:NormalModuleFactory.resolve 解析获取loader后

代码语言:javascript
复制
// Compilation.js
buildModule(module, optional, origin, dependencies, thisCallback){
    module.build(..., error => {/*...*/})
}

// NormalModule.js
const { getContext, runLoaders } = require("loader-runner"); // 调用的loader-runner包

build(options, compilation, resolver, fs, callback) {
    return this.doBuild(..., fs, err => {/*...*/})
}

doBuild(options, compilation, resolver, fs, callback) {
    // 创建loader的执行上下文
    const loaderContext = this.createLoaderContext(/*...*/);

    runLoaders({
        resource: this.resource,
        loaders: this.loaders, // 传入loaders ,NomalModleFactory new NormalModule()传入的
        context: loaderContext,
        readResource: fs.readFile.bind(fs)
    }, (err, result) => { /*...*/ })
}

并且经过xxxResolver.resolve拿到了loader和resouce的本地路径

LoadRunner

loader-runner@2.4.0目录结构如下:

主要是LoaderRunner.js和loadLoader.js,前者执行loader,后者通过指定本地路径加载js代码到内存。

runLoaders(loader-runner包)

LoaderRunner.js

这个文件主要包含三个部分内容

1. 给loaderContext对象添加部分属性和方法

代码语言:javascript
复制
// read options
var resource = options.resource || ""; // 资源信息,将上面截图
var loaders = options.loaders || [];

// 来自NormalModule创建的context,所以每个module的执行loader的loaderContext是独立的
// 但是该module的所有loaders共享这个上线文
var loaderContext = options.context || {}; 
var readResource = options.readResource || readFile; // 文件读取方法

// 拆分resource为path和query单独存储
var splittedResource = resource && splitQuery(resource);
var resourcePath = splittedResource ? splittedResource[0] : undefined;
var resourceQuery = splittedResource ? splittedResource[1] : undefined;
var contextDirectory = resourcePath ? dirname(resourcePath) : null; // 资源所在目录

// execution state
var requestCacheable = true; // 是否要缓存当前模块
var fileDependencies = []; // 依赖的文件
var contextDependencies = []; // 依赖的目录

loaders = loaders.map(createLoaderObject);

loaderContext.context = contextDirectory;
loaderContext.loaderIndex = 0; // 用于记录当前执行的loader的索引,查找当前执行的loader
loaderContext.loaders = loaders; // 保存这次所有的loaders
loaderContext.resourcePath = resourcePath; 
loaderContext.resourceQuery = resourceQuery;
loaderContext.async = null; // 异步loader标识
loaderContext.callback = null; // 异步loader回调
loaderContext.cacheable = function cacheable(flag) { // 缓存
    if(flag === false) {
        requestCacheable = false;
    }
};

当前demo中以解析a.js为例,原始的资源路径是:/Users/.../src/simple/a.js?c=d'

代码语言:javascript
复制
resourcePath: '/Users/.../src/simple/a.js'
resourceQuery: '?c=d'
  • asynccallback使用为支持异步loader,后面分析loader执行时会说到。
  • fileDependenciescontextDependencies:表示依赖的文件和文件夹,可以通过下面的方法addDependency和addContextDependency进行添加,getDependencies和getContextDependencies获取。关于fileDependencies和contextDependencies的具体作用后面会单独说。
代码语言:javascript
复制
// -------- 文件依赖、目录依赖 添加、获取、清理----------
// NormalModule保存了fileDependencies、contextDependencies,猜测变更时使用?
// 当前文件的processResource方法有用到
loaderContext.dependency = loaderContext.addDependency = ...
loaderContext.addContextDependency = //...
loaderContext.getDependencies = //...;
loaderContext.getContextDependencies = //...
loaderContext.clearDependencies = //...
  • cacheable(boolean)用来设置是否需要缓存当前模块的构建结果。后面会细说
  • 通过Object.defineProperty设置get/set,动态计算属性结果。
代码语言:javascript
复制
// 下面的xxx: request、remainingRequest、currentRequest、previousRequest、query、data
Object.defineProperty(loaderContext, `${xxx}`, ...);

// finish loader context
// 冻结,不让扩展loaderContext
if(Object.preventExtensions) {
    Object.preventExtensions(loaderContext);
}

requestremainingRequestcurrentRequestpreviousRequestloadersresource通过!拼接的结果,这几个属性的主要区别在于包含的loaders不一样;request属性包含所有的loaders,remainingRequest包含剩余未执行的loaders,currentRequest包含当前正在执行及后面未执行的loaders,previousRequest包含已经执行过的loaders

loaderContext.request在当前demo中的结果

代码语言:javascript
复制
// loaderContext.request
/Users/.../src/simple/custom-loaders/custom-post-loader.js!/Users/.../src/simple/custom-loaders/custom-inline-loader.js??share-opts!/Users/.../src/simple/custom-loaders/custom-normal-loader.js??share-opts!/Users/.../src/simple/custom-loaders/custom-pre-loader.js!/Users/.../src/simple/a.js?c=d

loaderContext.query:当前正在执行的loader的options或者query;假设当前这在执行custom-inline-loader,loaderContext.query的结果是

代码语言:javascript
复制
{ a: 'b' } // options

2. 给每一个loader创建一个运行时对象,用来存储该loader的执行状态

代码语言:javascript
复制
loaders = loaders.map(createLoaderObject); 
// createLoaderObject返回一个对象包含以下属性用于记录loader执行是的一些状态
{
   path: null,  // loader路径,通过Object.defineProperty定义request属性,然后 obj.request = loader;
   query: null, // 如果loader设置了options则返回options,否则返回laoder的query即‘?’及其后面的字符串
   options: null, // 定义loader可以是字符串,也可以是对象,对象的化可以提供该选项
   ident: null,   // 标识符,可以通过该标志查找loader
   normal: null,  // normal类型的loader - 函数
   pitch: null,   // pitch类型的loader - 函数
   raw: null,     // 是否返回二进制资源
   data: null,    // loader间共享的数据
   pitchExecuted: false, // 标识pitch类型的loader是否执行过,一个loader可以提供normal,pitch
   normalExecuted: false // 标识normal类型的loader是否执行过 
};

上面注释很清楚了,不在赘述

3. pitching阶段执行所有loaders

代码语言:javascript
复制
var processOptions = {
    resourceBuffer: null,
    readResource: readResource // 静态资源读取的方法
};

// 开始遍历loader并执行,执行每个loader文件中的module.exports.pitch函数
iteratePitchingLoaders(processOptions, loaderContext, function(err, result) {
    //...
    callback(null, { //返回给 NormalModule.doBuild的回调
        result: result, // result结构: [source, sourceMap, {webpackAST}] 
        resourceBuffer: processOptions.resourceBuffer,
        cacheable: requestCacheable,
        fileDependencies: fileDependencies,
        contextDependencies: contextDependencies
    });
});

在前面章节有说到loader的有两个执行阶段:pitching 和 normal;首先会进入pitching阶段,即这里的iteratePitchingLoaders,用于遍历所有的loader上pitch函数。

另外这里的callback参数是最终交给NormalModule.runLoaders的回调的。

piching阶段:iteratePitchingLoaders

iteratePitchingLoaders

代码语言:javascript
复制
// 这里面的执行逻辑都是 pitch函数 的
function iteratePitchingLoaders(options, loaderContext, callback) {
    // 如果pichingLoader已经执行完,则走 processResource
    // processResource 读取资源,然后进入normalLoader的遍历
    if (loaderContext.loaderIndex >= loaderContext.loaders.length)
        return processResource(options, loaderContext, callback);

    // 获取当前loader的属性
    var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];

    // iterate
    // 当前loader的pitchingLoader是否执行过了
    // 已经执行过,索引加一,进入下一个pitchingLoader的执行
    if (currentLoaderObject.pitchExecuted) {
        loaderContext.loaderIndex++;
        return iteratePitchingLoaders(options, loaderContext, callback);
    }

    // load loader module
    // 如果当前pitchingLoader没有执行过,则执行
    // loaderLoader类似于 reuqire或者import,根据loader.path加载loader模块
    // 然后设置currentLoaderObject的pitch、normal,raw等属性
    loadLoader(currentLoaderObject, function (err) {
        //...
        var fn = currentLoaderObject.pitch; // 获取pitchingLoader
        currentLoaderObject.pitchExecuted = true; // 设置当前pitchingLoader执行过的标志
        // 如果不是函数,则进入下一个pichingLoader的执行
        if (!fn) return iteratePitchingLoaders(options, loaderContext, callback);

        // 开始执行loader(支持同步和异步方式调用)
        runSyncOrAsync(fn,
            // 参数同时传递了 remainingRequest、previousRequest,loader.data引用
            loaderContext, [loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}],
            function (err) {
                //...

                // **注意**:这里的参数会被传递给normalLoader的入参
                // 应该是字符串或者数组,字符串或者数组的第一个元素当做normalLoader的入参:source
                // 即资源的内容
                // 解释了没有走 processResource 的source 该如何获取的问题
                // 如果所有的pitchLoader都没有返回参数则调用processResource读取,如果任意一个返回
                // 则从返回值获取source,传递给normalLoader
                var args = Array.prototype.slice.call(arguments, 1);

                // 如果你的pitchingLoader返回了结果,则会忽略后面所有的loader(不论是normal还是pitch)
                // 直接从从上一个loader.nomal开始执行
                if (args.length > 0) {
                    loaderContext.loaderIndex--; // 从上一个loader开始,即当前loader.normal不会被执行了
                    // 注意:有传递args给normalLoader
                    iterateNormalLoaders(options, loaderContext, args, callback);
                } else { // 否则继续执行下一个pitchingLoader
                    iteratePitchingLoaders(options, loaderContext, callback);
                }
            }
        );
    });
}
  • loader的执行顺序是通过loaderIndex控制的,默认是0。pitching阶段,loaderIndex一直自增直到执行完所有的loader即loaderIndex>=loaders.length
    • 当满足该条件后则开始资源的读取即执行processResource,该方法进入normal loader的执行。
    • 如果没有,判断当前loader是否执行过(pitchExecuted),
      • 如果已经执行过,则loaderIndex++后递归调用iteratePitchingLoaders,进入下一个loader的执行。
      • 如果没有执行过,则调用loadLoader本地路径中加载loader,这个加载的过程可能是异步的,加载成功后在回调中开始执行该loader.pitch,设置该loader.pitchExecuted=true标识该loader的pitch被执行过
        • 如果没有loader.pitch则递归调用iteratePitchingLoaders执行下一个loader
        • 如果有loader.pitch则调用runSyncOrAsync来执行loader.pitch函数(runSyncOrAsync可以让为同步函数动态添加异步能力,同步和异步由当前函数的执行过程动态决定),当执行完pitch函数后进入回调根据当前pitch的返回结果判断进入normal阶段还是继续pitching阶段的执行,如果返回了参数,则进入normal阶段执行loader即执行iterateNormalLoaders,注意这里会将返回值传递给iterateNormalLoaders

简单说下loadLoader,加载并执行对应的本地js资源,读取默认值currentLoaderObject.normal属性,读取pitch属性给currentLoaderObject.pitch,并记录raw的值

思考:只有执行完所有的loader.pitch才会进入资源的读取,那如果没有执行完怎么办❓

下面看下资源读取的方法processResource

processResource

代码语言:javascript
复制
function processResource(options, loaderContext, callback) {
    // set loader index to last loader
    // 从最后一个loader掉头反向执行所有的normalLoader
    loaderContext.loaderIndex = loaderContext.loaders.length - 1;

    var resourcePath = loaderContext.resourcePath;
    if(resourcePath) {
        // 添加文件依赖
        loaderContext.addDependency(resourcePath);
        // 根据资源地址读取文件内容
        options.readResource(resourcePath, function(err, buffer) {
            //...
            options.resourceBuffer = buffer;
            // 进入normalLoader的执行,将资源内容作为参数传递进去
            iterateNormalLoaders(options, loaderContext, [buffer], callback);
        });
    } else {
        // 如果没有文件路径,直接进入loader.normal的执行
        iterateNormalLoaders(options, loaderContext, [null], callback);
    }
}

逻辑很简单,获取resourcePath如这里的/Users/.../src/simple/a.js,如果没有这个地址,直接进入normal阶段的执行。如果有会先调用options.readSource(由NormalModuel调用runLoaders时传入)读取该文件,并且会将文件添加到文件依赖fileDependencies中,然后会进入到normal阶段的执行,注意这里将读取的文件内容传给了iterateNormalLoaders

normal阶段:iterateNormalLoaders

代码语言:javascript
复制
function iterateNormalLoaders(options, loaderContext, args, callback) {
    if (loaderContext.loaderIndex < 0) // 显然      
        // callback到runLoader方法调用iteratePitchingLoaders的回调
        return callback(null, args);

    var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];

    // iterate
    // 如果当前执行过,则执行下一个normalLoader - 递归
    if (currentLoaderObject.normalExecuted) {
        loaderContext.loaderIndex--;
        return iterateNormalLoaders(options, loaderContext, args, callback);
    }

    var fn = currentLoaderObject.normal;
    currentLoaderObject.normalExecuted = true;
    if (!fn) { // 如果没有提供normalLoader,则进入下一个normalLoader的执行    
        return iterateNormalLoaders(options, loaderContext, args, callback);
    }

    // 将上一个loader返回的source,根据raw值来决定是:string | buffer
    // 当前normalLoaderh会通过暴露一个raw属性来决定传入的source是什么类型
    // 通过图片等静态资源要求是buffer
    convertArgs(args, currentLoaderObject.raw);

    // 开始执行normalLoader
    runSyncOrAsync(fn, loaderContext, args, function (err) {
        if (err) return callback(err);

        // 将当前函数的参数的err移除,取后面所有参数传递给下一个normalLoader
        var args = Array.prototype.slice.call(arguments, 1);
        iterateNormalLoaders(options, loaderContext, args, callback);
    });
}

显然,这里的流程和iteratePitchingLoaders基本上是对应的,判断normal loaders是否执行完;判断当前normal loader是否执行过;区别如下:

  1. 多了一个convertArgs,会根据raw的值转换原始资源的内容,raw=true转为二进制,否则转为字符串
  2. 这里不需要loadLoader,因此该loader已经加载过了,只是之前是获取pitch属性,现在是获取默normal属性。,

runSyncOrAsync

runSyncOrAsync可以让为同步函数动态添加异步能力,同步和异步由当前函数的执行过程动态决定

如果loader调用this.async()则会动态的支持接收当前函数的异步结果;如果不调用,默认是同步的;

代码语言:javascript
复制
function runSyncOrAsync(fn, context, args, callback) {
    var isSync = true; // 当前loader是同步的还是异步的
    var isDone = false; // 当前loader是否执行完成
    var isError = false; // internal error
    var reportedError = false;
    context.async = function async() {
        if (isDone) //... 调用该函数时,如果loader已经执行完成则报错
        isSync = false; // 置为异步loader
        return innerCallback; // 返回异步loader需要的回调,可以通过该回调将异步结果返回给下一个loader
    };
    var innerCallback = context.callback = function () { // 异步回调
        if (isDone) //... 如果已经完成,则报错           
        isDone = true; // 异步loader调用函数传递结果,说明该loader执行完成
        isSync = false; // 置为异步的
        try {
            callback.apply(null, arguments); // 调用者的回调
        } catch (e) //...
    };
    try {
        // 注意:同步执行loader
        var result = (function LOADER_EXECUTION() { return fn.apply(context, args); }());
        // 在上面同步执行的过程中,如果没有发生调用 this.async()
        // 则说明这是一个同步loader
        if (isSync) {
            isDone = true; // 同步loader执行完成
            if (result === undefined) return callback();
            if (result &amp;&amp; typeof result === "object" &amp;&amp; typeof result.then === "function") {
                // 兼容loader返回Promise实现异步,可以提供一个案例
                return result.then(function (r) { callback(null, r); }, callback);
            }
            return callback(null, result);
        }
    } catch (e) // ...
}

默认是同步的,即在没有调用this.async情况下,loader执行完成后会直接调用runSyncOrAsync入参中的callback,结束当前loader的执行。

看到上面方法中给context添加了两个方法asynccallback,如果执行的loader函数中同步调用了这async(),则会设置isSync = false;那么在执行完LOADER_EXECUTION()后,由于isSync是false,所以不会立即调用runSyncOrAsync入参中的callback结束来结束当前loader的执行;当前loader的完成状态由async返回的innerCallback决定,需要调用者主动结束。

另外LOADER_EXECUTION()同步执行过程中可以直接调用this.callbackinnerCallback来结束当前loader的执行。

总结

loader的执行过程如下两种情况:

思考:看到loader执行完成后会返回cacheable、fileDependencies、contextDependencies三个参数的作用❓

代码语言:javascript
复制
// NormalModule.js 
// doBuild()
runLoaders({ /*...*/ }, (err, result) => {
      if (result) {
         this.buildInfo.cacheable = result.cacheable;
         this.buildInfo.fileDependencies = new Set(result.fileDependencies);
         this.buildInfo.contextDependencies = new Set(
            result.contextDependencies
         );
      }
      //...
})
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-11-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • LoadRunner
    • runLoaders(loader-runner包)
      • 1. 给loaderContext对象添加部分属性和方法
      • 2. 给每一个loader创建一个运行时对象,用来存储该loader的执行状态
      • 3. pitching阶段执行所有loaders
    • piching阶段:iteratePitchingLoaders
      • iteratePitchingLoaders
      • processResource
    • normal阶段:iterateNormalLoaders
      • runSyncOrAsync
      • 总结
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档