ソースコードの読み取り: クラス名
導入
classnames
条件付きでクラス名を連結するためのシンプルな JavaScript ユーティリティ。
npm
パッケージ マネージャーを介してレジストリからダウンロードできますnpm
。
npm install classnames
classNames
関数は、任意の数の引数 (文字列またはオブジェクト) を受け入れます。パラメータは'foo'
{ foo: true } の短縮形です。特定のキーに関連付けられた値が false の場合、そのキーは出力に含まれません。
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'
// 数组将按照上述规则递归展平
const arr = ['b', {
c: true, d: false }];
classNames('a', arr); // => 'a b c'
// 相当于
classNames('a', 'b', {
c: true, d: false }); // => 'a b c'
let buttonType = 'primary';
classNames({
[`btn-${
buttonType}`]: true });
React で使用:
次のコードは、対話型機能を備えたボタン コンポーネントを実装します。ボタンのスタイル クラス名はボタンの状態に応じて動的に変更され、それによってボタンの押下とマウス ホバーのフィードバック効果が実現されます。
import React, {
useState } from 'react';
export default function Button (props) {
const [isPressed, setIsPressed] = useState(false);
const [isHovered, setIsHovered] = useState(false);
let btnClass = 'btn';
if (isPressed) btnClass += ' btn-pressed';
else if (isHovered) btnClass += ' btn-over';
return (
<button
className={
btnClass}
onMouseDown={
() => setIsPressed(true)}
onMouseUp={
() => setIsPressed(false)}
onMouseEnter={
() => setIsHovered(true)}
onMouseLeave={
() => setIsHovered(false)}
>
{
props.label}
</button>
);
}
そして、classnames
ライブラリを使用してボタンのクラス名を動的に生成します。
import React, {
useState } from 'react';
import classNames from 'classnames';
export default function Button (props) {
const [isPressed, setIsPressed] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const btnClass = classNames({
btn: true,
'btn-pressed': isPressed,
'btn-over': !isPressed && isHovered,
});
return (
<button
className={
btnClass}
onMouseDown={
() => setIsPressed(true)}
onMouseUp={
() => setIsPressed(false)}
onMouseEnter={
() => setIsHovered(true)}
onMouseLeave={
() => setIsHovered(false)}
>
{
props.label}
</button>
);
}
'btn: true'
: キーは btn で、ボタンにクラス名を含める必要があることを示しますbtn
。'btn-pressed': isPressed
:key is はbtn-pressed
、 is の場合isPressed
、true
ボタンにクラス名を含める必要があることを示しますbtn-pressed
。'btn-over': !isPressed && isHovered
: キーは です。およびの場合、ボタンにはクラス名が含まれる必要があることbtn-over
を示します。isPressed
false
isHovered
true
btn-over
オブジェクト、配列、文字列パラメータを混合できるため、className
prop
実際のパラメータのみが結果に含まれるため、オプションのプロパティのサポートも簡単になります。
const btnClass = classNames('btn', this.props.className, {
'btn-pressed': isPressed,
'btn-over': !isPressed && isHovered,
});
さらに、作成者は他に 2 つのバージョン ( dedupe
version とbind
version) を提供しています。
wherededupe
バージョンはクラスを正しく重複排除し、次のパラメーターで指定された偽のクラスが結果セットから確実に除外されるようにします。ただし、このバージョンは遅い (約 5 倍) ため、オプションのバージョンです。
const classNames = require('classnames/dedupe');
classNames('foo', 'foo', 'bar'); // => 'foo bar'
classNames('foo', {
foo: false, bar: true }); // => 'bar'
別のバージョンでは、スコープを維持しながらコンポーネントから CSS クラス名を動的に追加または削除するためにbind
組み合わせることができます。css-modules
css-modules
css-modules
プロジェクト内でローカル スコープの CSS を使用する方法です。各クラス名に一意のハッシュ値を追加することで、クラス名がアプリケーション全体で一意になるようにし、グローバル スコープのクラス名の競合を回避します。
const classNames = require('classnames/bind');
const styles = {
foo: 'abc',
bar: 'def',
baz: 'xyz',
};
const cx = classNames.bind(styles);
const className = cx('foo', ['bar'], {
baz: true }); // => 'abc def xyz'
バージョンのclassnames
組み合わせの使用例を次に示します。bind
css-modules
import {
useState } from 'react';
import classNames from 'classnames/bind';
import styles from './submit-button.css';
const cx = classNames.bind(styles);
export default function SubmitButton ({
store, form }) {
const [submissionInProgress, setSubmissionInProgress] = useState(store.submissionInProgress);
const [errorOccurred, setErrorOccurred] = useState(store.errorOccurred);
const [valid, setValid] = useState(form.valid);
const text = submissionInProgress ? 'Processing...' : 'Submit';
const className = cx({
base: true,
inProgress: submissionInProgress,
error: errorOccurred,
disabled: valid,
});
return <button className={
className}>{
text}</button>;
}
ソースコードの解釈
コードは比較的短いため、読者が自分で読めるようにコメントを追加してソース コードをここに直接掲載します。
索引
/*!
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];
// 如果参数为空或为false,则跳过
if (!arg) continue;
// 获取参数的类型
var argType = typeof arg;
// 如果参数是字符串或数字,则直接添加到类名数组中
if (argType === 'string' || argType === 'number') {
classes.push(arg);
} else if (Array.isArray(arg)) {
if (arg.length) {
// 如果参数是数组,则递归调用classnames函数,并将数组作为参数传入
var inner = classNames.apply(null, arg);
if (inner) {
// 如果递归调用的结果不为空,则将结果添加到类名数组中
classes.push(inner);
}
}
} else if (argType === 'object') {
// 判断 object 是否是一个自定义对象
// 因为原生的 JavaScript 对象(例如 Array、Object 等)的 toString 方法包含 [native code]
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(' ');
}
// 判断是否在CommonJS环境下,如果是,则将classNames赋值给module.exports
if (typeof module !== 'undefined' && module.exports) {
classNames.default = classNames;
module.exports = classNames;
} else if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {
// 如果在AMD环境下,则将classnames函数注册为模块,并将其命名为'classnames'
define('classnames', [], function () {
return classNames;
});
} else {
// 在浏览器环境下,将classnames函数挂载到全局的window对象上
window.classNames = classNames;
}
}());
重複排除
/*!
Copyright (c) 2018 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
/* global define */
(function () {
'use strict';
var classNames = (function () {
// 创建一个不继承自Object的空对象,以便后面可以跳过hasOwnProperty的检查
function StorageObject() {
}
StorageObject.prototype = Object.create(null);
// 解析数组,将数组中的每个元素解析为classNames
function _parseArray (resultSet, array) {
var length = array.length;
for (var i = 0; i < length; ++i) {
_parse(resultSet, array[i]);
}
}
var hasOwn = {
}.hasOwnProperty;
// 解析数字,将数字作为classNames的属性
function _parseNumber (resultSet, num) {
resultSet[num] = true;
}
// 解析对象,将对象的属性作为classNames的属性
function _parseObject (resultSet, object) {
// 判断 object 是否是一个自定义对象
// 因为原生的 JavaScript 对象(例如 Array、Object 等)的 toString 方法包含 [native code]
if (object.toString !== Object.prototype.toString && !object.toString.toString().includes('[native code]')) {
resultSet[object.toString()] = true;
return;
}
for (var k in object) {
if (hasOwn.call(object, k)) {
// set value to false instead of deleting it to avoid changing object structure
// https://www.smashingmagazine.com/2012/11/writing-fast-memory-efficient-javascript/#de-referencing-misconceptions
resultSet[k] = !!object[k];
}
}
}
var SPACE = /\s+/;
// 解析字符串,将字符串按照空格分割为数组,并将数组中的每个元素作为classNames的属性
function _parseString (resultSet, str) {
var array = str.split(SPACE);
var length = array.length;
for (var i = 0; i < length; ++i) {
resultSet[array[i]] = true;
}
}
// 解析参数,根据参数的类型调用相应的解析函数
function _parse (resultSet, arg) {
if (!arg) return;
var argType = typeof arg;
// 处理字符串类型的参数
// 'foo bar'
if (argType === 'string') {
_parseString(resultSet, arg);
// 处理数组类型的参数
// ['foo', 'bar', ...]
} else if (Array.isArray(arg)) {
_parseArray(resultSet, arg);
// 处理对象类型的参数
// { 'foo': true, ... }
} else if (argType === 'object') {
_parseObject(resultSet, arg);
// 处理数字类型的参数
// '130'
} else if (argType === 'number') {
_parseNumber(resultSet, arg);
}
}
// 主函数
function _classNames () {
// 避免arguments泄漏
var len = arguments.length;
var args = Array(len);
for (var i = 0; i < len; i++) {
args[i] = arguments[i];
}
// 创建一个存储classNames的对象
var classSet = new StorageObject();
// 解析参数并将结果存储在classSet对象中
_parseArray(classSet, args);
var list = [];
// 将classSet中值为true的属性加入到list数组中
for (var k in classSet) {
if (classSet[k]) {
list.push(k)
}
}
return list.join(' ');
}
return _classNames;
})();
// 判断是否在CommonJS环境下,如果是,则将classNames赋值给module.exports
if (typeof module !== 'undefined' && module.exports) {
classNames.default = classNames;
module.exports = classNames;
} else if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {
// 如果在AMD环境下,则将classnames函数注册为模块,并将其命名为'classnames'
define('classnames', [], function () {
return classNames;
});
} else {
// 在浏览器环境下,将classnames函数挂载到全局的window对象上
window.classNames = classNames;
}
}());
練る
/*!
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];
// 如果参数为空或为false,则跳过
if (!arg) continue;
var argType = typeof arg;
// 如果参数是字符串或数字,则直接添加到类名数组中
if (argType === 'string' || argType === 'number') {
classes.push(this && this[arg] || arg);
} else if (Array.isArray(arg)) {
// 如果参数是数组,则递归调用classnames函数,并将数组作为参数传入
classes.push(classNames.apply(this, arg));
} else if (argType === 'object') {
// 判断 object 是否是一个自定义对象
// 因为原生的 JavaScript 对象(例如 Array、Object 等)的 toString 方法包含 [native code]
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(this && this[key] || key);
}
}
}
}
return classes.join(' ');
}
// 判断是否在CommonJS环境下,如果是,则将classNames赋值给module.exports
if (typeof module !== 'undefined' && module.exports) {
classNames.default = classNames;
module.exports = classNames;
} else if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {
// 如果在AMD环境下,则将classnames函数注册为模块,并将其命名为'classnames'
define('classnames', [], function () {
return classNames;
});
} else {
// 在浏览器环境下,将classnames函数挂载到全局的window对象上
window.classNames = classNames;
}
}());
と比較してindex.js
、このバージョンではthis
コンテキスト処理が追加されています。
型宣言
// 以下类型声明主要用于定义一个名为 "classNames" 的命名空间和相关的类型。
// 在这个声明中,"classNames" 命名空间中定义了一些类型和接口
declare namespace classNames {
// "Value" 是一个联合类型,表示可以接受的值的类型,包括字符串、数字、布尔值、未定义和空值
type Value = string | number | boolean | undefined | null;
// "Mapping" 是一个类型别名,表示一个键值对的集合,其中键是字符串,值可以是任何类型
type Mapping = Record<string, unknown>;
// "ArgumentArray" 是一个接口,继承自数组类型 "Array<Argument>",表示一个参数数组,其中每个元素都是 "Argument" 类型
interface ArgumentArray extends Array<Argument> {
}
// "ReadonlyArgumentArray" 是一个接口,继承自只读数组类型 "ReadonlyArray<Argument>",表示一个只读的参数数组
interface ReadonlyArgumentArray extends ReadonlyArray<Argument> {
}
// "Argument" 是一个联合类型,表示可以作为参数的类型,可以是 "Value"、"Mapping"、"ArgumentArray" 或 "ReadonlyArgumentArray"
type Argument = Value | Mapping | ArgumentArray | ReadonlyArgumentArray;
}
// 定义了一个名为 "ClassNames" 的接口,它是一个函数类型,可以接受 "classNames.ArgumentArray" 类型的参数,并返回一个字符串
interface ClassNames {
(...args: classNames.ArgumentArray): string;
default: ClassNames;
}
declare const classNames: ClassNames;
// 通过 "export as namespace" 来将 "classNames" 声明为全局命名空间
export as namespace classNames;
// 使用 "export =" 来导出 "classNames",使其可以在其他模块中使用
export = classNames;
学んで得る
- 厳密モードを使用する
ソース コードの先頭で厳密モードを使用する主な理由は、コードの品質と信頼性を確保するためです。厳密モードは、開発者がいくつかの一般的なエラーや不規則な構文を回避するのに役立つと同時に、より厳格なエラー チェックとより明確なエラー プロンプトを提供します。厳密モードを使用すると、隠れた危険性が軽減され、コードの保守性と可読性が向上します。
さらに、厳密モードでは、宣言されていない変数の使用の禁止、読み取り専用属性の割り当ての禁止、変数の削除の禁止など、一部の潜在的に危険な動作も禁止できます。これにより、コードのセキュリティが向上し、潜在的な脆弱性やセキュリティ リスクが軽減されます。
したがって、コードの品質、信頼性、セキュリティを確保するために、多くの開発者はソース コードの先頭で厳密モードを使用することを選択します。これにより、コードがより厳格な仕様に準拠するようになり、エラーや潜在的な問題が減少し、コードの保守性と可読性が向上します。
- Objectを継承しない空のオブジェクトを作成する
Object.create(null)
新しいオブジェクトを作成するメソッドですが、オブジェクトにはプロトタイプ チェーンがありません。つまり、プロパティやメソッドを継承しません。これは、オブジェクトには組み込みのプロパティやメソッドがなく、直接代入によってのみ追加できることを意味します。を使用して作成されたオブジェクトObject.create(null)
は「純粋オブジェクト」または「ディクショナリ オブジェクト」と呼ばれ、継承のないキーと値のペアの純粋なコレクションを必要とするシナリオに適しています。この種のオブジェクトでは、キーと値は文字列だけでなく、任意のタイプのデータにすることができます。
コード内で を使用すると、を継承しない空のオブジェクトStorageObject.prototype = Object.create(null);
が作成されます。これにより、チェックがスキップされ、コードのパフォーマンスが向上します。Object
StorageObject
hasOwnProperty
-
文字列、配列、オブジェクト、数値などのさまざまなタイプの引数を解析します
-
デザインパターン
シングルトン パターンは、クラスのインスタンスが 1 つだけ存在することを保証し、そのインスタンスにアクセスするためのグローバル アクセス ポイントを提供する創造的な設計パターンです。
- シングルトン モード: 関数をすぐに実行してコードをラップし、
classNames
実行中の関数内にオブジェクトを作成して、それをグローバル変数に割り当てますwindow.classNames
。これにより、classNames
オブジェクトは 1 つだけ存在し、他の場所に新しいclassNames
オブジェクトを作成できなくなります。
ファクトリ パターンは、オブジェクトを作成するためのインターフェイスを提供する創造的なデザイン パターンですが、作成される特定のオブジェクト タイプは実行時に決定できます。ファクトリ パターンは、単純なファクトリ パターン、ファクトリ メソッド パターン、および抽象ファクトリ パターンに分類できます。
- シンプル ファクトリ パターン: 静的ファクトリ パターンとも呼ばれ、静的メソッドを直接使用してオブジェクトを作成します。
- ファクトリ メソッド パターン: 仮想ファクトリ パターンとも呼ばれ、ファクトリ インターフェイスを定義し、さまざまな特定のファクトリ実装によってさまざまなオブジェクトを作成します。
- ファクトリ パターン:ファクトリ関数を通じてオブジェクト
_classNames()
を作成しますclassNames
。ファクトリ関数は、さまざまな解析関数を呼び出して、さまざまなパラメータ タイプに従ってパラメータを解析し、結果をclassSet
オブジェクトに保存できます。
- 実行環境を決定してエクスポートする
classNames
CommonJS
さまざまな動作環境に応じて、環境、環境、またはブラウザ環境のいずれであるかを判断しますAMD
。CommonJS
環境内の場合はclassNames
に割り当てmodule.exports
、AMD
環境内の場合はclassNames
モジュールとして登録して に名前を付け'classnames'
、ブラウザ環境の場合はclassNames
グローバルwindow
オブジェクトにマウントします。
- TypeScript の型宣言
- 名前空間宣言:
declare namespace
名前空間を定義し、関連する型とインターフェイスをまとめて整理し、名前の競合を防ぎ、モジュール構造を提供するために使用します。 - 型の別名と共用体型:
type
キーワードを使用して型の別名を定義し、複合型の再利用を容易にします。共用体型を使用すると、値が複数の異なる型のいずれかであることを表すことができます。 - インターフェイスと継承:
interface
キーワードを使用して、オブジェクトの構造を表すインターフェイスを定義します。インターフェイスは他のインターフェイスから継承でき、既存のインターフェイス定義は継承を通じて再利用できます。 - 関数タイプ: インターフェイスを使用して関数タイプを定義し、関数のパラメータのタイプと戻り値のタイプを指定できます。
- タイプのエクスポートとモジュールのインポート:
export
キーワードを使用してタイプまたは値をエクスポートし、他のモジュールで使用できるようにします。エクスポートされた型または値は、キーワードを使用してimport
他のモジュールにインポートできます。