从 webpack 内联 loader 学习“一致性设计”

174 阅读5分钟

一、前文回顾

上文主要讲述了 resolve 流程中解析 request 的工作,这部分工作主要包含两方面:

  1. 处理 match-resource 语法(!=!),这种语法是 webpack v4 之后支持的一种新语法,用于在编译中暴力修改原有 request 应该使用的 loader。值得注意的是,这种语法在业务中不要用,这个东西有点危险!这一步主要是获取 matchResourceData,并且将 matchResource 从原有的 request 上移除以便进行后续的解析工作。
  2. 解析 requestWithoutMatchResource,分析 request 开头的字符是不是 !/-!/!! 来决定是不是禁用调用配置中的loader,最后将 loader 和资源路径分离;

经过这两个步骤后我们得到 request 上带有的 内联 loader(elements) 和 模块资源路径(unresolvedResouce);

今天这篇小作文我们将接着讲处理 request 之后,解析内联 loader 的事儿!

二、解析行内 loader 路径

前面我们已经从 request 上得到了指定的行内 loader,下面就要调用 resolver 的能力解析这些 loader 的具体路径,下面是这部分简化后的代码框架:


class NormalModuleFactory extends ModuleFactory {
    constructor () {
        this.hooks.resolve.tapAsync(  
            {  
                name: "NormalModuleFactory",  
                stage: 100  
            },  
            (data, callback) => {
                // ...
                
                // 1. 
                let loaders;
                
                // 2.
                const continueCallback = needCalls(2, err => {
                    // ...
                });

                // 3.
                this.resolveRequestArray(
                    contextInfo,
                    contextScheme ? this.context : context,
                    elements,
                    loaderResolver,
                    resolveContext,
                    (err, result) => {
                        // 4.
                        loaders = result;
                        
                        // 5.
                        continueCallback();
                    }
                );
                
            }
        );
    
    }
}

整体上我把这部分分为了 5 个步骤:

  1. 声明 loaders 变量,不设初始值;
  2. 声明 continueCallback 函数,该函数是 needCall 的返回值,这个回调函数会最后被调用;
  3. 调用 this.resolveRequestArray() 方法,传入包含内联 loader 的数组 elements 以及用于解析 loader 用的 resolver —— loaderResolver,另外还传入了受理解析结果的回调函数;
  4. 在 this.resolveRequestArray() 方法的回调中,将解析所得到 result 赋值给前面声明的 loaders 变量;
  5. 调用 2. 声明的 continueCallback 函数继续后续流程;

2.1 needCall

这个方法是个机制的方法,webpack 内部有很多类似抖机灵的解决方案。我们来看看这个函数:

const needCalls = (times, callback) => {
    return err => {
        if (--times === 0) {
            return callback(err);
        }
        if (err && times > 0) {
            times = NaN;
            return callback(err);
        }
    };
};

函数内部十分简洁,接收一个 times 的计数和 times 调用结束后需要调用的 callback;最终返回一个回调函数。

返回值函数内部记录调用次数,每次调用执行 --times 的操作,为0是调用 callback,这么简单的逻辑相信代码初学者都可以组织好,但是为啥要讲它呢?

先思考一个问题:为啥 webpack 要这么做呢,为什么不用 Promise ...,为啥不用 asyncLib 啥的调度....啥的。。。?

优点主要有两点:

  1. 这是因为 webpack 要性能优化,一个简单的计数器能搞定的事儿就不用更消耗内存的其他实现方式;
  2. 另外这里是一个典型的闭包场景,随着 callback 被调用,由 needCall 所创建的所有有的执行栈被清空,内存也可以得到及时的释放。

2.2 nmf.resolveRequestArray

该方法由 NormalModuleFactory 类型在原型上实现,用于在 resolve 阶段 并发 解析 loader 和资源模块的路径,简化后的代码如下:

