生成式AI领域的最新成果都在这里!抢 QCon 展区门票 了解详情
写点什么

webpack 到底对我们的代码做了什么

  • 2019-09-18
  • 本文字数:4312 字

    阅读完需:约 14 分钟

webpack到底对我们的代码做了什么

webpack 对于每个前端儿来说都不陌生,它将每个静态文件当成一个模块,经过一系列的处理为我们整合出最后的需要的 js,css,图片,字体等文件。你有没有好奇过 webpack 是如何工作的?

这个系列将带大家深入 webpack 源码内部,剖析它的是如何解析资源,如何使用各种不同的装载机来处理各种资源又是如何将各个资源链接起来,打包成最终的文件的。希望通过这个系列文章,能带给大家对 webpack 机制有更深刻的认识,进而更好的辅助大家对 webpack 的使用。


1。写在前面的话

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


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


// main.jsimport { A } from './a'import B from './b'console.log(A)B()
复制代码


// a.jsexport const A = 'a'
复制代码


// b.jsexport 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 最后被编译成三个函数(下文将这三个函数称为模函数),关键是文件的相对路径.bundle.js 会执行到__webpack_require__(webpack_require.s = “./demo01/main.js”);即通过__webpack_require__(’./demo01/main.js’)开始主入口函数的执行。


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


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,module.exports,webpack_require。module.exports 用来收集模块中所有的出口 XXX。看” ./demo/a.js“的模块。


(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 = 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 中增加一个默认属性。


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


最后导入 B 来自’./b.js 编译为


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

2。异步加载

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


// c.jsexport 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(){...}}])
复制代码


执行导入(’./ c.js’)时,实际上通过在 HTML 中插入一个脚本标签加载 0.js.0.js 加载后会执行窗口[“webpackJsonp”]。推方法。在 main.js 在还有一段:


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


这里篡改了一下,window [“webpackJsonp”]的推方法,将推方法外包装了一层 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 保存到全局的模块变量中,并设置 installedChunks 的标志位。


有两点需要详细说明的:

1。

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


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


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


解决这个值在 webpackJsonpCallback 中会被用到,这时就会进入到我们写的导入(’./ c.js’)。然后() 的然后语句中了。

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;
复制代码


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


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


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


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


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



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


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


  • 诺言


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


  • 窗口[ “webpackJsonp”]


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

3。webpack 编译总流程

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


  • step1:webpack 配置文件处理


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


  • step2 : 文件位置解析


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


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


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


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


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


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


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



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


原文链接:


https://mp.weixin.qq.com/s/mNuhgXsCuyOAFk8q5bnOtg


2019-09-18 23:222450

评论

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

Apache APISIX 3.2.0 LTS 正式发布

API7.ai 技术团队

Java 8 的异步利器:CompletableFuture源码解析(建议精读)

Java你猿哥

Java ssm java8 源码解析

pytest学习和使用1-pytest安装和版本查看

Python 自动化测试 pytest

武汉等保测评有限公司有哪几家?具体位于哪里?

行云管家

等保 等保测评 等保2.0 武汉

将TiDB各服务组件混布到物理机集群和K8S环境

TiDB 社区干货传送门

实践案例 集群管理 管理与运维 安装 & 部署 数据库架构设计

TiDB SQL调优案例之避免TiFlash帮倒忙

TiDB 社区干货传送门

性能调优 实践案例 故障排查/诊断

快速尝鲜:RabbitMQ 搭建完就得用起来

Java你猿哥

Java Spring Boot ssm RabbitMQ

【分布式技术专题】「分布式技术架构」一文带你厘清分布式事务协议及分布式一致性协议的算法原理和核心流程机制(Paxos篇)

洛神灬殇

分布式 PAXOS paxos协议 算法分析

GuavaCache与物模型大对象引起的内存暴涨分析——设备管理运维类

阿里云AIoT

缓存 算法 监控 物联网 数据格式

深入理解spring mvc启动过程与原理

三十而立

Java spring 程序员 开发 IT

1个案例读懂——游戏产品如何用A/B测试做增长

字节跳动数据平台

云服务 AB testing实战 A/B测试 企业号 3 月 PK 榜

阿里云AIoT物联网平台如何实现设备全球就近接入——设备接入类

阿里云AIoT

运维 监控 物联网 中间件 数据采集

文盘Rust -- 安全连接 TiDB/Mysql

TiDB 社区干货传送门

开发语言

技术详解 阿里云AIoT物模型支撑设备规模已超亿级——设备管理运维类

阿里云AIoT

运维 安全 监控 物联网 芯片

国家高新技术企业是国企吗?获得高新企业证书有什么用?

行云管家

高新企业 高新技术

温湿度计设备通过阿里云IoT物联网套件上报数据到钉钉群机器人实践——数据价值类

阿里云AIoT

JavaScript Serverless 物联网 机器人 机器学习/深度学习

基于 Apache Flink 的实时计算数据流业务引擎在京东零售的实践和落地

Apache Flink

大数据 flink 实时计算

阿里120W年薪架构师力荐750页微服务架构深度解析笔记

程序知音

Java 微服务 编程语言 后端技术

有效载荷标识与内容类型--MQTT 5.0新特性

EMQ映云科技

物联网 IoT mqtt 企业号 3 月 PK 榜 有效载荷标识

PS 2023版本 24.2有哪些新功能?增加了哪些相机配置?

Rose

ps ps 2023 Photoshop 2023下载

监控告警处理之tidb_server_critical_error_total

TiDB 社区干货传送门

监控 故障排查/诊断

面试没有分库分表经验,就看这篇

三十而立

Java sql 程序员 IT 分库

BSN-DDC基础网络详解(五):接入DDC网络(2)

BSN研习社

BSN-DDC基础网络

手把手教你写spring boot starter

三十而立

Java 程序员 IT springboot boot

云数据库TiDB免费试用初体验

TiDB 社区干货传送门

版本测评 安装 & 部署

设计消息队列存储消息数据的 MySQL 表格

Geek_7d539e

NFTScan 与 UniPass 达成合作伙伴,双方在多链 NFT 数据方面展开合作!

NFT Research

NFT

TiCDC 源码解读(5)-- TiCDC DDL 事件处理逻辑 与 Filter 实现介绍

TiDB 社区干货传送门

TiCDC 源码解读

TiCDC 源码解读(6)- TiCDC Puller 模块介绍

TiDB 社区干货传送门

TiDB 源码解读 TiCDC 源码解读

Dr-autosync TiDB 集群的计划内和计划外切换验证步骤

TiDB 社区干货传送门

实践案例 集群管理 安装 & 部署 数据库架构选型 6.x 实践

pytest学习和使用2-初步使用和用例运行

Python 自动化测试 pytest

webpack到底对我们的代码做了什么_文化 & 方法_崔静_InfoQ精选文章