• 使用阿里开源的Dumi简化React自建组件库的开发
  • 发布于 2个月前
  • 302 热度
    0 评论
一、为什么开发企业专属组件库?

可能有些同学会问,我们有很多开源的UI库可用,为什么还需要自己去开发组件库呢?是的,大部分业务场景下,在一些要求UI不高,或者公司体量不大的情况下,这些开源的库足以满足我们日常开发的需求,但是一但达到一定的量级,公司就会升级视觉,交互,这时候如果有多个项目,共用了一套UI视觉设计,那么你怎么办呢?总不至于每个项目拷贝一份吧,此时我们的UI库就需要独立出来,打包成私有的NPM包,供给公司每个业务系统用。


二、怎么开始开发组件库呢?
开发组件库必不可少的考量,组件库的样式,组件模块化,文档,在这之前我是使用react脚手架魔改,搭配storybook写文档,不过现在我们有了更好的方案,这些配置我们都不用做了,直接使用阿里开源的Dumi方案,他已经为我们配置好了环境和文档,只需要我们按照规范开发就行了。
官网:https://d.umijs.org/

加上最近dumi升级到了2.0版本,使用起来更加的友好,总结起来就是,更好,更快,更便捷,当然更强!所以我们直接开撸!


三、安装配置Dumi
1、环境准备
确保正确安装 Node.js 且版本为 14+ 即可。
node -v v14.19.1
2、脚手架
# 先找个地方建个空目录。 
mkdir myapp && cd myapp 
# 执行npx create-dumi 命令,此时进入cli命令开始操作,如下图所示

等待片刻,完成所以的依赖项目
执行命令:npm run dev会打开一个本地端口为8000的服务

到这里已经成功了30%,让我们继续下面的操作
3、改的像一点
先改个名字,我们打开.dumirc.ts文件,这个是dumi的配置文件修改代码如下
themeConfig: {
    name: 'sslnui',
    nav: [
      { title: '介绍', link: '/guide' },
      { title: '组件', link: '/components/Foo' },
    ],
  },

没有问题的话就变成了这样。
介绍不会写怎么办,不慌,我们去github上拷贝个antd的,
打开docs文件夹下面的guide.md,将内容复制进去,该删除的删除点,然后跑起来就变成了这个样式,是不是瞬间就变的好看了起来,不会写md文件不要怕,没有什么是一个ctrl+v解决不了的问题。

四、先来个Button
通过上面的步骤我们基本上完成了文档的创建,编写完成了我们组件库的基本格式,下面让我们进入实战,写个button组件。
在src文件夹下面创建Button文件夹,在该文件夹下面创建index.tsx,index.md,index.less三个文件。
1、上个全局样式定义
全局的样式我这里定义在了src/global.less中,我们先在src文件夹下面创建global.less文件,因为我们是示例工程,属性我们就定义一个主题色,如果自己开发,需要定义主题颜色,字体,字号等所有的信息。
@sslnui-primary: #a862ea;
2、写个组件
下面我们开始写button组件。
首先在Button/index.tsx写入下面的代码
import classNames from 'classnames';
import React, { AnchorHTMLAttributes, ButtonHTMLAttributes, FC } from 'react';
import './index.less';

export type ButtonSize = 'lg' | 'sm';
export type ButtonType = 'primary' | 'default' | 'danger' | 'link';

interface BaseButtonProps {
  size?: ButtonSize;
  btnType?: ButtonType;
  children: React.ReactNode;
  href?: string;
  disabled?: boolean;
}

type NativeButtonProps = BaseButtonProps &
  ButtonHTMLAttributes<HTMLButtonElement>;
type AnchorButtonProps = BaseButtonProps &
  AnchorHTMLAttributes<HTMLAnchorElement>;
// 堆代码 duidaima.com
// 定义 Button 组件默认属性类型
interface ButtonDefaultProps {
  btnType?: ButtonType;
}

export type ButtonProps = Partial<
  NativeButtonProps & AnchorButtonProps & ButtonDefaultProps
>;

