前端 React 基础 UI 组件源码级深度剖析(一)

227 阅读37分钟

前端 React 基础 UI 组件源码级深度剖析

本人掘金号,欢迎点击关注:掘金号地址

本人公众号,欢迎点击关注:公众号地址

一、引言

在前端开发领域,React 作为一个强大的 JavaScript 库,已经成为构建用户界面的首选框架之一。它的核心优势在于组件化开发,允许开发者将复杂的界面拆分成多个小的、可复用的组件。基础 UI 组件是构建 React 应用的基石,它们包括按钮、输入框、下拉菜单等常见元素。深入理解这些基础 UI 组件的源码实现原理,对于开发者提升技术水平、优化组件性能以及进行定制开发都具有重要意义。本文将从源码级别对 React 基础 UI 组件进行全面分析。

二、按钮组件(Button)

2.1 按钮组件的基本实现

按钮是最常见的 UI 组件之一,用于触发各种操作。以下是一个简单的 React 按钮组件的实现:

jsx

// 导入 React 库,用于创建 React 组件
import React from 'react';

// 定义 Button 组件,它是一个函数式组件
const Button = (props) => {
    // 从 props 中解构出需要的属性
    const { 
        children, // 按钮内显示的内容,通常是文本
        onClick,  // 按钮点击事件的回调函数
        className // 用于自定义按钮样式的类名
    } = props;

    // 返回一个 button 元素,将传入的属性和事件绑定到该元素上
    return (
        <button 
            onClick={onClick} // 绑定点击事件处理函数
            className={className} // 应用自定义类名
        >
            {children} // 渲染按钮内的内容
        </button>
    );
};

// 导出 Button 组件,以便在其他文件中使用
export default Button;

在这个简单的按钮组件中,我们接收了三个属性:children 表示按钮内部的文本或其他元素,onClick 是按钮被点击时触发的回调函数,className 用于自定义按钮的样式。通过这种方式,我们可以创建一个基本的可复用按钮组件。

2.2 按钮组件的样式定制

为了让按钮组件更加美观和符合设计要求,我们通常需要对其样式进行定制。以下是一个使用 CSS 模块为按钮添加样式的示例:

jsx

// 导入 React 库
import React from 'react';
// 导入 CSS 模块,用于管理按钮的样式
import styles from './Button.module.css';

// 定义 Button 组件
const Button = (props) => {
    const { 
        children, 
        onClick, 
        className 
    } = props;

    // 合并 CSS 模块中的类名和自定义类名
    const combinedClassName = `${styles.button} ${className}`;

    return (
        <button 
            onClick={onClick} 
            className={combinedClassName}
        >
            {children}
        </button>
    );
};

export default Button;

css

/* Button.module.css */
.button {
    /* 设置按钮的背景颜色 */
    background-color: #007bff; 
    /* 设置按钮的文本颜色 */
    color: white; 
    /* 设置按钮的内边距 */
    padding: 10px 20px; 
    /* 设置按钮的边框 */
    border: none; 
    /* 设置按钮的圆角 */
    border-radius: 4px; 
    /* 设置鼠标悬停时的光标样式 */
    cursor: pointer; 
}

/* 定义按钮悬停时的样式 */
.button:hover {
    background-color: #0056b3;
}

在这个示例中,我们使用了 CSS 模块来管理按钮的样式。通过 styles.button 获取 CSS 模块中的类名,并将其与自定义的 className 合并,这样可以在不影响全局样式的情况下,为按钮添加自定义样式。

2.3 按钮组件的状态管理

按钮组件可能会有不同的状态,例如加载状态、禁用状态等。以下是一个支持加载状态的按钮组件的实现:

jsx

import React, { useState } from 'react';
import styles from './Button.module.css';

const Button = (props) => {
    const { 
        children, 
        onClick, 
        className 
    } = props;
    // 使用 useState 钩子来管理按钮的加载状态
    const [isLoading, setIsLoading] = useState(false);

    const handleClick = () => {
        // 设置加载状态为 true
        setIsLoading(true);
        // 调用传入的点击事件处理函数
        onClick();
        // 模拟加载完成,这里可以根据实际情况修改
        setTimeout(() => {
            setIsLoading(false);
        }, 2000);
    };

    const combinedClassName = `${styles.button} ${className}`;

    return (
        <button 
            onClick={handleClick} 
            className={combinedClassName}
            // 禁用按钮在加载状态下的点击事件
            disabled={isLoading} 
        >
            {isLoading ? 'Loading...' : children}
        </button>
    );
};

export default Button;

在这个示例中,我们使用 useState 钩子来管理按钮的加载状态。当按钮被点击时,将加载状态设置为 true,并显示 “Loading...” 文本。同时,禁用按钮的点击事件,防止用户重复点击。在模拟的加载完成后,将加载状态设置为 false,恢复按钮的正常状态。

2.4 按钮组件的不同类型实现

除了普通按钮,我们还可能需要实现不同类型的按钮,如提交按钮、链接按钮等。以下是一个支持不同类型的按钮组件的实现:

jsx

import React from 'react';
import styles from './Button.module.css';

const Button = (props) => {
    const { 
        children, 
        onClick, 
        className,
        type = 'button' // 按钮类型,默认为普通按钮
    } = props;

    const combinedClassName = `${styles.button} ${className}`;

    return (
        <button 
            type={type} // 设置按钮类型
            onClick={onClick} 
            className={combinedClassName}
        >
            {children}
        </button>
    );
};

export default Button;

在这个示例中,我们添加了一个 type 属性,用于指定按钮的类型。可以传入 'button''submit' 或 'reset' 等值,以实现不同类型的按钮功能。

2.5 按钮组件的尺寸和颜色定制

为了满足不同的设计需求,按钮组件通常需要支持尺寸和颜色的定制。以下是一个支持尺寸和颜色定制的按钮组件的实现:

jsx

import React from 'react';
import styles from './Button.module.css';

const Button = (props) => {
    const { 
        children, 
        onClick, 
        className,
        size = 'medium', // 按钮尺寸,默认为中等
        color = 'primary' // 按钮颜色,默认为主要颜色
    } = props;

    // 根据尺寸和颜色生成对应的类名
    const sizeClass = `button-${size}`;
    const colorClass = `button-${color}`;

    const combinedClassName = `${styles.button} ${sizeClass} ${colorClass} ${className}`;

    return (
        <button 
            onClick={onClick} 
            className={combinedClassName}
        >
            {children}
        </button>
    );
};

export default Button;

css

/* Button.module.css */
.button {
    background-color: #007bff;
    color: white;
    padding: 10px 20px;
    border: none;
    border-radius: 4px;
    cursor: pointer;
}

.button-small {
    padding: 5px 10px;
    font-size: 12px;
}

.button-large {
    padding: 15px 30px;
    font-size: 16px;
}

.button-primary {
    background-color: #007bff;
}

.button-secondary {
    background-color: #6c757d;
}

.button-danger {
    background-color: #dc3545;
}

在这个示例中,我们添加了 size 和 color 属性,用于定制按钮的尺寸和颜色。通过生成对应的类名,并将其应用到按钮上,实现了按钮的个性化定制。

三、输入框组件(Input)

3.1 输入框组件的基本实现

输入框是用于接收用户输入的重要组件。以下是一个简单的 React 输入框组件的实现:

jsx

import React from 'react';

const Input = (props) => {
    const { 
        type = 'text', // 输入框的类型,默认为文本输入框
        placeholder,  // 输入框的占位符文本
        value,        // 输入框的值
        onChange,     // 输入框内容变化时的回调函数
        className     // 自定义的类名
    } = props;

    return (
        <input 
            // 设置输入框的类型
            type={type} 
            // 设置输入框的占位符文本
            placeholder={placeholder} 
            // 设置输入框的值
            value={value} 
            // 绑定输入框内容变化的回调函数
            onChange={onChange} 
            // 应用自定义类名
            className={className} 
        />
    );
};

export default Input;

在这个输入框组件中,我们接收了几个常见的属性:type 表示输入框的类型,如 textpassword 等;placeholder 是输入框的占位符文本;value 是输入框当前的值;onChange 是输入框内容变化时触发的回调函数;className 用于自定义输入框的样式。

3.2 输入框组件的受控与非受控状态

在 React 中,输入框可以分为受控组件和非受控组件。受控组件的 value 属性由 React 组件的状态管理,而非受控组件的 value 属性由 DOM 本身管理。以下是一个受控输入框组件的示例:

jsx

import React, { useState } from 'react';
import Input from './Input';

