🧹 前端日志查询组件的重构实践:从 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 不只是复用,更是关注点分离
相关推荐
爱上妖精的尾巴1 分钟前
3-19 WPS JS宏调用工作表函数(JS 宏与工作表函数双剑合壁)学习笔记
服务器·前端·javascript·wps·js宏·jsa
草履虫建模11 分钟前
Web开发全栈流程 - Spring boot +Vue 前后端分离
java·前端·vue.js·spring boot·阿里云·elementui·mybatis
—Qeyser17 分钟前
让 Deepseek 写电器电费计算器(html版本)
前端·javascript·css·html·deepseek
UI设计和前端开发从业者1 小时前
从UI前端到数字孪生:构建数据驱动的智能生态系统
前端·ui
Junerver2 小时前
Kotlin 2.1.0的新改进带来哪些改变
前端·kotlin
千百元2 小时前
jenkins打包问题jar问题
前端
喝拿铁写前端2 小时前
前端批量校验还能这么写?函数式校验器组合太香了!
前端·javascript·架构
巴巴_羊2 小时前
6-16阿里前端面试记录
前端·面试·职场和发展
我是若尘2 小时前
前端遇到接口批量异常导致 Toast 弹窗轰炸该如何处理?
前端
该用户已不存在3 小时前
8个Docker的最佳替代方案,重塑你的开发工作流
前端·后端·docker