webpack loader 的优先级是这样组织的

一、前文回顾

前文中我们详细讨论模块创建的一个分支流程即如何获取当前的模块需要应用到 loaders 结果集。这个过程呢呢是有 RuleSetCompiler 这个类型来实现的。

上文详细介绍了 RuleSetCompiler 类型的工作原理,回顾整个调用过程:

  1. 首先 NMF 实例创建了 RuleSetCompiler 的实例;
  2. 接着调用了 ruleSetCompiler.compileRules 编译 rules,得到带有 exec 方法的对象;
  3. 在有了模块路径后调用上一步得到的 exec 方法获取当前模块路径需要应用的 loader;

回顾整个过程:

  1. RuleSetCompiler 把 webpack 内置的和通过 webpack.config.js.module.rules 配置而来的规则进行编译,得到标准的 ruleSet 对象,其中 { exec: () => { ... }, references };
  2. 在调用 nmf.ruleSet.exec 方法时,内部会遍历前面注册的各种 rule 最终把命中的 rule 添加到 effects 中,而这个 effect 就是最终被应用的 loader;

今天我们的视线回到 webpack 创建模块的过程中,接上文我们获取到了 loader 集合后面的事情。

二、ruleSet.exec 的返回值

以下是 exec 的调用过程:

js 复制代码
const result = this.ruleSet.exec({
    resource: resourceDataForRules.path,
    realResource: resourceData.path,
    resourceQuery: resourceDataForRules.query,
    resourceFragment: resourceDataForRules.fragment,
    scheme,
    assertions,
    mimetype: matchResourceData
        ? ""
        : resourceData.data.mimetype || "",
    dependency: dependencyType,
    descriptionData: matchResourceData
        ? undefined
        : resourceData.data.descriptionFileData,
    issuer: contextInfo.issuer,
    compiler: contextInfo.compiler,
    issuerLayer: contextInfo.issuerLayer || ""
});

2.1 重要参数

这里有几个关键的参数需要关注:

  1. resource: resourceDataForRules.path,在非 matchResource 模式下就是模块的真实的资源路径,如果是 matchResource 模式时,则是 matchResource 声明的目标资源路径,这个有点特殊,比如 你有一个 vue 的组件,a.vue,在 a.vue 被编译时,样式块就需要被应用一些 loader,比如 stylus-loader,此时就可以通过 matchResouce 返回这样一个 a.vue.stylus 这样的 资源模块;对应的也就得到这样一个虚拟的模块 path;
  2. realResource: resourceData.path,这里就是资源路径的真实路径;

2.2 result

nmf.ruleSet.exec 的返回值是一个数组对象,其中的每一项包含两个属性即 { type, value },以我们的例子而言:

json 复制代码
const result = [
  {
    "type": "use",
    "value": {
      "loader": "babel-loader",
      "options": {
        "presets": [
          "@babel/preset-env"
        ],
        "plugins": []
      },
      "ident": "ruleSet[1].rules[0].use[0]"
    }
  }
]

这里这个很简单,就是我们声明的 babel-loader,现在我们具体分析一下这个结构:

  1. type:类型,即 loader 的类型,类型分为三种类型,这个和 webpack 的配置中的 rule.enforce 结合,这个东西后面用来组织 webpack 的 loader 的执行顺序;
    • 1.1 "use": 常规 loader,也就是 normal loader;
    • 1.2 "use-pre":前置 loader;
    • 1.3 "use-post":后置 laoder;
  2. value:这个是值类型,这个 value 用于描述当前这个 loader 的信息,以我们上面例子中来看这里面的属性含义:
    • 2.1 loader:loader 的名称;
    • 2.2 options:传递给 loader 执行的选项配置信息,这里面的东西不是固定的,因 loader 而异,如果你是 loader 开发者,这里面的东西可以自定义;
    • 2.3 ident:loader 的标识符,这个玩意儿一般是由 webpack 在编译 rule 时自己生成的;

2.3 给 loader 分类

有了上面的 loader 之后并不能进入到执行 loader 的过程,在执行之前还有很多路要走!

有了 loader 需要先根据前面的结果中的类型 type 进行分类:

