使用Svgr搭建私有图标库

1,158 阅读2分钟

通常我们消费 svg 的方式比较简单:

<img src={require('filename.svg')} />

即可以在网页上打印出 svg 的图案。

但是为了满足作为一个图标库的各种需要(控制大小,旋转角度,填充颜色等),使用 img 标签是没办法达成的;所以我们需要把SVG作为 Component 使用;

查找方案:

如果使用的是Create-React-App的话,就比较便捷。在 CRA2.x开始,CRA在背后调用了一个叫做svgr的工具,所以我们可以:

import { ReactComponent as MyLogo } from './logo.svg';  // 选择 ReactComponent 导出项目

const App = () => (
  <div id="App">
    <MyLogo />
  </div>
);

export default App;

eject 以后查看 CRA为我们做的配置:

'use strict';

const path = require('path');
const camelcase = require('camelcase');

// This is a custom Jest transformer turning file imports into filenames.
// <http://facebook.github.io/jest/docs/en/webpack.html>

module.exports = {
  process(src, filename) {
    const assetFilename = JSON.stringify(path.basename(filename));

    if (filename.match(/\.svg$/)) {
      // Based on how SVGR generates a component name:
      // <https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6>
      const pascalCaseFilename = camelcase(path.parse(filename).name, {
        pascalCase: true,
      });
      const componentName = `Svg${pascalCaseFilename}`;
      return `const React = require('react');
      module.exports = {
        __esModule: true,
        default: ${assetFilename},
        ReactComponent: React.forwardRef(function ${componentName}(props, ref) {
          return {
            $$typeof: Symbol.for('react.element'),
            type: 'svg',
            ref: ref,
            key: null,
            props: Object.assign({}, props, {
              children: ${assetFilename}
            })
          };
        }),
      };`;
    }

    return `module.exports = ${assetFilename};`;
  },
};

注意到,CRA为 webpack loader生成配置的时候,导出了俩内容:1. 静态svg文件(default);2. ReactComponent 与它的模版内容。

然而,当我们构建流程不是 CRA ,或者有更多的自定义特性的时候,我们需要自行配置 svgr。

Svgr是什么?

Svgr 是一系列把 svg 文件转成 ReactComponent 的工具,包括了 webpack loader,一些命令行工具, 以及工具流配件。

普通的业务调用场景使用 webpack-loader 即可以满足需要,基本上按照CRA(或者链接)的配置即可。

如果我们希望,譬如批量转换 svg ,动态引入等特性,就需要用到命令行工具。

  1. 安装:
yarn add --dev @svgr/cli
  1. 编写npm脚本:
"scripts": {
	"boot": "npx @svgr/cli --template template.js --ext tsx --config-file .svgrrc.json -d svgrs svgs"
}
  1. 准备svg文件目录与编写模版文件template.js:
/* eslint-disable */
function template({ template }, opts, { imports, componentName, props, jsx, exports }) {
  const typeScriptTpl = template.smart({ plugins: ['typescript'] });
  return typeScriptTpl.ast`
      import * as React from 'react';
      const ${componentName} = (props: React.SVGProps<SVGSVGElement>) => ${jsx};
      export default ${componentName};
    `;
}
module.exports = template;
  1. 运行 boot 以后我们得到了一个svgrs 文件夹

18deb1936dbd5bc9340ec66bb.png

在这里,输出的是 tsx 是因为我们在命令制定了ext是 tsx。如果只是需要jsx 的话就不需要特意指定。可以看出,svgr命令行工具,按照我们给定 template.js 的格式输出了一个完整的 react component 文件。

  1. 编写图标库主体逻辑:
import classNames from 'classnames';
import React, { CSSProperties, FC, SVGAttributes } from 'react';
import '../../src/scss/base.scss';
import './index.scss';
import { ConfigConsumer, ConfigProps } from '../ConfigProvider';
import { toDashed } from './svgs/util';

type IconSize = 'small' | 'normal' | 'large';

export type Icons =
  | 'PendingStatus'
  | 'Book'
  // ...

interface Props {
  // ...
}

/**
 *
 * How to dynamic import assets?
 * <https://github.com/survivejs/webpack-book/issues/80#issuecomment-216068406>
 *
 */
export const dynamicImportSvgs = require.context('./svgrs', true);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type NativeAttrs = Omit<SVGAttributes<any>, keyof Props>;
export type IconProps = NativeAttrs & Props;

const Icon: FC<IconProps> = ({
  type,
  rotate,
  color,
  size = 'normal',
  style,
  className,
  classes,
  ...props
}) => {
  if (typeof type === 'undefined') {
    throw new Error(`Icon type empty: ${type}`);
  }
  const IconType = dynamicImportSvgs(`./${type}.tsx`).default;
  return (
    <ConfigConsumer>
      {({ prefixCls: customPrefixCls }: ConfigProps) => {
        const prefixCls = `${customPrefixCls}-icon`;
        const classNameString = classNames(
          prefixCls,
          `${prefixCls}${toDashed(type)}`,
          `${prefixCls}-${size}`,
          className,
          classes
        );
        return (
          <IconType
            className={classNameString}
            fill="currentColor"
            style={{
              transform: `rotate(${rotate}deg)`,
              color,
              ...style,
            }}
            {...props}
          />
        );
      }}
    </ConfigConsumer>
  );
};

export default Icon;

// Just for stories render props table
export const IconPropsForDoc: FC<Props> = () => null;

以上组件除了常规的组件逻辑编写以外,还实现了动态调用组件的逻辑,以方便我们在敲击Icon名称的时候可以展示出对应组件的视图。

最终,使用文档生成器输出的API列表:

baaeaae396266d24912f9dac5.png

一些小问题与解决办法

Q:我的组件生成出来的svg是黑色的,或者变样子了。

A: 使用sketch或者一些设计工具导出的文件是硬编码了svg的属性,所以我们需要替换掉一些色值的属性,以便于我们可以通过 Component props 去控制。 svgr 支持配置,在目录新增 .svgrrc.json , 既可以配置在转换过程中把一些硬编码的属性(比如#000色值)替换:

{
  "icon": true,
  "replaceAttrValues": {
    "#000": "{props.fill}",
    "#666": "{props.fill}",
    "#181818": "{props.fill}",
    "#111": "{props.fill}",
    "#111111": "{props.fill}",
    "#999": "{props.fill}",
    "#999999": "{props.fill}"
  }
}

Q:我没办法通过css设置svg的颜色。

A:"currentColor" 允许 svg 通过css color 属性来改变 fill 的值。 所以我们在组件设置属性 fill=”currentColor”,让消费者可以通过 css 更改颜色。