🧹 前端日志查询组件的重构实践:从 1600 行巨型组件到模块化 hooks

最近在处理日志查询功能时,发现整个模块的功能都集中在一个组件中,代码量接近 1600 行 ,逻辑冗余严重,维护难度大。本文将分享我如何将这个组件拆解重构为模块化、可维护的 hooks 和组件,并深入思考 hooks 的抽象边界与职责划分

🧭 重构背景

在现有项目中,日志查询是高频使用模块。但实现逻辑混乱、功能堆叠严重,导致组件臃肿难扩展。于是决定借此机会重构:

  • ✅ 减少重复逻辑
  • ✅ 拆分职责清晰的组件
  • ✅ 提高维护性与复用性

📋 功能初始化流程梳理

日志查询初始化时,依赖多个来源的数据,优先级如下:

  1. React-routersearch 参数读取查询参数
  2. 再尝试从 state 中读取
  3. 如果都没有,再读取 localStorage 中的缓存
  4. 在组件卸载时清理缓存

原先这些逻辑都写在 useEffect 里,通过分析后我将其封装为一个可配置初始化顺序的 hook:

ts 复制代码
interface UseQueryInitialProps<TState, TCached> {
    cachedkey?: string;
    handleFromSearch?: (search: string) => void;
    handleFromState?: (state: TState) => void;
    handleFromCached?: (cached: TCached) => void;
    onInitEnd?: () => void;
    order?: OrderType[]; // 新增顺序参数
}

// 类似日志查询初始化场景hooks,可通过order参数控制初始化顺序
export default function useInitLogQuery<TState, TCached>(props: UseQueryInitialProps<TState, TCached>) {
    const { handleFromSearch, handleFromState, handleFromCached, cachedkey, onInitEnd, order } = props;
    const { getStorage } = useLogQueryStorage(cachedkey);
    const location = useLocation();

    const hasInitRef = useRef(false);

    useEffect(() => {
        if (hasInitRef.current) return;
        hasInitRef.current = true;
        for (const key of order) {
            if (key === 'search' && location?.search && handleFromSearch) {
                void handleFromSearch(location.search);
                break;
            }
            if (key === 'state' && location.state && handleFromState) {
                void handleFromState(location.state as TState);
                break;
            }
            if (key === 'cached' && getStorage() && handleFromCached) {
                handleFromCached(getStorage());
                break;
            }
        }

        onInitEnd?.();
    }, [location?.search, location?.state]);
}


// 使用
    useInitLogQuery<ISearchProps, LogSearchFormValues>({
        cachedkey: LOCAL_STORAGE_KEY,
        handleFromSearch: () => {
            void handleJumpFromSearch(location?.search);
        },
        handleFromState: () => {
            void handleJumpFromState(location.state as ISearchProps);
        },
        handleFromCached: (cached) => {
            void handleCacheToSearch(cached);
        },
        onInitEnd: () => {
            // 初始化结束后清除缓存
            clearStorage();
        },
        order: [OrderType.search, OrderType.state, OrderType.cached], // 按照顺序处理
    });

✅ 可配置优先级

✅ 与路由、缓存解耦

✅ 适用于流程化页面初始化

🧩 Hook 拆分方案

除了 useInitLogQuery,我将其他逻辑也拆解为以下 hooks:

  • useParams:表单结构 → 接口查询参数
  • useLogQuery:查询参数、日志 key 处理、状态管理
  • useLogQueryStorage:缓存的读写清除
  • useInitLogQuery:初始化逻辑封装

🧱 查询组件拆解

我们将庞大的查询组件按照 单一职责 拆解为多个部分:

  • 查询表单组件
  • 日志展示组件
  • 日志导出组件
  • 交互容器组件

以查询表单组件为例,由于使用了 antd,我们采用了 Form 进行封装:

ts 复制代码
    const handleSearch = async () => {
        try {
            setLoading(true);
            await onSearch?.(curForm.getFieldsValue());
        } finally {
            setLoading(false);
        }
    };

    const handleReset = async () => {
        try {
            setLoading(true);
            const resetValues = {
                ...initialValues,
                ...defaultFormValues,
            };
            synchronousFormSet(resetValues);
            await onReset?.(resetValues);
        } finally {
            setLoading(false);
        }
    };

表单组件不负责实际查询逻辑,只提供查询参数、控制 loading 状态,查询函数由外部传入。

🔄 表单状态同步策略

由于项目使用的 antd 版本不支持自动监听 Form 更新,因此采用 onChange 方案同步外部状态。

ts 复制代码
   // form.setFieldsValue 不能触发onChange,所以需要手动调用onChange
    function synchronousFormSet(values: Partial<LogSearchFormValues>) {
        curForm.setFieldsValue(values);
        onChange?.(curForm.getFieldsValue());
    }
    
    <Form
        onValuesChange={(_, allValues) => {
            onChange?.(allValues);
        }}

当前项目没有使用状态驱动 UI 渲染,因此没有使用类似双向绑定的方式(表单变更会触发整个组件更新)。AI 建议可以将表单状态完全交由父组件控制,这样更符合 React 自顶向下的数据流模型,但会牺牲 Form 的缓存优势。

支持传入 formInstance 以获取表单缓存状态,也支持通过 onChange 同步状态,满足不同使用场景。

❗ 过去使用 ref 获取组件内部值的方式不再推荐 ------ 它无法响应数据更新,违背了 React 数据驱动理念。

💡 对 Hooks 的进一步思考

最初我对 hooks 的使用场景理解并不清晰,疑问是:

"是否每写一个组件都要把状态、函数、useEffect 全部放到 hooks 里?"

我认为:只有具备复用性的逻辑才适合抽成 hooks。但后来得出更深入的理解:

✅ 除了复用,hooks 也用于关注点分离

就算逻辑不可复用,只要它足够复杂、能让组件更清爽,也值得抽成 hook。

参考我得到的 AI 回答截图:

除了复用和纯逻辑场景,关注点分离也是 hooks 抽离的重要动机。即使逻辑耦合、不可复用,也可借助 hooks 将组件体量压缩,让职责更清晰、代码更可读。

✅ 总结

通过本次日志查询模块重构,我学到了:

  • 用 Hook 拆解复杂逻辑,让组件更清爽
  • Form 状态设计需结合版本能力 ,合理使用 onChange
  • 组件之间数据传递应避免使用 ref,保持数据流一致性
  • Hook 不只是复用,更是关注点分离
相关推荐
上单带刀不带妹38 分钟前
ES6 中的 Proxy 全面讲解
前端·ecmascript·es6·proxy
11054654011 小时前
37、需求预测与库存优化 (快消品) - /供应链管理组件/fmcg-inventory-optimization
前端·信息可视化·数据分析·js
nunumaymax2 小时前
在图片没有加载完成时设置默认图片
前端
OEC小胖胖2 小时前
【React 设计模式】受控与非受控:解构 React 组件设计的核心模式
前端·react.js·设计模式·前端框架·web
你怎么知道我是队长2 小时前
C语言---编译的最小单位---令牌(Token)
java·c语言·前端
一枚前端小能手3 小时前
🔥 Vue状态管理越写越乱,Pinia拯救了我
前端
cloudcruiser4 小时前
Apache HTTP Server:深入探索Web世界的磐石基石!!!
前端·其他·http·apache
一个专注api接口开发的小白4 小时前
手把手教程:使用 Postman 测试与调试淘宝商品详情 API
前端·数据挖掘·api
织_网4 小时前
Electron 核心 API 全解析:从基础到实战场景
前端·javascript·electron
vvilkim4 小时前
深入理解 Spring Boot Starter:简化依赖管理与自动配置的利器
java·前端·spring boot