【Webpack 进阶】Webpack 打包后的代码是怎样的?

【Webpack 进阶】Webpack 打包后的代码是怎样的?

webpack是我们现阶段要掌握的重要的打包工具之一,我们知道webpack会递归的构建依赖关系图,其中包含应用程序的每个模块,然后将这些模块打包成一个或者多个bundle

那么webpack打包后的代码是怎样的呢?是怎么将各个bundle连接在一起的?模块与模块之间的关系是怎么处理的?动态import()的时候又是怎样的呢?

本文让我们一步步来揭开webpack打包后代码的神秘面纱

准备工作

创建一个文件,并初始化

mkdir learn-webpack-output
cd learn-webpack-output
npm init -y 
yarn add webpack webpack-cli -D复制代码

根目录中新建一个文件webpack.config.js,这个是webpack默认的配置文件

constpath =require('path');module.exports = {mode:'development',// 可以设置为 production// 执行的入口文件entry:'./src/index.js',output: {// 输出的文件名filename:'bundle.js',// 输出文件都放在 distpath: path.resolve(__dirname,'./dist')
  },// 为了更加方便查看输出devtool:'cheap-source-map'}复制代码

然后我们回到package.json文件中,在npm script中添加启动webpack配置的命令

"scripts": {"test":"echo \"Error: no test specified\" && exit 1","build":"webpack"}复制代码

新建一个src文件夹,新增index.js文件和sayHello文件

// src/index.jsimportsayHellofrom'./sayHello';console.log(sayHello, sayHello('Gopal'));复制代码
// src/sayHello.jsfunctionsayHello(name){return`Hello${name}`;
}exportdefaultsayHello;复制代码

一切准备完毕,执行yarn build

分析主流程

看输出文件,这里不放具体的代码,有点占篇幅,可以点击这里查看

其实就是一个 IIFE

莫慌,我们一点点拆分开看,其实总体的文件就是一个IIFE——立即执行函数。

(function(modules){// webpackBootstrap// The module cachevarinstalledModules = {};function__webpack_require__(moduleId){// ...省略细节}// 入口文件return__webpack_require__(__webpack_require__.s ="./src/index.js");
})
({"./src/index.js": (function(module, __webpack_exports__, __webpack_require__){}),"./src/sayHello.js": (function(module, __webpack_exports__, __webpack_require__){})
});复制代码

函数的入参modules是一个对象,对象的key就是每个js模块的相对路径,value就是一个函数(我们下面称之为模块函数)。IIFE会先require入口模块。即上面就是./src/index.js

// 入口文件return__webpack_require__(__webpack_require__.s ="./src/index.js");复制代码

然后入口模块会在执行时require其他模块例如./src/sayHello.js"以下为简化后的代码,从而不断的加载所依赖的模块,形成依赖树,比如如下的模块函数中就引用了其他的文件sayHello.js

{"./src/index.js": (function(module, __webpack_exports__, __webpack_require__){ 
    __webpack_require__.r(__webpack_exports__);var_sayHello__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/sayHello.js");console.log(_sayHello__WEBPACK_IMPORTED_MODULE_0__["default"],Object(_sayHello__WEBPACK_IMPORTED_MODULE_0__["default"])('Gopal'));
  })
}复制代码

重要的实现机制——__webpack_require__

这里去require其他模块的函数主要是__webpack_require__。接下来主要介绍一下__webpack_require__这个函数

// 缓存模块使用varinstalledModules = {};// The require function// 模拟模块的加载,webpack 实现的 requirefunction__webpack_require__(moduleId){// Check if module is in cache// 检查模块是否在缓存中,有则直接从缓存中获取if(installedModules[moduleId]) {returninstalledModules[moduleId].exports;
    }// Create a new module (and put it into the cache)// 没有则创建并放入缓存中,其中 key 值就是模块 Id,也就是上面所说的文件路径varmodule= installedModules[moduleId] = {i: moduleId,// Module IDl:false,// 是否已经执行exports: {}
    };// Execute the module function// 执行模块函数,挂载到 module.exports 上。this 指向 module.exportsmodules[moduleId].call(module.exports,module,module.exports, __webpack_require__);// Flag the module as loaded// 标记这个 module 已经被加载module.l =true;// Return the exports of the module// module.exports通过在执行module的时候,作为参数存进去,然后会保存module中暴露给外界的接口,如函数、变量等returnmodule.exports;
  }复制代码

第一步,webpack这里做了一层优化,通过对象installedModules进行缓存,检查模块是否在缓存中,有则直接从缓存中获取,没有则创建并放入缓存中,其中key值就是模块Id,也就是上面所说的文件路径

