Webpack: 当你有了内联loader和资源路径,你想干什么?

0 阅读5分钟

一、前文回顾

上文主要讨论了 webpack 在创建模块的 resolve 阶段解析模块资源路径的过程,主要有以下细节:

  1. 通过 defaultResolver 去解析模块的资源路径;
  2. nmf.resolveResouce 方法用于解析资源路径;
  3. nmf._resolveResourceErrorHints 用于处理解析失败的信息,内部通过控制变量的方式预判三种易犯错误类型,并给出友好提示;

上面有关 webpack 解析路径失败时的处理智慧具有极高的学习价值,到这里我们已经完成了内联 loader、资源路径的解析工作,下文我们进入到 webpack 内部对于常规 loader 的计算及解析过程。

二、continueCallback 的 callback

到这里有点久远了,大致往前面倒一倒,经过前面两次的 resovle:

  1. 一次是 resolve 行内(内联)loader 的路径;
  2. 另一次是 resolve 模块的资源路径。大致代码如下:
this.resolveRequestArray(
    contextInfo,
    contextScheme ? this.context : context,
    elements,
    loaderResolver,
    resolveContext,
    (err, result) => {
        // 解析行内 loader 
        loaders = result;
        continueCallback();
    }
);

const defaultResolve = context => {
    // 解析资源路径
    this.resolveResource(
        contextInfo,
        context,
        unresolvedResource,
        normalResolver,
        resolveContext,
        (err, resolvedResource, resolvedResourceResolveData) => {
       
            continueCallback();
        }
    );
    }
};

2.1 目前的成果

到这里我们来复盘一下看看我们都有什么了:

  1. loaders —— 行内方式声明的loader(内联 loader);
  2. resourceData.resource —— 模块的资源路径(request);

2.2 callback

上述两个过程在拿到结果后都会调用 continueCallback 这个方法;


const continueCallback = needCalls(2, err => {
    // 这个就是 continueCallback 的回调
})

前面我们说过 needCall 方法是个计数触发器,即调用达到指定次数后执行传入的回调函数。比如上面的 continueCallback 就是调用两次后调用回调。

前面我们已经复盘过了,有了行内 loader,有了模块的资源路径,你猜猜我们接下来该干啥了?

是的,你猜的很对,常规的 loader 的解析工作。

(普通loader,webpack 原文为 normal loader,谁写听谁的,我写就叫常规 loader 😂)

结合代码我们看看这个过程以及后续的过程:

err => {
    // 1.
    try {
        for (const item of loaders) {
            if (typeof item.options === "string" && item.options[0] === "?") {
                const ident = item.options.slice(1);
                item.options = this.ruleSet.references.get(ident);                 
                item.ident = ident;
            }
        }
    } catch (e) {}

    // 2.
    const userRequest =
            (matchResourceData !== undefined
                    ? `${matchResourceData.resource}!=!`
                    : "") +
            stringifyLoadersAndResource(loaders, resourceData.resource);
    // 3.
    const settings = {};
    const useLoadersPost = [];
    const useLoaders = [];
    const useLoadersPre = [];

    // 4. handle .webpack[] suffix
    let resource;
    let match;
    if (
        matchResourceData &&
        typeof (resource = matchResourceData.resource) === "string" &&
        (match = /\.webpack\[([^\]]+)\]$/.exec(resource))
    ) {
        // 5.
        settings.type = match[1];
        matchResourceData.resource = matchResourceData.resource.slice(0,-settings.type.length - 10
        );
    } else {
        
        settings.type = "javascript/auto";
        const resourceDataForRules = matchResourceData || resourceData;
        // 6.
        const result = this.ruleSet.exec({/*....*/});
        
        // 7.
        for (const r of result) {
            
            // 8.
            if (r.type === "use") {
                if (!noAutoLoaders && !noPrePostAutoLoaders) {
                    useLoaders.push(r.value);
                }
            } else if (r.type === "use-post") {
                // 9.
                if (!noPrePostAutoLoaders) {
                    useLoadersPost.push(r.value);
                }
            } else if (r.type === "use-pre") {
                // 10.
                if (!noPreAutoLoaders && !noPrePostAutoLoaders) {
                    useLoadersPre.push(r.value);
                }
            } else if (
                typeof r.value === "object" &&
                r.value !== null &&
                typeof settings[r.type] === "object" &&
                settings[r.type] !== null
            ) {
                // 11.
                settings[r.type] = cachedCleverMerge(settings[r.type], r.value);
            } else {
                // 12.
                settings[r.type] = r.value;
            }
        }
    }
    
    // 13.
    let postLoaders, normalLoaders, preLoaders;

    // 14.
    const continueCallback = needCalls(3, err => { /* .... */ });
    
    // 15.
    this.resolveRequestArray(
        contextInfo,
        this.context,
        useLoadersPost,
        loaderResolver,
        resolveContext,
        (err, result) => {
            postLoaders = result;
            continueCallback(err);
        }
    );
    
    // 16.
    this.resolveRequestArray(
        contextInfo,
        this.context,
        useLoaders,
        loaderResolver,
        resolveContext,
        (err, result) => {
            normalLoaders = result;
            continueCallback(err);
        }
    );
    
    // 17.
    this.resolveRequestArray(
        contextInfo,
        this.context,
        useLoadersPre,
        loaderResolver,
        resolveContext,
        (err, result) => {
            preLoaders = result;
            continueCallback(err);
        }
    );
}

