24 個例項入門並掌握「Webpack4」(三)

Zsh發表於2019-04-12

24 個例項入門並掌握「Webpack4」(二) 後續:

  1. PWA 配置
  2. TypeScript 配置
  3. Eslint 配置
  4. 使用 DLLPlugin 加快打包速度
  5. 多頁面打包配置
  6. 編寫 loader
  7. 編寫 plugin
  8. 編寫 Bundle

十七、PWA 配置

demo17 原始碼地址

本節使用 demo15 的程式碼為基礎

我們來模擬平時開發中,將打包完的程式碼防止到伺服器上的操作,首先打包程式碼 npm run build

然後安裝一個外掛 npm i http-server -D

在 package.json 中配置一個 script 命令

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

執行 npm run start

24 個例項入門並掌握「Webpack4」(三)

現在就起了一個服務,埠是 8080,現在訪問 http://127.0.0.1:8080 就能看到效果了

如果你有在跑別的專案,埠也是 8080,埠就衝突,記得先關閉其他專案的 8080 埠,再 npm run start

我們按 ctrl + c 關閉 http-server 來模擬伺服器掛了的場景,再訪問 http://127.0.0.1:8080 就會是這樣

24 個例項入門並掌握「Webpack4」(三)

頁面訪問不到了,因為我們伺服器掛了,PWA 是什麼技術呢,它可以在你第一次訪問成功的時候,做一個快取,當伺服器掛了之後,你依然能夠訪問這個網頁

首先安裝一個外掛:workbox-webpack-plugin

npm i workbox-webpack-plugin -D
複製程式碼

只有要上線的程式碼,才需要做 PWA 的處理,開啟 webpack.prod.conf.js

const WorkboxPlugin = require('workbox-webpack-plugin') // 引入 PWA 外掛

const prodConfig = {
  plugins: [
    // 配置 PWA
    new WorkboxPlugin.GenerateSW({
      clientsClaim: true,
      skipWaiting: true
    })
  ]
}
複製程式碼

重新打包,在 dist 目錄下會多出 service-worker.jsprecache-manifest.js 兩個檔案,通過這兩個檔案就能使我們的網頁支援 PWA 技術,service-worker.js 可以理解為另類的快取

24 個例項入門並掌握「Webpack4」(三)

還需要去業務程式碼中使用 service-worker

在 app.js 中加上以下程式碼

// 判斷該瀏覽器支不支援 serviceWorker
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker
      .register('/service-worker.js')
      .then(registration => {
        console.log('service-worker registed')
      })
      .catch(error => {
        console.log('service-worker registed error')
      })
  })
}
複製程式碼

重新打包,然後執行 npm run start 來模擬伺服器上的操作,最好用無痕模式開啟 http://127.0.0.1:8080 ,開啟控制檯

24 個例項入門並掌握「Webpack4」(三)

現在檔案已經被快取住了,再按 ctrl + c 關閉服務,再次重新整理頁面也還是能顯示的

TypeScript配置

demo18 原始碼地址

TypeScript 是 JavaScript 型別的超集,它可以編譯成純 JavaScript

新建資料夾,npm init -ynpm i webpack webpack-cli -D,新建 src 目錄,建立 index.ts 檔案,這段程式碼在瀏覽器上是執行不了的,需要我們打包編譯,轉成 js

class Greeter {
  greeting: string
  constructor(message: string) {
    this.greeting = message
  }
  greet() {
    return 'Hello, ' + this.greeting
  }
}

let greeter = new Greeter('world')

alert(greeter.greet())
複製程式碼
npm i ts-loader typescript -D
複製程式碼

新建 webpack.config.js 並配置

const path = require('path')

module.exports = {
  mode: 'production',
  entry: './src/index.ts',
  module: {
    rules: [
      {
        test: /\.ts?$/,
        use: 'ts-loader',
        exclude: /node_modules/
      }
    ]
  },
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
}
複製程式碼

在 package.json 中配置 script

{
  "scripts": {
    "build": "webpack"
  }
}
複製程式碼

執行 npm ruh build,報錯了,缺少 tsconfig.json 檔案

24 個例項入門並掌握「Webpack4」(三)

當打包 typescript 檔案的時候,需要在專案的根目錄下建立一個 tsconfig.json 檔案

以下為簡單配置,更多詳情看官網

{
  "compileerOptions": {
    "outDir": "./dist", // 寫不寫都行
    "module": "es6", // 用 es6 模組引入 import
    "target": "es5", // 打包成 es5
    "allowJs": true // 允許在 ts 中也能引入 js 的檔案
  }
}
複製程式碼

再次打包,開啟 bundle.js 檔案,將程式碼全部拷貝到瀏覽器控制檯上,使用這段程式碼,可以看到彈窗出現 Hello,world,說明 ts 編譯打包成功

24 個例項入門並掌握「Webpack4」(三)

引入第三方庫

npm i lodash
複製程式碼
import _ from 'lodash'

class Greeter {
  greeting: string
  constructor(message: string) {
    this.greeting = message
  }
  greet() {
    return _.join()
  }
}

let greeter = new Greeter('world')

alert(greeter.greet())
複製程式碼

lodash 的 join 方法需要我們傳遞引數,但是現在我們什麼都沒傳,也沒有報錯,我們使用 typescript 就是為了型別檢查,在引入第三方庫的時候也能如此,可是現在缺並沒有報錯或者提示

我們還要安裝一個 lodash 的 typescript 外掛,這樣就能識別 lodash 方法中的引數,一旦使用的不對就會報錯出來

npm i @types/lodash -D
複製程式碼

安裝完以後可以發現下劃線 _ 報錯了

24 個例項入門並掌握「Webpack4」(三)

需要改成 import * as _ from 'lodash',將 join 方法傳遞的引數刪除,還可以發現 join 方法的報錯,這就體現了 typescript 的優勢,同理,引入 jQuery 也要引入一個 jQuery 對應的型別外掛

24 個例項入門並掌握「Webpack4」(三)

如何知道使用的庫需要安裝對應的型別外掛呢?

開啟TypeSearch,在這裡對應的去搜尋你想用的庫有沒有型別外掛,如果有隻需要 npm i @types/jquery -D 即可

24 個例項入門並掌握「Webpack4」(三)

十九、Eslint 配置

demo19 原始碼地址

建立一個空資料夾,npm init -ynpm webpack webpack-cli -D 起手式,之後安裝 eslint 依賴

npm i eslint -D
複製程式碼

使用 npx 執行此專案中的 eslint 來初始化配置,npx eslint --init

24 個例項入門並掌握「Webpack4」(三)

24 個例項入門並掌握「Webpack4」(三)

這裡會有選擇是 React/Vue/JavaScript,我們統一都先選擇 JavaScript。選完後會在專案的根目錄下新建一個 .eslintrc.js 配置檔案