第二步,然后执行模块函数,将module,module.exports,__webpack_require__作为参数传递,并把模块的函数调用对象指向module.exports,保证模块中的this指向永远指向当前的模块。

第三步,最后返回加载的模块,调用方直接调用即可。

所以这个__webpack_require__就是来加载一个模块,并在最后返回模块module.exports变量

webpack 是如何支持 ESM 的

可能大家已经发现,我上面的写法是ESM的写法,对于模块化的一些方案的了解,可以看看我的另外一篇文章【面试说】Javascript 中的 CJS, AMD, UMD 和 ESM是什么?

我们重新看回模块函数

{"./src/index.js": (function(module, __webpack_exports__, __webpack_require__){ 
    __webpack_require__.r(__webpack_exports__);var_sayHello__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/sayHello.js");console.log(_sayHello__WEBPACK_IMPORTED_MODULE_0__["default"],Object(_sayHello__WEBPACK_IMPORTED_MODULE_0__["default"])('Gopal'));
  })
}复制代码

我们看看__webpack_require__.r函数

__webpack_require__.r =function(exports){
 object.defineProperty(exports,'__esModule', {value:true});
};复制代码

就是为__webpack_exports__添加一个属性__esModule,值为true

再看一个__webpack_require__.n的实现

// getDefaultExport function for compatibility with non-harmony modules__webpack_require__.n =function(module){vargetter =module&&module.__esModule ?functiongetDefault(){returnmodule['default']; } :functiongetModuleExports(){returnmodule; };
  __webpack_require__.d(getter,'a', getter);returngetter;
};复制代码

__webpack_require__.n会判断module是否为es模块,当__esModule为 true 的时候,标识 module 为es 模块,默认返回module.default,否则返回module

最后看__webpack_require__.d,主要的工作就是将上面的getter函数绑定到 exports 中的属性 a 的getter

// define getter function for harmony exports__webpack_require__.d =function(exports, name, getter){if(!__webpack_require__.o(exports, name)) {Object.defineProperty(exports, name, {configurable:false,enumerable:true,get: getter
		});
	}
};复制代码

我们最后再看会sayHello.js打包后的模块函数,可以看到这里的导出是__webpack_exports__["default"],实际上就是__webpack_require__.n做了一层包装来实现的,其实也可以看出,实际上webpack是可以支持CommonJSES Module一起混用的

"./src/sayHello.js":/*! exports provided: default */(function(module, __webpack_exports__, __webpack_require__){"use strict";
  __webpack_require__.r(__webpack_exports__);functionsayHello(name){return`Hello${name}`;
  }/* harmony default export */__webpack_exports__["default"] = (sayHello);
 })复制代码

目前为止,我们大致知道了webpack打包出来的文件是怎么作用的了,接下来我们分析下代码分离的一种特殊场景——动态导入

动态导入

代码分离是webpack中最引人注目的特性之一。此特性能够把代码分离到不同的bundle中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。

常见的代码分割有以下几种方法:

  • 入口起点:使用entry配置手动地分离代码。
  • 防止重复:使用Entry dependencies或者SplitChunksPlugin去重和分离 chunk。
  • 动态导入:通过模块的内联函数调用来分离代码。

本文我们主要看看动态导入,我们在src下面新建一个文件another.js

functionAnother(){return'Hi, I am Another Module';
}export{ Another };复制代码

修改index.js

importsayHellofrom'./sayHello';console.log(sayHello, sayHello('Gopal'));// 单纯为了演示,就是有条件的时候才去动态加载if(true) {import('./Another.js').then(res=>console.log(res))
}复制代码

我们来看下打包出来的内容,忽略 .map 文件,可以看到多出一个0.bundle.js文件,这个我们称它为动态加载的chunkbundle.js我们称为主chunk

输出的代码的话,主chunk这里,动态加载的chunk这里,下面是针对这两份代码的分析

主 chunk 分析

我们先来看看主chunk

内容多了很多,我们来细看一下:

首先我们注意到,我们动态导入的地方编译后变成了以下,这是看起来就像是一个异步加载的函数

if(true) {
  __webpack_require__.e(/*! import() */0).then(__webpack_require__.bind(null,/*! ./Another.js */"./src/Another.js")).then(res=>console.log(res))
}复制代码

所以我们来看__webpack_require__.e这个函数的实现

__webpack_require__.e——使用 JSONP 动态加载

