淺入淺出webpack

Russ_Zhong發表於2019-03-04

準備了挺久,一直想要好好深入瞭解一下Webpack,之前一直嫌棄Webpack麻煩,偏向於Parcel這種零配置的模組打包工具一些,但是實際上還是Webpack比較靠譜,並且Webpack功能更加強大。由於上一次學習Webpack的時候並沒有瞭解過Node.js,所以很多時候真的感覺無能為力,連個__dirname都覺得好複雜,學習過Node.js之後再來學習Webpack,就會好理解很多,這一次算是比較深入的瞭解一下Webpack,爭取以後能夠脫離create-react-app或者Vue-Cli這種腳手架工具,或者自己也能夠寫一套指令碼自動配置開發環境。

由於寫這篇筆記的時候,Webpack已經發行了最新的Webpack 4.0,所以這篇筆記就算是學習Webpack 4.0的筆記吧,筆者所用版本是webpack 4.8.3,另外使用Webpack 4.x的命令列需要安裝單獨的命令列工具,筆者所使用的Webpack命令列工具是webpack-cli 2.1.3,學習的時候可以按照這個要求部署開發環境。

此外,在學習webpack之前,你最好對ES6、Node.js有一定的瞭解,最好使用過一個腳手架。

一、核心概念

Webpack具有四個核心的概念,想要入門Webpack就得先好好了解這四個核心概念。它們分別是Entry(入口)Output(輸出)loaderPlugins(外掛)。接下來詳細介紹這四個核心概念。

1.Entry

Entry是Webpack的入口起點指示,它指示webpack應該從哪個模組開始著手,來作為其構建內部依賴圖的開始。可以在配置檔案(webpack.config.js)中配置entry屬性來指定一個或多個入口點,預設為./src(webpack 4開始引入預設值)。

具體配置方法:

entry: string | Array<string>
複製程式碼

前者一個單獨的string是配置單獨的入口檔案,配置為後者(一個陣列)時,是多檔案入口。

另外還可以通過物件語法進行配置

entry: {
    [entryChunkName]: string | Array<string>
}
複製程式碼

比如:

//webpack.config.js
module.exports = {
    entry: {
        app: `./app.js`,
        vendors: `./vendors.js`
    }
};
複製程式碼

以上配置表示從app和vendors屬性開始打包構建依賴樹,這樣做的好處在於分離自己開發的業務邏輯程式碼和第三方庫的原始碼,因為第三方庫安裝後,原始碼基本就不再變化,這樣分開打包有利於提升打包速度,減少了打包檔案的個數,Vue-Cli採取的就是這種分開打包的模式。但是為了支援拆分程式碼更好的DllPlugin外掛,以上語法可能會被拋棄。

2.Output

Output屬性告訴webpack在哪裡輸出它所建立的bundles,也可指定bundles的名稱,預設位置為./dist。整個應用結構都會被編譯到指定的輸出資料夾中去,最基本的屬性包括filename(檔名)和path(輸出路徑)。

值得注意的是,即是你配置了多個入口檔案,你也只能有一個輸出點。

具體配置方法:

output: {
    filename: `bundle.js`,
    path: `/home/proj/public/dist`
}
複製程式碼

值得注意的是,output.filename必須是絕對路徑,如果是一個相對路徑,打包時webpack會丟擲異常。

多個入口時,使用下面的語法輸出多個bundle:

// webpack.config.js
module.exports = {
    entry: {
        app: `./src/app.js`,
        vendors: `./src/vendors.js`
    },
    output: {
        filename: `[name].js`,
        path: __dirname + `/dist`
    }
}
複製程式碼

以上配置將會輸出打包後檔案app.js和vendors.js到__dirname + `/dist`下。

3.Loaders

loader可以理解為webpack的編譯器,它使得webpack可以處理一些非JavaScript檔案,比如png、csv、xml、css、json等各種型別的檔案,使用合適的loader可以讓JavaScript的import匯入非JavaScript模組。JavaScript只認為JavaScript檔案是模組,而webpack的設計思想即萬物皆模組,為了使得webpack能夠認識其他“模組”,所以需要loader這個“編譯器”。

