[Vue Note] — 在 Vue 優雅的引入 SVG 幾種方式

Introduction & 前言

SVG

先說 Vue3 可以參考本篇紀錄

最近繼續在完成全端 Side Project 時剛好碰到需要引入 Icon 的情形,以往的專案都是使用 Fontawesome 的免費方案,但有時候需要使用到的 Icon 在上面找不到,需要再另外引入現成的 SVG 去使用,所以索性又乾脆直接研究一番怎麼直接引入 SVG,如果你也有相同目的也有興趣就往下看吧,這邊筆者會記錄幾種使用方式。

SVG(可縮放向量圖形) - Scalable Vector Graphics

- SVG 是一種圖型格式,SVG是一種基於可延伸標記式語言 XML(電腦所能理解的資訊符號,類似 HTML),用於描述二維向量圖形的圖形格式。SVG 由 W3C 制定,是一個開放標準。

有興趣可以參考這篇文章 設計師對 SVG 應該有的觀念 裡面介紹到了 SVG 的優點,包括 放大不會失真、可以做成動畫同時保有小檔案容量的優點 及 圖示集…等等。


超級前言

疫情爆炸

最近疫情大爆炸,一天前的報導 疫情即時/國內單日新增 180 例確診個案!雙北防疫升級到第三級疫情即時/國內單日新增 180 例確診個案!雙北防疫升級到第三級 已經到了單日新增 180 例。

最近經歷了停水停電又疫情,希望大家一起加油撐過這個難關,平日不能出門就在家讀讀書看看影片寫寫 Code 吧!

超級前言要先提醒大家 勤洗手、少外出、多戴口罩 之外,請大家下載 臺灣社交距離App,這是一款透過藍芽偵測附近是否有確診者的 APP,但放心全程不須填寫任何個人資料。

臺灣社交距離


Summary & 摘要

因為這次專案使用 Vue3 + TS(TypeScript) 所以設定上有些許不一樣,但不會差異太多,如果您使用 Vue2 也不用擔心!

主要會介紹幾種引入方式:

  1. <Img> 引入方式

  2. Inline 引入方式

  3. Component 引入方式


<Img> 引入方式

首先介紹最基本的 <Img> 引入方式,這個方式最簡單只需要在專案內引入圖片檔案,接著再放到圖片標籤裡即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<template>
<!-- Vue3 不需要在用一個 Div 包住所有的 Div -->
<img :src="tagIcon">
<img :src="profileIcon">

<!-- 或直接使用路徑>
<img src="xxx.svg">
</template>

<script lang="ts">
import { defineComponent, ref } from "vue";

// Icon
import tagIconUrl from '@/assets/images/icon/icon_tag_circle.svg';
import profileIconUrl from '@/assets/images/icon/icon_profile.svg';

export default defineComponent({
name: "MyIconComponent",
setup() {
const tagIcon = ref(tagIconUrl);
const profileIcon = ref(profileIconUrl);

return {
tagIcon,
profileIcon,
};
},
});
</script>
  • 第一種方式的優點是快速,缺點是好幾個 SVG 要引好幾次,另外無法直接對 SVG 改變顏色。

Inline 引入方式

第二種方式更簡單,如果你仔細打開 SVG 的檔案會發現長得像下面這樣。

SVG Code

直接把這幾段程式碼貼入到你的檔案中即可出現。

1
2
3
4
5
6
7
8
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path fill="var(--ci-primary-color, currentColor)" d="M256,16C123.452,16,16,123.452,16,256S123.452,496,256,496,496,388.548,496,256,388.548,16,256,16ZM403.078,403.078a207.253,207.253,0,1,1,44.589-66.125A207.332,207.332,0,0,1,403.078,403.078Z" class="ci-primary"/>
<rect width="40" height="40" x="152" y="200" fill="var(--ci-primary-color, currentColor)" class="ci-primary"/>
<rect width="40" height="40" x="320" y="200" fill="var(--ci-primary-color, currentColor)" class="ci-primary"/>
<path fill="var(--ci-primary-color, currentColor)" d="M338.289,307.2A83.6,83.6,0,0,1,260.3,360H251.7a83.6,83.6,0,0,1-77.992-52.8l-1.279-3.2H137.968L144,319.081A116,116,0,0,0,251.7,392H260.3A116,116,0,0,0,368,319.081L374.032,304H339.568Z" class="ci-primary"/>
</svg>
</template>
  • 第二種方式雖然可以自由改變大小及顏色,但與第一種方式同樣的,需要引入好幾個 SVG 要貼入好幾次,而且這種方式會讓版面嚴重的變得雜亂,一點也不清爽。

