Introduction & 前言 
還在找怎麼在 Next  最優雅引入 SVG  的使用方式嗎?看到這邊就對了。
 
如果你需要參考怎麼在 Next.js  透過 Component  方式引入 SVG  ,並且自定義一些參數可以讓開發者快速調整樣式,請看這篇
使用純 React.js  也適用
如果你是使用 Vue 也可以參考筆者早期的文章 [Vue Note] — 在 Vue 優雅的引入 SVG 幾種方式  
 
SVG(可縮放向量圖形) - Scalable Vector Graphics - SVG 是一種圖型格式,SVG是一種基於可延伸標記式語言 XML(電腦所能理解的資訊符號,類似 HTML),用於描述二維向量圖形的圖形格式。SVG 由 W3C 制定,是一個開放標準。 
有興趣可以參考這篇文章 設計師對 SVG 應該有的觀念  裡面介紹到了 SVG  的優點,包括 放大不會失真、可以做成動畫同時保有小檔案容量的優點 及 圖示集 …等等。
Summary & 摘要 不囉唆先上連結
筆者近期因為一些專案上的實作,接觸到了要把 SVG  元件化的設計,因為之前有曾經在 Vue  上元件畫過 SVG  (可參考 [Vue Note] — 在 Vue 優雅的引入 SVG 幾種方式  ),固這次也把過程筆記下來
因為這次實作的專案是 Next.js  ,如果你是使用純 React.js  也不用擔心,基本上只有設定部分會有些許差異,但實作方面是大同小異的
順帶一提上次實作 Vue.js 的 SVG 元件已經是疫情時候的事情了,時間過真快 
 
本篇文章預設學習前的基本條件需求:
知道且會基本使用 Next.js  
會使用終端機安裝套件 
一顆熱忱的心 -> 超級重要,碰到坑千萬不要氣餒 
 
本篇文章將有以下幾個步驟:
前前前言 SVG組成介紹 環境安裝 實際使用 進階使用 建立元件 額外問題  
如果只想知道怎麼使用,也可以跳過 前前前言  以及 SVG組成介紹  ,直接進入重點進階使用 
 
看完文章你可以做到:
開發者可以更容易的透過 CSS  或者參數客製化 SVG  
統一管理 SVG  的設定以及規範 
 
前前前言 基本上我們在專案使用 SVG  引入有幾種方式
首先介紹最基本的 <Img>
1 2 3 4 5 6 7 8 9 10 11 import  React  from  'react' import  LogoSvg  from  '@/assets/images/logo.svg' export  default  function  HomePage (  return  (     <div >        {/* 直接把 Svg 帶入 Img 標籤使用 */}       <img  src ={LogoSvg}  alt ="logo"  />      </div >    ) } 
如同之前文章提到的,第一種方式的優點是快速,缺點是好幾個 SVG  要引好幾次,另外無法直接對 SVG  改變顏色。 
 
Inline  引入方式這種方式之前文章也有提到,就直接貼過來
第二種方式更簡單,如果你仔細打開 SVG  的檔案會發現長得像下面這樣。
直接把這幾段程式碼貼入到你的檔案中即可出現。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import  React  from  'react' import  LogoSvg  from  '@/assets/images/logo.svg' export  default  function  HomePage (  return  (     <div >        {/* 直接把 Svg 帶入使用 */}       <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 >      </div >    ) } 
第二種方式雖然可以自由改變大小及顏色,但與第一種方式同樣的,需要引入好幾個 SVG  要貼入好幾次,而且這種方式會讓版面嚴重的變得雜亂,一點也不清爽。 
 
