【源码共读】Css-In-Js 的实现 classNames 库

news2024/12/24 2:28:03

classNames是一个简单的且实用的JavaScript应用程序,可以有条件的将多个类名组合在一起。它是一个非常有用的工具,可以用来动态的添加或者删除类名。

仓库地址:classNames

使用

根据classNamesREADME,可以发现库的作者对这个库非常认真,文档和测试用例都非常齐全,同时还有有不同环境的支持。

其他的就不多介绍了,因为库的作者写的很详细,就直接上使用示例:

var classNames = require('classnames');
classNames('foo', 'bar'); // => 'foo bar' 
  • 可以是多个字符串
classNames('foo', 'bar'); // => 'foo bar' 
  • 可以是字符串和对象的组合
classNames('foo', { bar: true }); // => 'foo bar' 
  • 可以是纯对象
classNames({ 'foo-bar': true }); // => 'foo-bar'
classNames({ 'foo-bar': false }); // => '' 
  • 可以是多个对象
classNames({ foo: true }, { bar: true }); // => 'foo bar'
classNames({ foo: true, bar: true }); // => 'foo bar' 
  • 多种不同数据类型的组合
classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); // => 'foo bar baz quux' 
  • 假值会被忽略
classNames(null, false, 'bar', undefined, 0, 1, { baz: null }, ''); // => 'bar 1' 
  • 可以是数组,数组中的元素可以是字符串、对象、数组,会被展平处理
var arr = ['b', { c: true, d: false }];
classNames('a', arr); // => 'a b c' 
  • 可以是动态属性名
let buttonType = 'primary';
classNames({ [`btn-${buttonType}`]: true }); 

还有其他的使用方式,包括在React中的使用,可以去看看README,接下里就开始阅读源码。

源码阅读

先来直接来看看classNames的源码,主要是index.js文件,代码量并不多:

/*<img src="http://jedwatson.github.io/classname" style="margin: auto" />
*/
/* global define */

(function () { 'use strict'; var hasOwn = {}.hasOwnProperty; function classNames() {var classes = [];for (var i = 0; i < arguments.length; i++) { var arg = arguments[i]; if (!arg) continue; var argType = typeof arg; 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); }} } else if (argType === 'object') {if (arg.toString !== Object.prototype.toString && !arg.toString.toString().includes('[native code]')) { classes.push(arg.toString()); continue;}for (var key in arg) { if (hasOwn.call(arg, key) && arg[key]) {classes.push(key); }} }}return classes.join(' '); } if (typeof module !== 'undefined' && module.exports) {classNames.default = classNames;module.exports = classNames; } else if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {// register as 'classnames', consistent with npm package namedefine('classnames', [], function () { return classNames;}); } else {window.classNames = classNames; }
}()); 

可以看到,classNames的实现非常简单,一共就是50行左右的代码,其中有一些是注释,有一些是兼容性的代码,主要的代码逻辑就是classNames函数,这个函数就是我们最终使用的函数,接下来就来看看这个函数的实现。

兼容性

直接看最后的一段if判断,这些就是兼容性的代码:

if (typeof module !== 'undefined' && module.exports) {classNames.default = classNames;module.exports = classNames;
} else if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {// register as 'classnames', consistent with npm package namedefine('classnames', [], function () {return classNames;});
} else {window.classNames = classNames;
} 

可以看到这里兼容了CommonJSAMDwindow三种方式,这样就可以在不同的环境下使用了。

一下就看到了三种兼容性方式的区别和特性了:

CommonJS

CommonJSNode.js的模块规范,Node.js中使用require来引入模块,使用module.exports来导出模块;

所以这里通过判断module是否存在来判断是否是CommonJS环境,如果是的话,就通过module.exports来导出模块。

AMD

AMDRequireJS在推广过程中对模块定义的规范化产出,AMD也是一种模块规范,AMD中使用define来定义模块,使用require来引入模块;

所以这里通过判断define是否存在来判断是否是AMD环境,如果是的话,就通过define来定义模块。

window 浏览器环境

window是浏览器中的全局对象,这里并没有判断,直接使用else兜底,因为这个库最终只会在浏览器中使用,所以这里直接使用window来定义模块。

实现

多个参数处理

接下来就来看看classNames函数的实现了,先来看看他是怎么处理多个参数的:

function classNames() {for (var i = 0; i < arguments.length; i++) {var arg = arguments[i];if (!arg) continue;}
} 

