文章摘要
加载中...|
此内容根据文章生成,并经过人工审核,仅用于文章内容的解释与总结 投诉

1. 方案概述

本项目使用 @svgr/webpack 将 SVG 文件自动转换为 React 组件,实现图标的组件化管理。

核心优势

  • 自动化注册:无需手动 import 每个 SVG,放入指定目录即可使用
  • 组件化封装:提供统一的 <SvgIcon /> 组件,支持类型安全和代码补全
  • 灵活导入:支持两种导入方式:
    • 作为 React 组件使用(默认)
    • 作为 URL 使用(添加 ?url 后缀)
  • 高性能:构建时优化 SVG,运行时零网络请求

2. 安装依赖

在项目根目录安装 @svgr/webpack

bash
pnpm add -D @svgr/webpack

3. 配置文件

3.1 配置 next.config.ts

修改 next.config.ts 以配置 webpack 的 SVG 处理规则:

typescript
import type { NextConfig } from "next";
import createNextIntlPlugin from "next-intl/plugin";
import AutoImport from "unplugin-auto-import/webpack";
import { codeInspectorPlugin } from "code-inspector-plugin";

const withNextIntl = createNextIntlPlugin();

const nextConfig: NextConfig = {
  reactStrictMode: false,
  output: "standalone",
  compress: true,
  // ... 其他配置

  webpack: (config) => {
    // ... 其他 webpack 插件配置

    // SVGR 配置 - 将 SVG 文件转换为 React 组件
    const fileLoaderRule = config.module.rules.find((rule: any) => rule.test?.test?.(".svg"));

    if (fileLoaderRule) {
      // 排除 SVG 文件,不使用默认的 file-loader
      fileLoaderRule.exclude = /\\.svg$/i;
    }

    config.module.rules.push(
      // 为 *.svg?url 保留原有的文件加载规则
      {
        ...fileLoaderRule,
        test: /\\.svg$/i,
        resourceQuery: /url/, // 匹配 *.svg?url
      },
      // 将其他所有 *.svg 转换为 React 组件
      {
        test: /\\.svg$/i,
        issuer: fileLoaderRule.issuer,
        resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] },
        use: [
          {
            loader: "@svgr/webpack",
            options: {
              svgo: true, // 开启 SVG 优化
              dimensions: false, // 移除默认宽高,方便通过 props 或 CSS 控制
              svgoConfig: {
                plugins: [
                  {
                    name: "preset-default",
                    params: {
                      overrides: {
                        // 保留 viewBox 属性,确保 SVG 缩放正确
                        removeViewBox: false,
                      },
                    },
                  },
                ],
              },
            },
          },
        ],
      },
    );

    return config;
  },
};

export default withNextIntl(nextConfig);

3.2 添加 TypeScript 类型定义

创建 src/types/svg.d.ts 文件,为 SVG 模块添加类型支持:

typescript
declare module "*.svg" {
  import React from "react";
  const SVGComponent: React.FC<React.SVGProps<SVGSVGElement>>;
  export default SVGComponent;
}

4. 核心实现

4.1 创建 SVG 注册表 (svgRegistry.ts)

创建 src/components/SvgIcon/svgRegistry.ts,自动扫描并注册所有 SVG 图标:

typescript
/**
 * SVG 图标注册表
 * 自动导入 src/assets/images/svgs/ 目录下的所有 SVG 文件(包括子目录)
 */

import { ComponentType, SVGProps } from 'react';

// SVG 组件类型
export type SvgComponent = ComponentType<SVGProps<SVGSVGElement>>;

// SVG 注册表接口
export interface SvgRegistry {
    [key: string]: SvgComponent;
}

// 使用 require.context 自动导入所有 SVG 文件(包括子目录)
// @ts-ignore - require.context 是 webpack 提供的功能
const svgContext = require.context('@/assets/images/svgs', true, /\\.svg$/);

