帶你瞭解webpack

ZHei發表於2019-02-16

1. 前端工程化專案打包歷史

前端工程化之前的時代略過

1. 半自動執行指令碼來壓縮合並檔案

自從xmlhttprequest被挖掘出來,網頁能夠和服務端通訊,js能做的事越來越多,檔案體積越來越大,互相引用原來越多。
然而網速只有幾兆的頻寬。於是想到,要把js檔案狠狠的壓縮,能合併的就給合併起來

主要有的js壓縮工具:

JSMin 使用簡單、靈活,對不同語言、環境支援好。一般配合不同的環境、語言用命令列執行壓縮
YUI Compressor 雅虎推出。有JAVA版本和.NET版本,需兩者環境配合
UglifyJS 基於nodejs,壓縮策略較為安全,所以不會對原始碼進行大幅度的改造。
Closure Compiler 谷歌出品

在我理解,壓縮主要做了區域性變數命名簡化、空格/換行/註釋消除、自動優化可簡化的語法等工作。

使用壓縮工具用es6語法寫的js在壓縮測試比較中:

  1. UglifyJS壓縮率高,可以自動格式化、優化程式碼。所以普及率高。現在也是主流的工具
  2. YUI compressor 好處就是壓縮策略安全,相比UglifyJS,自動優化程式碼的程度較保守
  3. Closure Compiler 的Advanced模式直接破壞程式碼的結構,bug多

壓縮有了,但是當a檔案和b檔案都引用了c檔案的方法時,如果把c檔案分別和a、b合併,這樣就只有兩個檔案了。這就是最開始的合併方式。
一般是通過在windows上用bat指令碼或者mac/linux上的shell指令碼來決定合併哪些檔案、用什麼工具壓縮、怎麼壓縮

  • 進步:
  1. 解決當時網速普遍較慢的情況下網頁載入資源較慢的問題
  2. 程式碼混淆了不容易被盜用
  • 存在的問題:
  1. 工程化的專案裡相互依賴關係變得非常複雜
  2. 合併的檔案裡可能會有很多無用程式碼

2. 自動構建的嘗試階段

通過指令碼構建的專案裡的檔案相互依賴複雜,命名什麼的完全有可能一不小心就衝突了,而且依賴可能是一層一層依賴下去的,維護起來要命呀。
所以先解決js相互依賴的問題

1. js依賴關係的規範探索

CommonJS規範解決了js模組化依賴的問題

CommonJs規範的簡單介紹(nodejs版)
  1. 一個檔案就是一個模組
  2. 每個模組內部,都有一個module物件,代表當前模組,有規定好的一些預設屬性
  3. module.exports屬性:初始值是一個空物件{},這個變數把定義的變數、方法暴露出去
  4. exports變數:Node為每個模組提供一個exports變數,指向module.exports。至於兩者的區別,不用去了解,平常就用module.exports
  5. require引用:把module.exports定義的變數/方法來過來用
  6. 注意:它是同步的

既然CommonJS提供了模組化的思路,也已經在服務端(nodejs)裡大展手腳。那麼瀏覽器裡可不可行?
瀏覽器不相容CommonJS的原因首先是,缺少了4個NodeJs環境變數:

  • module
  • exports
  • require
  • global

那麼只需要提供了這些個環境變數就行了吧。Browerify就是做這的

Browserify 是目前最常用的 CommonJS 格式轉換的工具

Browserify的核心思路是講module暴露出的模組放入一個陣列,require時根據模組id找到相應的module執行,總之就是給上面缺少的變數寫成可執行的es5的策略

那麼是不是這樣就能在瀏覽器上愉快使用CommonJS?

CommoJS是同步require的方式獲取js模組,在瀏覽器上會阻斷主執行緒。頁面會因載入js可能卡住

這肯定是不能容忍的

於是AMD(非同步模組定義)誕生
AMD也採用require()語句載入模組,但是要傳兩個引數require([module], callback)。是的,回撥思路

AMD規範的簡單介紹(RequireJS)
  1. 解決兩個問題:

