🧹 前端日志查询组件的重构实践:从 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 不只是复用,更是关注点分离
相关推荐
东方小月12 小时前
从零开发一个 Coding Agent(七):实现纯文本 Agent Loop
前端·人工智能
大家的林语冰12 小时前
👉 尤雨溪再次成立新公司,同时官宣 Pinia 4 正式发布!
前端·javascript·vue.js
浮生望12 小时前
React useRef 深度解析:从DOM操作到Web Worker多线程协作
前端
浮生望12 小时前
React Router v7 实战全景:路由配置、嵌套路由、鉴权守卫与懒加载完整指南
前端
用户9385156350712 小时前
从零在浏览器里跑 DeepSeek-R1:WebGPU + Transformer.js 全链路实战(二)
前端·javascript·typescript
雪隐13 小时前
个人电脑玩AI-16让5060 Ti给你打工——MiniMax H3 提速实录:从"泡杯茶等视频"到"视频等你",一张 5060 Ti 的自我修养
前端·人工智能·后端
FungLeo13 小时前
Flutter Web 中文字体困境与渲染器选择:CanvasKit vs HTML renderer 实战
前端·flutter·html
敲代码的玉米C14 小时前
我修的那个 bug,制造了另一个 bug
前端·人工智能·架构
gis开发之家14 小时前
《Vue3 从入门到大神50篇》Vue3 源码详解(二十):生命周期钩子源码解析 —— onMounted / onUpdated 如何实现?
前端·javascript·前端框架·vue3·vue3源码
Narrastory14 小时前
我用 Claude Code 写代码不到 2 小时,却花了 3 天做完这个软件—Vibe Coding 的正确姿势,是设计不是生成
前端·人工智能·github