webpack系列之一总览

栏目: 编程语言 · 发布时间: 5年前

内容简介:webpack 对于每个前端儿来说都不陌生,它将每个静态文件当成一个模块,经过一系列的处理为我们整合出最后的需要的 js、css、图片、字体等文件。来自官网的图很形象的阐述了 webpack 的功能 —— bundle js / css / ... (打包全世界ヾ(◍°∇°◍)ノ゙)在阅读一个东西的源码之前,首先需要了解这个东西是什么,怎么用。这样在阅读源码过程中才能在大脑中行程一副整体的认知。所以,先了解一下 webpack 打包前后代码发生了什么?找一个简单的例子入口文件为 main.js, 在其中引

webpack 对于每个前端儿来说都不陌生,它将每个静态文件当成一个模块,经过一系列的处理为我们整合出最后的需要的 js、css、图片、字体等文件。来自官网的图很形象的阐述了 webpack 的功能 —— bundle js / css / ... (打包全世界ヾ(◍°∇°◍)ノ゙)

webpack系列之一总览

写在前面的话

在阅读一个东西的源码之前,首先需要了解这个东西是什么,怎么用。这样在阅读源码过程中才能在大脑中行程一副整体的认知。所以,先了解一下 webpack 打包前后代码发生了什么?找一个简单的例子

入口文件为 main.js, 在其中引入了 a.js, b.js

// main.js
import { A } from './a'
import B from './b'
console.log(A)
B()
复制代码
// a.js
export const A = 'a'
复制代码
// b.js
export default function () {
    console.log('b')
}
复制代码

经过 webpack 的一番蹂躏,最后变成了一个文件:bundle.js。先忽略细节,看最外面的代码结构

(function(modules){
  ...(webpack的函数)
  return __webpack_require__(__webpack_require__.s = "./demo01/main.js");
})(
 {
   "./demo01/a.js": (function(){...}),
   "./demo01/b.js": (function(){...}),
   "./demo01/main.js": (function(){...}),
 }
)
复制代码

最外层是一个立即执行函数,参数是 modules。 a.js、b.js 和 main.js 最后被编译成三个函数(下文将这三个函数称为 module 函数),key 是文件的相对路径。bundle.js 会执行到 __webpack_require__(__webpack_require__.s = "./demo01/main.js"); 即通过 __webpack_require__('./demo01/main.js') 开始主入口函数的执行。

通过 bundle.js 的主接口可以清晰的看出,对于 webpack 每个文件就是一个 module。 我们写的 import 'xxx' ,则最终为 __webpack_require__ 函数执行。更多的时候我们使用的是 import A from 'xxx' 或者 import { B } from 'xxx' ,可以猜想一下,这个 __webpack_require__ 函数中除了找到对应的 'xxx' 来执行,还需要一个返回 'xxx' 中 export 出来的内容。

function __webpack_require__(moduleId) {
	// Check if module is in cache
	if(installedModules[moduleId]) {
		return installedModules[moduleId].exports;
	}
	// Create a new module (and put it into the cache)
	var module = installedModules[moduleId] = {
		i: moduleId,
		l: false,
		exports: {}
	};
	// Execute the module function
	modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
	// Flag the module as loaded
	module.l = true;
	// Return the exports of the module
	return module.exports;
}
复制代码

调用每一个 module 函数时,参数为 modulemodule.exports__webpack_require__module.exports 用来收集 module 中所有的 export xxx 。看 ”./demo/a.js“ 的 module

(function(module, __webpack_exports__, __webpack_require__) {

// ...
__webpack_require__.d(__webpack_exports__, "A", function() { return A; });
const A = 'a'

/***/ })

// ...
__webpack_require__.d = function(exports, name, getter) {
	if(!__webpack_require__.o(exports, name)) {
		Object.defineProperty(exports, name, {
			configurable: false,
			enumerable: true,
			get: getter
		});
	}
};
// Object.prototype.hasOwnProperty.call
__webpack_require__.o = function(object, property) { 
	return Object.prototype.hasOwnProperty.call(object, property); 
};
// ...
复制代码

__webpack_require__.d(__webpack_exports__, "A", function() { return A; }); 简单理解就是

__webpack_exports__.A = function(){ return A; }
复制代码