Component 引入方式

最後是筆者最推薦的一種方式,這種方式日後擴充性高使用方便且可以任意放大放小不失真加上想改變顏色就可以直接改變

在了解這種方式之前要先來介紹一下 SVG 的標籤,基本上你必須認識最基本的三個標籤,分別為 <defs><use><symbol>

基本上 <svg> 你們已經認識了,還有包在 <svg> 裡面的 <path><rect>,這些就是定義圖形的長相,但為什麼還有另外上面三個呢?

這三個標籤 <defs><symbol><use> 可以使我們輕鬆達到把所有的 SVG 放在同一個 .svg 檔案裡,然後透過分別給予他們的 ID Name 去呼叫並使用它。


<defs> 標籤

用於預定義一个元素使其能夠在 SVG 圖像中重複使用,舉的例子:

1
2
3
4
5
6
7
8
9
10
<svg>
<defs>
<symbol id="3d-svg" viewBox="0 0 512 512">
<path fill='var(--ci-primary-color, currentColor)' d='M68.983,382.642l171.35,98.928a32.082,32.082,0,0,0,32,0l171.352-98.929a32.093,32.093,0,0,0,16-27.713V157.071a32.092,32.092,0,0,0-16-27.713L272.334,30.429a32.086,32.086,0,0,0-32,0L68.983,129.358a32.09,32.09,0,0,0-16,27.713V354.929A32.09,32.09,0,0,0,68.983,382.642ZM272.333,67.38l155.351,89.691V334.449L272.333,246.642ZM256.282,274.327l157.155,88.828-157.1,90.7L99.179,363.125ZM84.983,157.071,240.333,67.38v179.2L84.983,334.39Z' class='ci-primary'/>
</symbol>
</defs>

<use xlink:href="#3d-svg" x="50" y="50" />
<use xlink:href="#3d-svg" x="200" y="550" />
</svg>

<symbol> 標籤

這個標籤通常會包在 <defs> 標籤裡面,用於定義可重複使用的符號,這邊要記得,不管使用 <defs> 或是 <symbol> 都不會直接顯示,必須使用最後一個要介紹的標籤。

<use> 標籤

透過 <use> 標籤可以使用上面定義那兩個標籤的 SVG,使用方法如上面所示:

1
2
<use xlink:href="#3d-svg" x="50" y="50" />
<use xlink:href="#3d-svg" x="200" y="550" />

切記 ID 名稱一定要對到才有用。


講解完這幾個標籤你大概就知道該怎麼做成 Component,一開始筆者不知道還以為是把所有要用到的 SVG 都引入到那個 Component 裡面再去呼叫,這樣也太不清爽了。

- 把所有要用到的 SVG 分類好分成幾個檔案放,然後再透過 Webpackrequire.context 遍歷 SVG 把他們都寫入 main.js 裡面,最後透過 <use> 的標籤寫在 Vue Component 裡面,透過帶入 Prop 去呼叫你要使用的 Icon。

*這邊使用 Coreui Free Admin 的免費 Icon 做範例*

  1. 建立 Icon 的相關資料夾

SVG 的範例檔案在步驟一的下方有附上,雖然是提供 Coreui Free Admin 的免費 Icon,但還是請不要使用在商業用途,以避免法律責任。

第一步

首先到 /src/assets/ 建立 Icons 資料夾,這邊筆者把 Icons 底下資料夾在分為 spritessvg 兩個,前者會放所有 SVG 的程式碼內容,像是這種程式碼 <path fill='var(--ci-primary-color, currentColor)' d='M68.983,382.642l171.35,98.928a32.082,32.082,0,0,0,32,0l171.352-98.929a32.093,32.093,0,0,0,16-27.713V157.071a32.092,32.092,0,0,0-16-27.......略,然後用 包起來。

這種所有 SVG設定組合集可以不一定只能有一隻檔案,筆者這邊就分為三個檔案,分別為 LOGO、國旗、其他 SVG 分類。

分類擺放

檔案範例請 **點我下載**,密碼為 5u7r,如果連結過期或不能使用請在下方留言告訴我。

  1. 安裝 svg-sprite-loader 套件

