一文詳解如何在基於webpack5的react專案中使用svg

w4ngzhen發表於2023-01-29

本文主要討論基於webpack5+TypeScript的React專案(cra、craco底層本質都是使用webpack,所以同理)在2023年的今天是如何在專案中使用svg資源的。

首先,假定您已經完成基於webpack5+TypeScript的React專案的搭建工作(如果您不太清楚搭建的背景,可以參考這篇筆記:【個人筆記】2023年搭建基於webpack5與typescript的react專案 - 知乎 (zhihu.com))。

HTML中SVG經典用法

SVG:可縮放向量圖形 | MDN (mozilla.org)

要在一般的html中使用SVG,我們可以直接編寫標籤:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div id="app">
    <svg xmlns="http://www.w3.org/2000/svg"
         viewBox="0 0 512 512" width="200" height="200">
        <path d="M256 32C114.6 32 0 125.1 0 240c0 47.6 19.9 91.2 52.9 126.3C38 405.7 7 439.1 6.5 439.5c-6.6 7-8.4 17.2-4.6 26S14.4 480 24 480c61.5 0 110-25.7 139.1-46.3C192 442.8 223.2 448 256 448c141.4 0 256-93.1 256-208S397.4 32 256 32zm0 368c-26.7 0-53.1-4.1-78.4-12.1l-22.7-7.2-19.5 13.8c-14.3 10.1-33.9 21.4-57.5 29 7.3-12.1 14.4-25.7 19.9-40.2l10.6-28.1-20.6-21.8C69.7 314.1 48 282.2 48 240c0-88.2 93.3-160 208-160s208 71.8 208 160-93.3 160-208 160z"/>
    </svg>
</div>
</body>
</html>

010-html-svg-usecase

React編寫SVG元件

在React中,React的jsx標籤與HTML中的標籤幾乎是一一對應的,我們可以透過編寫jsx來描述元件。所以不難想到,我們可以使用svg以及與其關聯的jsx標籤(譬如<path><g>等)來手寫一個React的SVG元件:

export const IconComment = () => {
    return (
        <svg xmlns="http://www.w3.org/2000/svg"
             viewBox="0 0 512 512"
             width="200"
             height="200">
            <path
                d="M256 32C114.6 32 0 125.1 0 240c0 47.6 19.9 91.2 52.9 126.3C38 405.7 7 439.1 6.5 439.5c-6.6 7-8.4 17.2-4.6 26S14.4 480 24 480c61.5 0 110-25.7 139.1-46.3C192 442.8 223.2 448 256 448c141.4 0 256-93.1 256-208S397.4 32 256 32zm0 368c-26.7 0-53.1-4.1-78.4-12.1l-22.7-7.2-19.5 13.8c-14.3 10.1-33.9 21.4-57.5 29 7.3-12.1 14.4-25.7 19.9-40.2l10.6-28.1-20.6-21.8C69.7 314.1 48 282.2 48 240c0-88.2 93.3-160 208-160s208 71.8 208 160-93.3 160-208 160z"/>
        </svg>
    );
}

這個IconComment就是一個普通的React元件,編寫完成後我們就可以在需要使用的地方引入了:

020-svg-react-component-usecase

效果如下:

030-svg-react-component-display

SVG檔案在React中的使用方式

元件模式使用

上面我們講到了如何編寫一個svg元件,但一般來說,我們都會讓設計出svg資源,然後存放在專案某個目錄下並進行使用。我們當然可以把設計出的svg的內容複製到我們的專案中,以元件的方式來使用:

040-copy-svg-content-to-react-component

但是每次都需要複製一個又一個的元件當然是一件很麻煩的事情,在webpack中我們使用svg資源的時候,其實更希望如同圖片資源一樣以模組的形式引入(import或者是require)並使用,就像下面一樣:

050-import-svg-component-flow

如果要達到上面的目的,我們首先需要弄清楚一件事情,那就是我們們"import"的"IconAbc"到底是個什麼?透過上面的程式碼反推,我們很容易回答,IconAbc肯定需要是一個React元件(函式元件或類元件)。

瞭解webpack的同學都知道,webpack可以透過loader,來處理一個資源在匯入的時候會變成什麼。但現在在webpack配置中,我們先不新增任何關於svg模組的處理loader,不出意外肯定會報錯:

060-import-svg-without-loader

