从0到1搭建组件库(dumi+father-build+ts+react)

486 阅读10分钟

持续更新ing...

安装dumi

官网:d.umijs.org/guide/initi… 安装npx create-dumi 搭建组件库选择 React Library,否则在 md 文件写组件demo文档是没办法生效的。React Library 就是说你想开发一个以 React 为基础的库,dumimd文档输出进行特殊的处理。

配置完成后文件列表

image.png

设置logo等基础配置

.dumirc.ts中配置主题颜色、图标等:

image.png

docs文件用于配置网页展示内容,用md格式编写

image.png

组件编写

src文件中做基本配置

image.png

button编写

index.ts统一导出组件

image.png

themeStyles文件定义一些常用主题颜色

image.png

一个普通组件的文件构成: image.png

index.ts来编写组件

image.png

image.png

md文件负责文档的展示,使用<code>标签引入demo中的代码展示

image.png

demo文件包含要在文档中展示的例子,在头部注释写title,descriptiondumi会自动解析。

image.png

效果:

image.png

style文件包含该组件对应的样式,引入我们编写的主题样式,使用@import url(../..)样式来引入

image.png

each遍历 image.png

dropdown下拉框编写

使用rc-trigger进行二次开发。

有两个必要属性:1.{children必要}2.popup弹出层内容必要

import React from 'react';
import Trigger from 'rc-trigger';

const MyPopup = () => {
  return <div>这里是弹出层的内容</div>;
};

const ExampleComponent = () => {
  return (
    <Trigger popup={<MyPopup />}>
      <button>点击触发弹出层</button>
    </Trigger>
  );
};

scope布局编写

使用到了一个自己不熟悉的知识:React.ChildrenReact提供的一个用于处理React元素的方法。其中callback接收到两个参数childindex

  • React.Children.map(children,callback)对每个子元素调用回调函数,并返回一个新数组。
  • React.Children.forEach(children,callback)对每个子元素掉用回调函数,没有返回值。
  • React.Children.count(children)返回子元素数量。
  • React.Children.only(children)验证子元素只有一个,并返回该子元素。
  • React.Children.toArray将子元素转换成一个数组。

Alert警示框编写

浮层元素是指在网页或应用程序中以覆盖式展示,并且需要用户进行操作才能关闭的UI组件。通常,浮层元素会在当前页面的顶部弹出一个层级较高的容器,使用户无法与页面的其他内容进行交互,直到浮层关闭或完成相应操作。

描述:用于页面展示重要的提示消息。Alert组件不属于浮层元素,不会自动消失或关闭。

样式CSS:width:max-content元素会根据其内容宽度自动扩展(不受限于父元素宽度或其他因素)

drawer抽屉

点击一个按钮,从屏幕边缘滑出浮层面板。需要点击遮罩区或者“x”图标来关闭。

改变open来控制wrapper组件显示与隐藏,open也控制遮罩层是否显示。open和masked初始值都是false。这里打印的open是最新改变的值,但打印的masked是原始记忆值。 image.png 如果动画不生效,考虑是否因为其他元素覆盖。

modal对话框

react-transition-group库(有三个可以引用:CSSTransition控制显示与隐藏、SwitchTransition控制内容切换、TransitionGroup控制列表)。使用SwitchTransition包裹CSSTransition。这个库让动画效果的完成方便很多。

引入:import {CSSTransition} from 'react-transition-group' 使用:

<CSSTransition className='cherry-modal-mask' in={masked} timeout={300}>
    <div className="modal-mask"></div>
</CSSTransition>

in是触发条件,timeout是持续时间。

具体CSS代码编写:

// 动画编写--直接绑定在指定元素上了。
.cherry-modal-mask-enter {
    opacity: 0;
}

.cherry-modal-mask-active {
    opacity: 1;
    transition: opacity 300ms;
}

.cherry-modal-mask-enter-done,
.cherry-modal-mask-exit {
    opacity: 1;
}

.cherry-modal-mask-exit-active {
    opacity: 0;
    transition: opacity 300ms;
}

默认页脚按钮定义:

// footer按钮类型--定义一个对象包含两个模式(即default和simple,他们的值都是ReactNode)
    const buttonArea: {default:ReactNode;simple:ReactNode} = {
        default:(
            <>
                <span className="footer-item" onClick={onCancel}>
                    <Button >取消</Button>
                </span>
                <span className="footer-item" onClick={onCancel}>
                    <Button >确定</Button>
                </span>
            </>
        ),
        simple:(
            <>
                <span className="footer-item" onClick={onCancel}>
                    <Button >知道了</Button>
                </span>
            </>
        )
    }

应用:

