「吐血整理」再來一打Webpack面試題

童歐巴發表於2020-05-26

從頭髮的濃密程度和幹練的走路姿勢我察覺到,面前坐著的這位面試官也是一把好手。

我像以往一樣,準備花3分鐘的時間進行自我介紹。在此期間,我的目光被16寸的MacBook Pro所吸引,這次的自我介紹我做足了準備,很有信心征服面試官。不出我所料,面試官被我引入了我擅長的領域。

看來你對Webpack很熟悉,那我來考考你

0.有哪些常見的Loader?你用過哪些Loader?

(我開始熟悉的報起了菜名)

  • raw-loader:載入檔案原始內容(utf-8)
  • file-loader:把檔案輸出到一個資料夾中,在程式碼中通過相對 URL 去引用輸出的檔案 (處理圖片和字型)
  • url-loader:與 file-loader 類似,區別是使用者可以設定一個閾值,大於閾值會交給file-loader處理,小於閾值時返回檔案 base64 形式編碼 (處理圖片和字型)
  • source-map-loader:載入額外的 Source Map 檔案,以方便斷點除錯
  • svg-inline-loader:將壓縮後的 SVG 內容注入程式碼中
  • image-loader:載入並且壓縮圖片檔案
  • json-loader 載入 JSON 檔案(預設包含)
  • handlebars-loader: 將 Handlebars 模版編譯成函式並返回
  • babel-loader:把 ES6 轉換成 ES5
  • ts-loader: 將 TypeScript 轉換成 JavaScript
  • awesome-typescript-loader:將 TypeScript 轉換成 JavaScript,效能優於 ts-loader
  • sass-loader:將SCSS/SASS程式碼轉換成CSS
  • css-loader:載入 CSS,支援模組化、壓縮、檔案匯入等特性
  • style-loader:把 CSS 程式碼注入到 JavaScript 中,通過 DOM 操作去載入 CSS
  • postcss-loader:擴充套件 CSS 語法,使用下一代 CSS,可以配合 autoprefixer 外掛自動補齊 CSS3 字首
  • eslint-loader:通過 ESLint 檢查 JavaScript 程式碼
  • tslint-loader:通過 TSLint檢查 TypeScript 程式碼
  • mocha-loader:載入 Mocha 測試用例的程式碼
  • coverjs-loader:計算測試的覆蓋率
  • vue-loader:載入 Vue.js 單檔案元件
  • i18n-loader: 國際化
  • cache-loader: 可以在一些效能開銷較大的 Loader 之前新增,目的是將結果快取到磁碟裡

更多 Loader 請參考官網

(面試官:挺好,知道的還挺多)

1.有哪些常見的Plugin?你用過哪些Plugin?

(這大兄弟好像聽上癮了,繼續開啟常規操作)

  • define-plugin:定義環境變數 (Webpack4 之後指定 mode 會自動配置)
  • ignore-plugin:忽略部分檔案
  • html-webpack-plugin:簡化 HTML 檔案建立 (依賴於 html-loader)
  • web-webpack-plugin:可方便地為單頁應用輸出 HTML,比 html-webpack-plugin 好用
  • uglifyjs-webpack-plugin:不支援 ES6 壓縮 (Webpack4 以前)
  • terser-webpack-plugin: 支援壓縮 ES6 (Webpack4)
  • webpack-parallel-uglify-plugin: 多程式執行程式碼壓縮,提升構建速度
  • mini-css-extract-plugin: 分離樣式檔案,CSS 提取為獨立檔案,支援按需載入 (替代extract-text-webpack-plugin)
  • serviceworker-webpack-plugin:為網頁應用增加離線快取功能
  • clean-webpack-plugin: 目錄清理
  • ModuleConcatenationPlugin: 開啟 Scope Hoisting
  • speed-measure-webpack-plugin: 可以看到每個 Loader 和 Plugin 執行耗時 (整個打包耗時、每個 Plugin 和 Loader 耗時)
  • webpack-bundle-analyzer: 視覺化 Webpack 輸出檔案的體積 (業務元件、依賴第三方模組)

更多 Plugin 請參考官網

(Double Kill)

2.那你再說一說Loader和Plugin的區別?

(就知道你會問這個,我用手掩蓋著嘴角的微笑)

