iT邦幫忙

2023 iThome 鐵人賽

DAY 25
0
Vue.js

Nuxt 3 實戰筆記系列 第 25

[Day 25] Nuxt 3 深入靜態資源的使用 Public & Assets

  • 分享至 

  • xImage
  •  

前言

不管在 Vue 與 Nuxt 的開發,常會有需要使用圖片或靜態資源的時刻,例如,已經具有外部的圖片連結,我們很輕易的可以使用 URL 等方式來做載入,若是要由自身服務提供這個圖片或其他靜態資源及使用,也就是在 SFC 內我們需要載入自身專案下靜態資源檔案,最後連帶這些檔案一起部署,那我們需要注意幾個細節。

Public & Assets 目錄

Nuxt 3 使用專案下的兩個目錄來處理專案內的圖片等靜態資源的載入,這兩個目錄分別為 public 與 assets,實際上這兩個目錄也有不同的使用機制與差異。 public 與 assets 目錄下的檔案,最大的差異就是編譯assets 目錄下檔案會經過編譯與最佳化處理。

Public & Assets 目錄的特性與差異

特性 Public 目錄 Assets 目錄
使用方式 網址 需要載入
最佳化 ❌ 否 ✅ 是
透過網址訪問 ✅ 根目錄下 🚧 會添加 Hash
適合檔案性質 不常變動 經常變動

使用 public 目錄下的檔案

首先我們準備一張 bg.jpg 的圖片檔案,並放置在專案目錄的 public 目錄下。

nuxt-app/
├── ...
└── public/
    ├── bg.jpg
    └── favicon.ico

接著我們在頁面或元件中使用 img 標籤來呈現這張圖片。

<template>
  <div>
    <img src="/bg.jpg" />
  </div>
</template>

當我們瀏覽網頁時,可以發現渲染出來的 HTML,<img> 標籤內的 src 屬性值,與我們在 SFC 中所使用的一模一樣,因為在 public 目錄下的檔案,最終會在網站的根路徑進行提供,所以我們可以直接做訪問。

<div>
  <img src="/bg.jpg" />
</div>

使用 assets 目錄下的檔案

首先我們準備一張 cat.jpg 的圖片檔案,並放置在專案目錄的 assets 目錄下。

nuxt-app/
├── ...
└── assets/
    └── cat.jpg

接著我們在頁面或元件中使用 img 標籤來呈現這張圖片,這邊要特別注意的是,我們在 src 屬性內,需要使用 Nuxt 3 所提供的路徑別名來指示 assets 的路徑,因為這會經過一些處理才會變成最終可訪問的路徑。

<template>
  <div>
    <img src="~/assets/cat.jpg" />
  </div>
</template>

首先,我們使用開發環境啟動測試伺服器的網站服務,當我們瀏覽網頁時,可以觀察一下渲染出來的 HTML,<img> 標籤內的 src 屬性值,SFC 所傳入 ~/assets/cat.jpg 路徑,被替換成了 /_nuxt/assets/cat.jpg

<div>
  <img src="/_nuxt/assets/cat.jpg" />
</div>

看到這裡,你可能會想,看起來挺有跡可循的,不就是加上前綴路徑 /_nuxt 在串接 assets 目錄,就可以訪問了嗎?

不,事情並沒有這麼簡單,因為我們所處的是開發測試的環境,接下來我們嘗試編譯打包專案,執行下列指令。

npm run build

如果順利編譯打包完成,應該會在專案目錄下出現一個 .output 的目錄,終端機也會提示打包完成,接著我們執行下列指令,來啟動網站服務進行預覽,這個預覽的網站就是 .output 的目錄內打包完成的網站,也是作為正式生產環境所使用的最終編譯完成的程式。預覽服務啟動後終端機會提示監聽的網址與連接阜,預設情況應該可以使用 http://localhost:3000/ 來進行預覽。

npm run preview

同樣的我們在預覽的網站下,瀏覽相同頁面所呈現的圖片,並觀察所渲染的 HTML,其中檔案名稱 cat.b23fd496.jpg 中所包含的 b23fd496 字串,可能會有所不同。

<div>
  <img src="/_nuxt/cat.b2373496.jpg" />
</div>

我們可以透過 http://localhost:3000/_nuxt/cat.b2373496.jpg 網址來瀏覽這張圖片,你能發現原本開發環境下所渲染的 /_nuxt/assets/cat.jpg/_nuxt/cat.b2373496.jpg 怎麼完全不同了。

