webpack 基礎

初識 webpack

概念

依據 webpack 官方文檔,webpack 是一個 module bundler (模塊打包器)。初次聽到這個概念的時候,可能會一臉矇蔽:這是個啥?對個人開發有啥影響麼?javascript

爲了更好理解 webpack,咱們須要先了解模塊 Module 與打包 Bundle 的具體含義。只有將這兩個概念理清楚了纔會更清楚 webpack 的做用。css

模塊 Module

不管使用何種編程語言開發大型應用,最關鍵的特性就是代碼模塊化。模塊化的必要性在於:提升代碼的開發效率,方便代碼/功能的維護與重構。在C++裏爲命名空間,Java中爲包,名稱不同但解決的是同一問題。html

可是,最初的 JavaScript 並非用來編寫大規模代碼應用的,因而它的規範裏面並無模塊化這個標準。對於此,開源開發者提出了一些標準,如 CommoneJs 模塊模型、異步模塊定義(AMD)以及一些庫,來實現模塊化。java

幸運的是:ES6 爲 JavaScript 帶來了模塊特性。但瀏覽器實現這一特性還須要一段時間。node

打包 Bundle

接着模塊化的思路。在 JavaScript 程序開發過程當中,模塊化會產生不少不一樣的代碼文件(如 js、css等)。舉個栗子, SPA 頁面 index.html 用到了三個JS文件 和 一個 CSS 文件,那麼其經過script標籤引入這些文件webpack

//文件結構
|- index.html
|- main.css
| - a.js
| - b.js
| - c.js

//代碼演示
// index.html
<!doctype html>
<html>
  <head><link href="main.css" rel="stylesheet"></head>
  <body>
    <div>hello world</div>
    <script type="text/javascript" src="a.js"></script>
    <script type="text/javascript" src="b.js"></script>
    <script type="text/javascript" src="c.js"></script>
  </body>
</html>
複製代碼

由於有3 個 js 文件,因此瀏覽器須要發送三次 http 請求來獲取這三個文件。當咱們的項目逐漸變大,有幾十個到上百個 JavaScript 文件的時候,那問題會更嚴重。諸多問題都會暴露無遺(如網絡延遲等)。es6

是否是把全部 JavaScript 文件合成一個文件就行了呢?是的。咱們確實能夠這樣作。web

可是,矛盾點來了:在開發階段,咱們使用模塊化開發;在實際應用中,咱們但願可以將多個文件合併爲一個文件。這該怎麼辦呢?chrome

很顯然,在開發結束以後,咱們須要一個合併的過程。在開發完成後的這個合併過程就是打包。npm

能夠說,webpack 所作的一切工做,都是爲了實現模塊打包。

webpack基本示意圖

Entry 與 Output

將 webpack 理解爲模塊打包器,將 webpack 工做的過程理解爲模塊打包的過程。

webpack 的靈活性在於:整個過程的大部分因子咱們都是能夠配置的,極爲個性化。咱們先來認識整個過程的頭與尾:Entry屬性、Output屬性。

Entry 屬性指示 webpack 應該使用哪一個模塊,來做爲構建其內部依賴圖的開始。進入入口起點後,webpack 會找出有哪些模塊和庫是入口起點(直接和間接)依賴的。入口文件能夠有多個。根據項目特色,能夠以多種方式來配置 Entry。

Output 屬性告訴 webpack 在哪裏輸出它所建立的 bundles,以及如何命名這些文件。它表示的是打包後輸出文件的路徑。

Loader

這時,咱們來了解一下 webpack 的基本工做機制。

在默認狀況下,webpack 只可以識別 .js .json 格式的文件,其餘的文件它是沒法識別的。對於更多格式的文件,webpack 爲咱們提供了 Loader 選項,Loader 能夠當作是 webpack 不一樣格式文件的解析器。針對不一樣的文件格式,咱們能夠配置不一樣的 Loader。

Plugins

更進一步,咱們須要瞭解的是:webpack 的運行過程存在一個生命週期的過程。詳細的,能夠安裝lifecycle-webpack-plugin 插件來查看生命週期信息。

plugin 插件,能夠在webpack運行到某個階段的時候(構建流程中的特定時機),幫你作某些事情(注⼊擴展邏輯來改變構建結果),相似於生命週期鉤子的做用。

webpack 基本配置

瞭解了上面的這些概念以後,咱們來看 webpack 的基本配置。默認的配置文件是項目目錄下的 webpack.config.js 文件。

經常使用 Loader 配置

對 web 開發而言,經常使用的須要解析的文件無非是這幾種:CSS文件、圖片文件、字體文件。那麼對應的 loader 配置爲:

module: {
    rules: [
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"]
      },
      {
        test: /\.(png|jpe?g|gif)$/,
        use: {
          loader: "file-loader",
          options: {
            name: "[name]_[hash:6].[ext]",
            outputPath: "images/"
          }
        }
      },
      {
        test: /\.(eot|ttf|woff|woff2)$/,
        use: {
          loader: "file-loader",
          options: {
            name: "[name].[ext]"
          }
        }
      }
      }
    ]
  },
