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

news/2024/5/21 0:14:51/文章来源:https://blog.csdn.net/web22050702/article/details/128416889

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.luyixian.cn/news_show_616847.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

我国牛血清行业现状:FBS是最常用血清添加剂 但目前市场亟需规范化

根据观研报告网发布的《中国牛血清行业现状深度研究与投资前景分析报告&#xff08;2022-2029年&#xff09;》显示&#xff0c;牛血清是血清的一种&#xff0c;是一种浅黄色澄清、无溶血、无异物稍粘稠液体&#xff0c;内含有各种血浆蛋白、多肽、脂肪、碳水化合物、生长因子、…

Unity下如何实现RTMP或RTSP流播放和录制

技术背景 在探讨Unity平台RTMP或RTSP直播流数据播放和录制之前&#xff0c;我们先简单回顾下RTSP或RTMP直播流数据在Unity平台的播放流程&#xff1a; 通过Native RTSP或RTSP直播播放SDK回调RGB/YUV420/NV12等其中的一种未压缩的图像格式&#xff1b;Unity下创建相应的RGB/YU…

c# winform 重启自己 简单实现

1.情景 有些时候&#xff0c;系统会出问题&#xff0c;问题原因很难排除&#xff0c;但是重启问题就能修正&#xff0c;这时候我们就需要在一个检测到问题的时机&#xff0c;让系统进行一次重启。 2.代码 using System; using System.Windows.Forms;namespace 程序重启自己 …

IDEA创建kotlin项目

今天新建了一个kotlin项目&#xff0c;竟然不能导入jar包&#xff0c;原因是新建项目的时候&#xff0c;选择了kotlin作为Gradle的开发语音&#xff0c;kotlin语音里面&#xff0c;下面这行配置识别不了&#xff1a; implementation fileTree(dir: libs, include: [*.jar])所以…

Selenium 常用函数总结

Seleninum作为自动化测试的工具&#xff0c;自然是提供了很多自动化操作的函数&#xff0c; 下面列举下个人觉得比较常用的函数&#xff0c;更多可见官方文档&#xff1a; 官方API文档&#xff1a; http://seleniumhq.github.io/selenium/docs/api/py/api.html 1) 定位元素 f…

Fragment

Fragment简单认识 1.简介 在大屏幕设备上支持更加动态和灵活的UI设计就是一种卡片的设计思路一个Activity可以有多个Fragment&#xff0c;一个Fragment可以被多个Activity使用可以进行动态的添加&#xff0c;替换和删除Fragment有着自己的生命周期&#xff0c;同时受到Activity…

Shiro之授权

授权 1、角色认证 在controller层创建接口 使用shiro中的注解RequiresRoles指定能访问的角色名称 /*** 登录认证角色*/ RequiresRoles("admin") GetMapping("/userLoginRoles") ResponseBody public String userLoginRoles(){System.out.println("…

微信键盘终于正式发布,张小龙说:其目的并不是为了抢夺输入法市场

自从2021年1月份&#xff0c;张小龙在微信公开课透露&#xff1a;微信将上线属于自己的专属输入法&#xff0c;到现在已经快2年过了。 今天终于正式发布了&#xff0c;下面我们一起来体验下。 1、安装 打开App Store&#xff0c;输入“微信键盘”&#xff0c;点击获取就可以…

基于Springboot+Mybatis+mysql+element-vue高校就业管理系统

基于SpringbootMybatismysqlelement-vue高校就业管理系统一、系统介绍二、功能展示1.用户登陆注册2.个人信息(学生端)3.查看企业岗位信息&#xff08;学生端&#xff09;4.我的应聘(学生端)5.学生信息管理&#xff08;辅导员&#xff09;6.三方协议书审核&#xff08;辅导员&am…

一文读懂Linux内核处理器架构中的栈

栈是什么&#xff1f;栈有什么作用&#xff1f; 首先&#xff0c;栈 (stack) 是一种串列形式的 数据结构。这种数据结构的特点是 后入先出 (LIFO, Last In First Out)&#xff0c;数据只能在串列的一端 (称为&#xff1a;栈顶 top) 进行 推入 (push) 和 弹出 (pop) 操作。根据…

自学编程和计算机科班出身的差别在哪里

前不久逛知乎的时候看到一个问题&#xff1a;自学编程和计算机科班出身的差别在哪里&#xff1f; 自己回答了一下&#xff0c;获得了比较多的点赞和评论&#xff0c;在这里也分享给大家。 985 通信专业学长&#xff0c;转行程序员&#xff0c;聊一聊我的看法&#xff1a;说一千…

YOLOV3论文学习

YOLOv3论文链接&#xff1a;https://pjreddie.com/media/files/papers/YOLOv3.pdf 综述 一、摘要 1、320*320的YOLOv3推理时间22ms&#xff0c;准确率28.2mAP&#xff0c;达到了SSD的精确度&#xff0c;推理速度却快了三倍。 2、基于.5mAp Iou 的YOLOv3的检测效果还比较不错&a…

Doo Prime 为泰国 SOS 儿童村送温暖,公益有起点爱心无疆界

一年一度的圣诞节即将来临&#xff0c;在这欢乐的时刻&#xff0c; Doo Prime 荣幸地宣布 &#xff0c;向泰国 SOS 儿童村捐赠了 35 万泰铢 ( 约合 1.23 万美元 )&#xff0c;作为泰国南部城市合艾府 SOS 儿童村的房屋翻修费用。 Doo Prime 希望 SOS 儿童村的孩子们都能在温馨…

Android入门第55天-在Android里使用OKHttp组件访问网络资源

简介 今天的课程开始进入高级课程类了&#xff0c;我们要开始接触网络协议、设备等领域编程了。在今天的课程里我们会使用OKHttp组件来访问网络资源而不是使用Android自带的URLConnection。一个是OKHttp组件更方便二个是OKHttp组件本身就带有异步回调功能。 下面就进入课程。…

(Java)车厢重组

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

Fragment案例

Fragment案例 1.案例要求 框架布局项目难点&#xff1a;1 导航栏的实现&#xff0c;显示导航按钮、切换Fragment 2 每个Fragment的创建、显示 3 Fragment的跳转&#xff08;从新闻列表到新闻详情&#xff0c;再返回&#xff09; 涉及的技术&#xff1a;用RadioGroup及RadioButt…

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

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

3ds Max:标准几何体

三维软件中一般有许多非常复杂的命令&#xff0c;能够完成非常复杂的图形运算&#xff0c;但其实许多绚丽的图形也是由最基本的几何体构成&#xff0c;许多复杂的命令也是基本的运算程序的集合&#xff0c;就像是砖块&#xff0c;构成了复杂的大厦。任何一个几何体&#xff0c;…

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

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

Sentinel系列——概述与安装1-1

Sentinel系列——概述与安装1-1概述服务雪崩解决方法基本概念资源规则Sentinel 是如何工作的安装Sentinel下载地址启动修改sentinel启动参数设置启动端口设置用户名密码概述 随着微服务的流行&#xff0c;服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式、多语言…