module.exports = {
  env: {
    browser: true,
    es6: true
  },
  extends: 'eslint:recommended',
  globals: {
    Atomics: 'readonly',
    SharedArrayBuffer: 'readonly'
  },
  parserOptions: {
    ecmaVersion: 2018,
    sourceType: 'module'
  },
  rules: {}
}
複製程式碼

裡面就是 eslint 的一些規範,也可以定義一些規則,具體看 eslint 配置規則

24 個例項入門並掌握「Webpack4」(三)

在 index.js 中隨便寫點程式碼來測試一下 eslint

24 個例項入門並掌握「Webpack4」(三)

eslint 報錯提示,變數定義後卻沒有使用,如果在編輯器裡沒出現報錯提示,需要在 vscode 裡先安裝一個 eslint 擴充套件,它會根據你當前目錄的下的 .eslintrc.js 檔案來做作為校驗的規則

24 個例項入門並掌握「Webpack4」(三)

也可以通過命令列的形式,讓 eslint 校驗整個 src 目錄下的檔案

24 個例項入門並掌握「Webpack4」(三)

如果你覺得某個規則很麻煩,想遮蔽掉某個規則的時候,可以這樣,根據 eslint 的報錯提示,比如上面的 no-unused-vars,將這條規則複製一下,在 .eslintrc.js 中的 rules 裡配置一下,"no-unused-vars": 0,0 表示禁用,儲存後,就不會報錯了,但是這種方式是適用於全域性的配置,如果你只想在某一行程式碼上遮蔽掉 eslint 校驗,可以這樣做

/* eslint-disable no-unused-vars */
let a = '1'
複製程式碼

這個 eslint 的 vscode 擴充套件和 webpack 是沒有什麼關聯的,我們現在要講的是如何在 webpack 裡使用 eslint,首先安裝一個外掛

npm i eslint-loader -D
複製程式碼

在 webpack.config.js 中進行配置

/* eslint-disable no-undef */
// eslint-disable-next-line no-undef
const path = require('path')

module.exports = {
  mode: 'production',
  entry: {
    app: './src/index.js' // 需要打包的檔案入口
  },
  module: {
    rules: [
      {
        test: /\.js$/, // 使用正則來匹配 js 檔案
        exclude: /nodes_modules/, // 排除依賴包資料夾
        use: {
          loader: 'eslint-loader' // 使用 eslint-loader
        }
      }
    ]
  },
  output: {
    // eslint-disable-next-line no-undef
    publicPath: __dirname + '/dist/', // js 引用的路徑或者 CDN 地址
    // eslint-disable-next-line no-undef
    path: path.resolve(__dirname, 'dist'), // 打包檔案的輸出目錄
    filename: 'bundle.js' // 打包後生產的 js 檔案
  }
}
複製程式碼

由於 webpack 配置檔案也會被 eslint 校驗,這裡我先寫上註釋,關閉校驗

如果你有使用 babel-loader 來轉譯,則 loader 應該這麼寫

loader: ['babel-loader', 'eslint-loader']

rules 的執行順序是從右往左,從下往上的,先經過 eslint 校驗判斷程式碼是否符合規範,然後再通過 babel 來做轉移

配置完 webpack.config.js,我們將 index.js 還原回之前報錯的狀態,不要使用註釋關閉校驗,然後執行打包命令,記得去 package.json 配置 script

24 個例項入門並掌握「Webpack4」(三)

會在打包的時候,提示程式碼不合格,不僅僅是生產環境,開發環境也可以配置,可以將 eslint-loader 配置到 webpack 的公共模組中,這樣更有利於我們檢查程式碼規範

如:設定 fix 為 true,它會幫你自動修復一些錯誤,不能自動修復的,還是需要你自己手動修復

{
 loader: 'eslint-loader', // 使用 eslint-loader
  options: {
    fix: true
  }
}
複製程式碼

關於 eslint-loader,webpack 的官網也給出了配置,感興趣的朋友自己去看一看

二十、使用 DLLPlugin 加快打包速度

demo20 原始碼地址

本節使用 demo15 的程式碼為基礎

我們先安裝一個 lodash 外掛 npm i lodash,並在 app.js 檔案中寫入

import _ from 'lodash'
console.log(_.join(['hello', 'world'], '-'))
複製程式碼

在 build 資料夾下新建 webpack.dll.js 檔案

const path = require('path')

module.exports = {
  mode: 'production',
  entry: {
    vendors: ['lodash', 'jquery']
  },
  output: {
    filename: '[name].dll.js',
    path: path.resolve(__dirname, '../dll'),
    library: '[name]'
  }
}
複製程式碼

這裡使用 library,忘記的朋友可以回顧一下第十六節,自定義函式庫裡的內容,定義了 library 就相當於掛載了這個全域性變數,只要在控制檯輸入全域性變數的名稱就可以顯示裡面的內容,比如這裡我們是 library: '[name]' 對應的 name 就是我們在 entry 裡定義的 vendors

在 package.json 中的 script 再新增一個命令

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

執行 npm run build:dll,會生成 dll 資料夾,並且檔案為 vendors.dll.js

24 個例項入門並掌握「Webpack4」(三)

開啟檔案可以發現 lodash 已經被打包到了 dll 檔案中

24 個例項入門並掌握「Webpack4」(三)

那我們要如何使用這個 vendors.dll.js 檔案呢

需要再安裝一個依賴 npm i add-asset-html-webpack-plugin,它會將我們打包後的 dll.js 檔案注入到我們生成的 index.html 中

在 webpack.base.conf.js 檔案中引入

const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')

module.exports = {
  plugins: [
    new AddAssetHtmlWebpackPlugin({
      filepath: path.resolve(__dirname, '../dll/vendors.dll.js') // 對應的 dll 檔案路徑
    })
  ]
}
複製程式碼

使用 npm run dev 來開啟網頁

24 個例項入門並掌握「Webpack4」(三)

現在我們已經把第三方模組單獨打包成了 dll 檔案,並使用

但是現在使用第三方模組的時候,要用 dll 檔案,而不是使用 /node_modules/ 中的庫,繼續來修改 webpack.dll.js 配置

const path = require('path')
const webpack = require('webpack')

module.exports = {
  mode: 'production',
  entry: {
    vendors: ['lodash', 'jquery']
  },
  output: {
    filename: '[name].dll.js',
    path: path.resolve(__dirname, '../dll'),
    library: '[name]'
  },
  plugins: [
    new webpack.DllPlugin({
      name: '[name]',
      // 用這個外掛來分析打包後的這個庫,把庫裡的第三方對映關係放在了這個 json 的檔案下,這個檔案在 dll 目錄下
      path: path.resolve(__dirname, '../dll/[name].manifest.json')
    })
  ]
}
複製程式碼