这里是直接使用arguments来获取参数,然后遍历参数,如果参数不存在,就直接continue

参考:arguments

参数类型处理

接下来就来看看参数类型的处理:

// ------省略其他代码------

var argType = typeof arg;

if (argType === 'string' || argType === 'number') {// string or numberclasses.push(arg);
} else if (Array.isArray(arg)) {// array
} else if (argType === 'object') {// object
} 

这里是通过typeof来判断参数的类型,只有三种分支结果:

1.string或者number,直接pushclasses数组中;
2.array,这里是递归调用classNames函数,将数组中的每一项作为参数传入;
3.object,这里是遍历对象的每一项,如果值为true,则将key作为类名pushclasses数组中;

string或者number的处理比较简单,就不多说了,接下来就来看看arrayobject的处理:

数组处理

// ------省略其他代码------

if (arg.length) {var inner = classNames.apply(null, arg);if (inner) {classes.push(inner);}
} 

这里的处理是先判断数组的长度,通过隐式转换,如果数组长度为0,则不会进入if分支;

然后就直接通过apply来调用classNames函数,将数组作为参数传入,这里的null是因为apply的第一个参数是this,这里没有this,所以传入null

然后获取返回值,如果返回值存在,则将返回值pushclasses数组中;

参考:apply

对象处理

  • 判断对象toString是否被重写:
// ------省略其他代码------
if (arg.toString !== Object.prototype.toString && !arg.toString.toString().includes('[native code]')) {classes.push(arg.toString());continue;
} 

这里的处理是先判断argtoString方法是否被重写,如果被重写了,则直接将argtoString方法的返回值pushclasses数组中;

这一步可以说是很巧妙,第一个判断是判断argtoString方法是否被重写;

第二个判断是判断Object.prototype.toString方法是否被重写,如果被重写了,则argtoString方法的返回值一定不会包含[native code]

  • 遍历对象的每一项:
for (var key in arg) {if (hasOwn.call(arg, key) && arg[key]) {classes.push(key);}
} 

这里使用for...in来遍历对象的每一项;

然后通过Object.prototype.hasOwnProperty.call来判断对象是否有某一项;

最后判断对象的某一项的值是否为真值,并不是直接判断arg[key]是否为true,这样可以处理arg[key]为不为boolean的情况;

然后将对象的key作为类名pushclasses数组中;

最后函数结束,通过joinclasses数组转换为字符串,返回;

测试用例

test目录下可以看到index.js文件,这里是测试用例,可以通过npm run test来运行测试用例;

这里测试用例测试了很多边界情况,通过测试用例上面的代码就可以看出来了:

  • 只有为真值的键值才会被保留
it('keeps object keys with truthy values', function () {assert.equal(classNames({a: true,b: false,c: 0,d: null,e: undefined,f: 1}), 'a f');
}); 
  • 参数中如果存在假值会被忽略
it('joins arrays of class names and ignore falsy values', function () {assert.equal(classNames('a', 0, null, undefined, true, 1, 'b'), 'a 1 b');
}); 

这里还传递了一个true,因为是boolean类型,在程序中是直接被忽略的,所以不会被保留;

  • 支持多种不同类型的参数
it('supports heterogenous arguments', function () {assert.equal(classNames({a: true}, 'b', 0), 'a b');
}); 
  • 不会保留无意义的参数
it('should be trimmed', function () {assert.equal(classNames('', 'b', {}, ''), 'b');
}); 
  • 空的参数会返回空字符串
it('returns an empty string for an empty configuration', function () {assert.equal(classNames({}), '');
}); 
  • 支持数组类型的参数
it('supports an array of class names', function () {assert.equal(classNames(['a', 'b']), 'a b');
}); 
  • 数组参数会和其他参数一起合并
it('joins array arguments with string arguments', function () {assert.equal(classNames(['a', 'b'], 'c'), 'a b c');assert.equal(classNames('c', ['a', 'b']), 'c a b');
}); 
  • 多个数组参数
it('handles multiple array arguments', function () {assert.equal(classNames(['a', 'b'], ['c', 'd']), 'a b c d');
}); 
  • 数组中包含真值和假值
it('handles arrays that include falsy and true values', function () {assert.equal(classNames(['a', 0, null, undefined, false, true, 'b']), 'a b');
}); 
  • 嵌套数组
it('handles arrays that include arrays', function () {assert.equal(classNames(['a', ['b', 'c']]), 'a b c');
}); 
  • 数组中包含对象