<footer className="modal-footer">
    {// 如果有自定义页脚footerItem则用自定义,没有则使用我们默认的type
        footerItem?footerItem.map((item,index)=>{
            return (
                <span className="footer-item" key={index}>
                    {item}
                </span>
            )
        }):buttonArea[footerType]
    }
</footer>

?? 是空值合并运算符(nullish coalescing operator)。它的作用是判断左侧的操作数是否为 null 或 undefined,如果是,则返回右侧的操作数。否则,返回左侧的操作数。

待完成:键盘退出

menu菜单

在index.tsx中定义MenuContext并暴露。使用createContext定义一个名为MenuContext的上下文对象,这个上下文的初始值是一个实现了MenuContext接口的对象,其中index属性被设置为0。导出这个上下文对象,其他模块可以引入并使用这个上下文对象来共享数据。

import React, { createContext } from 'react';
interface IMenuContext {
    index:string;
    onSelect?:(selectedIndex: string) => void;
    mode?:'horizontal' | 'vertical';
    defaultOpenSubMenus?:string[];
}
export const MenuContext = createContext<IMenuContext>({index:'0'})

    return (
        <ul className={classes}>
            <MenuContext.Provider value={passedContext}>
                {childrenRender()}  
            </MenuContext.Provider>
        </ul>
    )

在其他模块中引入并使用

import React, { useContext } from "react";
import { MenuContext } from ".";
const context = useContext(MenuContext)

在index.tsx文件中循环遍历menuItem加工生成对应的元素

const childrenRender = () => {
    return React.Children.map(children,(child,index)=>{
        const childElement = child as React.FunctionComponentElement<MenuItemProps>
        console.log(childElement);
        const {displayName} = childElement.type//解构幅值
        if(displayName == 'MenuItem'){
            return React.cloneElement(childElement,{
                index: index.toString(),
            })
        }else{
             return <Alert title="警告提示" type="warning" closeable={true}>Menu has a child which is not a MenuItem component</Alert>
        }
    })
}

打印一下childrenElement image.png clone处理之后加入index属性的childrenElement image.png childrenElement.type都是MenuItemProps中的定义 image.png

React.cloneElement 是 React 提供的一个函数,用于克隆并返回一个已有的 React 元素,并可以传递新的属性给克隆后的元素。 其语法如下:

jsxCopy Code
React.cloneElement(element, props, ...children)
  • element:要克隆的 React 元素。
  • props:要添加或替换的新属性对象。
  • ...children:可选参数,额外的子元素。 所以父组件给子组件加了index属性并传递index(通过cloneElement),子组件中调用留在父组件的回调函数来更新index。

subMenuItem二级菜单,title是一级菜单名,里面遍历生成带有xx-xxindex

const renderChildren = () => {
        const subMenuClasses = classNames('cherry-submenu', {
            'menu-opened': menuOpen,
            'cherry-submenu-vertical': context.mode == 'vertical',
            'submenu-item-horizontal': context.mode !== 'vertical'
        })
        // 循环遍历生成带有index属性的元素
        const childrenComponent = React.Children.map(children, (child, i) => {
            const childElement = child as FunctionComponentElement<MenuItemProps>
            if (childElement.type.displayName == 'MenuItem') {
                return React.cloneElement(childElement, {
                    index: `${index}-${i}`
                })
            } else {
                console.error('Menu has a child which is not a MenuItem component')
            }
        })
        // 返回子菜单
        return (
            <>{ menuOpen && (<CSSTransition in={menuOpen} timeout={300} classNames='cherry-subMenu-ul'>
                <ul className={subMenuClasses}>
                    {childrenComponent}
                </ul>
            </CSSTransition>)}</>
        )
    }

breadcrumb面包屑

a标签的target控制是否在当前页面跳转链接,默认_self在当前页面打开,_blank在新标签页面打开。

const Breadcrumb:React.FC<BreadcrumbProps> = (props)=>{
    const {objects, target='_self', ...restProps} = props;

    return (
        <div className='cherry-breadcrumb' {...restProps}>
            <ul className='breadcrumb-ul'>
                {objects?.map((obj,index)=>{
                    return (
                        <>
                            <li key={index} className='breadcrumb-li'>
                                <a href={obj.link} target={target}>{obj.text}</a>
                            </li>
                            {index !== objects.length-1 && <li className='breadcrumb-li'>/</li>}
                        </>
                    )
                })}
            </ul>
        </div>
    )
}

input输入框

类名为icon-wrapper元素的下一个兄弟元素input-inner(紧挨)

.icon-wrapper+.input-inner {
  padding-right: 35px;
}

outline: 0; 是一种 CSS 属性,用于移除元素获取焦点时的默认轮廓样式。在某些浏览器中,当元素获得焦点时,会出现一个默认的轮廓样式,通常是蓝色阴影或边框。

