ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Easy Peasy 热重载(Hot Reloading)实战:用 reconfigure 在开发期动态更新 Store 模型

Easy Peasy 热重载(Hot Reloading)实战:用 reconfigure 在开发期动态更新 Store 模型
  • 前端

【免费下载链接】easy-peasy

Vegetarian friendly state for React

项目地址:https://gitcode.com/gh_mirrors/ea/easy-peasy
点击查看免费下载

Easy Peasy 原生支持热重载(Hot Reloading),即在不刷新页面、不丢失当前应用状态的前提下,于开发阶段动态更新 store 的 model 定义。本文将基于官方 recipes 文档 hot-reloading.md,结合 create-store.js 的源码实现与 store.test.js 中的测试用例,讲解如何为 Easy Peasy store 接入 Webpack HMR、reconfigure的内部工作链路、状态保持机制与常见的注意事项,读完即可在你的项目中落地一套开发体验良好的热重载配置。

为什么要给 Easy Peasy store 做热重载

在传统的 React 开发中,每次修改 model(比如调整某个 action 的逻辑、给某个 slice 新增字段),都需要刷新页面才能看到效果。这带来两个痛点:

  • 页面刷新导致 store 中保存的临时状态(表单输入、路由信息、操作中间态等)全部丢失;
  • 在调试复杂交互流程时,每次改动都要重新走一遍操作路径,开发效率低下。

Easy Peasy 提供的热重载能力,核心是允许你在不重建 store 实例、不丢弃现有 state的情况下,把一份全新的 model 定义"换"进正在运行的 store 中。正如 store.md 中对该 API 的定位描述:reconfigure允许你用一份新的/更新过的 model 重新配置 store,且现有状态会被完整保留——这正是热重载场景的首选 API。

接入 Webpack HMR 的完整配置

Easy Peasy 官方给出的热重载配置非常轻量,核心只需两步:在 store 模块里监听 model 模块的热更新,并在更新回调中调用store.reconfigure(model)。下面是官方文档提供的完整示例(保持原样并补充注释):

// src/store/index.js import { createStore } from "easy-peasy"; import model from "./model"; const store = createStore(model); // Wrapping dev only code like this normally gets stripped out by bundlers // such as Webpack when creating a production build. if (process.env.NODE_ENV === "development") { if (module.hot) { module.hot.accept("./model", () => { store.reconfigure(model); // 👈 Here is the magic }); } } export default store;

这段代码的关键点拆解如下:

  • module.hot判断:这是 Webpack 注入的 HMR 全局对象,只在开发环境存在。加上process.env.NODE_ENV === "development"判断后,Webpack 在生产构建时会自动将整段代码剥离(dead code elimination),因此不会给生产包带来任何体积或行为负担。
  • module.hot.accept("./model", callback):表示该模块接受./model子模块的热更新。当你在编辑器中保存model模块(或其依赖)的改动时,Webpack 会重新编译并执行这个回调,而不会触发整页刷新。
  • store.reconfigure(model):回调中的model在 HMR 之后会指向最新编译出的 model 定义。把它传给reconfigure,store 内部便会基于新 model 重建 reducer 与 action 绑定,同时保住现有 state。

提示:如果你的项目不是 Webpack,而是使用 Vite 进行开发,Vite 的 HMR 提供的是import.meta.hotAPI(用法为import.meta.hot.accept('./model', () => { store.reconfigure(model); })),思路与上述完全一致——本质都是在模块热更新时把新 model 交给reconfigure。仓库中基于 Vite 的示例(如 examples/simple-todo)与热重载配置互不冲突,可放心叠加使用。

reconfigure 的核心 API 契约

reconfigure是 createStore 返回的 store 实例上的一个公开方法,其类型签名定义在 index.d.ts:

reconfigure: <NewStoreModel extends object>(model: NewStoreModel) => void;

可以看到它是泛型方法,允许传入一个与旧 model 结构不同的新 model(TypeScript 中会按新结构重新推导 actions 与 state 类型)。这一点很重要:热重载不仅支持"修改已有 action 的实现",还支持新增 slice、新增字段、删除 action 等结构性变更,而不需要你重启应用。

源码剖析:reconfigure 内部到底发生了什么

要理解"状态为什么能保住、action 为什么能更新",需要进入 create-store.js 的实现。reconfigure的定义在 create-store.js:

reconfigure: (newModel) => { modeldef = bindReplaceState(newModel); rebindStore(); },

bindReplaceState(create-store.js)只是给 model 额外注入一个内部 actionePRS,用于后续整体替换 state 节点;真正干活的是rebindStore(create-store.js):

