前言
最近在抄ant-design的代码给solid-js做一个antd风格的组件库,然后就不出意外的涉及到了icon部分,然后把自己的所见所得记录一下分享
包地址:www.npmjs.com/package/@be...
代码地址:github.com/ikunOrg/bee...
编写组件
最外层的组件为脚本所使用,主要的代码都做了注释
tsx
// classnames的平替 包体积更小
import { clsx as classNames } from 'clsx';
import type { IconDefinition } from '@ant-design/icons-svg/lib/types';
import { blue } from '@ant-design/colors';
// 全局上下文
import Context from './Context';
// 基础的props 是否旋转spin 类名class
import type { IconBaseProps } from './Icon';
// 基础的icon
import SolidIcon from './IconBase';
import { getTwoToneColor, setTwoToneColor } from './twoTonePrimaryColor';
import type { TwoToneColor } from './twoTonePrimaryColor';
import { normalizeTwoToneColors } from '../utils';
import { useContext, type Component } from 'solid-js';
export interface AntdIconProps extends IconBaseProps {
twoToneColor?: TwoToneColor;
ref?: any;
}
export interface IconComponentProps extends AntdIconProps {
icon: IconDefinition;
}
// 为渲染函数增加额外的三个属性
interface IconBaseComponent<P> extends Component<P> {
displayName: string;
getTwoToneColor: typeof getTwoToneColor;
setTwoToneColor: typeof setTwoToneColor;
}
// Initial setting
// should move it to antd main repo?
setTwoToneColor(blue.primary!);
const Icon: IconBaseComponent<IconComponentProps> = (props) => {
// 结构在solidjs中会破坏响应式 这么些又问题之后找到好的办法再说
const {
// affect outter <i>...</i>
// affect inner <svg>...</svg>
icon,
spin,
rotate,
tabIndex,
onClick,
// other
twoToneColor,
...restProps
} = props;
// 从上下文获取一些属性
const { prefixCls = 'anticon', rootClassName } = useContext(Context);
// 生产类名
const classString = classNames(
rootClassName,
prefixCls,
{
[`${prefixCls}-${icon.name}`]: !!icon.name,
[`${prefixCls}-spin`]: !!spin || icon.name === 'loading',
},
props.class,
);
let iconTabIndex = tabIndex;
if (iconTabIndex === undefined && onClick) {
iconTabIndex = -1;
}
const svgStyle = rotate
? {
msTransform: `rotate(${rotate}deg)`,
transform: `rotate(${rotate}deg)`,
}
: undefined;
const [primaryColor, secondaryColor] = normalizeTwoToneColors(twoToneColor);
return (
<span
role="img"
aria-label={icon.name}
{...restProps}
ref={props.ref}
tabIndex={iconTabIndex}
onClick={onClick}
class={classString}
>
<SolidIcon
icon={icon}
primaryColor={primaryColor}
secondaryColor={secondaryColor}
style={svgStyle}
/>
</span>
);
};
Icon.displayName = 'AntdIcon';
Icon.getTwoToneColor = getTwoToneColor;
Icon.setTwoToneColor = setTwoToneColor;
export default Icon;
内层的icon组件用于生产svg标签
tsx
import type { AbstractNode, IconDefinition } from '@ant-design/icons-svg/lib/types';
import { generate, getSecondaryColor, isIconDefinition, warning, useInsertStyles } from '../utils';
import type { Component, JSX, Ref } from 'solid-js';
export interface IconProps {
icon: IconDefinition;
className?: string;
onClick?: any;
style?: JSX.CSSProperties;
primaryColor?: string; // only for two-tone
secondaryColor?: string; // only for two-tone
focusable?: string;
}
export interface TwoToneColorPaletteSetter {
primaryColor: string;
secondaryColor?: string;
}
export interface TwoToneColorPalette extends TwoToneColorPaletteSetter {
calculated?: boolean; // marker for calculation
}
const twoToneColorPalette: TwoToneColorPalette = {
primaryColor: '#333',
secondaryColor: '#E6E6E6',
calculated: false,
};
function setTwoToneColors({ primaryColor, secondaryColor }: TwoToneColorPaletteSetter) {
twoToneColorPalette.primaryColor = primaryColor;
twoToneColorPalette.secondaryColor = secondaryColor || getSecondaryColor(primaryColor);
twoToneColorPalette.calculated = !!secondaryColor;
}
function getTwoToneColors(): TwoToneColorPalette {
return {
...twoToneColorPalette,
};
}
interface IconBaseComponent<P> extends Component<P> {
displayName: string;
getTwoToneColors: typeof getTwoToneColors;
setTwoToneColors: typeof setTwoToneColors;
}
// 内层icon的封装
const IconBase: IconBaseComponent<IconProps> = (props) => {
const {
icon,
className,
onClick,
style = {},
primaryColor,
secondaryColor,
...restProps
} = props;
// ref和react区别很大
let svgRef: Ref<HTMLElement> | undefined;
let colors: TwoToneColorPalette = twoToneColorPalette;
if (primaryColor) {
colors = {
primaryColor,
secondaryColor: secondaryColor || getSecondaryColor(primaryColor),
};
}
// 将icon挂在到header上 存在shadow dom的情况下会有问题待修复
useInsertStyles(svgRef);
warning(isIconDefinition(icon), `icon should be icon definiton, but got ${icon}`);
if (!isIconDefinition(icon)) {
return null;
}
let target = icon;
if (target && typeof target.icon === 'function') {
target = {
...target,
icon: target.icon(colors.primaryColor, colors.secondaryColor!),
};
}
// 工厂函数 生成jsx
return generate(target.icon as AbstractNode, `svg-${target.name}`, {
className,
onClick,
style,
'data-icon': target.name,
width: '1em',
height: '1em',
fill: 'currentColor',
'aria-hidden': 'true',
...restProps,
ref: svgRef,
});
};
IconBase.displayName = 'IconReact';
IconBase.getTwoToneColors = getTwoToneColors;
IconBase.setTwoToneColors = setTwoToneColors;
export default IconBase;
generate函数生产jsx
ts
export function generate(node: AbstractNode, key: string, rootProps?: RootProps | false): any {
if (!rootProps) {
// solid-js/h
return h(
node.tag,
{ key, ...normalizeAttrs(node.attrs) },
(node.children || []).map((child, index) => generate(child, `${key}-${node.tag}-${index}`)),
);
}
return h(
node.tag,
{
key,
...normalizeAttrs(node.attrs),
...rootProps,
},
(node.children || []).map((child, index) => generate(child, `${key}-${node.tag}-${index}`)),
);
}
编写genrate脚本
ts
import allIconDefs from '@ant-design/icons-svg';
import type { IconDefinition } from '@ant-design/icons-svg/es/types';
import fs from 'node:fs';
import path from 'node:path';
import { promisify } from 'node:util';
// eslint-disable-next-line lodash/import-scope
import { template } from 'lodash';
const writeFile = promisify(fs.writeFile);
interface IconDefinitionWithIdentifier extends IconDefinition {
svgIdentifier: string;
}
function walk<T>(fn: (iconDef: IconDefinitionWithIdentifier) => Promise<T>) {
return Promise.all(
// 便利所有svg文件 svgIdentifier为唯一标识大驼峰命名
Object.keys(allIconDefs).map((svgIdentifier) => {
const iconDef = (allIconDefs as { [id: string]: IconDefinition })[svgIdentifier];
// 为每一个svg文件生成组件
return fn({ svgIdentifier, ...iconDef });
}),
);
}
// 生成icon组件文件
async function generateIcons() {
const iconsDir = path.join(__dirname, '../src/icons');
try {
// 查看文件是否可以访问 第一次见
await promisify(fs.access)(iconsDir);
} catch (err) {
// 文件不存在就在创建一个
await promisify(fs.mkdir)(iconsDir);
}
// lodash template
const render = template(
`
// GENERATE BY ./scripts/generate.ts
// DON NOT EDIT IT MANUALLY
// 引入svg文件
import <%= svgIdentifier %>Svg from '@ant-design/icons-svg/lib/asn/<%= svgIdentifier %>';
// 引入组件
import AntdIcon, { type AntdIconProps } from '../components/AntdIcon';
// 最终产物组件 传入ref 与 icon
const <%= svgIdentifier %> = (
props: AntdIconProps,
) => <AntdIcon {...props} ref={props.ref} icon={<%= svgIdentifier %>Svg} />;
if (process.env.NODE_ENV !== 'production') {
<%= svgIdentifier %>.displayName = '<%= svgIdentifier %>';
}
export default <%= svgIdentifier %>;
`.trim(),
);
// 生成组件的函数 传入回调函数 会为所有的svg生成各自的组件
await walk(async ({ svgIdentifier }) => {
// generate icon file
await writeFile(
path.resolve(__dirname, `../src/icons/${svgIdentifier}.tsx`),
render({ svgIdentifier }),
);
});
// generate icon index
const entryText = Object.keys(allIconDefs)
.sort()
.map((svgIdentifier) => `export { default as ${svgIdentifier} } from './${svgIdentifier}';`)
.join('\n');
await promisify(fs.appendFile)(
path.resolve(__dirname, '../src/icons/index.tsx'),
`
// GENERATE BY ./scripts/generate.ts
// DON NOT EDIT IT MANUALLY
${entryText}
`.trim(),
);
}
// 生成入口文件
async function generateEntries() {
const render = template(
`
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
const _<%= svgIdentifier %> = _interopRequireDefault(require('./lib/icons/<%= svgIdentifier %>'));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
const _default = _<%= svgIdentifier %>;
exports.default = _default;
module.exports = _default;
`.trim(),
);
await walk(async ({ svgIdentifier }) => {
// generate `Icon.js` in root folder
await writeFile(
path.resolve(__dirname, `../${svgIdentifier}.js`),
render({
svgIdentifier,
}),
);
// generate `Icon.d.ts` in root folder
await writeFile(
path.resolve(__dirname, `../${svgIdentifier}.d.ts`),
`export { default } from './lib/icons/${svgIdentifier}';`,
);
});
}
if (process.argv[2] === '--target=icon') {
generateIcons();
}
if (process.argv[2] === '--target=entry') {
generateEntries();
}
在package执行
pnpm i tsx -D
在package的scripts增加脚本 (先清除产物文件夹,然后执行脚本)
"generate": "pnpm rimraf src/icons && pnpm tsx scripts/generate.ts --target=icon"
然后一共800+文件的产出io会被堆满
打包与发布
打包就使用rollup产出es与cjs文件,tsup产出dts文件 代码地址:github.com/ikunOrg/bee...
ts
import nodeResolve from '@rollup/plugin-node-resolve';
import type {
InputPluginOption,
OutputOptions,
RollupBuild,
RollupOptions,
WatcherOptions,
} from 'rollup';
import { rollup, watch as rollupWatch } from 'rollup';
import esbuild from 'rollup-plugin-esbuild';
import solidPlugin from 'vite-plugin-solid';
import commonjs from '@rollup/plugin-commonjs';
import { DEFAULT, generateExternal, resolveBuildConfig, resolveInput, target } from './ustils';
export interface Options {
/**
* @description
*/
input?: string;
sourcemap?: boolean;
dts?: boolean;
dtsDir?: string;
tsconfig?: string;
watch?: boolean;
minify?: boolean;
full?: boolean;
}
async function writeBundles(bundle: RollupBuild, options: OutputOptions[]) {
// 输出产出
return Promise.all(options.map((option) => bundle.write(option)));
}
async function resolveConfig(root: string, options: Options = {}): Promise<RollupOptions> {
//通过cac拿到的参数
const {
input = DEFAULT,
sourcemap = false,
watch = false,
minify = false,
full = false,
} = options;
const inputPath = resolveInput(root, input);
const watchOptions: WatcherOptions = {
clearScreen: true,
};
const plugins = [
// 识别不同的文件后缀
nodeResolve({
extensions: ['.js', '.jsx', '.ts', '.tsx'],
}),
// 识别commjs规范的包
commonjs(),
// 给solid打包用的不影响可以去掉
solidPlugin(),
//加速
esbuild({
sourceMap: sourcemap,
target,
minify,
}),
] as unknown as InputPluginOption[];
return {
//入口
input: inputPath,
plugins,
//树摇
treeshake: true,
//排除一些依赖
external: full ? [] : await generateExternal(root),
//这个没有用忘删了
watch: watch ? watchOptions : false,
};
}
export async function build(root: string, options: Options = {}) {
//产出config
const config = await resolveConfig(root, options);
//生成bundle实例
const bundle = await rollup(config);
//写入产出
await writeBundles(
bundle,
resolveBuildConfig(root).map(
([module, _config]): OutputOptions => ({
format: _config.format,
dir: _config.output.path,
exports: module === 'cjs' ? 'named' : undefined,
sourcemap: options.sourcemap,
}),
),
);
}
// 下面是watch的删了就行
export async function watchFuc(root: string, options: Options = {}) {
const _config = await resolveConfig(root, options);
const watcher = rollupWatch(
resolveBuildConfig(root).map(([module, config]) => ({
..._config,
output: {
format: config.format,
dir: config.output.path,
exports: module === 'cjs' ? 'named' : undefined,
sourcemap: options.sourcemap,
},
})),
);
watcher.on('event', (event) => {
// 事件处理逻辑
if (event.code === 'START') {
console.log('Rollup build started...');
} else if (event.code === 'END') {
console.log('Rollup build completed.');
} else if (event.code === 'ERROR') {
console.error('Error during Rollup build:', event.error);
}
});
}
ts
//dts生成比较简单不做解释
//不选择rollup生成是因为现有的方案多多少少都存在一些问题
import path from 'node:path';
import { build } from 'tsup';
import type { Options } from './build';
import { DEFAULT, resolveInput, target } from './ustils';
import { rootPath } from './path';
export async function dts(root: string, options: Options = {}) {
const {
input = DEFAULT,
watch = false,
tsconfig = path.resolve(rootPath, 'tsconfig.json'),
} = options;
const outputPath = path.resolve(root, 'dist/types');
const inputPath = resolveInput(root, input);
await build({
entry: [inputPath],
dts: {
only: true,
},
outDir: outputPath,
tsconfig,
target,
watch,
});
}
发包我是通过workflow+exec实现的之前文章应该有说,但是普通的npm publish也可以,发包前记得打包
测试能否使用
执行pnpm create vite创建solid-js项目
安装 pnpm i @bees-ui/icons
App.tsx
tsx
import './App.css';
import { AccountBookFilled } from '@bees-ui/icons';
function App() {
return (
<>
<AccountBookFilled style={{ color: 'red' }} spin />
</>
);
}
export default App;
最后
我的项目:github.com/ikunOrg/bee...
简介:想着写一个ant风格solidjs组件库(其实是抄代码了哈哈)如果觉得不错点个星星或者和我一起来写
上述的icon包:www.npmjs.com/package/@be...
如果觉得文章不错欢迎点赞~