/* 移除所有元素的焦点轮廓 */ * 
{ outline: 0; } 
/* 移除特定元素的焦点轮廓 */ 
input:focus, button:focus { outline: 0; }

<input placeholder="请输入">,占位文本的样式用input{&::placeholder}伪元素选择。 没有空格--多类名选择器 image.png 有空格--父子选择器 image.png

tree

store

设置一个store文件夹,包含index.tsx和node.tsx文件。

node.tsx

node.tsx文件主要定义NodeOptionsNodeimage.png

定义node上的属性 image.png

如果没有子节点,那么自己选中了就是返回true,如果有子节点,每个子节点都选中了则返回true。 半选:在没有没全选中的情况下,有子节点被选中/半选中,则返回true。 image.png

自上向下来更新:选中/取消了父节点,要更新它下方所有子节点的状态。自下向上来更新:选中/取消了子节点,要更新它上方所有父节点的状态。 image.png

store的accordion来控制是否只能展开一个节点。如果为true的话则判断,flase的话传入什么就设置这个node的collapse是什么。 image.png

封装子节点。还是采用dfs形式。比index.ts中的createTree多了判断append的步骤。 image.png 追加和删除子节点 image.png

index.tsx

Record 接受两个类型参数 KT,其中 K 是一个键的集合(通常是字符串字面量类型或联合类型),T 是对应键的值的类型。返回的类型是一个对象,其键是 K 中的每个元素,值的类型是 Timage.png 创建根节点/子节点的方法,都要把store的值绑定成当前Store实例,即当前的 Store 实例作为 store 属性的值赋给新创建的 Node 对象。Omit<A,B>表示从原始类型A中排除B属性。 image.png 递归dfs创建树 image.png 拍平数组(把children拍平) image.png 激活node的checked属性。new Map()可以接收一个伪数组作为参数,例如[[1,1],[2,3]]就是1->1,2->3。 image.png 基本一样的逻辑。 image.png

index.tsx

使用useStateuseEffect配合,当data改变时重新构建树,当tree改变时重新创建拍平的树的列表。新建一个传入datastore实例来控制状态。 image.png 根据初始化的默认点击/打开来调用store中的方法 image.png 判断一个节点是否展示,取决于它的父节点(祖先节点)的collapse,如果设置为false则不显示(有一个折叠就是折叠了),如果遍历到最上面的父节点都是设置为展开则该节点是显示的。 image.png 自适应高度功能。 image.png MutationObserver 是一个在 JavaScript 中用于监测 DOM 变化的接口。它可以观察指定的 DOM 元素或 DOM 树的变化,并在变化发生时触发回调函数执行相应的操作。

MutationObserver 的常用方法和属性有:

  • observe(target, options): 启动对目标元素的观察。target 参数指定要观察的 DOM 元素,而 options 参数是一个配置对象,用于指定观察的类型(如子节点变化、属性变化等)。通过此方法开启观察后,MutationObserver 会在目标元素变化时触发回调函数。
  • disconnect(): 停止对目标元素的观察。调用该方法后,MutationObserver 将不再触发回调函数,并且不再监听任何 DOM 变化。
  • takeRecords(): 返回当前已观察到但尚未处理的所有 DOM 变化记录的数组。
  • observe(...) 方法的返回值是一个 MutationObserver 实例,可以使用此实例的其他方法和属性。

在给定的代码中,MutationObserver 被用来监测 treeRef.current 的父元素的变化。具体来说,它会检测父元素的子节点变化以及子树中任何节点的变化。一旦变化发生,回调函数将被触发,并执行相应的操作。

offsetHeight是元素的实际高度,经常用于动态布局、元素定位和响应式设计。把元素实际的高度定义给这个元素达到自适应高度的效果。

创建treeNode。 image.png 创建每个小li作为node节点。用style来控制节点的显示与隐藏(showNode函数的返回结果)。缩进是根据node.depth来调节。设计一个标签来控制左边箭头的显示与隐藏。点击箭头,这个node的collapse取反,重新构建树。点击CheckBox的checked是根据node.checked设置的,点击则调用node绑定的setChecked方法,实现父节点和子节点对应的更新。 image.png 最后把拍平树的结果过滤一遍showNode作为中的itemData。<FixedSizeList> 是由 react-window 库提供的一个组件,用于高效地渲染大量数据时进行虚拟化。它可以帮助优化性能,减少内存消耗,并提供平滑的滚动体验。通过将 itemCount 属性设置为渲染项的总数,再结合 itemDataitemSizeinnerElementType 等属性,<FixedSizeList> 将仅渲染可见区域的项,而不是一次渲染整个列表。这种虚拟化的方式使得在处理大数据集时能够显著提高性能和响应速度。 image.png