webpack中配置loader有兩個目標:

  • (1)test屬性:標誌有哪些字尾的檔案應該被處理,是一個正規表示式。
  • (2)use屬性:指定test型別的檔案應該使用哪個loader進行預處理。

比如webpack.config.js:

module.exports = {
    entry: `...`,
    output: `...`,
    module: {
        rules: [
            {
                test: /.css$/,
                use: `css-loader`
            }
        ]
    }
};
複製程式碼

該配置檔案指示了所有的css檔案在import時都應該經過css-loader處理,經過css-loader處理後,可以在JavaScript模組中直接使用import語句匯入css模組。但是使用css-loader的前提是先使用npm安裝css-loader

此處需要注意的是定義loaders規則時,不是定義在物件的rules屬性上,而是定義在module屬性的rules屬性中。

配置多個loader

有時候,匯入一個模組可能要先使用多個loader進行預處理,這時就要對指定型別的檔案配置多個loader進行預處理,配置多個loader,把use屬性賦值為陣列即可,webpack會按照陣列中loader的先後順序,使用對應的loader依次對模組檔案進行預處理。

{
    module: {
        rules: [
            {
                test: /.css$/,
                use: [
                    {
                        loader: `style-loader`
                    },
                    {
                        loader: `css-loader`
                    }
                ]
            }
        ]
    }
}
複製程式碼

此外,還可以使用內聯方式進行loader配置:

import Styles from `style-loader!css-loader?modules!./style.css`
複製程式碼

但是這不是推薦的方法,請儘量使用module.rules進行配置。

4.Plugins

loader用於轉換非JavaScript型別的檔案,而外掛可以用於執行範圍更廣的任務,包括打包、優化、壓縮、搭建伺服器等等,功能十分強大。要是用一個外掛,一般是先使用npm包管理器進行安裝,然後在配置檔案中引入,最後將其例項化後傳遞給plugins陣列屬性。

外掛是webpack的支柱功能,目前主要是解決loader無法實現的其他許多複雜功能,通過plugins屬性使用外掛:

// webpack.config.js
const webpack = require(`webpack`);
module.exports = {
    plugins: [
        new webpack.optimize.UglifyJsPlugin()
    ]
}
複製程式碼

向plugins屬性傳遞例項陣列即可。

5.Mode

模式(Mode)可以通過配置物件的mode屬性進行配置,主要值為production或者development。兩種模式的區別在於一個是為生產環境編譯打包,一個是為了開發環境編譯打包。生產環境模式下,webpack會自動對程式碼進行壓縮等優化,省去了配置的麻煩。

學習完以上基本概念之後,基本也就入門webpack了,因為webpack的強大就是建立在這些基本概念之上,利用webpack多樣的loaders和plugins,可以實現強大的打包功能。

二、基本配置

按照以下步驟實現webpack簡單的打包功能:

  • (1)建立工程資料夾,位置和名稱隨意,並將cmd或者git bash的當前路徑切換到工程資料夾。

  • (2)安裝webpack和webpack-cli到開發環境:

      npm install webpack webpack-cli --save-dev
    複製程式碼
  • (3)在工程資料夾下建立以下檔案和目錄:

    • /src
      • index.js
      • index.css
    • /dist
      • index.html
    • webpack.config.js
  • (4)安裝css-loader

      npm install css-loader --save-dev
    複製程式碼
  • (5)配置webpack.config.js

      module.exports = {
          mode: `development`,
          entry: `./src/index.js`,
          output: {
              path: __dirname + `/dist`,
              filename: `bundle.js`
          },
          module: {
              rules: [
                  {
                      test: /.css$/,
                      use: `css-loader`
                  }
              ]
          }
      };
    複製程式碼
  • (6)在index.html中引入bundle.js

      <!--index.html-->
      <html>
          <head>
              <title>Test</title>
              <meta charset=`utf-8`/>
          </head>
          <body>
              <h1>Hello World!</h1>
          </body>
          <script src=`./bundle.js`></script>
      </html>
    複製程式碼
  • (7)在index.js中新增:

      import `./index.css`;
      console.log(`Success!`);
    複製程式碼
  • (8)在工程目錄下,使用以下命令打包:

      webpack
    複製程式碼

    檢視輸出結果,可以雙擊/dist/index.html檢視有沒有報錯以及控制檯的輸出內容。

