@ant-design/icons源码解读

15 篇文章 0 订阅
1 篇文章 0 订阅

@ant-design/icons源码解读

package.json开始

读源码,我习惯从package.json里的scripts开始,从这里,可以看到该npm包发布前做了哪些事。

"scripts": {
    "build": "npm run build:lib && npm run build:index-es &&  npm run build:umd",
    "build:lib": "cross-env NODE_ENV=production rimraf lib && tsc --project ./tsconfig.json --outDir lib",
    "build:index-es": "cross-env NODE_ENV=production rimraf lib/index.es.js && babel --extensions '.ts' --presets @babel/preset-typescript src/index.ts --out-file lib/index.es.js",
    "generate": "cross-env TS_NODE_PROJECT=build/tsconfig.json node --require ts-node/register build/index.ts",
    "clean:src": "cross-env TS_NODE_PROJECT=build/tsconfig.json node --require ts-node/register build/scripts/clean.ts",
    "test": "npm run test:unit",
    "test:unit": "jest",
    "lint": "tslint -c tslint.json 'src/**/*.{ts,tsx}' 'site/**/*.{ts,tsx}' 'test/**/*.{ts,tsx}'",
    "clean:build": "rimraf .cache es lib",
    "start": "gatsby develop",
    "build:umd": "webpack --config umd.webpack.config.js -p",
    "prepublish": "npm run lint && npm run test && npm run generate && npm run build"
},

执行npm publish前,会预先执行npm prepublish命令,该命令会先执行npm run lint,只有当该命令执行成功后,才会继续执行npm run test,这是&&的含义。让我们跳过npm run lint && npm run test这两条命令,直接来到该库的核心命令npm run generate,这个命令,负责将svg(根目录下的svg文件夹) -> filloutlinetwotone文件夹里的svg文件,全部编译到src(根目录下的src文件夹)以及-> filloutlinetwotone文件夹里的ts文件。

小知识点:prepublish这个钩子不仅会在npm publish命令之前运行,还会在npm install(不带任何参数)命令之前运行。这种行为很容易让用户感到困惑,所以 npm 4 引入了一个新的钩子prepare,行为等同于prepublish,而从 npm 5 开始,prepublish将只在npm publish命令之前运行。

解读generate命令

获取包含所有svg名称数组svgBasicNames

npm run generate会执行build/index.ts,而build/index.ts核心的方法是build,部分源码如下所示:

import { normalize } from './utils/normalizeNames';
export async function build(env: Environment) {
    ...
    const svgBasicNames = await normalize(env); // svgBasicNames为数组对象,包含全部的svg名称
    ...
}

normalize方法将svg -> filloutlinetwotone里所有相关的svg文件名称,原样输出,例如,若outline里有个search.svg,那么svgBasicNames数组里会有search字段

参数env为对象,里面定义了待解析的svg路径、模板路径等等

import {
  Environment
} from './typings';
export const environment: Environment = {
    paths: {
        SVG_DIR: path.resolve(__dirname, '../svg')
    }
}

svg文件转为AST

先看源码,如下:

import parse5 = require('parse5');
import { from, Observable } from 'rxjs';

import {
  BuildTimeIconMetaData,
  Environment,
  IconDefinition,
  Manifest,
  NameAndPath,
  Node,
  ThemeType,
  WriteFileMetaData
} from './typings';

export async function build(env: Environment) {
    ...
    const svgBasicNames = await normalize(env); // svgBasicNames为数组对象,包含全部的svg名称
    ...
    // SVG Meta Data Flow
    const svgMetaDataWithTheme$ = from<ThemeType[]>([
        'fill',
        'outline',
        'twotone'
    ]).pipe(
        map<ThemeType, Observable<any>>((theme) =>
            from(svgBasicNames).pipe(
                // 第一个map映射器
                map<string, NameAndPath>((kebabCaseName) => {
                    const identifier = getIdentifier(
                        _.upperFirst(_.camelCase(kebabCaseName)),
                        theme
                    );
                    // kebabCaseName:svg图标名称,identifier:将svg图标名称转成驼峰写法,且首字母大写
                    return { kebabCaseName, identifier };
                }),
                // 第二个fiter过滤器
                filter(({ kebabCaseName }) =>
                    // 判断svg文件是否可达
                    isAccessable(
                        path.resolve(env.paths.SVG_DIR, theme, `${kebabCaseName}.svg`)
                    )
                ),
                // 第三个mergeMap映射器
                mergeMap<NameAndPath, any>(
                    async ({ kebabCaseName, identifier }: any) => {
                        const tryUrl = path.resolve(
                            env.paths.SVG_DIR,
                            theme,
                            `${kebabCaseName}.svg`
                        );
                        let optimizer = svgo;
                        if (singleType.includes(theme)) {
                            optimizer = svgoForSingleIcon;
                        }
                        // 读取svg里面的数据
                        const { data } = await optimizer.optimize(
                            await fs.readFile(tryUrl, 'utf8')
                        );
                        const icon: IconDefinition = {
                            name: kebabCaseName,//添加icon的名称,例如double-down这样的类似格式
                            theme, // 添加主题名,为'fill' | 'outline' | 'twotone'三者之一
                            icon: {
                                ...generateAbstractTree(
                                (parse5.parseFragment(data) as any).childNodes[0] as Node,
                                kebabCaseName
                                )
                            }
                        };
                        // 若在outline文件夹下,有个alert.svg,则identifier为AlertOutline,icon为{ tag: 'svg', attrs: {class: 'icon', viewBox: '0 0 1024 1024', children: [...]} }
                        return { identifier, icon };
                    }
                )
            )
        )
    );
}