__webpack_exports__ 实际为上面的 __webpack_require__ 中传入的 moule.exports , 如此,就将 A 变量收集到了 module.exports 中。如此我们的

import { A } from './a.js' 
  console.log(A)
复制代码

就编译为

var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./demo/a.js"); 
  console.log(_a__WEBPACK_IMPORTED_MODULE_0__["A"])
复制代码

对于 b.js 我们使用的是 export default ,webpack 处理后,会在 module.exports 中增加一个 default 属性。

__webpack_exports__["default"] = (function () {
    console.log('b')
});
复制代码

最后 import B from './b.js 编译为

var _b__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__("./demo/b.js")
Object(_b__WEBPACK_IMPORTED_MODULE_1__["default"])()
复制代码

异步加载

在 webpack 中我们可以很方便的实现异步加载,以简单的 demo 入手

// c.js
export default {
  key: 'something'
}
复制代码
// main.js
  import('./c').then(test => {
    console.log(test)
})
复制代码

打包结果,异步加载的 c.js,最后打包在一个单独的文件 0.js 中

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
"./demo/c.js": (function(module, __webpack_exports__, __webpack_require__) {
  "use strict";
  __webpack_require__.r(__webpack_exports__);
  __webpack_exports__["default"] = ({
  		key2: 'key2'
	});
  })
}]);
复制代码

简化一下,执行的就是

var t = window["webpackJsonp"] = window["webpackJonsp"] || [])
t.push([[0], {function(){...}}])
复制代码

执行 import('./c.js') 时,实际上通过在 HTML 中插入一个 script 标签加载 0.js。 0.js 加载后会执行 window["webpackJsonp"].push 方法。 在 main.js 在还有一段:

var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
jsonpArray.push = webpackJsonpCallback;
复制代码

这里篡改了一下, window["webpackJsonp"] 的 push 方法,将 push 方法外包装了一层 webpackJonspCallback 的逻辑。当 0.js 加载后,会执行 window["webpackJsonp"].push ,这时便会进入 webpackJsonpCallback 的执行逻辑。

function webpackJsonpCallback(data) {
    var chunkIds = data[0];
    var moreModules = data[1];
    // add "moreModules" to the modules object,
    // then flag all "chunkIds" as loaded and fire callback
    var moduleId, chunkId, i = 0, resolves = [];
    for(;i < chunkIds.length; i++) {
        chunkId = chunkIds[i];
        if(installedChunks[chunkId]) {
            resolves.push(installedChunks[chunkId][0]);
        }
        installedChunks[chunkId] = 0;
    }
    for(moduleId in moreModules) {
        if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
            modules[moduleId] = moreModules[moduleId];
        }
    }
    if(parentJsonpFunction) parentJsonpFunction(data);
    while(resolves.length) {
        resolves.shift()();
    }
};
复制代码

在 webpackJsonpCallback 中会将 0.js 中的 chunks 和 modules 保存到全局的 modules 变量中,并设置 installedChunks 的标志位。

有两点需要详细说明的:

  1. 我们知道 import('xxx.js') 会返一个 Promise 实例 promise,在 webpack 打包出来的最终文件中是如何处理这个 promise 的?

    在加载 0.js 之前会在全局 installedChunks 中先存入了一个 promise 对象

    installedChunks[chunkId] = [resolve, reject, promise]
    复制代码

    resolve 这个值在 webpackJsonpCallback 中会被用到,这时就会进入到我们写的 import('./c.js').then() 的 then 语句中了。

  2. 在 main.js 中处理 webpackJsonp 过程中还有一段特殊的逻辑:

    jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
    var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
    ...
    jsonpArray = jsonpArray.slice();
    for(var i = 0; i < jsonpArray.length; i++)  webpackJsonpCallback(jsonpArray[i]);
    var parentJsonpFunction = oldJsonpFunction;
    复制代码

    也就是说如果之前已经存在全局的 window["webpackJsonp"] 那么在替换其 push 函数之前会将原有的 push 方法保存为 oldJsonpFunction,同时将已存在于 window["webpackJsonp"] 中的内容,一一执行 webpackJsonpCallback 。并且在 webpackJsonpCallback 中也将异步加载的内容也会在 parentJsonpFunction 中同样执行一次

    if(parentJsonpFunction) parentJsonpFunction(data);
    复制代码

    这样的同步意义何在?试想下面的场景,webpack 中多入口情况下,例如如下配置

    {
     entry: {
       bundle1: 'bundle1.js',
       bundle2: 'bundle2.js'
     }
    }
    复制代码

    并且 bundle1 和 bundle2 中都用到了异步加载了 0.js。而且在同一个页面中同时加载了 bundle1 和 bundle2。那么由于上面的逻辑,执行的流程如下图:

    webpack系列之一总览

    通过上图可以看到,这样设计对于多入口的地方,可以将 bundle1.js 和 bundle2.js 中异步模块进行同步,这样不仅保证了 0.js 可以同时在两个文件中被引用,而且不会重复加载。