這是讓 Webpack 讀懂你 SVG 的套件,如果沒安裝就會噴錯。

1
2
3
4
5
// NPM
npm i svg-sprite-loader -S

// YARN
yarn add svg-sprite-loader

選擇你的安裝方式,安裝後記得在 Vue.config.js 裡面加上設定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const path = require('path');

function resolve(dir) {
return path.join(__dirname, '.', dir)
}

module.exports = {
// 引入 Svg 需要搭配這個, 不然會出錯
configureWebpack: {
module: {
rules: [{
test: /\.svg$/,
use: ['svg-sprite-loader'],
}, ]
},
},
// 引入 Svg 檔案(主要寫在 main.js)
chainWebpack: config => {
config.module.rules.delete("svg");
config.module
.rule('svg-sprite-loader')
.test(/\.svg$/)
.include
.add(resolve('src/main.ts'))
.end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({
symbolId: '[name]'
})
},
};

正常的網路上教學都只會爬文到加入 chainWebpack 這個設定,但似乎都沒有加上 configureWebpack 這個設定,這邊筆者也不太確定為什麼加上就好了,如果有哪路大神知道還請不吝設在下方留言,感激不盡。

如果你是使用 Vue Cli 記得設定完要重新在啟動一次,設定檔才會被讀到!

  1. 將所有設定組合集包在 HTML 裡

聰明的你會看到在第一步 建立 Icon 的相關資料夾 圖片裡有多一個 index.ts 的檔案,如果你並不是使用 TS 的話可以創建一個 index.js

裡面的目的是要遍歷你的 SVG 組合設定集內容並把他們輸出到 HTML 上:

1
2
3
4
5
6
7
// 引入 Svg(都寫成 Sprites 了), 使用 Symbol 的標籤, 透過 use 可以顯示該 Svg,
// 所有 Svg 放在 /src/assets/icons/sprites 直接引入後使用
// Svg 放在 /src/assets/icons/svg 方便查看該 Svg 長怎樣
const req = require.context("@/assets/icons/sprites", false, /\.svg$/);
const requireAll = (requireContext: any) =>
requireContext.keys().map(requireContext);
requireAll(req);

這邊可以引入單一隻 SVG 或是組合設定集,但相信大部分的人都是選後者;之後回去頁面上打開開發者模式(F12)看應該就會看到引入的組合設定集。

有在這邊看到就是成功引入了

展開後會發現熟悉的標籤 <defs><symbol> 出現了。

之後透過 Use 標籤去使用

  1. 建立方便引用的 Component

這邊就是重頭戲了,在這邊我們需要建立一個 IconComponent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<template lang="pug">
svg.svgIcon(
xmlns="http://www.w3.org/2000/svg"
:class="className"
aria-hidden="true"
)
use(
:xlink:href="`#${iconType}_${iconFullName}`"
)
</template>

<script lang="ts">
import { defineComponent, toRef } from "vue";

type iconTitleType = string;

export default defineComponent({
name: "IconComponent",
props: {
// 客製化 Class Name
className: {
type: String,
},
iconType: {
type: String,
default: "free",
},
iconName: {
type: String,
default: "3d",
},
},
setup(props) {
// Props
let iconType = toRef(props, "iconType");
let iconName = toRef(props, "iconName");
// Icon 開頭
let iconFullName: iconTitleType;
switch (iconType.value) {
case "brand":
iconFullName = `cib-${iconName.value}`;
break;
case "flag":
iconFullName = `cif-${iconName.value}`;
break;
case "free":
default:
iconFullName = `cil-${iconName.value}`;
break;
}

return {
iconFullName,
...props,
};
},
});
</script>

仔細看看剛剛步驟三展開引入的 SVG 組合設定集,裡面的 SVG ID 開頭都會跟著我們分類的檔案名稱,所以這邊也必須判斷你要使用的 SVG 在哪一個分類,所以特別使用 iconTypeiconName 去判斷,並從父層透過 prop 傳過來。

如果你沒有使用 TS 其實也是一樣做法,最終目的都是想要透過父層傳過來的 prop 去判斷要使用哪一個 Icon

  1. 全局註冊 Component

到這邊幾乎就快大功告成了,到入口 main.ts 或是 main.js(沒有用 TS 的話)。

