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

Posted

技术标签:

【中文标题】如何动态导入 SVG 并内联渲染【英文标题】:How to dynamically import SVG and render it inline 【发布时间】:2020-08-03 22:43:49 【问题描述】:

我有一个接受一些参数并呈现 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 documentation for dynamic imports和神奇的评论“eager”:

"不生成额外的块。所有模块都包含在当前 块并且不发出额外的网络请求。承诺还在 返回但已经解决。与静态导入相比, 在调用 import() 之前,模块不会执行。"

这就是我的图标被解析为:

> 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 将其渲染为组件,或者甚至可以这样吗?

【问题讨论】:

如果您静态导入,您的 svg 是否正确显示? 是的!如果我做一个普通的import MyIcon from './icons/myicon.svg',我可以像&lt;MyIcon /&gt;一样渲染它。 您可能不得不将解析后的 SVG 存储在一个状态中。 @dev_junwen 正确,但将其存储在 state 中仍然无法让我将其呈现为内联 svg。 或者另一种相当“动态”的方式是您可以定义名称到 SVG 组件的映射,然后使用括号符号语法 iconMap[name] 检索正确的 SVG。我还没有测试它,但我认为这可以工作。在这种情况下,您需要导入所有 SVG 并将其分配给地图。 【参考方案1】:

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

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

【讨论】:

【参考方案2】:

导入 SVG 文件时可以使用refReactComponent 命名导出。请注意,它必须是 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 ;

打字稿中的示例动态 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 ;


对于那些在动态导入 SVG 时获得 undefinedReactComponent 的用户,这是由于 Webpack 插件将 ReactComponent 添加到以某种方式导入的每个 SVG 的错误不会触发动态进口。

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

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

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

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

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

例如,代替

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

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

你可以改成

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

// Icon.jsx
...
import(`../../icons/$name.svg`);
...

【讨论】:

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

我根据答案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;
    ;

【讨论】:

【参考方案4】:

您的渲染函数(用于类组件)和函数组件不应是异步的(因为它们必须返回 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 />;
;

【讨论】:

这可能适用于 img 标签,其中源将被导入Icon.default,正如你所写,但它不适用于内联 svg,我想将其呈现为 。我用异步useEffect尝试了你的方法,但是我需要setIcon整个模块,而不是importedIcon.default(文件路径),然后像一样渲染它,它给了我错误Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object. 为什么需要将其渲染为 &lt;Icon&gt; ?这种方法给你什么,有什么不同,使用&lt;img&gt;是无法克服的 如果是 img 标签,我将如何更改 svg 的属性,例如图标的填充颜色?是否可以使用 img 标签? Effects 不能承载 async 函数。它们返回一个作为清理函数调用的承诺。 robinwieruch.de/react-hooks-fetch-data 不管最初的问题是什么,这可能是“动态导入本地文件”的最佳答案。当我有一个文件 URL 列表并动态加载时,这很有效。

以上是关于如何动态导入 SVG 并内联渲染的主要内容,如果未能解决你的问题,请参考以下文章

动态调整内联 SVG 的大小

如何在 Python 中将 SVG 图像渲染为 PNG 文件?

如何使用Javascript将FontAwesome渲染为SVG图像?

如何将渲染器指定为 SVG?

将带有滤镜效果的 SVG 渲染为 PNG

动态渲染嵌套组件(在父组件中创建)作为指令