← 返回所有内容
Use svg sprite icons in React
使用 React 中的 svg 图标精灵
There are many ways to use svg icons in a React app. The most intuitive, and also the worst, is to write the svg code directly in a component as JSX.
有许多方法可以在 React 应用中使用 svg 图标。最直观,也是最糟糕的方法是将 svg 代码直接写在组件中的 JSX 中。
I won't go into all the reasons why, as other folks have already done a great job of explaining it, but in general it's inefficient and increases the size of your bundle dramaticallly.
我不会详细说明原因,因为其他人已经很好地解释了这一点,但总的来说,这效率低下,并且会极大地增加你的包的大小。
The best way to use icons is an svg spritesheet.
最佳使用图标的方式是使用 svg 精灵图。
A spritesheet is a single image that contains many sprites (icons in our case). This is one of the oldest tactics to optimize image loading. Instead of loading many small images, we load a single image and use code to display only the part we need.
精灵图集是一个包含许多精灵(在我们的案例中是图标)的单个图像。这是优化图像加载的最古老策略之一。我们不是加载许多小图像,而是加载一个单独的图像,并使用代码来显示我们需要的部分。
Video games have used spritesheets for decades to cram each frame of an animation into a single memory efficient resource.
视频游戏已经使用了数十年的精灵图集,将每个动画帧压缩成单个内存高效的资源。
Websites can use the same tactic to store many icons in a single svg file. This is called an svg spritesheet. Each icon sprite is stored as a <symbol>
element inside the svg file. We can display a specific icon by using the <use>
element and referencing the icon's id.
网站可以使用相同的策略在单个 svg 文件中存储许多图标。这被称为 svg 精灵图。每个图标精灵都存储在 svg 文件内的 <symbol>
元素中。我们可以通过使用 <use>
元素并引用图标的 id 来显示特定的图标。
<svg> <defs> <symbol id="icon1" viewBox="0 0 24 24"> <path d="..." /> </symbol> <symbol id="icon2" viewBox="0 0 24 24"> <path d="..." /> </symbol> </defs></svg><svg> <use href="#icon1" /></svg>
In this article, we will build a script that compiles a folder of svg icons into a single svg spritesheet. We will also build a React component that displays a specific icon by name with full type safe autocomplete for the available icons.
在这篇文章中,我们将构建一个脚本,将一个文件夹中的 svg 图标编译成一个单一的 svg 精灵图。我们还将构建一个 React 组件,通过名称显示特定的图标,并提供对可用图标的完整类型安全自动完成。
Create a folder for icons
创建图标文件夹
Create a folder called svg-icons
in the root of your project and add some SVGs.
在您的项目根目录下创建一个名为 svg-icons
的文件夹,并添加一些 SVG 图像。
You can use Sly CLI to download icons from the command line. This command will add the camera and card-stack icons from Radix UI to the svg-icons
folder.
您可以使用 Sly CLI 从命令行下载图标。此命令将 Radix UI 中的相机和卡片堆叠图标添加到 svg-icons
文件夹中。
npx @sly-cli/sly add @radix-ui/icons camera card-stack
To browse all the available icons, run npx @sly-cli/sly add
and follow the interactive menu. If you install Sly as a dev dependency, you can use the shorter npx sly add
command.
要浏览所有可用的图标,运行 npx @sly-cli/sly add
并按照交互式菜单操作。如果您将 Sly 作为开发依赖项安装,可以使用更短的 npx sly add
命令。
Create a list of icons in your app
创建您应用中的图标列表
The first step is to read the icons folder and get a list of all the svg files. There are a few ways to do this, such as by recursively walking through the directory and reading the filenames, but I prefer to use the glob
package.
第一步是读取图标文件夹并获取所有 svg 文件的列表。有几种方法可以实现,例如通过递归遍历目录并读取文件名,但我更喜欢使用 glob
包。
Create a file called build-icons.ts
创建一个名为 build-icons.ts
的文件
import { promises as fs } from "node:fs"import * as path from "node:path"import { glob } from "glob"import { parse } from "node-html-parser"const cwd = process.cwd()const inputDir = path.join(cwd, "svg-icons")const inputDirRelative = path.relative(cwd, inputDir)const outputDir = path.join( cwd, "app", "components", "icons",)const outputDirRelative = path.relative(cwd, outputDir)const files = glob .sync("**/*.svg", { cwd: inputDir, }) .sort((a, b) => a.localeCompare(b))if (files.length === 0) { console.log(`No SVG files found in ${inputDirRelative}`) process.exit(0)}// The relative paths are just for cleaner logsconsole.log(`Generating sprite for ${inputDirRelative}`)
Compile icons into a single spritesheet
将图标编译成单个精灵图
Instead of many distinct SVGs, each icon will become a <symbol>
element inside a single SVG file.
而不是许多不同的 SVG,每个图标都将成为单个 SVG 文件内的一个 <symbol>
元素。
SVGs often have some extra attributes that we don't need on our symbol, such as xmlns
and version
. We can remove them with the node-html-parser
package.
SVGs 经常有一些我们不需要的额外属性,例如 xmlns
和 version
。我们可以使用 node-html-parser
包来移除它们。
We'll also remove the width and height attributes from the root <svg>
element so that we can control the size of the icon with CSS.
我们将从根 <svg>
元素中移除宽度和高度属性,以便我们可以使用 CSS 来控制图标的大小。
const spritesheetContent = await generateSvgSprite({ files, inputDir,})await writeIfChanged( path.join(outputDir, "sprite.svg"), spritesheetContent,)/** * Outputs an SVG string with all the icons as symbols */async function generateSvgSprite({ files, inputDir,}: { files: string[] inputDir: string}) { // Each SVG becomes a symbol and we wrap them all in a single SVG const symbols = await Promise.all( files.map(async (file) => { const input = await fs.readFile( path.join(inputDir, file), "utf8", ) const root = parse(input) const svg = root.querySelector("svg") if (!svg) throw new Error("No SVG element found") svg.tagName = "symbol" svg.setAttribute("id", file.replace(/\.svg$/, "")) svg.removeAttribute("xmlns") svg.removeAttribute("xmlns:xlink") svg.removeAttribute("version") svg.removeAttribute("width") svg.removeAttribute("height") return svg.toString().trim() }), ) return [ `<?xml version="1.0" encoding="UTF-8"?>`, `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="0" height="0">`, `<defs>`, // for semantics: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/defs ...symbols, `</defs>`, `</svg>`, ].join("\n")}/** * Each write can trigger dev server reloads * so only write if the content has changed */async function writeIfChanged( filepath: string, newContent: string,) { const currentContent = await fs.readFile(filepath, "utf8") if (currentContent !== newContent) { return fs.writeFile(filepath, newContent, "utf8") }}
If you run this script now, it will create a single sprite.svg
file in the app/components/icons
folder.
如果您现在运行此脚本,它将在 app/components/icons
文件夹中创建一个单独的 sprite.svg
文件。
Create a React component that displays an icon
创建一个显示图标的 React 组件
There are two ways to host an asset in a Remix app
有两种方式在 Remix 应用中托管资产
- Place it in the
/public
folder and it will be served as a static asset by the server
将其放置在/public
文件夹中,服务器将将其作为静态资源提供 - Or import it as a module and it will be hashed and fingerprinted and served from the
/public/build
folder
或者将其作为模块导入,它将被哈希和指纹化,并从/public/build
文件夹中提供服务
I prefer the second option because we can cache it indefinitely. Every time we add new icons or change the existing ones, Remix will serve it with a different hash and the users browsers will download the new version.
我更喜欢第二个选项,因为我们可以无限期地缓存它。每次我们添加新图标或更改现有图标时,Remix 都会用不同的哈希值提供服务,用户的浏览器将下载新版本。
So now we can create a React component called Icon.tsx
and import the svg file to use it as the href
of the <use>
element.
现在我们可以创建一个名为 Icon.tsx
的 React 组件,并将 svg 文件导入以用作 href
元素的 <use>
。
To choose a specific icon, we add #id
to the end of the href
attribute. The id
is the name of the file without the .svg
extension, which was set in the generateSvgSprite
function.
为了选择特定的图标,我们在 href
属性的末尾添加 #id
。 id
是不带 .svg
扩展名的文件名,该文件名是在 generateSvgSprite
函数中设置的。
import { type SVGProps } from "react"import spriteHref from "~/app/components/icons/sprite.svg"export function Icon({ name, ...props}: SVGProps<SVGSVGElement> & { name: string}) { return ( <svg {...props}> <use href={`${spriteHref}#${name}`} /> </svg> )}
You can use the component like this
您可以使用该组件如下
<Icon name="camera" className="w-5 h-5" />
Provide fallback types for the Icon component
提供 Icon 组件的回退类型
We can improve the developer experience of the Icon by providing a list of all available icons to the type of the name
attribute. That will allow your editor to autocomplete the icon names, and throw errors if you try to use an icon that doesn't exist.
我们可以通过提供一个所有可用图标列表到 name
属性的类型,来提升 Icon 的开发者体验。这将允许您的编辑器自动完成图标名称,并在尝试使用不存在的图标时抛出错误。
We can generate the type automatically by reading the list of files in the svg-icons
folder and using the filename as the key.
我们可以通过读取 svg-icons
文件夹中的文件列表,并使用文件名作为键来自动生成类型。
Go back to the build-icons.ts
file and continue the script from right after the generateSvgSprite
function.
返回到 build-icons.ts
文件,并从 generateSvgSprite
函数之后继续脚本。
We already have the list of files, so we can just map them into the format we want and use JSON.stringify() to convert it to JSON, then print them as types.
我们已经有文件列表,所以我们可以直接将它们映射成我们想要的格式,并使用 JSON.stringify()将其转换为 JSON,然后按类型打印它们。
const typesContent = await generateTypes({ names: files.map((file) => JSON.stringify((file) => file.replace(/\.svg$/, "")), ),})await writeIfChanged( path.join(outputDir, "names.ts"), typesContent,)async function generateTypes({ names,}: { names: string[]}) { return [ `// This file is generated by npm run build:icons`, "", `export type IconName =`, ...names.map((name) => `\t| ${name}`), "", ].join("\n")}
Run the script automatically
自动运行脚本
Add a build:icons
script to your package.json file so you can run it with npm run build:icons
.
添加一个 build:icons
脚本到您的 package.json 文件中,以便您可以使用 npm run build:icons
运行它。
{ "scripts": { "build:icons": "tsx ./build-icons.ts" }}
If you try running this file now, you should see the generated types in names.ts
file update to include the names of all the icons you use, but it will not show up in git as a modified file.
如果您现在尝试运行此文件,您应该会看到在 names.ts
文件中生成的类型更新,包括您使用的所有图标名称,但在 git 中不会显示为已修改的文件。
If you're using Sly CLI, set the postinstall script in your sly.json
file so that it runs automatically when you add new icons.
如果您正在使用 Sly CLI,请将您的 sly.json
文件中的 postinstall 脚本设置好,以便在您添加新图标时自动运行。
{ "$schema": "https://sly-cli.fly.dev/registry/config.json", "libraries": [ { "name": "@radix-ui/icons", "directory": "./svg-icons", "postinstall": ["npm", "run", "build:icons"], "transformers": ["transform-icon.ts"] } ]}
Update the Icon component to use the new type
更新图标组件以使用新类型
import { type IconName } from "~/app/components/icons/names.ts"// and update the Icon component type to use itSVGProps<SVGSVGElement> & { name: IconName}
Try using the Icon component with an icon you don't have yet, like arrow-left
. You should get an error in your editor.
尝试使用尚未拥有的图标组件,如 arrow-left
。你应该在编辑器中收到一个错误。
<Icon name="arrow-left" className="w-5 h-5" />
Then run npx sly add @radix-ui/icons arrow-left
and the error should go away instantly as the script runs and adds the new icon to the spritesheet.
然后运行 npx sly add @radix-ui/icons arrow-left
,错误应该会立即消失,因为脚本运行并添加了新的图标到精灵图中。
Preload svg sprite 预加载 svg 精灵
The last step is optional, but if you preload the svg sprite as a resource, it will start downloading immediately and the browser will have it ready by the time you need to display an icon.
最后一步是可选的,但如果您将 svg 精灵作为资源预加载,它将立即开始下载,浏览器将在您需要显示图标时准备好它。
In Remix, you can do this by adding a links
function to your route file.
在 Remix 中,您可以通过在路由文件中添加一个 links
函数来实现这一点。
import iconHref from "~/icon.svg"export const links: LinksFunction = () => { return [ // Preload svg sprite as a resource to avoid render blocking { rel: "preload", href: iconHref, as: "image", type: "image/svg+xml", }, ]}