frompipe等等都是rxjs(ReactiveX,主要功能:处理异步通信,来自微软,感兴趣的同学,可查看其官网API)

ThemeType包含3种svg类型,代码如下:

// svg folder names
export type ThemeType = 'fill' | 'outline' | 'twotone';

将这3种类型,通过from转成Observable对象后,便可调用rxjs里的pipe等方法

对于上面的函数功能,你们可以想象一副画面:将svgBasicNamesThemeType放在流水管道里,遇到第一个map映射器,变成{ kebabCaseName, identifier },再继续往下流,遇到第二个fiter过滤器,只返回存在的svg,剩下的数据继续往下流,遇到第三个mergeMap映射器,读取svg里的data数据,调用generateAbstractTree方法将data转成AST(抽象语法树),将其存储在svgMetaDataWithTheme$变量里即可

过滤AST里的class属性,并对twotone情况进行底色默认填充处理

const BuildTimeIconMetaData$ = svgMetaDataWithTheme$.pipe(
    // 通过mergeMap,将svgMetaDataWithTheme$得到的最终结果作为这里的输入值{ identifier, icon }
    mergeMap<Observable<BuildTimeIconMetaData>, any>(
      (metaData$: any) => metaData$
    ),
    map<any, BuildTimeIconMetaData>(
      ({ identifier, icon }) => {
        // 对icon进行深度拷贝,避免影响icon的原来值  
        icon = _.cloneDeep(icon);
        // 上一步,我们得到某个icon为{ tag: 'svg', attrs: {class: 'icon', viewBox: '0 0 1024 1024', children: [...]} }
        // 在这里,进行进一步判断过滤,即将attrs里的class属性移除掉
        if (typeof icon.icon !== 'function') {
          icon.icon.attrs.focusable = false;
          if (icon.icon.attrs.class) {
            icon.icon.attrs = _.omit(icon.icon.attrs, ['class']);
          }
        }
        // 若icon的主题值为twotone,即两色调,则对attrs的fill进行默认填充色#333处理
        if (icon.theme === 'twotone') {
          if (typeof icon.icon !== 'function' && icon.icon.children) {
            icon.icon.children.forEach((pathElment: any) => {
              pathElment.attrs.fill = pathElment.attrs.fill || '#333';
            });
          }
        }
        // 返回处理过后的icon,及尚未处理的identifier
        return { identifier, icon };
      }
    )
);

BuildTimeIconMetaData$里的icon转为svg存储在inlineSVGFiles$

const inlineSVGFiles$ = BuildTimeIconMetaData$.pipe(
    map<BuildTimeIconMetaData, WriteFileMetaData>(({ icon }) => {     
      return { 
        path: path.resolve(
          env.paths.INLINE_SVG_OUTPUT_DIR, // 定义svg文件的输出路径,对应项目文件夹inline-svg
          icon.theme, // 定义svg文件的主题
          `./${icon.name}.svg`
        ),
        // 将icon抽象语法树,转变成svg,如<svg viewBox="0 0 1024 1024" focusable="false"><path d="M549.63 91.33l350.82 254.91a64 64 0 0123.26 71.55L789.7 830.21a64 64 0 01-60.87 44.22H295.17a64 64 0 01-60.87-44.22L100.3 417.79a64 64 0 0123.26-71.55L474.37 91.33a64 64 0 0175.26 0z"></path></svg>
        content: renderIconDefinitionToSVGElement(icon) 
      };
    })
);

注意:这里尚未生成任何svg文件