js 复制代码
for (const r of result) {
    if (r.type === "use") {
        if (!noAutoLoaders && !noPrePostAutoLoaders) {
            useLoaders.push(r.value);
        }
    } else if (r.type === "use-post") {
        if (!noPrePostAutoLoaders) {
            useLoadersPost.push(r.value);
        }
    } else if (r.type === "use-pre") {
        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
    ) {
        settings[r.type] = cachedCleverMerge(settings[r.type], r.value);
    } else {
        settings[r.type] = r.value;
    }
}

这个分类的过程其实已经简述过了,其实很简单就是遍历 result 这个结果集合,然后根据每一项的 type 分包放到不同的数组中:

  1. type === 'use':放到 useLoaders 数组中;
  2. type === 'use-post':放到 useLoaderPost 数组中;
  3. type === 'use-pre':放到 useLoadersPre 数组中;

三、解析 loaders

虽然上面已经得到了具体需要应用的 loaders,但是这些 loader 都只是一个名字而已,其具体的模块路径还不知道。因此在经历上面的排序和分类之后,下面的工作就要进入到针对 loader 的解析工作:

3.1 声明 continueCallback

js 复制代码
const continueCallback = needCalls(3, err => { /* ... */ });

这个 continueCallback 方法和外面的 continueCallback 名字一样,但是方法作用不同。这个 continueCallback 需要在 3 次调用后执行声明时传入的 callback 逻辑。

这 3 次调用后的 callback 作用是最后用于组织有关 nmf.hooks.resolve 这个钩子中的最终结果用的。具体的执行细节暂时先不过多展开咯!

3.2 解析后置 loader

后置 loader 就是前面提及的 useLoaderPost 数组了:

js 复制代码
this.resolveRequestArray(
    contextInfo,
    this.context,
    useLoadersPost, // 后置 loader 数组
    loaderResolver,
    resolveContext,
    (err, result) => {
        postLoaders = result;
        continueCallback(err);
    }
);

这里都是大家熟悉的戏码了,调用 this.resolveRequestArray,解析数组中的 request;这里第三个参数传入的就是 useLoadersPost;

完成解析后再回调用把 结果 result 赋值给 postLoaders,并调用 continueCallback 计数!

3.2 解析常规 loader

接着用调用 this.resolveRequestArray 解析普通的 loader,原理不再赘述

js 复制代码
this.resolveRequestArray(
    contextInfo,
    this.context,
    useLoaders, // 常规 loader
    loaderResolver,
    resolveContext,
    (err, result) => {
        normalLoaders = result;
        continueCallback(err);
    }
);

完成解析后再回调用把 结果 result 赋值给 normalLoaders,并调用 continueCallback 计数!

3.3 解析前置 loader

解析前置 loader

js 复制代码
this.resolveRequestArray(
    contextInfo,
    this.context,
    useLoadersPre, // 前置的 loader
    loaderResolver,
    resolveContext,
    (err, result) => {
        preLoaders = result;
        continueCallback(err);
    }
);

完成解析后再回调用把 结果 result 赋值给 normalLoaders,并调用 continueCallback 计数。

3.4 continueCallback 的 callabck 预告

这里算一个简单的总结了,continueCallback 要求计数 3 次后调用,解析后置 loader、常规 loader 解析、前置 loader 解析,三次已经到了。

并且这里我们已经获得了所有的 loader 了,下面我们进入 nmf.hooks.resolve 的收尾工作!

四、nmf.hooks.resolve 的收尾

这个收尾工作交由 continueCallback 完成,下面我们看看它都做了哪些工作:

js 复制代码
err => {
    // 1.
    if (err) {
        return callback(err);
    }
    
    // 2.
    const allLoaders = postLoaders;
    if (matchResourceData === undefined) {
        for (const loader of loaders) allLoaders.push(loader);
        for (const loader of normalLoaders) allLoaders.push(loader);
    } else {
        for (const loader of normalLoaders) allLoaders.push(loader);
        for (const loader of loaders) allLoaders.push(loader);
    }
    
    // 3.
    for (const loader of preLoaders) allLoaders.push(loader);
    
    
    // 4.
    let type = settings.type;
    const resolveOptions = settings.resolve;
    const layer = settings.layer;
    if (layer !== undefined && !layers) {
        return callback(
            new Error(
                    "'Rule.layer' is only allowed when 'experiments.layers' is enabled"
            )
        );
    }
    
    // 5.
    try {
        Object.assign(data.createData, {
            layer:
                    layer === undefined ? contextInfo.issuerLayer || null : layer,
            request: stringifyLoadersAndResource(
                    allLoaders,
                    resourceData.resource
            ),
            userRequest,
            rawRequest: request,
            loaders: allLoaders,
            resource: resourceData.resource,
            context:
                    resourceData.context || getContext(resourceData.resource),
            matchResource: matchResourceData
                    ? matchResourceData.resource
                    : undefined,
            resourceResolveData: resourceData.data,
            settings,
            type,
            parser: this.getParser(type, settings.parser),
            parserOptions: settings.parser,
            generator: this.getGenerator(type, settings.generator),
            generatorOptions: settings.generator,
            resolveOptions
        });
    } catch (e) {
        return callback(e);
    }
    
    // 6.
    callback();
}