儲存後重新打包 dll,npm run build:dll

24 個例項入門並掌握「Webpack4」(三)

修改 webpack.base.conf.js 檔案,新增 webpack.DllReferencePlugin 外掛

module.exports = {
  plugins: [
    // 引入我們打包後的對映檔案
    new webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, '../dll/vendors.manifest.json')
    })
  ]
}
複製程式碼

之後再 webpack 打包的時候,就可以結合之前的全域性變數 vendors 和 這個新生成的 vendors.manifest.json 對映檔案,然後來對我們的原始碼進行分析,一旦分析出使用第三方庫是在 vendors.dll.js 裡,就會去使用 vendors.dll.js,不會去使用 /node_modules/ 裡的第三方庫了

再次打包 npm run build,可以把 webpack.DllReferencePlugin 模組註釋後再打包對比一下

註釋前 4000ms 左右,註釋後 4300ms 左右,雖然只是快了 300ms,但是我們目前只是實驗性的 demo,實際專案中,比如拿 vue 來說,vue,vue-router,vuex,element-ui,axios 等第三方庫都可以打包到 dll.js 裡,那個時候的打包速度就能提升很多了

還可以繼續拆分,修改 webpack.dll.js 檔案

const path = require('path')
const webpack = require('webpack')

module.exports = {
  mode: 'production',
  entry: {
    lodash: ['lodash'],
    jquery: ['jquery']
  },
  output: {
    filename: '[name].dll.js',
    path: path.resolve(__dirname, '../dll'),
    library: '[name]'
  },
  plugins: [
    new webpack.DllPlugin({
      name: '[name]',
      path: path.resolve(__dirname, '../dll/[name].manifest.json') // 用這個外掛來分析打包後的這個庫,把庫裡的第三方對映關係放在了這個 json 的檔案下,這個檔案在 dll 目錄下
    })
  ]
}
複製程式碼

執行 npm run build:dll

24 個例項入門並掌握「Webpack4」(三)

可以把之前打包的 vendors.dll.jsvendors.manifest.json 對映檔案給刪除掉

然後再修改 webpack.base.conf.js

module.exports = {
  plugins: [
    new AddAssetHtmlWebpackPlugin({
      filepath: path.resolve(__dirname, '../dll/lodash.dll.js')
    }),
    new AddAssetHtmlWebpackPlugin({
      filepath: path.resolve(__dirname, '../dll/jquery.dll.js')
    }),
    new webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, '../dll/lodash.manifest.json')
    }),
    new webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, '../dll/jquery.manifest.json')
    })
  ]
}
複製程式碼

儲存後執行 npm run dev,看看能不能成功執行

24 個例項入門並掌握「Webpack4」(三)

這還只是拆分了兩個第三方模組,就要一個個配置過去,有沒有什麼辦法能簡便一點呢? 有!

這裡使用 node 的 api,fs 模組來讀取資料夾裡的內容,建立一個 plugins 陣列用來存放公共的外掛

const fs = require('fs')

const plugins = [
  // 開發環境和生產環境二者均需要的外掛
  new HtmlWebpackPlugin({
    title: 'webpack4 實戰',
    filename: 'index.html',
    template: path.resolve(__dirname, '..', 'index.html'),
    minify: {
      collapseWhitespace: true
    }
  }),
  new webpack.ProvidePlugin({ $: 'jquery' })
]

const files = fs.readdirSync(path.resolve(__dirname, '../dll'))
console.log(files)
複製程式碼

寫完可以先輸出一下,把 plugins 給註釋掉,npm run build 打包看看輸出的內容,可以看到資料夾中的內容以陣列的形式被列印出來了,之後我們對這個陣列做一些迴圈操作就行了

24 個例項入門並掌握「Webpack4」(三)

完整程式碼:

const path = require('path')
const fs = require('fs')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')

// 存放公共外掛
const plugins = [
  // 開發環境和生產環境二者均需要的外掛
  new HtmlWebpackPlugin({
    title: 'webpack4 實戰',
    filename: 'index.html',
    template: path.resolve(__dirname, '..', 'index.html'),
    minify: {
      collapseWhitespace: true
    }
  }),
  new webpack.ProvidePlugin({ $: 'jquery' })
]

// 自動引入 dll 中的檔案
const files = fs.readdirSync(path.resolve(__dirname, '../dll'))
files.forEach(file => {
  if (/.*\.dll.js/.test(file)) {
    plugins.push(
      new AddAssetHtmlWebpackPlugin({
        filepath: path.resolve(__dirname, '../dll', file)
      })
    )
  }
  if (/.*\.manifest.json/.test(file)) {
    plugins.push(
      new webpack.DllReferencePlugin({
        manifest: path.resolve(__dirname, '../dll', file)
      })
    )
  }
})

module.exports = {
  entry: {
    app: './src/app.js'
  },
  output: {
    path: path.resolve(__dirname, '..', 'dist')
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader'
          }
        ]
      },
      {
        test: /\.(png|jpg|jpeg|gif)$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              name: '[name]-[hash:5].min.[ext]',
              limit: 1000, // size <= 1KB
              outputPath: 'images/'
            }
          },
          // img-loader for zip img
          {
            loader: 'image-webpack-loader',
            options: {
              // 壓縮 jpg/jpeg 圖片
              mozjpeg: {
                progressive: true,
                quality: 65 // 壓縮率
              },
              // 壓縮 png 圖片
              pngquant: {
                quality: '65-90',
                speed: 4
              }
            }
          }
        ]
      },
      {
        test: /\.(eot|ttf|svg)$/,
        use: {
          loader: 'url-loader',
          options: {
            name: '[name]-[hash:5].min.[ext]',
            limit: 5000, // fonts file size <= 5KB, use 'base64'; else, output svg file
            publicPath: 'fonts/',
            outputPath: 'fonts/'
          }
        }
      }
    ]
  },
  plugins,
  performance: false
}
複製程式碼

使用 npm run dev 開啟網頁也沒有問題了,這樣自動注入 dll 檔案也搞定了,之後還要再打包第三方庫只要新增到 webpack.dll.js 裡面的 entry 屬性中就可以了

二十一、多頁面打包配置

demo21 原始碼地址

本節使用 demo20 的程式碼為基礎

在 src 目錄下新建 list.js 檔案,裡面寫 console.log('這裡是 list 頁面')

24 個例項入門並掌握「Webpack4」(三)

在 webpack.base.conf.js 中配置 entry,配置兩個入口

module.exports = {
  entry: {
    app: './src/app.js',
    list: './src/list.js'
  }
}
複製程式碼

如果現在我們直接 npm run build 打包,在打包自動生成的 index.html 檔案中會發現 list.js 也被引入了,說明多入口打包成功,但並沒有實現多個頁面的打包,我想打包出 index.htmllist.html 兩個頁面,並且在 index.html 中引入 app.js,在 list.html 中引入 list.js,該怎麼做?

