手撸React组件库前必须清楚的9个问题

1. 组件库文档问题

以前常用的组件库文档storybook,包括现在也有用dumi、vitepress做组件库文档等。storybook缺陷不美观、webpack的热更新太慢,尽管新版别支撑了vite进步了速度但还不算安稳。好在各种文档、mdx、测验等组件第三方工具很多集成进去能很方便咱们开发。dumi、vitepress尽管颜值高,但是在针对ui组件库层面还有待加强,更偏向于做静态网站。这儿讲下我以为文档工具最重要的几个方面,如果今后要自己完成一个文档框架或工具需求考虑的。

怎么根据文件层级的依靠联系烘托ui

dumi、storybook等文档都会经过node监听文件是否有改动,一旦有改动那么咱们去递归检查一切内部文件的依靠联系然后界说一个目标来描述,这儿会涉及exclude、include等装备,无非一些读取装备的操作,在md文档中更能够解析内部的特定格局的字符串来完成装备, 然后经过js来操作网站左边的sidebar的层级联系,一同装备路由跳转。

怎么在写代码的时分自动生成文档

storybook能够经过写注释代码来解析获取装备。其实便是读取文件的字符串经过一些开源库解析成ast树然后判别位置然后用node的fs来写入md文件,然后将md转为html被网站识别展示。那么咱们能够用babel写一个插件在运行时解析对应代码生成实时的文档数据。例如以下:

function sayHi (name: string, age: number, a: boolean) {
  console.log(`hi, ${name}`);
  return `hi, ${name}`;
}

转换为 ->

##sayHi
say 你好
name: 名字
>sayHi(name: string, age: number, a: boolean)
#### Parameters:
-name(string)
-age(number)
-a(boolean)

下面的代码简略看下就行,visitor类似在每个节点套了个壳去拜访而不影响本来的节点。里面界说了解析的各个节点,然后经过path的api或许各种terver的包批量修正。你只需知道下面做了两件工作,一个是生成类似vue的描述虚拟节点的集合,经过这个生成md的模板文件。

const autoDocsPlugin = declare((api, options, dirname) => {
  api.assertVersion(7);
  return {
    pre (file) {
      file.set('docs', []);
    },
    visitor: {
      FunctionDeclaration (path, state) {
        const docs = state.file.get('docs')
        docs.push({
          type: 'function',
          name: path.get('id').toString(),
          params: path.get('params').map(paramsPath => {
            return {
              name: paramsPath.toString(),
              type: resolveType(paramsPath.getTypeAnnotation()) //get type
            }
          }),
          return: resolveType(path.get('returnType').getTypeAnnotation()),
          doc: path.node.leadingComments && parseComment(path.node.leadingComments[0].value)
        })
        state.file.set('docs', docs)
      },
      ClassDeclaration (path, state) {
        const docs = state.file.get('docs');
        const classInfo = {
          type: 'class',
          name: path.get('id').toString(),
          constructorInfo: {},
          methodsInfo: [],
          propertiesInfo: []
        };
        if (path.node.leadingComments) {
          classInfo.doc = parseComment(path.node.leadingComments[0].value);
        }
        path.traverse({
          ClassProperty (path) {
            classInfo.propertiesInfo.push({
              name: path.get('key').toString(),
              type: resolveType(path.getTypeAnnotation()),
              doc: [path.node.leadingComments, path.node.trailingComments].filter(Boolean).map(comment => {
                return parseComment(comment.value);
              }).filter(Boolean)
            })
          },
          ClassMethod (path) {
            if (path.node.kind === 'constructor') {
              classInfo.constructorInfo = {
                params: path.get('params').map(paramPath => {
                  return {
                    name: paramPath.toString(),
                    type: resolveType(paramPath.getTypeAnnotation()),
                    doc: parseComment(path.node.leadingComments[0].value)
                  }
                })
              }
            } else {
              classInfo.methodsInfo.push({
                name: path.get('key').toString(),
                doc: parseComment(path.node.leadingComments[0].value),
                params: path.get('params').map(paramPath => {
                  return {
                    name: paramPath.toString(),
                    type: resolveType(paramPath.getTypeAnnotation())
                  }
                }),
                return: resolveType(path.getTypeAnnotation())
              })
            }
          }
        });
        docs.push(classInfo);
        state.file.set('docs', docs);
      }
    },
    post (file) {
      const docs = file.get('docs');
      const res = generate(docs, options.format);
      fse.ensureDirSync(options.outputDir);
      fse.writeFileSync(path.join(options.outputDir, 'docs' + res.ext), res.content);
    }
  }
})

2. react开发的组件要留意什么

export const verticalMenu: ComponentStory<typeof Menu> = () => (
  <>
    <PageHeader title="垂直"></PageHeader>
    <Menu mode="vertical" onSelect={(index) => console.log(index)}>
      <MenuItem>标签1</MenuItem>
      <MenuItem>标签2</MenuItem>
      <MenuItem>标签3</MenuItem>
      <SubMenu title="标签4">  
        <MenuItem>标签1</MenuItem>
        <MenuItem>标签2</MenuItem>
        <MenuItem>标签3</MenuItem>
      </SubMenu>
    </Menu></>
);

在antd中咱们经常能看到各种各样的嵌套, 比如这儿的SubMenu组件的title特点,很显然Menu父组件要遍历children,找到对应的tag,然后将props传入操控对应的子组件。或许你也能够用Provider和Consumer。这样的优点更多的标签能够协助咱们了解,对props做分层操控。下面举个比如, 这儿要留意下Children和cloneElement这两个核心api的运用。

const renderChildren = () => {
    return React.Children.map(children, (child, index) => {
      const childElement = child as React.FunctionComponentElement<IMenuItemProps>;
      const { displayName } = childElement.type;
      if (displayName === 'MenuItem' || displayName === 'SubMenu') {
        return React.cloneElement(childElement, {
          index: index.toString(),
        });
      } else {
        console.error('Warning: Menu has a child which is not a MenuItem component');
      }
    });
  };

在一些常见你也能够用类的写法,在antd中经过hoc高阶组件来完成比如Layout组件。

function generator({ suffixCls, tagName, displayName }: GeneratorProps) {
  return (BasicComponent: any) => {
    const Adapter = React.forwardRef<HTMLElement, BasicProps>((props, ref) => {
      return <BasicComponent ref={ref} prefixCls={suffixCls} tagName={tagName} {...props} />;
    });
    if (process.env.NODE_ENV !== 'production') {
      Adapter.displayName = displayName;
    }
    return Adapter;
  };
}
const Header = generator({
  suffixCls: "speed-header",
  tagName: "header",
  displayName: "Header",
})(Basic);
const Footer = generator({
  suffixCls: "speed-footer",
  tagName: "footer",
  displayName: "Footer",
})(Basic);

在一些弹窗组件你能够看到一个回调就能够执行组件的调用,显现躲藏、操控时间、结束的回调等。在vue也一样经过install函数传入Vue实例,然后挂载对应的sfc文件,$mount挂载对应dom。生成的组件实例赋值给Vue的原型目标,咱们能够调用对应的方法来操控大局组件。

function createNotification() {
  const div = document.createElement('div')
  document.body.appendChild(div)
  const notification = ReactDOM.render(<Toast />, div)
  return {
      addNotice(notice) {
          return notification.addNotice(notice)
      },
      destroy() {
          ReactDOM.unmountComponentAtNode(div)
          document.body.removeChild(div)
      }
  }
}

在一些Table表单组件、日历等组件是需求很大的自界说地步的,不能仅仅靠children解析来完成,那么你要考虑props传入对应的JSX的节点,例如下面:

const customTableTpl: ComponentStory<typeof Table> = args => {
  const [dataSource, setDataSource] = React.useState(defaultDataSource);
  const [paginationParams, setPaginationParams] = React.useState(defaultpaginationParams);
  const [isModalVisible, setIsModalVisible] = React.useState(false);
  /** input 单元格 */
  const hanldeBlur = e => {
    let val = e.target.value;
    if (val.trim()) {
      let cloneData = [...dataSource];
      cloneData.forEach((item, index) => {
        if (item.key === source.key) {
          cloneData[index].name = val;
        }
      });
      setDataSource(cloneData);
    }
  };
  /** 修改操作 单元格 */
  const showModal = () => {
    setIsModalVisible(true);
  };
  const handleConfirm = () => {
    setIsModalVisible(false);
  };
  const handleCancel = () => {
    setIsModalVisible(false);
  };
  const handleDelete = key => {
    let result = dataSource.filter(item => item.key !== key);
    setDataSource(result);
    setPaginationParams({
      ...paginationParams,
      total: paginationParams.total - 1,
    });
  };
  const columns = [
    {
      title: 'ID',
      dataIndex: 'key',
      key: 'key',
    },
    {
      title: '姓名',
      dataIndex: 'name',
      key: 'name',
      render: source => {
        if (!source) return;
        return <Input placeholder={source.name} onBlur={hanldeBlur} style={{ width: '200px' }} blurClear></Input>;
      },
    },
    {
      title: '年纪',
      dataIndex: 'age',
      key: 'age',
      render: source => {
        if (!source) return;
        return <Button>{source.age}</Button>;
      },
    },
    {
      title: '住址',
      dataIndex: 'address',
      key: 'address',
    },
    {
      title: '操作',
      dataIndex: 'edit',
      key: 'edit',
      render: source => {
        return (
          <>
            <Modal visible={isModalVisible} onConfirm={handleConfirm} onCancel={handleCancel}>
              <h2>我是{source.name}</h2>
            </Modal>
            <Space>
              <Button btnType='primary' onClick={showModal}>
                修改
              </Button>
              <Button btnType='danger' onClick={() => handleDelete(source.key)}>
                删去
              </Button>
            </Space>
          </>
        );
      },
    },
  ];
  return (
    <>
      <PageHeader title='自界说表格' />
      <Table dataSource={dataSource} columns={columns} paginationParams={paginationParams}></Table>
    </>
  );
};

在antd运用过程中你或许会遇到<From.FromItem /> 其实也便是导出的模块目标Form内部界说个特点,Form.Item = FormItem;

3. 组件库的主题怎么做、款式用什么计划

antd5以前,本来用了less的计划在网站的加载前置恳求link,动态获取css的装备,那么关于设计师修正计划的主题或许是一种摧残,你不能定位到组件等级的ui。less在某种层面导致对款式的可控性有很大的局限。less和scss一样能够经过js操控对应的款式变量,那么这个变量在scss或许less中能够被操控

@mixin theme-aware($key, $color) {
	@each $theme-name, $theme-color in $themes {
		.theme-#{"" + $theme-name} & {
			#{$key}: map-get(map-get($themes, $theme-name), $color);
		}
	}
}
/**
 * 这儿界说了map,对应多个主题面板,经过mixin函数获取对应的map的值,在对应的标签内运用@include 运用掩盖函数
 */
$themes: (
	light: (
		global-background: #fff,
		global-color: #37474f,
	),
	dark: (
		global-background: #37474f,
		global-color: #fff,
	),
	blue: (
		global-background: #10618a,
		global-color: #fff,
	),
);
body {
  @include theme-aware("background", "global-background");
  @include theme-aware("color", "global-color");
}
button {
	.theme-light & {
		background: #f30000;
	}
	.theme-dark & {
		background: #ee0fd0;
	}
}
//这儿经过js操控
  const changeTheme = (theme: string) => {
    document.documentElement.className = "";
    document.documentElement.classList.add(`theme-${theme}`);
  };

这种计划局限在只能操控大局的,无法细粒度的修正,就算修正也是很费事的一件事,一同维护也挺累的,不断的@include。

社区有很多计划,比如cssmodule、原子化的css、运行时的cssinjs、编译时的cssinjs。都各有利弊。那么咱们先来看看最近antd5做了什么吧,用了自研的运行时的cssinjs计划,优点是可控性更强了,在组件等级的更新相比emotion等计划有更好的功能,token的操控hash能够让组件的款式被缓存。我这儿写一写本来的老版别antd的大致的款式思路,在antd的会尽量很少用style挂载款式,尽量经过classNames这个库做的名更新。一同为了阻隔,经过大局的变量方法在各个组件内获取类名的前缀来阻隔,下面做个简略的演示。

  //....
  const { getPrefixCls } = useContext(ConfigContext);
  let prefixCls = getPrefixCls("notification", customizePrefixCls);
    const cls = classNames(prefixCls, className, {
    [`${prefixCls}-tc`]: position === 'tc',
    [`${prefixCls}-tl`]: position === 'tl',
    [`${prefixCls}-tr`]: position === 'tr',
    [`${prefixCls}-bc`]: position === 'bc',
    [`${prefixCls}-bl`]: position === 'bl',
    [`${prefixCls}-br`]: position === 'br',
  });
    return (
    < >
      {
        notices.map((notice, index) => {
          return (
            <div className={cls} style={getStyles()} key={index}
              <div className={`${prefixCls}-card`}>
                {
                  iconJSX ? iconJSX : <Icon icon={solid('check')} size='2x' color='#18ce55'></Icon>
                }
                <div className={`${prefixCls}-warp`}>
                  <h4>{notice.title}</h4>
                  <p className={`${prefixCls}-content`}>{notice.content}</p>
                </div>
              </div>
            </div>
          )
        })
      }
    </>
  );

如果自己完成,那么个人推荐用cssinjs的社区计划,更好的兼容、变量的同享、阻隔化、代码提示、主题定制更灵敏可控。缺陷便是hash序列化阻隔的时分功能微微的损耗,打包的体积大些。

4. 组件库用什么打包

组件库的代码调试的时分,其实能够考虑vite做项目的构建,更快的单模块恳求更新开发体验仍是很棒的。由于vite需求esm模块,用其他模块会有问题,如果你的项目及其依靠都是esm那么能够考虑vite打包, vite也是能够运用rollup生态的。rollup更合适小库的打包,静态分析很不错,treeshaking给力。打包上手及其简略。gulp相对合适中大项目,超卓的串行和并行的工作流,更规范和可控。webpack也能够但是有点笨重了,便是没有gulp轻量和相对简略。

//...
function compileScripts (babelEnv, destDir) {
  const { scripts } = paths;
  process.env.BABEL_ENV = babelEnv;
  return gulp
    .src(scripts)
    .pipe(babel({
      "presets": [
        [
          "@babel/preset-react",
          { "runtime": "automatic", "importSource": "@emotion/react" }
        ]
      ],
      "plugins": ["@emotion/babel-plugin"]
    })) // 运用gulp-babel处理
    .pipe(gulp.dest(destDir));
}
/**
 * 编译cjs
 */
function compileCJS () {
  const { dest } = paths;
  return compileScripts('cjs', dest.lib);
}
/**
 * 编译esm
 */
function compileESM () {
  const { dest } = paths;
  return compileScripts('esm', dest.esm);
}
//...
const buildScripts = gulp.series(compileCJS, compileESM);
const buildSDK = gulp.series(compressionSdkCJS, compressionSdkESM);
const buildStyle = gulp.parallel(buildBasicStylesMin, buildBasicStylesMax, buildSpeedStylesMax, buildSpeedStylesMin, buildComponentStyle);
// 整体并行执行任务
const build = gulp.parallel(buildTypes, buildScripts, buildSDK, buildStyle);

5. typescript在组件库中怎么用

这儿简略展示下大致的tsx文件的组件的ts类型界说。还有一些其他要留意的比如类型断语as,在ref获取dom节点的去操作的时分ts或许会提示你或许undefined,这个时分你能够很定心的告诉他是一定存在的。