// 已加载的chunk缓存varinstalledChunks = {"main":0};// ...__webpack_require__.e =functionrequireEnsure(chunkId){// promises 队列,等待多个异步 chunk 都加载完成才执行回调varpromises = [];// JSONP chunk loading for javascriptvarinstalledChunkData = installedChunks[chunkId];// 0 代表已经 installedif(installedChunkData !==0) {// 0 means "already installed".// a Promise means "currently loading".// 目标chunk正在加载,则将 promise push到 promises 数组if(installedChunkData) {
      promises.push(installedChunkData[2]);
    }else{// setup Promise in chunk cache// 利用Promise去异步加载目标chunkvarpromise =newPromise(function(resolve, reject){// 设置 installedChunks[chunkId]installedChunkData = installedChunks[chunkId] = [resolve, reject];
      });// i设置chunk加载的三种状态并缓存在 installedChunks 中,防止chunk重复加载// nstalledChunks[chunkId]  = [resolve, reject, promise]promises.push(installedChunkData[2] = promise);// start chunk loading// 使用 JSONPvarhead =document.getElementsByTagName('head')[0];varscript =document.createElement('script');

      script.charset ='utf-8';
      script.timeout =120;if(__webpack_require__.nc) {
        script.setAttribute("nonce", __webpack_require__.nc);
      }// 获取目标chunk的地址,__webpack_require__.p 表示设置的publicPath,默认为空串script.src = __webpack_require__.p +""+ chunkId +".bundle.js";// 请求超时的时候直接调用方法结束,时间为 120 svartimeout =setTimeout(function(){
        onScriptComplete({type:'timeout',target: script });
      },120000);
      script.onerror = script.onload = onScriptComplete;// 设置加载完成或者错误的回调functiononScriptComplete(event){// avoid mem leaks in IE.// 防止 IE 内存泄露script.onerror = script.onload =null;clearTimeout(timeout);varchunk = installedChunks[chunkId];// 如果为 0 则表示已加载,主要逻辑看 webpackJsonpCallback 函数if(chunk !==0) {if(chunk) {varerrorType = event && (event.type ==='load'?'missing': event.type);varrealSrc = event && event.target && event.target.src;varerror =newError('Loading chunk '+ chunkId +' failed.\n('+ errorType +': '+ realSrc +')');
            error.type = errorType;
            error.request = realSrc;
            chunk[1](error);
          }
          installedChunks[chunkId] =undefined;
        }
      };
      head.appendChild(script);
    }
  }returnPromise.all(promises);
};复制代码
  • 可以看出将import()转换成模拟JSONP去加载动态加载的chunk文件

  • 设置chunk加载的三种状态并缓存在installedChunks中,防止chunk重复加载。这些状态的改变会在webpackJsonpCallback中提到

    // 设置 installedChunks[chunkId]installedChunkData = installedChunks[chunkId] = [resolve, reject];复制代码
    • installedChunks[chunkId]0,代表该chunk已经加载完毕
    • installedChunks[chunkId]undefined,代表该chunk加载失败、加载超时、从未加载过
    • installedChunks[chunkId]Promise对象,代表该chunk正在加载

看完__webpack_require__.e,我们知道的是,我们通过 JSONP 去动态引入chunk文件,并根据引入的结果状态进行处理,那么我们怎么知道引入之后的状态呢?我们来看异步加载的chunk是怎样的

异步 Chunk

// window["webpackJsonp"] 实际上是一个数组,向中添加一个元素。这个元素也是一个数组,其中数组的第一个元素是chunkId,第二个对象,跟传入到 IIFE 中的参数一样(window["webpackJsonp"] =window["webpackJsonp"] || []).push([[0],{/***/"./src/Another.js":/***/(function(module, __webpack_exports__, __webpack_require__){"use strict";
  __webpack_require__.r(__webpack_exports__);/* harmony export (binding) */__webpack_require__.d(__webpack_exports__,"Another",function(){returnAnother; });functionAnother(){return'Hi, I am Another Module';
  }/***/})
  
  }]);//# sourceMappingURL=0.bundle.js.map复制代码

主要做的事情就是往一个数组window['webpackJsonp']中塞入一个元素,这个元素也是一个数组,其中数组的第一个元素是chunkId,第二个对象,跟主chunk中 IIFE 传入的参数类似。关键是这个window['webpackJsonp']在哪里会用到呢?我们回到主chunk中。在return __webpack_require__(__webpack_require__.s = "./src/index.js");进入入口之前还有一段

varjsonpArray =window["webpackJsonp"] =window["webpackJsonp"] || [];// 保存原始的 Array.prototype.push 方法varoldJsonpFunction = jsonpArray.push.bind(jsonpArray);// 将 push 方法的实现修改为 webpackJsonpCallback// 这样我们在异步 chunk 中执行的 window['webpackJsonp'].push 其实是 webpackJsonpCallback 函数。jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();// 对已在数组中的元素依次执行webpackJsonpCallback方法for(vari =0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);varparentJsonpFunction = oldJsonpFunction;复制代码