為了方便演示,先將 webpack.prod.conf.jscacheGroups 新增一個 default 屬性,自定義 name

optimization: {
  splitChunks: {
    chunks: 'all',
    cacheGroups: {
      jquery: {
        name: 'jquery', // 單獨將 jquery 拆包
        priority: 15,
        test: /[\\/]node_modules[\\/]jquery[\\/]/
      },
      vendors: {
        test: /[\\/]node_modules[\\/]/,
        name: 'vendors'
      },
      default: {
        name: 'code-segment'
      }
    }
  }
}
複製程式碼

開啟 webpack.base.conf.js 檔案,將 HtmlWebpackPlugin 拷貝一份,使用 chunks 屬性,將需要打包的模組對應寫入

// 存放公共外掛
const plugins = [
  new HtmlWebpackPlugin({
    title: 'webpack4 實戰',
    filename: 'index.html',
    template: path.resolve(__dirname, '..', 'index.html'),
    chunks: ['app', 'vendors', 'code-segment', 'jquery', 'lodash']
  }),
  new HtmlWebpackPlugin({
    title: '多頁面打包',
    filename: 'list.html',
    template: path.resolve(__dirname, '..', 'index.html'),
    chunks: ['list', 'vendors', 'code-segment', 'jquery', 'lodash']
  }),
  new CleanWebpackPlugin(),
  new webpack.ProvidePlugin({ $: 'jquery' })
]
複製程式碼

打包後的 dist 目錄下生成了兩個 html

24 個例項入門並掌握「Webpack4」(三)

開啟 index.html 可以看到引入的是 app.js,而 list.html 引入的是 list.js,這就是 HtmlWebpackPlugin 外掛的 chunks 屬性,自定義引入的 js

如果要打包三個頁面,再去 copy HtmlWebpackPlugin,通過在 entry 中配置,如果有四個,五個,這樣手動的複製就比較麻煩了,可以寫個方法自動生成 HtmlWebpackPlugin 配置

修改 webpack.base.conf.js

const path = require('path')
const fs = require('fs')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')
const CleanWebpackPlugin = require('clean-webpack-plugin')

const makePlugins = configs => {
  // 基礎外掛
  const plugins = [
    new CleanWebpackPlugin(),
    new webpack.ProvidePlugin({ $: 'jquery' })
  ]

  // 根據 entry 自動生成 HtmlWebpackPlugin 配置,配置多頁面
  Object.keys(configs.entry).forEach(item => {
    plugins.push(
      new HtmlWebpackPlugin({
        title: '多頁面配置',
        template: path.resolve(__dirname, '..', 'index.html'),
        filename: `${item}.html`,
        chunks: [item, 'vendors', 'code-segment', 'jquery', 'lodash']
      })
    )
  })

  // 自動引入 dll 中的檔案
  const files = fs.readdirSync(path.resolve(__dirname, '../dll'))
  files.forEach(file => {
    if (/.*\.dll.js/.test(file)) {
      plugins.push(
        new AddAssetHtmlWebpackPlugin({
          filepath: path.resolve(__dirname, '../dll', file)
        })
      )
    }
    if (/.*\.manifest.json/.test(file)) {
      plugins.push(
        new webpack.DllReferencePlugin({
          manifest: path.resolve(__dirname, '../dll', file)
        })
      )
    }
  })

  return plugins
}

const configs = {
  entry: {
    index: './src/app.js',
    list: './src/list.js'
  },
  output: {
    path: path.resolve(__dirname, '..', 'dist')
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader'
          }
        ]
      },
      {
        test: /\.(png|jpg|jpeg|gif)$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              name: '[name]-[hash:5].min.[ext]',
              limit: 1000, // size <= 1KB
              outputPath: 'images/'
            }
          },
          // img-loader for zip img
          {
            loader: 'image-webpack-loader',
            options: {
              // 壓縮 jpg/jpeg 圖片
              mozjpeg: {
                progressive: true,
                quality: 65 // 壓縮率
              },
              // 壓縮 png 圖片
              pngquant: {
                quality: '65-90',
                speed: 4
              }
            }
          }
        ]
      },
      {
        test: /\.(eot|ttf|svg)$/,
        use: {
          loader: 'url-loader',
          options: {
            name: '[name]-[hash:5].min.[ext]',
            limit: 5000, // fonts file size <= 5KB, use 'base64'; else, output svg file
            publicPath: 'fonts/',
            outputPath: 'fonts/'
          }
        }
      }
    ]
  },
  performance: false
}

makePlugins(configs)

configs.plugins = makePlugins(configs)

module.exports = configs
複製程式碼

再次打包後效果相同,如果還要增加頁面,只要在 entry 中再引入一個 js 檔案作為入口即可

多頁面配置其實就是定義多個 entry,配合 htmlWebpackPlugin 生成多個 html 頁面

二十二、編寫 loader

demo22 原始碼地址

新建資料夾,npm init -ynpm i webpack webpack-cli -D,新建 src/index.js,寫入 console.log('hello world')

新建 loaders/replaceLoader.js 檔案

module.exports = function(source) {
  return source.replace('world', 'loader')
}
複製程式碼

source 引數就是我們的原始碼,這裡是將原始碼中的 world 替換成 loader

新建 webpack.config.js

const path = require('path')

module.exports = {
  mode: 'development',
  entry: {
    main: './src/index.js'
  },
  module: {
    rules: [
      {
        test: /.js/,
        use: [path.resolve(__dirname, './loaders/replaceLoader.js')] // 引入自定義 loader
      }
    ]
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js'
  }
}
複製程式碼

目錄結構:

24 個例項入門並掌握「Webpack4」(三)

打包後開啟 dist/main.js 檔案,在最底部可以看到 world 已經被改為了 loader,一個最簡單的 loader 就寫完了

新增 optiions 屬性

const path = require('path')

module.exports = {
  mode: 'development',
  entry: {
    main: './src/index.js'
  },
  module: {
    rules: [
      {
        test: /.js/,
        use: [
          {
            loader: path.resolve(__dirname, './loaders/replaceLoader.js'),
            options: {
              name: 'xh'
            }
          }
        ]
      }
    ]
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js'
  }
}
複製程式碼

修改 replaceLoader.js 檔案,儲存後打包,輸出看看效果

module.exports = function(source) {
  console.log(this.query)
  return source.replace('world', this.query.name)
}
複製程式碼

24 個例項入門並掌握「Webpack4」(三)

打包後生成的檔案也改為了 options 中定義的 name

更多的配置見官網 API,找到 Loader Interface,裡面有個 this.query

24 個例項入門並掌握「Webpack4」(三)

