欢迎你们前往云+社区,获取更多腾讯海量技术实践干货哦~html
做者介绍:陈柏信,腾讯前端开发,目前主要负责手Q游戏中心业务开发,以及项目相关的技术升级、架构优化等工做。
webpack 是一个强大的模块打包工具,之因此强大的一个缘由在于它拥有灵活、丰富的插件机制。可是 webpack 的文档不太友好,就我本身的学习经从来说,官方的文档并不详细,网上的学习资料又少有完整的概述和例子。因此,在研究了一段时间的 webpack 源码以后,本身但愿写个系列文章,结合本身的实践一块儿来谈谈 webpack 插件这个主题,也但愿可以帮助其余人更全面地了解 webpack。前端
这篇文章是系列文章的第二篇,将会从对象的角度来说解 webpack。若是你想从总体角度了解 webpack,能够先阅读系列文章的第一篇:node
P.S. 如下的分析都基于 webpack 3.6.0
P.S. 本文将继续沿用第一篇文章的名词,任务点表示经过 plugin 方法注册的名称webpack
跟第一篇文章相似,咱们不会将全部 webpack 中的对象都拿出来说解,而是整理了一些比较核心的概念。咱们能够先看看下面的类图:web
下面的论述将会逐一讲述类图中的对象,首先咱们先来看一下最顶层的类 Tapable。算法
Tapable 提供了 webpack 中基于任务点的架构基础,它将提供任务点注册的方法以及触发的方法。缓存
一个简单的例子,使用 plugin 方法来注册一个任务点,而后使用 applyPlugins 方法触发:bash
Tapable 里面注册任务点只有 plugin 方法,可是触发任务点的方法是提供了不少,能够分为同步和异步执行两类:网络
同步执行:架构
异步执行:
虽然上面的方法看起来不少,但从函数名就联想到函数的实际功能:
最后 Tapable 类还提供了一个方法 apply,它的做用是提供了外部插件注册任务点的统一接口,要求都在 apply 方法内部进行任务点注册逻辑:
webpack 中自定义插件就是调用 Compiler 实例对象(继承于 Tapable)的 apply 方法:
webpack 源码中随处能够见 Tapable 的身影,在了解其工做原理对理解源码颇有帮助。 Compiler 继承了 Tapable,同时也做为构建的入口对象,下面咱们来看一下。
Compiler 是一个编译器实例,在 webpack 的每一个进程中只会建立一个对象,它用来建立构建对象 Compilation,自己须要注意的属性和方法并非不少。下面咱们找几个主要的属性来讲一下。
当 webpack 开始运行时,第一件事就是解析咱们传入的配置,而后将配置赋值给 Compiler 实例:
所以,咱们能够直接经过这个属性来获取到解析后的 webpack 配置:
若是你不知足于官网给出的配置文档,想要了解更多配置解析,能够看看 WebpackOptionsDefaulter.js 这个文件,这里再也不赘述。
Compiler 实例在一开始也会初始化输入输出,分别是 inputFileSystem 和 outputFileSystem 属性,通常状况下这两个属性都是对应的 nodejs 中拓展后的 fs 对象。可是有一点要注意,当 Compiler 实例以 watch模式运行时, outputFileSystem 会被重写成内存输出对象。也就是说,实际上在 watch 模式下,webpack 构建后的文件并不会生成真正的文件,而是保存在内存中。
咱们可使用 inputFileSystem 和 outputFileSystem 属性来帮助咱们实现一些文件操做,若是你但愿自定义插件的一些输入输出行为可以跟 webpack 尽可能同步,那么最好使用 Compiler 提供的这两个变量:
webpack 的 inputFileSystem 会相对更复杂一点,它内部实现了一些缓存的机制,使得性能效率更高。若是对这部分有兴趣,能够从这个 NodeEnvironmentPlugin 插件开始看起,它是内部初始化了 inputFileSystem 和 outputFileSystem:
在第一篇文章讲解 Compilation 实例化的时候,有略微说起到建立子编译器的内容:
这里为何会有 compilation 和 this-compilation 两个任务点?实际上是跟子编译器有关, Compiler 实例经过
createChildCompiler 方法能够建立子编译器实例 childCompiler,建立时 childCompiler 会复制
compiler 实例的任务点监听器。任务点 compilation 的监听器会被复制,而任务点 this-compilation
的监听器不会被复制。 更多关于子编译器的内容,将在其余文章中讨论。
这里咱们来仔细看一会儿编译器是如何建立的, Compiler 实例经过 createChildCompiler 的方法来建立:
上面的代码看起来不少,但其实主要逻辑基本都是在拷贝父编译器的属性到子编译器上面。值得注意的一点是第9行,子编译器在拷贝父编译器的任务点时,会过滤掉 make, compile, emit, after-emit, invalid, done, this-compilation这些任务点。
若是你阅读过第一篇文章(若是没有,推荐先看一下),应该会知道上面任务点在整个构建流程中的位置。从这里咱们也能够看出来,子编译器跟父编译器的一个差异在于,子编译器并无完整的构建流程。 好比子编译器没有文件生成阶段( emit任务点),它的文件生成必须挂靠在父编译器下面来实现。
另外须要注意的是,子编译器的运行入口并不是 run 方法 ,而是有单独的 runAsChild 方法来运行,从代码上面也可以直接看出来,它立刻调用了 compile 方法,跳过了 run, make等任务点:
那么子编译器有什么做用呢?从上面功能和流程来看,子编译器仍然拥有完整的模块解析和chunk生成阶段。也就是说咱们能够利用子编译器来独立(于父编译器)跑完一个核心构建流程,额外生成一些须要的模块或者chunk。
事实上一些外部的 webpack 插件就是这么作的,好比经常使用的插件 html-webpack-plugin 中,就是利用子编译器来独立完成 html 文件的构建,为何不能直接读取 html 文件?由于 html 文件中可能依赖其余外部资源(好比 img 的src属性),因此加载 html 文件时仍然须要一个额外的完整的构建流程来完成这个任务,子编译器的做用在这里就体现出来了:
在下一篇文章中咱们将亲自实现一个插件,关于子编译器的具体实践到时再继续讨论。
接下来咱们来看看最重要的 Compilation 对象,在上一篇文章中,咱们已经说明过部分属性了,好比咱们简单回顾一下
上面这三个属性已经包含了 Compilation 对象中大部分的信息,可是咱们也只是有个大体的概念,特别是 modules 中每一个模块实例究竟是什么东西,咱们并不太清楚。因此下面的内容将会比较细地讲解。
但若是你对这部份内容不感兴趣也能够直接跳过,由于能真正使用的场景不会太多,但它能加深对 webpack 构建的理解。
所谓的模块
Compilation 在解析过程当中,会将解析后的模块记录在 modules 属性中,那么每个模块实例又是什么呢?
首先咱们先回顾一下最开始的类图,咱们会发现跟模块相关的类很是多,看起来类之间的关系也十分复杂,但其实只要记住下面的公式就很好理解:
这个公式的解读是: 一个依赖对象(Dependency)通过对应的工厂对象(Factory)建立以后,就可以生成对应的模块实例(Module)。
首先什么是 Dependency?我我的的理解是,还未被解析成模块实例的依赖对象。好比咱们运行 webpack 时传入的入口模块,或者一个模块依赖的其余模块,都会先生成一个 Dependency 对象。做为基类的 Dependency 十分简单,内部只有一个 module 属性来记录最终生成的模块实例。可是它的派生类很是多,webpack 中有单独的文件夹( webpack/lib/dependencies)来存放全部的派生类,这里的每个派生类都对应着一种依赖的场景。好比从 CommonJS 中require一个模块,那么会先生成 CommonJSRequireDependency。
有了 Dependency 以后,如何找到对应的工厂对象呢? Dependecy 的每个派生类在使用前,都会先肯定对应的工厂对象,好比 SingleEntryDependency 对应的工厂对象是 NormalModuleFactory。这些信息所有是记录在 Compilation 对象的 dependencyFactories 属性中,这个属性是 ES6 中的 Map 对象。直接看下面的代码可能更容易理解:
一种工厂对象只会生成一种模块,因此不一样的模块实例都会有不一样的工厂对象来生成。模块的生成过程咱们在第一篇文章有讨论过,无非就是解析模块的 request, loaders等信息而后实例化。
模块对象有哪些特性呢?一样在第一篇文章中,咱们知道一个模块在实例化以后并不意味着构建就结束了,它还有一个内部构建的过程。全部的模块实例都有一个 build 方法,这个方法的做用是开始加载模块源码(并应用loaders),而且经过 js 解析器来完成依赖解析。这里要两个点要注意:
咱们再来看看这些模块类,从前面的类图看,它们是继承于 Module 类。这个类实际上才是咱们日常用来跟 chunk 打交道的类对象,它内部有 _chunks 属性来记录后续所在的 chunk 信息,而且提供了不少相关的方法来操做这个对象: addChunk, removeChunk, isInChunk, mapChunks等。后面咱们也会看到, Chunk 类与之对应。
Module 类往上还会继承于 DependenciesBlock,这个是全部模块的基类,它包含了处理依赖所须要的属性和方法。上面所说的 variables, dependencies, blocks 也是这个基类拥有的三个属性。它们分别是:
通过上面的讨论以后,咱们基本将 webpack 中于模块相关的对象、概念都涉及到了,剩下还有模块渲染相关的模板,会在下面描述 Template 时继续讨论。
Chunk
讨论完 webpack 的模块以后,下面须要说明的是 Chunk 对象。关于 chunk 的生成,在第一篇文章中有涉及,这里再也不赘述。 chunk 只有一个相关类,并且并不复杂。 Chunk 类内部的主要属性是 _modules,用来记录包含的全部模块对象,而且提供了不少方法来操做: addModule, removeModule, mapModules 等。 另外有几个方法可能比较实用,这里也列出来:
Template
Compilation 实例在生成最终文件时,须要将全部的 chunk 渲染(生成代码)出来,这个时候须要用到下面几个属性:
在第一篇文章时,有略微描述过 chunk 渲染的过程,这里再仔细地过一遍,看看这几个属性是如何应用在渲染过程当中的:
首先 chunk 的渲染入口是 mainTemplate 和 chunkTemplate 的 render 方法。根据 chunk 是不是入口 chunk 来区分使用哪个:
两个类的 render 方法将生成不一样的"包装代码", MainTemplate 对应的入口 chunk 须要带有 webpack 的启动代码,因此会有一些函数的声明和启动。 这两个类都只负责这些"包装代码"的生成,包装代码中间的每一个模块代码,是经过调用 renderChunkModules 方法来生成的。这里的 renderChunkModules 是由他们的基类 Template 类提供,方法会遍历 chunk 中的模块,而后使用 ModuleTemplate 来渲染。
ModuleTemplate 作的事情跟 MainTemplate 相似,它一样只是生成"包装代码"来封装真正的模块代码,而真正的模块代码,是经过模块实例的 source 方法来提供。该方法会先读取 _source 属性,即模块内部构建时应用loaders以后生成的代码,而后使用 dependencyTemplates 来更新模块源码。
dependencyTemplates 是 Compilation 对象的一个属性,它跟 dependencyFactories 一样是个 Map 对象,记录了全部的依赖类对应的模板类。
上面用文字来描述这个过程可能十分难懂,因此咱们直接看实际的例子。好比下面这个文件:
let a = require("./a")复制代码
其中,从 1-113 行都是 MainTemplate 生成的启动代码,剩余的代码生成以下图所示:
经过这篇文章,咱们将 webpack 中的一些核心概念和对象都进行了不一样程度的讨论,这里再总结一下他们主要的做用和意义:
以上内容,但愿可以帮助你们进一步了解 webpack ,感谢你们阅读~
玩转webpack(一)上篇:webpack的基本架构和构建流程
玩转webpack(一)下篇:webpack的基本架构和构建流程
利用神经网络算法的C#手写数字识别
此文已由做者受权云加社区发布,转载请注明文章出处