classnames: 一个根据条件判断应用类名的小型工具库

缘起

编写 React 组件时,我们通常会为条件设置类名而苦恼。

jsx 复制代码
import React, { useState } from 'react';

export default function Button (props) {
  const [isPressed, setIsPressed] = useState(false);
  const [isHovered, setIsHovered] = useState(false);

  let btnClass = 'btn';
  if (isPressed) btnClass += ' btn-pressed';
  else if (isHovered) btnClass += ' btn-over';

  return (
    <button
      className={btnClass}
      onMouseDown={() => setIsPressed(true)}
      onMouseUp={() => setIsPressed(false)}
      onMouseEnter={() => setIsHovered(true)}
      onMouseLeave={() => setIsHovered(false)}
      >
      {props.label}
    </button>
  );
}

不过有了 classnames 工具库的帮助就没那么麻烦了。

jsx 复制代码
import React, { useState } from 'react';
import classNames from 'classnames';

export default function Button (props) {
	const [isPressed, setIsPressed] = useState(false);
	const [isHovered, setIsHovered] = useState(false);

	const btnClass = classNames({
		btn: true,
		'btn-pressed': isPressed,
		'btn-over': !isPressed && isHovered,
	});

	// ...
}

以上面的代码为例。

classnames 会对外暴露一个工具函数 classNames()。当传入对象时,对象属性会根据属性值是否为 true,被添加到返回结果 btnClass 中,btnClass 是一个字符串。

  • 当 isPressed 为 false,isHovered 为 false,btnClass 等于 "btn"
  • 当 isPressed 为 true,isHovered 为 true,btnClass 等于 "btn btn-pressed"
  • 当 isPressed 为 true,isHovered 为 false,btnClass 等于 "btn btn-pressed"
  • 当 isPressed 为 false,isHovered 为 true,btnClass 等于 "btn btn-over"

这是 classnames 的大概介绍。

接下来介绍 classnames 比较前面的基本介绍。

基本使用

classNames() 函数接受任意数量的参数。参数类型可以是:字符串、对象或者数组。

当是字符串时 ,只要不是空字符串('')都会输出。

jsx 复制代码
const classNames = require('classnames');
classNames('foo', 'bar'); // => 'foo bar'

当是对象时 ,属性值通常使用布尔值表示当前属性是否输出。true 输出,false 不输出。

jsx 复制代码
classNames({ 'foo-bar': true }); // => 'foo-bar'
classNames({ 'foo-bar': false }); // => ''
classNames({ foo: true }, { bar: true }); // => 'foo bar'
classNames({ foo: true, bar: true }); // => 'foo bar'

利用 ES6 的对象动态属性名,还可以动态拼接类名。

jsx 复制代码
let buttonType = 'primary';
classNames({ [`btn-${buttonType}`]: true });

当是数组时,会将数组成员作为参数带入 classNames 函数调用处理。

jsx 复制代码
const arr = ['b', { c: true, d: false }];

classNames('a', arr); // => 'a b c'
// 等同于
classNames('a', ...arr); // => 'a b c'

对本例而言,'b' 和 { c: true, d: false } 两个数组成员会被单独判断,决定是否添加返回值中。

当然,不同类型参数还可以混合使用。

jsx 复制代码
classNames('foo', { bar: true });
// => 'foo bar'
classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); 
// => 'foo bar baz quux'

需要注意的是,所有的假值(falsy)判断都不会出现在结果里。

jsx 复制代码
// other falsy values are just ignored
classNames(
  null,
  false,
  'bar',
  undefined,
  0,
  1,
  { baz: null },
  ''
); // => 'bar 1'

同时,数值(这里的 1)也会被转化成字符串拼接到结果中。

以上,就讲完了 classnames 的基本使用。

代码实现

基于 classnames v2.5.2 版本。

classnames 这个工具库的使用很简单,实现起来也并不复杂。现在带大家来着手实现。

首先,导出 classNames() 函数,并接受多个参数。