如果你的 options 不是一個物件,而是按字串形式寫的話,可能會有一些問題,這裡官方推薦使用 loader-utils 來獲取 options 中的內容

安裝 npm i loader-utils -D,修改 replaceLoader.js

const loaderUtils = require('loader-utils')

module.exports = function(source) {
  const options = loaderUtils.getOptions(this)
  console.log(options)
  return source.replace('world', options.name)
}
複製程式碼

console.log(options)console.log(this.query) 輸出內容一致

如果你想傳遞額外的資訊出去,return 就不好用了,官網給我們提供了 this.callback API,用法如下

this.callback(
  err: Error | null,
  content: string | Buffer,
  sourceMap?: SourceMap,
  meta?: any
)
複製程式碼

修改 replaceLoader.js

const loaderUtils = require('loader-utils')

module.exports = function(source) {
  const options = loaderUtils.getOptions(this)
  const result = source.replace('world', options.name)

  this.callback(null, result)
}
複製程式碼

目前沒有用到 sourceMap(必須是此模組可解析的源對映)、meta(可以是任何內容(例如一些後設資料)) 這兩個可選引數,只將 result 返回回去,儲存重新打包後,效果和 return 是一樣的

如果在 loader 中寫非同步程式碼,會怎麼樣

const loaderUtils = require('loader-utils')

module.exports = function(source) {
  const options = loaderUtils.getOptions(this)

  setTimeout(() => {
    const result = source.replace('world', options.name)
    return result
  }, 1000)
}
複製程式碼

24 個例項入門並掌握「Webpack4」(三)

報錯 loader 沒有返回,這裡使用 this.async 來寫非同步程式碼

const loaderUtils = require('loader-utils')

module.exports = function(source) {
  const options = loaderUtils.getOptions(this)

  const callback = this.async()

  setTimeout(() => {
    const result = source.replace('world', options.name)
    callback(null, result)
  }, 1000)
}
複製程式碼

模擬一個同步 loader 和一個非同步 loader

新建一個 replaceLoaderAsync.js 檔案,將之前寫的非同步程式碼放入,修改 replaceLoader.js 為同步程式碼

// replaceLoaderAsync.js

const loaderUtils = require('loader-utils')
module.exports = function(source) {
  const options = loaderUtils.getOptions(this)
  const callback = this.async()
  setTimeout(() => {
    const result = source.replace('world', options.name)
    callback(null, result)
  }, 1000)
}

// replaceLoader.js
module.exports = function(source) {
  return source.replace('xh', 'world')
}
複製程式碼

修改 webpack.config.js,loader 的執行順序是從下到上,先執行非同步程式碼,將 world 改為 xh,再執行同步程式碼,將 xh 改為 world

module: {
  rules: [
    {
      test: /.js/,
      use: [
        {
          loader: path.resolve(__dirname, './loaders/replaceLoader.js')
        },
        {
          loader: path.resolve(__dirname, './loaders/replaceLoaderAsync.js'),
          options: {
            name: 'xh'
          }
        }
      ]
    }
  ]
}
複製程式碼

儲存後打包,在 mian.js 中可以看到已經改為了 hello world,使用多個 loader 也完成了

如果有多個自定義 loader,每次都通過 path.resolve(__dirname, xxx) 這種方式去寫,有沒有更好的方法?

使用 resolveLoader,定義 modules,當你使用 loader 的時候,會先去 node_modules 中去找,如果沒找到就會去 ./loaders 中找

const path = require('path')

module.exports = {
  mode: 'development',
  entry: {
    main: './src/index.js'
  },
  resolveLoader: {
    modules: ['node_modules', './loaders']
  },
  module: {
    rules: [
      {
        test: /.js/,
        use: [
          {
            loader: 'replaceLoader.js'
          },
          {
            loader: 'replaceLoaderAsync.js',
            options: {
              name: 'xh'
            }
          }
        ]
      }
    ]
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js'
  }
}
複製程式碼

二十三、編寫 plugin

demo23 原始碼地址

首先新建一個資料夾,npm 起手式操作一番,具體的在前幾節已經說了,不再贅述

在根目錄下新建 plugins 資料夾,新建 copyright-webpack-plugin.js,一般我們用的都是 xxx-webpack-plugin,所以我們命名也按這樣來,plugin 的定義是一個類

class CopyrightWebpackPlugin {
  constructor() {
    console.log('外掛被使用了')
  }
  apply(compiler) {}
}

module.exports = CopyrightWebpackPlugin
複製程式碼

在 webpack.config.js 中使用,所以每次使用 plugin 都要使用 new,因為本質上 plugin 是一個類

const path = require('path')
const CopyrightWebpackPlugin = require('./plugins/copyright-webpack-plugin')

module.exports = {
  mode: 'development',
  entry: {
    main: './src/index.js'
  },
  plugins: [new CopyrightWebpackPlugin()],
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js'
  }
}
複製程式碼

儲存後打包,外掛被使用了,只不過我們什麼都沒幹

24 個例項入門並掌握「Webpack4」(三)

如果我們要傳遞引數,可以這樣

new CopyrightWebpackPlugin({
  name: 'xh'
})
複製程式碼

同時在 copyright-webpack-plugin.js 中接收

class CopyrightWebpackPlugin {
  constructor(options) {
    console.log('外掛被使用了')
    console.log('options = ', options)
  }
  apply(compiler) {}
}

module.exports = CopyrightWebpackPlugin
複製程式碼

24 個例項入門並掌握「Webpack4」(三)

我們先把 constructor 註釋掉,在即將要把打包的結果,放入 dist 目錄之前的這個時刻,我們來做一些操作

apply(compiler) {} compiler 可以看作是 webpack 的例項,具體見官網 compiler-hooks

hooks 是鉤子,像 vue、react 的生命週期一樣,找到 emit 這個時刻,將打包結果放入 dist 目錄前執行,這裡是個 AsyncSeriesHook 非同步方法

24 個例項入門並掌握「Webpack4」(三)

class CopyrightWebpackPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync(
      'CopyrightWebpackPlugin',
      (compilation, cb) => {
        console.log(11)
        cb()
      }
    )
  }
}

module.exports = CopyrightWebpackPlugin
複製程式碼

因為 emit非同步的,可以通過 tapAsync 來寫,當要把程式碼放入到 dist 目錄之前,就會觸發這個鉤子,走到我們定義的函式裡,如果你用 tapAsync 函式,記得最後要用 cb() ,tapAsync 要傳遞兩個引數,第一個引數傳遞我們定義的外掛名稱

儲存後再次打包,我們寫的內容也輸出了

24 個例項入門並掌握「Webpack4」(三)

compilation 這個引數裡存放了這次打包的所有內容,可以輸出一下 compilation.assets 看一下

24 個例項入門並掌握「Webpack4」(三)

