likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 1

缘起

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

  • isPressedfalseisHoveredfalsebtnClass 等于 "btn"
  • isPressedtrueisHoveredtruebtnClass 等于 "btn btn-pressed"
  • isPressedtrueisHoveredfalsebtnClass 等于 "btn btn-pressed"
  • isPressedfalseisHoveredtruebtnClass 等于 "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 v2.5.2 版本

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 组件,都会有应用的场景。

感谢你的阅读,再见。