Component  引入方式最後也是之前文章的出發點,這次一模一樣的動機,也貼內容過來
是筆者最推薦的一種方式,這種方式日後擴充性高 ,使用方便 ,且可以任意放大放小不失真 ,加上想改變顏色就可以直接改變 。
在了解這種方式之前要先來介紹一下 SVG  的標籤,基本上你必須認識最基本的三個標籤,分別為 <defs>、<use>、<symbol>。
基本上 <svg> 你們已經認識了,還有包在 <svg> 裡面的 <path>、<rect>,這些就是定義圖形的長相,但為什麼還有另外上面三個呢?
這三個標籤 <defs>、<symbol>、<use> 可以使我們輕鬆達到把所有的 SVG  放在同一個 .svg  檔案裡,然後透過分別給予他們的 ID Name  去呼叫並使用它。
SVG組成介紹 <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  名稱一定要對到才有用。 
環境安裝 在要元件化之前要提一下這邊跟之前比較不同的地方在哪裡
之前我們是把 SVG  都引入到程式碼裡面
這樣做的優點是一開始就會載入全部 SVG  ,只需要載入一次後續圖案載入會很快,幾乎不需要 Loading 
缺點也顯而易見,很多沒用到的 SVG  可能也會被載入
這次我們需要修改一下做法,我們只會讓有使用到的 SVG  在元件載入到畫面的時候,才去引入使用
安裝重點套件 @svgr/webpack 讓我們先安裝重點套件 @svgr/webpack 
1 $ npm i @svgr/webpack -D 
這個套件其實就是貫穿整個文章最重要的東西,它會把 SVG  轉化為 React.js  元件
實際使用 確定套件有安裝好之後,就可以實際使用了
直接到要使用的地方輸入下面的程式碼
1 2 3 4 5 6 7 8 9 import  GithubIcon  from  "./github.svg" ; const  Page  = (  return  (     <GithubIcon  />    ); }; export  default  Page ;
如果你是使用 Next.js  turbopack @svgr/webpack  的介紹直接去修改 next.config.ts 補上下面設定會噴錯
1 2 3 4 5 6 7 8 9 10 11 12 13 import  type  { NextConfig  } from  "next" ;const  nextConfig : NextConfig  = {        webpack : (config ) =>  {     config.module .rules .push ({       test : /\.svg$/ ,       use : ["@svgr/webpack" ],     });   }, } 
這時候請在 next.config.ts 改為下面內容
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 import  type  { NextConfig  } from  "next" ;const  nextConfig : NextConfig  = {        experimental : {     turbo : {       rules : {         '*.svg' : {           loaders : ['@svgr/webpack' ],           as : '*.js' ,         },       },     },   }      webpack : (config ) =>  {     config.module .rules .push ({       test : /\.svg$/ ,       use : ['@svgr/webpack' ],     });     return  config;   }, }; export  default  nextConfig;
詳細內容可以參考 Demo 分支 feat/svgr-normal-use 
 
進階使用 
所以我說那個元件呢? 
 
