引子
最近公司在搭建部门的统一平台,我负责了统一登录前端的开发,因为要对接很多的系统,所以开发了统一登录的sdk,说是sdk其实就是一个组件库。以此为契机,外加之前开发其他的系统中也用了很多种不同的组件库(饿了没-ui/vant/antd/chakra-ui…),今天写一篇文章说说,什么样的组件设计是比较合理的,以及如何设计一个好用的组件。
研究chakra-ui

比较来说我觉得设计比较有特色的一个组件库,就是 chakra-ui 了,不过国内使用的并不多,我也是在之前技术选型的时候偶然找到的,但是仔细读了她的文档之后我发现这个是一个很有特色的组件库,下面细说:
样式自定义
找到这个 chakra-ui 的时候,就是因为需要在使用 tailwind CSS 的同时使用一个组件库,chakra-ui 的一个特色就是使用了和 tailwind CSS 几乎相同的样式 api , 例如:
import { Box } from "@chakra-ui/react"// m={2} refers to the value of `theme.space[2]`<Box m={2}>Tomato</Box>// You can also use custom values<Box maxW="960px" mx="auto" />// sets margin `8px` on all viewports and `16px` from the first breakpoint and up<Box m={[2, 3]} />
这样只要你记住了 tailwind CSS 的api, 那么就可以很快的上手 chakra-ui 。
那么这个是怎么实现的呢?
一开始我以为是组件库使用了tailwind CSS ,但是看了源码发现,chakra 将 styled-components 进行了二次封装,而这种 tailwindLike 的 api 是进行了模拟导致的。
在 /packages/styled-system/config/ 里面 写入了不同样式以及缩写,以 background 为例:
export const background: Config = {background: t.colors("background"),...bg: t.colors("background"),...}
这样实现的,可以说把dirty的工作封装了起来,展示出来的结果都是好用的。
组件组合
书接上文,组件里面做了很多的映射封装,为了减少代码量,统一进行管理,组件库进行了组件的组合(compose)。从一个基本的组件出发,通过默认一些样式,创造了一些新的组件。
例如 Square Circle 组件,是基于 Box 组件extend而来的。
export const Square = forwardRef<SquareProps, "div">((props, ref) => {const { size, centerContent = true, ...rest } = propsconst styles: SystemStyleObject = centerContent? { display: "flex", alignItems: "center", justifyContent: "center" }: {}return (<Boxref={ref}boxSize={size}__css={{...styles,flexShrink: 0,flexGrow: 0,}}{...rest}/>)})if (__DEV__) {Square.displayName = "Square"}export const Circle = forwardRef<SquareProps, "div">((props, ref) => {const { size, ...rest } = propsreturn <Square size={size} ref={ref} borderRadius="9999px" {...rest} />})if (__DEV__) {Circle.displayName = "Circle"}
这样写减少了重复的代码并且可以保持更好的可维护性。我们在开发的过程中也可以借鉴这种模式,开发出最抽象的组件,从这个最抽象的父类出发来进行派生。
Theminig
chakra ui 的另外一个特点就是拥有一个高度自定义的主题系统, 使用的方式类似于 tailwind CSS 设置,也就是说你可同时将一个theme文件应用到两个库中,使用方法可以看一下chakra文档,那么这个主题是如何实现的呢?
首先 chakra ui 维护了一个default theme ,用于在没有自定义 theme 或者 自定义了一部分的theme的时候进行合并,合并的过程(toCSSVar)是使用了 createThemeVars 方法将自己配置的theme转化成css var变量,然后将默认的theme和生成的theme进行合并。最后在
export const ThemeProvider = (props: ThemeProviderProps) => {const { cssVarsRoot = ":host, :root", theme, children } = propsconst computedTheme = React.useMemo(() => toCSSVar(theme), [theme])return (<EmotionThemeProvider theme={computedTheme}><Global styles={(theme: any) => ({ [cssVarsRoot]: theme.__cssVars })} />{children}</EmotionThemeProvider>)}
这里是借用了 emotion 的 ThemeProvider。这么一看其实主题设置还是很简单的。这样可以很方便的设置了一个自定义的主题
除此之外,如果想在二次开发的主题上进行三次开发,可以使用 chakra-ui 提供的api Theme extensions。提供了一个类似于 HOC 的包裹函数,以withDefaultColorScheme为例:
export function withDefaultColorScheme({colorScheme,components}): ThemeExtension {return (theme) => {let names = Object.keys(theme.components || {})// ....return mergeThemeOverride(theme, {components: Object.fromEntries(names.map((componentName) => {const withColorScheme = {defaultProps: {colorScheme,},}return [componentName, withColorScheme]}),),})}}
将配置的颜色Scheme赋值给了配置的对应的组件。内部实现大同小异,都是调用了 mergeThemeOverride这个方法
export function mergeThemeOverride<BaseTheme extends ChakraTheme = ChakraTheme>(...overrides: ThemeOverride<BaseTheme>[]): ThemeOverride<BaseTheme> {return mergeWith({}, ...overrides, mergeThemeCustomizer)}
内部使用了 lodash.mergewith的方法实现融合,对于此方法 chakra-ui 写了一个mergeThemeCustomizer 作为 lodash.mergwith 的第三个参数,这里的自定义mergeThemeCustomizer方法使用了递归的方式进行merge。
function mergeThemeCustomizer(source: unknown,override: unknown,key: string,object: any,) {if ((isFunction(source) || isFunction(override)) &&Object.prototype.hasOwnProperty.call(object, key)) {return (...args: unknown[]) => {const sourceValue = isFunction(source) ? source(...args) : sourceconst overrideValue = isFunction(override) ? override(...args) : overridereturn mergeWith({}, sourceValue, overrideValue, mergeThemeCustomizer)}}// fallback to default behaviourreturn undefined}
外部组件与二次封装
从上面可以看到,组件库也不是全部从零开始,也使用了很多第三方的库,例如样式库 emotion, styled-components,工具方法库 lodash,这里没啥特别好说的。
另外chakra-ui官方也推荐将组件库和许多第三方的lib一起使用,例如 表单验证库formik,此外,在element-ui中也会直接封装throttle-debounce, async-validator等第三方的库。
提供escape
在使用其他的组件库的时候,很多情况下会出现某些组件的细节和设计要求不一致的情况,对于element-ui和ant design来说,由于使用了sass/less等预处理器,可以使用覆盖的方式来覆写样式。在 chakra ui 中,则提供了一个 sx Props 来直接向组件传入样式。
<Box sx={{ "--my-color": "#53c8c4" }}><Heading color="var(--my-color)" size="lg">This uses CSS Custom Properties!</Heading></Box>
这个方式很强大,还支持嵌套样式,media query等。这里的sx是一个封装自@emotion/styled的方法,在 packages/system/src/system.ts, styled方法里面调用了 toCSSObject ,这里拿取到了输入的样式,而所有的ui 组件都会调用这个 styled方法,sx Props 就这样全局生效了。
export function styled<T extends As, P = {}>(component: T,options?: StyledOptions,) {const { baseStyle, ...styledOptions } = options ?? {}// ...const styleObject = toCSSObject({ baseStyle })return _styled(component as React.ComponentType<any>,styledOptions,)(styleObject) as ChakraComponent<T, P>}export const toCSSObject: GetStyleObject = ({ baseStyle }) => (props) => {const { theme, css: cssProp, __css, sx, ...rest } = propsconst styleProps = objectFilter(rest, (_, prop) => isStyleProp(prop))const finalBaseStyle = runIfFn(baseStyle, props)const finalStyles = Object.assign({}, __css, finalBaseStyle, styleProps, sx)const computedCSS = css(finalStyles)(props.theme)return cssProp ? [computedCSS, cssProp] : computedCSS}
如何设计一个好用的组件
参考了很多设计,那么如何设计一个好用的组件呢,这里以一个progressBar为例。
MVP 版本以及存在的问题
import React, { useState, useEffect } from "react";import styled from "styled-components";const ProgressBarWrapper = styled.div<{ progress: number }>`width: 100%;height: 4px;position: fixed;top: 0;left: 0;right: 0;z-index: 9999;.bar-used {background: #34c;width: ${({ progress }) => progress + "%"};height: 100%;border-radius: 0 2px 2px 0;}`;const ProgressBar = () => {const [progress, setProgress] = useState(0);useEffect(() => {window.addEventListener("scroll", () => {setProgress((document.documentElement.scrollTop /(document.body.scrollHeight - window.innerHeight)) *100);});return () => {window.removeEventListener("scroll", () => {});};});return (<ProgressBarWrapper progress={progress}><div className='bar-used'></div></ProgressBarWrapper>);};export { ProgressBar };
这里展示了一个页面顶部进度条的组件,类似于 es6标准入门 这里的样式,上面的功能可以很快的就实现出来,但是只是比较符合单一的应用场景,进度条固定在顶部,只有从左往右增长一种情况。但是实际上的进度条可能会用到很多的地方,因此我们需要对照可能的场景以及代码中的变量进行判断,哪些是需要做成参数,并设置对应的默认值。
需求有以下几种:
- 颜色可调,位置可调,方向可调,这三个是比较全局的可调整类型
- 具体样式修改,高度修改,圆角修改,这些是其他的一些props,如果保持progressbar的功能不变可能不太会用到的props
此外,这里的progeressBar还存在的一个问题就是,这个组件将展示和逻辑杂糅在了一起,组件内部就有对于页面滚动进度的计算逻辑(useEffect),但是如果使用的时候不需要这个逻辑呢?
根据上面的一些要修改的点以及一些问题,我们来对这个组件进行拆分和重构。
重构
首先是把逻辑和展示分开。新建一个hook用于计算百分比。
import { useState, useEffect } from "react";export function useProgress() {const [progress, setProgress] = useState(0);useEffect(() => {window.addEventListener("scroll", () => {setProgress((document.documentElement.scrollTop /(document.body.scrollHeight - window.innerHeight)) *100);});return () => {window.removeEventListener("scroll", () => {});};});return progress;}
之后是给需要的参数添加props,并设置默认值,这里只以高度为例,设置一个可选的高度参数,当传入的时候就使用传入的值否则是默认的。
同时注意颜色等可以使用一个theme系统。
const ProgressBarWrapper = styled.div<{ progress: number; height?: string }>`width: 100%;height: ${({ height }) => (height ? height : "4px")};.bar-used {background: ${({ theme }) => theme.themeColor};width: ${({ progress }) => progress + "%"};height: 100%;border-radius: ${({ height }) =>height ? `0 calc( ${height}/ 2) calc(${height}/ 2) 0` : "0 2px 2px 0"};}`;
除此之外,将fixed布局抽象出来,方便后面进行组合
const FixedTopWrapper = styled.div`position: fixed;top: 0;left: 0;right: 0;z-index: 9999;`;// 组合之后就是这样的const ProgressBarWrapperFixed = styled(FixedTopWrapper)<{progress: number;height?: string;}>`.....`;
这样组件就是这样的,分成了默认好用的 ProgressBar 和 自定义功能更多的 SimpleProgressBar
interface ProgressProps {progress: number;height?: string;}const ProgressBar = ({height,}: Omit<ProgressProps, "progress">) => {const progress = useProgress();return (<ProgressBarWrapperFixed progress={progress} height={height}><div className='bar-used'></div></ProgressBarWrapperFixed>);};const SimpleProgressBar = ({progress,height,}: ProgressProps) => {return (<ProgressBarWrapper progress={progress} height={height}><div className='bar-used'></div></ProgressBarWrapper>);};
另外就是添加 合适的 escape,方便使用的时候如果不符合需要可以自行修改。这里直接在组件上添加一个 style参数,
// usage<ProgressBar style={{ background: "#000" }}></ProgressBar>// 修改组件 添加rest参数接受附加的style,并且修改一下类型const ProgressBar = ({height,...rest}: Omit<ProgressProps, "progress"> & React.HTMLAttributes<HTMLDivElement>) => {const progress = useProgress();return (<ProgressBarWrapperFixed {...rest} progress={progress} height={height}><div className='bar-used'></div></ProgressBarWrapperFixed>);};
这样就写好了一个好用的ProgressBar组件了,并且提供了SimpleProgressBar用于其他的自定义用途。
在线演示:https://codepen.io/alfxjx/pen/ZEJyygo?editors=0010
点击查看【codepen】
总结
经过上面对 chakra ui 组件库源码的研究以及一个示例,相信你以及知道了该如何设计一个好用的组件库了,希望你能为你的公司也开发一套组件库,能更好的完成你的kpi/okr/etc…