這邊註冊分為 Vue2Vue3

  • Vue2:
1
2
3
4
5
import Vue from 'vue'
import "@/assets/icons";
import SvgComponent from "@/components/SvgComponent.vue";

Vue.component('SvgComponent', SvgComponent)
  • Vue3:

Vue3 已經遺棄 Vue.component() 這種方法了。

1
2
3
4
5
6
7
8
import "@/assets/icons";
import SvgComponent from "@/components/SvgComponent.vue";

createApp(App)
.component("SvgComponent", SvgComponent)
.use(store)
.use(router)
.mount("#app");

請記得 mount 一定要在最後。

  1. Enjoy It!

最後就是開心地使用了,由於全局註冊的關係,也不用每一個檔案都要在引入一次,直接使用即可。

1
2
3
4
5
<template lang="pug">
SvgIcon(
iconName="happy"
)
<template>
  1. 擴充方法

到這個步驟你已經可以順利的引入 SVG,如果你有自定義的 SVG 請繼續參考這個步驟。

在這邊我們先改寫剛剛的 步驟四建立方便引用的 Component - IconComponent,將 setup 裡面判斷目前要用哪一個 SVG 的地方改寫如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
switch (iconType.value) {
// 新增自定義 custom
case "custom":
iconFullName = `c-${iconName.value}`;
break;
case "brand":
iconFullName = `cib-${iconName.value}`;
break;
case "flag":
iconFullName = `cif-${iconName.value}`;
break;
case "free":
default:
iconFullName = `cil-${iconName.value}`;
break;
}

步驟一

接著在按照 步驟一 在資料夾 ./src/assets/icons/sprites 裡面新增 custom.svg,然後裡面輸入下列程式碼(這邊可以複製筆者的,會有驚喜):

1
2
3
4
5
6
7
8
9
10
11
<svg aria-hidden="true" style="position: absolute; width: 0; height: 0; overflow: hidden;" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<symbol id="c-test-svg">
// 這邊為可替換的內容
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 521.84 201"><defs><style>.cls-1{fill:#040000;}.cls-1,.cls-3{stroke:#231815;stroke-miterlimit:10;}.cls-2{font-size:90px;fill:#fff;}.cls-2,.cls-4{font-family:HanziPenTC-W5-B5pc-H, HanziPen TC;}.cls-3{fill:#e5852e;}.cls-4{font-size:70px;fill:#231815;}</style></defs><title>資產 2</title><g id="圖層_2" data-name="圖層 2"><g id="圖層_1-2" data-name="圖層 1"><rect class="cls-1" x="0.5" y="0.5" width="520.84" height="200"/><text class="cls-2" transform="translate(20.5 127.41)">Happy</text><rect class="cls-3" x="262.19" y="25.22" width="243.01" height="152.69" rx="12"/><text class="cls-4" transform="translate(321.67 122.5)">SVG</text></g></g></svg>
</symbol>
<symbol id="請輸入你的客製化ID">
// 新增更多你的 SVG 吧
</symbol>
</defs>
</svg>

接著依照上面 步驟三將所有設定組合集包在 HTML 裡,會把所有 ./src/assets/icons/sprites 下的 SVG 程式碼都寫進 HTML 裡面,我們只需要透過 步驟四建立方便引用的 Component 去把這個 SVG 呼叫出來即可。

1
2
3
4
5
6
7
8
9
10
11
12
<template lang="pug"/>
// 這邊套件怎麼傳參數進去也可以自定義,不一定要按照筆者的
SvgIcon(
className="tw-mr-1",
:width="100",
:height="40",
iconType="custom",
iconName="test-svg",
)
</template>

// 不需要在 <script> 再次引入 SvgIcon,因為已經全域註冊了

Conclusion & 結論

這次因為 Vue3 加上 TS 的關係又吸取了很多知識,雖然 Side Project 的進度緩慢,但許多新知吸取後覺得滿滿成就感,很多原本是簡單的東西,例如 SVG 的引入,但可以在一件事情上學習且練習到其他關聯的東西,這才是最無價的。

希望這篇文章可以幫助自己之後需要再次使用時可以快速回憶,也希望能幫助到各位,最後希望最近的疫情可以趨緩,我們可以一起度過這個困難的時期,再次提醒,大家要記得 勤洗手、少外出、一定要戴口罩

不戴好口罩你知道的!!!!!


參考網站