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

2022-12-29 09:05:21
目录
引言使用源码阅读兼容性CommonJSAMDwindow 浏览器环境实现多个参数处理参数类型处理数组处理对象处理测试用例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>来运行测试用例;

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

                          只有为真值的键值才会被保留
                          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库的资料请关注易采站长站其它相关文章!