如何利用webpack來提升前端開發效率(二)?

B2D1發表於2019-01-20

查漏補缺

通過如何利用webpack來提升前端開發效率(一)的學習,我們已經能夠通過webpackloaderpiugin機制來處理各種檔案資源。細心的小夥伴們發現了缺少了對字型檔案和HTML<img>標籤的資源處理,那讓我們先來解決這個問題。

接上篇文章,我們的目錄結構,如圖所示:

如何利用webpack來提升前端開發效率(二)?
首先是對字型檔案的處理,修改webpack.config.js

// webpack.config.js
// 新增對字型的loader
      {
        test: /\.(eot|woff|woff2|ttf)$/,
        use: [{
          loader: 'url-loader',
          options: {
            name: '[name].[hash:7].[ext]',
            limit: 8192,
            outputPath: 'font',  // 打包到 dist/font 目錄下
          }
        }]
      },
複製程式碼

如何我們從網上隨意下載了一種字型,放置於src資料夾下,並修改src/index.html

<!-- src/index.html -->
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>my-webpack</title>
</head>

<body>
  <h1>webpack大法好!!前端大法好!!</h1>
</body>

</html>
複製程式碼

index.scss中引入字型

/* src/index.scss */
/* 新增以下樣式 */
@font-face {
  font-family: 'myFont';
  src: url('./font/ZaoZiGongFangQiaoPinTi-2.ttf');
}

h1 {
  font-family: 'myFont';
}
複製程式碼

在此之前,每次重新打包都要刪除dist資料夾,實在是麻煩,現在我們可以藉助clean-webpack-plugin,它能夠在每次打包時刪除指定的資料夾,我們在命令列執行 npm i clean-webpack-plugin -D

修改webpack.config.js

// webpack.config.js
// 新增以下引入
const CleanWebpackPlugin = require('clean-webpack-plugin');

// 新增以下外掛
plugins: [
    new CleanWebpackPlugin(['dist']) // 在最新的v2版本中,如果預設刪除dist資料夾,只需new CleanWebpackPlugin()
],
複製程式碼

隨後在命令列執行npm run build,我們的dist資料夾會被自動刪除,並輸出以下結果,可以看到我們雖然成功打包了字型檔案,但字型檔案是在太大,連webpack都發出了警告[big]

如何利用webpack來提升前端開發效率(二)?

如何利用webpack來提升前端開發效率(二)?
這裡我們一般有以下解決方案:

  • 對字型檔案開啟CDN加速
  • 通過設計給出已經制作好的樣式圖
  • 利用font-spider對字型進行壓縮

我們實踐一下第三種方案,也是我推薦的方案 在命令列依次執行

npm i font-spider -D
font-spider ./dist/index.html
複製程式碼

可以看到將近4MB的字型檔案體積瞬間壓縮至不足6KB!!!而頁面效果和之前一模一樣。

如何利用webpack來提升前端開發效率(二)?
而對於HTML文件中<img>標籤的引入問題,我們需要藉助html-loader,它能將HTML文件中img.src解析成require,從而實現引入圖片,話不多說,我們直接看效果。在命令列執行npm i html-loader -D

修改以下檔案

// webpack.config.js
// 新增對html的loader
      {
        test: /\.html$/,
        use: {
          loader: 'html-loader',
          options: {
            attrs: ['img:src'] // img代表解析標籤,src代表要解析的值,以key:value形式存在於attrs陣列中
          }
        }
      }
複製程式碼
<!-- src/index.html -->
<body>
    +  <img src="./leaf.png" alt="">
</body>
複製程式碼

在命令列執行npm run build,檢視dist/index.html,看來已經成功啦

如何利用webpack來提升前端開發效率(二)?

動態載入

設想如果我們的入口檔案很大(包含了所有的業務邏輯程式碼),就會造成首屏載入變慢,使用者體驗感下降。 這裡我們從兩個方面解決:

  • 模組解耦,將入口檔案解耦,將基礎模組(UI,工具類)和業務模組分離,即能方便程式碼維護擴充,也能減少入口檔案的體積。
  • 動態載入,使用者不可能一開始就用到所有的功能,這時候我們可以將次要的,需要事件觸發的模組,在之後的互動過程中,動態引入。 在src目錄下新增dynamic.js
