如何搞一个ant风格的solidjs icon包

前言

最近在抄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...

如果觉得文章不错欢迎点赞~

相关推荐
小白小白从不日白3 分钟前
react 组件通讯
前端·react.js
罗_三金13 分钟前
前端框架对比和选择?
javascript·前端框架·vue·react·angular
Redstone Monstrosity20 分钟前
字节二面
前端·面试
东方翱翔27 分钟前
CSS的三种基本选择器
前端·css
Fan_web1 小时前
JavaScript高级——闭包应用-自定义js模块
开发语言·前端·javascript·css·html
yanglamei19621 小时前
基于GIKT深度知识追踪模型的习题推荐系统源代码+数据库+使用说明,后端采用flask,前端采用vue
前端·数据库·flask
千穹凌帝1 小时前
SpinalHDL之结构(二)
开发语言·前端·fpga开发
dot.Net安全矩阵1 小时前
.NET内网实战:通过命令行解密Web.config
前端·学习·安全·web安全·矩阵·.net
叫我:松哥1 小时前
基于Python flask的医院管理学院,医生能够增加/删除/修改/删除病人的数据信息,有可视化分析
javascript·后端·python·mysql·信息可视化·flask·bootstrap
Hellc0071 小时前
MacOS升级ruby版本
前端·macos·ruby