有哪些常見的Loader?你用過哪些Loader?
-
raw-loader:加載文件原始內(nèi)容(utf-8) -
file-loader:把文件輸出到一個文件夾中,在代碼中通過相對 URL 去引用輸出的文件 (處理圖片和字體) -
url-loader:與file-loader類似,區(qū)別是用戶可以設(shè)置一個閾值,大于閾值時返回其 publicPath,小于閾值時返回文件 base64 形式編碼 (處理圖片和字體) -
svg-inline-loader:將壓縮后的 SVG 內(nèi)容注入代碼中 -
image-loader:加載并且壓縮圖片文件 -
json-loader:加載 JSON 文件(默認(rèn)包含) -
handlebars-loader: 將 Handlebars 模版編譯成函數(shù)并返回 -
style-loader:把 CSS 代碼注入到 JavaScript 中,通過 DOM 操作去加載 CSS -
postcss-loader:擴展 CSS 語法,使用下一代 CSS,可以配合 autoprefixer 插件自動補齊 CSS3 前綴 -
sass-loader:將SCSS/SASS代碼轉(zhuǎn)換成CSS -
css-loader:加載 CSS,支持模塊化、壓縮、文件導(dǎo)入等特性 -
source-map-loader:加載額外的 Source Map 文件,以方便斷點調(diào)試 -
babel-loader:把 ES6 轉(zhuǎn)換成 ES5 -
ts-loader: 將 TypeScript 轉(zhuǎn)換成 JavaScript -
awesome-typescript-loader:將 TypeScript 轉(zhuǎn)換成 JavaScript,性能優(yōu)于 ts-loader -
eslint-loader:通過 ESLint 檢查 JavaScript 代碼 -
tslint-loader:通過 TSLint檢查 TypeScript 代碼 -
vue-loader:加載 Vue.js 單文件組件 -
mocha-loader:加載 Mocha 測試用例的代碼 -
coverjs-loader:計算測試的覆蓋率 -
i18n-loader: 國際化 -
cache-loader: 可以在一些性能開銷較大的 Loader 之前添加,目的是將結(jié)果緩存到磁盤里
更多 Loader 請參考官網(wǎng)
有哪些常見的Plugin?你用過哪些Plugin?(加粗部分為webpack提速相關(guān)插件)
-
define-plugin:定義環(huán)境變量 (Webpack4 之后指定 mode 會自動配置) -
ignore-plugin:忽略部分文件 -
html-webpack-plugin:簡化 HTML 文件創(chuàng)建 (依賴于 html-loader) -
web-webpack-plugin:可方便地為單頁應(yīng)用輸出 HTML,比 html-webpack-plugin 好用 -
mini-css-extract-plugin: 分離樣式文件,CSS 提取為獨立文件,支持按需加載 (替代extract-text-webpack-plugin) -
serviceworker-webpack-plugin:為網(wǎng)頁應(yīng)用增加離線緩存功能 -
clean-webpack-plugin: 目錄清理 -
uglifyjs-webpack-plugin:不支持 ES6 壓縮 (Webpack4 以前) -
terser-webpack-plugin: 支持壓縮 ES6 (Webpack4) -
webpack-parallel-uglify-plugin: 多進(jìn)程執(zhí)行代碼壓縮,提升構(gòu)建速度 -
ModuleConcatenationPlugin: 開啟 Scope Hoisting -
speed-measure-webpack-plugin: 可以看到每個 Loader 和 Plugin 執(zhí)行耗時 (整個打包耗時、每個 Plugin 和 Loader 耗時) -
webpack-bundle-analyzer: 可視化 Webpack 輸出文件的體積 (業(yè)務(wù)組件、依賴第三方模塊)
更多 Plugin 請參考官網(wǎng)
Loader和Plugin的區(qū)別
Loader 本質(zhì)就是一個函數(shù),在該函數(shù)中對接收到的內(nèi)容進(jìn)行轉(zhuǎn)換,返回轉(zhuǎn)換后的結(jié)果。因為 Webpack 只認(rèn)識 JavaScript,所以 Loader 就成了翻譯官,對其他類型的資源進(jìn)行轉(zhuǎn)譯的預(yù)處理工作。
Plugin 就是插件,基于事件流框架 Tapable,插件可以擴展 Webpack 的功能,在 Webpack 運行的生命周期中會廣播出許多事件,Plugin 可以監(jiān)聽這些事件,在合適的時機通過 Webpack 提供的 API 改變輸出結(jié)果。
Loader 在 module.rules 中配置,作為模塊的解析規(guī)則,類型為數(shù)組。每一項都是一個 Object,內(nèi)部包含了 test(類型文件)、loader、options (參數(shù))等屬性。
Plugin 在 plugins 中單獨配置,類型為數(shù)組,每一項是一個 Plugin 的實例,參數(shù)都通過構(gòu)造函數(shù)傳入。
Webpack構(gòu)建流程
Webpack 的運行流程是一個串行的過程,從啟動到結(jié)束會依次執(zhí)行以下流程:
-
初始化參數(shù):從配置文件和 Shell 語句中讀取與合并參數(shù),得出最終的參數(shù) -
開始編譯:用上一步得到的參數(shù)初始化 Compiler 對象,加載所有配置的插件,執(zhí)行對象的 run 方法開始執(zhí)行編譯 -
確定入口:根據(jù)配置中的 entry 找出所有的入口文件 -
編譯模塊:從入口文件出發(fā),調(diào)用所有配置的 Loader 對模塊進(jìn)行翻譯,再找出該模塊依賴的模塊,再遞歸本步驟直到所有入口依賴的文件都經(jīng)過了本步驟的處理 -
完成模塊編譯:在經(jīng)過第4步使用 Loader 翻譯完所有模塊后,得到了每個模塊被翻譯后的最終內(nèi)容以及它們之間的依賴關(guān)系 -
輸出資源:根據(jù)入口和模塊之間的依賴關(guān)系,組裝成一個個包含多個模塊的 Chunk,再把每個 Chunk 轉(zhuǎn)換成一個單獨的文件加入到輸出列表,這步是可以修改輸出內(nèi)容的最后機會 -
輸出完成:在確定好輸出內(nèi)容后,根據(jù)配置確定輸出的路徑和文件名,把文件內(nèi)容寫入到文件系統(tǒng)
在以上過程中,Webpack 會在特定的時間點廣播出特定的事件,插件在監(jiān)聽到感興趣的事件后會執(zhí)行特定的邏輯,并且插件可以調(diào)用 Webpack 提供的 API 改變 Webpack 的運行結(jié)果。
簡單說:
- 初始化:啟動構(gòu)建,讀取與合并配置參數(shù),加載 Plugin,實例化 Compiler
- 編譯:從 Entry 出發(fā),針對每個 Module 串行調(diào)用對應(yīng)的 Loader 去翻譯文件的內(nèi)容,再找到該 Module 依賴的 Module,遞歸地進(jìn)行編譯處理
- 輸出:將編譯后的 Module 組合成 Chunk,將 Chunk 轉(zhuǎn)換成文件,輸出到文件系統(tǒng)中
使用webpack開發(fā)時,你用過哪些可以提高效率的插件?
-
webpack-dashboard:可以更友好的展示相關(guān)打包信息。 -
webpack-merge:提取公共配置,減少重復(fù)配置代碼 -
speed-measure-webpack-plugin:簡稱 SMP,分析出 Webpack 打包過程中 Loader 和 Plugin 的耗時,有助于找到構(gòu)建過程中的性能瓶頸。 -
size-plugin:監(jiān)控資源體積變化,盡早發(fā)現(xiàn)問題 -
HotModuleReplacementPlugin:模塊熱替換
source map是什么?生產(chǎn)環(huán)境怎么用?
source map 是將編譯、打包、壓縮后的代碼映射回源代碼的過程。打包壓縮后的代碼不具備良好的可讀性,想要調(diào)試源碼就需要 soucre map。
map文件只要不打開開發(fā)者工具,瀏覽器是不會加載的。
線上環(huán)境一般有三種處理方案:
-
hidden-source-map:借助第三方錯誤監(jiān)控平臺 Sentry 使用 -
nosources-source-map:只會顯示具體行數(shù)以及查看源代碼的錯誤棧。安全性比 sourcemap 高 -
sourcemap:通過 nginx 設(shè)置將 .map 文件只對白名單開放(公司內(nèi)網(wǎng))
注意:避免在生產(chǎn)中使用 inline- 和 eval-,因為它們會增加 bundle 體積大小,并降低整體性能。
模塊打包原理
Webpack 把解析的所有模塊變成一個對象,然后通過入口模塊去加載我們的東西,然后依次實現(xiàn)遞歸的依賴關(guān)系,通過入口來運行所有的文件。Webpack 實際上為每個模塊創(chuàng)造了一個可以導(dǎo)出和導(dǎo)入的環(huán)境,本質(zhì)上并沒有修改代碼的執(zhí)行邏輯,代碼執(zhí)行順序與模塊加載順序也完全一致。
文件監(jiān)聽原理
在發(fā)現(xiàn)源碼發(fā)生變化時,自動重新構(gòu)建出新的輸出文件。
Webpack開啟監(jiān)聽模式,有兩種方式:
- 啟動
webpack命令時,帶上--watch參數(shù) - 在配置
webpack.config.js中設(shè)置watch:true
缺點:每次需要手動刷新瀏覽器
原理:輪詢判斷文件的最后編輯時間是否變化,如果某個文件發(fā)生了變化,并不會立刻告訴監(jiān)聽者,而是先緩存起來,等 aggregateTimeout 后再執(zhí)行。
module.export = {
// 默認(rèn)false,也就是不開啟
watch: true,
// 只有開啟監(jiān)聽模式時,watchOptions才有意義
watchOptions: {
// 默認(rèn)為空,不監(jiān)聽的文件或者文件夾,支持正則匹配
ignored: /node_modules/,
// 監(jiān)聽到變化發(fā)生后會等300ms再去執(zhí)行,默認(rèn)300ms
aggregateTimeout:300,
// 判斷文件是否發(fā)生變化是通過不停詢問系統(tǒng)指定文件有沒有變化實現(xiàn)的,默認(rèn)每秒問1000次
poll:1000
}
}
Webpack 熱更新原理
Webpack 的熱更新又稱熱替換(Hot Module Replacement),縮寫為 HMR。這個機制可以做到不用刷新瀏覽器而將新變更的模塊替換掉舊的模塊。
HMR的核心就是客戶端從服務(wù)端拉去更新后的文件,準(zhǔn)確的說是 chunk diff (chunk 需要更新的部分),實際上 Webpack-dev-server (WDS) 與瀏覽器之間維護(hù)了一個 Websocket(websocket 可建立本地服務(wù)和瀏覽器的雙向通信。),本地文件發(fā)生變化時,會通知瀏覽器熱更新代碼。
具體地說就是,當(dāng)本地資源發(fā)生變化時,WDS 會向瀏覽器推送更新,并帶上構(gòu)建時的 hash,讓客戶端與上一次資源進(jìn)行對比。客戶端對比出差異后會向 WDS 發(fā)起 Ajax 請求來獲取更改內(nèi)容(文件列表、hash),這樣客戶端就可以再借助這些信息繼續(xù)向 WDS 發(fā)起 jsonp 請求獲取該chunk的增量更新。
后續(xù)的部分(拿到增量更新之后如何處理?哪些狀態(tài)該保留?哪些又需要更新?)由 HotModulePlugin 來完成,提供了相關(guān) API 以供開發(fā)者針對自身場景進(jìn)行處理,像react-hot-loader 和 vue-loader 都是借助這些 API 實現(xiàn) HMR。具體可在這里了解
文件指紋是什么?怎么用?
文件指紋是打包后輸出的文件名的后綴。
-
Hash:和整個項目的構(gòu)建相關(guān),只要項目文件有修改,整個項目構(gòu)建的 hash 值就會更改 -
Chunkhash:和 Webpack 打包的 chunk 有關(guān),不同的 entry 會生出不同的 chunkhash -
Contenthash:根據(jù)文件內(nèi)容來定義 hash,文件內(nèi)容不變,則 contenthash 不變
JS的文件指紋設(shè)置
設(shè)置 output 的 filename,用 chunkhash。
module.exports = {
entry: {
app: './scr/app.js',
search: './src/search.js'
},
output: {
filename: '[name][chunkhash:8].js',
path:__dirname + '/dist'
}
}
CSS的文件指紋設(shè)置
設(shè)置 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`
})
]
}
圖片的文件指紋設(shè)置
設(shè)置file-loader的name,使用hash。
占位符名稱及含義
- ext 資源后綴名
- name 文件名稱
- path 文件的相對路徑
- folder 文件所在的文件夾
- contenthash 文件的內(nèi)容hash,默認(rèn)是md5生成
- hash 文件內(nèi)容的hash,默認(rèn)是md5生成
- emoji 一個隨機的指代文件內(nèi)容的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]'
}
}]
}]
}
}
在實際工程中,配置文件上百行乃是常事,如何保證各個loader按照預(yù)想方式工作?
可以使用 enforce 強制執(zhí)行 loader 的作用順序,pre 代表在所有正常 loader 之前執(zhí)行,post 是所有 loader 之后執(zhí)行。(inline 官方不推薦使用)
如何對bundle體積進(jìn)行監(jiān)控和分析?
VSCode 中有一個插件 Import Cost 可以幫助我們對引入模塊的大小進(jìn)行實時監(jiān)測,還可以使用 webpack-bundle-analyzer 生成 bundle 的模塊組成圖,顯示所占體積。
bundlesize 工具包可以進(jìn)行自動化資源體積監(jiān)控。
如何優(yōu)化 Webpack 的構(gòu)建速度?
-
多進(jìn)程/多實例構(gòu)建:HappyPack(不維護(hù)了)、thread-loader -
多進(jìn)程并行壓縮- webpack-paralle-uglify-plugin(不再維護(hù))
- uglifyjs-webpack-plugin 開啟 parallel 參數(shù) (不支持ES6)
- terser-webpack-plugin 開啟 parallel 參數(shù)(支持ES6)
-
DLL:- 使用 DllPlugin 進(jìn)行對第三方庫分包提前打包,使用 DllReferencePlugin(索引鏈接) 對 manifest.json 引用,讓一些基本不會改動的代碼先打包成靜態(tài)資源,通過 json 文件告訴webpack這些庫提前打包好了,避免反復(fù)編譯浪費時間。
- HashedModuleIdsPlugin 可以解決模塊數(shù)字id問題
-
充分利用緩存提升二次構(gòu)建速度:- babel-loader 開啟緩存
- terser-webpack-plugin 開啟緩存
- 使用 cache-loader 或者 hard-source-webpack-plugin
-
縮小構(gòu)建目標(biāo)/減少文件搜索范圍:- exclude(不需要被解析的模塊)/include(需要被解析的模塊)
- resolve.modules 告訴 webpack 解析模塊時搜索的目錄,指明第三方模塊的絕對路徑
- resolve.mainFields 限定模塊入口文件名,只采用 main 字段作為入口文件描述字段 (減少搜索步驟,需要考慮到所有運行時依賴的第三方模塊的入口文件描述字段)
- resolve.alias 當(dāng)從 npm 包中導(dǎo)入模塊時(例如,import * as React from 'react'),此選項將決定在 package.json 中使用哪個字段導(dǎo)入模塊。根據(jù) webpack 配置中指定的 target 不同,默認(rèn)值也會有所不同
- resolve.extensions 盡可能減少后綴嘗試的可能性
- noParse 對完全不需要解析的庫進(jìn)行忽略 (不去解析但仍會打包到 bundle 中,注意被忽略掉的文件里不應(yīng)該包含 import、require、define 等模塊化語句)
- IgnorePlugin (完全排除模塊)
-
動態(tài)Polyfill- 通過 Polyfill Service識別 User Agent,下發(fā)不同的 Polyfill,做到按需加載,社區(qū)維護(hù)。(部分國內(nèi)奇葩瀏覽器UA可能無法識別,但可以降級返回所需全部polyfill)
-
Scope hoisting (「作用域提升」)- 構(gòu)建后的代碼會存在大量閉包,造成體積增大,運行代碼時創(chuàng)建的函數(shù)作用域變多,內(nèi)存開銷變大。Scope hoisting 把引入的 js 文件“提升到”它的引入者頂部,其實現(xiàn)原理為:分析出模塊之間的依賴關(guān)系,盡可能的把打散的模塊合并到一個函數(shù)中去,但前提是不能造成代碼冗余。因此只有那些被引用了一次的模塊才能被合并。
- 必須是ES6的語法,因為有很多第三方庫仍采用 CommonJS 語法和 Scope Hoisting 要分析模塊之間的依賴關(guān)系,需要配置 mainFields 對第三方模塊優(yōu)先采用 jsnext:main 中指向的ES6模塊化語法
-
提取頁面公共資源:- 使用 html-webpack-externals-plugin,將基礎(chǔ)包通過 CDN 引入,不打入 bundle 中
- 使用 SplitChunksPlugin 進(jìn)行(公共腳本、基礎(chǔ)包、頁面公共文件)分離(Webpack4內(nèi)置) ,替代了 CommonsChunkPlugin 插件
- 基礎(chǔ)包分離
-
Tree shaking- purgecss-webpack-plugin 和 mini-css-extract-plugin配合使用(建議)
- 打包過程中檢測工程中沒有引用過的模塊并進(jìn)行標(biāo)記,在資源壓縮時將它們從最終的bundle中去掉(只能對ES6 Modlue生效) 開發(fā)中盡可能使用ES6 Module的模塊,提高tree shaking效率
- 禁用 babel-loader 的模塊依賴解析,否則 Webpack 接收到的就都是轉(zhuǎn)換過的 CommonJS 形式的模塊,無法進(jìn)行 tree-shaking
- 使用 PurifyCSS(不在維護(hù)) 或者 uncss 去除無用 CSS 代碼
更多優(yōu)化請參考官網(wǎng)-構(gòu)建性能
代碼分割的本質(zhì)是什么?有什么意義呢?
代碼分割的本質(zhì)其實就是在源代碼直接上線和打包成唯一腳本main.bundle.js這兩種極端方案之間的一種更適合實際場景的中間狀態(tài)。
「用可接受的服務(wù)器性能壓力增加來換取更好的用戶體驗?!?/strong>
- 源代碼直接上線:雖然過程可控,但是http請求多,性能開銷大。
- 打包成唯一腳本:一把梭完自己爽,服務(wù)器壓力小,但是頁面空白期長,用戶體驗不好。
是否寫過Loader?簡單描述一下編寫loader的思路?
Loader 支持鏈?zhǔn)秸{(diào)用,所以開發(fā)上需要嚴(yán)格遵循“單一職責(zé)”,每個 Loader 只負(fù)責(zé)自己需要負(fù)責(zé)的事情。
Loader的API 可以去官網(wǎng)查閱
- Loader 運行在 Node.js 中,我們可以調(diào)用任意 Node.js 自帶的 API 或者安裝第三方模塊進(jìn)行調(diào)用
- Webpack 傳給 Loader 的原內(nèi)容都是 UTF-8 格式編碼的字符串,當(dāng)某些場景下 Loader 處理二進(jìn)制文件時,需要通過 exports.raw = true 告訴 Webpack 該 Loader 是否需要二進(jìn)制數(shù)據(jù)
- 盡可能的異步化 Loader,如果計算量很小,同步也可以
- Loader 是無狀態(tài)的,我們不應(yīng)該在 Loader 中保留狀態(tài)
- 使用 loader-utils 和 schema-utils 為我們提供的實用工具
- 加載本地 Loader 方法
- Npm link
- ResolveLoader
是否寫過Plugin?簡單描述一下編寫Plugin的思路?
webpack在運行的生命周期中會廣播出許多事件,Plugin 可以監(jiān)聽這些事件,在特定的階段鉤入想要添加的自定義功能。Webpack 的 Tapable 事件流機制保證了插件的有序性,使得整個系統(tǒng)擴展性良好。
Plugin的API 可以去官網(wǎng)查閱
- compiler 暴露了和 Webpack 整個生命周期相關(guān)的鉤子
- compilation 暴露了與模塊和依賴有關(guān)的粒度更小的事件鉤子
- 插件需要在其原型上綁定apply方法,才能訪問 compiler 實例
- 傳給每個插件的 compiler 和 compilation對象都是同一個引用,若在一個插件中修改了它們身上的屬性,會影響后面的插件
- 找出合適的事件點去完成想要的功能
- emit 事件發(fā)生時,可以讀取到最終輸出的資源、代碼塊、模塊及其依賴,并進(jìn)行修改(emit 事件是修改 Webpack 輸出資源的最后時機)
- watch-run 當(dāng)依賴的文件發(fā)生變化時會觸發(fā)
- 異步的事件需要在插件處理完任務(wù)時調(diào)用回調(diào)函數(shù)通知 Webpack 進(jìn)入下一個流程,不然會卡住
聊一聊Babel原理吧
大多數(shù)JavaScript Parser遵循 estree 規(guī)范,Babel 最初基于 acorn 項目(輕量級現(xiàn)代 JavaScript 解析器),Babel 是對瀏覽器識別不了的代碼進(jìn)行轉(zhuǎn)換兼容的庫,Babel大概分為三大部分:
-
Parser解析:將代碼轉(zhuǎn)換成抽象語法樹 (AbstractSyntaxTree,簡稱 AST)- 詞法分析:將字符串形式的代碼分割為
令牌(token)流,即語法單元成的數(shù)組 - 語法分析:將token流轉(zhuǎn)換成 AST
- 詞法分析:將字符串形式的代碼分割為
-
Transformer轉(zhuǎn)換:根據(jù)配置好的plugins/presets把Parser生成的 AST 轉(zhuǎn)變?yōu)樾碌?AST- Taro就是利用 babel 完成的小程序語法轉(zhuǎn)換
-
Generator生成:把新的 AST 生成代碼
想了解如何一步一步實現(xiàn)一個編譯器的同學(xué)可以移步 Babel 官網(wǎng)曾經(jīng)推薦的開源項目the-super-tiny-compiler