// dynamic.js
export default () => {
  console.log('Im dynamically loaded.');
}
複製程式碼

修改以下檔案

<!-- src/index.html -->
<body>
  + <button id="btn">點選我,動態載入dynamic.js</button>
</body>
複製程式碼
// src/index.js
// 新增以下內容
const btn = document.getElementById('btn');
// 點選按鈕,動態載入dynamic.js
btn.onclick = () => {
  import(/* webpackChunkName: "dynamic" */ './dynamic.js').then(function (module) {
    const fn = module.default;
    fn();
  })
}
複製程式碼

執行npm run build,可以看到

如何利用webpack來提升前端開發效率(二)?
如果未設定/* webpackChunkName: "dynamic" */,則是
如何利用webpack來提升前端開發效率(二)?
可以得出得結論是:設定ChunkName"dynamic"是必要的,否則打包完成會是以自動分配的、可讀性很差的id命名的JS檔案。且沒有Chunk Names標識。

現在我們開啟dist/index.html,此時

如何利用webpack來提升前端開發效率(二)?
當我點選該按鈕時
如何利用webpack來提升前端開發效率(二)?
控制檯列印出
如何利用webpack來提升前端開發效率(二)?
Network網路請求顯示,動態載入了dynamic.js
如何利用webpack來提升前端開發效率(二)?
至此,我們成功實現了動態載入。

分離開發環境和生產環境

回頭看我們的webpack.config.js,不知不覺就寫了這麼多程式碼,鑑於我們在開發實際專案時,是開發和生產兩套工作模式,各司其職,我們不如做個了斷,分離配置。

命令列執行npm i webpack-merge cross-env -D

webpack-merge可以合併webpack配置項,cross-env可以設定及使用環境變數。

新增webpack.base.js,提供基本的webpack loader plugin配置

const path = require('path');
const htmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const pathResolve = (targetPath) => path.resolve(__dirname, targetPath);
const devMode = process.env.NODE_ENV !== 'production';
// 在node中,有全域性變數process表示的是當前的node程式。
// process.env包含著關於系統環境的資訊。
// NODE_ENV是使用者一個自定義的變數,在webpack中它的用途是來判斷當前是生產環境或開發環境。
// 我們可以通過 cross-env 將 NODE_ENV=development 寫入 npm run dev的指令中,從而注入NODE_ENV變數。

module.exports = {
  entry: {
    index: pathResolve('js/index.js')
  },
  output: {
    path: pathResolve('dist'),
  },
  module: {
    rules: [
      {
        test: /\.html$/,
        use: {
          loader: 'html-loader',
          options: {
            attrs: ['img:src']
          },
        },
      },
      {
        test: /\.(eot|woff|woff2|ttf)$/,
        use: [{
          loader: 'url-loader',
          options: {
            name: '[name].[hash:7].[ext]',
            limit: 8192,
            outputPath: 'font',
          },
        }],
      },
      {
        test: /\.(sa|sc|c)ss$/,
        use: [
          devMode ? 'style-loader' : {  // 如果處於開發模式,則無需再外鏈CSS,直接插入到<style>標籤中
            loader: MiniCssExtractPlugin.loader,
            options: {
              publicPath: '../'
            }
          },
          'css-loader',
          'postcss-loader',
          'sass-loader',
        ],
      },
      {
        test: /\.(png|jpg|jpeg|svg|gif)$/,
        use: [{
          loader: 'url-loader',
          options: {
            limit: 8192,
            name: '[name].[hash:7].[ext]',
            outputPath: 'img',
          },
        }],
      },
    ],
  },
  plugins: [
    new htmlWebpackPlugin({
      minify: {
        collapseWhitespace: true,  // 移除空格
        removeAttributeQuotes: true, // 移除引號
        removeComments: true // 移除註釋
      },
      filename: pathResolve('dist/index.html'),
      template: pathResolve('src/index.html'),
    })
  ]
};
複製程式碼