const ControlledInputExample = () => {
    // 使用 useState 钩子来管理输入框的值
    const [inputValue, setInputValue] = useState('');

    const handleInputChange = (event) => {
        // 更新输入框的值
        setInputValue(event.target.value);
    };

    return (
        <div>
            <Input 
                value={inputValue} 
                onChange={handleInputChange} 
                placeholder="Enter some text"
            />
            <p>You entered: {inputValue}</p>
        </div>
    );
};

export default ControlledInputExample;

在这个示例中,我们使用 useState 钩子来管理输入框的值。当输入框的内容发生变化时,调用 handleInputChange 函数更新 inputValue 状态。这样,输入框的值始终由 React 组件的状态控制。

3.3 输入框组件的验证功能

输入框组件通常需要对用户输入进行验证。以下是一个支持简单验证功能的输入框组件的实现:

jsx

import React, { useState } from 'react';
import Input from './Input';

const ValidatedInput = (props) => {
    const { 
        type = 'text', 
        placeholder, 
        value, 
        onChange, 
        className, 
        validationRegex, // 验证规则的正则表达式
        errorMessage // 验证失败时显示的错误信息
    } = props;

    const [isValid, setIsValid] = useState(true);

    const handleInputChange = (event) => {
        const inputValue = event.target.value;
        // 调用传入的 onChange 函数
        onChange(event);
        // 验证输入值是否符合正则表达式
        if (validationRegex) {
            setIsValid(validationRegex.test(inputValue));
        }
    };

    return (
        <div>
            <Input 
                type={type}
                placeholder={placeholder}
                value={value}
                onChange={handleInputChange}
                className={className}
            />
            {!isValid && <p className="error">{errorMessage}</p>}
        </div>
    );
};

export default ValidatedInput;

在这个示例中,我们添加了 validationRegex 和 errorMessage 属性,用于定义验证规则和验证失败时显示的错误信息。当输入框内容变化时,调用 validationRegex.test(inputValue) 方法进行验证,并更新 isValid 状态。如果验证失败,显示错误信息。

3.4 输入框组件的前缀和后缀

有些场景下,输入框可能需要添加前缀或后缀,如货币符号、单位等。以下是一个支持前缀和后缀的输入框组件的实现:

jsx

import React from 'react';
import Input from './Input';

const InputWithPrefixSuffix = (props) => {
    const { 
        type = 'text', 
        placeholder, 
        value, 
        onChange, 
        className,
        prefix, // 输入框的前缀
        suffix  // 输入框的后缀
    } = props;

    return (
        <div className="input-wrapper">
            {prefix && <span className="input-prefix">{prefix}</span>}
            <Input 
                type={type}
                placeholder={placeholder}
                value={value}
                onChange={onChange}
                className={className}
            />
            {suffix && <span className="input-suffix">{suffix}</span>}
        </div>
    );
};

export default InputWithPrefixSuffix;

css

/* InputWithPrefixSuffix.module.css */
.input-wrapper {
    display: flex;
    align-items: center;
}

.input-prefix,
.input-suffix {
    padding: 0 5px;
    color: #666;
}

在这个示例中,我们添加了 prefix 和 suffix 属性,用于指定输入框的前缀和后缀。通过 flex 布局将前缀、输入框和后缀组合在一起,实现了带有前缀和后缀的输入框效果。

3.5 输入框组件的自动完成功能

自动完成功能可以提高用户输入效率。以下是一个简单的支持自动完成功能的输入框组件的实现:

jsx

import React, { useState } from 'react';
import Input from './Input';

const AutocompleteInput = (props) => {
    const { 
        type = 'text', 
        placeholder, 
        value, 
        onChange, 
        className,
        options // 自动完成的选项列表
    } = props;

    const [showOptions, setShowOptions] = useState(false);
    const [filteredOptions, setFilteredOptions] = useState(options);

    const handleInputChange = (event) => {
        const inputValue = event.target.value;
        onChange(event);
        // 根据输入值过滤选项列表
        const filtered = options.filter(option => option.toLowerCase().includes(inputValue.toLowerCase()));
        setFilteredOptions(filtered);
        setShowOptions(inputValue.length > 0);
    };

    const handleOptionSelect = (option) => {
        onChange({ target: { value: option } });
        setShowOptions(false);
    };

    return (
        <div className="autocomplete-wrapper">
            <Input 
                type={type}
                placeholder={placeholder}
                value={value}
                onChange={handleInputChange}
                className={className}
            />
            {showOptions && (
                <ul className="autocomplete-options">
                    {filteredOptions.map(option => (
                        <li key={option} onClick={() => handleOptionSelect(option)}>
                            {option}
                        </li>
                    ))}
                </ul>
            )}
        </div>
    );
};

export default AutocompleteInput;

css

/* AutocompleteInput.module.css */
.autocomplete-wrapper {
    position: relative;
}

.autocomplete-options {
    position: absolute;
    top: 100%;
    left: 0;
    width: 100%;
    list-style: none;
    padding: 0;
    margin: 0;
    border: 1px solid #ccc;
    background-color: white;
    z-index: 1;
}

.autocomplete-options li {
    padding: 5px 10px;
    cursor: pointer;
}

.autocomplete-options li:hover {
    background-color: #f0f0f0;
}

在这个示例中,我们添加了 options 属性,用于指定自动完成的选项列表。当输入框内容变化时,根据输入值过滤选项列表,并显示匹配的选项。用户点击选项时,将选项值设置为输入框的值。

四、下拉菜单组件(Select)

4.1 下拉菜单组件的基本实现

下拉菜单组件用于让用户从多个选项中选择一个。以下是一个简单的 React 下拉菜单组件的实现:

jsx

import React from 'react';

const Select = (props) => {
    const { 
        options, // 下拉菜单的选项列表
        value,   // 当前选中的值
        onChange, // 选项改变时的回调函数
        className // 自定义的类名
    } = props;

    return (
        <select 
            value={value} 
            onChange={onChange} 
            className={className}
        >
            {options.map(option => (
                <option key={option.value} value={option.value}>
                    {option.label}
                </option>
            ))}
        </select>
    );
};

export default Select;

在这个下拉菜单组件中,我们接收了三个主要属性:options 是一个包含选项信息的数组,每个选项对象包含 value 和 label 属性;value 是当前选中的值;onChange 是选项改变时触发的回调函数;className 用于自定义下拉菜单的样式。

4.2 下拉菜单组件的分组功能

有时候,下拉菜单的选项需要进行分组显示。以下是一个支持分组功能的下拉菜单组件的实现:

jsx

import React from 'react';

const GroupedSelect = (props) => {
    const { 
        groups, // 分组后的选项列表
        value, 
        onChange, 
        className 
    } = props;

    return (
        <select 
            value={value} 
            onChange={onChange} 
            className={className}
        >
            {groups.map(group => (
                <optgroup key={group.label} label={group.label}>
                    {group.options.map(option => (
                        <option key={option.value} value={option.value}>
                            {option.label}
                        </option>
                    ))}
                </optgroup>
            ))}
        </select>
    );
};

export default GroupedSelect;

在这个示例中,我们将选项按照分组进行组织,groups 属性是一个包含多个分组对象的数组,每个分组对象包含 label 和 options 属性。使用 <optgroup> 标签将选项进行分组显示。

4.3 下拉菜单组件的搜索功能

当选项较多时,为下拉菜单添加搜索功能可以提高用户体验。以下是一个支持搜索功能的下拉菜单组件的实现:

jsx

import React, { useState } from 'react';
import Input from './Input';

const SearchableSelect = (props) => {
    const { 
        options, 
        value, 
        onChange, 
        className 
    } = props;

    const [searchTerm, setSearchTerm] = useState('');
    const [showOptions, setShowOptions] = useState(false);

    const filteredOptions = options.filter(option => 
        option.label.toLowerCase().includes(searchTerm.toLowerCase())
    );

    const handleSearchChange = (event) => {
        setSearchTerm(event.target.value);
    };

    const handleOptionSelect = (option) => {
        onChange({ target: { value: option.value } });
        setShowOptions(false);
        setSearchTerm('');
    };

    return (
        <div className="searchable-select-wrapper">
            <Input 
                type="text"
                placeholder="Search..."
                value={searchTerm}
                onChange={handleSearchChange}
                onClick={() => setShowOptions(true)}
                className={className}
            />
            {showOptions && (
                <ul className="searchable-select-options">
                    {filteredOptions.map(option => (
                        <li key={option.value} onClick={() => handleOptionSelect(option)}>
                            {option.label}
                        </li>
                    ))}
                </ul>
            )}
        </div>
    );
};

export default SearchableSelect;

css

