HarmonyOS开发者限时福利来啦!最高10w+现金激励等你拿~ 了解详情
写点什么

webpack 系列之四 loader 详解 1

  • 2019-09-19
  • 本文字数:4475 字

    阅读完需:约 15 分钟

webpack系列之四loader详解1

在上一期我们 webpack 系列中,我们介绍了针对普通文件的 resolve 流程 和 loader 的 resolve 主流程。本期我们就来介绍 loader 的基本配置以及匹配规则。如有疑问或想要交流,欢迎在文末留言。


本篇来分析下 webpack loader 详细的分析部分,由于涉及内容比较多,所以总共分成三篇文章来分析:


  1. loader 的基本配置以及匹配规则

  2. loader 的解析执行详解

  3. loader 的实践

1.loader 的配置

webpack 对于一个 module 所使用的 loader 对开发者提供了 2 种使用方式:

webpack config 配置形式

形如:


// webpack.config.jsmodule.exports = {  ...  module: {    rules: [{      test: /.vue$/,      loader: 'vue-loader'    }, {      test: /.scss$/,      use: [        'vue-style-loader',        'css-loader',        {          loader: 'sass-loader',          options: {            data: '$color: red;'          }        }      ]    }]  }  ...}
复制代码

inline 内联形式

// moduleimport a from 'raw-loader!../../utils.js'
复制代码


2 种不同的配置形式,在 webpack 内部有着不同的解析方式。此外,不同的配置方式也决定了最终在实际加载 module 过程中不同 loader 之间相互的执行顺序等。

2.loader 的匹配

在讲 loader 的匹配过程之前,首先从整体上了解下 loader 在整个 webpack 的 workflow 过程中出现的时机。



在一个 module 构建过程中,首先根据 module 的依赖类型(例如 NormalModuleFactory)调用对应的构造函数来创建对应的模块。在创建模块的过程中(new NormalModuleFactory()),会根据开发者的 webpack.config 当中的 rules 以及 webpack 内置的 rules 规则实例化 RuleSet 匹配实例,这个 RuleSet 实例在 loader 的匹配过滤过程中非常的关键,具体的源码解析可参见 Webpack Loader Ruleset 匹配规则解析。实例化 RuleSet 后,还会注册 2 个钩子函数:


class NormalModuleFactory {  ...  // 内部嵌套 resolver 的钩子,完成相关的解析后,创建这个 normalModule  this.hooks.factory.tap('NormalModuleFactory', () => (result, callback) => { ... })  // 在 hooks.factory 的钩子内部进行调用,实际的作用为解析构建一共 module 所需要的 loaders 及这个 module 的相关构建信息(例如获取 module 的 packge.json等)  this.hooks.resolver.tap('NormalModuleFactory', () => (result, callback) => { ... })  ...}
复制代码


当 NormalModuleFactory 实例化完成后,并在 compilation 内部调用这个实例的 create 方法开始真实开始创建这个 normalModule。首先调用 hooks.factory 获取对应的钩子函数,接下来就调用 resolver 钩子(hooks.resolver)进入到了 resolve 的阶段,在真正开始 resolve loader 之前,首先就是需要匹配过滤找到构建这个 module 所需要使用的所有的 loaders。首先进行的是对于 inline loaders 的处理:


// NormalModuleFactory.js// 是否忽略 preLoader 以及 normalLoaderconst noPreAutoLoaders = requestWithoutMatchResource.startsWith("-!");// 是否忽略 normalLoaderconst noAutoLoaders =  noPreAutoLoaders || requestWithoutMatchResource.startsWith("!");// 忽略所有的 preLoader / normalLoader / postLoaderconst noPrePostAutoLoaders = requestWithoutMatchResource.startsWith("!!");// 首先解析出所需要的 loader,这种 loader 为内联的 loaderlet elements = requestWithoutMatchResource  .replace(/^-?!+/, "")  .replace(/!!+/g, "!")  .split("!");let resource = elements.pop(); // 获取资源的路径elements = elements.map(identToLoaderRequest); // 获取每个loader及对应的options配置(将inline loader的写法变更为module.rule的写法)
复制代码


首先是根据模块的路径规则,例如模块的路径是以这些符号开头的 ! / -! / !! 来判断这个模块是否只是使用 inline loader,或者剔除掉 preLoader, postLoader 等规则:


  • ! 忽略 webpack.config 配置当中符合规则的 normalLoader

  • -! 忽略 webpack.config 配置当中符合规则的 preLoader/normalLoader