三、如何通過Node指令碼使用webpack?

webpack提供Node API,方便我們在Node指令碼中使用webpack。

基本程式碼如下:

// 引入webpack模組。
const webpack = require(`webpack`);
// 引入配置資訊。
const config = require(`./webpack.config`);
// 通過webpack函式直接傳入config配置資訊。
const compiler = webpack(config);
// 通過compiler物件的apply方法應用外掛,也可在配置資訊中配置外掛。
compiler.apply(new webpack.ProgressPlugin());
// 使用compiler物件的run方法執行webpack,開始打包。
compiler.run((err, stats) => {
    if(err) {
        // 回撥中接收錯誤資訊。
        console.error(err);
    }
    else {
        // 回撥中接收打包成功的具體反饋資訊。
        console.log(stats);
    }
});
複製程式碼

四、動態生成index.html和bundle.js

動態生成是啥?動態生成就是指在打包後的模組名稱內插入hash值,使得每一次生成的模組具有不同的名稱,而index.html之所以要動態生成是因為每次打包生成的模組名稱不同,所以在HTML檔案內引用時也要更改script標籤,這樣才能保證每次都能引用到正確的JavaScript檔案。

為什麼要新增hash值?

之所以要動態生態生成bundle檔案,是為了防止瀏覽器快取機制阻礙檔案的更新,在每次修改程式碼之後,檔名中的hash都會發生改變,強制瀏覽器進行重新整理,獲取當前最新的檔案。

如何新增hash到bundle檔案中?

只需要在設定output時,在output.filename中新增[hash]到檔名中即可,比如:

// webpack.config.js
module.exports = {
    output: {
        path: __dirname + `/dist`,
        filename: `[name].[hash].js`
    }
};
複製程式碼

現在可以動態生成bundle檔案了,那麼如何動態新增bundle到HTML檔案呢?

每次打包bundle檔案之後,其名稱都會發生更改,每次人為地修改對應的HTML檔案以新增JavaScript檔案引用實在是令人煩躁,這時需要使用到強大的webpack外掛了,有一個叫html-webpack-plugin的外掛,可以自動生成HTML檔案。安裝到開發環境:

npm install html-webpack-plugin --save-dev
複製程式碼

安裝之後,在webpack.config.js中引入,並新增其例項到外掛屬性(plugins)中去:

// webpack.config.js
const HtmlWebpackPlugin = require(`html-webpack-plugin`);
module.exports = {
    // other configs ...
    plugins: [
        new HtmlWebpackPlugin({
            // options配置
        })
    ]
};
複製程式碼

這時就可以看到每次生成bundle檔案之後,都會被動態生成對應的html檔案。

在上面的程式碼中還可以看到HtmlWebpackPlugin外掛的建構函式還可以傳遞一個配置物件作為引數。比較有用的配置屬性有title(指定HTML中title標籤的內容,及網頁標題)、template(指定模板HTML檔案)等等,其他更多具體參考資訊請訪問:Html-Webpack-Plugin

五、清理/dist資料夾

由於每次生成的JavaScript檔案都不同名,所以新的檔案不會覆蓋舊的檔案,而舊的檔案一隻會存在於/dist資料夾中,隨著編譯次數的增加,這個資料夾會越來越膨脹,所以應該想辦法每次生成新的bundle檔案之前清理/dist資料夾,以確保資料夾的乾淨整潔,有以下兩個較好的處理辦法:

如果你是Node指令碼呼叫webpack打包:

如果通過Node API呼叫webpack進行打包,可以在打包之前直接使用Node的fs模組刪除/dist資料夾中的所有檔案:

const webpack = require(`webpack`);
const config = require(`./webpack.config`);
const fs = require(`fs`);
const compiler = webpack(config);

var deleteFolderRecursive = function(path) {
    if (fs.existsSync(path)) {
        fs.readdirSync(path).forEach(function(file, index){
            var curPath = path + "/" + file;
            if (fs.lstatSync(curPath).isDirectory()) { // recurse
                deleteFolderRecursive(curPath);
            } else { // delete file
                fs.unlinkSync(curPath);
            }
        });
        fs.rmdirSync(path);
    }
};

deleteFolderRecursive(__dirname + `/dist`);
compiler.run((err, stats) => {
    if(err) {
        console.error(err);
    }
    else {
        console.log(stats.hash);
    }
});
複製程式碼

可以看到在呼叫compiler.run打包之前,先使用自定義的deleteFolderRecursive方法刪除了/dist目錄下的所有檔案。

如果你使用webpack-cli進行打包

這時候就得通過webpack的外掛完成這個任務了,用到的外掛是clean-webpack-plugin

安裝:

npm install clean-webpack-plugin --save-dev
複製程式碼

然後在webpack.config.js檔案中新增外掛:

// webpack.config.js
const CleanWebpackPlugin = require(`clean-webpack-plugin`);
module.exports = {
    plugins: [
        new CleanWebpackPlugin([`dist`])
    ]
};
複製程式碼

之後再次打包,你會發現之前的打包檔案全部被刪除了。

六、搭建開發環境

開發環境與生產環境存在許多的差異,生產環境更講究生產效率,因此程式碼必須壓縮、精簡,必須去除一些生產環境並不需要用到的除錯工具,只需要提高應用的效率和效能即可。開發環境更講究除錯、測試,為了方便開發,我們需要搭建一個合適的開發環境。

(一)使用source maps進行除錯

為何要使用source maps?

因為webpack對原始碼進行打包後,會對原始碼進行壓縮、精簡、甚至變數名替換,在瀏覽器中,無法對程式碼逐行打斷點進行除錯,所有需要使用source maps進行除錯,它使得我們在瀏覽器中可以看到原始碼,進而逐行打斷點除錯。

如何使用source maps?

在配置中新增devtool屬性,賦值為source-map或者inline-source-map即可,後者報錯資訊更加具體,會指示原始碼中的具體錯誤位置,而source-map選項無法指示到原始碼中的具體位置。

(二)使用開發工具

每次寫完程式碼儲存之後還需要手動輸入命令或啟動Node指令碼進行編譯是一件令人不勝其煩的事情,選擇一下工具可以簡化開發過程中的工作:

  • 啟用watch模式
  • 使用webpack-dev-server
  • 使用webpack-dev-middleware

(1)使用watch模式

在使用webpack-cli進行打包時,通過命令webpack --watch即可開啟watch模式,進入watch模式之後,一旦依賴樹中的某一個模組發生了變化,webpack就會重新進行編譯。

(2)使用webpack-dev-server

使用過create-react-app或者Vue-Cli這種腳手架的童鞋都知道,通過命令npm run start即可建立一個本地伺服器,並且webpack會自動開啟瀏覽器開啟你正在開發的頁面,並且一旦你修改了檔案,瀏覽器會自動進行重新整理,基本做到了所見即所得的效果,比webpack的watch模式更加方便給力。

使用方法:

  • ① 安裝webpack-dev-server:

      npm install --save-dev webpack-dev-server
    複製程式碼
  • ② 修改配置檔案,新增devServer屬性:

      // webpack.config.js
      module.exports = {
          devServer: {
              contentBase: `./dist`
          }
      };
    複製程式碼
  • ③ 新增命令屬性到package.json

      // package.json
      {
          "scripts": {
              "start": "webpack-dev-server --open"
          }
      }
    複製程式碼
  • ④ 執行命令

      npm run start
    複製程式碼

    可以看到瀏覽器開啟後的實際效果,嘗試修改檔案,檢視瀏覽器是否實時更新。