export interface ResultProps {
  /** 款式命名阻隔 */
  prefixCls?: string;
  /** 组件子节点 */
  children?: ReactNode;
  /** 容器内联款式 */
  style?: React.CSSProperties;
  /** 组件类名 */
  className?: string;
  /** 操作区 */
  extra?: React.ReactNode;
  /** 自界说icon */
  icon?: React.ReactNode;
  /** 改变的回调 */
  onChange: (e: ChangeEvent<HTMLInputElement>) => void;
  /** 失掉焦点的回调 */
  onBlur: (e: ChangeEvent<HTMLInputElement>) => void;
  /** 状况 */
  status?: StatusType;
  /** 主标题 */
  title?: React.ReactNode;
  /** 副标题 */
  subTitle?: React.ReactNode;
}
const Result: FC<ResultProps> = props => {
  const {
    children,
    className,
    prefixCls: customizePrefixCls,
    style,
    title,
    subTitle,
    icon,
    extra,
    status = 'success',
  } = props;
  //...
  return (
    <div className={cls} style={style}>
    </div>
  );
};

6. 单元测验怎么做

咱们会在每个文件夹都界说一个__tests__文件夹,那么你要留意这个文件夹不能被gulp打包进入。
默认咱们运用的是@testing-library/react、’@testing-library/jest-dom, 操作跟jest很相似,一个是react的轻量的测验库、一个是jest的dom的一些api集成。当然还有其他库能够运用,下面我简略用了比如

要特别留意组件库或许并不合适TDD开发,测验驱动开发。咱们能够在写完组件后,针对关键功能做测验用例,更关注结果,里面繁杂的处理过程一定要忽视。

咱们一般都常用的便是快照测验、款式的判别、dom的判别、一些异步推迟的场景判别。

import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import { Calendar } from '../../index';
describe('test Calendar component', () => {
  it('should render base Calendar', () => {
    const { asFragment } = render(<Calendar></Calendar>);
    expect(asFragment()).toMatchSnapshot();
  });
  it('callback is successfully after click event', () => {
    const handleSelect = jest.fn();
    const handlePanelChange = jest.fn();
    render(<Calendar onSelect={handleSelect} onPanelChange={handlePanelChange}></Calendar>);
    fireEvent.click(screen.getByText('15'));
    fireEvent.click(screen.getByText('>'));
    fireEvent.click(screen.getByText('<'));
    expect(handlePanelChange).toHaveBeenCalledTimes(2);
    expect(handleSelect).toHaveBeenCalledTimes(2);
  });
  it('should find classnames', () => {
    const { container } = render(<Calendar></Calendar>);
    expect(container.querySelector('.speed-calendar')).toBeTruthy();
    expect(container.querySelector('.speed-calendar-picker')).toBeTruthy();
    expect(container.querySelector('.speed-calendar-arrow')).toBeTruthy();
    expect(container.querySelector('.speed-calendar-detail')).toBeTruthy();
    expect(container.querySelector('.speed-calendar-week')).toBeTruthy();
    expect(container.querySelector('.speed-calendar-day')).toBeTruthy();
  });
  it('custom data in the Calendar component', () => {
    const Demo = () => {
      let date = new Date();
      const customData = [
        {
          day: 1,
          month: date.getMonth() + 1,
          year: date.getFullYear(),
          getNode: () => {
            return (
              <div style={{ display: 'flex' }}>
                <div>吃饭</div>
                <div
                  style={{
                    background: 'red',
                    width: '5px',
                    height: '5px',
                    borderRadius: '50%',
                    position: 'relative',
                    left: '5px',
                  }}
                ></div>
              </div>
            );
          },
        },
        {
          day: 2,
          month: date.getMonth() + 1,
          year: date.getFullYear(),
          getNode: () => {
            return <div>睡觉</div>;
          },
        },
      ];
      return <Calendar customData={customData}></Calendar>;
    };
    const { asFragment } = render(<Demo></Demo>);
    expect(asFragment()).toMatchSnapshot();
    expect(screen.getByText('吃饭')).toBeInTheDocument();
    expect(screen.getByText('睡觉')).toBeInTheDocument();
  });
});