下面我们先整体看看这个 callback 都干了什么,然后我们再展开内部的一些细节。

  1. 遍历 loaders (行内loader)重新计算行内 loader 的 ident(当前 loader 的标识,这个标识不不含问号);

  2. 计算 userRequest,如果有 matchResource 语法,则先为 matchResource.resource 后拼接 !=! 这个 matchResource 的特殊标识做前缀,接着再拼接 stringifyLoadersAndResource 方法返回的行内 loader 和经过解析的模块路径拼接;

  3. 声明若干变量:

    • 3.1 settings 对象,用于设置用于匹配的模块,和 rule.type 作用类似,常规的 js 模块的 type 为 'javascript/auto';
    • 3.2 useLoadersPost 数组,后置 loader;
    • 3.3 useLoaders 数组,常规 loader;
    • 3.4 useLoadersPres 数组,前置 loader;
  4. 处理 .webpack[] 语法的后缀,类似 webpack[javascript/auto] 这种, .webpack[javascript/auto] 是 .webpack[type] 模式的伪拓展,当没有指定其他模块类型时,我们使用它指定一个默认 模块类型,它通常和 !=! 语法一起使用。

  5. 当命中 matchResource 并且命中了 .webpack[] 语法时的处理,其中 match[1] 就是具体的 type,需要把这个 type 赋值给 settings 对象;

  6. 这里则说的是没有使用 matchResouce 语法的情况,此时我们我们设置 settings.type 为默认的 'javascript/auto',接着干了一件重要的事儿调用 nmf.ruleSet.exec 方法,将结果赋值给 result 常量,这个方法用于计算当前 request 需要使用的 loader,这个是我们后面的一个重点内容;

  7. 遍历上一步得到的 loader 结果集合 result,下面根据 type 不同进行分类;

  8. 如果 type 为 use,说明其实常规 loader,则将该 loader 加入到 useLoaders 中,加入前需要保证没有禁用常规 loader且为禁用前置后置常规 loader(noPrePostAutoLoader 为 false,下同);

  9. 如果 type 是 use-post,说明这是个后置 loader,则需要加将其加入到 useLoaderPost 中,加入前需要保证此前没有禁用前置后置普通 loader;

  10. 如果 type 是 use-pre,说明这是个前置 loader,则需要将其加入到 useLoaderPre 中,加入前需要保障此前没有禁用前置后置普通 loader;

  11. 处理声明了 value 且值为对象的情况,此时需要将合并 value 和此前的 setting[r.type],这个忽略吧,暂时没有遇到过;

  12. 如果 value 不是对象,则需要将值赋值给 settings[r.type];

  13. 声明 postLoaders, normalLoaders, preLoaders 变量,这几个是承载最终解析结果的数组,分别对应后置 loader、常规 loader,前置 loader;

  14. 再次声明 continueCallback 方法,此时需要调用 3 次;这个回调将会在 post/normal/pre 这三个批次的 loader 均被成功解析后执行,其中的逻辑暂时不展开;

  15. 调用 nmf.resolveRequestArray 并传入 loaderResolver 解析后置 loader,成功后结果赋值给 postLoaders;

  16. 调用 nmf.resolveRequestArray 并传入 loaderResolver 解析常规 loader,成功后结果赋值给 normalLoaders;

  17. 调用 nmf.resolveReqestArray 并传入 loaderResolver 解析前置 loader,成功后将结果赋值给 preLoaders;

三、总结

本文接上文讨论的是解析行内 loader 和模块资源路径之后的事情——调用 continueCallback 的 callback;该 callback 主要做了以下工作:

  1. 处理行内 loader 的 ident 标识;
  2. 处理 matchResouce 及其 matchResource 语法相关的 userRequest、settings.type 等;
  3. 调用 nmf.ruleSet.exec 并传入相关数据获取当前 request 应该被应用的 loader;
  4. 遍历上一步获取到的 loader 集合,根据各个 loader 的 type 将各个 loader 分类,分别放到 pre/post/auto 三个数组中,分别对应 前置、后置、常规 loader,这里注意除了 type 本身的要求,还需要满足是否禁用其他 loader 的相关设置,即 noPrePostAutoLoader 等变量;

以上是大致的 callback 内容,其中有很多的细节内容我们还需要进一步探究,比如 ruleSet 这个东西哪里来的,又是如何工作的等.