webpack 小技巧:動態批量加載文件

背景

最近筆者在工做中遇到了一個小需求:vue

要實現一個組件來播放幀圖片

這個需求自己不復雜,可是須要在組件中一次性引入十張圖片,就像下面這樣:webpack

// 就是這麼任性,下標從0開始~
import frame0 from './assets/frame_0.png'
import frame1 from './assets/frame_1.png'
import frame2 from './assets/frame_2.png'
// ..省略n張
import frame7 from './assets/frame_8.png'
import frame8 from './assets/frame_9.png'
import frame9 from './assets/frame_10.png'

做爲一個有代碼潔癖的程序員,我是不容許這種重複性代碼存在滴,因而乎就嘗試有沒有什麼簡單的方法。程序員

方法一:繞過 webpack

因爲筆者用的是 vue-cli 3,熟悉的小夥伴都知道,將圖片以固定的格式放在 public 文件夾下面,而後在代碼中直接以絕對路徑引入便可。這麼作的話,就能夠根據文件名構造一個 url 數組,簡單代碼以下:web

const frames = []
_.times(10, v => {
    frames.push(`/images/frame_${v}.png`)
})
// 而後你就獲得 10個 url 的數組啦

此方法自己是 vue-cli 提供的一個 應急手段,它有幾個缺點:vue-cli

  1. 沒法利用 webpack 處理資源,沒法產生內容哈希,不利於緩存更新
  2. 沒法利用 url-loader 將資源內聯成 base64 字符串 以減小網絡請求

方法二:require

因爲 import 是靜態關鍵字,因此若是想要批量加載文件,可使用 require,可是直接像下面這樣寫是不行的:api

const frames = []
_.times(10, v => {
    const path = `./assets/images/frame_${v}.png`
    frames.push(require(path))
}

上面的代碼中的 path 是在程序運行時才能肯定的,即屬於 runtime 階段,而 webpack 中的 require 是在構建階段肯定文件位置的,因此 webpack 無法推測出這個 path 在哪裏。數組

可是卻能夠這樣寫:緩存

const frames = []
_.times(10, v => {
    frames.push(require(`./assets/images/frame_${v}.png`))
}
// frames 中就獲得 帶 hash 值的路徑

雖然這兩種寫法在語法上沒有差異,可是第二種寫法在構建時提示了 webpack,webpack 會將 ./assets/images 中的全部文件都加入到 bundle 中,從而在你運行時能夠找到對應的文件。網絡

在使用方法二的時候筆者嘗試將批量加載的邏輯提取到其餘模塊用來複用:函數

export function loadAll (n, prefix, suffix) {
  const frames = []
  _.times(n, v => {
    frames.push(require('./' + prefix + v + suffix))
  })
  return frames
}

可是顯然失敗了,由於提取後的代碼,運行的 context 屬於另外一個模塊,因此也就沒法找到相對路徑中的文件。

方法三:require.context

上面兩種方法都不算很優雅,因而就去翻 webpack 的文檔,終於,讓我找到了這麼一個方法:require.context

require.context(
  directory: String,
  includeSubdirs: Boolean /* 可選的,默認值是 true */,
  filter: RegExp /* 可選的,默認值是 /^\.\/.*$/,全部文件 */,
  mode: String  /* 可選的,'sync' | 'eager' | 'weak' | 'lazy' | 'lazy-once',默認值是 'sync' */
)
指定一系列完整的依賴關係,經過一個 directory 路徑、一個 includeSubdirs 選項、一個 filter 更細粒度的控制模塊引入和一個 mode 定義加載方式。而後能夠很容易地解析模塊.

咱們仍是看上面的例子:

const frames = []
const context = require.context('./assets/images', false, /frame_\d+.png/)
context.keys().forEach(k => {
    frames.push(context(k))
})

這裏的代碼經過 require.context 建立了一個 require 上下文。

  • 第一個參數指定了須要加載的文件夾,即組件當前目錄下的 ./assets/images 文件夾
  • 第二個參數指定是否須要包含子目錄,因爲沒有子目錄,因此傳 false
  • 第三個參數指定須要包含的文件的匹配規則,咱們用一個正則表示

而後使用 context.keys() 就能拿到該上下文的文件路徑列表,而 context 自己也是一個方法,至關於設置過上下文的 require,咱們將 require 後的文件放入數組中,數組中的路徑實際上是帶 hash 值的,以下是我項目中的圖片:

["/static/img/frame_0.965ef86f.png", "/static/img/frame_1.c7465967.png", "/static/img/frame_2.41e82904.png", "/static/img/frame_3.faef7de9.png", "/static/img/frame_4.27ebbe45.png", "/static/img/frame_5.d98cbebe.png", "/static/img/frame_6.c10859bc.png", "/static/img/frame_7.5e9cbdf0.png", "/static/img/frame_8.b3b92c71.png", "/static/img/frame_9.36660295.png"]

並且若是設置過內聯圖片的話,數組中可能還有圖片的 base64 串。

重構一下

方法三已經解決了咱們的問題,並且能夠批量 require 某個文件夾中的文件。可是 forEach 那塊的邏輯明顯是重複的,因此咱們固然提取出來啦,之後多個組件調用的時候只須要引入便可:

公共模塊:

/**
 * 批量加載幀圖片
 * @param {Function} context - require.context 建立的函數
 * @returns {Array<string>} 返回的全部圖片
 */
function loadFrames (context) {
  const frames = []
  context.keys().forEach(k => {
    frames.push(context(k))
  })
  return frames
}

組件中:

const context = require.context('./assets/images', false, /frame_\d+.png/)
const frames = loadFrames(context)

大功告成!感興趣的小夥伴能夠點擊文末連接查看詳細文檔~

參考連接


本文首發於公衆號:碼力全開(codingonfire)

本文隨意轉載哈,註明原文連接便可,公號文章轉載聯繫我開白名單就好~

codingonfire.jpg

相關文章
相關標籤/搜索