新增webpack.dev.js,服務於開發模式下

const path = require('path');
const webpack = require('webpack');
const base = require('./webpack.base.js');
const { smart } = require('webpack-merge');

const pathResolve = (targetPath) => path.resolve(__dirname, targetPath);

module.exports = smart(base, {
  mode: 'development',
  output: {
    filename: 'js/[name].[hash:7].js'
  },
  devServer: {
    contentBase: pathResolve('dist'),
    port: '8080',
    inline: true,
    historyApiFallback: true,
    hot: true
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NamedModulesPlugin()
  ]
})
複製程式碼

新增webpack.prod.js,服務於生產模式下

const path = require('path');
const base = require('./webpack.base.js');
const { smart } = require('webpack-merge');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const pathResolve = (targetPath) => path.resolve(__dirname, targetPath);

module.exports = smart(base, {
  mode: 'production',
  devtool: 'source-map', // 會生成對於除錯的完整的.map檔案,但同時也會減慢打包速度,適用於打包後的程式碼查錯
  output: {
    filename: 'js/[name].[chunkhash:7].js',
    chunkFilename: 'js/[name].[chunkhash:7].js',
  },
  plugins: [
    new CleanWebpackPlugin(['dist']),
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:7].css',
    }),
  ],
});
複製程式碼

相應的,package.json也需要修改

// 新增以下兩條命令
// cross-env 決定執行環境   --config 決定執行哪個配置檔案
"dev": "cross-env NODE_ENV=development webpack-dev-server --config webpack.dev.js",
"build": "cross-env NODE_ENV=production webpack --config webpack.prod.js "
複製程式碼

快取之爭

快取在前端的地位毋庸置疑,正確的利用快取就能極大地提高應用的載入速度和效能。 webpack利用了hash值作為檔名的組成部分,能有效利用快取。當修改檔案,重新打包時,hash值就會改變,導致快取失效,HTTP請求重新拉取資源。
webpack有三種hash處理策略,分別是:

hash

屬於專案工程級別的,即每次修改任何一個檔案,所有檔名的hash值都將改變。所以一旦修改了任何一個檔案,整個專案的檔案快取都將失效。如將整個專案的filename的命名策略改為name.[hash:7](:7的意思是從完整hash值中擷取前七位),我們可以看到,打包後的檔案hash值是一樣的,所以對於沒有改變的模組而言,hash也被更新了,導致快取失效了。

如何利用webpack來提升前端開發效率(二)?

chunkhash

chunkhash根據不同的入口檔案(Entry)進行依賴檔案解析、構建對應的chunk,生成對應的雜湊值。如將整個專案filename的命名策略改為name.[chunkhash:7],我們可以看到Chunk Names"index"的檔案hash值一致,而不同chunkhash值不同。這也就避免了修改某個檔案,整個工程hash值都將改變的情況。

如何利用webpack來提升前端開發效率(二)?

contenthash

但問題隨之而來,index.scss是作為模組匯入到index.js中的,其chunkhash值是一致的,只要其中之一改變,與其關聯的檔案chunkhash值也會改變。這時候就要用到contenthash,它是根據檔案的內容計算,該檔案的內容改變了,contenthash值才會改變。我們將css檔案的命名策略改為name.[contenthash:7],並修改src/index.js,不改動其他檔案,再次打包,發現:

如何利用webpack來提升前端開發效率(二)?
如何利用webpack來提升前端開發效率(二)?

生產環境的配置優化

tree-shaking

字面意思理解為從一棵樹上把葉子搖晃下來,這樣數的重量就減輕了,類比程式,就如同從我們的應用上刪除沒用的程式碼,從而減少體積。借於ES6的模組引入是靜態分析的,故而webpack可以在編譯時正確判斷到底載入了什麼程式碼,即沒有被引用的模組不會被打包進來,減少我們的包大小,縮小應用的載入時間,呈現給使用者更佳的體驗。那麼怎麼使用呢?

新建src/utils.js