返回結果是一個物件,main.js 是 key,也就是打包後生成的檔名及檔案字尾,我們可以來仿照一下

class CopyrightWebpackPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync(
      'CopyrightWebpackPlugin',
      (compilation, cb) => {
        // 生成一個 copyright.txt 檔案
        compilation.assets['copyright.txt'] = {
          source: function() {
            return 'copyright by xh'
          },
          size: function() {
            return 15 // 上面 source 返回的字元長度
          }
        }
        console.log('compilation.assets = ', compilation.assets)
        cb()
      }
    )
  }
}

module.exports = CopyrightWebpackPlugin
複製程式碼

24 個例項入門並掌握「Webpack4」(三)

在 dist 目錄下生成了 copyright.txt 檔案

之前介紹的是非同步鉤子,現在使用同步鉤子

24 個例項入門並掌握「Webpack4」(三)

class CopyrightWebpackPlugin {
  apply(compiler) {
    // 同步鉤子
    compiler.hooks.compile.tap('CopyrightWebpackPlugin', compilation => {
      console.log('compile')
    })

    // 非同步鉤子
    compiler.hooks.emit.tapAsync(
      'CopyrightWebpackPlugin',
      (compilation, cb) => {
        compilation.assets['copyright.txt'] = {
          source: function() {
            return 'copyright by xh'
          },
          size: function() {
            return 15 // 字元長度
          }
        }
        console.log('compilation.assets = ', compilation.assets)
        cb()
      }
    )
  }
}

module.exports = CopyrightWebpackPlugin
複製程式碼

二十四、編寫 Bundle

demo24 原始碼地址

模組分析

在 src 目錄下新建三個檔案 word.jsmessage.jsindex.js,對應的程式碼:

// word.js
export const word = 'hello'

// message.js
import { word } from './word.js'

const message = `say ${word}`

export default message

// index.js
import message from './message.js'

console.log(message)
複製程式碼

新建 bundle.js

const fs = require('fs')

const moduleAnalyser = filename => {
  const content = fs.readFileSync(filename, 'utf-8')
  console.log(content)
}

moduleAnalyser('./src/index.js')
複製程式碼

使用 node 的 fs 模組,讀取檔案資訊,並在控制檯輸出,這裡全域性安裝一個外掛,來顯示程式碼高亮,npm i cli-highlight -g,執行 node bundle.js | highlight

24 個例項入門並掌握「Webpack4」(三)

index.js 中的程式碼已經被輸出到控制檯上,而且程式碼有高亮,方便閱讀,讀取入口檔案資訊就完成了

現在我們要讀取 index.js 檔案中使用的 message.js 依賴,import message from './message.js'

安裝一個第三方外掛 npm i @babel/parser

@babel/parser 是 Babel 中使用的 JavaScript 解析器。

官網也提供了相應的示例程式碼,根據示例程式碼來仿照,修改我們的檔案

24 個例項入門並掌握「Webpack4」(三)

const fs = require('fs')
const parser = require('@babel/parser')

const moduleAnalyser = filename => {
  const content = fs.readFileSync(filename, 'utf-8')
  console.log(
    parser.parse(content, {
      sourceType: 'module'
    })
  )
}

moduleAnalyser('./src/index.js')
複製程式碼

我們使用的是 es6 的 module 語法,所以 sourceType: 'module'

24 個例項入門並掌握「Webpack4」(三)

儲存後執行,輸出了 AST (抽象語法樹),裡面有一個 body 欄位,我們輸出這個欄位

const fs = require('fs')
const parser = require('@babel/parser')

const moduleAnalyser = filename => {
  const content = fs.readFileSync(filename, 'utf-8')
  const ast = parser.parse(content, {
    sourceType: 'module'
  })
  console.log(ast.program.body)
}

moduleAnalyser('./src/index.js')
複製程式碼

列印出了兩個 Node 節點,第一個節點的 type 是 ImportDeclaration(引入的宣告),對照我們在 index.js 中寫的 import message from './message.js',第二個節點的 type 是 ExpressionStatement (表示式的宣告),對照我們寫的 console.log(message)

24 個例項入門並掌握「Webpack4」(三)

使用 babel 來幫我們生成抽象語法樹,我們再匯入 import message1 from './message1.js' 再執行

24 個例項入門並掌握「Webpack4」(三)

抽象語法樹將我們的 js 程式碼轉成了物件的形式,現在就可以遍歷抽象語法樹生成的節點物件中的 type,是否為 ImportDeclaration,就能找到程式碼中引入的依賴了

再借助一個工具 npm i @babel/traverse

const fs = require('fs')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default

const moduleAnalyser = filename => {
  const content = fs.readFileSync(filename, 'utf-8')
  const ast = parser.parse(content, {
    sourceType: 'module'
  })
  traverse(ast, {
    ImportDeclaration({ node }) {
      console.log(node)
    }
  })
}

moduleAnalyser('./src/index.js')
複製程式碼

24 個例項入門並掌握「Webpack4」(三)

只列印了兩個 ImportDeclaration,遍歷結束,我們只需要取到依賴的檔名,在列印的內容中,每個節點都有個 source 屬性,裡面有個 value 欄位,表示的就是檔案路徑及檔名

const fs = require('fs')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default

const moduleAnalyser = filename => {
  const content = fs.readFileSync(filename, 'utf-8')
  const ast = parser.parse(content, {
    sourceType: 'module'
  })
  const dependencise = []
  traverse(ast, {
    ImportDeclaration({ node }) {
      dependencise.push(node.source.value)
    }
  })
  console.log(dependencise)
}

moduleAnalyser('./src/index.js')
複製程式碼

儲存完重新執行,輸出結果:

['./message.js', './message1.js']
複製程式碼

這樣就對入口檔案的依賴分析就分析出來了,現在把 index.js 中引入的 message1.js 的依賴給刪除,這裡有個注意點,列印出來的檔案路徑是相對路徑,相對於 src/index.js 檔案,但是我們打包的時候不能是入口檔案(index.js)的相對路徑,而應該是根目錄的相對路徑(或者說是絕對路徑),藉助 node 的 api,引入一個 path

const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default

const moduleAnalyser = filename => {
  const content = fs.readFileSync(filename, 'utf-8')
  const ast = parser.parse(content, {
    sourceType: 'module'
  })
  const dependencise = []
  traverse(ast, {
    ImportDeclaration({ node }) {
      const dirname = path.dirname(filename)
      console.log(dirname)
      dependencise.push(node.source.value)
    }
  })
  // console.log(dependencise)
}

moduleAnalyser('./src/index.js')
複製程式碼

輸出為 ./src,繼續修改

ImportDeclaration({ node }) {
  const dirname = path.dirname(filename)
  const newFile = path.join(dirname, node.source.value)
  console.log(newFile)
  dependencise.push(node.source.value)
}
複製程式碼