7. 组件库怎么本地调试

这儿要留意你的package的装备

//package.json 
  {
    "types": "dist/types/index.js",
    "main": "dist/lib/components/index.js",
    "module": "dist/esm/components/index.js",
    "files": [
    "dist"
    ],
  }

1. npm run link

最快速的一般经过 npm run link 能够找到大局的npm文件夹内找到,这个时分npm i xxx -g就能够了。

2. Verdaccio

经过建立私有库房npm,需求在自己的服务器上部署Verdaccio, npm生成新的镜像源,然后输入用户名和暗码衔接成功后,nrm use <自己的镜像地址名称>

3. package装备

在你要引进的项目的package.json中装备加个link装备个相对途径,个人以为这个是最简略的

devDependencies: {
    "yourPackage": "link:../../dist"  //留意这儿的link哈!
}

8. 开发中怎么进步功率,削减重复代码

如果你真的写过组件库那么你会感到组件库是一个很繁琐,倒不是说没有技术含量。每次当你创建文件夹,复制粘贴其他组件的重复代码其实是一个很苦楚的问题。所以一定需求这么一个模板,我输入一行命令行直接给我生成组件文件夹、组件tsx、根底款式、文档阐明、根底测验用例。我敢说真能削减30%的工作量了。腾出的时间去做些其他有技术含量的东西他不香么?

ejs模板解析

当然我目前完成上仍是有问题,我用的ejs模板做解析,ejs模板只是处理字符串罢了做个变量的替换只需替换文件后缀名就能够了,留意这儿要去掉prettier或许vscode的代码格局约束,经过noode的argv来获取的组件参数名来操控创建的文件夹,那么有了这个组件名,咱们能够将字符串解析并替换。其他的无非node的读写到对应文件罢了。留意ejs如同对css的语法无法做解析。读者能够考虑用vue cli内部运用的库来完成模板解析。这儿暂时用ejs演示详细用法.

/**
 * @description 命令行直接生成组件开发模板
 * 
 * 在命令行输入 node src/generator.ts Test  留意要大写
 */
const fs = require('fs')
const path = require('path')
let ejs = require('ejs')
let prettier = require('prettier')
const componentName = process.argv[2] || 'TestComponent'
const lowerName = componentName.toLowerCase()
const templatePath = path.join(__dirname, 'components', 'Template')   //模板途径
const toPath = path.join(__dirname, 'components', componentName)   //生成途径
const stylePath = path.join(__dirname, 'styles', 'componentStyle')   //生成途径
console.log(`当时正在生成${process.argv[2]}组件模板.....`);
function copyDir (srcDir, desDir) {
  fs.readdir(srcDir, { withFileTypes: true }, (err, files) => {
    if (fs.existsSync(desDir)) {
      console.log("无法掩盖原文件, 请删去已有文件夹");
      return
    } else {
      fs.mkdirSync(desDir);
    }
    for (const file of files) {
      //判别是否为文件夹
      if (file.isDirectory()) {
        const dirS = path.resolve(srcDir, file.name);
        const dirD = path.resolve(desDir, file.name);
        //判别是否存在dirD文件夹
        if (!fs.existsSync(dirD)) {
          fs.mkdir(dirD, (err) => {
            if (err) console.log(err);
          });
        }
        copyDir(dirS, dirD);
      } else {
        function handleOutputFilename (name) {
          if (name === 'template.stories.ejs') {
            return `${lowerName}.stories.tsx`
          }
          if (name === 'Template.ejs') {
            return `${componentName}.tsx`
          }
          if (name === 'index.ejs') {
            return `index.ts`
          }
          if (name === 'style.ejs') {
            return `${lowerName}.scss`
          }
        }
        const srcFile = path.resolve(srcDir, file.name);
        let desFile  //输出的途径
        let desName = handleOutputFilename(file.name)   //输出的文件名
        //如果是款式途径
        if (desName.includes('scss')) {
          desFile = path.resolve(stylePath, desName);
        } else {
          //如果是文件途径
          desFile = path.resolve(desDir, desName);
        }
        fs.copyFileSync(srcFile, desFile);
        //传入ejs烘托
        const template = fs.readFileSync(desFile, {
          encoding: 'utf-8'
        })
        const code = ejs.render(template, { name: componentName, lowname: lowerName })
        let newCode = prettier.format(code, {
          parser: "babel-ts"
        }); //格局化
        fs.writeFileSync(desFile, newCode)
      }
    }
  })
}
copyDir(templatePath, toPath)