jsx 复制代码
module.exports = function classNames(...args) {
  let classes = ''

  for (let arg of args) {
    if (arg) {
      // not good!
      classes = appendClass(classes, String(arg))
    }
  }

  return classes
}

function appendClass(value, newClass) {
  return value + (newClass ? ` ${newClass}` : '')
}

上面的实现并不好,只考虑了字符串和数字拼接,没有考虑对象和数组。因此,我们有必要抽象出一个 parseValue() 函数来针对不同类型进行处理。

diff 复制代码
module.exports = function classNames(...args) {
  let classes = ''

  for (let arg of args) {
    if (arg) {
-     classes = appendClass(classes, String(arg))
+     classes = appendClass(classes, parseValue(arg))
    }
  }

  return classes
}

下面来实现 parseValue() 函数。

首先,字符串和数字直接返回,进行字符串拼接就行。

javascript 复制代码
function parseValue(arg) {
  if (typeof arg === 'string' || typeof arg === 'number' ) {
    return arg
  }
  
  // ...
}

其次,不是对象、也不是数组的参数类型都不处理(返回空字符串 '')。

javascript 复制代码
function parseValue(arg) {
  if (typeof arg === 'string' || typeof arg === 'number' ) {
    return arg
  }

  if (typeof arg !== 'object') {
    return ''
  }
  
  // ...
}

接着,如果接受的是数组,那么将数组成员直接作为参数调用 classNames() 函数。

javascript 复制代码
function parseValue(arg) {
  if (typeof arg === 'string' || typeof arg === 'number' ) {
    return arg
  }

  if (typeof arg !== 'object') {
    return ''
  }

  if (Array.isArray(arg)) {
    return classNames.apply(null, arg)
  }
  
  // ...
}

最后,处理对象类型参数。for...in 遍历,属性值为真(truthy),就把属性名拼接上。

javascript 复制代码
function parseValue(arg) {
  // ...

  let classes = ''
  for (let key in arg) {
    if (Object.hasOwnProperty.call(arg, key) && arg[key]) {
      class = appendClass(classes, key)
    }
  }

  return classes
}

到这里差不多就完成了 classnames 的核心概念。

总结

本文我们介绍了 classnames 这样一个根据条件判断应用类名的小型工具库,介绍了它的基本使用以及核心代码实现。无论你是编写你 React 还是 Vue 组件,都会有应用的场景。

感谢你的阅读,再见。

相关推荐
福兮说1 小时前
设计稿是 #4A7C6F,页面量出来是 #4B7C6F:HEX、HSL、透明度、canvas 来回转的七个坑
前端·javascript·css·canvas
郑州光合科技余经理2 小时前
海外版外卖加盟:总站与分站配送规则怎么分开管
java·开发语言·前端·后端·uni-app·php·ai编程
小呆呆6663 小时前
副业搞起来,小说,漫画,漫剧的成本优化思路
前端·后端·面试
凤城老人3 小时前
从 PyQt6 到 Electron:给 Edge TTS 做一个“多角色配音机“的踩坑手记
javascript·typescript·electron
Dovis(誓平步青云)3 小时前
浇水提醒刚弹出又消失,植物状态别只存一个百分比
开发语言·前端·javascript·pdf·ecmascript·电脑
码艺-Alimjan3 小时前
Vben Admin 新增维吾尔语 Vben-Modal的关键坑之一
前端·javascript·vue.js
可乐鸡翅yeah_4 小时前
hls.js 手动自定义 http 请求 loader,修改请求头实战
开发语言·前端·javascript·网络协议·http·ecmascript·m3u8在线
IT_陈寒4 小时前
Vite静态资源导入这个坑我帮你们踩过了
前端·人工智能·后端
广州华水科技4 小时前
大坝安全监测解决方案:单北斗GNSS形变监测系统应用与维护
前端
yivifu5 小时前
中文古籍电子书注释集成
前端·javascript·python·beautifulsoup·epub