此外還可以再devServer屬性下指定更多的配置資訊,比如開發伺服器的埠、熱更新模式、是否壓縮等等,具體查詢:Webpack

通過Node API使用webpack-dev-server

`use strict`;

const Webpack = require(`webpack`);
const WebpackDevServer = require(`../../../lib/Server`);
const webpackConfig = require(`./webpack.config`);

const compiler = Webpack(webpackConfig);
const devServerOptions = Object.assign({}, webpackConfig.devServer, {
    stats: {
        colors: true
    }
});
const server = new WebpackDevServer(compiler, devServerOptions);

server.listen(8080, `127.0.0.1`, () => {
    console.log(`Starting server on http://localhost:8080`);
});
複製程式碼

(3)使用webpack-dev-middleware

webpack-dev-middleware是一個比webpack-dev-server更加基礎的外掛,webpack-dev-server也使用了這個外掛,所以可以理解為webpack-dev-middleware的封裝層次更低,使用起來更加複雜,但是低封裝性意味著較高的自定義性,使用webpack-dev-middleware可以定義更多的設定來滿足更多的開發需求,它基於express模組。

這一塊不做過多介紹,因為webpack-dev-server已經能夠應付大多數開發場景,不用再設定更多的express屬性了,想要詳細瞭解的童鞋可以瞭解:使用 webpack-dev-middleware

(4)設定IDE

某些IDE具有安全寫入功能,導致開發伺服器執行時IDE無法儲存檔案,此時需要進行對應的設定。

具體參考:調整文字編輯器

(三)熱模組替換

熱模組替換(Hot Module Replacement,HMR),代表在應用程式執行過程中替換、新增、刪除模組,瀏覽器無需重新整理頁面即可呈現出相應的變化。

使用方法:

  • (1)在devServer屬性中新增hot屬性並賦值為true:

      // webpack.config.js
      module.exports = {
          devServer: {
              hot: true
          }
      }
    複製程式碼
  • (2)引入兩個外掛到webpack配置檔案:

      // webpack.config.js
      const webpack = require(`webpack`);
      module.exports = {
          devServer: {
              hot: true
          },
          plugins: [
              new webpack.NamedModulesPlugin(),
              new webpack.HotModuleReplacementPlugin()
          ]
      };
    複製程式碼
  • (3)在入口檔案底部新增程式碼,使得在所有程式碼發生變化時,都能夠通知webpack:

      if (module.hot) {
          module.hot.accept(`./print.js`, function() {
              console.log(`Accepting the updated intMe module!`);
              printMe();
          })
      }
    複製程式碼

熱模組替換比較難以掌控,容易報錯,推薦在不同的開發配置下使用不同的loader簡化HMR過程。具體參考:其他程式碼和框架

七、搭建生產環境

生產環境要求程式碼精簡、效能優異,而開發要求開發快速、測試方便,程式碼不要求簡潔,所以兩種環境下webpack打包的目的也不相同,所以最好將兩種環境下的配置檔案分開來。對於分開的配置檔案,在使用webpack時還是要對其中的配置資訊進行整合,webpack-merge是一個不錯的整合工具(Vue-Cli也有使用到)。

使用方法:

  • (1)安裝webpack-merge:

      npm install webpack-merge --save-dev
    複製程式碼
  • (2)建立三個配置檔案:

    • webpack.base.conf.js
    • webpack.dev.conf.js
    • webpack.prod.conf.js

    其中,webpack.base.conf.js表示最基礎的配置資訊,開發環境和生產環境都需要設定的資訊,比如entryoutputmodule等。在另外兩個檔案中配置一些對應環境下特有的資訊,然後通過webpack-merge模組與webpack.base.conf.js整合。

  • (3)新增npm scripts:

      // package.json
      {
          "scripts": {
              "start": "webpack-dev-server --open --config webpack.dev.conf.js",
              "build": "webpack --config webpack.prod.conf.js"
          }
      }
    複製程式碼

