首頁 > 軟體

Css-In-Js實現classNames庫原始碼解讀

2022-12-29 14:01:32

引言

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庫的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com