手把手带你入门「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>
  )
}

参考资料

相关推荐
拉里小猪的迷弟4 分钟前
设计模式-创建型-常用:单例模式、工厂模式、建造者模式
单例模式·设计模式·建造者模式·工厂模式
_.Switch37 分钟前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光41 分钟前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   41 分钟前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   42 分钟前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web1 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常1 小时前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇2 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
严文文-Chris2 小时前
【设计模式-中介者模式】
设计模式·中介者模式
Jiaberrr2 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui