Webpack 帶我飛

讓打包程式碼比包檳榔還順

Posted by Lil Toby on Monday, July 8, 2019

TOC

基本介紹

  • Webpack 可以做到的事
    • 將相依的 js 檔打包在一起,降低請求次數
    • 將 js 檔案 Bundle 變成單一的檔案
    • 在前端程式碼中使用 npm packages
    • 撰寫 JavaScript ES6 或 ES7(需要透過 babel 來幫助)
    • Minify 或優化程式碼
    • 類似 gulp plugin 做 pipeline 的文件轉換動作
    • 使用 HMR(Hot Module Replacement)

基本語法

webpack --config webpack.config.js

Config

基本屬性

  • context

    • 基礎目錄,預設是檔案所在的目錄
  • mode

    • webpack4 加入的元素
    • 在 production 模式下會默認開啟 UglifyJsPlugin 等等一些內建優化 plugins
    • 可以不用配置 alias
      • 比如 react 即可依照 mode 決定載入 production.min.js 還是 development.js
      • but 大部分的第三方 library 並沒有做入口環境判斷
  • optimization

    • 優化項目,這裡可以客製化 webpack 內建優化 plugin 的參數
  • resolve

    • extensions
      • 可以在引入時不用帶副檔名
      • import Comp from '../path/to/component';
    • alias
      • 直接指定別名,不用更改相對路徑
      • import $ from '../path/to/jquery';
      • 可搭配 ProvidePlugin 助攻
  • devtools

    • 有 7 種 sourcemap 模式
    • 打包方式不同,決定是否壓縮或節省時間
    • 建議用

      module.exports = {
          devtool: "source-map",
      }
      

四大核心

Entry : 一個可執行模塊或庫的入口文件

  • Single Entry Syntax

    • 全部都將檔案打包成一個檔案
    • 開發 library 時或 single page 時,才會比較常用

      module.exports = {
          entry: {
              main: './app/index.js',
          },
      }
      
  • Object Syntax

    • 將 vendors 統一打包成另一個檔案
    • 減少網路 requests 的次數,快取過就不 load 了
    • 可以各自引入想要的 js 減輕網路負擔
    • 當然 output 也要跟著調整

      module.exports = {
          entry: {
              app: './app/index.js',
              vendors: './app/vendors.js',
              pageOne: './src/pageOne/index.js',
              pageTwo: './src/pageTwo/index.js',
              pageThree: './src/pageThree/index.js'
          },
      }
      

Output : 輸出

  • output.filename

    • 可以按照 entry 輸出對應的 output

      其中 name 為 entry 的 key

      module.exports = {
          output: {
              filename: '[name].js',
          },
      }
      
  • output.path

    • 指定輸出到哪個資料夾

      module.exports = {
          output: {
              path: './dist'
          },
      }
      

Loaders : 文件轉換器

  • 為了要讓 webpack 也能理解並處理 css 圖片 等等 非js 的文件並轉譯為 js 形式
  • test: /\.<型別>$/ 來決定用什麼 loader