9. 怎么完成按需加载的组件库

咱们知道在以前按需加载经过babel-import-plugin引进,原理也很简略便是babel解析ast做个模块导入的转换。我去尝试运用感觉有问题,或许跟我组件库的文件目录不是很契合。哈哈,所以自己搞了一个类似的babel插件。

有人或许会说现在esm的tree-shaking不是已经能够了么,但其实在副作用的函数调用,传参是个目标那么你打包后你能够看下仍是会被引进。更不用说其他特别的副作用。当然rollup内部的一些算法能处理这些问题。所以esm你能够不考虑这个问题,那么其他模块比如common、cmd、amd咱们仍是考虑下吧。一同你要打包出来的文件要拆分好,css、js不同模块文件。下面我做个此插件的简略原理完成:

手撸一个

module.exports = function (api, options) {
  return {
    visitor: {
      ImportDeclaration (path, state) {
        function outStyleImportAst (libName, stylePath, targetName) {
          const buildRequire = api.template(`
          import "${libName}/${stylePath}/${targetName}";
        `);
          let ast = buildRequire()
          return ast
        }
        function outCompImportAst (libName, stylePath, targetName) {
          const buildRequire = api.template(`
          import ${targetName} from "${libName}/${stylePath}/${targetName}";
        `);
          let ast = buildRequire()
          return ast
        }
        function generatorCompImport (name) {
          const { libName, componentPath } = options
          let compNodes = outCompImportAst(libName, componentPath, name)
          path.insertAfter(compNodes)
        }
        function generatorStyleImport (name) {
          const { libName, stylePath, styleOneLower } = options
          let cssName = name
          if (styleOneLower) {
            cssName = cssName.substring(0, 1).toLowerCase() + cssName.substring(1)
          }
          let styleNodes = outStyleImportAst(libName, stylePath, `${cssName}.css`)
          path.insertAfter(styleNodes)
        }
        if (path.node.type === 'ImportDeclaration') {
          if (path.node.source.type === 'StringLiteral') {
            const { libName } = options
            let souceName = path.node.source.value
            if (souceName === libName) {
              const importList = []  //考虑按需导入多个
              if (Array.isArray(path.node.specifiers)) {
                const specifiers = path.node.specifiers
                specifiers.forEach(item => importList.push(item.imported.name))
              }
              for (const name of importList) {
                generatorStyleImport(name)
              }
              for (const name of importList) {
                generatorCompImport(name)
              }
              path.remove()
            }
          }
        }
      }
    }
  }
}

完成挺简略的,有兴趣的能够看下这个库房

ccj-007/babel-plugin-idea: 开发一些babel插件……. (github.com)

babel.config.js装备

 module.exports = {
  "plugins": [
  [
    require('../babel-preset-import-plugin'),
    {
      "libName": "react-speed-ui",  //组件库名
      "stylePath": "dist/css/components", //款式途径
      "styleOneLower": true,  //款式文件首字母是否大写
      "componentPath": "dist/lib/components", //组件文件途径
    },
  ],
];
}

阐明

以上为个人了解,花了几个小时写完来总结和分享自己写组件库的考虑,存在很多问题,期望我们一同交流提升你我。下面是个人完成一个组件库,觉得不错能够一个star鼓舞鼓舞,当然bug不免,互相学习进步生长。

ccj-007/react-speed-ui: ⚡一个追求极致功能的react组件库 (github.com)