(1)實現js檔案的非同步載入,避免網頁失去響應
(2)管理模組之間的依賴性,便於程式碼的編寫和維護

  1. 模組必須採用特定的define()函式來定義
  2. 非AMD的第三方庫載入之前要用require.config()定義固有特徵

CMD規範 和AMD大同小異,具體實現是seajs。沒用過,應該都差不多吧,啊哈哈

2. html/css模組化的規範

less,sass,stylus 的 css 前處理器簡化css語法
ejs,jade 等html的模板語法

這些真的是前端狗的福音,不多說,css-next來了,繼續啃咯。

這樣html/css/js 就都有了適合自動構建的擴充套件結構。但是這時候寫一個構建這些依賴的命令太長太複雜,所以打包工具開始流行:

3.Grunt/Gulp 流處理構建工具讓前端構建更容易

grunt 寫法簡單,外掛還賊多
gulp 效率更高,可擴充套件性更強

nodejs配合這倆大佬做web專案的自動化構建用著都挺爽的

var gulp = require(`gulp`)
var nodemon = require(`gulp-nodemon`)
var browserSync = require(`browser-sync`).create()

gulp.task(`nodemon`, function(cb) {
    var started = false
    return nodemon({
        script: `mswadmin.js`
        , ext: `js`
        , env: { `NODE_ENV`: `default` }  
    }).on(`start`, function() {
      if (!started) {
          cb();
        started = true;
      }
    })
});
gulp.task(`serve`, function(){
    browserSync.init({
    proxy: `http://10.3.10.27:18282`,
    browser: `chrome`,
    port: 18282
  })
    gulp.watch(`static/**/*.+(scss|jade|ls)`, [`inject`])
    .on(`change`, browserSync.reload);
})
gulp.task(`default`, [`nodemon`,`serve`]);

上面是一個用nodemon監控本地服務+watch程式碼熱更新的配置。可以看出,以流任務的方式一個個執行。用起來也簡單

2. SPA(Single-page application)來了

js 對應的 AMD 模組,然後該 AMD 模組渲染對應的 html 到容器內

這樣網頁不再是傳統的文件一類的頁面了。而是更像一個完整的程式。一個主入口,js完成的前端路由,AMD模組完成頁面內重新渲染。
雖然是做出來這個SPA了,但是小問題多:

  1. 很多成熟的第三方庫不支援AMD規範,引用起來賊麻煩
  2. RequireJS在載入html依賴時,html裡的img路徑要使用絕對路徑
  3. 只能一次性載入所有css檔案
  4. 分模組打包js檔案時的通用依賴項很難配置
  5. 最重要的,AMD/CMD CommonJS規範太多造成很多第三方庫對規範支出不夠。。。而且ES6規範都要普及了,你不用???

3. webpack來解救你

首先,webpack是靜態模組打包器(bundler),grunt/gulp是流任務執行器。
區分兩者可以用grunt-webpack形象說明:你可以將 webpack 或 webpack-dev-server 作為一項任務(task)執行

webpack為啥好用:

  1. webpack 能夠為ES6的 import/export 提供開箱即用般的支援
  2. 還支援CommonJS CMD/AMD模組規範,做到隨時可用

這兩點是我覺得最突出的地方,詳細對比請參考對比

瀏覽器環境下,用了ES6規範的話,你應該不想用其他的了

webpack的工作步驟如下:

  1. 從入口檔案開始遞迴地建立一個依賴關係圖。
  2. 把所有檔案都轉化成模組函式。
  3. 根據依賴關係,按照配置檔案把模組函式分組打包成若干個bundle。
  4. 通過script標籤把打包的bundle注入到html中,通過manifest檔案來管理bundle檔案的執行和載入。

打包的規則為:一個入口檔案對應一個bundle。該bundle包括入口檔案模組和其依賴的模組。按需載入的模組或需單獨載入的模組則分開打包成其他的bundle。