Plugins : 用於擴展 webpack 的功能

  • 可在 webpack 構建生命週期的節點上加入擴展 hook,為 webpack 加入客製化功能
  • 基本格式

    function ConsoleLogOnBuildWebpackPlugin() {
    };
    
    ConsoleLogOnBuildWebpackPlugin.prototype.apply = function(compiler) {
        compiler.plugin('run', function(compiler, callback) {
            console.log("The webpack build process is starting!!!");
            callback();
        });
    };
    
    module.exports = ConsoleLogOnBuildWebpackPlugin;
    

    優化

    原則

    • 縮小打包體積
    • 減少請求數
    • 使用快取策略
    • 規劃開發階段打包策略

    方向

    Code Splitting

    • 新版本的webpack會預設對程式碼碼進行拆分,拆分的規則是:

    • 模塊被重複引用或者來自 node_modules 中的模塊

    • 在壓縮前為 30kb

    • 在按需加載時,請求數量小於等於 5

    • 在初始化加載時,請求數量小於等於 3

    • 把打包文件分割成為更小的文件,允許使用者在他們需要的時候才下載需要的程式碼

    • 主要做法

    • 分離第三方 library( vendor )

    • 為非同步載入的程式碼打包成一個公共的模組 (Lazy load)

    • 為 Manifest 單獨打包 (Webpack 的 Runtime 程式碼)

    • 依功能模組打包程式碼,並在需要特定功能時才載入對應的模組

      • 為不同入口的公共業務代碼打包(同理,也是為了緩存和加載速度)
      • 比如說 app.js 拆分成 home.jsprofile.js 各自依頁面載入
      • 畢竟使用者不是一開始就會用到所有的功能

    Manifest

    • ./app/index.js 有任何變化時,執行打包會包出一個全新的檔案 👎
    • 希望 ./dist/[hash].vendor.js 不要一直變動,瀏覽器會重複載入
    • 起因:webpack 本身有自己的 runtime code,造成 [hash] 數值不一樣
    • 解決方法
    • 執行 webpack 時,額外產生 ./dist/[hash].manifest.js
    • 這樣打包出來的檔案就不會變,達到 vendor cache 的效果
        new webpack.optimize.CommonsChunkPlugin({
            name: ['vendor', 'manifest'] // Specify the common bundle's name.
        })
    

Tree Shaking

  • webpack 2 引入,但好像不是很好用
  • 實踐 DCE 的方式之一

    DCE: Dead code elimination 編譯最佳化技術之一

把項目中沒必要的模塊全部抖掉,用於在不同的模塊之間消除無用的代碼
  • 因為 ES6 的 import 跟 export 是完全靜態化的,所以可以藉此剔除無用的程式碼

    靜態化: 在運行前就明確指定依賴關係 像 CommonJS 就是屬於動態載入

具體 shake shake 規則 (符合以下規則才不會被 shake 掉)
  • 兩個關鍵字只能作為模塊頂層的語句出現,不能出現在函數或者其他的塊語句裡面
  • import 的模塊名只能是字符串常量,不能使用變量
  • 不管 import 的語句的出現位置在哪裡,模塊在初始化的所有的 import 都必須已經導入完成
  • import 的綁定是不可變的,類似於 const
Lazy Load

舉例來說一個情境

  • 載入需看到的

    • 導航列
    • 最外層骨架的 code
    • 內容最上層
  • 不需看到的

    • 其他頁面
    • 未點開的 popup
    • 在可視範圍之外的元件
  • webpack 會將此 import 視為可分割點,並切割成 x.bundle.min.js

  • 需要安裝 babel plugins 才可以 動態載入

        "plugins": [
            "@babel/plugin-syntax-dynamic-import"
        ],
    
    • 支持零配置使用,可以從命令行指定 entry 的位置
    • 不指定就是 src/index.js
    • mode 參數也可以透過命令行傳入
    • 但如果需要多個 entry 還是需要一個配置文件

    常用 Loaders

    css-loader
    • 以下為有順序性的
    • sass-loader
      • 將 scss/sass 轉譯成一般 css
    • css-loader
      • 單純載入 css 用,瀏覽器尚無法解析
    • style-loader

      • 將載入的 css 用 <style></style> 包起來並引入至 <head>

      切記: loader 順序是不能變的,是 由後往前 一層一層轉譯

      module: {
          rules: [
              {
                  test: /\.css$/,
                  use: [
                      { loader: 'style-loader'},
                      {
                          loader: 'css-loader',
                          options: {
                              modules: true
                          }
                      },
                      { loader: 'sass-loader' }
                  ]
              }
          ]
      }
      
      • 最終效果 (有效簡化引入寫法)
      • require("!style-loader!style-loader!css-loader!./file.css"); ➡️ require("./file.scss");
      vue-loader

      搭配 vue-html-loader vue-style-loader vue-template-compiler 都是 fork 來的

      • 可以處理 vue 元件且各自解析對應的 tag
      <style lang="sass">
      /* write sass here */
      </style>
      
