本週精讀的是 webpack4.0 一些變化,以及 typescript 該怎麼做才能最大化利用 webpack4.0 的所有特性。
1 引言
前段時間嘗試了 parcel 作為構建工具,就像農村人享受了都市的生活,就再也回不去了一樣,發現無配置真是前端構建工具的大趨勢,用起來非常方便快捷,再也不想碰 webpack 的配置了。
可是實踐一段實踐後,發現 parcel 還是不夠成熟,主要體現在暫時不支援一些 rollup 優秀特性:Tree shaking、Scope Hoist,大型專案打包速度反而比 webpack3.0 慢。由於筆者完全零配置,當發現構建速度急速下降時,自然把矛頭指向了 parcel :p.
就在前幾周,webpack4.0 釋出了,也擁抱了零配置,我想,是時候再回到 webpack 了。可是,文件好少,怎麼遷移呢?
就在這幾天,webpack 文件釋出了 4.0 版本,雖然遺留了大量舊文件,不過也足夠參考了。
2 精讀
筆者嘗試了 webpack node api,嘗試了很久,發現被坑了。文件裡隻字未提 mode
模式,4.0 環境下 compiler
總是提示沒有 mode
的 warning。
讀了一些文件,發現 webpack4.0 大力度宣傳的是 cli 方式啟動,裡面提到了最重要的 webpack --mode
模式,可見 webpack4.0 更推崇的是讓開發者使用高度封裝的 cli,而不是使用 node 方式開發(那 node 文件也應該更新呀)。筆者又看了一圈,發現 webpack-dev-server
的 webpack 版本升到了 4.0,ts-loader
也升級到了 4.0,可能生態已經全部準備好了。
使用 webpack cli、webpack-dev-server cli
安裝 webpack^4.1.1
webpack-cli^2.0.10
webpack-dev-server^3.1.0
,以及建立一個公共配置檔案 webpack.config.ts
:
export default {
entry,
output,
module: {
rules
},
resolve,
resolveLoader,
devServer: {
https: true,
open: true,
overlay: {
warnings: true,
errors: true
},
port
}
}
複製程式碼
記得用 tsc
轉換為 webpack.config.js
作為 cli 入口。
開發模式下使用 webpack-dev-server
:
webpack-dev-server --mode development --progress --hot --hotOnly --config ./webpack.config.js
複製程式碼
生產環境 build 使用 webpack
:
webpack --mode production --progress --config ./webpack.config.js
複製程式碼
開發/生產模式,都以 webpack.config.ts
作為配置,其中 devServer
項僅在開發模式下,對 webpack-dev-server
生效。
一旦開啟了 --mode production
,會自動開啟程式碼壓縮、scope hoist 等外掛,以及自動傳遞環境變數給 lib 包,所以已經不需要 plugins
這個配置項了。同理,開啟了 --mode development
會自動開啟 sourceMap 等開發外掛,我們只要關心更簡單的配置,這就是 4.0 零配置的重要改變。
mode=production
,mode=development
具體內建了哪些配置,可以參考這篇文章:webpack 4 終於知道「約定優於配置」了。恰恰有意思的是,webpack4 這麼做,就是不想我們浪費時間瞭解這些機制,社群應該會慢慢習慣零配置的開發方式。
當然,雖然說零配置,但配置檔案基本三板斧還是非常有必要配置:entry
output
module
。
我們可能還要給配置檔案傳一些引數,比如定製多種開發模式的入口,通過 --env
傳遞:
webpack-dev-server --mode development --env.entry ./src/main.tsx
複製程式碼
webpack.config.ts
接收:
const entry = yargs.argv.env.entry
複製程式碼
使用 typescript + webpack
簡單來說,只需要 ts-loader
就夠了。在 webpack.config.ts
中增加新的 rules
:
{
module: {
rules: [{
test: /\.(tsx|ts)?$/,
use: ["ts-loader"]
}]
}
}
複製程式碼
注意 tsconfig.json
中模組解析策略使用: "module": "esnext"
。
原因是 webpack 需要 es6 import 語句,才能進行 tree shaking 或者動態 import 優化,我們不再讓 ts-loader
包辦模組設定,換句話說,我們採用白名單方式看待 typescript
以及 babel
,只讓他做我們需要的工作,剩下的丟給 webpack 處理,可以獲得最大程度效能優化。
如果僅使用 webpack + typescript,建議將 ts 編譯輸出模式調整為 es3
,因為 webpack 自帶的壓縮工具對 es6 語法還存在報錯,而且也不會做相容處理。
使用 typescript + babel + webpcak
注意處理順序,ts -> babel -> webpack。
因為多出了 babel,我們將 ts 編譯相容模式關閉:"target": "esnext"
,模組也不要解析:"module": "esnext"
,ts-loader
僅僅將 typescript 程式碼轉換成 js,其他一切優化都不要做,將 esnext 原生程式碼直接傳給 babel 處理。
babel 這一層的職責是對程式碼進行相容處理,不要壓縮,也不要把 import 轉成 require。筆者發現 babel 直接解析 import 程式碼會無法處理,因此需要 stage-2
preset:
{
presets: [
["env", {
modules: false,
}],
["stage-2"]
],
plugins: [
["transform-runtime"]
],
comments: true
}
複製程式碼
從上面配置可以看到,babel 這層對 esnext 的程式碼進行了瀏覽器相容處理(env 外掛),直接透傳 import
(stage-2 外掛讓 babel 識別 esModule),以及支援 async await(transform-runtime) 外掛。
本來想用 env 替代 transform-runtime 的功能,筆者暫時沒有查詢到可行方式,歡迎讀者補充。
另外要允許 babel 保留註釋(comments: true
),因為 webpack import 支援自定義 chunkName 是通過註釋的方式:
import(/* webpackChunkName: "src" */ "./src")
複製程式碼
配合 react-loadable
使用更佳:
Loadable({
loader: () => import(/* webpackChunkName: "src" */ "./src"),
loading: (): any => null
})
複製程式碼
因為 react-loadable
讓頁面按 chunk 方式打包,而 webpack 又會自動 picke shared chunks,配合給每個 page chunks 通過 webpackChunkName
定義名稱,webpack 可以給每個共享 chunks 更加可讀的名字,比如:vendor~src,about,login
,你就知道這個是 src
about
login
三個頁面間公共模組。
可能已經有人看出瑕疵了,給每個檔案增加 webpackChunkName
註釋既麻煩又不優雅,而且只要有一個開發者沒有加這個註釋,上面說的可讀 chunks 可能就缺少了某個模組名。
這就要筆者之前一篇精讀來看了:精讀《Rekit Studio》,專案可以通過約定的方式定義頁面,入口檔案通過 cli 自動生成,不就既減少業務代量,又統一加上了 webpackChunkName
嘛?
這裡小小安利下整合了這個思路的專案腳手架 pri,使用了 ts + babel + webpack4.0,上述的小優化也是內建的功能之一。
webpack4 帶來的是適配成本的大幅優化
社群似乎有部分聲音在抱怨,webpack 又發新版本,我們又要適配一輪。其實 webpack 這麼做恰恰沒有帶來適配成本,出問題的在於我們對 webpack 的使用方式與理念。
如果我們開始就將 webpack 當作一體化打包方案,開發除錯使用 webpack-dev-server cli
,開發環境編譯使用 webpack cli
,那麼 webpack4 其實只是補充了開發環境這個最重要的配置變數而已。類比 parcel
的兩個命令:
parcel index.html
parcel build index.html
複製程式碼
對應:
webpack-dev-server --mode development
webpack --mode production
複製程式碼
所以 webpack4 幾乎是有史以來最方便使用與遷移的版本,前提是使用思維得正確,捨得將編譯環節全權交給兩個官方的 Cli。
3 總結
只要合理的使用 typescript、babel,讓各自只發揮最小功能,將原生的模組化程式碼拋給 webpack,再配合 --mode production
配置,webpack 會自動開啟一切可能的外掛優化你的專案,而我們再不需要閱讀形形色色的 webpack 外掛了,更令人激動的是,隨著 webpack 版本升級,優化會不斷升級,而我們只要留著 --mode
引數,不需要改一行配置。
總結起來,就是不用關心優化相關的配置,我們只需要配置業務相關的 entry
output
module
,這就是 webpack4.0.
我以前為了實現第一次編譯完後立即開啟瀏覽器的功能,寫了一共 200 行的 customCompiler
以及 format-webpack-message
,而且利用 koa 開了一個 server,利用 await 和 flags 等待第一次編譯完的時機,並利用 opn
庫開啟網頁。
其實用 cli 只需要 webpack-dev-server --open
。
隨著新的一波零配置浪潮,真的不應該在編譯配置上花那麼多時間了。
4 番外 - prefetch
讀者自習閱讀就會發現,這不是一篇單純 webpack4 升級指南,仔細閱讀可以發現文中蘊藏的一些工程優化思路。文章末尾再給一波福利,分析一下 prefetch 優化是什麼,以及怎麼做。
現代瀏覽器支援了以下兩種語法:
<link rel="preload" />
<link rel="prefetch" />
複製程式碼
相容性自己查 Caniuse,筆者重點在功能上。preload
收集當前用到的資源,prefetch
收集未來用到的資源。
頁面本質上也是未來一種資源,如果認為使用者會點選另一個頁面(如果對產品沒自信,或者 pv 過低可以忽略這個功能),就可以用 prefetch
讓瀏覽器在空閒時間下載下一個頁面的 chunk 檔案。
前端包體積優化效率一般和使用者體驗是違背的,既然下一個頁面在另一個 chunk 中,使用者點選後必然會產生 loading。可是如果結合了 prefetch
,魚和熊掌就兼得了(正常使用者不可能頁面還沒載入完就立刻點按鈕跳頁,所以唯一的缺點幾乎不會對正常使用者產生影響)。
api 有了,那麼最大的問題就是,當前頁面怎麼知道要載入哪些 chunks?一般兩種做法:
全量模式 使用比如 preload-webpack-plugin 外掛,將所有生成的 chunk 都作為 prefetch
資源,在所有頁面中。幾乎所有規模的專案都不會產生過多的 chunks,所以這個方案理論上不夠優雅,但能解決實際問題。
按需模式,是理論和實踐雙重優雅的方案,是否要這麼做取決於您是否有程式碼潔癖。方法是提供一個定製的 Link
標籤,根據 URL 地址按需生成 prefetch
標籤。這種方案最大缺陷是,如果使用者不按照約定使用內建的 Link
,prefetch
規則將會無效。
5 更多討論
如果你想參與討論,請點選這裡,每週都有新的主題,每週五發布。