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

Zsh發表於2019-04-12

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

十七、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」(二)

相關文章