輸出為 src\message.js

windows 和 類 Unix(linux/mac),路徑是有區別的。windows 是用反斜槓 \ 分割目錄或者檔案的,而在類 Unix 的系統中是用的 /

由於我是 windows 系統,所以這裡輸出為 src\message.js,而類 Unix 輸出的為 src/message.js

.\src\message.js 這個路徑是我們真正打包時要用到的路徑

newFile .\src\message.js
[ '.\\src\\message.js' ]
複製程式碼

既存一個相對路徑,又存一個絕對路徑

const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default

const moduleAnalyser = filename => {
  const content = fs.readFileSync(filename, 'utf-8')
  const ast = parser.parse(content, {
    sourceType: 'module'
  })
  const dependencise = {}
  traverse(ast, {
    ImportDeclaration({ node }) {
      const dirname = path.dirname(filename)
      const newFile = '.\\' + path.join(dirname, node.source.value)
      console.log('newFile', newFile)
      dependencise[node.source.value] = newFile
    }
  })
  console.log(dependencise)
  return {
    filename,
    dependencise
  }
}

moduleAnalyser('./src/index.js')
複製程式碼
newFile .\src\message.js
{ './message.js': '.\\src\\message.js' }
複製程式碼

因為我們寫的程式碼是 es6,瀏覽器無法識別,還是需要 babel 來做轉換

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

'use strict'

var _message = _interopRequireDefault(require('./message.js'))

function _interopRequireDefault(obj) {
  return obj && obj.__esModule ? obj : { default: obj }
}

console.log(_message.default)
複製程式碼
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')

const moduleAnalyser = filename => {
  const content = fs.readFileSync(filename, 'utf-8')
  const ast = parser.parse(content, {
    sourceType: 'module'
  })
  const dependencise = {}
  traverse(ast, {
    ImportDeclaration({ node }) {
      const dirname = path.dirname(filename)
      const newFile = '.\\' + path.join(dirname, node.source.value)
      dependencise[node.source.value] = newFile
    }
  })
  const { code } = babel.transformFromAst(ast, null, {
    presets: ['@babel/preset-env']
  })
  return {
    filename,
    dependencise,
    code
  }
}

const moduleInfo = moduleAnalyser('./src/index.js')
console.log(moduleInfo)
複製程式碼

分析的結果就在控制檯上列印了

{ filename: './src/index.js',
  dependencise: { './message.js': '.\\src\\message.js' },
  code:
   '"use strict";\n\nvar _message = _interopRequireDefault(require("./message.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nconsole.log(_message.default);' }
複製程式碼

目前我們只對一個模組進行分析,接下來要對整個專案進行分析,所以我們先分析了入口檔案,再分析入口檔案中所使用的依賴

依賴圖譜

建立一個函式來迴圈依賴並生成圖譜

// 依賴圖譜
const makeDependenciesGraph = entry => {
  const entryModule = moduleAnalyser(entry)
  const graphArray = [entryModule]
  for (let i = 0; i < graphArray.length; i++) {
    const item = graphArray[i]
    const { dependencise } = item
    // 如果入口檔案有依賴就去做迴圈依賴,對每一個依賴做分析
    if (dependencise) {
      for (const j in dependencise) {
        if (dependencise.hasOwnProperty(j)) {
          graphArray.push(moduleAnalyser(dependencise[j]))
        }
      }
    }
  }
  console.log('graphArray = ', graphArray)
}
複製程式碼

將入口的依賴,依賴中的依賴全部都分析完放到 graphArray 中,控制檯輸出的列印結果

24 個例項入門並掌握「Webpack4」(三)

可以看到 graphArray 中一共有三個物件,就是我們在專案中引入的三個檔案,全部被分析出來了,為了方便閱讀,我們建立一個 graph 物件,將分析的結果依次放入

// 依賴圖譜
const makeDependenciesGraph = entry => {
  const entryModule = moduleAnalyser(entry)
  const graphArray = [entryModule]
  for (let i = 0; i < graphArray.length; i++) {
    const item = graphArray[i]
    const { dependencise } = item
    // 如果入口檔案有依賴就去做迴圈依賴,對每一個依賴做分析
    if (dependencise) {
      for (const j in dependencise) {
        if (dependencise.hasOwnProperty(j)) {
          graphArray.push(moduleAnalyser(dependencise[j]))
        }
      }
    }
  }
  // console.log('graphArray = ', graphArray)

  // 建立一個物件,將分析後的結果放入
  const graph = {}
  graphArray.forEach(item => {
    graph[item.filename] = {
      dependencise: item.dependencise,
      code: item.code
    }
  })
  console.log('graph = ', graph)
  return graph
}
複製程式碼

輸出的 graph 為:

24 個例項入門並掌握「Webpack4」(三)

最後在 makeDependenciesGraph 函式中將 graph 返回,賦值給 graphInfo,輸出的結果和 graph 是一樣的

const graghInfo = makeDependenciesGraph('./src/index.js')
console.log(graghInfo)
複製程式碼

生成程式碼

現在已經拿到了所有程式碼生成的結果,現在我們藉助 DependenciesGraph(依賴圖譜) 來生成真正能在瀏覽器上執行的程式碼

最好放在一個大的閉包中來執行,避免汙染全域性環境

const generateCode = entry => {
  // makeDependenciesGraph 返回的是一個物件,需要轉換成字串
  const graph = JSON.stringify(makeDependenciesGraph(entry))
  return `
    (function (graph) {

    })(${graph})
  `
}

const code = generateCode('./src/index.js')
console.log(code)
複製程式碼

24 個例項入門並掌握「Webpack4」(三)

我這裡先把輸出的 graph 程式碼格式化了一下,可以發現在 index.js 用到了 require 方法,message.js 中不僅用了 require 方法,還用 exports 物件,但是在瀏覽器中,這些都是不存在的,如果我們直接去執行,是會報錯的

let graph = {
  './src/index.js': {
    dependencise: { './message.js': '.\\src\\message.js' },
    code: `
      "use strict";\n\n
       var _message = _interopRequireDefault(require("./message.js"));\n\n
       function _interopRequireDefault(obj){ return obj && obj.__esModule ? obj : { default: obj }; } \n\n
       console.log(_message.default);
      `
  },
  '.\\src\\message.js': {
    dependencise: { './word.js': '.\\src\\word.js' },
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.default = void 0;\n\nvar _word = require("./word.js");\n\nvar message = "say ".concat(_word.word);\nvar _default = message;\nexports.default = _default;'
  },
  '.\\src\\word.js': {
    dependencise: {},
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.word = void 0;\nvar word = \'hello\';\nexports.word = word;'
  }
}
複製程式碼

接下來要去構造 require 方法和 exports 物件