异步加载中,有两个需要注意的地方:

  • Promise

    在 webpack 异步加载使用了 Promise。要兼容低版本的安卓,比如4.x 的代码来说,需要有全局的 Promise polyfill。

  • window["webpackJsonp"]

    如果一个 HTML 页面中,会加载多个 webpack 独立打包出来的文件。那么这些文件异步加载的回调函数,默认都叫 "webpackJonsp",会相互冲突。需要通过 output.jsonpFunction 配置修改这个默认的函数名称。

webpack 编译总流程

知道上面的产出,根据产出看 webpack 的总流程。这里我们暂时不考虑 webpack 的缓存、错误处理、watch 等逻辑,只看主流程。 首先会有一个入口文件写在配置文件中,确定 webpack 从哪个文件开始处理。

step1 webpack 配置文件处理

我们在写配置文件中 entry 的时候,肯定写过 ./main.js 这时一个相对目录,所以会有一个将相对目录变成绝对目录的处理

step2 文件位置解析

webpack 需要从入口文件开始,顺藤摸瓜找到所有的文件。那么会有一个

step3 加载文件 step4 文件解析 step5 从解析结果中找到文件引入的其他文件

在加载文件的时候,我们会在 webpack 中配置很多的 loaders 来处理 js 文件的 babel 转化等等,还应该有文件对应的 loader 解析,loader 执行。

step3.1 找到所有对应的 loader,然后逐个执行

处理完整入口文件之后,得到依赖的其他文件,递归进行处理。最后得到了所有文件的 module 。最终输出的是打包完成的 bundle 文件。所以会有

step4 module 合并成 chunk 中 输出最终文件

根据 webpack 的使用和结果,我们猜测了一下 webpack 中大概的流程。然后看一下 webpack 的源码,并和我们脑中的流程对比一下。实际的 webpack 流程图如下:

webpack系列之一总览

对整体框架和流程有了大致的概念之后,我们可以将源码拆分为一部分一部分来详细阅读。后续会通过一系列文章一一介绍:

  • 底层 Tapable 介绍

    webpack 的底层使用的 Tapable 用来处理各种类型的 hook。这部分主要介绍 Tapable 原理。

  • reslove 过程

    webpack 中我们所写的各种相对路径/绝对路径,alias 等是如何被处理,最终找到正确的执行文件的。

  • loaders 处理

    写在 webpack 配置中,各种 loaders 如何被加载、解析

  • module 生成

    js文件如何被解析,分析出依赖,同时递归处理所有的依赖

  • chunk 生成

    项目中各个文件之间的依赖图的生成,以及根据定义的规则,module 最终如何聚合为 chunk。

  • 最终文件的生成

    经历了上面的所有过程后,内存中保存了生成文件的各种信息,这些信息如何整合吐出最终真正执行的所有文件。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

结网

结网

王坚 / 人民邮电出版社 / 2010-4 / 55.00元

本书作者一直从事互联网产品的研究和实战,经验丰富,同时作为导师,指导了大量优秀的产品经理,本书的内容也是作者8年来培养产品经理新兵的经验集萃。如果你缺乏培养产品经理的教材,本书正好总结了产品经理知识体系,无疑是你很好的选择。 本书覆盖了相当全面的互联网知识,对于想要了解互联网行业或想要借助互联网进行营销的人来说,都是很好的入门读物。 本书并不是一本完善的互联网创业指南,而是写给胸怀互联......一起来看看 《结网》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

随机密码生成器
随机密码生成器

多种字符组合密码

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试