/* SearchableSelect.module.css */
.searchable-select-wrapper {
    position: relative;
}

.searchable-select-options {
    position: absolute;
    top: 100%;
    left: 0;
    width: 100%;
    list-style: none;
    padding: 0;
    margin: 0;
    border: 1px solid #ccc;
    background-color: white;
    z-index: 1;
}

.searchable-select-options li {
    padding: 5px 10px;
    cursor: pointer;
}

.searchable-select-options li:hover {
    background-color: #f0f0f0;
}

在这个示例中,我们添加了一个输入框用于搜索选项。当输入框内容变化时,根据输入值过滤选项列表,并显示匹配的选项。用户点击选项时,将选项值设置为下拉菜单的选中值。

4.4 下拉菜单组件的多选功能

除了单选下拉菜单,有时候我们还需要实现多选功能。以下是一个支持多选功能的下拉菜单组件的实现:

jsx

import React, { useState } from 'react';

const MultiSelect = (props) => {
    const { 
        options, 
        value = [], // 默认选中的值为空数组
        onChange, 
        className 
    } = props;

    const [selectedOptions, setSelectedOptions] = useState(value);

    const handleOptionChange = (option) => {
        const isSelected = selectedOptions.includes(option.value);
        let newSelectedOptions;
        if (isSelected) {
            newSelectedOptions = selectedOptions.filter(selected => selected!== option.value);
        } else {
            newSelectedOptions = [...selectedOptions, option.value];
        }
        setSelectedOptions(newSelectedOptions);
        onChange(newSelectedOptions);
    };

    return (
        <div className="multi-select-wrapper">
            {options.map(option => (
                <label key={option.value} className="multi-select-option">
                    <input 
                        type="checkbox"
                        checked={selectedOptions.includes(option.value)}
                        onChange={() => handleOptionChange(option)}
                    />
                    {option.label}
                </label>
            ))}
        </div>
    );
};

export default MultiSelect;

css

/* MultiSelect.module.css */
.multi-select-wrapper {
    display: flex;
    flex-direction: column;
}

.multi-select-option {
    display: flex;
    align-items: center;
    margin-bottom: 5px;
}

.multi-select-option input {
    margin-right: 5px;
}

在这个示例中,我们使用 useState 钩子来管理选中的选项。每个选项对应一个复选框,当复选框状态改变时,更新选中的选项列表,并调用 onChange 回调函数通知父组件。

4.5 下拉菜单组件的动态加载选项

在某些情况下,下拉菜单的选项可能需要动态加载,例如根据用户的输入或其他条件进行查询。以下是一个支持动态加载选项的下拉菜单组件的实现:

jsx

import React, { useState, useEffect } from 'react';
import Input from './Input';

const DynamicSelect = (props) => {
    const { 
        loadOptions, // 加载选项的函数
        value, 
        onChange, 
        className 
    } = props;

    const [options, setOptions] = useState([]);
    const [searchTerm, setSearchTerm] = useState('');
    const [showOptions, setShowOptions] = useState(false);

    useEffect(() => {
        const fetchOptions = async () => {
            const newOptions = await loadOptions(searchTerm);
            setOptions(newOptions);
        };
        fetchOptions();
    }, [searchTerm, loadOptions]);

    const handleSearchChange = (event) => {
        setSearchTerm(event.target.value);
    };

    const handleOptionSelect = (option) => {
        onChange(option.value);
        setShowOptions(false);
        setSearchTerm('');
    };

    return (
        <div className="dynamic-select-wrapper">
            <Input 
                type="text"
                placeholder="Search..."
                value={searchTerm}
                onChange={handleSearchChange}
                onClick={() => setShowOptions(true)}
                className={className}
            />
            {showOptions && (
                <ul className="dynamic-select-options">
                    {options.map(option => (
                        <li key={option.value} onClick={() => handleOptionSelect(option)}>
                            {option.label}
                        </li>
                    ))}
                </ul>
            )}
        </div>
    );
};

export default DynamicSelect;

在这个示例中,我们添加了一个 loadOptions 属性,它是一个异步函数,用于根据搜索关键词加载选项。使用 useEffect 钩子在搜索关键词变化时调用 loadOptions 函数,更新选项列表。

五、复选框组件(Checkbox)

5.1 复选框组件的基本实现

复选框用于让用户选择一个或多个选项。以下是一个简单的 React 复选框组件的实现:

jsx

import React from 'react';

const Checkbox = (props) => {
    const { 
        label, // 复选框的标签文本
        checked, // 复选框的选中状态
        onChange, // 复选框状态改变时的回调函数
        className // 自定义的类名
    } = props;

    return (
        <label className={className}>
            <input 
                type="checkbox"
                checked={checked}
                onChange={onChange}
            />
            {label}
        </label>
    );
};

export default Checkbox;

在这个复选框组件中,我们接收了四个属性:label 是复选框旁边的标签文本;checked 表示复选框的选中状态;onChange 是复选框状态改变时触发的回调函数;className 用于自定义复选框的样式。

5.2 复选框组件的组管理

当有多个复选框需要进行组管理时,例如全选、反选等操作。以下是一个支持组管理的复选框组件的实现:

jsx

import React, { useState } from 'react';
import Checkbox from './Checkbox';

const CheckboxGroup = (props) => {
    const { 
        options, // 复选框的选项列表
        value = [], // 默认选中的值
        onChange // 选项改变时的回调函数
    } = props;

    const [selectedOptions, setSelectedOptions] = useState(value);

    const handleCheckboxChange = (option) => {
        const isSelected = selectedOptions.includes(option.value);
        let newSelectedOptions;
        if (isSelected) {
            newSelectedOptions = selectedOptions.filter(selected => selected!== option.value);
        } else {
            newSelectedOptions = [...selectedOptions, option.value];
        }
        setSelectedOptions(newSelectedOptions);
        onChange(newSelectedOptions);
    };

    const handleSelectAll = () => {
        const allOptions = options.map(option => option.value);
        setSelectedOptions(allOptions);
        onChange(allOptions);
    };

    const handleDeselectAll = () => {
        setSelectedOptions([]);
        onChange([]);
    };

    return (
        <div>
            <button onClick={handleSelectAll}>Select All</button>
            <button onClick={handleDeselectAll}>Deselect All</button>
            {options.map(option => (
                <Checkbox 
                    key={option.value}
                    label={option.label}
                    checked={selectedOptions.includes(option.value)}
                    onChange={() => handleCheckboxChange(option)}
                />
            ))}
        </div>
    );
};

export default CheckboxGroup;

在这个示例中,我们使用 useState 钩子来管理选中的选项。提供了 “Select All” 和 “Deselect All” 按钮,分别用于全选和反选所有复选框。

5.3 复选框组件的禁用状态

有时候,我们需要将复选框设置为禁用状态,禁止用户操作。以下是一个支持禁用状态的复选框组件的实现:

jsx

import React from 'react';

const DisabledCheckbox = (props) => {
    const { 
        label, 
        checked, 
        onChange, 
        className,
        disabled = false // 是否禁用,默认为 false
    } = props;

    return (
        <label className={className}>
            <input 
                type="checkbox"
                checked={checked}
                onChange={onChange}
                disabled={disabled}
            />
            {label}
        </label>
    );
};

export default DisabledCheckbox;

在这个示例中,我们添加了一个 disabled 属性,用于控制复选框是否禁用。当 disabled 为 true 时,复选框将变为不可用状态。

5.4 复选框组件的样式定制

为了满足不同的设计需求,我们可以对复选框的样式进行定制。以下是一个使用 CSS 模块定制复选框样式的示例:

jsx

import React from 'react';
import styles from './Checkbox.module.css';

const StyledCheckbox = (props) => {
    const { 
        label, 
        checked, 
        onChange 
    } = props;

    return (
        <label className={styles.checkbox}>
            <input 
                type="checkbox"
                checked={checked}
                onChange={onChange}
            />
            <span className={styles.checkmark}></span>
            {label}
        </label>
    );
};

export default StyledCheckbox;

css

/* Checkbox.module.css */
.checkbox {
    display: block;
    position: relative;
    padding-left: 35px;
    margin-bottom: 12px;
    cursor: pointer;
    font-size: 22px;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
}

.checkbox input {
    position: absolute;
    opacity: 0;
    cursor: pointer;
    height: 0;
    width: 0;
}

.checkmark {
    position: absolute;
    top: 0;
    left: 0;
    height: 25px;
    width: 25px;
    background-color: #eee;
}

.checkbox:hover input ~.checkmark {
    background-color: #ccc;
}

.checkbox input:checked ~.checkmark {
    background-color: #2196F3;
}

