系列文章
Webpack系列-第一篇基礎雜記
Webpack系列-第二篇外掛機制雜記
Webpack系列-第三篇流程雜記
前言
公司的前端專案基本都是用Webpack來做工程化的,而Webpack雖然只是一個工具,但內部涉及到非常多的知識,之前一直靠CV來解決問題,之知其然不知其所以然,希望這次能整理一下相關的知識點。
簡介
這是webpack官方的首頁圖
本質上,webpack 是一個現代 JavaScript 應用程式的靜態模組打包器(module bundler)。當 webpack 處理應用程式時,它會遞迴地構建一個依賴關係圖(dependency graph),其中包含應用程式需要的每個模組,然後將所有這些模組打包成一個或多個 bundle。
那麼打個比方就是我們搭建一個專案好比搭建一個房子,我們把所需要的材料(js檔案、圖片等)交給webpack,最後webpack會幫我們做好一切,並把房子(即bundle)輸出。
webpack中有幾個概念需要記住
entry(入口)
入口起點(entry point)即是webpack通過該起點找到本次專案所直接或間接依賴的資源(模組、庫等),並對其進行處理,最後輸出到bundle中。入口檔案由使用者自定義,可以是一個或者多個,每一個entry最後對應一個bundle。
output(出口)
通過配置output屬性可以告訴webpack將bundle命名並輸出到對應的位置。
loader
webpack核心,webpack本身只能識別js檔案,對於非js檔案,即需要loader轉換為js檔案。換句話說,,Loader就是資源轉換器。由於在webpack裡,所有的資源都是模組,不同資源都最終轉化成js去處理。針對不同形式的資源採用不同的Loader去編譯,這就是Loader的意義。
外掛(plugin)
webpack核心,loader處理非js檔案,那麼外掛可以有更廣泛的用途。整個webpack其實就是各類的外掛形成的,外掛的範圍包括,從打包優化和壓縮,一直到重新定義環境中的變數。外掛介面功能極其強大,可以用來處理各種各樣的任務。
Chunk
被entry所依賴的額外的程式碼塊,同樣可以包含一個或者多個檔案。chunk也就是一個個的js檔案,在非同步載入中用處很大。chunk實際上就是webpack打包後的產物,如果你不想最後生成一個包含所有的bundle,那麼可以生成一個個chunk,並通過按需載入引入。同時它還能通過外掛提取公共依賴生成公共chunk,避免多個bundle中有多個相同的依賴程式碼。
配置
webpack的相關配置語法官方文件比較詳細,這裡就不贅述了。
指南
配置
實踐&優化
url-loader & image-webpack-loader
url-loader 可以在檔案大小(單位 byte)低於指定的限制,將檔案轉換為DataURL,這在實際開發中非常有效,能夠減少請求數,在vue-cli和create-react-app中也都能看到對這個loader的使用。
// "url" loader works just like "file" loader but it also embeds
// assets smaller than specified size as data URLs to avoid requests.
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
loader: require.resolve('url-loader'),
options: {
limit: 10000,
name: 'static/media/[name].[hash:8].[ext]',
},
},
複製程式碼
image-webpack-loader 這是一個可以通過設定質量引數來壓縮圖片的外掛,但個人覺得在實際開發中並不會經常使用,圖片一般是UI提供,一般來說,他們是不會同意圖片的質量有問題。
資源私有化
以這種方式載入資源,你可以以更直觀的方式將模組和資源組合在一起。無需依賴於含有全部資源的 /assets 目錄,而是將資源與程式碼組合在一起。例如,類似這樣的結構會非常有用
- |- /assets
+ |– /components
+ | |– /my-component
+ | | |– index.jsx
+ | | |– index.css
+ | | |– icon.svg
+ | | |– img.png
複製程式碼
當然,這種選擇見仁見智
Tree-Shaking
前端中的tree-shaking就是將一些無關的程式碼刪掉不打包。在Webpack專案中,我們通常會引用很多檔案,但實際上我們只引用了其中的某些模組,但卻需要引入整個檔案進行打包,會導致我們的打包結果變得很大,通過tree-shaking將沒有使用的模組搖掉,這樣來達到刪除無用程式碼的目的。
歸納起來就是
1.ES6的模組引入是靜態分析的,故而可以在編譯時正確判斷到底載入了什麼程式碼。
2.分析程式流,判斷哪些變數未被使用、引用,進而刪除此程式碼
歸納起來就是
因為Babel的轉譯,使得引用包的程式碼有了副作用,而副作用會導致Tree-Shaking失效。
Webpack 4 預設啟用了 Tree Shaking。對副作用進行了消除,以下是我在4.19.1的實驗
index.js
import { cube } from './math.js'
console.log(cube(5))
複製程式碼
math.js
// 不打包square
export class square {
constructor() {
console.log('square')
}
}
export class cube {
constructor(x) {
return x * x * x
}
}
複製程式碼
// babel編譯後 同不打包
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.cube = cube;
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var square = exports.square = function square() {
_classCallCheck(this, square);
console.log('square');
};
function cube(x) {
console.log('cube');
return x * x * x;
}
複製程式碼
// 不打包
export function square(x) {
console.log('square')
return x.a
}
export function cube (x) {
return x * x * x
}
複製程式碼
// wow 被打包
export function square() {
console.log('square')
return x.a
}
square({a: 1})
export function cube () {
return x * x * x
}
複製程式碼
sourcemap
簡單說,Source map就是一個資訊檔案,裡面儲存著位置資訊。也就是說,轉換後的程式碼的每一個位置,所對應的轉換前的位置。
有了它,出錯的時候,除錯工具將直接顯示原始程式碼,而不是轉換後的程式碼。這無疑給開發者帶來了很大方便。
webpack中的devtool配置項可以設定sourcemap,可以參考官方文件然而,devtool的許多選項都講的不是很清楚,這裡推薦該文章,講的比較詳細
要注意,避免在生產中使用 inline-* 和 eval-*,因為它們可以增加 bundle 大小,並降低整體效能。
模組熱替換
熱替換這一塊目前大多數都是用的webpack-dev-middleware外掛配合伺服器使用的,而官方提供的watch模式反而比較少用,當然,webpack-dev-middleware的底層監聽watch mode,至於為什麼不直接使用watch模式,則是webpack-dev-middleware快速編譯,走記憶體;只依賴webpack的watch mode來監聽檔案變更,自動打包,每次變更,都將新檔案打包到本地,就會很慢。
DefinePlugin
webpack.DefinePlugin 定義環境變數process.env,這在實際開發中比較常用,參考create-react-app中的程式碼如下:
// Makes some environment variables available to the JS code, for example:
// if (process.env.NODE_ENV === 'development') { ... }. See `./env.js`.
new webpack.DefinePlugin(env.stringified),
複製程式碼
不過,要注意不能在config中使用,因為
process.env.NODE_ENV === 'production' ? '[name].[hash].bundle.js' : '[name].bundle.js'
複製程式碼
NODE_ENV
is set in the compiled code, not in the webpack.config.js file. You should not use enviroment variables in your configuration. Pass options via--env.option abc
and export a function from the webpack.config.js.
大致意思就是NODE_ENV是設定在compiled裡面,而不是config檔案裡。
ExtractTextWebpackPlugin
ExtractTextWebpackPlugin,將css抽取成單獨檔案,可以通過這種方式配合後端對CSS檔案進行快取。
SplitChunksPlugin
webpack4的程式碼分割外掛。
webpack4中支援了零配置的特性,同時對塊打包也做了優化,CommonsChunkPlugin已經被移除了,現在是使用optimization.splitChunks代替。
SplitChunksPlugin的配置有幾個需要比較關注一下
chunks: async | initial | all
- async: 預設值, 將按需引用的模組打包
- initial: 分開優化打包非同步和非非同步模組
- all: all會把非同步和非非同步同時進行優化打包。也就是說moduleA在indexA中非同步引入,indexB中同步引入,initial下moduleA會出現在兩個打包塊中,而all只會出現一個。
cacheGroups
使用cacheGroups可以自定義配置打包塊。
更多詳細內容參考該文章
動態引入
則是利用動態引入的檔案打包成另一個包,並懶載入它。其與SplitChunksPlugin的cacheGroups區別:
- Bundle splitting:實際上就是建立多個更小的檔案,並行載入,以獲得更好的快取效果;主要的作用就是使瀏覽器並行下載,提高下載速度。並且運用瀏覽器快取,只有程式碼被修改,檔名中的雜湊值改變了才會去再次載入。
- Code splitting:只載入使用者最需要的部分,其餘的程式碼都遵從懶載入的策略;主要的作用就是加快頁面載入速度,不載入不必要載入的東西。 參考程式碼:
+ import _ from 'lodash';
+
+ function component() {
var element = document.createElement('div');
+ var button = document.createElement('button');
+ var br = document.createElement('br');
+ button.innerHTML = 'Click me and look at the console!';
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+ element.appendChild(br);
+ element.appendChild(button);
+
+ // Note that because a network request is involved, some indication
+ // of loading would need to be shown in a production-level site/app.
+ button.onclick = e => import(/* webpackChunkName: "print" */ './print').then(module => {
+ var print = module.default;
+
+ print();
+ });
return element;
}
+ document.body.appendChild(component());
複製程式碼
注意當呼叫 ES6 模組的 import() 方法(引入模組)時,必須指向模組的 .default 值,因為它才是 promise 被處理後返回的實際的 module 物件。
快取runtimeChunk
因為webpack會把執行時程式碼放到最後的一個bundle中, 所以即使我們修改了其他檔案的程式碼,最後的一個bundle的hash也會改變,runtimeChunk是把執行時程式碼單獨提取出來的配置。這樣就有利於我們和後端配合快取檔案。
配置項
- single: 所有入口共享一個生成的runtimeChunk
- true/mutiple: 每個入口生成一個單獨的runtimeChunk
模組識別符號
有時候我們只是新增了個檔案print.js, 並在index引入
import Print from './print'
複製程式碼
打包的時候,期望只有runtime和main兩個bundle的hash發生改變,但是通常所有bundle都發生了變化,因為每個 module.id 會基於預設的解析順序(resolve order)進行增量。也就是說,當解析順序發生變化,ID 也會隨之改變。
可以使用兩個外掛來解決這個問題。第一個外掛是 NamedModulesPlugin,將使用模組的路徑,而不是數字識別符號。雖然此外掛有助於在開發過程中輸出結果的可讀性,然而執行時間會長一些。第二個選擇是使用 HashedModuleIdsPlugin。
參考文章
ProvidePlugin
通過ProvidePlugin處理全域性變數
其他更細粒度的處理
polyfills的處理
首先了解一下polyfills, 雖然在webpack中能夠使用es6\es7等的API,但並不代表編譯器支援這些API,所以通常我們會用polyfills來自定義一個API。
那麼在webpack中,一般是使用babel-polyfill VS babel-runtime VS babel-preset-env等來支援這些API,而這三種怎麼選擇也是一個問題。
在真正進入主題之前,我們先看一個preset-env的配置項,同時也是package.json中的一個配置項browserslist
{
"browserslist": [
"last 1 version",
"> 1%",
"maintained node versions",
"not dead"
]
}
複製程式碼
根據這個配置,preset-env或者postcss等會根據你的引數支援不同的polyfills,具體的引數配置參考該文章
另外推薦一個網站,可以看各種瀏覽器的使用情況。
- babel-polyfill 只需要引入一次,但會重寫一些原生的已支援的方法,而且體積很大。
- transform-runtime 是利用 plugin 自動識別並替換程式碼中的新特性,你不需要再引入,只需要裝好 babel-runtime 和 配好 plugin 就可以了。好處是按需替換,檢測到你需要哪個,就引入哪個 polyfill,值得注意的是,instance 上新新增的一些方法,babel-plugin-transform-runtime 是沒有做處理的,比如 陣列的 includes, filter, fill 等
- babel-preset-env 根據當前的執行環境,自動確定你需要的 plugins 和 polyfills。通過各個 es標準 feature 在不同瀏覽器以及 node 版本的支援情況,再去維護一個 feature 跟 plugins 之間的對映關係,最終確定需要的 plugins。
參考文章
後編譯
日常我們引用的Npm包都是編譯好的,這樣帶來的方便的同時也暴露了一些問題。
程式碼冗餘:一般來說,這些 NPM 包也是基於 ES2015+ 開發的,每個包都需要經過 babel 編譯釋出後才能被主應用使用,而這個編譯過程往往會附加很多“編譯程式碼”;每個包都會有一些相同的編譯程式碼,這就造成大量程式碼的冗餘,並且這部分冗餘程式碼是不能通過 Tree Shaking 等技術去除掉的。
非必要的依賴:考慮到元件庫的場景,通常我們為了方便一股腦引入了所有元件;但實際情況下對於一個應用而言可能只是用到了部分元件,此時如果全部引入,也會造成程式碼冗餘。
所以我們自己的公司元件可以採用後編譯的形式,即釋出的是未經編譯的npm包,在專案構建時才編譯,我們公司採用的也是這種做法,因為我們的包都在一個目錄下,所以不用考慮遞迴編譯的問題。
更多的詳細請直接參考該文章
設定環境變數
這個比較簡單,直接看程式碼或者官方文件即可
webpack --env.NODE_ENV=local --env.production --progress
複製程式碼
其他外掛
- CompressionWebpackPlugin 將檔案壓縮 檔案大小減小很多 需要後端協助配置
- mini-css-extract-plugin將CSS分離出來
- wbepack.IgnorePlugin忽略匹配的模組
- uglifyjs-webpack-plugin程式碼醜化,webpack4的mode(product)自動配置
- optimize-css-assets-webpack-plugincss壓縮
- webpack-md5-hash使你的chunk根據內容生成md5,用這個md5取代 webpack chunkhash。
- dllPlugin提高構建速度
總結
Webpack本身並不難於理解,難的是各種各樣的配置和周圍生態帶來的複雜,然而也是這種複雜給我們帶來了極高的便利性,理解這些有助於在搭建專案更好的優化。後面會繼續寫出兩篇總結,分別是webpack的內部原理流程和webpack的外掛開發原理。