此外,建議設定mode屬性,因為生產環境下會自動開啟程式碼壓縮,免去了配置的麻煩。

八、效能優化

TreeShaking

TreeShaking表示移除JavaScript檔案中的未使用到的程式碼,webpack 4增強了這一部分的功能。通過配置package.json的sideEffects屬性,可以指定哪些檔案可以移除多餘程式碼。如果sideEffects設定為false,那麼表示檔案中的未使用程式碼可以放心移除,沒有副作用。如果有些檔案中的冗餘程式碼不能被移除,那麼可以設定sideEffects屬性為一個陣列,陣列內容為檔案的路徑字串。

指定無副作用的檔案之後,設定mode為”production”,再次構建程式碼,可以發現未使用到的程式碼已經被移除。

Tips

  • module.rules屬性中,設定include屬性以指定哪些檔案需要被loader處理。
  • 只使用必要的loader。
  • 保持最新版本。
  • 減少專案檔案數。

九、通過webpack構建PWA應用

漸進式網路應用程式(Progressive Web Application – PWA),是一種可以提供類似於原生應用程式(native app)體驗的網路應用程式(web app),在離線(offline)時應用程式能夠繼續執行功能,這是通過 Service Workers 技術來實現的。PWA是最近幾年比較火的概念,它的核心是由service worker技術實現的在客戶瀏覽器與伺服器之間搭建的一個代理伺服器,在網路暢通時,客戶瀏覽器會通過service worker訪問伺服器,並且快取註冊的檔案;在網路斷開時,瀏覽器會訪問service worker這個代理伺服器,使得在網路斷開的情況下,頁面還是能夠訪問,實現了類似原生應用的網站開發。create-react-app已經實現了PWA開發的配置。

下面介紹如何通過webpack快速開發PWA。

  • (1)安裝外掛workbox-webpack-plugin

      npm install workbox-webpack-plugin --save-dev
    複製程式碼
  • (2)在配置檔案中引入該外掛:

      // webpack.config.js
      const WorkboxPlugin = require(`workbox-webpack-plugin`);
      module.exports = {
          plugins: [
              new WorkboxPlugin.GenerateSW({
                  clientsClaim: true,
                  skipWaiting: true
              })
          ]
      };
    複製程式碼
  • (3)使用webpack進行編譯,打包出service-worker.js

  • (4)在入口檔案底部註冊service worker:

      if (`serviceWorker` in navigator) {
          window.addEventListener(`load`, () => {
              navigator.serviceWorker.register(`/service-worker.js`).then(registration => {
                  console.log(`SW registered: `, registration);
              }).catch(registrationError => {
                  console.log(`SW registration failed: `, registrationError);
              });
          });
      }
    複製程式碼
  • (5)開啟頁面,進行除錯:

      npm run start
    複製程式碼
  • (6)開啟瀏覽器除錯工具,檢視控制檯的輸出,如果輸出“SW registered: … …”,表示註冊service worker成功,接下來可以斷開網路,或者關閉伺服器,再次重新整理,可以看到頁面仍然可以顯示。

十、參考文章

十一、總結

webpack確實是一個功能強大的模組打包工具,豐富的loader和plugin使得其功能多而強。學習webpack使得我們可以自定義自己的開發環境,無需依賴create-react-appVue-Cli這類腳手架,也可以針對不同的需求對程式碼進行不同方案的處理。這篇筆記還只是一篇入門的筆記,如果要真正的構建較為複雜的開發環境和生產環境,還需要了解許多的loader和plugin,好在webpack官網提供了所有的說明,可以給使用者提供使用指南:

閱讀腳手架的原始碼也有助於學習webpack,今後應該還有進行這方面的學習,但是答辯即將到來,不知道畢業之前還有沒有機會^_^。

相關文章