当前位置: 首页 > Web前端 > HTML5

探索webpack运行时

时间:2023-04-05 00:58:14 HTML5

前言本文推荐自己尝试。最近在研究webpackruntime的源码。在这篇文章中,我记录了我对webpack复杂玩具方法的探索,并以图形的形式记录了webpack运行时的过程。我们在讨论什么?本文主要记录webpack打包文件后如何在浏览器中加载和解析文件的过程。webpack的方法太复杂了。为了避免额外的干扰,我使用最小化的实例来探索webpack是如何加载和解析模块文件的。具体手段:先准备几个很简单的js文件,再准备它们对应的webpack配置文件。一个一个测试之后,观察他们的输出,阅读他们的源代码,然后得出结论。webpack运行时的简单总结,和nodejs在解析同步模块时的规则完全一致。什么意思,每个模块在运行之前都会被一个函数包裹起来:(function(module,exports,__webpack_require__){//dosomething})看起来和nodejs一样,工作起来也一样。这里当前模块的导出模块和导入模块为:exports_webpack_require_,当需要一个模块时,webpack会执行该函数获取其对应的exports对象。注意:我们需要的是模块执行后的exports对象,而不是模块功能。在webpack中,我们可以使用ESM规范和commonjs规范来加载模块。无论使用哪种规范,实际上都可以这样使用。总结一下,因为这两种常见的方式存在相同的导入和导出的概念,所以webpack统一了这两种形式的包装,消除了差异。对于异步模块,这里有两种情况是异步的:文件的加载顺序不对,入口文件可能最后使用import()语法加载,导入的模块。现在你需要知道的是,webpack运行时完全不依赖于文件加载顺序,无论文件加载顺序是哪种方式,webpack都能轻松处理。import()语法通常用于代码切割或延迟加载。在这种情况下,webpack使用脚本导入打包后的文件,然后使用Promise语法进行异步处理(后面会有进一步的讨论)。取决于“webpack":"^4.31.0","webpack-cli":"^3.3.2"只需要webpack本身,只有自己尝试过才能理解透彻。练习同步模块——不分离runtime在console.log('helloworld')里面提供一个index.js,然后打包检测webpack.config.js:{//Omittedexportmode:'development',entry:{app:'./src/index.js',},output:{filename:'[name].js'},devtool:'hidden-source-map',//这样做生成的代码注释比较少,不是针对sourceMap}webpack默认会输出一个app.js,只有100行(这还是在无用注释的情况下),打开文件最后会发现一个IIFE函数,它包含两部分:runtime本身,也就是IIFE函数体模块的内容,以及IIFE函数的参数。注意:其实IIFE函数内部是有一些冗余代码的,这些冗余代码其实是针对特殊情况和异步情况下准备的,所以不用太担心看不懂。有的内容结合更多的后续分析,看起来很简单。在引入同步时,嵌入入口文件的模块id会显示在runtimebody内部,当前配置webpack使用该文件路径作为模块的唯一id。当IIFE函数执行到最后,webpack会以这个id为起点解析执行模块。IIFE函数的参数:{"./src/index.js":(function(module,exports){console.log('helloworld')})}图:执行过程分析同步模块-没有运行时分离-导入和执行现在我们在简单运行代码的模块index.js中添加一个导出,然后观察打包后的文件会发生什么变化:{"./src/index.js":(function(module,__webpack_exports__,__webpack_require__){"使用严格";__webpack_require__.r(__webpack_exports__);__webpack_require__.d(__webpack_exports__,"echo",function(){returnecho;});constecho=()=>{console.log('你好世界');}回声();})}果然,打包后的内容只有IIFE函数的参数发生了变化。让我们仔细看看,发现还有两个函数调用。这是什么鬼:__webpack_require__.r用于exports添加描述标识这个模块是一个ESM模块__webpack_require__.d用于定义模块exports。与直接向导出添加属性不同,使用此函数定义的属性是不可变的。同步模块——运行时不分离——多文件现在我们在代码中添加import和export来测试webpack的多个同步模块是如何被引用的。我另外创建了一个文件demo.js,负责导出一个函数,而原来的index.js导入这个函数后,执行这个函数。构造后明显的变化就是IIFE函数的参数内容多了。这里多出来的内容是因为新增了一个demo.js。{"./src/demo.js":(function(module,__webpack_exports__,__webpack_require__){"usestrict";__webpack_require__.r(__webpack_exports__);__webpack_require__.d(__webpack_exports__,"demo",function(){});constdemo=()=>{console.log('helloworld')}}),"./src/index.js":(function(module,__webpack_exports__,__webpack_require__){"使用严格";__webpack_require__.r(__webpack_exports__);var_demo__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__(/*!./demo*/"./src/demo.js");();})}这里提醒一下webpackruntime提供的不同函数的功能:__webpack_require__顾名思义就是用来导入其他模块的。__webpack_require__.r用于向导出添加描述。该模块是ESM模块。定义模块导出不同于直接向导出添加属性。使用此函数定义的属性是不可变的。然后在moduleid./src/index.js中使用导入的模块,也就是__webpack_require__。/src/demo.js中导出的模块是__webpack_require__.d。图:执行流程分析异步模块-运行时分离这里我们将运行时与demo.js和index.js分离。与上一步相比,我们需要修改配置文件:{mode:'development',entry:{app:'./src/index.js',},output:{filename:'[name].js'},devtool:'hidden-source-map',//这样做生成的代码注释比较少,不是为了sourceMap优化:{runtimeChunk:{name:'runtime'//单独运行时},}}在这time,runtime被分离到一个单独的文件中,而demo.js和index.js构成一个名为app.js的块。我们知道,当浏览器加载一个文件时,默认会在解析完HTML中所有的script标签后执行。也就是说,javascript在浏览器中的执行会受到script标签顺序的影响。很明显,app.js的执行依赖于runtime.js,那么如果违反了加载顺序,能正常执行吗?答:可以在webpack运行时作为最后一个文件加载,也就是说不受加载顺序和是否同步加载的影响。此时输出的runtime.js代码比同步版多了一半的代码。这些代码用于处理多个文件直接加载的Processing。总而言之,之前的多文件同步加载版本只有模块的概念(模块是全局唯一的)。当有多个文件时,我们会将文件拆分成块。此时,模块属于chunk的内容。(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["app"],//块名称{//模块"./src/demo.js":(函数(module,__webpack_exports__,__webpack_require__){"usestrict";__webpack_require__.r(__webpack_exports__);__webpack_require__.d(__webpack_exports__,"demo",function(){returndemo;});constdemo=()=>{控制台.log('helloworld')}}),"./src/index.js":(function(module,__webpack_exports__,__webpack_require__){"usestrict";__webpack_require__.r(__webpack_exports__);var_demo__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__(/*!./demo*/"./src/demo.js");对象(_demo__WEBPACK_IMPORTED_MODULE_0__[“演示”])();})},[["./src/index.js","runtime"]]//第二个列表项后面的内容就是这个chunk所依赖的chunk]);那么webpack在文件顺序加载乱序而内容能正常执行的情况下如何运行呢?webpack运行时内部维护创建了一个数组变量,挂载在window对象上:window["webpackJsonp"]=[]不管是runtime还是普通chunk,都会在IIFE函数中尝试读取这个属性,如果是不读,就会给它赋值一个数组。(window["webpackJsonp"]=window["webpackJsonp"]||[]).push(xxx)//在runtime中,每个chunk都有一个runtime。如果读取数组,则意味着在加载运行时之前加载了其他块。这时候runtime只需要读取数组的内容然后加载chunk再解析就OK了(实际运行时会修改数组对象改变其行为,后面的chunk调用其实就是运行时的块分析功能)。图片:执行过程分析异步模块-import()语法import()语法才是webpack中真正的异步模块。当你使用import()的语法来延迟加载模块时,运行时会提供一些包装。是的,我们运行时的代码增加了。为了增加难度,我们增加了一个额外的文件,不过这是最后一节了,别担心,难度不会再提高了。demo1.js导出一个函数。demo.js在demo1.js中导入导出的函数并再次导出。index.js使用import()语法将导出的内容导入到demo.js中。//demo1.jsexportconstecho=()=>{console.log('helloworld')}//demo.jsexport{echo}from"./demo1";//index.jsimport(/*webpackChunkName:"demo"*/'./demo').then(({echo})=>{echo();});让我们看看app.js做了什么:(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["app"],{"./src/index.js":(function(module,exports,__webpack_require__){__webpack_require__.e(/*!import()|demo*/"demo").then(__webpack_require__.bind(null,/*!./demo*/"./src/demo.js")).then(({echo})=>{echo();})})},[["./src/index.js","runtime"]]]);这里导入的模块使用了__webpack_require__.e。这个API以前是不存在的。然后是两个。第一个是获取依赖模块的export,第二个是使用Dependencyexecution.__webpack_require__.e主要完成以下事情:./index.js是入口模块runtime会先执行相应的函数,此时`调用__webpack_require__.e在内部创建一个Promise,并解析Promise和reject,包括这个Promise返回的对象挂载到内部的chunkcache中。根据提供的名称和运行时的publicPath,在内部将url拼接到script标签,向服务器发起脚本请求。监听script标签的完成事件和失败事件,之后会下载chunk->执行,调用window['webpackJsonp'].push方法,push方法会在内部读取chunk缓存,遇到Promise就执行它的resolve。resolve之后,then被调用,调用之前我们的chunk已经解析完毕。这时候我们可以使用__webpack_require__来获取模块,然后在second中读取模块提供的内容。图:执行过程分析