除了這些bundle外,還有一個特別重要的bundle,就是manifest.bundle.js檔案,即webpackBootstrap。這個manifest檔案是最先載入的,負責解析webpack打包的其他bundle檔案,使其按要求進行載入和執行。
無論你選擇哪種模組語法,那些 import 或 require 語句現在都已經轉換為 webpack_require 方法,此方法指向模組識別符號(module identifier)。通過使用 manifest 中的資料,runtime 將能夠查詢模組識別符號,檢索出背後對應的模組。

webpack 怎麼入門

雖然網上有很多 十分鐘入門webpack 的教程。但還是推薦去擼一遍webpack官方指南

個人覺得指南里你要注意的細節:

  1. webpack 不會更改程式碼中除 import 和 export 語句以外的部分。如果你在使用其它 ES2015 特性,請確保你在 webpack 的 loader 系統中使用了一個像是 Babel 或 Bublé 的轉譯器
  2. npm指令碼執行時預設可以使用npx命令
  3. source map要合理使用
  4. 留意webpack-dev-middleware,配合express做服務端渲染要用到哦
  5. HMR(模組熱替代)一般用你選用的框架自帶的loader(vue-loader)
  6. 用UglifyJsPlugin外掛自動移除 JavaScript 上下文中的未引用程式碼(dead-code)。webpack4裡使用 mode=production 替代。要結合SideEffects使用,webpack4又提供了SideEffects外掛使用的方式
  7. process.env.NODE_ENV === `production` ? `[name].[hash].bundle.js` : `[name].bundle.js` 這樣的條件語句在配置檔案裡無法使用,用if/else
  8. splitChunks優化,webpack4已經移除了CommonsChunkPlugin。下文會詳細解釋
  9. dynamic imports(動態匯入)優化,chunkFilename決定非入口 chunk 的名稱,vue裡的運用例項就是路由懶載入(vue-lazyload),生成了新的bundle

4. webpack的優化點的補充說明

  1. 動態匯入在vue裡的時間注意點:

webpack 可以使用dynamic imports的方式引用模組,我們使用 async/ await 和 dynamic import 來實現。每一個dynamic import都將作為一個單獨的chunk打包。在vue中的一個例子就是路由懶載入+babel-plugin-dynamic-import-node的構建方案。使用babel-plugin-dynamic-import-node是因為開發環境下觸發熱更新很慢,這個外掛講import非同步全部改成require同步

  1. 打包生成的檔案模組識別符號的問題

一般來說我們在dist生成了一下三種bundle

main bundle 會隨著自身的新增內容的修改,而發生變化。
vendor bundle 會隨著自身的 module.id 的修改,而發生變化。
manifest bundle 會因為當前包含一個新模組的引用,而發生變化。

然而我們並不希望vendor每次構建都生成新的hash,畢竟我們希望用到快取的。解決方法官方有兩個外掛NamedModulesPlugin和HashedModuleIdsPlugin
vue裡使用的是HashedModuleIdsPlugin

相信很多人從webpack3升級到4會碰到問題,接下來
### 5. 升級到webpack4你該搞明白

1. 零配置的概念把配置門檻降低了

主要使用了模式的概念。

development 模式下,預設開啟了NamedChunksPlugin 和NamedModulesPlugin方便除錯,提供了更完整的錯誤資訊,更快的重新編譯的速度
production 模式下,由於提供了splitChunks和minimizer,所以基本零配置,程式碼就會自動分割、壓縮、優化,同時 webpack 也會自動幫你 Scope hoisting(作用域提升) 和 Tree-shaking

相當於把一些基本的配置當成預設配置。只需要在命令列執行時帶上mode引數就搞定
#### 2. 一些外掛的廢除和替換

廢棄了 頂替者(用optimization屬性) 變化
uglifyjs-webpack-plugin minimizer 壓縮優化
CommonsChunkPlugin splitChunks 程式碼分割,下面詳解

還有一些新的外掛:Tree Shaking,SideEffects。我還不知道怎麼用–

3. 要注意的新的優化點

  1. extract-text-webpack-plugin -> mini-css-extract-plugin