// src/utils.js
const square = (num) => num ** 2;
const cube = num => num * num * num;
// 匯出了兩個方法
export {
  square,
  cube
}
複製程式碼

新建src/shake.js

// src/shake.js
import { cube } from './utils.js';
// 只使用了cube方法
console.log('cube(3) is' + cube(3));
複製程式碼

webpack.base.js中新增入口檔案shake.js

  entry: {
    +  shake: pathResolve('src/shake.js')
  },
複製程式碼

命令列執行npm run build,檢視打包後的shake.js,並沒有發現square方法沒有被打包進來,說明tree-shaking起作用了。 而這一切都是webpackproduction環境下自動為我們實現的。

如何利用webpack來提升前端開發效率(二)?

splitChunks

字面意思為拆分程式碼塊,預設情況下它將只會影響按需載入的程式碼塊,因為改變初始化的程式碼塊將會影響HTML中執行專案需要包含的script標籤。還記得我們在src/index.js中動態引入了src/dynamic.js嗎,最終dynamic.js被獨立打包,就是歸功於splitChunks

在實際生產中,我們經常會引入第三方庫(JQueryLodash),往往這些第三方庫體積高達幾十KB摻雜在業務程式碼中,並且不會像業務程式碼一樣經常更新,這時候我們就需要將他們拆分出來,既能保持第三方庫持久快取,又能縮減業務程式碼的體積。

修改webpack.prod.js

// 在module.exports中新增如下內容
  optimization: {
    runtimeChunk: {
      name: 'manifest', // 被注入了webpackJsonp的定義及非同步載入相關的定義,單獨打包模組資訊清單,利於快取
    },
    splitChunks: {
      cacheGroups: { // 快取組,預設將所有來源於node_modules的模組分配到叫做'venders'的快取組,所有引用超過兩次的模組分配到'default'快取組.
        vendor: {
          chunks: "all",                    // all, async, initial 三選一, 外掛作用的chunks範圍,推薦all
          test: /[\\/]node_modules[\\/]/,   // 快取組所選擇的的模組範圍
          name: "vendor",                   // Chunk Names及打包出來的檔名
          minChunks: 1,                     // 引用次數>=1
          maxInitialRequests: 5,            // 頁面初始化時載入程式碼塊的請求數量應該<=5
          minSize: 0,                       // 程式碼塊的最小尺寸
          priority: 100,                    // 快取優先順序權重
        },
      }
    }
  },
複製程式碼

命令列執行npm i lodash -S

修改src/index.js

// 新增以下內容
import _ from 'lodash';
複製程式碼

執行npm run build,可以看到優化前lodash被打包進index.js,優化後lodash被打包進vendor.js

如何利用webpack來提升前端開發效率(二)?
如何利用webpack來提升前端開發效率(二)?

壓縮程式碼,去除冗餘

往往在CSS程式碼中,存在很多我們沒有用到的樣式,它們是冗餘的,我們需要將它們剔除,並壓縮剩餘的CSS樣式,以減少CSS檔案體積。

在命令列執行npm i glob optimize-css-assets-webpack-plugin purifycss-webpack purify-css -D

修改webpack.prod.js

// 新增以下引入
const glob = require('glob'); // 匹配所需檔案
const PurifyCssWebpack = require('purifycss-webpack'); // 去除冗餘CSS
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin"); // 壓縮CSS

// 新增以下外掛
new PurifyCssWebpack({
    paths: glob.sync(pathResolve('src/*.html')) // 同步掃描所有html檔案中所引用的css,並去除冗餘樣式
})

// 新增以下優化
optimization: {
    minimizer: [
      new OptimizeCSSAssetsPlugin({}) // 壓縮CSS
    ]
  }

複製程式碼

執行npm run build

如何利用webpack來提升前端開發效率(二)?
可以看到,去除了冗餘CSS,並壓縮至一行。接下來,我們需要壓縮JS程式碼。 由於我們使用的是uglifyjs-webpack-plugin,它需要ES6的支援,所以我們先讓工程支援ES6的語法。 Babel 是一個 JavaScript 編譯器。它能把下一代 JavaScript 語法轉譯成ES5,以適配多種執行環境。

