Css-In-Js實(shí)現(xiàn)classNames庫(kù)源碼解讀
引言
classNames
是一個(gè)簡(jiǎn)單的且實(shí)用的JavaScript
應(yīng)用程序,可以有條件的將多個(gè)類名組合在一起。它是一個(gè)非常有用的工具,可以用來(lái)動(dòng)態(tài)的添加或者刪除類名。
倉(cāng)庫(kù)地址:classNames
使用
根據(jù)classNames
的README
,可以發(fā)現(xiàn)庫(kù)的作者對(duì)這個(gè)庫(kù)非常認(rèn)真,文檔和測(cè)試用例都非常齊全,同時(shí)還有有不同環(huán)境的支持。
其他的就不多介紹了,因?yàn)閹?kù)的作者寫的很詳細(xì),就直接上使用示例:
var classNames = require('classnames'); classNames('foo', 'bar'); // => 'foo bar'
- 可以是多個(gè)字符串
classNames('foo', 'bar'); // => 'foo bar'
- 可以是字符串和對(duì)象的組合
classNames('foo', { bar: true }); // => 'foo bar'
- 可以是純對(duì)象
classNames({ 'foo-bar': true }); // => 'foo-bar' classNames({ 'foo-bar': false }); // => ''
- 可以是多個(gè)對(duì)象
classNames({ foo: true }, { bar: true }); // => 'foo bar' classNames({ foo: true, bar: true }); // => 'foo bar'
- 多種不同數(shù)據(jù)類型的組合
classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); // => 'foo bar baz quux'
- 假值會(huì)被忽略
classNames(null, false, 'bar', undefined, 0, 1, { baz: null }, ''); // => 'bar 1'
- 可以是數(shù)組,數(shù)組中的元素可以是字符串、對(duì)象、數(shù)組,會(huì)被展平處理
var arr = ['b', { c: true, d: false }]; classNames('a', arr); // => 'a b c'
- 可以是動(dòng)態(tài)屬性名
let buttonType = 'primary'; classNames({ [`btn-${buttonType}`]: true });
還有其他的使用方式,包括在React
中的使用,可以去看看README
,接下里就開始閱讀源碼。
源碼閱讀
先來(lái)直接來(lái)看看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
的實(shí)現(xiàn)非常簡(jiǎn)單,一共就是50
行左右的代碼,其中有一些是注釋,有一些是兼容性的代碼,主要的代碼邏輯就是classNames
函數(shù),這個(gè)函數(shù)就是我們最終使用的函數(shù),接下來(lái)就來(lái)看看這個(gè)函數(shù)的實(shí)現(xiàn)。
兼容性
直接看最后的一段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; }
可以看到這里兼容了CommonJS
、AMD
、window
三種方式,這樣就可以在不同的環(huán)境下使用了。
一下就看到了三種兼容性方式的區(qū)別和特性了:
CommonJS
CommonJS
是Node.js
的模塊規(guī)范,Node.js
中使用require
來(lái)引入模塊,使用module.exports
來(lái)導(dǎo)出模塊;
所以這里通過(guò)判斷module
是否存在來(lái)判斷是否是CommonJS
環(huán)境,如果是的話,就通過(guò)module.exports
來(lái)導(dǎo)出模塊。
AMD
AMD
是RequireJS
在推廣過(guò)程中對(duì)模塊定義的規(guī)范化產(chǎn)出,AMD
也是一種模塊規(guī)范,AMD
中使用define
來(lái)定義模塊,使用require
來(lái)引入模塊;
所以這里通過(guò)判斷define
是否存在來(lái)判斷是否是AMD
環(huán)境,如果是的話,就通過(guò)define
來(lái)定義模塊。
window 瀏覽器環(huán)境
window
是瀏覽器中的全局對(duì)象,這里并沒(méi)有判斷,直接使用else
兜底,因?yàn)檫@個(gè)庫(kù)最終只會(huì)在瀏覽器中使用,所以這里直接使用window
來(lái)定義模塊。
實(shí)現(xiàn)
多個(gè)參數(shù)處理
接下來(lái)就來(lái)看看classNames
函數(shù)的實(shí)現(xiàn)了,先來(lái)看看他是怎么處理多個(gè)參數(shù)的:
function classNames() { for (var i = 0; i < arguments.length; i++) { var arg = arguments[i]; if (!arg) continue; } }
這里是直接使用arguments
來(lái)獲取參數(shù),然后遍歷參數(shù),如果參數(shù)不存在,就直接continue
;
參考:arguments
參數(shù)類型處理
接下來(lái)就來(lái)看看參數(shù)類型的處理:
// ------ 省略其他代碼 ------ 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 }
這里是通過(guò)typeof
來(lái)判斷參數(shù)的類型,只有三種分支結(jié)果:
string
或者number
,直接push
到classes
數(shù)組中;array
,這里是遞歸調(diào)用classNames
函數(shù),將數(shù)組中的每一項(xiàng)作為參數(shù)傳入;object
,這里是遍歷對(duì)象的每一項(xiàng),如果值為true
,則將key
作為類名push
到classes
數(shù)組中;
string
或者number
的處理比較簡(jiǎn)單,就不多說(shuō)了,接下來(lái)就來(lái)看看array
和object
的處理:
數(shù)組處理
// ------ 省略其他代碼 ------ if (arg.length) { var inner = classNames.apply(null, arg); if (inner) { classes.push(inner); } }
這里的處理是先判斷數(shù)組的長(zhǎng)度,通過(guò)隱式轉(zhuǎn)換,如果數(shù)組長(zhǎng)度為0
,則不會(huì)進(jìn)入if
分支;
然后就直接通過(guò)apply
來(lái)調(diào)用classNames
函數(shù),將數(shù)組作為參數(shù)傳入,這里的null
是因?yàn)?code>apply的第一個(gè)參數(shù)是this
,這里沒(méi)有this
,所以傳入null
;
然后獲取返回值,如果返回值存在,則將返回值push
到classes
數(shù)組中;
參考:apply
對(duì)象處理
- 判斷對(duì)象
toString
是否被重寫:
// ------ 省略其他代碼 ------ if (arg.toString !== Object.prototype.toString && !arg.toString.toString().includes('[native code]')) { classes.push(arg.toString()); continue; }
這里的處理是先判斷arg
的toString
方法是否被重寫,如果被重寫了,則直接將arg
的toString
方法的返回值push
到classes
數(shù)組中;
這一步可以說(shuō)是很巧妙,第一個(gè)判斷是判斷arg
的toString
方法是否被重寫;
第二個(gè)判斷是判斷Object.prototype.toString
方法是否被重寫,如果被重寫了,則arg
的toString
方法的返回值一定不會(huì)包含[native code]
;
- 遍歷對(duì)象的每一項(xiàng):
for (var key in arg) { if (hasOwn.call(arg, key) && arg[key]) { classes.push(key); } }
這里使用for...in
來(lái)遍歷對(duì)象的每一項(xiàng);
然后通過(guò)Object.prototype.hasOwnProperty.call
來(lái)判斷對(duì)象是否有某一項(xiàng);
最后判斷對(duì)象的某一項(xiàng)的值是否為真值,并不是直接判斷arg[key]
是否為true
,這樣可以處理arg[key]
為不為boolean
的情況;
然后將對(duì)象的key
作為類名push
到classes
數(shù)組中;
最后函數(shù)結(jié)束,通過(guò)join
將classes
數(shù)組轉(zhuǎn)換為字符串,返回;
測(cè)試用例
在test
目錄下可以看到index.js
文件,這里是測(cè)試用例,可以通過(guò)npm run test
來(lái)運(yùn)行測(cè)試用例;
這里測(cè)試用例測(cè)試了很多邊界情況,通過(guò)測(cè)試用例上面的代碼就可以看出來(lái)了:
- 只有為真值的鍵值才會(huì)被保留
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'); });
- 參數(shù)中如果存在假值會(huì)被忽略
it('joins arrays of class names and ignore falsy values', function () { assert.equal(classNames('a', 0, null, undefined, true, 1, 'b'), 'a 1 b'); });
這里還傳遞了一個(gè)true
,因?yàn)槭?code>boolean類型,在程序中是直接被忽略的,所以不會(huì)被保留;
- 支持多種不同類型的參數(shù)
it('supports heterogenous arguments', function () { assert.equal(classNames({a: true}, 'b', 0), 'a b'); });
- 不會(huì)保留無(wú)意義的參數(shù)
it('should be trimmed', function () { assert.equal(classNames('', 'b', {}, ''), 'b'); });
- 空的參數(shù)會(huì)返回空字符串
it('returns an empty string for an empty configuration', function () { assert.equal(classNames({}), ''); });
- 支持?jǐn)?shù)組類型的參數(shù)
it('supports an array of class names', function () { assert.equal(classNames(['a', 'b']), 'a b'); });
- 數(shù)組參數(shù)會(huì)和其他參數(shù)一起合并
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'); });
- 多個(gè)數(shù)組參數(shù)
it('handles multiple array arguments', function () { assert.equal(classNames(['a', 'b'], ['c', 'd']), 'a b c d'); });
- 數(shù)組中包含真值和假值
it('handles arrays that include falsy and true values', function () { assert.equal(classNames(['a', 0, null, undefined, false, true, 'b']), 'a b'); });
- 嵌套數(shù)組
it('handles arrays that include arrays', function () { assert.equal(classNames(['a', ['b', 'c']]), 'a b c'); });
- 數(shù)組中包含對(duì)象
it('handles arrays that include objects', function () { assert.equal(classNames(['a', {b: true, c: false}]), 'a b'); });
- 深層嵌套數(shù)組和對(duì)象
it('handles deep array recursion', function () { assert.equal(classNames(['a', ['b', ['c', {d: true}]]]), 'a b c d'); });
- 空數(shù)組
it('handles arrays that are empty', function () { assert.equal(classNames('a', []), 'a'); });
- 嵌套的空數(shù)組
it('handles nested arrays that have empty nested arrays', function () { assert.equal(classNames('a', [[]]), 'a'); });
- 所有類型的數(shù)據(jù),包括預(yù)期的真值和假值
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
方法的對(duì)象
it('handles toString() method defined on object', function () { assert.equal(classNames({ toString: function () { return 'classFromMethod'; } }), 'classFromMethod'); });
- 處理來(lái)自繼承的
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'); });
- 在虛擬機(jī)上運(yùn)行
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
是一種將Css
和JavaScript
結(jié)合在一起的方法,它允許你在JavaScript
中使用Css
,并且可以在運(yùn)行時(shí)動(dòng)態(tài)地生成Css
。
這種方法的優(yōu)點(diǎn)是可以在JavaScript
中使用Css
的所有功能,包括變量、條件語(yǔ)句、循環(huán)等,而且可以在運(yùn)行時(shí)動(dòng)態(tài)地生成Css
,這樣就可以根據(jù)不同的狀態(tài)來(lái)生成不同的Css
,從而實(shí)現(xiàn)更加豐富的交互效果。
Css-in-JS
的缺點(diǎn)是會(huì)增加JavaScript
的體積,因?yàn)?code>JavaScript中的Css
是以字符串的形式存在的,所以會(huì)增加JavaScript
的體積。
Css-in-JS
的實(shí)現(xiàn)方式有很多種,比如styled-components
、glamorous
、glamor
、aphrodite
、radium
等。
而這個(gè)庫(kù)就是一個(gè)將className
可以動(dòng)態(tài)生成的庫(kù),在庫(kù)的README
中有在React
中使用的例子,其實(shí)完全可以拋開React
,在任何需要的地方使用。
示例
例如我在普通的HTML
中使用className
,例如有一個(gè)按鈕,我想根據(jù)按鈕的狀態(tài)來(lái)動(dòng)態(tài)地生成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)">切換狀態(tài)</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>
總結(jié)
classnames
是一個(gè)非常簡(jiǎn)單的庫(kù),但是它的功能卻非常強(qiáng)大,它可以根據(jù)不同的條件來(lái)動(dòng)態(tài)地生成className
,這樣就可以根據(jù)不同的狀態(tài)來(lái)動(dòng)態(tài)地生成不同的className
,從而實(shí)現(xiàn)更加豐富的交互效果。
除了React
在使用Css-in-JS
,還有很多庫(kù)都在使用Css-in-JS
的方式來(lái)實(shí)現(xiàn),這個(gè)庫(kù)代碼量雖然少,但是帶來(lái)的概念卻是非常重要的,所以值得學(xué)習(xí)。
其實(shí)拋開Css-in-JS
的概念,這個(gè)庫(kù)的實(shí)現(xiàn)也很值得我們學(xué)習(xí),例如對(duì)參數(shù)的處理,深層嵌套的數(shù)據(jù)結(jié)構(gòu)的處理,已經(jīng)測(cè)試用例的完善程度等等,都是值得我們學(xué)習(xí)的。
以上就是Css-In-Js實(shí)現(xiàn)classNames庫(kù)源碼解讀的詳細(xì)內(nèi)容,更多關(guān)于Css-In-Js實(shí)現(xiàn)classNames庫(kù)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
微信小程序 wxapp導(dǎo)航 navigator詳解
這篇文章主要介紹了微信小程序 wxapp導(dǎo)航 navigator詳解的相關(guān)資料,并附簡(jiǎn)單實(shí)例代碼,需要的朋友可以參考下2016-10-10js前端表單數(shù)據(jù)處理表單數(shù)據(jù)校驗(yàn)
這篇文章主要為大家介紹了js前端表單數(shù)據(jù)處理表單數(shù)據(jù)校驗(yàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07JavaScript的模塊化開發(fā)框架Sea.js上手指南
Sea.js的目的是追求簡(jiǎn)單的代碼書寫和組織方式,Sea.js并沒(méi)有過(guò)多功能而是主要對(duì)前端程序的部署結(jié)構(gòu)作出約束,下面我們就來(lái)看一下JavaScript的模塊化開發(fā)框架Sea.js上手指南:2016-05-05await-to-js源碼深入理解處理異步任務(wù)用法示例
這篇文章主要為大家介紹了await-to-js源碼更完美處理異步任務(wù)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08JS前端設(shè)計(jì)模式之發(fā)布訂閱模式詳解
這篇文章主要為大家介紹了JS前端設(shè)計(jì)模式之發(fā)布訂閱模式詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08