整个方法我们分为 6 个步骤:

  1. 处理错误,一旦有错则调用 callback 传入 err 并终止流程;

  2. 处理 loader 的排序,因为 loader 最终是倒着执行的,但是组织顺序的时候 postLoader 就放在数组的最前面的,这也是声明 allLoaders = postLoaders 的原因,整个过程如下:

    • 2.1 处理不使用 matchResource 的情况,此时先添加 loaders 中的行内 loader 到 allLoaders 中;
    • 2.2 处理完行内 loader 再把 normalLoader 中的常规 loader 添加到 allLoaders 数组;
    • 2.3 接着处理命中 matchResource 语法的情况,此时需要先添加常规 loaders 到 allLoaders 中;
    • 2.4 在 matchResource 语法中,matchResource 中的行内 loader 要优先于常规 loader 执行,因此要放到常规loader 后面添加(注意,这个是倒着执行的顺序,先添加的后执行!!!!)
  3. 最后则是添加 preLoader 到 allLoaders 数组中(这里还是因为 allLoaders 中的 loader 是倒着执行的,最后添加的将来到了执行的时候最先执行);

  4. 处理 type 和 layer,这里不再多说;

  5. 最后组织 data.createData 对象,这里面包含了 nmf.hooks.resolve 的最终工作产出,这里有些数据后面要用需要注意一下!

  6. 调用 callback 并传入 data,这个 callback 会结束 nmf.hooks.resolve 的执行,回到 nmf.hooks.factory 中;

五、总结

本文回到了 NMF 中创建模块的重要流程 ------ hooks.resolve 的流程中,核心点还是关于 loader 的组织,期间讨论了以下重点内容:

  1. nmf.ruleSet.exec 方法的工作原理,重点解析了其参数、返回值、以及其中的 loader 分类的过程;
  2. 接着讨论了解析 loader 的过程,按照 loader 的类型即 use/use-post/use-pre 进行分开解析;
  3. 在解析完成后会执行 continueCallback:
    • 3.1. 这个过程主要是处理 loader 的顺序问题,与顺序相关的因素处理 loader 的类型外,还有一个 matchResource 语法,它主要影响行内 loader 的是在常规 loader 还是之后;
    • 3.2 最后则是组织 data.createData 为创建模块和构建模块备用;
    • 3.3 调用 callback 把执行权限重新交还到 nmf.hooks.factory 继续模块的创建工作;
相关推荐
chxii19 分钟前
6.3Element UI 的表单
javascript·vue.js·elementui
深兰科技25 分钟前
深兰科技:搬迁公告,我们搬家了
javascript·人工智能·python·科技·typescript·laravel·深兰科技
lumi.1 小时前
Swiper属性全解析:快速掌握滑块视图核心配置!(2.3补充细节,详细文档在uniapp官网)
前端·javascript·css·小程序·uni-app
芝士加1 小时前
还在用html2canvas?介绍一个比它快100倍的截图神器!
前端·javascript·开源
阿虎儿1 小时前
React 引用(Ref)完全指南
前端·javascript·react.js
绝无仅有2 小时前
使用 Docker、Jenkins、Harbor 和 GitLab 构建 CI/CD 流水线
后端·面试·github
前端小大白2 小时前
JavaScript 循环三巨头:for vs forEach vs map 终极指南
前端·javascript·面试
晴空雨2 小时前
面试题:如何判断一个对象是否为可迭代对象?
前端·javascript·面试
阿虎儿2 小时前
React 事件类型完全指南:深入理解合成事件系统
前端·javascript·react.js
Hilaku2 小时前
前端需要掌握多少Node.js?
前端·javascript·node.js