webpack使用和原理

342 阅读8分钟

前言

  1. 既是读书笔记,也是对于webpack和工程化的一次梳理
  2. 书的内容 优点是:系统性强,引导思维性强,缺点是知识点也许不是最新的,所以书能教给你知识的一个基本核心或者渔,剩下的新内容需要你以这个核心去构建、吸收并为自己所用
  3. 请配套github示例代码,这应该是写文档和学习的一个要求标准

背景

前端近些年的技术发展特点:

  • 模块化
    • CommonJS
    • AMD
    • ES6模块
    • 样式文件的模块化
  • 新框架
    • React
    • Vue
    • Agular
  • 新语言
    • TS
    • ES6(部分新特性)
    • SCSS
    • Flow

以上的这些新的前端技术无法直接在浏览器运行,这时候需要一个工程化的打包工具,将开发阶段的代码转成可以直接在浏览器中直接运行的代码。
伴随着前端技术的发展,构建工具呈现的特点是:

  • 流程化
  • 自动化
  • 可扩展化

构建工具的基本职能

横向对比

Npm ScriptGruntGulpwebpackRollup
优点
- npm内置

- 灵活,可以自定义任务
- 有大量可复用的插件

- 引入了流的概念
- 有大量插件
- 灵活,可以和其他工具搭配使用

- 一切文件皆模块
- 专注于构建模块化项目
- 开箱即用
- 可扩展
- 使用场景不局限于web开发
- 社区庞大且活跃,紧跟新特性
- 良好的开发体验
- 有良好的维护团队
- 基本是一站式的解决方案
- 业界可以参考的教程比较多

- 在打包库比webpack更有优势,打包出来的文件更小,更快
缺点
- 功能过于简单

- 集成度不高,配置工作量大,无法做到开箱即用

- 集成度不高,配置工作量大,无法做到开箱即用

- 只能用于模块化开发的项目

- 生态链还不完善,体验不如webpack
- 功能不完善,在很多场景下都找不到现成的解决方案
- 很多特性都已经被webpack模仿实现
底层原理调用shell运行脚本命令进化版的Npm Script可以认作:Grunt + 监听文件 + 读写文件 + 流式处理
- 更适合于js库

使用

核心概念

  • chunk
    • 一般一个entry对应一个chunk, 一个chunk里包括了这个entry以及该entry的所有依赖
    • 一个chunk是由多个模块组合而成,用于代码的合并和分割
  • module
    • webpack中一切皆模块
    • 一个模块对应一个文件
  • loader
    • 执行的次序: right -> left

经典使用-场景

构建多页应用

构建同构应用

publicPath

  • CDN与publicPath
  • 最核心的部分是通过 publicPath 参数设置存放静态资源的 CDN 目录 URL, 为了让不同类型的资源输出到不同的 CDN,需要分别在:
  • output.publicPath 中设置 JavaScript 的地址。
  • css-loader.publicPath 中设置被 CSS 导入的资源的的地址。
  • WebPlugin.stylePublicPath 中设置 CSS 文件的地址。

设置好 publicPath 后,WebPlugin 在生成 HTML 文件和 css-loader 转换 CSS 代码时,会考虑到配置中的 publicPath,用对应的线上地址替换原来的相对地址

解决方案-原理

  1. 关于js:获取js内容 + 执行文件
  2. webpack构建后的输出:一个匿名自执行函数
  3. 一个模块被__webpack_require__(某个模块的相对路径)的时候,webpack会根据这个相对路径从modules对象中获取对应的源码并执行,对象的属性值为一个函数,函数内容为当前模块的eval(**源码**)
    1. 也就是说:打包的出的每一个模块都是一个匿名的自执行函数,当A模块require模块B的时候,相当于将B模块的打包后的自执行函数嵌套到了当前的A模块的自执行函数中
    2. modules对象保存的就是入口文件及其依赖模块的路径和源码对应关系,webpack打包输出文件bundle.js执行的时候就会执行匿名自执行函数中的__webpack_require__(entryId),从modules对象中找到入口文件对应的源码执行,执行入口文件的时候,发现其依赖,又继续执行__webpack_require__(dependId),再从modules对象中获取dependId的源码执行,直到全部依赖都执行完成。
  4. 插件设计原理:编译器构造函数中还有一个非常重要的事情要处理,那就是安装插件,即遍历配置文件中配置的plugins插件数组,然后调用插件对象的apply()方法,apply()方法会被传入compiler编译器对象,可以通过传入的compiler编译器对象进行监听编译器发射出来的事件,插件就可以选择在特定的时机完成一些事情。
    1. 所谓插件安装:本质上就是调用插件数组的apply方法,在该方法中通过入参传入compiler编译器对象。在该方法中,插件可以通过compiler对象来注册一些事件的handler,当这些事件被调用的时候,handler被执行。

