Css-In-Js实现classNames库源码解读

目录
  • 引言
  • 使用
  • 源码阅读
    • 兼容性
    • CommonJS
    • AMD
    • window 浏览器环境
  • 实现
    • 多个参数处理
    • 参数类型处理
    • 数组处理
    • 对象处理
    • 测试用例
  • Css-in-JS
    • 示例
  • 总结

引言

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文件,代码量并不多:

/*!
   Copyright (c) 2018 Jed Watson.
   Licensed under the MIT License (MIT), see
   http://jedwatson.github.io/classnames
*/
/* 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 name
      define('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 name
    define('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 number
    classes.push(arg);
} else if (Array.isArray(arg)) {
    // array
} else if (argType === 'object') {
    // object
}

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

  • string或者number,直接pushclasses数组中;
  • array,这里是递归调用classNames函数,将数组中的每一项作为参数传入;
  • 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) &amp;&amp; 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的概念,这个库的实现也很值得我们学习,例如对参数的处理,深层嵌套的数据结构的处理,已经测试用例的完善程度等等,都是值得我们学习的。

以上就是Css-In-Js实现classNames库源码解读的详细内容,更多关于Css-In-Js实现classNames库的资料请关注我们其它相关文章!

(0)

相关推荐

  • React classnames原理及测试用例

    目录 前言 classnames 的用法 学会 classnames 的原理 测试用例的使用 总结 前言 本期的源码阅读任务是: 学会 classnames 的用法 学会 classnames 的原理 测试用例的使用 源码地址:JedWatson/classnames: A simple javascript utility for conditionally joining classNames together (github.com) classnames 的用法 Classname 是一

  • React中classnames库使用示例

    目录 classnames的引入 引入 使用 Node.js, Browserify, or webpack: classnames函数的使用 数组的形式 ES6中使用动态类名 结合React一起使用 总结: classnames的引入 从名字上可以看出,这个库是和类名有关的.官方的介绍就是一个简单的支持动态多类名的工具库. 支持使用 npm, Bower, or Yarn 使用 npm安装 npm install classnames 使用 Bower安装 bower install clas

  • React通过classnames库添加类的方法

    React添加Class的方式 在vue中添加class是一件非常简单的事情: 你可以通过传入一个对象, 通过布尔值决定是否添加类: <button :class="{ active: isFlag, aaa: true}">按钮</button> 你也可以传入一个数组: <!-- 1.基本使用 --> <h2 :class="['aaa', 'bbb']">Hello Vue</h2> <!-- 2

  • Css-In-Js实现classNames库源码解读

    目录 引言 使用 源码阅读 兼容性 CommonJS AMD window 浏览器环境 实现 多个参数处理 参数类型处理 数组处理 对象处理 测试用例 Css-in-JS 示例 总结 引言 classNames是一个简单的且实用的JavaScript应用程序,可以有条件的将多个类名组合在一起.它是一个非常有用的工具,可以用来动态的添加或者删除类名. 仓库地址:classNames 使用 根据classNames的README,可以发现库的作者对这个库非常认真,文档和测试用例都非常齐全,同时还有有

  • ahooks整体架构及React工具库源码解读

    目录 引言 React hooks utils 库 ahooks 简介 特点 hooks 种类 ahooks 整体架构 项目启动 整体结构 hooks 总结 引言 本文是深入浅出 ahooks 源码系列文章的第一篇,这个系列的目标主要有以下几点: 加深对 React hooks 的理解. 学习如何抽象自定义 hooks.构建属于自己的 React hooks 工具库. 培养阅读学习源码的习惯,工具库是一个对源码阅读不错的选择. 注:本系列对 ahooks 的源码解析是基于 v3.3.13.自己

  • 分享JS表单验证源码(带错误提示及密码等级)

    先晒图 index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Index</title> <link rel="stylesheet" href="css/style.css"> </head> <body> &l

  • solid.js响应式createSignal 源码解析

    目录 正文 createSignal readSignal writeSignal 案例分析 总结 正文 www.solidjs.com/docs/latest… createSignal 用来创建响应式数据,它可以跟踪单个值的变化. solid.js 的响应式实现参考了 S.js,它是一个体积超小的 reactive 库,支持自动收集依赖和简单的响应式编程. createSignal createSignal 首先我们来看下 createSignal 的声明: // packages/soli

  • 非常实用的js验证框架实现源码 附原理方法

    本文为大家分享一个很实用的js验证框架实现源码,供大家参考,具体内容如下 关键方法和原理: function check(thisInput) 方法中的 if (!eval(scriptCode)) { return false; } 调用示例: 复制代码 代码如下: <input type="text" class="text_field percentCheck" name="progress_payment_two" id="

  • 模块一 GO语言基础知识-库源码文件

    你已经使用过 Go 语言编写了小命令(或者说微型程序)吗? 当你在编写"Hello, world"的时候,一个源码文件就足够了,虽然这种小玩意儿没什么用,最多能给你一点点莫名的成就感.如果你对这一点点并不满足,别着急,跟着学,我肯定你也可以写出很厉害的程序. 我们在上一篇的文章中学到了命令源码文件的相关知识,那么除了命令源码文件,你还能用 Go 语言编写库源码文件.那么什么是库源码文件呢? 在我的定义中,库源码文件是不能被直接运行的源码文件,它仅用于存放程序实体,这些程序实体可以被其他

  • Golang标准库unsafe源码解读

    目录 引言 unsafe包 unsafe构成 type ArbitraryType int type Pointer *ArbitraryType 灵活转换 潜在的危险性 正确的使用姿势 错误的使用姿势 func Sizeof(x ArbitraryType) uintptr func Offsetof(x ArbitraryType) uintptr func Alignof(x ArbitraryType) uintptr 引言 当你阅读Golang源码时一定遇到过unsafe.Pointe

  • Evil.js项目源码解读

    目录 引言 源码解析 立即执行函数 为什么要用立即执行函数? includes方法 map方法 filter方法 setTimeout Promise.then JSON.stringify Date.getTime localStorage.getItem 用途 引言 2022年8月18日,一个名叫Evil.js的项目突然走红,README介绍如下: 什么?黑心996公司要让你提桶跑路了? 想在离开前给你们的项目留点小 礼物 ? 偷偷地把本项目引入你们的项目吧,你们的项目会有但不仅限于如下的神

  • JS前端操作 Cookie源码示例解析

    目录 引言 源码分析 使用 源码 分析 set get remove withAttributes & withConverter 总结 引言 前端操作Cookie的场景其实并不多见,Cookie也因为各种问题被逐渐淘汰,但是我们不用Cookie也可以学习一下它的思想,或者通过这次的源码来学习其他的一些知识. 今天带来的是:js-cookie 源码分析 使用 根据README,我们可以看到js-cookie的使用方式: // 设置 Cookies.set('name', 'value'); //

  • Andorid jar库源码Bolts原理解析

    Bolts: 作用: 用于链式执行跨线程代码,且传递数据 栗子: 复制代码 Task.call(new Callable<Boolean>() { @Override public Boolean call() throws Exception { return true; } }, Task.UI_THREAD_EXECUTOR); Task.callInBackground(new Callable<Boolean>() { @Override public Boolean c

随机推荐