const generateCode = entry => {
  console.log(makeDependenciesGraph(entry))
  // makeDependenciesGraph 返回的是一個物件,需要轉換成字串
  const graph = JSON.stringify(makeDependenciesGraph(entry))
  return `
    (function (graph) {
      // 定義 require 方法
      function require(module) {

      };
      require('${entry}')
    })(${graph})
  `
}

const code = generateCode('./src/index.js')
console.log(code)
複製程式碼

graph 是依賴圖譜,拿到 entry 後去執行 ./src/index.js 中的 code,也就是下面高亮部分的程式碼,為了直觀我把前面輸出的 graph 程式碼拿下來參考:

let graph = {
  './src/index.js': {
    dependencise: { './message.js': '.\\src\\message.js' },
    code: `
      "use strict";\n\n
       var _message = _interopRequireDefault(require("./message.js"));\n\n
       function _interopRequireDefault(obj){ return obj && obj.__esModule ? obj : { default: obj }; } \n\n
       console.log(_message.default);
      `
  }
}
複製程式碼

為了讓 code 中的程式碼執行,這裡再使用一個閉包,讓每一個模組裡的程式碼放到閉包裡來執行,這樣模組的變數就不會影響到外部的變數

return `
    (function (graph) {
      // 定義 require 方法
      function require(module) {
        (function (code) {
          eval(code)
        })(graph[module].code)
      };
      require('${entry}')
    })(${graph})
  `
複製程式碼

閉包裡傳遞的是 graph[module].code,現在 entry 也就是 ./src/index.js 這個檔案,會傳給 require 中的 module 變數,實際上去找依賴圖譜中 ./src/index.js 對應的物件,然後再去找到 code 中對應的程式碼,也就是下面這段程式碼,被我格式化過,為了演示效果

'use strict'
var _message = _interopRequireDefault(require('./message.js'))
function _interopRequireDefault(obj) {
  return obj && obj.__esModule ? obj : { default: obj }
}
console.log(_message.default)
複製程式碼

但是我們會發現,這裡 _interopRequireDefault(require('./message.js')) 引入的是 ./message.js 相對路徑,等到第二次執行的時候,require(module) 這裡的 module 對應的就是 ./message.js

它會到 graph 中去找 ./message.js 下對應的 code,可是我們在 graph 中存的是 '.\\src\\message.js' 絕對路徑,這樣就會找不到物件

因為我們之前寫程式碼的時候引入的是相對路徑,現在我們要把相對路徑轉換成絕對路徑才能正確執行,定義一個 localRequire 方法,這樣當下次去找的時候就會走我們自己定義的 localRequire,其實就是一個相對路徑轉換的方法

return `
    (function (graph) {
      // 定義 require 方法
      function require(module) {
        // 相對路徑轉換
        function localRequire(relativePath) {
          return require(graph[module].dependencise[relativePath])
        }
        (function (require, code) {
          eval(code)
        })(localRequire, graph[module].code)
      };
      require('${entry}')
    })(${graph})
  `
複製程式碼

我們定義了 localRequire 方法,並把它傳遞到閉包裡,當執行了 eval(code) 時執行了 require 方法,就不是執行外部的 require(module) 這個方法,而是執行我們傳遞進去的 localRequire 方法

我們在分析出的程式碼中是這樣引入 message.js

var _message = _interopRequireDefault(require('./message.js'))

這裡呼叫了 require('./message.js'),就是我們上面寫的 require 方法,也就是 localRequire(relativePath)

所以 relativePath 就是 './message.js'

這個方法返回的是 require(graph[module].dependencise[relativePath])

這裡我把引數帶進去,就是這樣:

graph('./src/index.js').dependencise['./message.js']

let graph = {
  './src/index.js': {
    dependencise: { './message.js': '.\\src\\message.js' },
    code: `
      "use strict";\n\n
       var _message = _interopRequireDefault(require("./message.js"));\n\n
       function _interopRequireDefault(obj){ return obj && obj.__esModule ? obj : { default: obj }; } \n\n
       console.log(_message.default);
      `
  }
}
複製程式碼

對照著圖譜就能發現最終返回的就是 '.\\src\\message.js' 絕對路徑,返回絕對路徑後,我們再呼叫 require(graph('./src/index.js').dependencise['./message.js']) 就是執行外部定義的 require(module) 這個方法,重新遞迴的去執行,光這樣還不夠,這只是實現了 require 方法,還差 exports 物件,所以我們再定義一個 exports 物件

return `
    (function (graph) {
      // 定義 require 方法
      function require(module) {
        // 相對路徑轉換
        function localRequire(relativePath) {
          return require(graph[module].dependencise[relativePath])
        }
        var exports = {};
        (function (require, exports, code) {
          eval(code)
        })(localRequire, exports, graph[module].code)
        return exports
      };
      require('${entry}')
    })(${graph})
  `
複製程式碼

最後要記得 return exports 將 exports 匯出,這樣下一個模組在引入這個模組的時候才能拿到匯出的結果,現在程式碼生成的流程就寫完了,最終返回的是一個大的字串,儲存再次執行 node bundle.js | highlight

24 個例項入門並掌握「Webpack4」(三)

這裡我是 windows 環境,將輸出完的程式碼直接放到瀏覽器裡不行,我就把壓縮的程式碼格式化成下面這種樣子,再放到瀏覽器裡就能輸出成功了

;(function(graph) {
  function require(module) {
    function localRequire(relativePath) {
      return require(graph[module].dependencise[relativePath])
    }
    var exports = {}
    ;(function(require, exports, code) {
      eval(code)
    })(localRequire, exports, graph[module].code)
    return exports
  }
  require('./src/index.js')
})({
  './src/index.js': {
    dependencise: { './message.js': '.\\src\\message.js' },
    code:
      '"use strict";\n\nvar _message = _interopRequireDefault(require("./message.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nconsole.log(_message.default);'
  },
  '.\\src\\message.js': {
    dependencise: { './word.js': '.\\src\\word.js' },
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.default = void 0;\n\nvar _word = require("./word.js");\n\nvar message = "say ".concat(_word.word);\nvar _default = message;\nexports.default = _default;'
  },
  '.\\src\\word.js': {
    dependencise: {},
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.word = void 0;\nvar word = \'hello\';\nexports.word = word;'
  }
})
複製程式碼

將上面程式碼放入瀏覽器的控制檯中,回車就能輸出 say hello

24 個例項入門並掌握「Webpack4」(三)

總結

這就是打包工具打包後的內容,期間涉及了 node 知識,使用 babel 來轉譯 ast(抽象語法樹),最後的 generateCode 函式涉及到了遞迴閉包形參實參,需要大家多看幾遍,加深理解

To Be Continued

個人部落格

24 個例項入門並掌握「Webpack4」(一)

24 個例項入門並掌握「Webpack4」(二)

相關文章