它與extract-text-webpack-plugin最大的區別是:它在code spliting的時候會將原先內聯寫在每一個 js chunk bundle的 css,單獨拆成了一個個 css 檔案。js變得更乾淨了,css是根據optimization.splitChunks的配置自動拆分css檔案為單獨的模組的規則拆分的,不用擔心過多的httlp資源請求問題

  1. 所有的[chunkhash] ->[contenthash]

這是為了解決當css與js檔案有依賴時,兩者有相同的chunkhash。這樣js修改了,css沒改的情況下chunkhash頁被修改了,沒法快取了呀
contenthash 你可以簡單理解為是 moduleId + content 所生成的 hash
相關issue

  1. 程式碼的壓縮優化改成了optimization.minimizer

在optimization.minimizer裡推薦使用optimize-css-assets-webpack-plugin直接配置。但是vue-cli3裡的配置自己配的。嗯…反正也不想看那些配置,就這樣吧~~~

4. 第三方庫和業務程式碼分開打包策略

上面多處提到了這個optimization.splitChunks

Webpack 4 最大的改進便是Code Splitting chunk。webpack3是通過CommonsChunkPlugin拆分的。然後現在直接被廢棄了,我能怎麼辦?,跟著學唄。

開啟Code Splitting很簡單,使用production的mode就行,會自動開啟。並有一個設定好了的一個很合理的配置

如果同時滿足下列條件,chunk 就會被拆分:

  • 新的 chunk 能被複用,或者模組是來自 node_modules 目錄
  • 新的 chunk 大於 30Kb(min+gz 壓縮前)
  • 按需載入 chunk 的併發請求數量小於等於 5 個
  • 頁面初始載入時的併發請求數量小於等於 3 個

預設配置已經很合理了,然而當出現如下情況:
已vue-cli建立的專案為例。專案用到了第三方的UI元件庫,在main.js入口處依賴了第三方庫。
因為在入口引入了,所以第三方庫會被打包進app.js。這樣,只要我修改了app.js裡的其他程式碼,打出來的包的hash就變了。瀏覽器又得再次快取app.js。第三庫相當於又被快取了一次,這顯然不是我們想要的。

看一下花褲衩的配置

splitChunks: {
  chunks: "all",
  cacheGroups: {
    libs: {
      name: "chunk-libs",
      test: /[\/]node_modules[\/]/,
      priority: 10,
      chunks: "initial" // 只打包初始時依賴的第三方
    },
    elementUI: {
      name: "chunk-elementUI", // 單獨將 elementUI 拆包
      priority: 20, // 權重要大於 libs 和 app 不然會被打包進 libs 或者 app
      test: /[\/]node_modules[\/]element-ui[\/]/
    },
    commons: {
      name: "chunk-commons",
      test: resolve("src/components"), // 可自定義擴充你的規則
      minChunks: 2, // 最小共用次數
      priority: 5,
      reuseExistingChunk: true
    }
  }
};

主要思路就是

  1. 把初始化時依賴的第三方打包成基礎類庫,這一類改動小,又被全域性需要
  2. 把類似elementUI這一類的比較大、改動較小的抽出來
  3. 全域性公用的router、函式、svg圖示、layout佈局元件等這些不管,直接扔app.js
  4. 業務裡會經常使用但沒在main.js引入的的components被打包成一個common
  5. 業務裡經常使用但是體積相當較小,就直接在main.js引入,打包進app.js
  6. 其他的低頻使用的元件會自動按預設splitChunks的設定來拆分

提醒: 程式碼的拆分一定要結合專案的實際情況,比如你就用到element裡的一兩個元件,完全可以按需載入在main.js,然後直接打包進app.js。所以沒有最合理的拆分規則,只有最適合你的。

5. Prefetching/Preloading modules

支援了Prefetching/Preloading瀏覽器資源載入優化
核心思想是減少JS下載時間
學不動了學不動了,先緩緩

6. 最後推薦http://webpack.wuhaolin.cn/

相關文章