reactwebpackJavaScriptreact如何自建组件库,并发布到npm?
## 一、为什么开发企业专属组件库? 可能有些同学会问,我们有很多开源的UI库可用,为什么还需要自己去开发组件库呢?是的,大部分业务场景下,在一些要求UI不高,或者公司体量不大的情况下,这些开源的库足以满足我们日常开发的需求,但是一但达到一定的量级,公司就会升级视觉,交互,这时候如果有多个项目,共用了一套UI视觉设计,那么你怎么办呢?总不至于每个项目拷贝一份吧,此时我们的UI库就需要独立出来,打包成私有的NPM包,供给公司每个业务系统用。 ## 二、怎么开始开发组件库呢? 开发组件库必不可少的考量,组件库的样式,组件模块化,文档,在这之前我是使用react脚手架魔改,搭配storybook写文档,不过现在我们有了更好的方案,这些配置我们都不用做了,直接使用阿里开源的Dumi方案,他已经为我们配置好了环境和文档,只需要我们按照规范开发就行了 > 官网[:https://d.umijs.org/](https://d.umijs.org/) 加上最近dumi升级到了2.0版本,使用起来更加的友好,总结起来就是,更好,更快,更便捷,当然更强!所以我们直接开撸 ## 三、安装配置Dumi #### 1、环境准备 确保正确安装 [Node.js](https://nodejs.org/en/) 且版本为 14+ 即可。 ```js node -v v14.19.1 ``` #### 2、脚手架 ```js # 先找个地方建个空目录。 mkdir myapp && cd myapp # 执行npx create-dumi 命令,此时进入cli命令开始操作,如下图所示 ```  等待片刻,完成所以的依赖项目 执行命令:npm run dev会打开一个本地端口为8000的服务  到这里已经成功了30%,让我们继续下面的操作 #### 3、改的像一点 先改个名字,我们打开.dumirc.ts文件,这个是dumi的配置文件修改代码如下 ```js 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文件 因为我们是示例工程,属性我们就定义一个主题色,如果自己开发,需要定义主题颜色,字体,字号等所有的信息 ```js @sslnui-primary: #a862ea; ``` #### 2、写个组件 下面我们开始写button组件 首先在Button/index.tsx写入下面的代码 ```js 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>; // 定义 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样式,代码如下: ```js @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文件中编写文档 ```js # Button 按钮用于开始一个即时操作。 ## 代码演示 import React from 'react'; import { Button } from 'sslnui'; export default () => { return ( <> <Button btnType="default">默认按钮</Button> <Button btnType="primary">主要按钮</Button> </> ); }; ``` 此时让我们进入到组件目录下,点击button按钮  如果你的组件库也是这个样子,那标志着你已经学会了如何自建组件库的80% ## 五、代码有测试 程序有测试代码才健壮,下面让我们安装jest生态,对我们的代码进行自动化测试 ```js cnpm i jest @testing-library/react @types/jest ts-jest jest-environment-jsdom jest-less-loader typescript@4 -D ``` 命令执行完毕后,我们在根目录下创建jest.config.js文件 jest配置如下: ```js /** @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,内容如下: ```js 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 内容如下: ```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中添加命令 ```js "scripts": { "test:staged": "node jest.staged.js" } ``` 然后再控制台执行npm run test:staged就可以只针对变动的地方测试了 ## 七、打包发布 打包发布很简单,打包时npm给我们配置好的,只需要执行npm run build即可打包出文件, 然后我们去npm官方注册个账号 注册成功后在控制台执行 `npm login`,如果能链接到国外的npm,就可以输入账号,密码完成登陆,这里记得翻墙, 完成登陆后执行执行 `npm publish`就可以发布到npm了 代码库[:https://gitee.com/SongTaoo/myui](https://gitee.com/SongTaoo/myui) 好了以上就是我的组件库方案,下面计划分享基于webpack5的微前端架构,这可能需要一些时间来写,所以需要您小小的赞来支持作者持续创作的动力

