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

「觀感度:????????????????????」

「口味:清蒸鱸魚」

「烹飪時間:15min」


從頭髮的濃密程度和幹練的走路姿勢我察覺到,面前坐着的這位面試官也是一把好手。我像以往一樣,準備花3分鐘的時間進行自我介紹。在此期間,我的目光被16寸的MacBook Pro所吸引,這次的自我介紹我做足了準備,很有信心征服面試官。不出我所料,面試官被我引入了我擅長的領域。

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

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

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

  • raw-loader:加載文件原始內容(utf-8)

  • file-loader:把文件輸出到一個文件夾中,在代碼中通過相對 URL 去引用輸出的文件 (處理圖片和字體)

  • url-loader:與 file-loader 類似,區別是用戶可以設置一個閾值,大於閾值時返回其 publicPath,小於閾值時返回文件 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

  • style-loader:將 CSS 代碼注入 JavaScript 中,通過 DOM 操作去加載 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 請參考官網

https://webpack.docschina.org/loaders

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

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 請參考官網

https://webpack.docschina.org/plugins

(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 原理解析

https://zhuanlan.zhihu.com/p/30669007

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

(基操,勿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            一個隨機的指代文件內容的emoji

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

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

    • 打包過程中檢測工程中沒有引用過的模塊並進行標記,在資源壓縮時將它們從最終的bundle中去掉(只能對ES6 Modlue生效) 開發中儘可能使用ES6 Module的模塊,提高tree shaking效率

    • 禁用 babel-loader 的模塊依賴解析,否則 Webpack 接收到的就都是轉換過的 CommonJS 形式的模塊,無法進行 tree-shaking

    • 使用 PurifyCSS(不在維護) 或者 uncss 去除無用 CSS 代碼

  • Scope hoisting

    • 構建後的代碼會存在大量閉包,造成體積增大,運行代碼時創建的函數作用域變多,內存開銷變大。Scope hoisting 將所有模塊的代碼按照引用順序放在一個函數作用域裏,然後適當的重命名一些變量以防止變量名衝突

    • 必須是ES6的語法,因爲有很多第三方庫仍採用 CommonJS 語法,爲了充分發揮 Scope hoisting 的作用,需要配置 mainFields 對第三方模塊優先採用 jsnext:main 中指向的ES6模塊化語法

  • 動態Polyfill

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

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

https://www.webpackjs.com/guides/build-performance

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

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

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

「用可接受的服務器性能壓力增加來換取更好的用戶體驗。」

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

打包成唯一腳本:一把梭完自己爽,服務器壓力小,但是頁面空白期長,用戶體驗不好。

(Easy peezy right)

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

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

Loader的API 可以去官網查閱

https://www.webpackjs.com/api/loaders

  • 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 可以去官網查閱

https://www.webpackjs.com/api/plugins

  • 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 完成的小程序語法轉換

      https://github.com/NervJS/taro/blob/master/packages/taro-transformer-wx/src/index.ts#L15

  • 生成:以新的 AST 爲基礎生成代碼

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

https://github.com/jamiebuilds/the-super-tiny-compiler

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

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

(這小夥子有點東西)

「持續更新……」

參考

  • 深入淺出 Webpack

  • Webpack 實戰

  • 玩轉 Webpack

---END---

推薦閱讀

張一鳴:爲什麼 BAT 挖不走我們的人才?

微信號 可以改了 !!!真事 !!

再見!杭州!再見!阿里巴巴!

2020年北京,上海擺攤夜市分佈

公衆號對話框,回覆關鍵字 “1024”

免費領取程序員賺錢實操教程

- 長按識別關注 -

技術,職場,產品,思維

行業觀察

                                                                          

如有收穫,點個在看,誠摯感謝

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章