.checkmark:after {
    content: "";
    position: absolute;
    display: none;
}

.checkbox input:checked ~.checkmark:after {
    display: block;
}

.checkbox.checkmark:after {
    left: 9px;
    top: 5px;
    width: 5px;
    height: 10px;
    border: solid white;
    border-width: 0 3px 3px 0;
    -webkit-transform: rotate(45deg);
    -ms-transform: rotate(45deg);
    transform: rotate(45deg);
}

在这个示例中,我们使用 CSS 模块创建了一个自定义样式的复选框。通过隐藏原生的复选框,使用一个自定义的 span 元素作为复选框的外观,并通过 CSS 伪类和选择器来实现选中状态的样式变化。

5.5 复选框组件的半选状态

在某些场景下,复选框可能需要显示半选状态,例如在多级选择中。以下是一个支持半选状态的复选框组件的实现:

jsx

import React from 'react';

const IndeterminateCheckbox = (props) => {
    const { 
        label, 
        checked, 
        onChange, 
        indeterminate = false // 是否为半选状态,默认为 false
    } = props;

    const checkboxRef = React.createRef();

    React.useEffect(() => {
        if (checkboxRef.current) {
            checkboxRef.current.indeterminate = indeterminate;
        }
    }, [indeterminate]);

    return (
        <label>
            <input 
                type="checkbox"
                ref={checkboxRef}
                checked={checked}
                onChange={onChange}
            />
            {label}
        </label>
    );
};

export default IndeterminateCheckbox;

在这个示例中,我们添加了一个 indeterminate 属性,用于控制复选框是否显示半选状态。使用 React.createRef() 创建一个引用,在 useEffect 钩子中根据 indeterminate 的值设置复选框的 indeterminate 属性。

六、单选框组件(Radio)

6.1 单选框组件的基本实现

单选框用于让用户从多个选项中选择一个。以下是一个简单的 React 单选框组件的实现:

jsx

import React from 'react';

const Radio = (props) => {
    const { 
        label, // 单选框的标签文本
        value, // 单选框的值
        checked, // 单选框的选中状态
        onChange, // 单选框状态改变时的回调函数
        className // 自定义的类名
    } = props;

    return (
        <label className={className}>
            <input 
                type="radio"
                value={value}
                checked={checked}
                onChange={onChange}
            />
            {label}
        </label>
    );
};

export default Radio;

在这个单选框组件中,我们接收了五个属性:label 是单选框旁边的标签文本;value 是单选框的值;checked 表示单选框的选中状态;onChange 是单选框状态改变时触发的回调函数;className 用于自定义单选框的样式。

6.2 单选框组件的组管理

与复选框类似,单选框通常也需要进行组管理,确保同一组内只有一个单选框可以被选中。以下是一个支持组管理的单选框组件的实现:

jsx

import React, { useState } from 'react';
import Radio from './Radio';