Loader 本質就是一個函式,在該函式中對接收到的內容進行轉換,返回轉換後的結果。
因為 Webpack 只認識 JavaScript,所以 Loader 就成了翻譯官,對其他型別的資源進行轉譯的預處理工作。

Plugin 就是外掛,基於事件流框架 Tapable,外掛可以擴充套件 Webpack 的功能,在 Webpack 執行的生命週期中會廣播出許多事件,Plugin 可以監聽這些事件,在合適的時機通過 Webpack 提供的 API 改變輸出結果。

Loader 在 module.rules 中配置,作為模組的解析規則,型別為陣列。每一項都是一個 Object,內部包含了 test(型別檔案)、loader、options (引數)等屬性。

Plugin 在 plugins 中單獨配置,型別為陣列,每一項是一個 Plugin 的例項,引數都通過建構函式傳入。

3.Webpack構建流程簡單說一下

Webpack 的執行流程是一個序列的過程,從啟動到結束會依次執行以下流程:

  • 初始化引數:從配置檔案和 Shell 語句中讀取與合併引數,得出最終的引數
  • 開始編譯:用上一步得到的引數初始化 Compiler 物件,載入所有配置的外掛,執行物件的 run 方法開始執行編譯
  • 確定入口:根據配置中的 entry 找出所有的入口檔案
  • 編譯模組:從入口檔案出發,呼叫所有配置的 Loader 對模組進行翻譯,再找出該模組依賴的模組,再遞迴本步驟直到所有入口依賴的檔案都經過了本步驟的處理
  • 完成模組編譯:在經過第4步使用 Loader 翻譯完所有模組後,得到了每個模組被翻譯後的最終內容以及它們之間的依賴關係
  • 輸出資源:根據入口和模組之間的依賴關係,組裝成一個個包含多個模組的 Chunk,再把每個 Chunk 轉換成一個單獨的檔案加入到輸出列表,這步是可以修改輸出內容的最後機會
  • 輸出完成:在確定好輸出內容後,根據配置確定輸出的路徑和檔名,把檔案內容寫入到檔案系統

在以上過程中,Webpack 會在特定的時間點廣播出特定的事件,外掛在監聽到感興趣的事件後會執行特定的邏輯,並且外掛可以呼叫 Webpack 提供的 API 改變 Webpack 的執行結果。

簡單說

  • 初始化:啟動構建,讀取與合併配置引數,載入 Plugin,例項化 Compiler
  • 編譯:從 Entry 出發,針對每個 Module 序列呼叫對應的 Loader 去翻譯檔案的內容,再找到該 Module 依賴的 Module,遞迴地進行編譯處理
  • 輸出:將編譯後的 Module 組合成 Chunk,將 Chunk 轉換成檔案,輸出到檔案系統中

對原始碼感興趣的同學可以移步我的另一篇專欄從原始碼窺探Webpack4.x原理

4.使用webpack開發時,你用過哪些可以提高效率的外掛?

(這道題還蠻注重實際,使用者的體驗還是要從小抓起的)

  • webpack-dashboard:可以更友好的展示相關打包資訊。
  • webpack-merge:提取公共配置,減少重複配置程式碼
  • speed-measure-webpack-plugin:簡稱 SMP,分析出 Webpack 打包過程中 Loader 和 Plugin 的耗時,有助於找到構建過程中的效能瓶頸。
  • size-plugin:監控資源體積變化,儘早發現問題
  • HotModuleReplacementPlugin:模組熱替換

5.source map是什麼?生產環境怎麼用?

source map 是將編譯、打包、壓縮後的程式碼對映回原始碼的過程。打包壓縮後的程式碼不具備良好的可讀性,想要除錯原始碼就需要 soucre map。

map檔案只要不開啟開發者工具,瀏覽器是不會載入的。

線上環境一般有三種處理方案:

  • hidden-source-map:藉助第三方錯誤監控平臺 Sentry 使用
  • nosources-source-map:只會顯示具體行數以及檢視原始碼的錯誤棧。安全性比 sourcemap 高
  • sourcemap:通過 nginx 設定將 .map 檔案只對白名單開放(公司內網)

注意:避免在生產中使用 inline-eval-,因為它們會增加 bundle 體積大小,並降低整體效能。

6.模組打包原理知道嗎?

