<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Webpack面試題

          共 11972字,需瀏覽 24分鐘

           ·

          2021-05-06 09:38


          前言

          在前端工程化日趨復(fù)雜的今天,模塊打包工具在我們的開(kāi)發(fā)中起到了越來(lái)越重要的作用,其中webpack就是最熱門的打包工具之一。

          說(shuō)到webpack,可能很多小伙伴會(huì)覺(jué)得既熟悉又陌生,熟悉是因?yàn)閹缀踉诿恳粋€(gè)項(xiàng)目中我們都會(huì)用上它,又因?yàn)?code style="">webpack復(fù)雜的配置和五花八門的功能感到陌生。尤其當(dāng)我們使用諸如umi.js之類的應(yīng)用框架還幫我們把webpack配置再封裝一層的時(shí)候,webpack的本質(zhì)似乎離我們更加遙遠(yuǎn)和深不可測(cè)了。

          當(dāng)面試官問(wèn)你是否了解webpack的時(shí)候,或許你可以說(shuō)出一串耳熟能詳?shù)?code style="">webpack loader和plugin的名字,甚至還能說(shuō)出插件和一系列配置做按需加載和打包優(yōu)化,那你是否了解他的運(yùn)行機(jī)制以及實(shí)現(xiàn)原理呢,那我們今天就一起探索webpack的能力邊界,嘗試了解webpack的一些實(shí)現(xiàn)流程和原理,拒做API工程師。

          CgqCHl6pSFmAC5UzAAEwx63IBwE024.png

          你知道webpack的作用是什么嗎?

          從官網(wǎng)上的描述我們其實(shí)不難理解,webpack的作用其實(shí)有以下幾點(diǎn):

          • 模塊打包??梢詫⒉煌K的文件打包整合在一起,并且保證它們之間的引用正確,執(zhí)行有序。利用打包我們就可以在開(kāi)發(fā)的時(shí)候根據(jù)我們自己的業(yè)務(wù)自由劃分文件模塊,保證項(xiàng)目結(jié)構(gòu)的清晰和可讀性。

          • 編譯兼容。在前端的“上古時(shí)期”,手寫(xiě)一堆瀏覽器兼容代碼一直是令前端工程師頭皮發(fā)麻的事情,而在今天這個(gè)問(wèn)題被大大的弱化了,通過(guò)webpackLoader機(jī)制,不僅僅可以幫助我們對(duì)代碼做polyfill,還可以編譯轉(zhuǎn)換諸如.less, .vue, .jsx這類在瀏覽器無(wú)法識(shí)別的格式文件,讓我們?cè)陂_(kāi)發(fā)的時(shí)候可以使用新特性和新語(yǔ)法做開(kāi)發(fā),提高開(kāi)發(fā)效率。

          • 能力擴(kuò)展。通過(guò)webpackPlugin機(jī)制,我們?cè)趯?shí)現(xiàn)模塊化打包和編譯兼容的基礎(chǔ)上,可以進(jìn)一步實(shí)現(xiàn)諸如按需加載,代碼壓縮等一系列功能,幫助我們進(jìn)一步提高自動(dòng)化程度,工程效率以及打包輸出的質(zhì)量。

          說(shuō)一下模塊打包運(yùn)行原理?

          如果面試官問(wèn)你Webpack是如何把這些模塊合并到一起,并且保證其正常工作的,你是否了解呢?

          首先我們應(yīng)該簡(jiǎn)單了解一下webpack的整個(gè)打包流程:

          • 1、讀取webpack的配置參數(shù);
          • 2、啟動(dòng)webpack,創(chuàng)建Compiler對(duì)象并開(kāi)始解析項(xiàng)目;
          • 3、從入口文件(entry)開(kāi)始解析,并且找到其導(dǎo)入的依賴模塊,遞歸遍歷分析,形成依賴關(guān)系樹(shù);
          • 4、對(duì)不同文件類型的依賴模塊文件使用對(duì)應(yīng)的Loader進(jìn)行編譯,最終轉(zhuǎn)為Javascript文件;
          • 5、整個(gè)過(guò)程中webpack會(huì)通過(guò)發(fā)布訂閱模式,向外拋出一些hooks,而webpack的插件即可通過(guò)監(jiān)聽(tīng)這些關(guān)鍵的事件節(jié)點(diǎn),執(zhí)行插件任務(wù)進(jìn)而達(dá)到干預(yù)輸出結(jié)果的目的。

          其中文件的解析與構(gòu)建是一個(gè)比較復(fù)雜的過(guò)程,在webpack源碼中主要依賴于compilercompilation兩個(gè)核心對(duì)象實(shí)現(xiàn)。

          compiler對(duì)象是一個(gè)全局單例,他負(fù)責(zé)把控整個(gè)webpack打包的構(gòu)建流程。compilation對(duì)象是每一次構(gòu)建的上下文對(duì)象,它包含了當(dāng)次構(gòu)建所需要的所有信息,每次熱更新和重新構(gòu)建,compiler都會(huì)重新生成一個(gè)新的compilation對(duì)象,負(fù)責(zé)此次更新的構(gòu)建過(guò)程。

          而每個(gè)模塊間的依賴關(guān)系,則依賴于AST語(yǔ)法樹(shù)。每個(gè)模塊文件在通過(guò)Loader解析完成之后,會(huì)通過(guò)acorn庫(kù)生成模塊代碼的AST語(yǔ)法樹(shù),通過(guò)語(yǔ)法樹(shù)就可以分析這個(gè)模塊是否還有依賴的模塊,進(jìn)而繼續(xù)循環(huán)執(zhí)行下一個(gè)模塊的編譯解析。

          最終Webpack打包出來(lái)的bundle文件是一個(gè)IIFE的執(zhí)行函數(shù)。

          // webpack 5 打包的bundle文件內(nèi)容

          (() => { // webpackBootstrap
              var __webpack_modules__ = ({
                  'file-A-path'((modules) => { // ... })
                  'index-file-path'((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => { // ... })
              })
              
              // The module cache
              var __webpack_module_cache__ = {};
              
              // The require function
              function __webpack_require__(moduleId{
                  // Check if module is in cache
                  var cachedModule = __webpack_module_cache__[moduleId];
                  if (cachedModule !== undefined) {
                          return cachedModule.exports;
                  }
                  // Create a new module (and put it into the cache)
                  var module = __webpack_module_cache__[moduleId] = {
                          // no module.id needed
                          // no module.loaded needed
                          exports: {}
                  };

                  // Execute the module function
                  __webpack_modules__[moduleId](modulemodule.exports, __webpack_require__);

                  // Return the exports of the module
                  return module.exports;
              }
              
              // startup
              // Load entry module and return exports
              // This entry module can't be inlined because the eval devtool is used.
              var __webpack_exports__ = __webpack_require__("./src/index.js");
          })

          webpack4相比,webpack5打包出來(lái)的bundle做了相當(dāng)?shù)木?jiǎn)。在上面的打包demo中,整個(gè)立即執(zhí)行函數(shù)里邊只有三個(gè)變量和一個(gè)函數(shù)方法,__webpack_modules__存放了編譯后的各個(gè)文件模塊的JS內(nèi)容,__webpack_module_cache__用來(lái)做模塊緩存,__webpack_require__Webpack內(nèi)部實(shí)現(xiàn)的一套依賴引入函數(shù)。最后一句則是代碼運(yùn)行的起點(diǎn),從入口文件開(kāi)始,啟動(dòng)整個(gè)項(xiàng)目。

          其中值得一提的是__webpack_require__模塊引入函數(shù),我們?cè)谀K化開(kāi)發(fā)的時(shí)候,通常會(huì)使用ES Module或者CommonJS規(guī)范導(dǎo)出/引入依賴模塊,webpack打包編譯的時(shí)候,會(huì)統(tǒng)一替換成自己的__webpack_require__來(lái)實(shí)現(xiàn)模塊的引入和導(dǎo)出,從而實(shí)現(xiàn)模塊緩存機(jī)制,以及抹平不同模塊規(guī)范之間的一些差異性。

          你知道sourceMap是什么嗎?

          提到sourceMap,很多小伙伴可能會(huì)立刻想到Webpack配置里邊的devtool參數(shù),以及對(duì)應(yīng)的eval,eval-cheap-source-map等等可選值以及它們的含義。除了知道不同參數(shù)之間的區(qū)別以及性能上的差異外,我們也可以一起了解一下sourceMap的實(shí)現(xiàn)方式。

          sourceMap是一項(xiàng)將編譯、打包、壓縮后的代碼映射回源代碼的技術(shù),由于打包壓縮后的代碼并沒(méi)有閱讀性可言,一旦在開(kāi)發(fā)中報(bào)錯(cuò)或者遇到問(wèn)題,直接在混淆代碼中debug問(wèn)題會(huì)帶來(lái)非常糟糕的體驗(yàn),sourceMap可以幫助我們快速定位到源代碼的位置,提高我們的開(kāi)發(fā)效率。sourceMap其實(shí)并不是Webpack特有的功能,而是Webpack支持sourceMap,像JQuery也支持souceMap。

          既然是一種源碼的映射,那必然就需要有一份映射的文件,來(lái)標(biāo)記混淆代碼里對(duì)應(yīng)的源碼的位置,通常這份映射文件以.map結(jié)尾,里邊的數(shù)據(jù)結(jié)構(gòu)大概長(zhǎng)這樣:

          {
            "version" : 3,                          // Source Map版本
            "file""out.js",                       // 輸出文件(可選)
            "sourceRoot""",                       // 源文件根目錄(可選)
            "sources": ["foo.js""bar.js"],        // 源文件列表
            "sourcesContent": [nullnull],         // 源內(nèi)容列表(可選,和源文件列表順序一致)
            "names": ["src""maps""are""fun"], // mappings使用的符號(hào)名稱列表
            "mappings""A,AAAB;;ABCDE;"            // 帶有編碼映射數(shù)據(jù)的字符串
          }

          其中mappings數(shù)據(jù)有如下規(guī)則:

          • 生成文件中的一行的每個(gè)組用“;”分隔;
          • 每一段用“,”分隔;
          • 每個(gè)段由1、4或5個(gè)可變長(zhǎng)度字段組成;

          有了這份映射文件,我們只需要在我們的壓縮代碼的最末端加上這句注釋,即可讓sourceMap生效:

          //# sourceURL=/path/to/file.js.map

          有了這段注釋后,瀏覽器就會(huì)通過(guò)sourceURL去獲取這份映射文件,通過(guò)解釋器解析后,實(shí)現(xiàn)源碼和混淆代碼之間的映射。因此sourceMap其實(shí)也是一項(xiàng)需要瀏覽器支持的技術(shù)。

          如果我們仔細(xì)查看webpack打包出來(lái)的bundle文件,就可以發(fā)現(xiàn)在默認(rèn)的development開(kāi)發(fā)模式下,每個(gè)_webpack_modules__文件模塊的代碼最末端,都會(huì)加上//# sourceURL=webpack://file-path?,從而實(shí)現(xiàn)對(duì)sourceMap的支持。

          sourceMap映射表的生成有一套較為復(fù)雜的規(guī)則,有興趣的小伙伴可以看看以下文章,幫助理解soucrMap的原理實(shí)現(xiàn):

          Source Map的原理探究

          Source Maps under the hood – VLQ, Base64 and Yoda

          是否寫(xiě)過(guò)Loader?簡(jiǎn)單描述一下編寫(xiě)loader的思路?

          從上面的打包代碼我們其實(shí)可以知道,Webpack最后打包出來(lái)的成果是一份Javascript代碼,實(shí)際上在Webpack內(nèi)部默認(rèn)也只能夠處理JS模塊代碼,在打包過(guò)程中,會(huì)默認(rèn)把所有遇到的文件都當(dāng)作 JavaScript代碼進(jìn)行解析,因此當(dāng)項(xiàng)目存在非JS類型文件時(shí),我們需要先對(duì)其進(jìn)行必要的轉(zhuǎn)換,才能繼續(xù)執(zhí)行打包任務(wù),這也是Loader機(jī)制存在的意義。

          Loader的配置使用我們應(yīng)該已經(jīng)非常的熟悉:

          // webpack.config.js
          module.exports = {
            // ...other config
            module: {
              rules: [
                {
                  test/^your-regExp$/,
                  use: [
                    {
                       loader'loader-name-A',
                    }, 
                    {
                       loader'loader-name-B',
                    }
                  ]
                },
              ]
            }
          }

          通過(guò)配置可以看出,針對(duì)每個(gè)文件類型,loader是支持以數(shù)組的形式配置多個(gè)的,因此當(dāng)Webpack在轉(zhuǎn)換該文件類型的時(shí)候,會(huì)按順序鏈?zhǔn)秸{(diào)用每一個(gè)loader,前一個(gè)loader返回的內(nèi)容會(huì)作為下一個(gè)loader的入?yún)?。因?code style="">loader的開(kāi)發(fā)需要遵循一些規(guī)范,比如返回值必須是標(biāo)準(zhǔn)的JS代碼字符串,以保證下一個(gè)loader能夠正常工作,同時(shí)在開(kāi)發(fā)上需要嚴(yán)格遵循“單一職責(zé)”,只關(guān)心loader的輸出以及對(duì)應(yīng)的輸出。

          loader函數(shù)中的this上下文由webpack提供,可以通過(guò)this對(duì)象提供的相關(guān)屬性,獲取當(dāng)前loader需要的各種信息數(shù)據(jù),事實(shí)上,這個(gè)this指向了一個(gè)叫loaderContextloader-runner特有對(duì)象。有興趣的小伙伴可以自行閱讀源碼。

          module.exports = function(source{
              const content = doSomeThing2JsString(source);
              
              // 如果 loader 配置了 options 對(duì)象,那么this.query將指向 options
              const options = this.query;
              
              // 可以用作解析其他模塊路徑的上下文
              console.log('this.context');
              
              /*
               * this.callback 參數(shù):
               * error:Error | null,當(dāng) loader 出錯(cuò)時(shí)向外拋出一個(gè) error
               * content:String | Buffer,經(jīng)過(guò) loader 編譯后需要導(dǎo)出的內(nèi)容
               * sourceMap:為方便調(diào)試生成的編譯后內(nèi)容的 source map
               * ast:本次編譯生成的 AST 靜態(tài)語(yǔ)法樹(shù),之后執(zhí)行的 loader 可以直接使用這個(gè) AST,進(jìn)而省去重復(fù)生成 AST 的過(guò)程
               */

              this.callback(null, content);
              // or return content;
          }

          更詳細(xì)的開(kāi)發(fā)文檔可以直接查看官網(wǎng)的 Loader API。

          是否寫(xiě)過(guò)Plugin?簡(jiǎn)單描述一下編寫(xiě)plugin的思路?

          如果說(shuō)Loader負(fù)責(zé)文件轉(zhuǎn)換,那么Plugin便是負(fù)責(zé)功能擴(kuò)展。LoaderPlugin作為Webpack的兩個(gè)重要組成部分,承擔(dān)著兩部分不同的職責(zé)。

          上文已經(jīng)說(shuō)過(guò),webpack基于發(fā)布訂閱模式,在運(yùn)行的生命周期中會(huì)廣播出許多事件,插件通過(guò)監(jiān)聽(tīng)這些事件,就可以在特定的階段執(zhí)行自己的插件任務(wù),從而實(shí)現(xiàn)自己想要的功能。

          既然基于發(fā)布訂閱模式,那么知道Webpack到底提供了哪些事件鉤子供插件開(kāi)發(fā)者使用是非常重要的,上文提到過(guò)compilercompilationWebpack兩個(gè)非常核心的對(duì)象,其中compiler暴露了和 Webpack整個(gè)生命周期相關(guān)的鉤子(compiler-hooks),而compilation則暴露了與模塊和依賴有關(guān)的粒度更小的事件鉤子(Compilation Hooks)。

          Webpack的事件機(jī)制基于webpack自己實(shí)現(xiàn)的一套Tapable事件流方案(github)

          // Tapable的簡(jiǎn)單使用
          const { SyncHook } = require("tapable");

          class Car {
              constructor() {
                  // 在this.hooks中定義所有的鉤子事件
                  this.hooks = {
                      acceleratenew SyncHook(["newSpeed"]),
                      brakenew SyncHook(),
                      calculateRoutesnew AsyncParallelHook(["source""target""routesList"])
                  };
              }

              /* ... */
          }


          const myCar = new Car();
          // 通過(guò)調(diào)用tap方法即可增加一個(gè)消費(fèi)者,訂閱對(duì)應(yīng)的鉤子事件了
          myCar.hooks.brake.tap("WarningLampPlugin", () => warningLamp.on());

          Plugin的開(kāi)發(fā)和開(kāi)發(fā)Loader一樣,需要遵循一些開(kāi)發(fā)上的規(guī)范和原則:

          • 插件必須是一個(gè)函數(shù)或者是一個(gè)包含 apply 方法的對(duì)象,這樣才能訪問(wèn)compiler實(shí)例;
          • 傳給每個(gè)插件的 compilercompilation 對(duì)象都是同一個(gè)引用,若在一個(gè)插件中修改了它們身上的屬性,會(huì)影響后面的插件;
          • 異步的事件需要在插件處理完任務(wù)時(shí)調(diào)用回調(diào)函數(shù)通知 Webpack 進(jìn)入下一個(gè)流程,不然會(huì)卡住;

          了解了以上這些內(nèi)容,想要開(kāi)發(fā)一個(gè) Webpack Plugin,其實(shí)也并不困難。

          class MyPlugin {
            apply (compiler) {
              // 找到合適的事件鉤子,實(shí)現(xiàn)自己的插件功能
              compiler.hooks.emit.tap('MyPlugin', compilation => {
                  // compilation: 當(dāng)前打包構(gòu)建流程的上下文
                  console.log(compilation);
                  
                  // do something...
              })
            }
          }

          更詳細(xì)的開(kāi)發(fā)文檔可以直接查看官網(wǎng)的 Plugin API。

          最后

          本文也是結(jié)合一些優(yōu)秀的文章和webpack本身的源碼,大概地說(shuō)了幾個(gè)相對(duì)重要的概念和流程,其中的實(shí)現(xiàn)細(xì)節(jié)和設(shè)計(jì)思路還需要結(jié)合源碼去閱讀和慢慢理解。

          Webpack作為一款優(yōu)秀的打包工具,它改變了傳統(tǒng)前端的開(kāi)發(fā)模式,是現(xiàn)代化前端開(kāi)發(fā)的基石。這樣一個(gè)優(yōu)秀的開(kāi)源項(xiàng)目有許多優(yōu)秀的設(shè)計(jì)思想和理念可以借鑒,我們自然也不應(yīng)該僅僅停留在API的使用層面,嘗試帶著問(wèn)題閱讀源碼,理解實(shí)現(xiàn)的流程和原理,也能讓我們學(xué)到更多知識(shí),理解得更加深刻,在項(xiàng)目中才能游刃有余的應(yīng)用。

          相關(guān)文檔鏈接

          Webpack官網(wǎng)

          「吐血整理」再來(lái)一打Webpack面試題

          如果覺(jué)得這篇文章還不錯(cuò)
          點(diǎn)擊下面卡片關(guān)注我
          來(lái)個(gè)【分享、點(diǎn)贊、在看】三連支持一下吧

             “分享、點(diǎn)贊、在看” 支持一波  

          瀏覽 59
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  豆花成人视频在线观看 | 国内视频精品在线播放 | 手机看片免费av 手机免费观看AV 手机在线操B视频 | 狼人综合伊人网 | 大香蕉凹凸|