核心流程

模块化方案

commonJS和ES6 module的差异

它们有两个重大差异:
① CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
② CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

参考:node模块化方案

var test = require('./test.js');
// node: 其实就是把test.js模块的exports属性赋值给test变量。
  • node打包的实质

模块化方案实现

webpack打包出来的代码可以简单分为两类:

  • 一类是webpack模块化的前端runtime,你可以简单类比为RequireJS这样的前端模块化类库所实现的功能。它会控制模块的加载、缓存,提供诸如__webpack_require__这样的require方法等。(runtime代码)
  • 另一类则是模块注册与运行的代码,包含了源码中的模块代码。为了进一步理解,我们先来看一下这部分的代码是怎样的。(我们的业务代码)

编译

  • 为了简化步骤,我希望在constructor中直接开始对文件进行编译。这里需要声明一个 moduleWalker 方法(这个名字是笔者取的,不是webpack官方取的),顾名思义,这个方法将会从入口模块开始进行编译,并且顺藤摸瓜将构建过程中所有的模块递归进行编译。

编译步骤主要分为两步

  1. 第一步是使用所有满足条件的loader对其进行编译并且返回编译之后的代码
  2. 第二步相当于是webpack自己的编译步骤,其中最核心的目的是构建各个独立模块之间的调用关系。我们需要做的是将所有的 require 方法替换成webpack自己定义的 __webpack_require__ 函数。因为所有被编译后的模块将被webpack存储在一个闭包的对象 moduleMap 中,当模块被引用时,都将从这个全局的 moduleMap 中获取代码。

在完成第二步编译的同时,会对当前模块内的引用进行收集,并且作为 moduleWalker 方法的回调返回到 Compilation 中, moduleWalker 方法会对这些依赖模块进行递归的编译。当然里面可能存在重复引用,我们会根据引用文件的路径生成一个独一无二的key值,在key值重复时进行跳过

chunk

  1. chunk 有两种形式:
  • initial(初始化) 是入口起点的 main chunk。此 chunk 包含为入口起点指定的所有模块及其依赖项。
  • non-initial 是可以延迟加载的块。可能会出现在使用 动态导入(dynamic imports) 或者 SplitChunksPlugin 时。

插件机制

  1. applay(compiler): 仔细体会这种注册机制实现的插件机制:
    1. 插件需要编写一个系统(webpack)所要求的注册方法(apply),该方法的参数一般是系统在运作时传入的一个全局对象(compiler或者其他信息)
    2. 系统使用一个数据结构存储插件(一般使用队列)
    3. 在系统运作时,首先将插件的队列进行注册(遍历队列,调用插件定义的注册方法apply)
    4. 插件的注册一般作为系统启动时init的一部分
  2. webpack的每一个生命周期钩子除了挂载我们自己的plugin,还挂载了一些官方默认需要挂载的 plugin

配置信息注册

  1. 配置信息一般对用户而言是配置文件(eg: webpack.config.js)
  2. 注册配置信息:一般就是把配置文件中的信息,经过自身默认值等一些列merge后,挂载到this(也就是compiler)上

优化方案

构建过程优化

构建结果优化

手写like

ME

  1. 上下文 - 是一些大型软件设计的一个元素/概念,例如:Vue中的Vue,webpack中的compiler对象。它一般是一个对象,挂载了很多需要共享的信息(属性和方法等,以及一些全局的状态信息等);

参考