【问题标题】:How to declare types of props of SVG component? [React, TypeScript and Webpack]如何声明 SVG 组件的 props 类型? [React、TypeScript 和 Webpack]
【发布时间】:2020-10-24 04:14:01
【问题描述】:

基本上,我想做的是将一个 SVG 图标导入我的 react 组件并为其添加道具。喜欢size="24px" 使其作为组件更加灵活。或者通过添加 className 道具使其可以使用 CSS 进行编辑(这样我可以添加例如悬停道具)。 由于这是我第一次将 TypeScript 与 Webpack 一起使用,所以我对如何为 SVG 元素声明类型感到困惑,并且出现错误(如下所示)

由于包含 SVG 的方式有很多种,我决定将其作为 ReactComponent 导入。

菜单图标.svg

<svg width="24" height="24" viewBox="0 0 24 24">
  <path fill="currentColor" fillRule="evenodd" d="M4.5 5h15a.5.5 0 1 1 0 1h-15a.5.5 0 0 1 0-1zm0 6h15a.5.5 0 1 1 0 1h-15a.5.5 0 1 1 0-1zm0 6h15a.5.5 0 1 1 0 1h-15a.5.5 0 1 1 0-1z"></path>
</svg>

header.tsx(这里我想要我的 svg 图标)

import React from 'react';
import MenuIcon from '../assets/menu-icon.svg';

const Header: React.SFC = () => {
  return (
    <header className="c-header u-side-paddings">
      <MenuIcon className="c-header__icon" />  // <-- className prop doesn't match provided type
    </header>
  );
};

export default Header;

index.d.ts(因此 .svg 文件可以被视为一个组件)

declare module '*.svg' {
  import React = require('react');
  export const ReactComponent: React.SFC<React.SVGProps<SVGSVGElement>>;
  const src: string;
  export default src;
}

向 MenuIcon SVG 组件添加 className 属性会导致错误:

(JSX attribute) className: string
Type '{ className: string; }' is not assignable to type 'IntrinsicAttributes'.
  Property 'className' does not exist on type 'IntrinsicAttributes'.ts(2322)

到目前为止我的理解

  • 我可以将 svg 组件包装在一个 div 中,然后像这样在其中添加一个 className&lt;div className="c-header__icon"&gt;&lt;MenuIcon/&gt;&lt;/div&gt;,但我觉得这是一个不优雅的解决方案,并不是一个很好的做法
  • 我从this answer 了解到SVG 道具不是字符串,因为它们是SVGAnimatedString 对象。所以:
  • 我尝试创建 .tsx 文件而不是 .svg(那时我不需要 index.d.ts 文件),但它仅在 className 的类型为 string 时才有效。此外,我不确定将 SVG 图标存储在具有不同扩展名 .svg 的文件中是否是一个好习惯。在我看来,这不利于清晰。如果我错了,请告诉我真正的好做法是什么。示例如下:
    import React from 'react';
    
    interface MenuIcon {
      className?: SVGAnimatedString;
    }
    
    export class MenuIcon extends React.PureComponent<MenuIcon> {
      render() {
        return (
          <svg width="24" height="24" viewBox="0 0 24 24">
      <path fill="currentColor" fillRule="evenodd" d="M4.5 5h15a.5.5 0 1 1 0 1h-15a.5.5 0 0 1 0-1zm0 
 6h15a.5.5 0 1 1 0 1h-15a.5.5 0 1 1 0-1zm0 6h15a.5.5 0 1 1 0 1h-15a.5.5 0 1 1 0-1z"></path>
    </svg>
        );
      }
    }

我觉得我缺乏一些基础知识,我真的很难弄清楚我应该关注什么,因为有几个主题组合在一起