const RadioGroup = (props) => {
    const { 
        options, // 单选框的选项列表

6.2 单选框组件的组管理

与复选框类似,单选框通常也需要进行组管理,确保同一组内只有一个单选框可以被选中。以下是一个支持组管理的单选框组件的实现:

jsx

import React, { useState } from 'react';
import Radio from './Radio';

const RadioGroup = (props) => {
    const { 
        options, // 单选框的选项列表,每个选项是包含value和label的对象
        value,   // 当前选中的值
        onChange // 选项改变时的回调函数
    } = props;

    // 使用useState钩子管理当前选中的值,初始值为传入的value
    const [selectedValue, setSelectedValue] = useState(value); 

    // 单选框状态改变时的处理函数
    const handleRadioChange = (optionValue) => { 
        setSelectedValue(optionValue);
        // 调用父组件传入的onChange回调,通知选中值变化
        onChange(optionValue); 
    };

    return (
        <div>
            {options.map(option => (
                // 为每个选项渲染一个Radio组件
                <Radio 
                    key={option.value}
                    label={option.label}
                    value={option.value}
                    // 判断当前选项是否被选中
                    checked={selectedValue === option.value} 
                    // 绑定状态改变处理函数
                    onChange={() => handleRadioChange(option.value)} 
                />
            ))}
        </div>
    );
};

export default RadioGroup;

在上述代码中,RadioGroup 组件通过 useState 管理组内当前选中的单选框值。当某个单选框状态改变时,更新 selectedValue 并触发父组件的 onChange 回调。每个 Radio 组件根据 selectedValue 判断自身是否处于选中状态。

6.3 单选框组件的禁用状态

和其他组件一样,单选框也可能需要设置为禁用状态,禁止用户进行选择操作。实现如下:

jsx

import React from 'react';

const DisabledRadio = (props) => {
    const { 
        label, 
        value, 
        checked, 
        onChange, 
        className,
        disabled = false // 是否禁用,默认值为false
    } = props;

    return (
        <label className={className}>
            <input 
                type="radio"
                value={value}
                checked={checked}
                onChange={onChange}
                // 根据disabled属性设置是否禁用
                disabled={disabled} 
            />
            {label}
        </label>
    );
};

export default DisabledRadio;

在这个 DisabledRadio 组件中,通过 disabled 属性控制单选框的禁用状态。当 disabled 为 true 时,input 元素的 disabled 属性被设置,单选框呈现不可操作状态。

6.4 单选框组件的样式定制

为了符合不同的设计风格,单选框的样式也需要进行定制。这里通过 CSS 模块实现自定义样式:

jsx

import React from 'react';
import styles from './Radio.module.css';

const StyledRadio = (props) => {
    const { 
        label, 
        value, 
        checked, 
        onChange 
    } = props;

    return (
        <label className={styles.radio}>
            <input 
                type="radio"
                value={value}
                checked={checked}
                onChange={onChange}
            />
            <span className={styles.radioCheckmark}></span>
            {label}
        </label>
    );
};

export default StyledRadio;

css

/* Radio.module.css */
.radio {
    display: block;
    position: relative;
    padding-left: 35px;
    margin-bottom: 12px;
    cursor: pointer;
    font-size: 22px;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
}

.radio input {
    position: absolute;
    opacity: 0;
    cursor: pointer;
    height: 0;
    width: 0;
}

.radioCheckmark {
    position: absolute;
    top: 0;
    left: 0;
    height: 25px;
    width: 25px;
    background-color: #eee;
    border-radius: 50%;
}

.radio:hover input ~.radioCheckmark {
    background-color: #ccc;
}

.radio input:checked ~.radioCheckmark {
    background-color: #2196F3;
}

.radioCheckmark:after {
    content: "";
    position: absolute;
    display: none;
}

.radio input:checked ~.radioCheckmark:after {
    display: block;
}

.radio.radioCheckmark:after {
    top: 9px;
    left: 9px;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: white;
}

上述代码中,通过隐藏原生单选框,使用自定义的 span 元素作为单选框外观。利用 CSS 伪类和选择器,实现单选框未选中、悬停和选中状态下的不同样式展示。

6.5 单选框组件与表单结合

在实际应用中,单选框通常会作为表单的一部分。以下是一个包含单选框组的表单示例:

jsx

import React, { useState } from 'react';
import RadioGroup from './RadioGroup';

const RadioForm = () => {
    const options = [
        { value: 'option1', label: '选项一' },
        { value: 'option2', label: '选项二' },
        { value: 'option3', label: '选项三' }
    ];

    // 管理表单中单选框组的选中值
    const [selectedOption, setSelectedOption] = useState(''); 

    const handleSubmit = (e) => {
        e.preventDefault();
        // 表单提交时,打印选中的选项值
        console.log('Selected option:', selectedOption); 
    };

    return (
        <form onSubmit={handleSubmit}>
            <RadioGroup 
                options={options}
                value={selectedOption}
                onChange={(value) => setSelectedOption(value)}
            />
            <button type="submit">提交</button>
        </form>
    );
};

export default RadioForm;

在 RadioForm 组件中,通过 useState 管理单选框组的选中值。当用户提交表单时,handleSubmit 函数会被触发,此时可以获取并处理选中的选项值,比如发送到后端服务器。

七、标签组件(Label)

7.1 标签组件的基本实现

标签组件主要用于为其他 UI 元素(如输入框、单选框、复选框等)提供描述性文本。其基本实现如下:

jsx

import React from 'react';

const Label = (props) => {
    const { 
        children, // 标签内的文本内容
        htmlFor,  // 与关联表单元素的id属性对应
        className // 自定义的类名
    } = props;

    return (
        <label 
            htmlFor={htmlFor} // 设置关联表单元素的id
            className={className}
        >
            {children}
        </label>
    );
};

export default Label;

在这个 Label 组件中,htmlFor 属性用于关联对应的表单元素,当用户点击标签文本时,会将焦点切换到关联的表单元素上。children 表示标签内显示的文本内容,className 用于自定义样式。

7.2 标签组件的样式定制

为了使标签文本更突出或符合设计要求,需要对标签组件进行样式定制。使用 CSS 模块实现如下:

jsx

import React from 'react';
import styles from './Label.module.css';

const StyledLabel = (props) => {
    const { 
        children, 
        htmlFor, 
        className 
    } = props;

    // 合并自定义类名和CSS模块中的类名
    const combinedClassName = `${styles.label} ${className}`; 

    return (
        <label 
            htmlFor={htmlFor}
            className={combinedClassName}
        >
            {children}
        </label>
    );
};

export default StyledLabel;

css

/* Label.module.css */
.label {
    font-weight: bold;
    color: #333;
    display: block;
    margin-bottom: 5px;
}

上述代码通过 CSS 模块定义了 label 的基本样式,如加粗字体、颜色、显示方式和底部边距。在组件中通过合并类名应用这些样式,同时也支持添加自定义类名进一步定制。

7.3 标签组件与表单元素的关联应用

标签组件通常与表单元素配合使用,以下是一个完整的表单示例,展示标签组件如何与输入框、单选框和复选框关联:

jsx

import React, { useState } from 'react';
import Label from './Label';
import Input from './Input';
import RadioGroup from './RadioGroup';
import CheckboxGroup from './CheckboxGroup';

const FormExample = () => {
    const [username, setUsername] = useState('');
    const radioOptions = [
        { value: 'male', label: '男' },
        { value: 'female', label: '女' }
    ];
    const [gender, setGender] = useState('');
    const checkboxOptions = [
        { value: 'option1', label: '爱好一' },
        { value: 'option2', label: '爱好二' },
        { value: 'option3', label: '爱好三' }
    ];
    const [hobbies, setHobbies] = useState([]);

    const handleSubmit = (e) => {
        e.preventDefault();
        // 打印表单数据
        console.log('Username:', username);
        console.log('Gender:', gender);
        console.log('Hobbies:', hobbies); 
    };

    return (
        <form onSubmit={handleSubmit}>
            <Label htmlFor="username" className="form-label">用户名:</Label>
            <Input 
                id="username"
                value={username}
                onChange={(e) => setUsername(e.target.value)}
                className="form-input"
            />
            <Label htmlFor="gender" className="form-label">性别:</Label>
            <RadioGroup 
                options={radioOptions}
                value={gender}
                onChange={(value) => setGender(value)}
            />
            <Label htmlFor="hobbies" className="form-label">爱好:</Label>
            <CheckboxGroup 
                options={checkboxOptions}
                value={hobbies}
                onChange={(values) => setHobbies(values)}
            />
            <button type="submit">提交</button>
        </form>
    );
};

export default FormExample;

在这个表单示例中,每个 Label 组件通过 htmlFor 属性与对应的表单元素(InputRadioGroupCheckboxGroup)关联。用户在填写和选择表单内容后,点击提交按钮,handleSubmit 函数会获取并处理表单数据。

7.4 标签组件的国际化支持

在多语言应用中,标签组件的文本内容需要支持国际化。可以借助 react-i18next 库实现,以下是一个简单示例:

jsx

import React from 'react';
import { useTranslation } from'react-i18next';

const InternationalizedLabel = (props) => {
    const { 
        key, // 用于翻译的键
        className 
    } = props;
    const { t } = useTranslation();

    return (
        <label className={className}>
            {t(key)}
        </label>
    );
};

export default InternationalizedLabel;

jsx

import React from'react';
import InternationalizedLabel from './InternationalizedLabel';

const InternationalizedForm = () => {
    return (
        <form>
            <InternationalizedLabel key="username_label" className="form-label" />
            <input type="text" />
            <InternationalizedLabel key="password_label" className="form-label" />
            <input type="password" />
            <button type="submit">
                <InternationalizedLabel key="submit_button" />
            </button>
        </form>
    );
};

export default InternationalizedForm;

在上述代码中,InternationalizedLabel 组件通过 react-i18next 库的 useTranslation 钩子获取翻译函数 t。根据传入的 keyt 函数会从翻译资源文件中获取对应的本地化文本,实现标签组件的国际化支持 。

八、表格组件(Table)

8.1 表格组件的基本实现

表格组件用于以行列形式展示数据。一个简单的 React 表格组件实现如下:

jsx

import React from'react';

const Table = (props) => {
    const { 
        headers, // 表头数据数组
        data // 表格数据数组,每个元素是包含列数据的对象
    } = props;

    return (
        <table>
            <thead>
                <tr>
                    {headers.map(header => (
                        <th key={header}>{header}</th>
                    ))}
                </tr>
            </thead>
            <tbody>
                {data.map(row => (
                    <tr key={row.id}>
                        {Object.values(row).map(cell => (
                            <td key={cell}>{cell}</td>
                        ))}
                    </tr>
                ))}
            </tbody>
        </table>
    );
};

export default Table;

在这个 Table 组件中,headers 数组用于渲染表头,data 数组包含表格的行数据。通过 map 方法分别循环渲染表头和表体内容。

8.2 表格组件的样式定制

为了让表格更美观和易读,需要进行样式定制。使用 CSS 模块实现如下:

jsx

import React from'react';
import styles from './Table.module.css';

const StyledTable = (props) => {
    const { 
        headers, 
        data 
    } = props;

    return (
        <table className={styles.table}>
            <thead>
                <tr>
                    {headers.map(header => (
                        <th key={header} className={styles.tableHeader}>{header}</th>
                    ))}
                </tr>
            </thead>
            <tbody>
                {data.map(row => (
                    <tr key={row.id} className={styles.tableRow}>
                        {Object.values(row).map(cell => (
                            <td key={cell} className={styles.tableCell}>{cell}</td>
                        ))}
                    </tr>
                ))}
            </tbody>
        </table>
    );
};

export default StyledTable;

css

/* Table.module.css */
.table {
    border-collapse: collapse;
    width: 100%;
}

.tableHeader {
    background-color: #f2f2f2;
    text-align: left;
    padding: 8px;
}

.tableRow:nth-child(even) {
    background-color: #f9f9f9;
}

.tableCell {
    border: 1px solid #ddd;
    padding: 8px;
}

上述代码通过 CSS 模块定义了表格的边框样式、表头背景色、奇偶行不同背景色以及单元格样式。在组件中通过类名应用这些样式,提升表格的视觉效果。

8.3 表格组件的排序功能

为了方便用户对表格数据进行排序,需要为表格添加排序功能。实现如下:

jsx

import React, { useState } from'react';

const SortableTable = (props) => {
    const { 
        headers, 
        data 
    } = props;
    // 记录当前排序的列索引,-1表示未排序
    const [sortColumn, setSortColumn] = useState(-1); 
    // 记录排序方向,1为升序,-1为降序
    const [sortDirection, setSortDirection] = useState(1); 

    // 根据列索引和方向对数据进行排序
    const sortedData = data.sort((a, b) => {
        if (sortColumn === -1) {
            return 0;
        }
        const keyA = Object.keys(a)[sortColumn];
        const keyB = Object.keys(b)[sortColumn];
        if (typeof a[keyA] ==='string' && typeof b[keyB] ==='string') {
            return sortDirection * a[keyA].localeCompare(b[keyB]);
        }
        return sortDirection * (a[keyA] - b[keyB]);
    });

    // 切换排序列和方向的处理函数
    const handleSort = (index) => {
        if (sortColumn === index) {
            setSortDirection(sortDirection * -1);
        } else {
            setSortColumn(index);
            setSortDirection(1);
        }
    };

    return (
        <table>
            <thead>
                <tr>
                    {headers.map((header, index) => (
                        <th 
                            key={header}

8.3 表格组件的排序功能

(续上)

jsx

import React, { useState } from'react';

const SortableTable = (props) => {
    const { 
        headers, 
        data 
    } = props;
    // 记录当前排序的列索引,-1表示未排序
    const [sortColumn, setSortColumn] = useState(-1); 
    // 记录排序方向,1为升序,-1为降序
    const [sortDirection, setSortDirection] = useState(1); 

    // 根据列索引和方向对数据进行排序
    const sortedData = data.sort((a, b) => {
        if (sortColumn === -1) {
            return 0;
        }
        const keyA = Object.keys(a)[sortColumn];
        const keyB = Object.keys(b)[sortColumn];
        if (typeof a[keyA] ==='string' && typeof b[keyB] ==='string') {
            return sortDirection * a[keyA].localeCompare(b[keyB]);
        }
        return sortDirection * (a[keyA] - b[keyB]);
    });

    // 切换排序列和方向的处理函数
    const handleSort = (index) => {
        if (sortColumn === index) {
            setSortDirection(sortDirection * -1);
        } else {
            setSortColumn(index);
            setSortDirection(1);
        }
    };

    return (
        <table>
            <thead>
                <tr>
                    {headers.map((header, index) => (
                        <th 
                            key={header}
                            onClick={() => handleSort(index)} // 绑定点击事件以触发排序
                            style={{ cursor: 'pointer' }} // 设置鼠标悬停为指针样式
                        >
                            {header}
                            {sortColumn === index && (
                                <span>
                                    {sortDirection === 1? '▲' : '▼'} {/* 根据排序方向显示箭头 */}
                                </span>
                            )}
                        </th>
                    ))}
                </tr>
            </thead>
            <tbody>
                {sortedData.map(row => (
                    <tr key={row.id}>
                        {Object.values(row).map(cell => (
                            <td key={cell}>{cell}</td>
                        ))}
                    </tr>
                ))}
            </tbody>
        </table>
    );
};

export default SortableTable;

在上述代码中,SortableTable 组件通过 useState 钩子管理当前排序列的索引 sortColumn 和排序方向 sortDirection。当用户点击表头时,handleSort 函数被触发,根据点击的列索引和当前排序状态更新 sortColumn 和 sortDirection。然后,通过 Array.sort 方法对数据进行排序,并在表头显示对应的排序箭头以指示当前排序状态。

8.4 表格组件的分页功能

为了处理大量数据,避免一次性渲染过多内容影响性能,表格通常需要分页功能。实现如下:

jsx

import React, { useState } from'react';

const PaginationTable = (props) => {
    const { 
        headers, 
        data,
        pageSize = 10 // 每页显示的数据条数,默认10条
    } = props;
    // 当前页码,初始为1
    const [currentPage, setCurrentPage] = useState(1); 
    // 计算总页数
    const totalPages = Math.ceil(data.length / pageSize); 
    // 计算当前页显示的数据
    const startIndex = (currentPage - 1) * pageSize;
    const endIndex = startIndex + pageSize;
    const currentData = data.slice(startIndex, endIndex);

    // 页码改变处理函数
    const handlePageChange = (page) => {
        setCurrentPage(page);
    };

    return (
        <div>
            <table>
                <thead>
                    <tr>
                        {headers.map(header => (
                            <th key={header}>{header}</th>
                        ))}
                    </tr>
                </thead>
                <tbody>
                    {currentData.map(row => (
                        <tr key={row.id}>
                            {Object.values(row).map(cell => (
                                <td key={cell}>{cell}</td>
                            ))}
                        </tr>
                    ))}
                </tbody>
            </table>
            <div className="pagination">
                <button 
                    disabled={currentPage === 1} // 禁用首页按钮如果当前是第一页
                    onClick={() => handlePageChange(1)}
                >
                    首页
                </button>
                <button 
                    disabled={currentPage === 1} // 禁用上一页按钮如果当前是第一页
                    onClick={() => handlePageChange(currentPage - 1)}
                >
                    上一页
                </button>
                {Array.from({ length: totalPages }, (_, i) => (
                    <button 
                        key={i + 1}
                        className={currentPage === i + 1? 'active' : ''} // 给当前页码添加active类
                        onClick={() => handlePageChange(i + 1)}
                    >
                        {i + 1}
                    </button>
                ))}
                <button 
                    disabled={currentPage === totalPages} // 禁用下一页按钮如果当前是最后一页
                    onClick={() => handlePageChange(currentPage + 1)}
                >
                    下一页
                </button>
                <button 
                    disabled={currentPage === totalPages} // 禁用尾页按钮如果当前是最后一页
                    onClick={() => handlePageChange(totalPages)}
                >
                    尾页
                </button>
            </div>
        </div>
    );
};

export default PaginationTable;

css

/* 分页样式 */
.pagination {
    display: flex;
    justify-content: center;
    margin-top: 10px;
}

.pagination button {
    margin: 0 5px;
    padding: 5px 10px;
    cursor: pointer;
}

.pagination button.active {
    background-color: #007bff;
    color: white;
    border: none;
}

在 PaginationTable 组件中,通过 useState 管理当前页码 currentPage。根据 pageSize 计算总页数 totalPages 和当前页显示的数据 currentData。提供了首页、上一页、页码按钮、下一页和尾页的交互功能,用户点击相应按钮时,通过 handlePageChange 函数更新 currentPage 并重新渲染当前页数据。同时,通过 CSS 样式对分页按钮进行美化和状态展示。

8.5 表格组件的筛选功能

表格的筛选功能可以帮助用户快速找到符合特定条件的数据。以下是一个支持简单文本筛选的表格组件实现:

jsx

import React, { useState } from'react';

const FilterableTable = (props) => {
    const { 
        headers, 
        data 
    } = props;
    // 记录筛选文本,初始为空字符串
    const [filterText, setFilterText] = useState(''); 
    // 根据筛选文本过滤后的数据
    const filteredData = data.filter(row => {
        return Object.values(row).some(cell => {
            return String(cell).toLowerCase().includes(filterText.toLowerCase());
        });
    });

    return (
        <div>
            <input 
                type="text"
                placeholder="输入关键词筛选..."
                value={filterText}
                onChange={(e) => setFilterText(e.target.value)} // 绑定输入变化事件更新筛选文本
            />
            <table>
                <thead>
                    <tr>
                        {headers.map(header => (
                            <th key={header}>{header}</th>
                        ))}
                    </tr>
                </thead>
                <tbody>
                    {filteredData.map(row => (
                        <tr key={row.id}>
                            {Object.values(row).map(cell => (
                                <td key={cell}>{cell}</td>
                            ))}
                        </tr>
                    ))}
                </tbody>
            </table>
        </div>
    );
};

export default FilterableTable;

在 FilterableTable 组件中,通过 useState 管理筛选文本 filterText。当输入框内容发生变化时,onChange 事件触发更新 filterText。然后,通过 Array.filter 方法对原始数据 data 进行筛选,只保留包含筛选文本的数据行,并渲染过滤后的数据。

8.6 表格组件的单元格编辑功能

有时需要在表格中直接编辑单元格内容,以下是一个支持单元格编辑的表格组件实现:

jsx

import React, { useState } from'react';

const EditableTable = (props) => {
    const { 
        headers, 
        data 
    } = props;
    // 记录处于编辑状态的单元格坐标 [行索引, 列索引],初始为null
    const [editCell, setEditCell] = useState(null); 
    // 深拷贝数据,用于编辑操作
    const [tableData, setTableData] = useState([...data]); 

    // 进入编辑状态的处理函数
    const startEdit = (rowIndex, colIndex) => {
        setEditCell([rowIndex, colIndex]);
    };

    // 保存编辑内容的处理函数
    const saveEdit = (rowIndex, colIndex, newValue) => {
        const newData = [...tableData];
        const key = Object.keys(newData[rowIndex])[colIndex];
        newData[rowIndex][key] = newValue;
        setTableData(newData);
        setEditCell(null);
    };

    // 取消编辑的处理函数
    const cancelEdit = () => {
        setEditCell(null);
    };

    return (
        <table>
            <thead>
                <tr>
                    {headers.map(header => (
                        <th key={header}>{header}</th>
                    ))}
                </tr>
            </thead>
            <tbody>
                {tableData.map((row, rowIndex) => (
                    <tr key={row.id}>
                        {Object.entries(row).map(([key, cell], colIndex) => (
                            {editCell && editCell[0] === rowIndex && editCell[1] === colIndex? (
                                <td key={key}>
                                    <input 
                                        type="text"
                                        value={cell}
                                        onBlur={() => saveEdit(rowIndex, colIndex, event.target.value)} // 失去焦点时保存编辑
                                        onKeyDown={(e) => {
                                            if (e.key === 'Escape') {
                                                cancelEdit(); // 按下Esc键取消编辑
                                            }
                                        }}
                                    />
                                </td>
                            ) : (
                                <td key={key}>
                                    {cell}
                                    <button 
                                        onClick={() => startEdit(rowIndex, colIndex)} // 点击按钮进入编辑状态
                                        style={{ display: editCell && editCell[0] === rowIndex && editCell[1] === colIndex? 'none' : 'inline' }}
                                    >
                                        编辑
                                    </button>
                                </td>
                            )}
                        ))}
                    </tr>
                ))}
            </tbody>
        </table>
    );
};

export default EditableTable;

在 EditableTable 组件中,通过 useState 管理处于编辑状态的单元格坐标 editCell 和表格数据 tableData。当用户点击单元格的 “编辑” 按钮时,startEdit 函数被触发,设置 editCell 进入编辑状态。在编辑过程中,用户输入内容后,失去焦点时 saveEdit 函数保存编辑内容;按下 Esc 键时,cancelEdit 函数取消编辑操作,恢复单元格显示原始内容。

九、模态框组件(Modal)

9.1 模态框组件的基本实现

模态框常用于在页面上弹出重要信息或提示用户进行确认操作。以下是一个简单的 React 模态框组件实现:

jsx

import React, { useState } from'react';
import './Modal.css';

const Modal = (props) => {
    const { 
        title, // 模态框标题
        content, // 模态框内容
        isOpen, // 模态框是否显示
        onClose // 关闭模态框的回调函数
    } = props;

    return (
        {isOpen && (
            <div className="modal-overlay">
                <div className="modal-content">
                    <h2 className="modal-title">{title}</h2>
                    <div className="modal-body">{content}</div>
                    <button className="modal-close" onClick={onClose}>关闭</button>
                </div>
            </div>
        )}
    );
};

export default Modal;

css

/* Modal.css */
.modal-overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.5);
    display: flex;
    justify-content: center;
    align-items: center;
    z-index: 1000;
}

.modal-content {
    background-color: white;
    padding: 20px;
    border-radius: 5px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
}

.modal-title {
    margin-top: 0;
}

.modal-close {
    margin-top: 10px;
    padding: 5px 10px;
    cursor: pointer;
}

在 Modal 组件中,通过判断 isOpen 属性决定是否渲染模态框。当 isOpen 为 true 时,显示一个覆盖整个页面的半透明遮罩层(.modal-overlay),并在中间显示模态框内容(.modal-content),包含标题、主体内容和关闭按钮。关闭按钮点击时触发 onClose 回调函数关闭模态框。

9.2 模态框组件的动画效果

为了提升用户体验,模态框可以添加动画效果,如淡入淡出或滑入滑出。以下是一个添加淡入淡出动画的模态框组件实现:

jsx

import React, { useState } from'react';
import './AnimatedModal.css';

const AnimatedModal = (props) => {
    const { 
        title, 
        content, 
        isOpen, 
        onClose 
    } = props;

    return (
        {isOpen && (
            <div className="animated-modal-overlay fade-in">
                <div className="animated-modal-content">
                    <h2 className="modal-title">{title}</h2>
                    <div className="modal-body">{content}</div>
                    <button className="modal-close" onClick={onClose}>关闭</button>
                </div>
            </div>
        )}
    );
};

export default AnimatedModal;

css

/* AnimatedModal.css */
.animated-modal-overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.5);
    display: flex;
    justify-content: center;
    align-items: center;
    z-index: 1000;
    opacity: 0;
    transition: opacity 0.3s ease-in-out;
}

.fade-in {
    opacity: 1;
}

.animated-modal-content {
    background-color: white;
    padding: 20px;
    border-radius: 5px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
    transform: translateY(-20px);
    opacity: 0;
    transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out;
    transition-delay: 0.1s;
}

.fade-in.animated-modal-content {
    transform: translateY(0);
    opacity: 1;
}

在上述代码中,通过 CSS 过渡(transition)属性为模态框的遮罩层和内容添加淡入淡出及滑入的动画效果。当 isOpen 为 true 时,添加 fade-in 类名,触发相应的动画过渡,使模态框以更平滑的方式显示和隐藏。

9.3 模态框组件的嵌套使用

在一些复杂场景下,可能需要在一个模态框中打开另一个模态框,即模态框的嵌套使用。以下是一个支持嵌套的模态框组件示例:

jsx

import React, { useState } from'react';
import Modal from './Modal';

const NestedModalExample = () => {
    const [mainModalOpen, setMainModalOpen] = useState(false);
    const [nestedModalOpen, setNestedModalOpen] = useState(false);

    return (
        <div>
            <button onClick={() => setMainModalOpen(true)}>打开主模态框</button>
            {mainModalOpen && (
                <Modal 
                    title="主模态框"
                    content="这是主模态框的内容"
                    isOpen={mainModalOpen}
                    onClose={()

9.3 模态框组件的嵌套使用

(续上)

jsx

import React, { useState } from'react';
import Modal from './Modal';

const NestedModalExample = () => {
    const [mainModalOpen, setMainModalOpen] = useState(false);
    const [nestedModalOpen, setNestedModalOpen] = useState(false);

    return (
        <div>
            <button onClick={() => setMainModalOpen(true)}>打开主模态框</button>
            {mainModalOpen && (
                <Modal 
                    title="主模态框"
                    content="这是主模态框的内容
                    <button onClick={() => setNestedModalOpen(true)}>打开嵌套模态框</button>"
                    isOpen={mainModalOpen}
                    onClose={() => setMainModalOpen(false)}
                />
            )}
            {nestedModalOpen && (
                <Modal 
                    title="嵌套模态框"
                    content="这是嵌套模态框的内容"
                    isOpen={nestedModalOpen}
                    onClose={() => setNestedModalOpen(false)}
                />
            )}
        </div>
    );
};

export default NestedModalExample;

在这个示例中,通过两个状态变量 mainModalOpen 和 nestedModalOpen 分别控制主模态框和嵌套模态框的显示与隐藏。在主模态框的内容中添加一个按钮,点击该按钮时设置 nestedModalOpen 为 true 以打开嵌套模态框。每个模态框都有独立的关闭回调函数,确保关闭操作互不干扰。

9.4 模态框组件的自定义按钮配置

除了默认的关闭按钮,有时需要在模态框中添加自定义按钮以执行特定操作。以下是支持自定义按钮配置的模态框组件实现:

jsx

import React from'react';
import './Modal.css';

const CustomButtonModal = (props) => {
    const { 
        title, 
        content, 
        isOpen, 
        onClose,
        customButtons // 自定义按钮数组,每个元素是包含label和onClick的对象
    } = props;

    return (
        {isOpen && (
            <div className="modal-overlay">
                <div className="modal-content">
                    <h2 className="modal-title">{title}</h2>
                    <div className="modal-body">{content}</div>
                    <div className="modal-buttons">
                        {customButtons.map((button, index) => (
                            <button 
                                key={index}
                                className="modal-custom-button"
                                onClick={button.onClick}
                            >
                                {button.label}
                            </button>
                        ))}
                        <button className="modal-close" onClick={onClose}>关闭</button>
                    </div>
                </div>
            </div>
        )}
    );
};

export default CustomButtonModal;

css

/* 新增自定义按钮样式 */
.modal-buttons {
    display: flex;
    justify-content: space-around;
    margin-top: 10px;
}

.modal-custom-button {
    padding: 5px 10px;
    cursor: pointer;
    border: none;
    background-color: #007bff;
    color: white;
    border-radius: 3px;
}

.modal-custom-button:hover {
    background-color: #0056b3;
}

在 CustomButtonModal 组件中,新增 customButtons 属性,它是一个数组,每个元素包含按钮的文本 label 和点击回调函数 onClick。在组件内部,通过 map 方法循环渲染自定义按钮,并将其与默认的关闭按钮一起展示在模态框底部。

9.5 模态框组件的尺寸和位置定制

为了适应不同的使用场景,模态框的尺寸和位置可能需要进行定制。以下是支持尺寸和位置定制的模态框组件实现:

jsx

import React from'react';
import './Modal.css';

const CustomSizePositionModal = (props) => {
    const { 
        title, 
        content, 
        isOpen, 
        onClose,
        width = '500px', // 模态框宽度,默认500px
        height = '300px', // 模态框高度,默认300px
        top = '50%', // 模态框顶部位置,默认垂直居中
        left = '50%', // 模态框左侧位置,默认水平居中
        transform = 'translate(-50%, -50%)' // 用于定位的transform属性值
    } = props;

    return (
        {isOpen && (
            <div className="modal-overlay">
                <div 
                    className="modal-content"
                    style={{ 
                        width, 
                        height, 
                        top, 
                        left, 
                        transform 
                    }}
                >
                    <h2 className="modal-title">{title}</h2>
                    <div className="modal-body">{content}</div>
                    <button className="modal-close" onClick={onClose}>关闭</button>
                </div>
            </div>
        )}
    );
};

export default CustomSizePositionModal;

在 CustomSizePositionModal 组件中,通过新增 widthheighttopleft 和 transform 属性,允许用户自定义模态框的宽度、高度、顶部位置、左侧位置以及定位方式。在渲染时,将这些属性通过 style 对象应用到模态框内容元素上,实现尺寸和位置的灵活定制。

十、通知提示组件(Toast)

10.1 通知提示组件的基本实现

通知提示组件用于在页面上短暂显示提示信息,不打断用户的主要操作流程。以下是一个简单的 React 通知提示组件实现:

jsx

import React, { useState } from'react';
import './Toast.css';

const Toast = (props) => {
    const { 
        message, // 提示信息内容
        duration = 3000 // 提示信息显示时长,默认3秒
    } = props;
    const [showToast, setShowToast] = useState(true);

    React.useEffect(() => {
        const timer = setTimeout(() => {
            setShowToast(false);
        }, duration);
        return () => clearTimeout(timer);
    }, [duration]);

    return (
        {showToast && (
            <div className="toast">
                <p>{message}</p>
            </div>
        )}
    );
};

export default Toast;

css

/* Toast.css */
.toast {
    position: fixed;
    bottom: 20px;
    left: 50%;
    transform: translateX(-50%);
    background-color: rgba(0, 0, 0, 0.8);
    color: white;
    padding: 10px 20px;
    border-radius: 5px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
    z-index: 1001;
}

在 Toast 组件中,通过 useState 管理提示信息的显示状态 showToast,默认显示。使用 useEffect 钩子在组件挂载时启动一个定时器,定时结束后将 showToast 设置为 false 以隐藏提示信息。提示信息默认显示在页面底部中间位置,通过 CSS 样式设置背景、文本颜色、边框等样式。

10.2 通知提示组件的多种类型

通知提示组件可能需要展示不同类型的提示,如成功提示、警告提示、错误提示等。以下是支持多种类型的通知提示组件实现:

jsx

import React, { useState } from'react';
import './Toast.css';

const TypeToast = (props) => {
    const { 
        message, 
        type ='info', // 提示类型,默认info
        duration = 3000 
    } = props;
    const [showToast, setShowToast] = useState(true);

    React.useEffect(() => {
        const timer = setTimeout(() => {
            setShowToast(false);
        }, duration);
        return () => clearTimeout(timer);
    }, [duration]);

    return (
        {showToast && (
            <div className={`toast toast-${type}`}>
                <p>{message}</p>
            </div>
        )}
    );
};

export default TypeToast;

css

/* 新增不同类型的提示样式 */
.toast-info {
    background-color: #007bff;
}

.toast-success {
    background-color: #28a745;
}

.toast-warning {
    background-color: #ffc107;
}

.toast-error {
    background-color: #dc3545;
}

在 TypeToast 组件中,新增 type 属性用于指定提示类型。通过在 CSS 中定义不同类型对应的样式类(如 .toast-info.toast-success 等),在组件渲染时根据 type 值动态添加相应的类名,实现不同类型提示信息的样式区分。

10.3 通知提示组件的队列管理

当短时间内有多个通知提示需要显示时,需要进行队列管理,避免多个提示重叠显示。以下是支持队列管理的通知提示组件实现:

jsx

import React, { useState, useEffect, useRef } from'react';
import './Toast.css';

const ToastQueue = (props) => {
    const { 
        messages // 提示信息数组,每个元素是包含message和type的对象
    } = props;
    const [toastQueue, setToastQueue] = useState([]);
    const timeoutRef = useRef(null);

    useEffect(() => {
        setToastQueue(messages);
    }, [messages]);

    useEffect(() => {
        const showNextToast = () => {
            if (toastQueue.length > 0) {
                const { message, type } = toastQueue[0];
                // 模拟显示Toast组件
                setTimeout(() => {
                    setToastQueue(prevQueue => prevQueue.slice(1));
                }, 3000);
            }
        };

        if (toastQueue.length > 0 && timeoutRef.current === null) {
            timeoutRef.current = setTimeout(showNextToast, 0);
        }

        return () => {
            if (timeoutRef.current) {
                clearTimeout(timeoutRef.current);
                timeoutRef.current = null;
            }
        };
    }, [toastQueue]);

    return (
        <div>
            {toastQueue.map(({ message, type }, index) => (
                <div key={index} className={`toast toast-${type}`}>
                    <p>{message}</p>
                </div>
            ))}
        </div>
    );
};

export default ToastQueue;

在 ToastQueue 组件中,通过 useState 管理提示信息队列 toastQueueuseEffect 钩子在 messages 数据更新时更新队列。另一个 useEffect 钩子用于处理队列中提示信息的显示逻辑,按照顺序依次显示每个提示信息,每个提示显示 3 秒后从队列中移除,确保提示信息有序显示且不重叠。

10.4 通知提示组件的全局调用

为了方便在应用的各个部分调用通知提示组件,通常会实现全局调用的方式。以下是通过 React Context 实现全局调用通知提示组件的示例:

jsx

import React, { createContext, useState, useEffect } from'react';
import Toast from './Toast';

const ToastContext = createContext();

const ToastProvider = (props) => {
    const [toastQueue, setToastQueue] = useState([]);

    const showToast = (message, type = 'info', duration = 3000) => {
        setToastQueue(prevQueue => [...prevQueue, { message, type, duration }]);
    };

    useEffect(() => {
        const handleQueue = () => {
            if (toastQueue.length > 0) {
                const { message, type, duration } = toastQueue[0];
                setTimeout(() => {
                    setToastQueue(prevQueue => prevQueue.slice(1));
                }, duration);
            }
        };

        if (toastQueue.length > 0) {
            handleQueue();
        }

        return () => {
            // 清理可能存在的定时器
        };
    }, [toastQueue]);

    return (
        <ToastContext.Provider value={{ showToast }}>
            {props.children}
            {toastQueue.map(({ message, type }, index) => (
                <Toast key={index} message={message} type={type} />
            ))}
        </ToastContext.Provider>
    );
};

export { ToastContext, ToastProvider };

使用方式:

jsx

import React, { useContext } from'react';
import { ToastContext } from './ToastProvider';

const App = () => {
    const { showToast } = useContext(ToastContext);

    const handleClick = () => {
        showToast('这是一条提示信息', 'info');
    };

    return (
        <div>
            <button onClick={handleClick}>显示提示</button>
        </div>
    );
};

export default App;

在上述代码中,通过 createContext 创建 ToastContext,在 ToastProvider 组件中管理提示信息队列和 showToast 方法。在应用的根组件中包裹 ToastProvider,其他组件通过 useContext 钩子获取 showToast 方法,实现全局调用通知提示组件的功能。

十一、总结与展望

11.1 总结

通过对 React 基础 UI 组件的源码级分析,我们深入了解了按钮、输入框、下拉菜单、复选框、单选框、标签、表格、模态框和通知提示等组件的实现原理和常见扩展功能。从基础的组件结构到复杂的功能定制,每个组件都体现了 React 组件化开发的思想。

在实现过程中,我们运用了 React 的核心特性,如函数式组件、状态管理(useState)、副作用处理(useEffect)以及组件间通信等。同时,结合 CSS 样式定制和各种 JavaScript 逻辑,实现了丰富多样的 UI 效果和交互功能。这些基础 UI 组件是构建复杂 React 应用的基石,理解它们的内部原理有助于开发者更好地进行组件开发、优化和维护。

11.2 展望

随着前端技术的不断发展,React 基础 UI 组件也将迎来更多的变化和发展:

  • 性能优化:未来会有更多针对基础 UI 组件的性能优化方案出现,例如更高效的渲染机制、减少不必要的重渲染等。React 团队也在不断优化核心算法,基础 UI 组件可以从中受益,提升应用整体性能。
  • 与新技术融合:随着 Web 技术的更新,如 CSS 新特性(如 CSS Grid、Flexbox 的进一步发展)、Web Components 等,基础 UI 组件可能会与这些新技术更好地融合,提供更强大和灵活的功能。同时,与新兴的前端框架和库(如 React Server Components、SWR、TanStack Query 等)的结合,也将拓展基础 UI 组件的应用场景。
  • 低代码 / 无代码开发:低代码和无代码开发平台越来越受欢迎,基础 UI 组件将作为重要的构建块被集成到这些平台中。未来可能会出现更标准化、可配置化的基础 UI 组件,方便开发者通过可视化界面快速搭建应用,降低开发门槛。
  • 无障碍性和国际化:对无障碍性和国际化的要求日益增加,基础 UI 组件将更加注重对残障人士的友好支持(如更好的键盘导航、屏幕阅读器兼容性),以及更完善的多语言和本地化支持,以满足全球用户的需求 。