export const Button: FC<ButtonProps> = (props) => {
  const { btnType, size, children, href, disabled, ...restProps } = props;

  const classes = classNames('btn', btnType, size);

  if (btnType === 'link' && href) {
    return (
      <a className={classes} href={href} {...restProps}>
        {children}
      </a>
    );
  } else {
    return (
      <button
        className={classes}
        disabled={disabled}
        {...restProps}
        type="button"
      >
        {children}
      </button>
    );
  }
};

// 设置 Button 组件的默认属性
Button.defaultProps = {
  btnType: 'default',
};

export default Button;
然后写less样式,代码如下:
@import '../global.less';
.btn {
  // width: 100px;
  padding: 8px 16px;
  border-width: 0px;
  cursor: pointer;
}

.primary {
  border-radius: 8px;
  // padding: 8px;
  background: @sslnui-primary;
  font-family: Source Han Sans CN;
  font-size: 14px;
  font-weight: 500;
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  letter-spacing: 0em;
  color: #ffffff;
  // transition: all ease-in-out 0.15s;

  &:hover {
    background: @sslnui-primary;
  }
}

.default {
  border-radius: 8px;
  // padding: 8px 14px;
  background: #f0f2f5;
  font-family: Source Han Sans CN;
  font-size: 14px;
  font-weight: 500;
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  letter-spacing: 0em;
  color: #5b667c;
  transition: all ease-in-out 0.15s;

  &:hover {
    color: #0e1420 !important;
  }
}

.link {
  outline: none;
  position: relative;
  display: inline-block;
  font-weight: 400;
  white-space: nowrap;
  text-align: center;
  background-image: none;
  background-color: transparent;
  border: 1px solid transparent;
  cursor: pointer;
  transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
  user-select: none;
  touch-action: manipulation;
  line-height: 1.5714285714285714;
  color: rgba(0, 0, 0, 0.88);
  color: @sslnui-primary;

  &:hover {
    color: @sslnui-primary;
  }
}

.danger {
  border-radius: 8px;
  // padding: 8px 14px;
  background: #ffffff;
  box-shadow: 2px 0px 6px 0px rgba(0, 0, 0, 0.07);
  font-family: Source Han Sans CN;
  font-size: 14px;
  font-weight: 500;
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  letter-spacing: 0em;
  color: #0e1420;
}

.disabled {
  cursor: not-allowed;
  border-radius: 8px;
  background: rgba(71, 92, 246, 0.4);

  &:hover {
    background: rgba(71, 92, 246, 0.4);
  }
}

.lg {
  width: 120px;
  height: 38px;
}

.sm {
  width: 100px;
}
最后我们在index.md文件中编写文档。
按钮用于开始一个即时操作。
## 代码演示
import React from 'react';
import { Button } from 'sslnui';

export default () => {
  return (
    <>
      <Button btnType="default">默认按钮</Button> &nbsp;
      <Button btnType="primary">主要按钮</Button>
    </>
  );
};
此时让我们进入到组件目录下,点击button按钮

如果你的组件库也是这个样子,那标志着你已经学会了如何自建组件库的80% 。
五、代码有测试
程序有测试代码才健壮,下面让我们安装jest生态,对我们的代码进行自动化测试
cnpm i jest @testing-library/react @types/jest ts-jest jest-environment-jsdom jest-less-loader typescript@4 -D
命令执行完毕后,我们在根目录下创建jest.config.js文件
jest配置如下:
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',
  roots: ['./src'], // 查找src目录中的文件
  collectCoverage: true, // 统计覆盖率
  coverageDirectory: 'coverage', // 覆盖率结果输出的文件夹
  transform: {
    '.(less|css)$': 'jest-less-loader', // 支持less
  },
  // 单元覆盖率统计的文件
  collectCoverageFrom: [
    'src/**/*.tsx',
    'src/**/*.ts',
  ],
};
然后再src/Button目录下创建测试文件index.test.tsx,内容如下:
import { fireEvent, render } from '@testing-library/react';
import React from 'react';
import Button from './index';