babel-loader
  • babel-loader babel-core
    
    {
        test: /.js$/,
        exclude: /node_modules/,
        use: [
            { loader: ‘babel-loader’ }
        ]
    }
    
file-loader
  • 可以指定要複製和放置資源文件的位置,以及如何使用版本 hash 命名以獲得更好的 cache
  • 可以使用相對路徑而不用擔心部署時 URL 的問題
  • webpack 將會在打包輸出中自動重寫文件路徑為正確的 URL。
url-loader
  • 允許你有條件地將文件轉換為內聯的 base-64 URL,這會減少小文件的 HTTP 請求數。
  • 如果文件大於 threshold,會自動的交給 file-loader 處理。

適用小圖,C/P 值的概念

```js
{
    loader: 'url-loader',
    options: {
        limit: 8192,
        name: 'img/[name].[hash].[ext]',
        publicPath: '../'
    }
}

```
  • 大於 limit —> file-loader

    • 轉換後還是好幾 MB,就該改用 lazy load 解決
  • 小於 limit —> url-loader

    • 壓成 base64 文字可以減少請求數
  • 優點

    • 減少網路請求
  • 缺點

    • 為降低 html 可讀性
    • 無法 cache 起來,但寫在 css 裡就可以 cache 囉
resolve-url-loader
  • 可以解決在 css 裡的 url 引入
  • background-image: url(asset/img/bla bla bla.png)

好用 Plugins

Webpack 4 內建

webpack.optimization.UglifyJsPlugin
  • 將模組化好的程式,直接整個壓成一行
  • 去除空格並置換字元

    
        module.exports = {
            plugins: [
                new webpack.optimize.UglifyJsPlugin()
            ]
        }
    
webpack.DefinePlugin
  • 可以定義 global 變數且在元件內直接使用
webpack.ProvidePlugin
  • 讓相關 libs 不用 require 就直接用
    new webpack.ProvidePlugin({ $: "jquery" })  // 有用 resolve.alias 的話
webpack.optimization.SplitChunksPlugin
  • 可依據檔案大小(minSize)
  • module 引用次數(minChunks)
  • 同步/非同步(chunks)
  • priority(優先級)等自訂優化決定打包檔案的邏輯。

獨立 plugins

mini-css-extract-plugin
  • 非同步載入
  • 不重複編譯,性能更好
  • 只針對 css (所以順序性有差)
  • 抽離出來的有一個通用的 css 和每個頁面一個 css
  • 訪問對應的頁面才會加載(Async loading)
VueLoaderPlugin

vue-loader/lib/plugin

  • 用於打包 vue 相關檔案
  • 可處理 SFC 檔案
  • 需搭配 vue-template-compiler 一起安裝
ProgressBarPlugin
  • 打包時顯示進度條
NamedModulesPlugin
  • 熱加載時直接返回更新文件名,而不是文件的id
html-webpack-plugin
  • 可以幫忙建一個 index.html 骨架
  • 並可以加 meta favicon gacode 之類的
  • 幫忙生出 ./dist/index.html 檔案
  • 且自動將 bundle 好的所有 css、js 等相關路徑,會直接都塞進 index.html
  • 為html文件中引入的外部資源如script、link動態添加每次compile後的hash,防止引用緩存的外部文件問題
  • 可以生成創建html入口文件,比如單頁面可以生成一個html文件入口,配置N個html-webpack-plugin可以生成N個頁面入口
  • 將 webpack中entry配置的相關入口 chunk 和 extract-text-webpack-plugin抽取的css樣式 插入到該插件提供的template或者templateContent配置項指定的內容基礎上生成一個html文件,具體插入方式是將樣式link插入到head元素中,script插入到head或者body中。

    
    module.exports = {
        plugins: [
            new HtmlWebpackPlugin()
        ]
    }
    
webpack-merge
  • 將不同情境的 config 檔跟 base.config 合起來
clean-webpack-plugin
  • 讓 webpack 每次打包前都清除特定資料夾
