如何动态导入SVG并内联渲染

IT技术 javascript reactjs webpack webpack-4
2021-04-19 13:00:37

我有一个函数,它接受一些参数并呈现一个 SVG。我想根据传递给函数的名称动态导入该 svg。它看起来像这样:

import React from 'react';

export default async ({name, size = 16, color = '#000'}) => {
  const Icon = await import(/* webpackMode: "eager" */ `./icons/${name}.svg`);
  return <Icon width={size} height={size} fill={color} />;
};

根据动态导入webpack 文档和神奇的注释“eager”:

“不生成额外的块。所有module都包含在当前块中,并且不会发出额外的网络请求。仍然返回 Promise,但已经解决。与静态导入相比,module在调用导入之前不会执行() 制成。”

这就是我的图标所解决的问题:

> Module
default: "static/media/antenna.11b95602.svg"
__esModule: true
Symbol(Symbol.toStringTag): "Module"

试图以我的函数试图给我这个错误的方式呈现它:

Objects are not valid as a React child (found: [object Promise]). If you meant to render a collection of children, use an array instead.

我不明白如何使用这个导入的 Module 将它呈现为一个组件,或者甚至有可能这样吗?

4个回答

您可以使用refReactComponent导入SVG文件时命名为出口。请注意,它必须是ref为了它的工作。

以下示例使用需要版本v16.8及更高版本的 React 钩子

示例动态 SVG 导入挂钩:

function useDynamicSVGImport(name, options = {}) {
  const ImportedIconRef = useRef();
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState();

  const { onCompleted, onError } = options;
  useEffect(() => {
    setLoading(true);
    const importIcon = async () => {
      try {
        ImportedIconRef.current = (
          await import(`./${name}.svg`)
        ).ReactComponent;
        if (onCompleted) {
          onCompleted(name, ImportedIconRef.current);
        }
      } catch (err) {
        if (onError) {
          onError(err);
        }
        setError(err);
      } finally {
        setLoading(false);
      }
    };
    importIcon();
  }, [name, onCompleted, onError]);

  return { error, loading, SvgIcon: ImportedIconRef.current };
}

编辑 react-dynamic-svg-import

typescript中的动态 SVG 导入钩子示例:

interface UseDynamicSVGImportOptions {
  onCompleted?: (
    name: string,
    SvgIcon: React.FC<React.SVGProps<SVGSVGElement>> | undefined
  ) => void;
  onError?: (err: Error) => void;
}

function useDynamicSVGImport(
  name: string,
  options: UseDynamicSVGImportOptions = {}
) {
  const ImportedIconRef = useRef<React.FC<React.SVGProps<SVGSVGElement>>>();
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<Error>();

  const { onCompleted, onError } = options;
  useEffect(() => {
    setLoading(true);
    const importIcon = async (): Promise<void> => {
      try {
        ImportedIconRef.current = (
          await import(`./${name}.svg`)
        ).ReactComponent;
        onCompleted?.(name, ImportedIconRef.current);
      } catch (err) {
        onError?.(err);
        setError(err);
      } finally {
        setLoading(false);
      }
    };
    importIcon();
  }, [name, onCompleted, onError]);

  return { error, loading, SvgIcon: ImportedIconRef.current };
}

编辑 react-dynamic-svg-import-ts


对于那些谁是得到undefinedReactComponent时,SVG动态进口的,这是由于一个错误,其中的WebPack插件添加ReactComponent到以某种方式进口不会触发动态进口各SVG。

基于此解决方案,我们可以通过在动态 SVG 导入中强制使用相同的加载器来临时解决该问题。

唯一的区别是ReactComponent现在是default输出。

ImportedIconRef.current = (await import(`!!@svgr/webpack?-svgo,+titleProp,+ref!./${name}.svg`)).default;

另请注意,使用带有可变部分的动态导入时存在限制。这个 SO 答案详细解释了这个问题。

要解决此问题,您可以使动态导入路径更加明确。

例如,代替

// App.js
<Icon path="../../icons/icon.svg" />