通过icon.ts.template模板,指定在src目录内的各个ts文件

icon.ts.template模板代码如下所示:

// This icon file is generated by build/generateIcons.ts
// tslint:disable

import { IconDefinition } from '../types';

const <% ICON_IDENTIFIER %>: IconDefinition = <% ICON_JSON %>;

export default <% ICON_IDENTIFIER %>;

模板文件内,定义了两个占位符ICON_IDENTIFIERICON_JSON,用于后续的ts文件生成

指定在src目录内,各个svg对应的ts文件

const iconTsTemplate = await fs.readFile(env.paths.ICON_TEMPLATE, 'utf8');
const iconFiles$ = BuildTimeIconMetaData$.pipe(
    map<
      BuildTimeIconMetaData,
      { identifier: string; content: string; theme: ThemeType }
    >(({ identifier, icon }) => {
      return {
        identifier,
        theme: icon.theme,
        content:
          icon.theme === 'twotone'
            ?
            // 处理twotone的icon情况
            Prettier.format(//进行Prettier格式化
                iconTsTemplate
                  .replace(ICON_IDENTIFIER, identifier) //替换模板文件内的ICON_IDENTIFIER为BuildTimeIconMetaData$里的identifier
                  .replace(
                    ICON_JSON,
                    JSON.stringify({ ...icon, icon: 'FUNCTION' }).replace( // 对icon为FUNCTION的做特殊处理
                      `"FUNCTION"`,
                      `function (primaryColor: string, secondaryColor: string) {` +
                        ` return ${replaceFillColor(
                          JSON.stringify(icon.icon)
                        )};` +
                        ` }`
                    )
                  ),
                { ...env.options.prettier, parser: 'typescript' }
              )
            :
            // 处理outline、fill的icon情况
            Prettier.format(
                iconTsTemplate
                  .replace(ICON_IDENTIFIER, identifier)
                  .replace(ICON_JSON, JSON.stringify(icon)), // BuildTimeIconMetaData$里的icon为AST,即上文内提到的格式:{ tag: 'svg', attrs: {class: 'icon', viewBox: '0 0 1024 1024', children: [...]} }
                env.options.prettier
            )
      };
    }),
    // 将解析后的content,文件输出目录暂时存于iconFiles$,等待后面的统一输出
    map<
      { identifier: string; content: string; theme: ThemeType },
      WriteFileMetaData
    >(({ identifier, content, theme }) => ({
      path: path.resolve(
        env.paths.ICON_OUTPUT_DIR,
        theme,
        `./${identifier}.ts`
      ),
      content
    }))
);

注意:该阶段尚未生成任何ts文件

指定在src目录内,入口index.ts文件

index.ts文件也有模板文件index.ts.template

// This icon file is generated by build/generateIcons.ts
// tslint:disable

<% EXPORT_DEFAULT_COMPONENT_FROM_DIR %>

将入口index.ts文件信息存在于indexFile$内

const indexTsTemplate = await fs.readFile(env.paths.INDEX_TEMPLATE, 'utf8');
  const indexFile$ = svgMetaDataWithTheme$.pipe(
    mergeMap<Observable<BuildTimeIconMetaData>, any>(
      (metaData$: any) => metaData$
    ),
    reduce<any, string>(
      (acc, { identifier, icon }) =>
        acc +
        `export { default as ${identifier} } from './${
          icon.theme
        }/${identifier}';\n`,
      ''
    ),
    map<string, WriteFileMetaData>((content) => ({
      path: env.paths.INDEX_OUTPUT,
      content: Prettier.format(
        indexTsTemplate.replace(EXPORT_DEFAULT_COMPONENT_FROM_DIR, content),
        env.options.prettier
      )
    }))
);

注意:该阶段尚未生成任何ts文件

后面的代码便是生成manifest.ts文件,dist.ts文件等,这里便不一一赘述了。

inlineSVGFiles$iconFiles$等统一生成各自对应的文件

const files$ = iconFiles$.pipe(
    concat(inlineSVGFiles$),
    ...
    concat(indexFile$),
    ...
  );

return new Promise<Subscription>((resolve, reject) => {
    const subscription = files$
      .pipe(
        mergeMap(async ({ path: writeFilePath, content }) => {
          await fs.writeFile(writeFilePath, content, 'utf8');
          log.info(`Generated ./${path.relative(env.base, writeFilePath)}.`); // 生成各个文件
        })
      )
      .subscribe(undefined, reject, () => {
        log.notice('Done.'); // 提示已完成
        resolve(subscription);
      });
});

对应的控制台输出信息,如下图所示:

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值