// 自动生成 SVG 注册表
export const svgRegistry: SvgRegistry = svgContext
    .keys()
    .reduce((registry: SvgRegistry, path: string) => {
        // 从路径中提取文件名并转换为图标名称
        // 例如:'./ai-camera.svg' -> 'ai-camera'
        //      './home/arrow.svg' -> 'home-arrow'
        const name = path
            .replace(/^\\.\\//, '')      // 移除开头的 ./
            .replace(/\\.svg$/, '')      // 移除 .svg 扩展名
            .replace(/\\//g, '-');       // 将 / 替换为 -

        // 导入并注册 SVG 组件
        const component = svgContext(path).default;
        registry[name] = component;

        return registry;
    }, {});

/**
 * 获取 SVG 组件
 * @param name - SVG 名称(不含 icon- 前缀)
 */
export const getSvgComponent = (name: string): SvgComponent | undefined => {
    return svgRegistry[name];
};

/**
 * 获取所有可用的 SVG 图标名称
 */
export const getAvailableSvgNames = (): string[] => {
    return Object.keys(svgRegistry);
};

// 开发环境下打印所有可用图标
if (process.env.NODE_ENV === 'development') {
    console.log('[SVG Registry] 已加载的图标:', getAvailableSvgNames().sort());
}

4.2 创建 SvgIcon 组件

创建 src/components/SvgIcon/index.tsx,提供统一的图标使用接口:

typescript
import React from 'react';
import { getSvgComponent } from './svgRegistry';

export interface SvgIconProps {
    /**
     * 图标名称
     * 格式:icon-{文件名}
     * 例如:icon-play, icon-pause
     */
    name: string;
    /**
     * 图标大小(宽高相同)
     * @default 24
     */
    size?: number;
    /**
     * 自定义类名
     */
    className?: string;
    /**
     * 自定义样式
     */
    style?: React.CSSProperties;
}

/**
 * 通用 SVG 图标组件
 *
 * 使用方式:
 * ```tsx
 * <SvgIcon name="icon-play" size={24} className="text-blue-500" />
 * ```
 */
const SvgIcon: React.FC<SvgIconProps> = ({
    name,
    size = 24,
    className = '',
    style
}) => {
    // 移除 icon- 前缀获取实际的 SVG 名称
    const iconName = name.replace(/^icon-/, '');

    // 从注册表中获取 SVG 组件
    const SvgComponent = getSvgComponent(iconName);

    // 如果找不到对应的 SVG 组件,返回 null 并在开发环境下输出警告
    if (!SvgComponent) {
        if (process.env.NODE_ENV === 'development') {
            console.warn(
                `[SvgIcon] Icon not found: ${iconName}. ` +
                `Make sure it's registered in svgRegistry.ts`
            );
        }
        return null;
    }

    // 渲染 SVG 组件
    return (
        <SvgComponent
            width={size}
            height={size}
            className={className}
            style={style}
        />
    );
};

export default SvgIcon;

5. 使用指南

5.1 添加图标

将 SVG 文件放入 src/assets/images/svgs/ 目录(支持子目录):

src/assets/images/svgs/
├── play.svg           -> icon-play
├── pause.svg          -> icon-pause
└── home/
    └── arrow.svg      -> icon-home-arrow

5.2 使用 SvgIcon 组件

tsx
import SvgIcon from "@/components/SvgIcon";

export default function MyComponent() {
  return (
    <div className="flex gap-4">
      {/* 基础用法 */}
      <SvgIcon name="icon-play" />

      {/* 自定义大小 */}
      <SvgIcon name="icon-pause" size={32} />

      {/* 使用 Tailwind CSS 样式 */}
      <SvgIcon name="icon-home-arrow" className="w-8 h-8 text-blue-500" />
    </div>
  );
}

5.3 直接导入为 React 组件(可选)

如果需要直接导入 SVG 作为组件:

tsx
import PlayIcon from "@/assets/images/svgs/play.svg";

export default function MyComponent() {
  return <PlayIcon width={24} height={24} className="text-red-500" />;
}

5.4 导入为 URL(可选)

如果需要导入 SVG 文件的 URL:

tsx
import playIconUrl from "@/assets/images/svgs/play.svg?url";

export default function MyComponent() {
  return <img src={playIconUrl} alt="Play" />;
}

6. 文件组织规范

6.1 命名规范

  • SVG 文件名使用 kebab-case 命名(小写字母 + 连字符)
  • 例如:play-icon.svguser-profile.svg

6.2 目录结构

建议按功能或类别组织 SVG 文件:

src/assets/images/svgs/
├── actions/          # 操作类图标
│   ├── play.svg
│   └── pause.svg
├── navigation/       # 导航类图标
│   ├── home.svg
│   └── back.svg
└── social/          # 社交媒体图标
    ├── facebook.svg
    └── twitter.svg

7. 性能分析

7.1 构建优化

  • SVGO 优化:配置中已启用 svgo: true,会自动:
    • 移除无用的元数据和注释
    • 优化路径数据
    • 合并重复的元素
    • 减小文件体积

7.2 运行时性能

  • 零网络请求:所有 SVG 都被编译为 React 组件,无需额外的 HTTP 请求
  • Tree Shaking:未使用的 SVG 图标可以被 webpack 移除
  • 查找效率:图标通过哈希表存储,查找时间复杂度 O(1)

7.3 打包体积

  • 机制require.context 会将所有 SVG 打包到主 bundle 中
  • 建议
    • 定期清理不再使用的图标
    • 对于超大型图标库(>1000 个),考虑按需加载
    • 使用 bundle analyzer 监控图标包大小

8. 故障排查

8.1 图标不显示

原因:图标名称错误或文件未正确导入

解决方案

  1. 检查控制台警告信息
  2. 确认图标文件路径正确
  3. 查看开发环境下打印的已加载图标列表

8.2 TypeScript 报错

原因:类型定义缺失

解决方案

  • 确保 src/types/svg.d.ts 文件存在
  • 重启 TypeScript 服务器

8.3 样式不生效

原因:SVG 内部可能包含固定的 fill/stroke 属性

解决方案

  • 编辑 SVG 文件,移除固定的颜色属性
  • 使用 currentColor 值以支持 CSS 控制

9. 最佳实践

  1. 统一使用 SvgIcon 组件:保持代码一致性
  2. 合理组织目录结构:按功能分类便于维护
  3. 定期清理:移除不再使用的图标
  4. 优化 SVG 源文件:在添加前使用 SVGO 工具预先优化
  5. 使用语义化命名:图标名称应清晰表达用途

10. 参考资源

赞赏博主
评论 隐私政策