如果依照上面使用方式未免太不清爽了,我們每一個元件都必須要一個一個自己引入去使用,雖然不是不行,但我們有提到要做成元件
請先在 src  底下開一個 components  資料夾,然後再建立一個 SvgIcon  的元件資料夾
首先我們要先定義型別(非使用 TS 可以跳過) 在 SvgIcon  底下建立 types  資料夾,再分別建立兩個檔案
1 2 3 4 5 declare  module  '*.svg'  {  import  { FC , SVGProps  } from  'react'    const  content : FC <SVGProps <SVGElement >>   export  default  content } 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 interface  WebpackRequireContext  {  keys (): string []   (id : string ): { default : React .ComponentType <React .SVGProps <SVGElement >> } } declare  global  {  interface  NodeRequire  {     context (directory : string , useSubdirectories : boolean , regExp : RegExp ): WebpackRequireContext    }   const  require : NodeRequire  } export  { WebpackRequireContext  }
安裝配套插件 這邊我們需要另外安裝兩個插件,一個是可以把 className  組起來的插件 clsx 
另一個是排版優化插件 prettier 
1 2 $ pnpm i clsx $ pnpm i prettier 
建立指令產出對應型別 再來我們需要建立 generate  這個資料夾在 SvgIcon  資料夾下,裡面會建立一支檔案
這支檔案是幫我們生成型別用的,也會順便生成 Demo  的 html 
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 import  fs from  'fs' import  path from  'path' import  { fileURLToPath } from  'node:url' const  __filename = fileURLToPath (import .meta .url )const  __dirname = path.dirname (__filename)import  prettier from  'prettier' function  formatHtml (html, parser = 'html'  ) {  try  {     return  prettier.format (html, {       parser,       printWidth : 120 ,       tabWidth : 2 ,       useTabs : false ,       singleQuote : true ,       semi : false ,       trailingComma : 'es5' ,       bracketSpacing : true ,       arrowParens : 'always' ,       endOfLine : 'lf' ,     })   } catch  {     return  html   } } function  needsModification (svgContent ) {     const  svgTag = svgContent.match (/<svg[^>]*>/ )[0 ]   if  (svgTag.includes ('width=' ) || svgTag.includes ('height=' )) {     return  true    }      const  fillMatches = svgContent.match (/fill=["']([^"']*)["']/g )   if  (fillMatches) {     for  (const  match of  fillMatches) {       const  value = match.match (/["']([^"']*)["']/ )[1 ]       if  (value !== 'currentColor'  && value !== 'none' ) {         return  true        }     }   }      const  strokeMatches = svgContent.match (/stroke=["']([^"']*)["']/g )   if  (strokeMatches) {     for  (const  match of  strokeMatches) {       const  value = match.match (/["']([^"']*)["']/ )[1 ]       if  (value !== 'currentColor'  && value !== 'none' ) {         return  true        }     }   }   return  false  } function  removeSvgDimensions (svgContent ) {  return  svgContent.replace (/<svg[^>]*\s(width|height)=["'][^"']*["'][^>]*>/g , (match ) =>  {     return  match.replace (/\s(width|height)=["'][^"']*["']/g , '' )   }) } function  replaceFillWithCurrentColor (svgContent ) {  return  svgContent.replace (/<(path|circle|rect)[^>]*>/g , (match ) =>  {     return  match       .replace (/\sfill=["'][^"']*["']/g , ' fill="currentColor"' )       .replace (/\sstroke=["'][^"']*["']/g , ' stroke="currentColor"' )   }) } function  processSvgContent (svgContent ) {  return  replaceFillWithCurrentColor (removeSvgDimensions (svgContent)) } function  extractSvgName (svgContent ) {  const  nameMatch = svgContent.match (/name=["']([^"']+)["']/ )   return  nameMatch ? nameMatch[1 ] : null  } async  function  generateIconTypes (  const  svgDir = path.join (__dirname, '..' , 'svg' )   const  withColorDir = path.join (svgDir, 'withColor' )   const  typesDir = path.join (__dirname, '..' , 'types' )   const  demoDir = path.join (__dirname, '..' , 'demo' )   const  outputFile = path.join (typesDir, 'iconNames.ts' )   const  demoFile = path.join (demoDir, 'index.html' )   if  (!fs.existsSync (typesDir)) {     fs.mkdirSync (typesDir, { recursive : true  })   }   if  (!fs.existsSync (demoDir)) {     fs.mkdirSync (demoDir, { recursive : true  })   }   try  {          const  files = fs.readdirSync (svgDir).filter ((f ) =>  f.endsWith ('.svg' ))          let  withColorFiles = []     if  (fs.existsSync (withColorDir)) {       withColorFiles = fs.readdirSync (withColorDir).filter ((f ) =>  f.endsWith ('.svg' ))     }     const  iconNames = [       ...files.map ((file ) =>  file.replace ('.svg' , '' )),       ...withColorFiles.map ((file ) =>  'withColor/'  + file.replace ('.svg' , '' )),     ].sort ()     if  (iconNames.length  === 0 ) {       console .warn ('在 svg 及 withColor 資料夾中找不到任何 SVG 檔案' )       return      }          const  updatedFiles = []          files.forEach ((name ) =>  {       const  svgPath = path.join (svgDir, `${name} ` )       const  svgContent = fs.readFileSync (svgPath, 'utf8' )       if  (needsModification (svgContent)) {         const  processedContent = processSvgContent (svgContent)         if  (svgContent !== processedContent) {           fs.writeFileSync (svgPath, processedContent, 'utf8' )           updatedFiles.push (name)         }       }     })          withColorFiles.forEach ((file ) =>  {       const  svgPath = path.join (withColorDir, file)       const  svgContent = fs.readFileSync (svgPath, 'utf8' )              const  svgTag = svgContent.match (/<svg[^>]*>/ )?.[0 ] || ''        if  (svgTag.includes ('width=' ) || svgTag.includes ('height=' )) {         const  processedContent = removeSvgDimensions (svgContent)         if  (svgContent !== processedContent) {           fs.writeFileSync (svgPath, processedContent, 'utf8' )           updatedFiles.push ('withColor/'  + file.replace ('.svg' , '' ))         }       }     })     const  typeDefinition = `export type IconName =  ${iconNames.map((name) => `  | '${name} '` ).join('\n' )} export const availableIconNames: IconName[] = [ ${iconNames.map((name) => `  '${name} ',` ).join('\n' )} ] `     fs.writeFileSync (outputFile, typeDefinition, 'utf8' )     const  getSvgContentByName  = (name ) => {       if  (name.startsWith ('withColor/' )) {         const  file = name.replace ('withColor/' , '' )         return  fs.readFileSync (path.join (withColorDir, file + '.svg' ), 'utf8' )       }       return  fs.readFileSync (path.join (svgDir, name + '.svg' ), 'utf8' )     }     const  htmlContent = (() =>  {       const  svgElements = iconNames         .map ((name ) =>  {           const  svgContent = getSvgContentByName (name)           const  displayName = extractSvgName (svgContent)           return  `                  <div class="bg-white rounded-xl p-6 shadow-lg hover:shadow-xl transition-all duration-300 cursor-pointer hover:-translate-y-2 group relative icon-item"                     data-name="${name} "                     data-display-name="${displayName || '' } "                     onclick="copyIconName('${name} ')">                     <div class="w-16 h-16 mx-auto mb-4 flex items-center justify-center bg-slate-50 rounded-lg p-3">                         ${svgContent}                      </div>                     ${displayName ? `<div class="text-sm text-slate-500 text-center mb-1">${displayName} </div>`  : '' }                      <div class="text-sm font-semibold text-slate-700 text-center break-all">${name} </div>                     <div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200">                         <div class="bg-blue-500 text-white text-xs px-2 py-1 rounded shadow-lg">                             點擊複製                         </div>                </div>                 </div>`         })         .join ('' )       return  `<!DOCTYPE html>  <html lang="zh-TW"> <head>   <meta charset="UTF-8">   <meta name="viewport" content="width=device-width, initial-scale=1.0">   <title>SVG Icon Demo - ${iconNames.length}  個圖示</title>   <script src="https://cdn.tailwindcss.com"></script>   <script>     tailwind.config = {       theme: {         extend: {           fontFamily: {             sans: ['-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Microsoft JhengHei', 'sans-serif']           }         }       }     }   </script>   <style>     .gradient-bg {       background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);     }          .toast {       position: fixed;       top: 2rem;       right: 2rem;       z-index: 1000;       transform: translateX(100%);       transition: transform 0.3s ease;     }          .toast.show {       transform: translateX(0);     }     .icon-item {       transition: all 0.3s ease;     }     .icon-item.hidden {       display: none;     }   </style> </head> <body class="gradient-bg min-h-screen p-8 font-sans">   <div class="max-w-6xl mx-auto">     <div class="text-center mb-4 text-white">       <h1 class="text-4xl md:text-5xl font-bold mb-2 drop-shadow-lg">🎨 SVG 圖示庫</h1>       <p class="text-xl opacity-90">點擊複製名稱</p>     </div>          <div class="bg-white/10 backdrop-blur-lg rounded-xl p-6 text-white text-center mb-8 border border-white/20">       <strong>總共 ${iconNames.length}  個圖示</strong> | 最後更新:${new  Date ().toLocaleString('zh-TW' )}      </div>     <div class="mb-8">       <div class="relative">         <input type="text" id="searchInput"           class="w-full px-4 py-3 rounded-lg bg-white/10 border border-white/20 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-white/30"           placeholder="搜尋圖示名稱或中文名稱..." oninput="searchIcons(this.value)">         <div class="absolute right-3 top-1/2 -translate-y-1/2 text-white/50">           <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">             <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"               d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>           </svg>         </div>       </div>     </div>          <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">         ${svgElements}      </div>   </div>   <!-- Toast 通知 -->   <div id="toast" class="toast bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg right-0">     <div class="flex items-center">       <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">         <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>       </svg>       <span id="toast-message">已複製到剪貼簿</span>     </div>   </div>   <script>     function copyIconName(iconName) {       navigator.clipboard.writeText(iconName).then(() => {         showToast('已複製 "' + iconName + '" 到剪貼簿');       }).catch(err => {         console.error('複製失敗:', err);         showToast('複製失敗,請手動複製', 'error');       });     }     function showToast(message, type = 'success') {       const toast = document.getElementById('toast');       const toastMessage = document.getElementById('toast-message');       toastMessage.textContent = message;       if (type === 'error') {         toast.className = 'toast bg-red-500 text-white px-6 py-3 rounded-lg shadow-lg';       } else {         toast.className = 'toast bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg';       }       toast.classList.add('show');       setTimeout(() => {         toast.classList.remove('show');       }, 3000);     }     function searchIcons(query) {       const searchText = query.toLowerCase();       const items = document.querySelectorAll('.icon-item');       items.forEach(item => {         const name = item.dataset.name.toLowerCase();         const displayName = item.dataset.displayName.toLowerCase();         if (name.includes(searchText) || displayName.includes(searchText)) {           item.classList.remove('hidden');         } else {           item.classList.add('hidden');         }       });     }   </script> </body> </html>`     })()     const  html = await  formatHtml (htmlContent)     fs.writeFileSync (demoFile, html, 'utf8' )     console .log (`\n✅ 成功生成檔案!` )     console .log (`📁 型別檔案: ${outputFile} ` )     console .log (`🎨 Demo 檔案: ${demoFile} ` )     console .log (`🎯 包含 ${iconNames.length}  個圖示` )     if  (updatedFiles.length  > 0 ) {       console .log (`\n🔄 已更新以下 ${updatedFiles.length}  個 SVG 檔案:` )       updatedFiles.forEach ((name ) =>  {         console .log (`   - ${name} .svg` )       })     } else  {       console .log ('\n✨ 所有 SVG 檔案都已經符合規範,無需更新' )     }   } catch  (error) {     console .error ('❌ 生成檔案時發生錯誤:' , error.message )     process.exit (1 )   } } generateIconTypes ()
如果沒有使用 TS  可以將 generateIconDemo() 那段改成下面這樣
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 async  function  generateIconDemo (  const  svgDir = path.join (__dirname, '..' , 'svg' )   const  withColorDir = path.join (svgDir, 'withColor' )   const  demoDir = path.join (__dirname, '..' , 'demo' )   const  demoFile = path.join (demoDir, 'index.html' )   if  (!fs.existsSync (demoDir)) {     fs.mkdirSync (demoDir, { recursive : true  })   }                               } 
接著我們到 package.json  的 script  新增一行
1 2 3 4 5 6 7 8 9 10 11 12 13 {   "name" :  "next-svg-component" ,    "version" :  "0.1.0" ,    "private" :  true ,    "scripts" :  {      "dev" :  "next dev --turbopack" ,      "build" :  "next build" ,      "start" :  "next start" ,      "lint" :  "next lint" ,      "svg:generate" :  "node src/components/SvgIcon/generate/generate-icon-types.mjs"     } ,     } 
存放 SVG 在跑指令之前,我們需要在 SvgIcon  資料夾底下再建立一個 svg  資料夾,在這個資料夾底下也順手再開一個 withColor  的資料夾
這個就是你的 SVG  原始檔存放位置
建立好丟檔案進去接著就可以跑指令
目前為止你的目錄長上面這樣
好了之後可以使用下面兩種其中一種方式跑指令
1 2 $ pnpm run svg:generate $ node generate-icon-types.mjs 
終端機顯示有更新之後
你的 types  應該會多出一個 iconNames.ts  
你的 SvgIcon  底下會多出一個 demo/index.html  
 