首先開發環境為了方便測試並沒有實際經過打包的過程,而要產生正式環境所需要的部署資料,我們使用 build 指令進行了專案的編譯打包,這個過程會將有使用到 assets 目錄下的檔案進行編譯與最佳化,最終也會產生一個新的檔案並重新命名成一個包含 Hash 雜湊字串的檔案, cat.b2373496.jpg 檔案名稱的 b2373496 正是產生的雜湊字串,而且在每次打包編譯時都會重新產生一組 Hash 值賦予每一個不同的靜態資源,加上 Hash 雜湊字串主要可以是為了解決瀏覽器可能快取著上一個網站打包版本的靜態資源或 Vue 程式碼檔案。

所以,在使用 assets 下的靜態資源檔案時,無法直接透過根路徑來進行訪問,因為你無法預估最終打包出來的雜湊值為何,你需要使用路徑的別名,或按照你所需要的檔案在頁面或元件中做載入 (Import) 才能在 template 中做使用,最終打包工具才會正確的轉換為最佳化完成的新檔案名稱與對應的路徑。

舉例來說,同一個 bg.png 的圖片檔案,放置在 public 與 assets 目錄下並進行打包編譯,最終產生出來的 .output 資料夾,可以看到如下圖的資源路徑與檔案名稱,訪問的方式也會有所不同。

https://ithelp.ithome.com.tw/upload/images/20231010/20152617cMF1An0jZE.png

動態使用不同的靜態資源

有些情況我們可能需要動態的來載入與使用這些靜態資源,例如在頁面中呈現輪播的圖片或依據響應式狀態來動態調整呈現的圖片。

動態使用 public 目錄下的檔案

因為 public 目錄下檔案的特性,我們可以直接透過網站的根路徑進行訪問 ,所以我們可以在頁面元件中,使用字串模板來串接這個根路徑網址來載入不同的檔案,例如下面程式碼。

<template>
  <div>
    <img :src="`/bg-${bgIndex}.jpg`" />
  </div>
</template>

<script setup>
const bgIndex = ref(1)
</script>

嘗試動態使用 assets 目錄下的檔案

public 目錄不同,我們想要做到使用字串模板來組合 assets 目錄下的檔案路徑,如下程式碼。

<template>
  <div>
    <img :src="`~/assets/${assetName}.jpg`" />
  </div>
</template>

<script setup>
const assetName = ref('cat')
</script>

你會發現 assets 目錄下的圖片檔案,無法正確的在頁面中呈現,所渲染的 HTML 甚至不會進行自動轉換,變成很單純的字串模板的值,自然也就正確無法訪問到理應為 /_nuxt 路徑下的檔案資源。

<div>
  <img src="~/assets/cat.jpg" />
</div>

你可能會覺得,就算開發環境與正式環境,實際上路徑的圖片,不就是都在 /_nuxt 路徑之下嗎?那不就只要透過這個網址來組合字串就行了嗎?終於你完成了下面的程式碼來準備測試。

<template>
  <div>
    <img :src="`/_nuxt/assets/${assetName}.jpg`" />
  </div>
</template>

<script setup>
const assetName = ref('cat')
</script>

你會發現在開發環境的測試伺服器,很順利的圖片出現了!字串模板 /_nuxt/assets/${assetName}.jpg 順利的串接成 /_nuxt/assets/cat.jpg,而開發環境也確實可以透過 /_nuxt/assets/cat.jpg 來瀏覽圖片。但不要忘記了!!!,我們還有正式環境。

經過打包過後啟動預覽網站,同樣的頁面,同樣的字串模板,產生同樣的連結 /_nuxt/assets/cat.jpg,但是,這次圖片沒有正確的出現,而且這個網址出現了 404 Not Found 的錯誤。

這是因為打包完成後,就算 cat.jpg 圖片檔案順利參與編譯打包,實際上可能是需要透過 /_nuxt/cat.d5278ddf.jpg 來訪問,除了 /_nuxt 沒有了 /assets 層級,檔案名稱也多了 d5278ddf Hash 雜湊字串,使得圖片完全無法預估正確的檔案名稱,字串模板自然也就沒有效果。

如果你想要透過動態的方式來載入靜態資源,你可能需要使用如下程式碼的方式來明確的載入 (Import) 檔案資源後,並將物件放置在 src 屬性,如此一來打包工具才能正確的轉換為正確的資源訪問路徑。

<template>
  <div>
    <img :src="assetName === 'cat' ? catImage : defaultImage" />
  </div>
</template>

<script setup>
import catImage from 'assets/cat.jpg'
import defaultImage from 'assets/default.jpg'

