【问题标题】:How to dynamically import SVG and render it inline如何动态导入 SVG 并内联渲染
【发布时间】: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 并将其分配给地图。

标签: javascript reactjs webpack webpack-4


【解决方案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 列表并动态加载时,这很有效。
      猜你喜欢
      • 2021-12-09
      • 1970-01-01
      • 1970-01-01
      • 2020-02-28
      • 1970-01-01
      • 1970-01-01
      • 2021-12-10
      • 2017-11-22
      • 2023-01-28
      相关资源
      最近更新 更多