Webpack 實際上為每個模組創造了一個可以匯出和匯入的環境,本質上並沒有修改
程式碼的執行邏輯,程式碼執行順序與模組載入順序也完全一致。

7.檔案監聽原理呢?

在發現原始碼發生變化時,自動重新構建出新的輸出檔案。

Webpack開啟監聽模式,有兩種方式:

  • 啟動 webpack 命令時,帶上 --watch 引數
  • 在配置 webpack.config.js 中設定 watch:true

缺點:每次需要手動重新整理瀏覽器

原理:輪詢判斷檔案的最後編輯時間是否變化,如果某個檔案發生了變化,並不會立刻告訴監聽者,而是先快取起來,等 aggregateTimeout 後再執行。

module.export = {
    // 預設false,也就是不開啟
    watch: true,
    // 只有開啟監聽模式時,watchOptions才有意義
    watchOptions: {
        // 預設為空,不監聽的檔案或者資料夾,支援正則匹配
        ignored: /node_modules/,
        // 監聽到變化發生後會等300ms再去執行,預設300ms
        aggregateTimeout:300,
        // 判斷檔案是否發生變化是通過不停詢問系統指定檔案有沒有變化實現的,預設每秒問1000次
        poll:1000
    }
}

8.說一下 Webpack 的熱更新原理吧

(敲黑板,這道題必考)

Webpack 的熱更新又稱熱替換(Hot Module Replacement),縮寫為 HMR。 這個機制可以做到不用重新整理瀏覽器而將新變更的模組替換掉舊的模組。

HMR的核心就是客戶端從服務端拉去更新後的檔案,準確的說是 chunk diff (chunk 需要更新的部分),實際上 WDS 與瀏覽器之間維護了一個 Websocket,當本地資源發生變化時,WDS 會向瀏覽器推送更新,並帶上構建時的 hash,讓客戶端與上一次資源進行對比。客戶端對比出差異後會向 WDS 發起 Ajax 請求來獲取更改內容(檔案列表、hash),這樣客戶端就可以再借助這些資訊繼續向 WDS 發起 jsonp 請求獲取該chunk的增量更新。

後續的部分(拿到增量更新之後如何處理?哪些狀態該保留?哪些又需要更新?)由 HotModulePlugin 來完成,提供了相關 API 以供開發者針對自身場景進行處理,像react-hot-loadervue-loader 都是藉助這些 API 實現 HMR。

細節請參考Webpack HMR 原理解析

(面試官:不錯不錯,小夥子表達能力不錯)

(基操,勿6)

9.如何對bundle體積進行監控和分析?

VSCode 中有一個外掛 Import Cost 可以幫助我們對引入模組的大小進行實時監測,還可以使用 webpack-bundle-analyzer 生成 bundle 的模組組成圖,顯示所佔體積。

bundlesize 工具包可以進行自動化資源體積監控。

10.檔案指紋是什麼?怎麼用?

檔案指紋是打包後輸出的檔名的字尾。

  • Hash:和整個專案的構建相關,只要專案檔案有修改,整個專案構建的 hash 值就會更改
  • Chunkhash:和 Webpack 打包的 chunk 有關,不同的 entry 會生出不同的 chunkhash
  • Contenthash:根據檔案內容來定義 hash,檔案內容不變,則 contenthash 不變

JS的檔案指紋設定

設定 output 的 filename,用 chunkhash。

module.exports = {
    entry: {
        app: './scr/app.js',
        search: './src/search.js'
    },
    output: {
        filename: '[name][chunkhash:8].js',
        path:__dirname + '/dist'
    }
}

CSS的檔案指紋設定

設定 MiniCssExtractPlugin 的 filename,使用 contenthash。

module.exports = {
    entry: {
        app: './scr/app.js',
        search: './src/search.js'
    },
    output: {
        filename: '[name][chunkhash:8].js',
        path:__dirname + '/dist'
    },
    plugins:[
        new MiniCssExtractPlugin({
            filename: `[name][contenthash:8].css`
        })
    ]
}

圖片的檔案指紋設定

設定file-loader的name,使用hash。

佔位符名稱及含義

  • ext 資源字尾名
  • name 檔名稱
  • path 檔案的相對路徑
  • folder 檔案所在的資料夾
  • contenthash 檔案的內容hash,預設是md5生成
  • hash 檔案內容的hash,預設是md5生成
  • emoji 一個隨機的指代檔案內容的emoj