const assetName = ref('cat')
</script>

路徑別名

Nuxt 3 提供了可以設定專案目錄檔案的路徑的別名,預設情況下 Nuxt Config 的 alias 選項作為路徑的別名設定,其預設的設定所產生的別名對應如下 (<rootDir> 表示專案跟目錄):

{
  "~": "/<rootDir>",
  "@": "/<rootDir>",
  "~~": "/<rootDir>",
  "@@": "/<rootDir>",
  "assets": "/<rootDir>/assets",
  "public": "/<rootDir>/public"
}

透過路徑的別名,我們在使用靜態資源檔案時,就可以使用下列方式來在頁面或元件中的 src 屬性載入資源,這個方式也適用於 CSS 內有使用到靜態資源連結時的路徑別名的使用,~/~~/@/@@/ 路徑別名皆為對應 /<rootDir>,下面程式碼也就是在專案根目錄下的 assets 與 public 目錄來指定檔案路徑。

<template>
  <div>
    <img src="~/assets/cat.jpg" />
    <img src="~~/assets/cat.jpg" />
    
    <img src="@/assets/cat.jpg" />
    <img src="@@/assets/cat.jpg" />

    <img src="~/public/bg.jpg" />
    <img src="~~/public/bg.jpg" />
    
    <img src="@/public/bg.jpg" />
    <img src="@@/public/bg.jpg" />
  </div>
</template>

如果要在圖片的 src 屬性或 CSS 內使用 "assets": "/<rootDir>/assets""public": "/<rootDir>/public" 這兩個路徑別名,必須在別名前面添加波浪符 ~

<template>
  <div>
    <img src="~assets/cat.jpg" />

    <img src="~public/bg.jpg" />
  </div>
</template>

如果在 SFC 的 setup 或 JavaScript 中,你就能使用路徑別名中預設的 assets 與 public 來搭配 import 來載入資源。

<script setup>
import catImage1 from '~/assets/cat-1.jpg'
import catImage2 from '~~/assets/cat-2.jpg'
import catImage3 from '@/assets/cat-3.jpg'
import catImage4 from '@@/assets/cat-4.jpg'
import catImage5 from 'assets/cat-5.jpg' // assets 開頭的路徑別名,只有在 JavaScript 中可以做使用而且不需要再開頭加波浪符 ~

import bgImage1 from '~/public/bg-1.jpg'
import bgImage2 from '~~/public/bg-2.jpg'
import bgImage3 from '@/public/bg-3.jpg'
import bgImage4 from '@@/public/bg-4.jpg'
import bgImage5 from 'public/bg-5.jpg' // public 開頭的路徑別名,只有在 JavaScript 中可以做使用而且不需要再開頭加波浪符 ~
</script>

如果你想要自訂其他路由別名,可以調整 Nuxt Config 中的 dir 選項,並參考如下設定。

export default defineNuxtConfig({
  alias: {
    images: fileURLToPath(new URL('./assets/images', import.meta.url)),
    styles: fileURLToPath(new URL('./assets/styles', import.meta.url)),
    fonts: fileURLToPath(new URL('./assets/fonts', import.meta.url))
  }
})

如此一來當建立了 ./assets/images/cat.jpg 檔案。

nuxt-app/
├── ...
└── assets/
    └── cat.jpg

你就能在頁面或元件中使用 images 路徑別名來載入 images 目錄下的資源做使用。

<template>
  <div>
    <img src="~images/cat.jpg" />
    <img :src="catImage" />
  </div>
</template>

<script setup>
import catImage from 'images/cat.jpg'
</script>

require() & Vite

如果你有曾經使用過 Vue + Webpack 來做開發,你可能知道可以使用 require 來在 HTML 標籤中的 src 屬性中做使用,例如下列程式碼:

<template>
  <div>
    <img :src="require('./assets/cat.jpg')">
  </div>
</template>

但是這個 require() 方式在 Vue + Vite 中是無法做使用的,因為會引發 require is not defined 的錯誤。

當使用 Vite 作為開發伺服器與建構打包的工具時,因為 Vite 是一個基於 ES 模組的構建工具,主要是為了提供更快速的開發體驗,也正是因為 Vite 是專為 ES 模組設計的,它有一些與傳統的 CommonJS 有關的限制和差異。

所以在使用 Webpack 建構打包 Vue 專案時,可以而 CommonJS 的 require() 來載入靜態資源,但 Vite 是為 ESM (ECMAScript Module) 設計的,ESM 是現代 JavaScript 模組系統的規範,而 Vite 的很多特性都是建立在 ESM 的基礎上的,也不支援使用 require() 來載入資源。

