手把手带你入门「Headless UI」

本文详细介绍了Headless UI的概念和收益,并以数字输入组件为例讲解了Headless UI的实现方式。

1. [What] 什么是 Headless UI

在前端开发中,我们经常会用到数字输入组件,例如arco-design的InputNumber组件,可以通过传入形如mode这样的属性来配置按钮展示的形式。

mode = 'embed'

mode = 'button'

在实际工作中,不同场景下的数字输入组件可能会有不同的样式或布局,例如:

  1. 自定义样式:修改加减按钮的背景颜色

对于这类诉求,组件使用者可能会找到加减按钮对应的CSS类名,然后覆盖原有样式。

  1. 自定义布局:修改加减按钮的位置

对于这类诉求,组件维护者可能会新增入参,但入参的数量总是有限的,而布局的可能性是无限的。

随着UI自定义的诉求越来越丰富,组件的维护成本也越来越高。自然而然地,我们就会思考是否可以把逻辑从视图中剥离出来,这就是Headless UI的使用场景。

什么是 「Headless UI」

  • 字面含义:无UI的组件,只包含状态和交互等逻辑;
  • 实际含义:分别维护UI和逻辑Hooks,从而实现「关注点分离」,解除UI和逻辑之间的耦合。

2. [Why] 为什么要用 Headless UI

Headles UI是一种复用逻辑代码 的手段,它适用于"不同页面呈现不同UI,但交互逻辑高度相似"的场景。

如果基于Headless UI去开发一个组件库(例如Chakra-UI),同时暴露UI组件和逻辑Hooks,那么对于组件库的使用者和维护者而言,都能有不错的收益:

  • 组件库使用者:可以基于逻辑Hooks,灵活地自定义UI样式;
  • 组件库维护者:可以实现关注点分离,UI组件仅关注样式本身,逻辑Hooks仅关注状态和交互。

3. [How] Headless UI 怎么写

以上文提到的数字输入组件为例说明实现方式。

3.1 定义组件的状态

对于数字输入组件而言,它的状态如下:(包括但不仅限于)

  • count:当前数值;

  • step:数值变化步长;

  • disabled:组件整体是否禁用;

  • plusDisabled:增加按钮是否禁用;

  • minusDisabled:减少按钮是否禁用。

3.2 分析组件的生命周期

组件的生命周期可概括如下:

  • 初态:对应页面挂载时;
  • 运行时:页面挂载完成后,用户进行交互;
  • 终态:对应页面卸载时。

接下来就要去分析各个阶段下,有哪些行为会改变组件的状态。

状态 初态 运行时 终态
count 调用方传入初始值 用户点击增加或减少按钮
step 调用方传入初始值 调用方传入
disabled 调用方传入初始值 调用方传入
plusDisabled 初始值取决于disabled的值 取决于count的值(是否达到最大值)
minusDisabled 初始值取决于disabled的值 取决于count的值(是否达到最小值)

3.3 明确逻辑Hooks的形状

  • 逻辑Hooks需要从参数中获取:状态的初始值;
  • 调用方需要从返回值中获取:状态、状态变化时的回调函数、直接更新状态的函数。

定义参数形状如下:

TypeScript 复制代码
interface UseInputNumberProps {
  max?: number;
  min?: number;
  step?: number;
  defaultCount?: number
  disabled?: boolean
}

定义返回值形状如下:

TypeScript 复制代码
interface UseInputNumberReturn {
  count: number;
  disabled: boolean;
  setDisabled: (v: boolean) => void;
  plusDisabled: boolean;
  minusDisabled: boolean;
  onPlusClick: () => void;
  onMinusClick: () => void;
}

3.4 代码实现

按照上述思路进行实现,我们就得到了一个只包含状态和交互的逻辑Hooks。

TypeScript 复制代码
export function useInputNumber(props: UseInputNumberProps) {
  const {
    max,
    min,
    step: propsStep,
    defaultCount,
    disabled: propsDisabled,
   } = props
   
  const maxCount = typeof max === 'number' ? max : Number.MAX_SAFE_INTEGER
  const minCount = typeof min === 'number' ? min : Number.MIN_SAFE_INTEGER
  const step = typeof propsStep === 'number' ? propsStep : 1
  const [count, setCount] = useState(() => {
    if (typeof defaultCount !== 'number') {
      return 0
    }
    if (defaultCount > maxCount) {
      return max
    }
    if (defaultCount < minCount) {
      return min
    }
    return defaultCount
  })
  const [disabled, setDisabled] = useState(Boolean(propsDisabled))
  const [plusDisabled, setPlusDisabled] = useState(Boolean(propsDisabled))
  const [minusDisabled, setMinusDisabled] = useState(Boolean(propsDisabled))

  function onPlusClick() {
    updateCount(count + step)
  }

  function onMinusClick() {
    updateCount(count - step)
  }

  function updateCount(newCount) {
    setCount(newCount)
    if (newCount + step > maxCount) {
      setPlusDisabled(true)
    } else if (newCount - step < minCount) {
      setMinusDisabled(true)
    } else {
      setPlusDisabled(false)
      setMinusDisabled(false)
    }
  }

  return {
    count,
    disabled,
    setDisabled,
    plusDisabled,
    minusDisabled,
    onPlusClick,
    onMinusClick,
  }
}

调用方只需从逻辑Hooks的返回值取出所需的状态和函数,传递给UI组件即可。

TypeScript 复制代码
export function App() {
  const {
    count,
    onMinusClick,
    onPlusClick,
    plusDisabled,
    minusDisabled,
  } = useInputNumber({
    max: 100,
    min: 0,
    defaultCout: 1
  })
  
  return (
    <div>
      <h2>当前数值为{count}</h2>
      <button onClick={onPlusClick} disabled={plusDisabled}>增加</button>
      <button onClick={onMinusClick} disabled={minusDisabled}>减少</button>
    </div>
  )
}

参考资料

相关推荐
打不着的大喇叭16 分钟前
uniapp的光标跟随和打字机效果
前端·javascript·uni-app
无我Code21 分钟前
2025----前端个人年中总结
前端·年终总结·创业
程序猿阿伟24 分钟前
《前端路由重构:解锁多语言交互的底层逻辑》
前端·重构
Sun_light40 分钟前
6个你必须掌握的「React Hooks」实用技巧✨
前端·javascript·react.js
爱学习的茄子42 分钟前
深度解析JavaScript中的call方法实现:从原理到手写实现的完整指南
前端·javascript·面试
莫空000042 分钟前
Vue组件通信方式详解
前端·面试
呆呆的心43 分钟前
揭秘 CSS 伪元素:不用加标签也能玩转出花的界面技巧 ✨
前端·css·html
susnm1 小时前
Dioxus 与数据库协作
前端·rust
优雅永不过时_v1 小时前
基于vite适用于 vue和 react 的Three.js低代码与Ai结合编辑器
前端·javascript
小皮侠1 小时前
nginx的使用
java·运维·服务器·前端·git·nginx·github