@babel/core提供了babel的轉譯API,如babel.transform等,用於對程式碼進行轉譯。像webpackbabel-loader就是呼叫這些API來完成轉譯過程的。

@babel/preset-env可以根據配置的目標瀏覽器或者執行環境來自動將ES2015+的程式碼轉換為ES5

先在命令列執行npm i @babel/core @babel/preset-env babel-loader @babel/plugin-syntax-dynamic-import -D

新建.babelrc檔案

{
  "presets": [  // 配置預設環境
    ["@babel/preset-env", {
      "modules": false
    }]
  ],
  "plugins": [
    "@babel/plugin-syntax-dynamic-import" // 處理src/index.js中動態載入
  ]
}
複製程式碼

修改webpack.base.js

// 新增js的解析規則
    {
        test: /\.(js|jsx)$/,
        use: 'babel-loader',
        exclude: /node_modules/
    },
複製程式碼

然後命令列執行npm i uglifyjs-webpack-plugin -D

修改webpack.prod.js

// 新增以下引入
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");

// 新增以下優化
optimization: {
    minimizer: [
    +  new UglifyJsPlugin({ // 壓縮JS
        cache: true,
        parallel: true,
        sourceMap: true
      })
    ]
  }
複製程式碼

執行npm run build,可以看到打包的檔案體積大大減少,大功告成,JS也被壓縮了。

index.html為例,我們可以開啟Chrome的開發者工具,選擇More tools,點選Coverage皮膚,可以看到JS、CSS等檔案的使用率,配合我們定製的webpack配置進行極致優化。

如何利用webpack來提升前端開發效率(二)?

如何利用webpack來提升前端開發效率(二)?

多頁面

有時候,我們需要同時構建多個頁面,藉助html-webpack-plugin,只需在plugins中新增新頁面的配置項。

新增src/main.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>main page</title>
</head>

<body>
  <h1>I am Main Page</h1>
</body>

</html>
複製程式碼

修改webpack.base.js

// 修改以下內容
  plugins: [
    new htmlWebpackPlugin({ // 配置index.html
      minify: {
        collapseWhitespace: true,
        removeAttributeQuotes: true,
        removeComments: true
      },
      filename: pathResolve('dist/index.html'),
      template: pathResolve('src/index.html'),
      chunks: ['manifest', 'vendor', 'index', ]  // 配置index.html需要用的chunk塊,即載入哪些JS檔案,manifest模組管理的核心,必須第一個進行載入,不然會報錯
    }),
    new htmlWebpackPlugin({ // 配置main.html
      minify: {
        collapseWhitespace: true,
        removeAttributeQuotes: true,
        removeComments: true
      },
      filename: pathResolve('dist/main.html'),
      template: pathResolve('src/main.html'),
      chunks: ['manifest', 'shake'] // 配置index.html需要用的chunk塊,載入manifest.js,shake.js
    }),
  ],
複製程式碼

執行npm run build,成功構建了index.htmlmain.html

如何利用webpack來提升前端開發效率(二)?
如何利用webpack來提升前端開發效率(二)?

結語

至此,我們擺脫了第三方腳手架的的禁錮,循序漸進的搭建了屬於自己的前端流程工具,做到了即改即用,功能俱全,快速便捷,複用性強的特點。希望小夥伴能親自動手,別老是紙上談webpack,要理解它的構建、優化原理,得心應手得融入到自己的工程專案中,拒絕再用以前繁瑣,不規範的開發流程,不做“CV工程師”,建立屬於自己的知識體系、工作流程,提高前端的開發效率。

最後,本專案原始碼已部署在Github上,並增加了許多額外優化(less的支援,ESLint檢測,針對圖片格式的壓縮...),讓大家可以直接下載體驗,並協助專案開發,日後也會持續維護,希望小夥伴們可以互相學習,提提建議。

GitHub地址:easy-frontend 一個快速,簡單,易用的前端開發效率提升工具

Star該專案,就是你們對我最大的的鼓勵!!

前端路上,不忘初心,祝大家早日發財!!

相關文章