@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文件夹) -> fill
、outline
、twotone
文件夹里的svg
文件,全部编译到src
(根目录下的src文件夹)以及-> fill
、outline
、twotone
文件夹里的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
-> fill
、outline
、twotone
里所有相关的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 };
}
)
)
)
);
}
from
、pipe
等等都是rxjs
(ReactiveX
,主要功能:处理异步通信,来自微软,感兴趣的同学,可查看其官网API)
ThemeType
包含3种svg
类型,代码如下:
// svg folder names
export type ThemeType = 'fill' | 'outline' | 'twotone';
将这3种类型,通过from
转成Observable
对象后,便可调用rxjs
里的pipe
等方法
对于上面的函数功能,你们可以想象一副画面:将svgBasicNames
和ThemeType
放在流水管道里,遇到第一个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_IDENTIFIER
和ICON_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);
});
});
对应的控制台输出信息,如下图所示: