🧹 前端日志查询组件的重构实践:从 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 不只是复用,更是关注点分离
相关推荐
蓝倾35 分钟前
淘宝批量获取商品SKU实战案例
前端·后端·api
comelong40 分钟前
Docker容器启动postgres端口映射失败问题
前端
花海如潮淹42 分钟前
硬件产品研发管理工具实战指南
前端·python
用户38022585982442 分钟前
vue3源码解析:依赖收集
前端·vue.js
WaiterL42 分钟前
一文读懂 MCP 与 Agent
前端·人工智能·cursor
gzzeason1 小时前
使用Vite创建React初始化项目
前端·javascript·react.js
又双叒叕7781 小时前
React19 新增Hooks:useOptimistic
前端·javascript·react.js
归于尽1 小时前
V8 引擎是如何给 JS"打扫房间"的 ?
前端·javascript
小old弟1 小时前
让对象保持定义的顺序来排列
前端
漫天星梦1 小时前
前端列表页大数据内存优化的思考
前端·面试