const path = require('path');

module.exports = {
    entry: './src/index.js',
    output: {
        filename:'bundle.js',
        path:path.resolve(__dirname, 'dist')
    },
    module:{
        rules:[{
            test:/\.(png|svg|jpg|gif)$/,
            use:[{
                loader:'file-loader',
                options:{
                    name:'img/[name][hash:8].[ext]'
                }
            }]
        }]
    }
}

11.在實際工程中,配置檔案上百行乃是常事,如何保證各個loader按照預想方式工作?

可以使用 enforce 強制執行 loader 的作用順序,pre 代表在所有正常 loader 之前執行,post 是所有 loader 之後執行。(inline 官方不推薦使用)

12.如何優化 Webpack 的構建速度?

(這個問題就像能不能說一說從URL輸入到頁面顯示發生了什麼一樣)

(我只想說:您希望我講多長時間呢?)

(面試官:。。。)

  • 使用高版本的 Webpack 和 Node.js
  • 多程式/多例項構建:HappyPack(不維護了)、thread-loader
  • 壓縮程式碼

    • 多程式並行壓縮

      • webpack-paralle-uglify-plugin
      • uglifyjs-webpack-plugin 開啟 parallel 引數 (不支援ES6)
      • terser-webpack-plugin 開啟 parallel 引數
    • 通過 mini-css-extract-plugin 提取 Chunk 中的 CSS 程式碼到單獨檔案,通過 css-loader 的 minimize 選項開啟 cssnano 壓縮 CSS。
  • 圖片壓縮

    • 使用基於 Node 庫的 imagemin (很多定製選項、可以處理多種圖片格式)
    • 配置 image-webpack-loader
  • 縮小打包作用域

    • exclude/include (確定 loader 規則範圍)
    • resolve.modules 指明第三方模組的絕對路徑 (減少不必要的查詢)
    • resolve.mainFields 只採用 main 欄位作為入口檔案描述欄位 (減少搜尋步驟,需要考慮到所有執行時依賴的第三方模組的入口檔案描述欄位)
    • resolve.extensions 儘可能減少字尾嘗試的可能性
    • noParse 對完全不需要解析的庫進行忽略 (不去解析但仍會打包到 bundle 中,注意被忽略掉的檔案裡不應該包含 import、require、define 等模組化語句)
    • IgnorePlugin (完全排除模組)
    • 合理使用alias
  • 提取頁面公共資源

    • 基礎包分離:

      • 使用 html-webpack-externals-plugin,將基礎包通過 CDN 引入,不打入 bundle 中
      • 使用 SplitChunksPlugin 進行(公共指令碼、基礎包、頁面公共檔案)分離(Webpack4內建) ,替代了 CommonsChunkPlugin 外掛
  • DLL

    • 使用 DllPlugin 進行分包,使用 DllReferencePlugin(索引連結) 對 manifest.json 引用,讓一些基本不會改動的程式碼先打包成靜態資源,避免反覆編譯浪費時間。
    • HashedModuleIdsPlugin 可以解決模組數字id問題
  • 充分利用快取提升二次構建速度

    • babel-loader 開啟快取
    • terser-webpack-plugin 開啟快取
    • 使用 cache-loader 或者 hard-source-webpack-plugin
  • Tree shaking

    • 打包過程中檢測工程中沒有引用過的模組並進行標記,在資源壓縮時將它們從最終的bundle中去掉(只能對ES6 Modlue生效) 開發中儘可能使用ES6 Module的模組,提高tree shaking效率
    • 禁用 babel-loader 的模組依賴解析,否則 Webpack 接收到的就都是轉換過的 CommonJS 形式的模組,無法進行 tree-shaking
    • 使用 PurifyCSS(不在維護) 或者 uncss 去除無用 CSS 程式碼

      • purgecss-webpack-plugin 和 mini-css-extract-plugin配合使用(建議)
  • Scope hoisting

    • 構建後的程式碼會存在大量閉包,造成體積增大,執行程式碼時建立的函式作用域變多,記憶體開銷變大。Scope hoisting 將所有模組的程式碼按照引用順序放在一個函式作用域裡,然後適當的重新命名一些變數以防止變數名衝突
    • 必須是ES6的語法,因為有很多第三方庫仍採用 CommonJS 語法,為了充分發揮 Scope hoisting 的作用,需要配置 mainFields 對第三方模組優先採用 jsnext:main 中指向的ES6模組化語法
  • 動態Polyfill

    • 建議採用 polyfill-service 只給使用者返回需要的polyfill,社群維護。 (部分國內奇葩瀏覽器UA可能無法識別,但可以降級返回所需全部polyfill)