第二個指令需要看你實際位置在哪裡去修改路徑,可以先用 pwd 查看目前處在哪一個地方 
 
這時候可以打開 /SvgIcon/demo/index.html
指令做了些什麼? 這邊說一下指令都做了些什麼
產出 types  定義 
將 Svg  內寫死的 width, height  及 color  都移除,顏色改為 currentColor  
產生 demo/index.html  ,方便透過網頁直接看所有可使用的 SVG  圖案 
 
如果不移除寬高跟顏色,會導致之後透過 CSS  無法修改 Svg  的樣式
建立元件 前面搞這麼多,終於可以來建立我們的 SVG  元件了
在 SvgIcon 資料夾底下建立 index.tsx
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 59 60 import  { SVGProps  } from  'react' import  { WebpackRequireContext  } from  './types/webpack' import  { IconName  } from  './types/iconNames' import  clsx from  'clsx' function  importAll (requireContext : WebpackRequireContext , prefix = ''   const  icons : Record <string , React .ComponentType <SVGProps <SVGElement >>> = {}   requireContext.keys ().forEach ((item : string   {     const  iconName = prefix + item.replace ('./' , '' ).replace ('.svg' , '' )     icons[iconName] = requireContext (item).default    })   return  icons } const  svgIcons = importAll (  (     require  as  unknown  as  { context : (dir : string , useSubdirs : boolean , pattern : RegExp   WebpackRequireContext  }   ).context ('./svg' , false , /\.svg$/ ) ) const  svgWithColorIcons = importAll (  (     require  as  unknown  as  { context : (dir : string , useSubdirs : boolean , pattern : RegExp   WebpackRequireContext  }   ).context ('./svg/withColor' , false , /\.svg$/ ),   'withColor/'  ) const  Icons  = { ...svgIcons, ...svgWithColorIcons }export  interface  IconProps  extends  SVGProps <SVGElement > {  name : IconName    size ?: number    className ?: string    childrenSVGProps ?: SVGProps <SVGElement >[] } const  SvgIcon : React .FC <IconProps > = ({ name, size = 16 , className = '' , childrenSVGProps, ...props } ) =>  {  const  IconComponent  = Icons [name]   if  (!IconComponent ) {     console .warn (`Icon "${name} " not found` )     return  null    }   return  (     <IconComponent         width ={size}        height ={size}        className ={clsx( 'cursor-pointer ', className )}       {... (childrenSVGProps  ? ({ childrenSVGProps  } as  unknown  as  Record <string , unknown > ) : {})}      {...props}     />   ) } export default SvgIcon 
對於元件可以傳遞的參數後續都可以依照使用情境去進行修改 
 
實際使用 建立結束後我們就可以到想使用的地方去進行使用
1 2 3 4 5 6 7 8 9 import  SvgIcon  from  "@/components/SvgIcon" ; const  Page  = (  return  (     <SvgIcon  name ="github"  className ="text-red-500"  />    ); }; export  default  Page ;
詳細內容可以參考 Demo 分支 feat/svgr-component-use 
 
額外問題 插件導致直接引入 SVG  掛掉 如果你使用 @svgr/webpack  插件後,影響到了你原本的 SVG  載入,導致原本的 SVG  都壞掉不顯示了,可以嘗試在 next.config.ts 修改 @svgr/webpack  的 config.module.rules  設定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import  type  { NextConfig  } from  "next" ;const  nextConfig : NextConfig  = {  webpack : (config ) =>  {      const  iconPath = path.resolve (__dirname, 'src/components/SvgIcon/svg' )   config.module .rules .push ({     test : /\.svg$/ ,     include : iconPath,     use : [       {         loader : '@svgr/webpack' ,         options : {           svgo : false ,           template : (variables, { tpl } ) =>  {                        },         },       },     ],   }) } 
透過 include  可以只針對你的 SvgIcon  底下的 SVG  去進行轉換而已,不會影響到其他單純直接引入的 SVG  圖案
相關文章也可以參考這篇 今天聊聊@svgr/webpack遇到的坑  
 
使用 svgr  插件,但是在 turbopack  模式下發生錯誤 可以參考這篇文章 How to integrate @svgr/webpack to make turbopack work? #50337 
Conclusion & 結論 參考網站