classnames
是一个能将传入的多个对象或者字符串转换成类名的工具库。在 React
开发中, 如果需要条件判断添加类名,classnames
可以帮助你很方便的实现。 通过这篇文章,你可以了解到 classnames 的使用方式和实现。
在 React 中 使用
想象下列场景,我们需要封装一个 Button
组件,父组件 props
会传递 loading 和 disabled 属性,如果对应的值为 true
,需要给 button 添加对应的类名,我们的代码可能是下面这样:
js
function Button({disabled, loading}) {
let btnClass = 'btn';
if (disabled) btnClass += ' btn-disabled';
if (loading) btnClass += ' btn-loading';
return <button className={btnClass}>button</button>
}
借助 classnames
,你可以通过一个对象更简单,更直观的表达这种条件类名:
js
import classNames from "classnames";
function Button({disabled, loading}) {
const btnClass = classNames({
"btn": true,
"btn-disabled": disabled,
"btn-loading": loading
})
return <button className={btnClass}>button</button>
}
如上所示,传入 classnames 函数的参数类型是对象时,会将属性值为真值的属性名输出到类名结果中。classnames 函数可以接收任意数量的参数, string 类型会添加到类名结果中,数组类型的会对每一个元素应用上述规则, 比如:
ts
const arr = ['b', { c: true, d: false }];
classNames('a', arr); // => 'a b c'
实现一个classnames
了解了 classnames 的使用场景和用法之后,就可以照猫画虎实现一个了。classnames的参数类型可以是 string
, object
或者 array
,其他的假值会被忽略。为了简单起见,我们的 Array 类型只包含字符串类型的元素。
classnames 的输出结果就是将类名使用空格隔开的字符串。我们可以将符合规则的类名放入到数组 classArray 中,通过 classArray.join(' ') 输出结果。
1.处理字符串类型的参数
假定参数都是字符串类型的,剩余参数就是结果数组了
js
function classnames(...args: string[]): string {
return args.join(" ")
}
2.处理对象类型的参数
对象类型的,拿出真值的属性名,放到结果数组。
ts
function classnames(...args: {[propNames: string]: any}[]): string {
const classNames: string[] = [];
for(let i = 0; i < args.length; i++) {
const value = args[i];
Object.keys(value).forEach((k) => {
if(value[k]) classNames.push(k);
})
}
return classNames.join(" ");
}
3.处理数组类型的参数
数组类型的,通过 push, concat...... ,只要将里面的元素加入到结果数组中就可以,这里就选用push了。
ini
function classnames(...args: string[][]): string {
const classNames: string[] = [];
for (let i = 0; i < args.length; i++) {
const value = args[i];
classNames.push(...value);
}
return classNames.join(' ');
}
4.简单实现
当前,只要判断出参数类型,依次使用上面的思路输出结果就好了,判断参数类型,有很多种办法,这里就通过调用 Object.prototype.toString
方法,获取对象的 [[Class]]
值,然后解析该值,得到参数类型。
ts
const opt = Object.prototype.toString;
function isString(arg: any): arg is string {
return opt.call(arg) === '[object String]'
}
function isArray(arg: any): arg is any[]{
return opt.call(arg) === '[object Array]';
}
function isObject(arg: any): arg is {[propName: key]: any}{
return opt.call(arg) === '[object Object]';
}
最后的代码如下
js
type ArgType = string | string[] | Record<string, any> | undefined | null | boolean;
function classnames(...args: argType): string {
const classNames = [];
for (let i = 0; i < args.length; i++) {
const value = args[i];
if (!value) continue; //跳过假值
if (isString(value)) {
classNames.push(value);
} else if (isArray(value)) {
classNames.push(...value);
} else if (isObject(value)) {
Object.keys(value).forEach((k) => {
if (value[k]) classNames.push(k);
});
}
}
return classNames.join(' ');
}
5.边界实现
第四步的结果基本可以满足开发场景了,有一种情况没考虑进去,classnames 库中的数组类型可以包含 argType 中的任意类型, 我们为了实现方便,将其限制成了字符串数组。要实现这个需求,在遇到数组类型的参数时,对每个参数递归的调用 上面编写的 classnames, 就可以 cover
这种写法了:
ts
function classnames(...args) {
const classNames = [];
function appendClass(...args) {
for (let i = 0; i < args.length; i++) {
const value = args[i];
if (!value) continue;
if (isString(value)) {
classNames.push(value);
} else if (isArray(value)) {
for (const v of value) {
appendClass(v); //递归调用 appendClass,
}
} else if (isObject(value)) {
Object.keys(value).forEach((k) => {
if (value[k]) classNames.push(k);
});
}
}
}
appendClass(...args); //开始执行
return classNames.join(' ');
}
这样子,就可以处理下面这种嵌套结构的情况了
js
classnames(['111', '222', '333'], 'string1', 'string2', undefined, null, { obj1: true, obj2: false }, [
{ obj3: true, obj4: false },
'aaa',
["res1", "res2"]
]);
// output: 111 222 333 string1 string2 obj1 obj3 aaa res1 res2
总结
本文介绍了在 React 开发中常用的类名处理工具库 classnames
的使用场景和实现方式。在 React 组件中,经常需要根据条件来动态生成类名,而 classnames
可以简化这个过程。通过传递对象、数组或字符串参数,classnames
将它们转换成合适的类名字符串。
文章中实现了一个简化版的 classnames
,包括处理字符串、对象和数组类型的参数。对于数组类型的参数,采用了递归调用的方式,以支持嵌套的情况。
第一次尝试写文章,写的不明白或者有问题的地方,希望大佬们可以指出来~