更多優化請參考官網-構建效能

13.你剛才也提到了程式碼分割,那程式碼分割的本質是什麼?有什麼意義呢?

程式碼分割的本質其實就是在原始碼直接上線打包成唯一指令碼main.bundle.js這兩種極端方案之間的一種更適合實際場景的中間狀態。

阿卡麗:榮耀劍下取,均衡亂中求

用可接受的伺服器效能壓力增加來換取更好的使用者體驗。

原始碼直接上線:雖然過程可控,但是http請求多,效能開銷大。

打包成唯一指令碼:一把梭完自己爽,伺服器壓力小,但是頁面空白期長,使用者體驗不好。

(Easy peezy right)

14.是否寫過Loader?簡單描述一下編寫loader的思路?

Loader 支援鏈式呼叫,所以開發上需要嚴格遵循“單一職責”,每個 Loader 只負責自己需要負責的事情。

Loader的API 可以去官網查閱

  • Loader 執行在 Node.js 中,我們可以呼叫任意 Node.js 自帶的 API 或者安裝第三方模組進行呼叫
  • Webpack 傳給 Loader 的原內容都是 UTF-8 格式編碼的字串,當某些場景下 Loader 處理二進位制檔案時,需要通過 exports.raw = true 告訴 Webpack 該 Loader 是否需要二進位制資料
  • 儘可能的非同步化 Loader,如果計算量很小,同步也可以
  • Loader 是無狀態的,我們不應該在 Loader 中保留狀態
  • 使用 loader-utils 和 schema-utils 為我們提供的實用工具
  • 載入本地 Loader 方法

    • Npm link
    • ResolveLoader

15.是否寫過Plugin?簡單描述一下編寫Plugin的思路?

webpack在執行的生命週期中會廣播出許多事件,Plugin 可以監聽這些事件,在特定的階段鉤入想要新增的自定義功能。Webpack 的 Tapable 事件流機制保證了外掛的有序性,使得整個系統擴充套件性良好。

Plugin的API 可以去官網查閱

  • compiler 暴露了和 Webpack 整個生命週期相關的鉤子
  • compilation 暴露了與模組和依賴有關的粒度更小的事件鉤子
  • 外掛需要在其原型上繫結apply方法,才能訪問 compiler 例項
  • 傳給每個外掛的 compiler 和 compilation物件都是同一個引用,若在一個外掛中修改了它們身上的屬性,會影響後面的外掛
  • 找出合適的事件點去完成想要的功能

    • emit 事件發生時,可以讀取到最終輸出的資源、程式碼塊、模組及其依賴,並進行修改(emit 事件是修改 Webpack 輸出資源的最後時機)
    • watch-run 當依賴的檔案發生變化時會觸發
  • 非同步的事件需要在外掛處理完任務時呼叫回撥函式通知 Webpack 進入下一個流程,不然會卡住

16.聊一聊Babel原理吧

大多數JavaScript Parser遵循 estree 規範,Babel 最初基於 acorn 專案(輕量級現代 JavaScript 解析器)
Babel大概分為三大部分:

  • 解析:將程式碼轉換成 AST

    • 詞法分析:將程式碼(字串)分割為token流,即語法單元成的陣列
    • 語法分析:分析token流(上面生成的陣列)並生成 AST
  • 轉換:訪問 AST 的節點進行變換操作生產新的 AST

    • Taro就是利用 babel 完成的小程式語法轉換
  • 生成:以新的 AST 為基礎生成程式碼

想了解如何一步一步實現一個編譯器的同學可以移步 Babel 官網曾經推薦的開源專案
the-super-tiny-compiler

面試官:(我聽的口渴了,我們們休息一會,一會進行下半場)

面試官拿起旁邊已經涼透的龍井,喝了一口。

(這小夥子有點東西)

持續更新……

參考

  • 深入淺出 Webpack
  • Webpack 實戰
  • 玩轉 Webpack

相關文章