ERROR in ./src/icon-comment.svg 1:0
Module parse failed: Unexpected token (1:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders

譯文:您可能需要適當的載入程式(loader)來處理此檔案型別,目前沒有配置載入程式來處理此檔案。請參閱 https://webpack.js.org/concepts#loaders

問題我們已經很清楚了webpack無法找到處理svg模組的loader,那麼現在的解決方案是什麼呢?我們可以使用svgr提供的配合webpack的loader(Webpack - SVGR (react-svgr.com))就可以完成這個任務。

首先安裝必要的依賴:yarn add -D @svgr/webpack

然後,配置webpack處理svg檔案:

module.exports = {
    ... ...
    module: {
        rules: [
            {
                test: /\.tsx?/,
                use: [
                    'babel-loader'
                ],
                exclude: /node_moudles/
            },
            ... ...
+           {
+               test: /\.svg$/,
+               use: ['@svgr/webpack']
+           }
        ]
    },
    ... ...
}

完成配置以後,重新經過webpack編譯打包,執行後會看到控制檯的輸出:

070-import-svg-by-svgr

  • 效果1:我們透過console.log輸出的IconComment是一個React元件純函式。
  • 效果2:程式碼中我們使用<IconComment/>在螢幕上展示出來了。

PS:上圖中import報錯暫時可以不用關心,是IDE型別檢查的語法提示,webpack打包是沒有問題的,想要深入瞭解,可以參考:【長文詳解】TypeScript與Babel、webpack的關係以及IDE對TS的型別檢查 - 知乎 (zhihu.com)

回顧整個過程,我們可以用下面的圖來描述這個過程:

080-svgrwebpack-handle-flow

資源模式使用

當然,我們有的時候並不想按照React元件的使用。例如,svg同樣可以作為一些元素的背景,這個時候我們需要把svg是為類似於圖片一樣的資源,就像下面的方式:

090-use-svg-by-url

如果svg的loader配置保持不變,還是@svgr/webpack,我們會看到沒有起效果,並且,檢視對應生成css樣式檔案,我們可以看到對應的url('./icon-comment.svg')被編譯為了url(8ed4ed501566520a5cd0.svg)

100-svg-url-result

這個8ed4ed501566520a5cd0.svg是什麼呢?可能看起來還有點懵,我們嘗試打包編譯專案,看一下編譯後的產物就知道了:

110-build-dist-svg

透過上圖的結果可知,很明顯svg在這種場景下依然被@svgr/webpack這個loader處理為了React元件,又因為我們們是在less/css中引用這個svg,loader內部將這種場景回退到了檔案資源存放了。

現在,我們希望webpack在處理這種場景的時候,還是以普通資源的方式進行;同時,在React程式碼中依然能夠將svg資源以元件的形式被引入。好在webpack支援這樣的配置:

module.exports = {
    ... ...
    module: {
        rules: [
            ... ...
            {
                // 引用的資源如果是 '${svg-path}/icon-comment.svg?abc'
                test: /\.svg$/,
                resourceQuery: /abc/,
                // 以webpack的資源形式載入(普通資原始檔、base64等)
                type: 'asset',
            },
            {
                // 除了上面的匹配規則,我們都按照React元件來使用
                test: /\.svg$/,
                resourceQuery: {not: [/abc/]},
                use: ['@svgr/webpack']
            }
        ]
    },
    ... ...
}

webpack5中的 type: "assets" 是什麼?可以看這篇文章:

資源模組 | webpack 中文文件 (docschina.org)

在上述配置中,我們都將匹配svg資源的引用,不同的是,如果這個引用路徑帶上url query,則使用webpack5的asset資源模組來處理;否則,呼叫@svgr/webpack來將其轉換為React元件。

完成上述的配置以後,我們適當的修改程式碼,如下所示:

120-code-show-svg-usecase

關於關鍵程式碼的解釋:

  1. index.tsx第三行和第四行我們均引入了./icon-comment.svg模組,不同的是第四行的引入路徑我們還新增了與webpac配置中保持一致的url query = "abc"。同時,在下面我們分別列印了IconComment和IconCommentUrl。
  2. 在index.module.less中,.app樣式中,我們新增的背景也使用./icon-comment.svg,也新增了url query = "abc"。

程式碼執行以後,我們首先從UI上能夠看到效果:

130-svg-usecase-ui-display

其次,從控制檯也能看到對應的IconComment就是React函式元件;IconComment是svg資源的base64 DataUrl:

140-svg-usecase-console-output

demo地址

本文相關demo已提交至webpack5-react-demo的svg_use_case分支,供讀者參考:

w4ngzhen/webpack5-react-demo at svg_use_case (github.com)

相關文章