## easy-model 常见问题(FAQ) > [English Version / 英文版](./FAQ.en.md) 本文整理了一些在使用 easy-model 过程中容易遇到的问题与建议解法,方便快速排查与沟通。 --- ## 1. 字段变了为什么组件没有重新渲染? **可能原因:** - 没有通过 `useModel` / `useInstance` 获取实例,而是手动 `new` 了一个 model。 - 修改的是一个普通对象,而不是 model 中被代理的字段(例如通过@offWatch装饰的对象)。 **建议写法:** - 始终通过 `useModel` 或 `useInstance` 获取实例: - 直接用 `useModel(ModelClass, [args])` 即可实现跨组件共享(内部就是调用 useInstance) ```tsx // 方式一:直接用 useModel(推荐) const model = useModel(CounterModel, []); // 相同参数会自动共享实例 return
{model.count}
; // 方式二:用 useInstance const model = useInstance(existingModel); return
{model.count}
; ``` --- ## 2. 什么时候应该用 easy-model,什么时候不用? **适合用 easy-model 的情况:** - 同一份业务状态需要在多个组件或页面中共享。 - 业务逻辑复杂(涉及多步流程、校验、异步调用、状态流转等)。 - 需要一套可测试、可复用的“领域模型”,而不希望逻辑散落在组件、hooks、工具函数中。 **不一定需要 easy-model 的情况:** - 完全局部的小组件,只在组件内部用到的轻量状态(如“开关是否展开”、“当前 tab 索引”等),用 `useState` 就足够。 - 一次性 demo 或非常简单的表单,没有明显的复用/演进需求。 经验做法:**当你在组件里开始写越来越多 if/for/计算逻辑时,就可以考虑把这块逻辑抽成一个 model。** --- ## 3. `watch` 和 `useWatcher` 有什么区别? - **`useWatcher(model, callback)`** - 只能在 React 组件内使用。 - 生命周期跟随组件:组件挂载时开始监听,卸载时自动停止。 - 适合在组件中做渲染相关的副作用(例如记录当前值的变更历史)。 - **`watch(instance, callback)`** - 可在组件外单独使用。 - 返回一个 `stop` 函数,需要手动调用来停止监听。 - 更适合全局日志、埋点、调试等与具体组件无关的场景。 简单理解:**`useWatcher` 是“组件版 watch”,主要处理与 UI 紧密相关的副作用;`watch` 更偏底层和全局。** --- ## 4. loader 的 `@loader.load` 到底做了什么?和 `useLoader` 怎么配合? - 给某个异步方法加上 `@loader.load` 装饰后,easy-model 会在方法执行期间: - 记录该方法对应的“加载 token”。 - 在 `useLoader()` 返回的 `isLoading(target)` 中可查询到这个方法当前是否在加载。 - 可选地(当配置为全局时)增加/减少全局 loading 计数。 - 在组件中通过: ```ts const { isGlobalLoading, isLoading } = useLoader(); const model = useModel(SomeModel); const localLoading = isLoading(model.someAsyncMethod); ``` 就可以实现: - 全局 loading(如页面遮罩、顶部进度条):用 `isGlobalLoading` 决定是否显示。 - 单个按钮 loading:用 `isLoading(model.someAsyncMethod)` 控制当前按钮的 disabled / loading 状态。 > 常见误区:只在组件里维护 `useState` 的 loading 标志,而没有复用 loader,导致一个异步操作触发多个 loading 状态,很难统一管理。 --- ## 5. 我需要自己写 `useCallback` / `useMemo` 吗? easy-model 关注的是 **状态与领域逻辑** 的组织,本身不会禁止你使用 React 的性能优化手段。 - 如果组件 props 传的是 model 字段或方法本身,通常 React 的重渲染成本是可接受的。 - 当遇到性能瓶颈时,可以考虑: - 在组件外使用 `memo` 包裹纯展示组件。 - 对传入子组件的回调使用 `useCallback` 固定引用。 **经验法则:** - 优先把“减少无意义重渲染”的工作交给合理的组件拆分和 model 设计,而不是一开始就在全局铺满 `useCallback`。 --- ## 6. Model 里能不能直接操作 DOM 或调用浏览器 API? 不建议。 - Model 更适合作为“纯业务层”,只关心数据和流程。 - 直接操作 DOM / 使用浏览器 API(如 `window.location`)更适合放在: - 组件中(基于使用场景)。 - 或封装成专门的 service 再由 model 间接调用。 **推荐做法:** - 需要路由跳转?在 model 中注入一个“导航服务”,由服务封装具体实现(例如使用 React Router)。 - 需要本地存储?在 `services` 中封装一个 storage 工具,而不是在 model 中随处 `localStorage.setItem`。 --- ## 7. 一个页面里可以用多个 model 吗?会不会很乱? 完全可以,而且在复杂页面中是常态。 典型例子: - 一个“订单详情页”可能同时使用: - 订单主信息 model - 支付信息 model - 评论/评分 model **组织方式建议:** - 在页面组件顶部使用多个 `useModel` / `useInstance`: ```tsx const order = useModel(OrderModel, []); const payment = useModel(PaymentModel, []); const comment = useModel(CommentModel, []); ``` - 再将它们拆分传递给子组件,保持每个子组件只关心自己领域的 model 或数据。 关键是:**每个 model 清楚自己负责什么,页面只是把它们组合在一起。** --- ## 8. 如何做单元测试?需要起 React 环境吗? 不需要。 - easy-model 的 model 本质上是普通的 TypeScript 类,状态和方法都可以直接操作。 - 通常可以在“非 React 环境”下直接使用 provider 初始化 model,然后断言: ```ts test("counter works", () => { const counter = provide(CounterModel)(); counter.increment(); expect(counter.count).toBe(1); }); ``` 当 model 依赖外部服务时: - 把外部依赖抽象成接口,在测试中传入 mock 实现; - 或者在 DI 容器中注册一个测试专用的实现。 > 只有在测试 `useModel` / `useInstance` 与组件协作行为时,才需要 React 测试环境。 --- ## 9. 迁移老项目时,从哪里开始比较好? 推荐从 **边界清晰、依赖少的模块** 开始: - 比如登录模块、用户信息模块、某个相对独立的小页面。 大致步骤: 1. 选定一个模块,将其业务状态和逻辑抽成一个 model。 2. 页面使用 `useModel` 替代原有的 Redux / MobX / Zustand / Context 调用。 3. 在保持功能不变的前提下,逐步删除原有状态管理方案中该模块的代码。 4. 重复以上步骤,逐步覆盖更多模块。 关键点:**每次只动一块业务,保证改动可控、易回滚。** --- ## 10. 出现问题时,排查顺序怎么走? 一个简单的排查 Checklist: 1. **组件是否通过 `useModel` / `useInstance` 获取 model?** 2. **model 方法是否真的被调用了?(可以暂时在里边加上 `console.log` 或 watch)** 3. **异步方法是否被 `await` 或正确使用 loader?** 4. **是否在多个地方维护了重复的状态?(例如组件自己 `useState` + model 里也有同名字段)** 如果确认问题来自 easy-model 的行为,可以: - 用 `watch` 或 `useWatcher` 打印关键字段变化。 - 在最小可复现场景下写一份小 demo,对比预期行为与实际行为。