classnames: 一个根据条件判断应用类名的小型工具库
缘起
编写 React 组件时,我们通常会为条件设置类名而苦恼。
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 工具库的帮助就没那么麻烦了。
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()
函数接受任意数量的参数。参数类型可以是:字符串、对象或者数组。
当是字符串时,只要不是空字符串(''
)都会输出。
const classNames = require('classnames');
classNames('foo', 'bar'); // => 'foo bar'
当是对象时,属性值通常使用布尔值表示当前属性是否输出。true
输出,false
不输出。
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 的对象动态属性名,还可以动态拼接类名。
let buttonType = 'primary';
classNames({ [`btn-${buttonType}`]: true });
当是数组时,会将数组成员作为参数带入 classNames 函数调用处理。
const arr = ['b', { c: true, d: false }];
classNames('a', arr); // => 'a b c'
// 等同于
classNames('a', ...arr); // => 'a b c'
对本例而言,'b'
和 { c: true, d: false }
两个数组成员会被单独判断,决定是否添加返回值中。
当然,不同类型参数还可以混合使用。
classNames('foo', { bar: true });
// => 'foo bar'
classNames('foo', { bar: true, duck: false }, 'baz', { quux: true });
// => 'foo bar baz quux'
需要注意的是,所有的假值(falsy)判断都不会出现在结果里。
// other falsy values are just ignored
classNames(
null,
false,
'bar',
undefined,
0,
1,
{ baz: null },
''
); // => 'bar 1'
同时,数值(这里的 1
)也会被转化成字符串拼接到结果中。
以上,就讲完了 classnames 的基本使用。
代码实现
classnames 这个工具库的使用很简单,实现起来也并不复杂。现在带大家来着手实现。
首先,导出 classNames()
函数,并接受多个参数。
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()
函数来针对不同类型进行处理。
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()
函数。
首先,字符串和数字直接返回,进行字符串拼接就行。
function parseValue(arg) {
if (typeof arg === 'string' || typeof arg === 'number' ) {
return arg
}
// ...
}
其次,不是对象、也不是数组的参数类型都不处理(返回空字符串 ''
)。
function parseValue(arg) {
if (typeof arg === 'string' || typeof arg === 'number' ) {
return arg
}
if (typeof arg !== 'object') {
return ''
}
// ...
}
接着,如果接受的是数组,那么将数组成员直接作为参数调用 classNames()
函数。
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),就把属性名拼接上。
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 组件,都会有应用的场景。
感谢你的阅读,再见。
转载自:https://juejin.cn/post/7335726984999059494