it('handles arrays that include objects', function () {assert.equal(classNames(['a', {b: true, c: false}]), 'a b');
}); 
  • 深层嵌套数组和对象
it('handles deep array recursion', function () {assert.equal(classNames(['a', ['b', ['c', {d: true}]]]), 'a b c d');
}); 
  • 空数组
it('handles arrays that are empty', function () {assert.equal(classNames('a', []), 'a');
}); 
  • 嵌套的空数组
it('handles nested arrays that have empty nested arrays', function () {assert.equal(classNames('a', [[]]), 'a');
}); 
  • 所有类型的数据,包括预期的真值和假值
it('handles all types of truthy and falsy property values as expected', function () {assert.equal(classNames({// falsy:null: null,emptyString: "",noNumber: NaN,zero: 0,negativeZero: -0,false: false,undefined: undefined,// truthy (literally anything else):nonEmptyString: "foobar",whitespace: ' ',function: Object.prototype.toString,emptyObject: {},nonEmptyObject: {a: 1, b: 2},emptyList: [],nonEmptyList: [1, 2, 3],greaterZero: 1}), 'nonEmptyString whitespace function emptyObject nonEmptyObject emptyList nonEmptyList greaterZero');
}); 
  • 重写toString方法的对象
it('handles toString() method defined on object', function () {assert.equal(classNames({toString: function () {return 'classFromMethod';}}), 'classFromMethod');
}); 
  • 处理来自继承的toString方法
it('handles toString() method defined inherited in object', function () {var Class1 = function () {};var Class2 = function () {};Class1.prototype.toString = function () {return 'classFromMethod';}Class2.prototype = Object.create(Class1.prototype);assert.equal(classNames(new Class2()), 'classFromMethod');
}); 
  • 在虚拟机上运行
it('handles objects in a VM', function () {var context = {classNames, output: undefined};vm.createContext(context);var code = 'output = classNames({ a: true, b: true });';vm.runInContext(code, context);assert.equal(context.output, 'a b');
}); 

Css-in-JS

Css-in-JS是一种将CssJavaScript结合在一起的方法,它允许你在JavaScript中使用Css,并且可以在运行时动态地生成Css

这种方法的优点是可以在JavaScript中使用Css的所有功能,包括变量、条件语句、循环等,而且可以在运行时动态地生成Css,这样就可以根据不同的状态来生成不同的Css,从而实现更加丰富的交互效果。

Css-in-JS的缺点是会增加JavaScript的体积,因为JavaScript中的Css是以字符串的形式存在的,所以会增加JavaScript的体积。

Css-in-JS的实现方式有很多种,比如styled-componentsglamorousglamoraphroditeradium等。

而这个库就是一个将className可以动态生成的库,在库的README中有在React中使用的例子,其实完全可以抛开React,在任何需要的地方使用。

示例

例如我在普通的HTML中使用className,例如有一个按钮,我想根据按钮的状态来动态地生成className,那么可以这样写:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Document</title><style> .btn {width: 100px;height: 30px;background-color: #ccc;}.btn-size-large {width: 200px;height: 60px;}.btn-size-small {width: 50px;height: 15px;}.btn-type-primary {background-color: #f00;}.btn-type-secondary {background-color: #0f0;} </style>
</head>
<body><button class="btn btn-size-large btn-type-primary" onclick="toggleSize(this)">切换大小</button><button class="btn btn-size-large btn-type-primary" onclick="toggleType(this)">切换状态</button><script src="classnames.js"></script><script> function toggleSize(el) {el.className = classNames('btn', {'btn-size-large': el.className.indexOf('btn-size-large') === -1,'btn-size-small': el.className.indexOf('btn-size-large') !== -1});}function toggleType(el) {el.className = classNames('btn', {'btn-type-primary': el.className.indexOf('btn-type-primary') === -1,'btn-type-secondary': el.className.indexOf('btn-type-primary') !== -1});} </script>
</body>
</html> 

总结

classnames是一个非常简单的库,但是它的功能却非常强大,它可以根据不同的条件来动态地生成className,这样就可以根据不同的状态来动态地生成不同的className,从而实现更加丰富的交互效果。

除了React在使用Css-in-JS,还有很多库都在使用Css-in-JS的方式来实现,这个库代码量虽然少,但是带来的概念却是非常重要的,所以值得学习。

其实抛开Css-in-JS的概念,这个库的实现也很值得我们学习,例如对参数的处理,深层嵌套的数据结构的处理,已经测试用例的完善程度等等,都是值得我们学习的。

最后

整理了一套《前端大厂面试宝典》,包含了HTML、CSS、JavaScript、HTTP、TCP协议、浏览器、VUE、React、数据结构和算法,一共201道面试题,并对每个问题作出了回答和解析。

有需要的小伙伴,可以点击文末卡片领取这份文档,无偿分享

部分文档展示:



文章篇幅有限,后面的内容就不一一展示了

有需要的小伙伴,可以点下方卡片免费领取

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/111645.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring 事务失效的常见八大场景,注意避坑

1. 抛出检查异常导致事务不能正确回滚 Servicepublic class Service1 {Autowiredprivate AccountMapper accountMapper;Transactionalpublic void transfer(int from, int to, int amount) throws FileNotFoundException {int fromBalance accountMapper.findBalanceBy(from);…

【源码共读】学习 axios 源码整体架构 (II)

源码分析 跳转至Axios.js文件中 // 构造函数 constructor(instanceConfig) {this.defaults instanceConfig// 创建对应的拦截器this.interceptors {request: new InterceptorManager(),response: new InterceptorManager()} } 那么&#xff0c;拦截器是怎么创建的呢 首先&a…

【云服务器 ECS 实战】一文掌握弹性伸缩服务原理及配置方法

1. 弹性伸缩概述2. 实现模式3. 基于 GRE 实现 VPC 的互联4. 弹性伸缩服务的配置使用4.1 创建伸缩组4.2 伸缩配置4.3 创建伸缩规则1. 弹性伸缩概述 弹性伸缩&#xff08;Auto Scaling&#xff09;就是自动为我们调整弹性计算资源大小&#xff0c;以满足业务需求的变化&#xff…

javaee之spring1

什么是Spring 一、Spring的优势 二、Spring的体系结构 先说一下从什么位置去下载Spring的源码 进入Spring官网&#xff0c;找到Spring Framework框架 点进去之后&#xff0c;找到如下位置&#xff0c;继续点击 进去之后&#xff0c;继续下拉&#xff0c;找到下面这个位置点进…

慕了,我要是早点看到这篇写 Kafka 的分区管理的文章就好了

Kafka可以将主题划分为多个分区&#xff08;Partition&#xff09;&#xff0c;会根据分区规则选择把消息存储到哪个分区中&#xff0c;只要如果分区规则设置的合理&#xff0c;那么所有的消息将会被均匀的分布到不同的分区中&#xff0c;这样就实现了负载均衡和水平扩展。另外…

可以做抽奖活动的微信小程序在哪里做_分享抽奖活动小程序制作步骤

越来越多的企业开始了解微信抽奖游戏的实用性和价值&#xff0c;因为用户更喜欢简单有趣的游戏抽奖方式&#xff0c;如大转盘、摇一摇、抢福袋、砸金蛋、摇一摇、刮刮卡等互动抽奖游戏。 如果企业想制作这种抽奖游戏&#xff0c;都倾向使用市场上的各种抽奖制作软件&#xff0c…

(Java)车厢重组

车厢重组一、题目描述二、输入格式三、输出格式四、样例&#xff08;1&#xff09;样例输入&#xff08;2&#xff09;样例输出五、正确代码六、思路一、题目描述 在一个旧式的火车站旁边有一座桥&#xff0c;其桥面可以绕河中心的桥墩水平旋转。一个车站的职工发现桥的长度最…

网络技术——网络运维工程师必会的网络知识(2)(详细讲解)

作者简介&#xff1a;一名在校云计算网络运维学生、每天分享网络运维的学习经验、和学习笔记。 座右铭&#xff1a;低头赶路&#xff0c;敬事如仪 个人主页&#xff1a;网络豆的主页​​​​​​ 目录 前言 网络传输介质 信号分类和失真 双绞线分类&#xff1a; 双绞线…

非计算机专业,可以学好编程吗?

现在IT行业越来越火热&#xff0c;想要学习编程的人也越来越多。IT行业的薪资连续好几年赶超金融行业&#xff0c;位居行业之首&#xff0c;有太多人转行跨界&#xff0c;想要进入这个领域&#xff0c;那么作为初学者的你&#xff0c;是不是也很困惑&#xff0c;非科班&#xf…

Web入门开发【四】- 基础语言

欢迎来到霍大侠的小院&#xff0c;我们来学习Web入门开发的系列课程。 首先我们来了解下这个课程能学到什么&#xff1f; 1、你将可以掌握Web网站的开发全过程。 2、了解基础的HTML&#xff0c;CSS&#xff0c;JavaScript语言。 3、开发自己的第一个网站。 4、认识很多对编…

Java笔记之多线程(一)

文章目录前言一、什么是进程与线程&#xff1f;1.进程2.线程3.其他相关概念二、如何创建线程1.继承Thread类&#xff0c;重新run方法2.实现Runnable接口3.通过Callable和Future创建线程4. 继承Thread vs实现Runnable的区别三、用户线程和守护线程守护线程的使用设置成守护线程四…

【Python百日进阶-数据分析】Day137 - plotly旭日图:go.sunburst()实例

文章目录4.2 带有 go.Sunburst 的基本旭日图4.2.1 基本go.sunburst()旭日图4.2.2 带有重复标签的旭日图4.2.3 分支值4.2.4 大量切片4.2.5 控制旭日形扇区内的文本方向4.2.6 使用 uniformtext 控制文本字体大小4.2.7 具有连续色标的旭日图4.2.8 Dash中的go.sunburst()4.2 带有 g…

Android hilt 依赖注入使用详解

文章目录官方文档添加依赖初始化hiltMainActivity 使用共享类在 MainActivity 添加依赖注入ActivityScoped 作用域Singleton 作用域构造参数&#xff0c;添加 Context参数ApplicationContext、ActivityContext官方文档 https://developer.android.com/training/dependency-inj…

【Linux】缓冲区/磁盘inode/动静态库制作

目录 一、缓冲区 1、缓冲区的概念 2、缓冲区的意义 3、缓冲区刷新策略 4、同一份代码&#xff0c;打印结果不同 5、仿写FILE 5.1myFILE.h 5.2myFILE.c 5.3main.c 6、内核缓冲区 二、了解磁盘 1、磁盘的物理结构 2、磁盘的存储结构 2.1磁盘的定位 3、磁盘的抽象…

基于价值迭代求解迷宫寻路问题

摘 要 迷宫寻路是人工智能和计算机科学中一个经典的问题。它涉及在迷宫中找到一条从起点到终点的最短路径。这个问题可以用来模拟真实世界中的许多情况&#xff0c;例如机器人在工厂中自动导航&#xff0c;搜索引擎在网络中寻找信息&#xff0c;或者人类在城市中导航。 迷宫寻路…

【Javascript基础】--零基础--超详细且简洁的Javascript笔记--简介(01)

参考资料&#xff1a; 【现代Javascript教程】https://zh.javascript.info/ 【MDN】https://developer.mozilla.org/zh-CN/ 笔记仅作为学习交流载体&#xff0c;无任何商业或盈利目的 JavaScript 简介 了解 JavaScript 有什么特别之处&#xff0c;我们可以用它实现什么&#…

适合编程初学者的开源博客系统(Vue3+Element Plus版)

目标 为编程初学者打造入门学习项目&#xff0c;使用各种主流编程语言来实现。让想学编程的&#xff0c;一个都不落下。 上述基本涵盖了当前编程开发所有主流语言。 左侧为前端版本&#xff1a;安卓、iOS、鸿蒙、Flutter、Vue、uni-app、微信小程序。 右侧为服务器端版本&am…

YOLOV7学习记录之模型推理

前面我们学习了YOLOV7的训练过程&#xff0c;今天我们学习其推理过程&#xff0c;即模型预测&#xff1a;其包含损失函数计算&#xff0c;输出值解码&#xff0c;非极大值抑制&#xff0c;mAP计算等过程。 同时还介绍原始图像上绘制目标框等功能。 我们从predict.py文件开始&am…

【源码共读】Vite 项目自动添加 eslint 和 prettier

vite-pretty-lint库是一个为Vite创建的Vue或React项目初始化eslint和prettier的库。 该库的目的是为了让开发者在创建项目时&#xff0c;不需要手动配置eslint和prettier&#xff0c;而是通过vite-pretty-lint库来自动配置。 源码地址&#xff1a; vite-pretty-lintgithub1s…

设计模式 - 单例模式(一)

单例模式一 官方定义二 单例模式八种方式2.1 饿汉式&#xff08;静态常量&#xff09;代码案例案例分析2.2 饿汉式&#xff08;静态代码块&#xff09;代码案例案例分析2.3 懒汉式(线程不安全)代码案例案例分析2.4 懒汉式(线程安全&#xff0c;同步方法)代码案例案例分析2.5 懒…