CommonsChunkPlugin
  • 通常是配置 code spiliting
  • 分別為 vendor、manifest 和 vendor-async 配置
  • name, async, children, minChunks
OptimizeCSSAssetsPlugin
  • 最後會含 autoprefixer
  • 但會把一些 他認為 不需要的 prefix 拿掉QQ
DefinePlugin
  • 可以對專案程式碼注入全域環境變數

        new webpack.DefinePlugin({
            DESCRIPTION: 'Oh Yah'
        })
    
        console.log(DESCRIPTION);   // Oh Yah
    
    ProvidePlugin
    • 可以在無需引入的情況下,在全域的模式直接使用 lib 的變數
        new webpack.ProvidePlugin({
            'React': 'react'       // 這是有先用 resolve.alias
        })
    
        import React,{Component} from 'react';   // 之後檔案的這一行可以拿掉了
    
extract-text-webpack-plugin
CopyWebpackPlugin

分析工具

webpack-bundle-analyzer
  • 統計和優化webpack日誌的工具
  • 安裝 & 啟動

    
        npm i -g webpack-bundle-analyzer
        webpack-bundle-analyzer stats.json -p 8888
    
webpack-bundle-size-analyzer
  • 在終端機
webpack-dashboard
```js
    npm install webpack-dashboard --save-dev
```
webpack-jarvis
  • 精美 dashboard
  • 不過是在 build 時另起 server…
  • 所以只適合 development 模式 @@?

自問自答

  • 那多少 k 才算高效呢?

    • 似乎在單檔超過 500k 就會被 alert 了
  • Loader 跟 Plugin 所作用的時間不一樣?

    • Plugin 的作用時間會是在 Webpack 編譯之前、或者編譯完成之後
    • Loader 則是 Webpack 編譯時與 Webpack 一起協作。
  • 如果是已有既有的 html 要怎麼動態引入 code spilit 後的 js 檔呢?

    • 可以用 template 的方式引入?

哎呀有坑

  • 打包後 webkit- 前綴會被拔掉?
    • 設定到 minimize: true 在匹配到css後直接壓縮
    • 用了autoprefix自動添加前綴,這樣壓縮,會導致添加的前綴丟失
    • 解法
      • 使用 optimize-css-assets-webpack-plugin 解掉?

遇到問題

  • 如果是 vue 專案,會遇到 SFC 失靈的問題

    • 安裝 vue-style-loader
  • 接著導致 ReferenceError: document is not defined

    • 靠北,結果是因為 minifyPlugin 在前面的關係
    • 對調順序即可
  • TypeError: Cannot read property 'parseComponent' of undefined

    • 版本要一樣

      "dependencies": {
          "vue": "2.5.21",
          "vue-template-compiler": "2.5.21"
      }
      
  • Error: Can't resolve '/Users/toby/work/pixnet/pixpixnetid/node_modules/html-webpack-template/index.ejs

    • 依賴到 html-webpack-template 依賴的 index
    • 拔掉 option 即可
  • vue-loader was used without the corresponding plugin

    • 缺對應的 plugin
  • Module not found: Error: Can't resolve '../../helpers/click-i13n'

    • 可能是 resolve 裡的 alias 沒有加
    • 因為沒有加 .vue 啦…
  • assertInputSourceMap option-assertions.js

    • babel-loader 需升級到 8.0.0-beta.1
  • Error: Unexpected '/'. Escaping special characters with \ may help

    • 需把註解 // 拿掉
  • Module not found: Error: Can't resolve

    • 缺少 sass-loader
  • 輸出沒有 css ?

    • 原因
      • 在 loader 裡需要引入 MiniCssExtractPlugin.loader
    • 解決

      module: {
          rules: [
              {
                  test: /.(scss|sass)$/,
                  use: [
                      MiniCssExtractPlugin.loader,
                      'css-loader',
                  ],
              },
          ]
      }
      

參考連結

- 請 Toby 喝珍奶,你請我就喝 -

Lil Toby Blog

YA~大杯還小杯~看你誠意 ❤ ️

使用手機掃描 QRCODE 完成 pay 下去就對