綜合上述的情況,Vite 推薦載入靜態資源的方法,應該為使用 import 來做載入

import catImageUrl from 'assets/cat.jpg'

最終我們在使用時,將 catImageUrl 傳入 src 屬性中

<template>
  <div>
    <img :src="catImageUrl" />
  </div>
</template>

<script setup>
import catImageUrl from 'assets/cat.jpg'
</script>

動態使用 assets 目錄下的檔案

在 Nuxt 或 Vue 中,我們無法使用很優雅的方式來動態的載入這些 assets 目錄下的檔案,但仍有一些小技巧,可以達到類似的效果,雖然存在著一些弊端,但也總比不能使用的好。

首先我們在專案下建立一個組合式函式 useAssets(),檔案的路徑為 composables/useAssets.js,實作如下程式碼:

export default function () {
  const images = computed(() =>
    import.meta.glob('~/assets/images/**/*', { eager: true, import: 'default' })
  )
  const getImageUrl = (filename) => {
    const url = `/assets/images/${String(filename).replace(/^(\.\/+|\/+)/, '')}`
    return images.value?.[url]
  }

  return {
    getImageUrl
  }
}

可以發現 useAssets.js 的實作中,我們使用 import.meta.glob 來一次性載入 ~/assets/images 目錄下的所有檔案,並且會獲得一個物件,用作於檔案的對應表,例如建構打包後我們可以使用 /assets/images/cat.jpg 取得對應的 /_nuxt/cat.60063258.jpg 網址路徑。

我們建立一個 getImageUrl 函式,提供一個檔案名稱,可以來對應產生包含 Hash 雜湊字串的檔案訪問路徑,並作為 useAssets() 組合式函式回傳的物件之一。

由於我們使用 import.meta.glob 僅包含使用了 ~/assets/images 目錄與子目錄下的檔案,所以我們將圖片檔案 cat.jpgdog.jpg 放置在這個目錄之下。

nuxt-app/
├── ...
└── assets/
    ├── cat.jpg
    └── dog.jpg

接著我們就可以在頁面元件中,呼叫 useAssets() 組合式函式從回傳物件中來解構出 getImageUrl() 函式,透過 getImageUrl 傳入圖片檔案路徑,就可以實現動態載入圖片的效果。

<template>
  <div>
    <img :src="getImageUrl(`${assetName}.jpg`)" />

    <img :src="getImageUrl('dog.jpg')" />
  </div>
</template>
<script setup>
const { getImageUrl } = useAssets()
const assetName = ref('cat')
</script>

雖然透過這種方式可以實現動態的載入靜態資源,但是當我們使用 import.meta.glob 來一次性載入檔案,可能會將一些網站上其實沒使用到的圖片也一併打包,雖然多數情況有使用到的圖片或靜態資源檔案才會做放置,但是難免會有一些漏網之魚或是測試用的資源,這些檔案就算最終沒有實際在頁面元件中使用,也會因為萬用字元 * 的匹配而一併被打包,可能導致包含了一些非必要的檔案。

甚至在路徑的使用上,如果將 ~/assets/images/**/* 調整成 ~/assets/**/*,雖然可以使整個 assets 目錄被載入,同時也可能導致打包了非必要檔案,所以建議還是在 assets 目錄下建立子目錄來分類管理靜態資源。

小結

目前動態載入 assets 目錄下的檔案,除了根本問題外,至今也仍沒有一個有效的解決辦法,本文提供的組合式函式也是透過這個討論串的靈感修改而來,因為這個問題的核心在於 Vite 的靜態分析上,而在動態的載入永遠沒辦法知道到底會傳入什麼樣子的字串來請求載入資源,我們能做到的就只有將所有可能使用到的資源一併打包,並提供一個組合式函式來產生對應表提供使用,算是一種折衷辦法。


感謝大家的閱讀,歡迎大家給予建議與討論,也請各位大大鞭小力一些:)
如果對這個 Nuxt 3 系列感興趣,可以訂閱接收通知,也歡迎分享給喜歡或正在學習 Nuxt 3 的夥伴。

參考資料


上一篇
[Day 24] Nuxt 3 多國語系模組 Nuxt I18n 的進階使用方法與 SEO 搜尋引擎最佳化
下一篇
[Day 26] Nuxt 3 錯誤頁面 (Error Page) 與錯誤處理 (Error handling)
系列文
Nuxt 3 實戰筆記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言