// Icon.jsx
...
import(path);
...

你可以把它改成

// App.js
<Icon name="icon" />

// Icon.jsx
...
import(`../../icons/${name}.svg`);
...
哇!谢谢,很干净!我完全错过了任何文档中的 ReactComponent。我认为它会在检查时显示为导入对象的公共方法,恕我直言,但我想这不是这里的工作方式。感谢帮助!
2021-05-29 13:00:37
@dev_junwen 不幸的是,这个解决方案对我不起作用。我什么都试过了。即使我从 CodeSandbox 下载代码并从头开始安装所有依赖项npm install,也不会加载 SVG 图像。我也没有收到错误消息。我做错了什么(可能是 webpack、babel 或 typescript 配置)?
2021-06-01 13:00:37
@mamiu 你在使用 Create React App 吗?如果不是,您将需要手动设置内联 svg 加载器。这个答案可能会有所帮助。
2021-06-09 13:00:37
@Willege 我已经更新了答案。请随意查看代码示例。
2021-06-10 13:00:37
顺便说一句@dev_junwen ReactComponent 从来没有为我工作过,我不知道这段代码在你的 CodeSandbox 中是如何工作的,但.ReactComponent只是为我返回 undefined。我不得不改变它并使用.default下面@Enchew 的例子。如果您转到“导入默认值”,我会在这里找到一些很棒的文档:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/...
2021-06-14 13:00:37

您的渲染函数(对于类组件)和函数组件不应该是异步的(因为它们必须返回 DOMNode 或 null - 在您的情况下,它们返回一个 Promise)。相反,您可以以常规方式渲染它们,然后导入图标并在下一次渲染中使用它。请尝试以下操作:

const Test = () => {
  let [icon, setIcon] = useState('');

  useEffect(async () => {
    let importedIcon = await import('your_path');
    setIcon(importedIcon.default);
  }, []);

  return <img alt='' src={ icon }/>;
};
效果不能承载async功能。它们返回一个作为清理函数调用的Promise。robinwieruch.de/react-hooks-fetch-data
2021-05-25 13:00:37
为什么需要将其渲染为<Icon>这种方法给你什么,有什么区别,不能通过使用来克服<img>
2021-06-07 13:00:37
不管最初的问题是什么,这可能是“动态导入本地文件”的最佳答案。当我有一个文件 URL 列表并动态加载时,这很有效。
2021-06-09 13:00:37
这可能适用于源将是 importIcon.default 的 img 标记,如您所写,但在我的情况下,它不适用于内联 svg,我想将其呈现为 <Icon />。我用异步 useEffect 尝试了你的方法,但后来我需要setIcon使用整个module,而不是importIcon.default (文件路径),然后像 <Icon /> 一样渲染它,它给了我错误Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.
2021-06-18 13:00:37
如果 svg 是 img 标签,例如图标的填充颜色,我将如何更改它的属性?可以用img标签吗?
2021-06-18 13:00:37

我根据答案进行了更改https://github.com/facebook/create-react-app/issues/5276#issuecomment-665628393

export const Icon: FC<IconProps> = ({ name, ...rest }): JSX.Element | null => {
      const ImportedIconRef = useRef<FC<SVGProps<SVGSVGElement>> | any>();
      const [loading, setLoading] = React.useState(false);
      useEffect((): void => {
        setLoading(true);
        const importIcon = async (): Promise<void> => {
          try {
            // Changing this line works fine to me
            ImportedIconRef.current = (await import(`!!@svgr/webpack?-svgo,+titleProp,+ref!./${name}.svg`)).default;
          } catch (err) {
            throw err;
          } finally {
            setLoading(false);
          }
        };
        importIcon();
      }, [name]);

      if (!loading && ImportedIconRef.current) {
        const { current: ImportedIcon } = ImportedIconRef;
        return <ImportedIcon {...rest} />;
      }
      return null;
    };

动态加载 svg 的一种解决方案是使用require将其加载到img,例如:

<img src={require(`../assets/${logoNameVariable}`)?.default} />