写点什么

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:583172

评论

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

工作都是公司的,技术才是自己的,【2021Android最新学习路线

android 程序员 移动开发

开发部署提速8倍!这款IDE插件了解一下?,211本硕如何通过字节跳动、百度、美团Android面试

android 程序员 移动开发

当中国诗词大会邂逅了Flutter,从此我的眼里只有你!,附架构师必备技术详解

android 程序员 移动开发

学习Flutter,你需要了解的Dart 编码规范,sharedpreferences用法

android 程序员 移动开发

官方推荐Flow,LiveData:那我走,绝对干货

android 程序员 移动开发

小企业呆了3年多,人都呆废了!准备跳槽字节跳动,2021年不想被公司优化

android 程序员 移动开发

开发8年程序员:你是不是疯了,为什么字节跳动的Offer都不要了

android 程序员 移动开发

开发者须知:女性玩家和手机游戏注意事项(1),flutter面试题2021

android 程序员 移动开发

实战|Android后台启动Activity实践之路续,2021年Android者未来的出路在哪里

android 程序员 移动开发

年后想跳槽涨薪?你想要的面试题全在这里,kotlinmap初始化

android 程序员 移动开发

年底被裁,程序员 2020 年的出路在哪里?,Android开发实用必备的几款插件

android 程序员 移动开发

开源爆款,阿里P7Android技术笔记,图文并茂,Android经典面试题详解

android 程序员 移动开发

安卓11来了,快!扶我起来,音视频开发书籍

android 程序员 移动开发

带了一个实习生程序员,这波操作搞得我人都傻了,android开发者网站

android 程序员 移动开发

建造者模式,android原生开发技术

android 程序员 移动开发

开发者须知:女性玩家和手机游戏注意事项,安卓开发中遇到最难的问题

android 程序员 移动开发

当中国诗词大会邂逅了Flutter,从此我的眼里只有你!(1)

android 程序员 移动开发

字节跳动:必面题说一下Android消息机制,重要概念一网打尽

android 程序员 移动开发

带着问题重学Android事件分发,移动端内嵌h5页面

android 程序员 移动开发

干了5年Android开发,突然感觉自己啥也不会,啥也不想干

android 程序员 移动开发

巧用Android网络通信技术,在网络上直接传输对象 (1),Android工程师面试题

android 程序员 移动开发

安卓-如何用正确的姿势监听Android屏幕旋转,flutter修改textfield内容

android 程序员 移动开发

安卓屏幕完美适配方案,组件化与插件化的差别在哪里

android 程序员 移动开发

客2消,客1消,客0消,android热修复视频

android 程序员 移动开发

安卓指纹对称加密及登录功能的实现,android面试简历

android 程序员 移动开发

实践App内存优化:如何有序地做内存分析与优化,flutter免费视频教程

android 程序员 移动开发

当 Android 的 Compat 库不能拯救你的时候,覆盖所有面试知识点

android 程序员 移动开发

当事人:现在就是非常后悔,开工那天没去上班,Flutter中网络图片加载和缓存源码分析

android 程序员 移动开发

工作两年裸辞备战一个多月收割offer,一二线互联网公司Android面试题汇总

android 程序员 移动开发

带你高效入门 Flutter,安卓开发入门书籍推荐

android 程序员 移动开发

开发八年的程序员,在字节跳动5年,30岁的我被刚升职后辈一句话惊醒

android 程序员 移动开发

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