jsonpArray就是window["webpackJsonp"],重点看下面这一句代码,当执行push方法的时候,就会执行webpackJsonpCallback,相当于做了一层劫持,也就是执行完 push 操作的时候就会调用这个函数

jsonpArray.push = webpackJsonpCallback;复制代码

webpackJsonpCallback ——加载完动态 chunk 之后的回调

我们再来看看webpackJsonpCallback函数,这里的入参就是动态加载的chunkwindow['webpackJsonp']push 进去的参数。

varinstalledChunks = {"main":0};functionwebpackJsonpCallback(data){// window["webpackJsonp"] 中的第一个参数——即[0]varchunkIds = data[0];// 对应的模块详细信息,详见打包出来的 chunk 模块中的 push 进 window["webpackJsonp"] 中的第二个参数varmoreModules = data[1];// add "moreModules" to the modules object,// then flag all "chunkIds" as loaded and fire callbackvarmoduleId, chunkId, i =0, resolves = [];for(;i < chunkIds.length; i++) {
    chunkId = chunkIds[i];// 所以此处是找到那些未加载完的chunk,他们的value还是[resolve, reject, promise]// 这个可以看 __webpack_require__.e 中设置的状态// 表示正在执行的chunk,加入到 resolves 数组中if(installedChunks[chunkId]) {
      resolves.push(installedChunks[chunkId][0]);
    }// 标记成已经执行完installedChunks[chunkId] =0;
  }// 挨个将异步 chunk 中的 module 加入主 chunk 的 modules 数组中for(moduleIdinmoreModules) {if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
      modules[moduleId] = moreModules[moduleId];
    }
  }// parentJsonpFunction: 原始的数组 push 方法,将 data 加入 window["webpackJsonp"] 数组。if(parentJsonpFunction) parentJsonpFunction(data);// 等到 while 循环结束后,__webpack_require__.e 的返回值 Promise 得到 resolve// 执行 resolovewhile(resolves.length) {
    resolves.shift()();
  }
};复制代码

当我们JSONP去加载异步chunk完成之后,就会去执行window["webpackJsonp"] || []).push,也就是webpackJsonpCallback。主要有以下几步

  • 遍历要加载的 chunkIds,找到未执行完的 chunk,并加入到 resolves 中
for(;i < chunkIds.length; i++) {
  chunkId = chunkIds[i];// 所以此处是找到那些未加载完的chunk,他们的value还是[resolve, reject, promise]// 这个可以看 __webpack_require__.e 中设置的状态// 表示正在执行的chunk,加入到 resolves 数组中if(installedChunks[chunkId]) {
    resolves.push(installedChunks[chunkId][0]);
  }// 标记成已经执行完installedChunks[chunkId] =0;
}复制代码
  • 这里未执行的是非 0 状态,执行完就设置为0

  • installedChunks[chunkId][0]实际上就是 Promise 构造函数中的 resolve

    // __webpack_require__.evarpromise =newPromise(function(resolve, reject){
    	installedChunkData = installedChunks[chunkId] = [resolve, reject];
    });复制代码
  • 挨个将异步chunk中的module加入主chunkmodules数组中

  • 原始的数组push方法,将data加入window["webpackJsonp"]数组

  • 执行各个resolves方法,告诉__webpack_require__.e中回调函数的状态

只有当这个方法执行完成的时候,我们才知道JSONP成功与否,也就是script.onload/onerror会在webpackJsonpCallback之后执行。所以onload/onerror其实是用来检查webpackJsonpCallback的完成度:有没有将installedChunks中对应的chunk值设为 0

动态导入小结

大致的流程如下图所示

流程图

总结

本篇文章分析了webpack打包主流程以及和动态加载情况下输出代码,总结如下

  • 总体的文件就是一个IIFE——立即执行函数
  • webpack会对加载过的文件进行缓存,从而优化性能
  • 主要是通过__webpack_require__来模拟import一个模块,并在最后返回模块export的变量
  • webpack是如何支持ES Module
  • 动态加载import()的实现主要是使用JSONP动态加载模块,并通过webpackJsonpCallback判断加载的结果

  • 作者:Gopal
    链接:https://juejin.cn/post/6937086236926410783
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

      IT.互联网   技术

    作者  :  金韦曲

    志在峰巅的攀登者,不会陶醉在沿途的某个脚印之中




    小程序

    面试一点通

    创作中心

    分享职场知识、帮助到更多的人

    软件开发

    承接、各行各业、软件开发需求

    最新发布