class NormalModuleFactory extends ModuleFactory {
    resolveRequestArray(
        contextInfo,
        context,
        array,
        resolver,
        resolveContext,
        callback
    ) {
        // 1.
        asyncLib.map(
            array,
            (item, callback) => {
                
                // 2.
                resolver.resolve(
                    contextInfo,
                    context,
                    item.loader,
                    resolveContext,
                    (err, result) => {
                        // 3.
                        const parsedResult = this._parseResourceWithoutFragment(result);
                        
                        // 4.
                        const resolved = {
                                loader: parsedResult.path,
                                options:
                                    item.options === undefined
                                            ? parsedResult.query
                                                ? parsedResult.query.slice(1)
                                                : undefined
                                            : item.options,
                                ident: item.options === undefined ? undefined : item.ident
                        };
                        // 5.
                        return callback(null, resolved);
                    }
            );
        },
        callback // 6.
    );
    }
}

2.2.1 参数

  1. contextInfo:构建相关上下文信息,在 factory.create() 调用时传递,经层层传递到达这里;
  2. context:当前构建的项目的上下文目录;
  3. array:带解析的路径数组,可以是 loader 的,也可以是自愿模块的;
  4. resolver:用于解析上面 array 的 resolver,根据解析的物料不同而不同;
  5. resolverContext:resolver 执行 resolve 时需要的上下文对象,这个对象会伴随 resolver 流水线工作,贯穿整个 resolve 的过程。这个对象中包含三个我们熟悉的对象 file/missing/context dependencies;从这里可以看出后面要用的 file/missing dependencies 是在 resolve 阶段填充的;
  6. callback:这个 callback 是受理解析结果的回调函数;

下图是个方法在工作时接收到的实参:

image.png

当然,在我们的例子中,我们并没有内联的 loader,因此 array 将会是个空数组。。。。(我好想抛开事实不谈。。。。。。🧜‍♀️🧚‍♀️)

2.2.2 逻辑

逻辑也是相当的简洁,一共分为 6 个步骤:

  1. 调用 asyncLib.map 异步并发迭代 array,这个 asyncLib.map(arr, iteratorFun, cb) 接收三个参数,异步的迭代 arr 中的每一项,为它调用 iteratorFun,当所有都迭代结束后调用 cb;
  2. resolver.resolve() 调用作为迭代函数执行,接收每个待解析的项目进行解析;
  3. 在得到 resolver.resolve 的结果后调用 _parseResourceWithoutFragment 方法处理一下成标准的 { path, query, resource } 对象;
  4. 组织 resolved 对象;
  5. 调用控制 asyncLib.map 的 callback,注意这个 callback 不是 resolveRequestArray 的 callback;
  6. 在 asyncLib.map 完成对 array 中每一项的迭代后就拿到了所有的结果,此时调用 callback 完成解析工作;

2.3 内联 loader 的优先解析特性

这一点并不是 webpack 显式的告知我们的,而是随着阅读的深入(严格来讲是写作本文时)才发现的特性。内联 loader 无论是在 rquest 的处理还是在 loader 的路径解析层面都有着非常高的优先级。这个设定和 webpack 设计内联 loader 时给予的更高优先级相辉映。

但是,我想说但是,到这里也只是第一层,我想说的是 一致性。这种设计和实现的一致性很值得提倡,这为二次开发的你我,或者阅读这个部分代码的读者来说都是十分容易理解的。

当然,这里并不是最终优先级的组织时机,但是从流程上讲这样处理是非常赞的!

三、总结

本文讲述了 webpack 在 resolve 阶段解析 内联 loader 的细节工作,主要包含了一些内容:

  1. needCall 这个小方法的意义,以及它背后的设计理念;
  2. NormalModuleFactory.prototype.resolveRequestArray 方法的实现:并发调用 resolver.reslve 解析,得到解析结果后调用 _parseResourceWithoutFragment 方法后处理解析结果后返回;
  3. 最后我们分享了 webpack 内部对于 内联loader 从设计到实现的一致性,这一点非常值得推荐;

下面我们开始进入更精彩的环境——常规 loader 的解析以及 loader 顺序的确定还有资源模块的解析!