const rebindStore = (removeKey) => { const currentState = store.getState(); // 1. 取出当前状态 if (removeKey) { delete currentState[removeKey]; } bindStoreInternals(currentState); // 2. 以当前状态为基底重新提取模型 store.replaceReducer(_r._i.reducer); // 3. 用新 reducer 替换旧 reducer replaceState(_r._i._dS); // 4. 派发内部 action 填充默认状态 bindActionCreators(); // 5. 重新绑定 dispatch 上的 actions };

这条调用链的每一环都对应一个明确的职责:

  1. store.getState():在重绑之前先读取 store 当前的完整状态树,这是"现有状态被保持"的起点;
  2. bindStoreInternals(currentState)(create-store.js)调用extractDataFromModel基于新 model 重新提取 actions、thunks、listeners、computed properties、reducers 与持久化配置,并构建新的根 reducer。关键细节在 extract-data-from-model.js:当它遍历新 model 遇到纯状态字段时,会优先从传入的initialState(即上一步的currentState)中取出同名 key 的值,只有在新 model 中新增的字段才使用 model 中定义的默认值。这正是"已有状态完整保留、新增字段获得默认值"的机制来源;
  3. store.replaceReducer:Easy Peasy 的 store 本质是基于 Redux 构建的(见 create-store.js 的reduxCreateStore调用),因此这里直接使用 Redux 的replaceReducer让 store 在不重新创建实例的前提下切换到新 reducer,连接的 React 组件不会卸载;
  4. replaceState(create-store.js)通过内部ePRSaction 把第 2 步提取出的完整状态树派发进 reducer,使 state 与新 reducer 结构对齐;
  5. bindActionCreators(create-store.js)清理 dispatch 上旧的 action creators,再按新 model 提取出的_aC重新挂载,从而保证store.getActions()与store.dispatch暴露的 action 集合始终与最新 model 一致。

测试用例验证:新增、保留与移除

仓库在 store.test.js 中用support model reconfiguration这个测试完整验证了 reconfigure 的三类行为,恰好可以作为热重载场景的行为规范参考:

① 新增字段且保留既有状态(store.test.js):

store.getActions().todos.addTodo('support hot reloading'); store.reconfigure({ todos: { items: [], addTodo: action((state, payload) => { state.items.push(payload); }), bob: 1, // 新增字段 }, }); store.getActions().todos.addTodo('zing'); expect(store.getState()).toEqual({ todos: { items: ['support hot reloading', 'zing'], bob: 1 }, });

reconfigure 之前派发的'support hot reloading'仍在items中,新增的bob: 1也出现在状态树上。

② 修改 action 实现后按新逻辑执行(store.test.js 同段测试的后半部分):把addTodo换成removeTodo后调用,状态中的items由两个元素变为一个,证明新 reducer 已完全接管。

③ 移除的 action 不再可用(store.test.js):

store.reconfigure({ todos: { items: [], removeTodo: ... } }); expect(store.getActions().todos.addTodo).toBeUndefined();

被删除的addTodo在 action 集合中被彻底移除,避免了调用到过期 action 的隐患。

与 addModel / removeModel 的分工

热重载场景下,reconfigure负责"整体换新",而 store 上还有一对面向代码分割的动态 API 值得区分:

  • reconfigure(newModel):整体替换整个 model,适用于 HMR 这类"整份 model 定义更新"的场景,状态整体保留;
  • addModel(key, slice)/removeModel(key)(create-store.js):按 key 动态增删某个 model 切片,适用于按需加载的代码分割策略,例如路由懒加载对应的 store 切片。内部同样走rebindStore,且addModel还会尝试对新增切片做持久化数据的 rehydrate。

两者共用同一套重绑机制,因此行为一致、可以放心混用。

使用热重载时的注意事项

综合源码实现与测试用例,接入热重载时建议留意以下几点:

  1. process.env.NODE_ENV包裹是安全的:如文档所述,这类开发期专属代码会被 Webpack 等打包器在生产构建时剥离,不会进入产物;
  2. 持久化(persist)与 reconfigure 的交互:reconfigure之后 store 依然按照 persistence.js 中建立的持久化配置工作。如果新 model 中删除了某个带persist配置的切片,其已持久化的数据不会自动清除,可结合store.persist.clear()(定义见 create-store.js)手动清理,具体持久化 API 参见 persist.md;
  3. reducer 与组件协调:由于 store 实例与 Provider 都没有被重建(store 的创建与提供方式参见 create-store.md 与 store-provider.md),React 组件树不会卸载,因此热更新后的 UI 会即时反映新 model 的行为,这正是理想的热重载体验;
  4. 只接受自己关心的模块:module.hot.accept的依赖路径应精确指向 model 模块,避免过度接受导致不必要的整页刷新;
  5. 测试环境无需 HMR:reconfigure本身可脱离 Webpack 独立使用(如上述测试所示),因此在测试中模拟"运行时换 model"同样成立,这也是它作为 API 而非纯工具函数被暴露的价值。

小结

Easy Peasy 的热重载方案,本质是"一个reconfigure方法 + 一条打包器 HMR 管线"的组合:前者负责在运行时无损地重建 store 的 reducer 与 action 绑定,后者负责感知 model 模块的更新并触发前者。借助 store.test.js 的用例,我们可以确认状态保留、字段新增、action 移除等行为都经过了仓库级验证。对于追求开发效率的 React + Easy Peasy 项目,把这段十几行的配置放进 store 模块,即可获得"改 model 不丢状态、不改页面"的开发体验。

  • 前端

【免费下载链接】easy-peasy

Vegetarian friendly state for React

项目地址:https://gitcode.com/gh_mirrors/ea/easy-peasy
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进