  • !! 忽略 webpack.config 配置当中符合规则的 postLoader/preLoader/normalLoader


这几个匹配规则主要适用于在 webpack.config 已经配置了对应模块使用的 loader,但是针对一些特殊的 module,你可能需要单独的定制化的 loader 去处理,而不是走常规的配置,因此可以使用这些规则来进行处理。


接下来将所有的 inline loader 转化为数组的形式,例如:


import 'style-loader!css-loader!stylus-loader?a=b!../../common.styl'
复制代码


最终 inline loader 统一格式输出为:


[{  loader: 'style-loader',  options: undefined}, {  loader: 'css-lodaer',  options: undefined}, {  loader: 'stylus-loader',  options: '?a=b'}]
复制代码


对于 inline loader 的处理便是直接对其进行 resolve,获取对应 loader 的相关信息:


asyncLib.parallel([  callback =>     this.resolveRequestArray(      contextInfo,      context,      elements,      loaderResolver,      callback    ),  callback => {    // 对这个 module 进行 resolve    ...    callack(null, {      resouceResolveData, // 模块的基础信息,包含 descriptionFilePath / descriptionFileData 等(即 package.json 等信息)      resource // 模块的绝对路径    })  }], (err, results) => {  const loaders = results[0] // 所有内联的 loaders  const resourceResolveData = results[1].resourceResolveData; // 获取模块的基本信息  resource = results[1].resource; // 模块的绝对路径  ...    // 接下来就要开始根据引入模块的路径开始匹配对应的 loaders  let resourcePath =    matchResource !== undefined ? matchResource : resource;  let resourceQuery = "";  const queryIndex = resourcePath.indexOf("?");  if (queryIndex >= 0) {    resourceQuery = resourcePath.substr(queryIndex);    resourcePath = resourcePath.substr(0, queryIndex);  }  // 获取符合条件配置的 loader,具体的 ruleset 是如何匹配的请参见 ruleset 解析(https://github.com/CommanderXL/Biu-blog/issues/30)  const result = this.ruleSet.exec({    resource: resourcePath, // module 的绝对路径    realResource:      matchResource !== undefined        ? resource.replace(/\?.*/, "")        : resourcePath,    resourceQuery, // module 路径上所带的 query 参数    issuer: contextInfo.issuer, // 所解析的 module 的发布者    compiler: contextInfo.compiler   });  // result 为最终根据 module 的路径及相关匹配规则过滤后得到的 loaders,为 webpack.config 进行配置的  // 输出的数据格式为:
/* [{ type: 'use', value: { loader: 'vue-style-loader', options: {} }, enforce: undefined // 可选值还有 pre/post 分别为 pre-loader 和 post-loader }, { type: 'use', value: { loader: 'css-loader', options: {} }, enforce: undefined }, { type: 'use', value: { loader: 'stylus-loader', options: { data: '$color red' } }, enforce: undefined }] */
const settings = {}; const useLoadersPost = []; // post loader const useLoaders = []; // normal loader const useLoadersPre = []; // pre loader for (const r of result) { if (r.type === "use") { // postLoader if (r.enforce === "post" && !noPrePostAutoLoaders) { useLoadersPost.push(r.value); } else if ( r.enforce === "pre" && !noPreAutoLoaders && !noPrePostAutoLoaders ) { // preLoader useLoadersPre.push(r.value); } else if ( !r.enforce && !noAutoLoaders && !noPrePostAutoLoaders ) { // normal loader useLoaders.push(r.value); } } else if ( typeof r.value === "object" && r.value !== null && typeof settings[r.type] === "object" && settings[r.type] !== null ) { settings[r.type] = cachedMerge(settings[r.type], r.value); } else { settings[r.type] = r.value; } // 当获取到 webpack.config 当中配置的 loader 后,再根据 loader 的类型进行分组(enforce 配置类型) // postLoader 存储到 useLoaders 内部 // preLoader 存储到 usePreLoaders 内部 // normalLoader 存储到 useLoaders 内部 // 这些分组最终会决定加载一个 module 时不同 loader 之间的调用顺序
// 当分组过程进行完之后,即开始 loader 模块的 resolve 过程 asyncLib.parallel([ [ // resolve postLoader this.resolveRequestArray.bind( this, contextInfo, this.context, useLoadersPost, loaderResolver ), // resove normal loaders this.resolveRequestArray.bind( this, contextInfo, this.context, useLoaders, loaderResolver ), // resolve preLoader this.resolveRequestArray.bind( this, contextInfo, this.context, useLoadersPre, loaderResolver ) ], (err, results) => { ... // results[0] -> postLoader // results[1] -> normalLoader // results[2] -> preLoader // 这里将构建 module 需要的所有类型的 loaders 按照一定顺序组合起来,对应于: // [postLoader, inlineLoader, normalLoader, preLoader] // 最终 loader 所执行的顺序对应为: preLoader -> normalLoader -> inlineLoader -> postLoader // 不同类型 loader 上的 pitch 方法执行的顺序为: postLoader.pitch -> inlineLoader.pitch -> normalLoader.pitch -> preLoader.pitch (具体loader内部执行的机制后文会单独讲解) loaders = results[0].concat(loaders, results[1], results[2]);
process.nextTick(() => { ... // 执行回调,创建 module }) } ]) }})
复制代码


简单总结下匹配的流程就是:


首先处理 inlineLoaders,对其进行解析,获取对应的 loader 模块的信息,接下来利用 ruleset 实例上的匹配过滤方法对 webpack.config 中配置的相关 loaders 进行匹配过滤,获取构建这个 module 所需要的配置的的 loaders,并进行解析,这个过程完成后,便进行所有 loaders 的拼装工作,并传入创建 module 的回调中。


本文转载自公众号滴滴技术(ID:didi_tech)。


原文链接:


https://mp.weixin.qq.com/s/CdMGRVPO9KxYr1F-dVJkYw


2019-09-19 15:583136

评论

发布
暂无评论
发现更多内容

HarmonyOS助力构建“食用菌智慧农场”

HarmonyOS开发者

HarmonyOS

前端面试遇到了这些手写题

helloworld1024fd

JavaScript 前端

龙蜥开发者说:一人行快,众人行远!在龙蜥社区的日子,我想说这些 | 第 12 期

OpenAnolis小助手

Linux 开源 合作 龙蜥开发者说 红旗

java培训学员需要注意什么呢

小谷哥

软件测试 | 测试开发 | 移动端App自动化之触屏操作自动化

测吧(北京)科技有限公司

测试

什么是容灾和备份?

源字节1号

微信小程序 软件开发 前端开发 后端开发

智能电饭煲

OpenHarmony开发者

OpenHarmony

从近期欧美法规看软件供应链安全趋势

墨菲安全

软件供应链安全 开源安全与治理

软件测试 | 测试开发 | 解决 App 自动化测试的常见痛点(弹框及首页启动加载完成判断处理)

测吧(北京)科技有限公司

测试

Java 14 令人期待的 5 大新特性,打包工具终于要来了

小小怪下士

Java 编程 程序员 架构

将生活的热情放进1平米工位中|ONES 人物特别策划

万事ONES

软件测试 | 测试开发 | app自动化测试之Appium问题分析及定位

测吧(北京)科技有限公司

测试

零基础参加java培训机构有用吗

小谷哥

感觉最近vue相关面试题回答的不好,那就总结一下吧

bb_xiaxia1998

Vue 前端

软件测试 | 测试开发 | app自动化测试之Appium 源码修改定制分析

测吧(北京)科技有限公司

测试

前端js手写题经常忘,记录一下

helloworld1024fd

JavaScript 前端

软件测试 | 测试开发 | app自动化测试之Capability 使用进阶

测吧(北京)科技有限公司

测试

软件测试 | 测试开发 | Spring Boot 异常处理

测吧(北京)科技有限公司

测试

蓝海变红海,NFT 的未来在哪里

TinTinLand

区块链 创业 web3 NFT生态链游

软件测试 | 测试开发 | 背熟这些 Docker 命令,面试再也不怕啦~

测吧(北京)科技有限公司

测试

9月《中国数据库行业分析报告》重磅发布!关键词:软硬兼施,创新融合

墨天轮

数据库 oracle cpu 硬件 国产数据库

软件测试 | 测试开发 | app自动化测试之Andriod微信小程序的自动化测试

测吧(北京)科技有限公司

测试

2022年史上最全Java面试题:数据结构+算法+JVM+线程+finalize+GC

小小怪下士

Java JVM java面试 秋招编程 算法结构

ONES 参与《软件研发效能权威指南》编写

万事ONES

java开发培训课程应该注意什么

小谷哥

分享一个研发工作优先级的计算公式 | Liga译文

LigaAI

Scrum 产品经理 敏捷开发 产品优先级 企业号九月金秋榜

等保费用是什么?为什么这么贵?

行云管家

网络安全 等保 等级保护

TiDB+TiSpark部署--安装,扩缩容及升级操作

TiDB 社区干货传送门

安装 & 部署

软件测试 | 测试开发 | 谁懂这篇文,玩游戏还会卡顿?

测吧(北京)科技有限公司

测试

leetcode 144. Binary Tree Preorder Traversal 二叉树展开为链表(中等)

okokabcd

LeetCode 算法与数据结构

软件测试 | 测试开发 | app自动化测试之设备交互API详解

测吧(北京)科技有限公司

测试

webpack系列之四loader详解1_文化 & 方法_肖磊_InfoQ精选文章