前言
在 React 组件中,点击按钮将一段文本的颜色由黑色改为红色,简单粗暴的做法如下:
const App = () => {
const [color, setColor] = useState('#000')
const onChangeTextColor = () => {
setColor('red')
}
return (
<div>
<p style={{ color }}>this is text </p>
<button onClick={onChangeTextColor}>change text color</button>
</div>
)
}
直接使用行内样式做法,使用 state 控制 style 的样式。再把这个例子加深难度,比如要求字体调大为 20px,上下给 8px 的内边距,再给字体添加 background-color 等等
const App = () => {
const [color, setColor] = useState('#000')
const [fontSize, setFontSize] = useState('14px')
const [bgColor, setBgColor] = useState('#fff')
const onChangeTextColor = () => {
setColor('red')
setFontSize('20px')
setBgColor('#e8e8e8')
}
return (
<div>
<p style={{ color, fontSize, backgroundColor: bgColor }}>this is text </p>
<button onClick={onChangeTextColor}>change text color</button>
</div>
)
}
通过对比发现,随着改变的样式变多,需要开发人员花费更多精力维护行内样式,并且行内样式导致样式与 HTML 杂糅在一起。那么如何解决这个问题呢? 可以试试社区提供的 classnames 工具库
classnames
classnames 库通过 JavaScript 动态控制标签的类名。常用于 React 项目里用来控制组件的类名。它有如下的优点:
- 避免在组件里写行内样式
- 配置性高,比如通过对象动态配置类名
快速开始
在根目录安装 classnames 依赖
npm i classnames --save
现在用 classnames 优化前面的例子,下面是具体的实现
const App = () => {
const [visible, setVisible] = useState(false)
const onChangeTextColor = () => {
setVisible(true)
}
return (
<div>
<p style={classnames({ button: visible })}>this is text </p>
<button onClick={onChangeTextColor}>change text color</button>
</div>
)
}
下面是样式文件
.button {
color: red;
font-size: 20px;
background-color: #e8e8e8;
}
使用 classnames 既解耦样式与 HTML,也可以动态改变元素的样式
接下来一起看看 classnames 多种用法
// 这是最常见的用法
classNames('foo', 'bar')
// => 'foo bar'
// 通过变量控制每个类名,visible 为 false,则结果为 'foo', 为 true,则结果为 'foo bar'
classNames('foo', { bar: visible })
// => 'foo bar'
// 比如在 Tag 组件里,控制 Tag 的状态的类名可以写成下列形式,但 type 必须有默认值,比如默认 default
classNames('tag', `is-${type}`)
// => 'tag is-default'
// type 无默认值, 不推荐
classNames('tag', `is-${type}`)
// => 'tag is-undefined'
// 为 false 的值会被过滤掉
classNames(null, false, 'bar', undefined, 0, 1, { baz: null }, '')
// => 'bar 1'
源码分析
我们研究下 classnames 的具体实现。在 GitHub 上找到 classnames 的源码。classnames 的代码很少,仅仅只有 51 行。下面逐行分析源码
;(function () {
'use strict'
//...
})()
这是 IIFE (立即调用函数表达式),使用 IIFE 可以做到作用域隔离。
var hasOwn = {}.hasOwnProperty
将 Object 类型原型对象上的 hasOwnProperty 方法存入变量中,方便之后使用 hasOwnProperty 方法时,不用顺着原型链查找该方法,提高性能。
接下来,便是 classnames 库的核心代码,classNames 方法的具体实现,同时也是 classnames 库唯一的 API。
function classNames() {
var classes = []
for (var i = 0; i < arguments.length; i++) {
var arg = arguments[i]
/*
如果传入的值有 null、 undefined、 ""、false、NaN
等值直接跳过本次循环,进行下一次循环
*/
if (!arg) continue
var argType = typeof arg
/*
classNames 真正处理四类数据,分别为字符串、数值、数组和对象
*/
/*
如果传入的参数是字符串或数值类型,直接添加到最终的结果里。
eg。 classNames('foo', 1) => "foo 1"
*/
if (argType === 'string' || argType === 'number') {
classes.push(arg)
/*
如果传入的是数组类型,就递归遍历数组
*/
} else if (Array.isArray(arg)) {
if (arg.length) {
var inner = classNames.apply(null, arg)
if (inner) {
classes.push(inner)
}
}
/*
如果传入的参数是对象类型。则分为两种情况:
1. 对象没有 toString 属性;
classNames({foo: true}) => "foo"
2. 对象有 toString 属性;
classNames({"foo": toString: 'bar'}) => "bar"
*/
} else if (argType === 'object') {
if (arg.toString !== Object.prototype.toString) {
classes.push(arg.toString())
} else {
for (var key in arg) {
if (hasOwn.call(arg, key) && arg[key]) {
classes.push(key)
}
}
}
}
}
return classes.join(' ')
}
梳理一下整个 classNames 方法流程
- 声明一个空数组变量
classes,最后将classes里所有元素以空格符分隔组成字符串; - 使用
for循环对传入classNames方法的参数迭代; - 若传入的参数经过隐式转换为
false,则将其过滤,结束本次循环,进行下一次循环; - 参数为字符串或数值类型,则将其参数添加到
classes数组中; - 参数为数组类型,则进行递归处理,继续从步骤 1 开始,直至最后将返回值
inner(一维数组)添加到classes数组中; - 参数如果为对象类型,对象的属性值中没有
toString时,直接根据属性值的true/false判断该属性是否成为其类名,如果有toString,它的值作为其类名。
但是目前广泛使用的最新的 classnames v2.2.6 版本中。
// classnames v2.2.6
// ...
else if (argType === 'object') {
for (var key in arg) {
if (hasOwn.call(arg, key) && arg[key]) {
classes.push(key);
}
}
}
// ...
注意:对参数为对象类型的情况只做了简单处理,只要对象属性的属性值为 true,即将该属性追加到 classes 数组中。
最后,我们来看看 classNames 定义的模块导出部分源码
if (typeof module !== 'undefined' && module.exports) {
/*
commonJS 规范
*/
classNames.default = classNames
module.exports = classNames
} else if (
/*
amd 规范
*/
typeof define === 'function' &&
typeof define.amd === 'object' &&
define.amd
) {
define('classnames', [], function () {
return classNames
})
} else {
/*
直接暴露到全局
默认为 browser 环境
*/
window.classNames = classNames
}
classnames 使用了 UMD 模块导出规范。
- 先判断是否存在
module对象,如果存在,即在 Node.js 环境中,遵循commonJS规范导出; - 如果存在
define方法,即为浏览器环境中,遵循 AMD 规范导出; - 如果
module和define都不存在,直接将其暴露到全局环境(默认为浏览器环境)中,挂载到window全局对象上。
挑战者 clsx
clsx 是一个用于有条件构造 className 字符串的小型 (239B) 实用程序。还可以作为模块的更快、更小的直接替代品 classnames。其用法同 classnames 基本一样
参考链接
❤️爱心双连击
- 如果你觉得文章对你有帮助,就点个赞支持下吧,你的赞是我最大的动力。
- 关注公众号前端加油宝,里面有更精彩的文章,谢谢🙏