【问题讨论】:

    标签: reactjs typescript svg webpack


    【解决方案1】:

    我一直面临这个问题,这个解决方案对我来说很好:

    declare module "*.svg" {
      import { ReactElement, SVGProps } from "react";
      const content: (props: SVGProps<SVGElement>) => ReactElement;
      export default content;
    }

    此外,我使用 @svgr/webpack 作为 SVG 加载器以及 Next.js

    【讨论】:

    • 谢谢,完美运行,顺便说一句,你能精确地为 Next.js + TypeScript 配置你的 svgr 吗?我想知道是否需要为 svgr 使用任何打字稿选项。谢谢!
    • 将它添加到您的 typescript 项目根文件夹中的 index.d.ts 上
    【解决方案2】:

    您可以参数化 svg,以便通过全局抽象轻松自定义 JSX.Element 级别。如何?使用 React 的 FC。尝试执行以下操作:

    • 创建如下界面
    interface SvgIconConstituentValues {
        strokeColor?: string;
        strokeWidth?: string;
        strokeWidth2?: string;
        strokeWidth3?: string;
        strokeFill?: string;
        fillColor?: string;
        fillColor2?: string;
        fillColor3?: string;
        fillColor4?: string;
        fillColor5?: string;
        fillColor6?: string;
        fillColor7?: string;
        imageWidth?: string;
        imageHeight?: string;
        width?: string;
        height?: string;
        rotateCenter?: number;
        className?: string;
        className2?: string;
        className3?: string;
        className4?: string;
        className5?: string;
    }
    
    export default SvgIconConstituentValues;
    
    • SvgIconConstituentValues 导入到 tsx 文件中
    • { FC } 从 React 导入到同一个 tsx 文件中
    import { FC } from 'react';
    import SvgIconConstituentValues from 'types/svg-icons';
    
    // FC can be parameterized via Abstraction
    
    • 创建一个扩展FCSvgIconConstituentValuesSvgIcon 接口
    export interface SvgIcon extends FC<SvgIconConstituentValues> {}
    
    • 使用SvgIcon 参数化抽象svg 的属性,如下所示
    export const ArIcon: SvgIcon = ({
        width = '8.0556vw',
        height = '8.0556vw',
        strokeColor = `stroke-current`,
        strokeWidth = '2',
        fillColor = 'none',
        fillColor2 = `fill-primary`,
        rotateCenter = 0,
        className = ` antialiased w-svgIcon max-w-svgIcon`,
        className2 = ` stroke-current`,
        className3 = ` fill-primary`
    }): JSX.Element => {
        return (
            <svg
                width={width}
                height={height}
                viewBox='0 0 65 65'
                fill={fillColor}
                xmlns='http://www.w3.org/2000/svg'
                className={className}
                transform={`rotate(${rotateCenter}, 65, 65)`}
            >
                <circle
                    cx='32.5'
                    cy='32.5'
                    r='31.5'
                    stroke={strokeColor}
                    strokeWidth={strokeWidth}
                    className={className2}
                />
                <path
                    d='M30.116 39H32.816L27.956 26.238H25.076L20.18 39H22.808L23.87 36.084H29.054L30.116 39ZM26.462 28.992L28.226 33.816H24.698L26.462 28.992ZM40.7482 39H43.5202L40.7842 33.78C42.4582 33.294 43.5022 31.944 43.5022 30.162C43.5022 27.948 41.9182 26.238 39.4342 26.238H34.4482V39H36.9502V34.086H38.2462L40.7482 39ZM36.9502 31.944V28.398H38.9662C40.2262 28.398 40.9642 29.1 40.9642 30.18C40.9642 31.224 40.2262 31.944 38.9662 31.944H36.9502Z'
                    fill={fillColor2}
                    className={className3}
                />
            </svg>
        );
    };
    
    • 如您所见,抽象了三个独立的className参数(1、2、3):(1)className for &lt;svg&gt;...&lt;/svg&gt;,property JSX.IntrinsicElements.svg: SVGProps&lt;SVGSVGElement&gt;; (2)&lt;circle /&gt;属性JSX.IntrinsicElements.circle: SVGProps&lt;SVGCircleElement&gt;的className2; (3) &lt;path /&gt; 属性 JSX.IntrinsicElements.path: SVGProps 的 className3。

    • 请注意,const ArIcon: SvgIcon = ({ ... }): JSX.Element =&gt; {...} 确实是一个 JSX.Element。因此,&lt;svg&gt;&lt;/svg&gt; 本身和任何子节点(圆、路径等)都是JSX.IntrinsicElements,每个都允许有自己唯一的className。这些 className 调用是手动添加到 svg 中的,transform 调用也是如此(将内联图标旋转到别处)。

    • JSX.IntrinsicElementsJSX Attribute className 定义如下

    SVGAttributes<T>.className?: string | undefined
    
    • 每个 JSX.IntrinsicElement 都有权使用 className 属性。一个 svg 中有 100 条路径和一个圆圈?您可以拥有 102 个可以通过抽象参数化的类名。

    • 现在是最好的部分。以下来自我的投资组合中的一个文件,我对抽象 svg 参数进行了修改,以使其与暗模式切换 (use-dark-mode) 和屏幕宽度相关的图标渲染 (@artsy/fresnel) 配合得很好。您可以全局导入此图标并在每个 JSX.Element 内内联调用参数,而无需传递任何道具

    import { ArIcon } from 'components/svg-icons';
    import Link from 'next/link';
    import { Media } from 'components/window-width';
    import { Fragment } from 'react';
    import DarkMode from 'components/lead-dark-mode';
    
    const ArIconConditional = (): JSX.Element => {
        const arIconXs: JSX.Element = (
            <Media at='xs'>
                <Link href='/'>
                    <a
                        className='container block pl-portfolio pt-portfolio justify-between mx-auto w-full min-w-full '
                        id='top'
                        aria-label='top'
                    >
                        <ArIcon width='18vw' height='18vw' className='transition-all transform translate-y-90' 
     className2='transition-all duration-1000 delay-200 transform' className3='text-secondary fill-secondary' />
                    </a>
                </Link>
            </Media>
        );
    
        const arIconSm: JSX.Element = (
            <Media at='sm'>
                <Link href='/'>
                    <a
                        className='container block pl-portfolio pt-portfolio justify-between mx-auto w-full min-w-full '
                        id='top'
                        aria-label='top'
                    >
                        <ArIcon width='15vw' height='15vw' className='' className2='' className3='' />
                    </a>
                </Link>
            </Media>
        );
    
        const arIconMd: JSX.Element = (
            <Media at='md'>
                <Link href='/'>
                    <a
                        className='container block pl-portfolio pt-portfolio justify-between mx-auto w-full min-w-full '
                        id='top'
                        aria-label='top'
                    >
                        <ArIcon width='12.5vw' height='12.5vw' className='' className2='' className3='' />
                    </a>
                </Link>
            </Media>
        );
    
        const arIconDesktop: JSX.Element = (
            <Media greaterThan='md'>
                <Link href='/'>
                    <a
                        className='container block pl-portfolio pt-portfolio justify-between mx-auto w-full min-w-full '
                        id='top'
                        aria-label='top'
                    >
                        <ArIcon width='10vw' height='10vw' className='' className2='' className3='' />
                    </a>
                </Link>
            </Media>
        );
    
        const ArIconsCoalesced = (): JSX.Element => (
            <Fragment>
                <div className='relative block justify-between lg:w-auto lg:static lg:block lg:justify-start transition-all w-full min-w-full col-span-5'>
                    {arIconXs}
                    {arIconSm}
                    {arIconMd}
                    {arIconDesktop}
                </div>
            </Fragment>
        );
        return (
            <Fragment>
                <div className='select-none relative z-1 justify-between pt-portfolioDivider navbar-expand-lg grid grid-cols-6 min-w-full w-full container overflow-y-hidden overflow-x-hidden transform'>
                    <ArIconsCoalesced />
                    <div className='pt-portfolio'>
                        <DarkMode />
                    </div>
                </div>
            </Fragment>
        );
    };
    
    export default ArIconConditional;
    
    
    • 这个project 使用tailwindcss 和React 的Next.js 框架。也就是说,如果我希望包含图标的JSX.IntrinsicElement 圆圈仅在移动设备上脉冲呢?添加顺风的animate-pulseclassName2如下
    // ...
        const arIconXs: JSX.Element = (
            <Media at='xs'>
                <Link href='/'>
                    <a
                        className='container block pl-portfolio pt-portfolio justify-between mx-auto w-full min-w-full '
                        id='top'
                        aria-label='top'
                    >
                        <ArIcon width='18vw' height='18vw' className='transition-all transform translate-y-90' 
     className2='transition-all duration-1000 delay-200 transform animate-pulse' className3='text-secondary fill-secondary' />
                    </a>
                </Link>
            </Media>
        );
    // ...
    
    • fill-primary 调用是为 .dark-mode.light-mode css 类定义的 css 变量,然后将其传递给 :root 并在客户端 (onChange={darkMode.toggle}) 切换暗模式时激活。

    • 因此,onClick={darkMode.enable} 触发图标以更改其 fillColor 和 strokeColor 值作为 css 变量的函数。使用 React 的 FC 通过抽象来参数化 props 会产生真正卓越的粒度控制。在全局范围内使用 JSX.Element 级别的内联调用自定义 SVG 从未如此无缝。

    • darkMode.disable

    • darkMode.enable

    • 查看我的recent DEV post,如果使用 typescript 破解 react-fontawesome 库,然后创建自定义 fontawesome SVG 图标,这些图标会持续到生产环境并且不会因库版本更新而改变,这激起了您的兴趣。

    干杯

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-11-26
      • 2020-09-19
      • 2016-07-22
      • 1970-01-01
      • 1970-01-01
      • 2023-04-04
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多