describe('Button组件', () => {
  it('能够正确渲染按钮文字', () => {
    const buttonText = '正确';
    const { getByRole } = render(<Button>{buttonText}</Button>);
    const buttonElement = getByRole('button');
    expect(buttonElement.innerHTML).toBe(buttonText);
  });

  it('能够正确渲染主要样式的按钮', () => {
    const { getByRole } = render(<Button btnType="primary">主要按钮</Button>);
    const buttonElement = getByRole('button');
    expect(buttonElement.classList.contains('primary')).toBe(true);
  });

  it('能够触发点击事件', () => {
    const handleClick = jest.fn();
    const { getByRole } = render(
      <Button btnType="primary" onClick={handleClick}>
        点击按钮
      </Button>,
    );
    const buttonElement = getByRole('button');
    fireEvent.click(buttonElement);
    // 断言函数被调用了一次。
    expect(handleClick).toHaveBeenCalledTimes(1);
  });
});
配置好后我们在控制台执行npx jest就会对代码库进行全量检查,如果你的控制台也输出这样,表示是成功的

六、修改为按需测试
上面的测试文件每次执行会进行全量测试,这样比较耗时,而且我们不想对未发生更改的组件也进行测试,只想测试我们修改过的文件
下面针对上面的问题,我们修改测试,可以使用git diff --staged --diff-filter=ACMR --name-only命令获取到本次修改的文件列表,然后进行分析需要执行哪些单元测试,通过--findRelatedTests参数去精准执行对应的单元测试文件。
我们在根目录下新建jest.staged.js
内容如下:
const fs = require('fs').promises;
const path = require('path');
const { execSync } = require('child_process');

/** 处理jest只执行本次修改到的工具方法内的测试用例 */
async function start() {
  /** 1. 获取git add 的文件的列表 */
  const addFiles = execSync(`git diff --staged --diff-filter=ACMR --name-only`)
    .toString()
    .split('\n');
  /** 2. 获取文件的绝对路径 */
  const diffFileList = addFiles
    .filter(Boolean)
    .map((item) => path.join(__dirname, item));

  /** 3. 获取src源码目录 */
  const srcPath = path.join(__dirname, './src');

  /** 4. 记录本次修改的函数方法 */
  const diffFileMap = {};
  diffFileList.forEach((filePath) => {
    if (
      filePath.includes(srcPath) &&
      (filePath.endsWith('.ts') || filePath.endsWith('.tsx'))
    ) {
      const relativePath = path.relative(srcPath, filePath);
      if (relativePath.includes('/')) {
        diffFileMap[relativePath.split('/')[0]] = true;
      }
    }
  });

  console.log('本次改动的方法', Object.keys(diffFileMap));

  /** 5. 找到改动方法下面所有的单元测试文件 */
  const list = (
    await Promise.all(
      Object.keys(diffFileMap).map(async (toolPath) => {
        const testsDir = path.join(srcPath, toolPath, '__tests__');
        try {
          const files = await fs.readdir(testsDir);
          return files.map((item) => path.join(testsDir, item));
        } catch (error) {
          return [];
        }
      }),
    )
  ).flat();

  /** 6. 执行单元测试脚本 */
  if (list.length) {
    try {
      execSync(`npx jest --bail --findRelatedTests ${list.join(/ /)}`, {
        cwd: __dirname,
        stdio: 'inherit',
      });
    } catch (error) {
      process.exit(1);
    }
  }
}

start();
然后在package中添加命令
scripts": { "test:staged": "node jest.staged.js" }

然后再控制台执行npm run test:staged就可以只针对变动的地方测试了。


七、打包发布
打包发布很简单,打包时npm给我们配置好的,只需要执行npm run build即可打包出文件,然后我们去npm官方注册个账号。
注册成功后在控制台执行npm login,如果能链接到国外的npm,就可以输入账号,密码完成登陆,这里记得翻墙,
完成登陆后执行执行 npm publish就可以发布到npm了!

好了以上就是我的组件库方案!
用户评论