為什麼要用 SVG
SVG 是一種圖形格式,是可縮放向量圖形 (Scalable Vector Graphics) 的縮寫。與我們一般常見的點陣圖形(像 JPEG 或 PNG)不同,最大的特色就是不會因為圖片尺寸過小或放大倍數過高而失真,在網站中非常適合用於標誌和圖形(Icon)。
向量圖形與點陣圖型在組成上有根本的差異。點陣圖形以像素為基本,是「方塊格」組成的矩形,如果將點陣圖放大,就會看到顆粒狀和像素化的方格。如果點陣圖縮得太小,則會變得模糊不清。相對的,向量圖形檔案則是根據點、線、形狀和顏色所構成的複雜數學算式組成。可以把它理解成就像是用程式「畫」出來的圖案。
網站設計師常會在介面上添加標示特定功能的圖形標誌,就是我們熟知的 Icon,例如登入註冊會員、社群媒體等功能按鈕常以 Icon 代替。因為 Icon 大多是尺寸較小的圖像矩形,放上網站為避免失真大多會轉存成 SVG 檔案交給前端工程師。
當網站規模大了 Icon 數量變多,用到數十個甚至上百個 SVG 都有可能,而這些很可能只是 hover 時需要顏色變化就多出一個檔案。如果是一般 PNG 檔案那確實需要,但 SVG 本質上就是程式碼組成的,可以透過參數變化,在需要的地方客製化顏色、尺寸大大提高開發彈性。
在專案中使用 SVGR
@svgr/webpack⎘ 是一個 JavaScript 的套件,可以將 SVG 圖像轉換成 React component。這個套件可以幫助開發者在 React 中方便地使用 SVG 圖像,並且可以輕鬆地進行樣式和事件的設定。像是將 SVG 圖像作為 React 組件導入,並使用 props 來進行修改。
如果是使用 Create React App 指令產生的專案,它已經預先安裝了 @svgr/webpack 套件,因此不需要額外安裝。可以直接在組件中導入 SVG ,就像這樣使用:
import { ReactComponent as logo } from "./logo.svg";
但是在 Next.js 中我們需要手動進行安裝及配置:
- 在控制台中運行以下命令以安裝 @svgr/webpack:
npm install --save-dev @svgr/webpack
- 再來配置新的 Webpack 規則,用於處理 SVG,在
next.config.js
文件中添加以下設定:
webpack(config) { config.module.rules.push({ // .svg 結尾的檔案才進行處理 test: /\.svg$/i, // 限制 jsx, tsx 的檔案才能引入 SVG issuer: /\.[jt]sx?$/, use: ['@svgr/webpack'], }) return config },
- 現在可以在 TypeScript 中使用和導入 SVG 文件,就像這樣:
import { Icon } from '../public/icons/x.svg'; const Home: NextPage = () => { return ( <main> <h1 className="text-3xl font-bold underline">Hello world!</h1> <Icon /> </main> ); }; export default Home;
但是接下來會馬上遇到這個問題,怎麼回事?
Cannot find module '../public/icons/x.svg' or its corresponding type declarations.ts(2307)
這是因為 TypeScript 無法識別 SVG 文件的類型,具體原因是在 Next.js 11 以後導入了自己的 image type⎘,把 .svg
的檔案設定成了 any
,可以在 /node_modules/next/image-types/global.d.ts
看到這些設定。
// ... declare module '*.svg' { /** * Use `any` to avoid conflicts with * `@svgr/webpack` plugin or * `babel-plugin-inline-react-svg` plugin. */ const content: any export default content } // ...
要修正這個問題很簡單。在你的專案根目錄下,新增一個名叫 /types
的資料夾,並在裡面創建一個 .d.ts
檔案,這是 TypeScript 的宣告檔,用來描述 JavaScript 函式庫或是瀏覽器環境的型別。
在 .d.ts
檔案裡,你可以重寫一些內建型別,比如說,當你需要使用到 GTM 事件時,你可能會用到 window.dataLayer
。但標準的 window
介面並沒有包含 dataLayer
屬性,這會導致型別錯誤。我們可以透過在一個名為 global.d.ts
的檔案裡,為 window
添加 dataLayer
屬性的型別宣告,來給 dataLayer
提供正確的型別。
首先在 tsconfig.json
檔案裡,加入 "typeRoots": ["./types"]
這一行設定。這行設定會告訴 TypeScript 在解析型別宣告時,應該要檢視位於 /types
資料夾下的 .d.ts
檔案。
然後建立名為 /types
的資料夾,在裡面建立一個名為 images.d.ts
的檔案,並把原本在 /node_modules/next/image-types/global.d.ts
的內容複製過去。這裡要將 *.svg
的宣告替換掉。根據你的專案情況和使用的工具,你可能需要修改相關的型別註釋。以下是一個例子:
declare module '*.svg' { const content: (props: React.SVGProps<SVGElement>) => ReactElement; export default content; }
因為定義了 props: React.SVGProps<SVGElement>
,所以你需要任何的 SVG 屬性時直接打一個關鍵字,會自動跳出程式碼提示(Intelligent code completion)告訴你可用的屬性,修改起來更便利。
最後,你可能還需要將 next-env.d.ts
內的圖片型別定義宣告刪除,才能確保 TypeScript 不會加載並使用它原始的型別定義。next-env.d.ts
是 Next.js 專案中自動生成的一個 TypeScript 宣告檔案。這個檔案的主要目的是讓 TypeScript 能夠識別 Next.js 提供的型別定義,以及與 Next.js 相關的型別(比如圖片檔案的)。
/// <reference types="next" /> /// <reference types="next/image-types/global" /> <= 移除這行
集中管理 SVG
既然 SVG 都變成 component 了,可以通通放到放到一個檔案內再匯出,有需要使用到的地方再匯入就行了。例如新建一個 /components/icons
資料夾,在裡面新增一個 /img
資料夾把用到的 SVG 放進去,及一個 index.tsx
檔案把 SVG 集中匯出:
import LogoSVG from './img/logo.svg'; import ShoppingCartSVG from './img/shopping_cart.svg'; ... export { LogoSVG, ShoppingCartSVG, ... }
如此一來要使用這些 SVG 時就能從同一個檔案匯入了。
svgviewer
搭配這個網站 svgviewer⎘,它可以用來找一些別人做好的 SVG 應用在專案上,也是一個線上 SVG 檢視器,透過它可以貼上設計師給的 SVG 程式碼來查看效果,針對 SVG 的程式碼進行美化(Prettify)或是最佳化(Optimize),讓檔案容量變得更小,其他還有產生 SVG 檔案分享連結以及轉存成 PNG 的功能。
在使用 SVG 時把 width
及 height
的尺寸屬性拿掉,如果是單一色系的 Icon 甚至會把 fill
及 stroke
屬性也拿掉,然後丟進 svgivewer 預覽效果,沒問題就 Optimize 後再放回專案使用。
如此一來便可將開發網站上常會用到的 Icon 都製作成可透過程式碼完全控制的組件,例如 "Add", "Remove", "Edit" 等 Icon,可能會因為組件 state 而產生顏色變化時,不需要再匯入一張相同但只是顏色不同的檔案,而是可以透過 props 來變更,達到 SVG 元件庫的效果。
最後要注意,如果沒有把 SVG 原始碼的外觀屬性拿掉,即便在做成 component 後也無法透過 props 修改它,因此讓 SVG 保持在最乾淨的狀態,一切的顏色、尺寸都由 component 化來傳值控制,就能達到最大的靈活度是很重要的。不過樣式複雜的 SVG 就建議不要拿掉外觀屬性,以免最後改出來的效果不對了。