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 是一个字符串。

  • isPressedfalseisHoveredfalsebtnClass 等于 "btn"
  • isPressedtrueisHoveredtruebtnClass 等于 "btn btn-pressed"
  • isPressedtrueisHoveredfalsebtnClass 等于 "btn btn-pressed"
  • isPressedfalseisHoveredtruebtnClass 等于 "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 组件,都会有应用的场景。

感谢你的阅读,再见。

相关推荐
一斤代码4 小时前
vue3 下载图片(标签内容可转图)
前端·javascript·vue
中微子4 小时前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
光影少年5 小时前
从前端转go开发的学习路线
前端·学习·golang
中微子5 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架
3Katrina5 小时前
深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器
前端·javascript·面试
前端_学习之路6 小时前
React--Fiber 架构
前端·react.js·架构
coderlin_6 小时前
BI布局拖拽 (1) 深入react-gird-layout源码
android·javascript·react.js
伍哥的传说6 小时前
React 实现五子棋人机对战小游戏
前端·javascript·react.js·前端框架·node.js·ecmascript·js
qq_424409196 小时前
uniapp的app项目,某个页面长时间无操作,返回首页
前端·vue.js·uni-app
我在北京coding6 小时前
element el-table渲染二维对象数组
前端·javascript·vue.js