實例帶你入門並掌握Webpack4(實戰篇三)

上篇 實例帶你入門並掌握Webpack4(實戰篇一)html

上篇 實例帶你入門並掌握Webpack4(實戰篇二)vue

原做者:Zshnode

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

17.PWA 配置

demo17 源碼地址react

本節使用 demo15 的代碼爲基礎jquery

咱們來模擬平時開發中,將打包完的代碼防止到服務器上的操做,首先打包代碼 npm run buildlinux

而後安裝一個插件 npm i http-server -Dwebpack

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

{
  "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 startgit

如今就起了一個服務,端口是 8080,如今訪問 http://127.0.0.1:8080 就能看到效果了es6

若是你有在跑別的項目,端口也是 8080,端口就衝突,記得先關閉其餘項目的 8080 端口,再 npm run start

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

頁面訪問不到了,由於咱們服務器掛了,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 能夠理解爲另類的緩存

還須要去業務代碼中使用 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 ,打開控制檯

如今文件已經被緩存住了,再按 ctrl + c 關閉服務,再次刷新頁面也仍是能顯示的

18.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 文件

當打包 typescript 文件的時候,須要在項目的根目錄下建立一個 tsconfig.json 文件

如下爲簡單配置,更多詳情看官網

{
  "compileerOptions": {
    "outDir": "./dist", // 寫不寫都行
    "module": "es6", // 用 es6 模塊引入 import
    "target": "es5", // 打包成 es5
    "allowJs": true // 容許在 ts 中也能引入 js 的文件
  }
}
複製代碼
複製代碼

再次打包,打開 bundle.js 文件,將代碼所有拷貝到瀏覽器控制檯上,使用這段代碼,能夠看到彈窗出現 Hello,world,說明 ts 編譯打包成功

引入第三方庫


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
複製代碼
複製代碼

安裝完之後能夠發現下劃線 _ 報錯了

須要改爲 import * as _ from 'lodash',將 join 方法傳遞的參數刪除,還能夠發現 join 方法的報錯,這就體現了 typescript 的優點,同理,引入 jQuery 也要引入一個 jQuery 對應的類型插件

如何知道使用的庫須要安裝對應的類型插件呢?

打開TypeSearch,在這裏對應的去搜索你想用的庫有沒有類型插件,若是有隻須要 npm i @types/jquery -D 便可

19.Eslint 配置

demo19 源碼地址

建立一個空文件夾,npm init -ynpm webpack webpack-cli -D 起手式,以後安裝 eslint 依賴

npm i eslint -D
複製代碼
複製代碼

使用 npx 運行此項目中的 eslint 來初始化配置,npx eslint --init

這裏會有選擇是 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 配置規則

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

eslint 報錯提示,變量定義後卻沒有使用,若是在編輯器裏沒出現報錯提示,須要在 vscode 裏先安裝一個 eslint 擴展,它會根據你當前目錄的下的 .eslintrc.js 文件來作做爲校驗的規則

也能夠經過命令行的形式,讓 eslint 校驗整個 src 目錄下的文件

若是你以爲某個規則很麻煩,想屏蔽掉某個規則的時候,能夠這樣,根據 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

會在打包的時候,提示代碼不合格,不只僅是生產環境,開發環境也能夠配置,能夠將 eslint-loader 配置到 webpack 的公共模塊中,這樣更有利於咱們檢查代碼規範

如:設置 fix 爲 true,它會幫你自動修復一些錯誤,不能自動修復的,仍是須要你本身手動修復

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

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

20.使用 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

打開文件能夠發現 lodash 已經被打包到了 dll 文件中

那咱們要如何使用這個 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 來打開網頁

如今咱們已經把第三方模塊單獨打包成了 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

修改 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

能夠把以前打包的 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,看看能不能成功運行

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

這裏使用 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 打包看看輸出的內容,能夠看到文件夾中的內容以數組的形式被打印出來了,以後咱們對這個數組作一些循環操做就好了

)

完整代碼:

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 屬性中就能夠了

21.多頁面打包配置

demo21 源碼地址

本節使用 demo20 的代碼爲基礎

在 src 目錄下新建 list.js 文件,裏面寫 console.log('這裏是 list 頁面')

在 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

打開 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 頁面

22.編寫 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'
  }
}
複製代碼
複製代碼

目錄結構:

打包後打開 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)
}
複製代碼
複製代碼

打包後生成的文件也改成了 options 中定義的 name

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

若是你的 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)
}
複製代碼
複製代碼

報錯 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'
  }
}
複製代碼
複製代碼

23.編寫 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'
  }
}
複製代碼
複製代碼

保存後打包,插件被使用了,只不過咱們什麼都沒幹

若是咱們要傳遞參數,能夠這樣

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

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

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

module.exports = CopyrightWebpackPlugin
複製代碼
複製代碼

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

apply(compiler) {} compiler 能夠看做是 webpack 的實例,具體見官網 compiler-hooks

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

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

module.exports = CopyrightWebpackPlugin
複製代碼
複製代碼

由於 emit異步的,能夠經過 tapAsync 來寫,當要把代碼放入到 dist 目錄以前,就會觸發這個鉤子,走到咱們定義的函數裏,若是你用 tapAsync 函數,記得最後要用 cb() ,tapAsync 要傳遞兩個參數,第一個參數傳遞咱們定義的插件名稱

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

compilation 這個參數裏存放了此次打包的全部內容,能夠輸出一下 compilation.assets 看一下

返回結果是一個對象,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
複製代碼
複製代碼

在 dist 目錄下生成了 copyright.txt 文件

以前介紹的是異步鉤子,如今使用同步鉤子

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
複製代碼
複製代碼

24.編寫 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

index.js 中的代碼已經被輸出到控制檯上,並且代碼有高亮,方便閱讀,讀取入口文件信息就完成了

如今咱們要讀取 index.js 文件中使用的 message.js 依賴,import message from './message.js'

安裝一個第三方插件 npm i @babel/parser

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

官網也提供了相應的示例代碼,根據示例代碼來仿照,修改咱們的文件

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'

保存後運行,輸出了 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)

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

抽象語法樹將咱們的 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')
複製代碼
複製代碼

只打印了兩個 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 中,控制檯輸出的打印結果

能夠看到 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 爲:

最後在 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)
複製代碼
複製代碼

我這裏先把輸出的 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

這裏我是 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

總結

--

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

相關文章
相關標籤/搜索