複製代碼

常見 Plugin 配置

Plugin 配置,就很是個性化了。隨着後期優化的不斷增強,咱們使用的插件會隨之增多。在此只介紹兩個插件:

  • htmlwebpackplugin :生成建立html入口文件,並將打包後的文件自動插入其中。
  • CleanWebpackPlugin :自動清除特定文件夾內容,用於刷新每次打包加載的 output 文件夾。

Babel 配置

有時候咱們會使用新的 ECMAScript 規範語法,但瀏覽器對這個新的語法規範可能不支持。因而須要降級處理。這個時候 Babel 就出現了。

Babel 是 JavaScript 編譯器,能將 ES6(或更新的)代碼轉換成 ES5 代碼,讓咱們開發過程當中放⼼使⽤ JS 新特性而不⽤擔憂兼容性問題。而且還能夠經過插件機制根據需求靈活的擴展。

Babel 配置會由於 Babel 版本不一樣而發生變化。最新的 Babel 7 配置,相比於 Babel 6 簡化了很多。

Babel 在執行編譯的過程當中,會從項目根⽬錄下的 .babelrc JSON⽂件中讀取配置。沒有該文件會從對應 loader 的 options 地⽅讀取配置。

基礎安裝

npm i babel-loader @babel/core @babel/preset-env -D

這幾個依賴包的做用以下:

  • babel-loader 是 webpack 與 babel 通訊的橋樑
  • @babel/core 是 babel 的核心功能庫
  • @babel/preset 裏面包含了 es6/es7/es8 轉 es5 的轉換規則

基礎配置

//配置方式一:直接在webpack.config.js 對應 loader 的 options 地⽅配置

{
    test: /\.js$/,
    use: {
      loader: "babel-loader",
      options: {
        presets: ["@babel/preset-env"]
      }
    }
 }
 
//配置方式二:項目根⽬錄下的 .babelrc 文件配置
//推薦方式二

{
     presets: ["@babel/preset-env"]
}
複製代碼

讓 babel 支持高級特性

經過上面的幾步還不夠,默認的 babel 只支持 let 等一些基礎的特性轉換(只轉換語法,不轉換新的 API),promise 等高級特性尚未轉換過來。這個時候還須要藉助 @babel/polyfill,將es的新特性都裝進來,來彌補低版本瀏覽器中缺失的特性。

npm install --save @babel/polyfill

值得注意的是 @babel/polyfill 是運行時依賴。

按需引入

咱們能夠嘗試全局引入 polyfill 。

// index.js 頂部
import "@babel/polyfill";
複製代碼

會發現打包的體積大了不少。這是由於 polyfill 默認會把全部特性注入進來。我但願當我使用到了 es 6+ 特性的時候,才注入,不用到的不注入,從而減小打包的體積。且對於現代的已經支持高級特性的瀏覽器,不須要用到 polyfill。所以咱們要按需引入 polyfill。

修改.babelrc 文件配置

{
     presets: [
     	[
     		"@babel/preset-env",
     		{
     			targets: { //編譯後的代碼支持的運行環境對象
     				edge: "17",
     				firefox: "60",
     				chrome: "67",
     				safari: "11.1"
     			},
     			corejs: 2, // 指定核心庫版本
     			useBuiltIns: "usage" //按需注入
     		}
}
複製代碼

重點是 useBuiltIns這個選項。 useBuiltIns選項是 babel 7 的新功能,這個選項告訴 babel 如何配置 @babel/polyfill 。 它有三個參數可使⽤:

  • entry: 須要在 webpack 的⼊口⽂件⾥ import "@babel/polyfill" ⼀一次。 babel 會根據你的使⽤用狀況導入墊片,沒有使⽤的功能不會被導⼊入相應的墊⽚。
  • usage: 不須要 import ,全⾃動檢測,可是要安裝 @babel/polyfill 。(試驗階段)
  • false: 若是你 import "@babel/polyfill" ,它不會排除掉沒有使用的墊⽚,程序體積會龐⼤。(不推薦)

咱們使用useBuiltIns: usage 即知足咱們的需求。

手寫實現Loader 與 Plugin

通過上面的操做,咱們已經能夠作到了對 webpack 的基本配置,並讓其可以運行 ES6 的代碼了。在講 webpack 的性能優化配置以前,咱們來嘗試一下手寫 Loader 與 Plugin 兩個模塊,以更好地理解這兩個模塊的做用。別太驚訝,這其實不難。往下看吧:

手寫Loader

咱們知道,webpack 中的 loader 是各類格式文件的解析器。簡單看待 loader,就能夠理解爲它是一個處理器。對輸入進行一番處理(解析)以後,輸出結果。

本身嘗試編寫一個 Loader,這個過程咱們能夠更好理解 Loader 的工做原理。其過程是比較簡單的。

在 webpack 中,Loader 就是一個函數(聲明式函數,不能用箭頭函數)。它經過參數項獲取到源代碼,作進一步的修飾處理後,返回處理過的源代碼。

就讓咱們來看看一個最簡單的替換源碼中文字符串的 loader (它的名字就叫 replaceLoader)是如何寫出來的吧:

  • 建立原材料(帶放入 loader 的源代碼,以及 loader):

    //index.js
    console.log("hello 親愛的");
    
    //replaceLoader.js
    module.exports = function(source){
      console.log(source, this, this.query);
      return source.replace("親愛的", "dear");
    }
    複製代碼
  • 在配置文件中使用 loader

    //webpack.config.js
    
    //node核⼼模塊path來處理路徑 
    const path = require('path') 
    ···
      module: {
        rules: [ {
                test: /\.js$/,
                use: path.resolve(__dirname,"./loader/replaceLoader.js"),
                options: {
                	name : "frank"
                }
        } ]
      },
    ···
    複製代碼
  • 如何給 loader 配置參數? loader 如何接收參數?

    正如上面的 loader 中,咱們能夠寫入對應的參數放入到 options 中,那麼 loader中如何接收到呢?有兩種方式:

    • this.query //經過 this.query 來接收配置文件傳遞進來的參數
    • loader-utils //該工具能夠協助處理包含參數在內的更多細節
  • 讓 loader 返回多個信息

    有的時候咱們不只僅但願 loader 可以返回一個信息,而是多是更多的信息,好比:報錯信息、source-map 信息等。webpack 中的 loader 能夠經過調用 this.callback 來返回多個信息。

    //replaceLoader.js
    
    module.exports = function(source) {
      const result = source.replace("親愛的", this.query.name);
      this.callback(null, result);
    };
    
    //callback中能夠包含的參數項
    //this.callback (
    	err: Error | null,
    	content: String | Buffer,
    	sourceMap?: SourceMap,
    	meta?: any
    )
    複製代碼
  • 讓 loader 異步返回

    //replaceLoader.js
    
    //使用 setTimeout 3sec 再返回
    module.exports = function(source) {
      console.log(this, this.query);
      const callback = this.async();
      setTimeout(() => {
        const result = source.replace("親愛的", this.query.name);
        callback(null, result);
      }, 3000);
    };
    複製代碼

手寫Plugin

經歷了本身手寫 loader 以後,咱們也能夠試試本身手寫一個簡單的 plugin。來加深對於 webpack 的認識。

前面講過:plugin 插件能夠在 webpack 運行到某個階段的時候(構建流程中的特定時機),幫你作某些事情(注⼊擴展邏輯來改變構建結果)。

在 webpack 中, plugin 必須是一個類(class),裏面必須包含一個 apply 函數,該函數接收一個參數:compiler。就這麼簡單,沒有更多的要求了。因此讓咱們來試試吧:

咱們來嘗試書寫一個在 webpack 打包完成後,往 dist 文件夾增長一個 js 文件的 plugin。很簡單沒啥真實做用,只是爲了演示生成 plugin 的過程而已。

  • 建立 ./plugin/add-js-file-webpack-plugin.js

    class AddJsFileWebpackPlugin {
    	constructor(){ 
      }
      apply(compiler){
      }
    }
    
    module.exports = AddJsFileWebpackPlugin;
    複製代碼
  • 配置文件裏使用

    const AddJsFileWebpackPlugin = require('./plugin/add-js-file-webpack-plugin.js');
    
    ...
    plugins: [new AddJsFileWebpackPlugin({name:"frank"})]
    ...
    複製代碼
  • 如何應用

    在上面的配置中,咱們看到插件傳入了參數{name:"frank"},咱們能夠在構造器中捕獲,並保存起來。

    而後咱們如何使用apply函數呢?這個時候就須要結合 webpack 的生命週期函數鉤子了。在官網上,能夠看到大量的鉤子能夠供咱們使用(有同步執行的鉤子,也有異步執行的鉤子)。

    plugin鉤子

    爲了演示的目的,咱們使用了 compiler.emit異步鉤子和compiler.compile同步鉤子:

    class AddJsFileWebpackPlugin {
      
      constructor(options) {
        this.name = options.name;
        console.log(options);
      }
      
      apply(compiler) {
        compiler.hooks.compile.tap("AddJsFileWebpackPlugin", compilation => {
          console.log("執行了, ");
        });
        
        compiler.hooks.emit.tapAsync(
          "AddJsFileWebpackPlugin",
          (compilation, cb) => {
            compilation.assets["finish.js"] = {
              source: function() {
                return "hello dear";
              },
              size: function() {
                return 20;
              }
            };
            cb();//異步鉤子中,必須調用 cb
          }
        );
      }
    }
    
    module.exports = AddJsFileWebpackPlugin;
    複製代碼

小結

在這篇文章中,咱們對 webpack 作了一個基本的認識與瞭解,並經過簡單的配置,讓其可以簡單運行代碼。最後,經過本身手寫 Loader 與 Plugin ,更好地認識其中的模塊與工做原理。接下來,咱們來看看具體如何利用 webpack 來進行優化配置吧!

相關文章
相關標籤/搜索