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

Maj*_*ren 14 javascript reactjs webpack webpack-4

我有一个函数,它接受一些参数并呈现一个 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} />;
};
Run Code Online (Sandbox Code Playgroud)

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

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

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

> Module
default: "static/media/antenna.11b95602.svg"
__esModule: true
Symbol(Symbol.toStringTag): "Module"
Run Code Online (Sandbox Code Playgroud)

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

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 将它呈现为一个组件,或者甚至有可能这样吗?

jun*_*n-k 39

您可以在导入 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 };
}
Run Code Online (Sandbox Code Playgroud)

编辑 react-dynamic-svg-import

打字稿中的动态 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 };
}
Run Code Online (Sandbox Code Playgroud)

编辑 react-dynamic-svg-import-ts


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

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

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

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 };
}
Run Code Online (Sandbox Code Playgroud)

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

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

例如,而不是

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 };
}
Run Code Online (Sandbox Code Playgroud)

你可以把它改成

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

  • 顺便说一句 @dev_junwen ReactComponent 从来没有为我工作过,我不知道这段代码在你的 CodeSandbox 中是如何工作的,但是 `.ReactComponent` 只是为我返回 undefined 。我不得不更改它并使用`.default`,如下面@Enchew 的示例所示。如果您转到“导入默认值”,我在这里找到了一些很棒的文档:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import (3认同)
  • 对于仍然遇到“未定义”的人,当为 svgs 设置“issuer”属性时,动态 webpack 加载会出现错误(https://github.com/webpack/webpack/discussions/15117#discussioncomment-1925385)。修复方法是从 webpack 配置中的 svg 加载器中删除“issurer”配置。由于我们使用的是 create-react-app,并且尚未弹出,因此我们使用“patch-package”从 webpack 配置中删除这些行,现在一切正常。 (3认同)
  • @Willege 我已经更新了答案。请随意查看代码示例。 (2认同)
  • @dev_junwen 不幸的是这个解决方案对我不起作用。我已经尝试了一切。即使我从 CodeSandbox 下载代码并使用“npm install”从头开始安装所有依赖项,SVG 图像也不会加载。我也没有收到错误消息。我做错了什么吗(可能是 webpack、babel 或 typescript 配置)? (2认同)
  • @mamiu 你在使用 Create React App 吗?如果没有,您将需要手动设置内联 svg 加载器。[这个](/sf/answers/4319955251/)答案可能会有所帮助。 (2认同)

小智 8

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

  • 正如您所写,这可能适用于源将被 importIcon.default 的 img 标签,但在我的情况下,它不适用于内联 svg,我想将其渲染为 &lt;Icon /&gt;。我尝试了使用异步 useEffect 的方法,但随后我需要对整个模块进行“setIcon”,而不是 importIcon.default (文件路径),然后像 &lt;Icon /&gt; 一样渲染它,它给了我错误“元素类型”无效:需要一个字符串(对于内置组件)或一个类/函数(对于复合组件),但得到:对象。` (4认同)
  • 如果 svg 是 img 标签,那么我将如何更改它的属性,例如图标的填充颜色?可以用img标签吗? (2认同)

小智 5

我根据答案进行了更改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;
    };
Run Code Online (Sandbox Code Playgroud)