Css-In-Js實現classNames庫源碼解讀
引言
classNames是一個簡單的且實用的JavaScript應用程序,可以有條件的將多個類名組合在一起。它是一個非常有用的工具,可以用來動態(tài)的添加或者刪除類名。
倉庫地址:classNames
使用
根據classNames的README,可以發(fā)現庫的作者對這個庫非常認真,文檔和測試用例都非常齊全,同時還有有不同環(huán)境的支持。
其他的就不多介紹了,因為庫的作者寫的很詳細,就直接上使用示例:
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'
- 可以是動態(tài)屬性名
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;
}
可以看到這里兼容了CommonJS、AMD、window三種方式,這樣就可以在不同的環(huán)境下使用了。
一下就看到了三種兼容性方式的區(qū)別和特性了:
CommonJS
CommonJS是Node.js的模塊規(guī)范,Node.js中使用require來引入模塊,使用module.exports來導出模塊;
所以這里通過判斷module是否存在來判斷是否是CommonJS環(huán)境,如果是的話,就通過module.exports來導出模塊。
AMD
AMD是RequireJS在推廣過程中對模塊定義的規(guī)范化產出,AMD也是一種模塊規(guī)范,AMD中使用define來定義模塊,使用require來引入模塊;
所以這里通過判斷define是否存在來判斷是否是AMD環(huán)境,如果是的話,就通過define來定義模塊。
window 瀏覽器環(huán)境
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,直接push到classes數組中;array,這里是遞歸調用classNames函數,將數組中的每一項作為參數傳入;object,這里是遍歷對象的每一項,如果值為true,則將key作為類名push到classes數組中;
string或者number的處理比較簡單,就不多說了,接下來就來看看array和object的處理:
數組處理
// ------ 省略其他代碼 ------
if (arg.length) {
var inner = classNames.apply(null, arg);
if (inner) {
classes.push(inner);
}
}
這里的處理是先判斷數組的長度,通過隱式轉換,如果數組長度為0,則不會進入if分支;
然后就直接通過apply來調用classNames函數,將數組作為參數傳入,這里的null是因為apply的第一個參數是this,這里沒有this,所以傳入null;
然后獲取返回值,如果返回值存在,則將返回值push到classes數組中;
參考:apply
對象處理
- 判斷對象
toString是否被重寫:
// ------ 省略其他代碼 ------
if (arg.toString !== Object.prototype.toString && !arg.toString.toString().includes('[native code]')) {
classes.push(arg.toString());
continue;
}
這里的處理是先判斷arg的toString方法是否被重寫,如果被重寫了,則直接將arg的toString方法的返回值push到classes數組中;
這一步可以說是很巧妙,第一個判斷是判斷arg的toString方法是否被重寫;
第二個判斷是判斷Object.prototype.toString方法是否被重寫,如果被重寫了,則arg的toString方法的返回值一定不會包含[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作為類名push到classes數組中;
最后函數結束,通過join將classes數組轉換為字符串,返回;
測試用例
在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是一種將Css和JavaScript結合在一起的方法,它允許你在JavaScript中使用Css,并且可以在運行時動態(tài)地生成Css。
這種方法的優(yōu)點是可以在JavaScript中使用Css的所有功能,包括變量、條件語句、循環(huán)等,而且可以在運行時動態(tài)地生成Css,這樣就可以根據不同的狀態(tài)來生成不同的Css,從而實現更加豐富的交互效果。
Css-in-JS的缺點是會增加JavaScript的體積,因為JavaScript中的Css是以字符串的形式存在的,所以會增加JavaScript的體積。
Css-in-JS的實現方式有很多種,比如styled-components、glamorous、glamor、aphrodite、radium等。
而這個庫就是一個將className可以動態(tài)生成的庫,在庫的README中有在React中使用的例子,其實完全可以拋開React,在任何需要的地方使用。
示例
例如我在普通的HTML中使用className,例如有一個按鈕,我想根據按鈕的狀態(tài)來動態(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>
總結
classnames是一個非常簡單的庫,但是它的功能卻非常強大,它可以根據不同的條件來動態(tài)地生成className,這樣就可以根據不同的狀態(tài)來動態(tài)地生成不同的className,從而實現更加豐富的交互效果。
除了React在使用Css-in-JS,還有很多庫都在使用Css-in-JS的方式來實現,這個庫代碼量雖然少,但是帶來的概念卻是非常重要的,所以值得學習。
其實拋開Css-in-JS的概念,這個庫的實現也很值得我們學習,例如對參數的處理,深層嵌套的數據結構的處理,已經測試用例的完善程度等等,都是值得我們學習的。
以上就是Css-In-Js實現classNames庫源碼解讀的詳細內容,更多關于Css-In-Js實現classNames庫的資料請關注腳本之家其它相關文章!
相關文章
JavaScript的模塊化開發(fā)框架Sea.js上手指南
Sea.js的目的是追求簡單的代碼書寫和組織方式,Sea.js并沒有過多功能而是主要對前端程序的部署結構作出約束,下面我們就來看一下JavaScript的模塊化開發(fā)框架Sea.js上手指南:2016-05-05

