{"id":20022245,"url":"https://github.com/lucifier129/pure-model","last_synced_at":"2025-05-05T01:31:27.257Z","repository":{"id":44537595,"uuid":"305628010","full_name":"Lucifier129/pure-model","owner":"Lucifier129","description":"A framework for writing model-oriented programming","archived":false,"fork":false,"pushed_at":"2024-08-30T07:03:25.000Z","size":2105,"stargazers_count":18,"open_issues_count":0,"forks_count":5,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-04-08T14:52:29.659Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Lucifier129.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-10-20T07:39:35.000Z","updated_at":"2025-01-12T17:20:10.000Z","dependencies_parsed_at":"2024-07-17T12:12:35.853Z","dependency_job_id":null,"html_url":"https://github.com/Lucifier129/pure-model","commit_stats":null,"previous_names":[],"tags_count":21,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Lucifier129%2Fpure-model","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Lucifier129%2Fpure-model/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Lucifier129%2Fpure-model/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Lucifier129%2Fpure-model/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Lucifier129","download_url":"https://codeload.github.com/Lucifier129/pure-model/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252423154,"owners_count":21745551,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-13T08:39:41.042Z","updated_at":"2025-05-05T01:31:23.287Z","avatar_url":"https://github.com/Lucifier129.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# pure-model\n\nA framework for writing model-oriented programming\n\n编写 UI 无关的通用业务逻辑，可适配 react-native 或者 react-dom 等多个平台、多个框架。\n\n- 使用 redux 进行状态管理，支持 redux-devtools 和 redux-logger\n- 支持通过 immer 简化 state 更新操作\n- 支持 fetch/post/get 等接口交互\n- 支持 SSR 服务端渲染\n- 支持使用 Typescript 开发\n- 提供 react-hooks api 优化使用方式\n- 适配 react-imvc 和 react-native\n- 可脱离 UI 独立运行和测试\n\n## 安装\n\n```shell\n# install core\nnpm install --save @pure-model/core\n# install react adapter\nnpm install --save @pure-model/react\n# install immer adapter\nnpm install --save @pure-model/immer\n# install pure-model hooks\nnpm install --save @pure-model/hooks\n# install pure-model test utils\nnpm install --save @pure-model/test\n# install next.js adapter\nnpm install --save @pure-model/next.js\n```\n\n快速安装 pure-model + react + next.js + immer\n\n```shell\nnpm install --save @pure-model/core @pure-model/react @pure-model/next.js @pure-model/immer\n```\n\n## 目录\n\n- [基本用法](#基本用法)演示了 pure-model + react 的朴素写法，可以让我们看到运行 pure-model 的几个基本步骤\n\n- [API 介绍](#api-介绍)\n  - [基础 API](#基础-api)\n    - [createPureModel](#createpuremodelinitializer-options)\n    - [setupStore](#setupstore-name-initialstate-reducers-devtools-logger-)\n    - [setupModel](\u003c#setupmodel(model)\u003e)\n    - [createModelContext](#createmodelcontextinitialvalue-setupcontextmodelcontext-mergemodelcontextmodelcontxtvalue)\n    - [setupPreloadCallback](#setuppreloadcallbacklistener)\n    - [setupStartCallback](#setupstartcallbacklistener)\n    - [setupFinishCallback](#setupfinishcallbacklistener)\n    - [subscribe](#subscribemodel-listener)\n    - [select](#selectoptions)\n  - [React 组件适配 API](#react-组件适配-api)\n    - [createReactModel](#createreactmodelinitializer)\n    - [Provider](#provider-组件)\n    - [preload](#preload-model-context-preloadedstate-)\n    - [useReactModel](#usereactmodelreactmodel-options)\n  - [Next.js 框架适配 API](#nextjs-框架适配-api)\n  - [immer 适配 API](#immer-适配)\n  - [http 接口请求 API](#http-接口请求-api)\n  - [测试辅助套件 API](#测试辅助套件-api)\n  - [其它 API](#其它-api)\n    - [setupCancel](#setupcancel)\n    - [setupSequence](#setupsequence)\n    - [setupInterval](#setupinterval)\n\n### 基本用法\n\n第一步，编写基于 redux 的状态管理代码\n\n```typescript\n// model/todo.ts\n\n// 引入 setupStore\nimport { setupStore } from '@pure-model/core'\nimport { createReactModel } from '@pure-model/react'\n\n// 定义 state 的类型\nexport type Todo = {\n  id: number\n  content: string\n  completed: boolean\n}\n\nexport type Todos = Todo[]\n\n// 定义初始化 state\nconst initialState: Todos = []\n\n// export react model\nexport default createReactModel(() =\u003e {\n  let { store, actions } = setupStore({\n    // 可选参数，会反映到 redux-devtools 里的 name\n    name: 'todos',\n    // 必选参数：initialState\n    initialState,\n    // 必须参数：reducers，更新状态函数\n    reducers: {\n      addTodo,\n      removeTodo,\n      updateTodoContent,\n      updateTodoStatus,\n      toggleTodo,\n      toggleAll,\n      clearCompleted,\n    },\n    // 可选参数，是否开启 redux-logger，默认为 false\n    logger: true,\n    // 可选参数，是否开启 redux-devtools，默认为 true\n    devtools: true,\n  })\n\n  // 必须返回 store + actions 的对象结构\n  return { store, actions }\n})\n\n/**\n * 编写 reducer 的方式进行了简化\n * 第一个参数为 state\n * 第二个参数为 payload，不需要添加 { type, payload } 的对象\n * payload 可以是任意纯数据类型(JSON)，但不能是函数，或者带原型的对象\n */\nconst addTodo = (todos: Todos, content: string) =\u003e {\n  let todo = {\n    id: Date.now(),\n    content,\n    completed: false,\n  }\n  return todos.concat(todo)\n}\n\nconst removeTodo = (todos: Todos, id: number) =\u003e {\n  return todos.filter((todo) =\u003e todo.id !== id)\n}\n\nconst updateTodoContent = (todos: Todos, { id, content }: { id: number; content: string }) =\u003e {\n  return todos.map((todo) =\u003e {\n    if (todo.id !== id) return todo\n    return {\n      ...todo,\n      content: content,\n    }\n  })\n}\n\nconst updateTodoStatus = (todos: Todos, { id, completed }: { id: number; completed: boolean }) =\u003e {\n  return todos.map((todo) =\u003e {\n    if (todo.id !== id) return todo\n    return {\n      ...todo,\n      completed,\n    }\n  })\n}\n\nconst toggleTodo = (todos: Todos, id: number) =\u003e {\n  return todos.map((todo) =\u003e {\n    if (todo.id !== id) return todo\n    return {\n      ...todo,\n      completed: !todo.completed,\n    }\n  })\n}\n\nconst toggleAll = (todos: Todos) =\u003e {\n  let isAllCompleted = todos.every((todo) =\u003e todo.completed)\n\n  return todos.map((todo) =\u003e {\n    return {\n      ...todo,\n      completed: !isAllCompleted,\n    }\n  })\n}\n\nconst clearCompleted = (todos: Todos) =\u003e {\n  return todos.filter((todo) =\u003e !todo.completed)\n}\n```\n\n第二步，在 react 组件中，引入和使用 react model\n\n```tsx\n// index.tsx\nimport React from 'react'\nimport ReactDOM from 'react-dom'\n\nimport { Provider } from '@pure-model/react'\n\n// 引入第一步编写的 react model 模块\nimport TodoModel from './model/todo'\n\nconst App = () =\u003e {\n  let [text, setText] = React.useState('')\n\n  // 通过 TodoModel.useState 获取到 TodoModel 内部的 redux store 的 state 状态\n  let state = TodoModel.useState()\n\n  // 通过 TodoModel.useActions 获取到 TodoModel 内部暴露出来的 actions 对象\n  let actions = TodoModel.useActions()\n\n  /**\n   * 在 event-handler 里，调用 actions 函数，触发状态更新\n   * 视图将自动更新\n   * 注意：请勿直接将 event 对象传给 actions，这样会破坏 action 跨平台的能力\n   * 将数据提纯为普通的 JSON 数据对象，再传入 action 函数\n   */\n  let handleAddTodo = (event) =\u003e {\n    setText('')\n    actions.addTodo(text)\n  }\n\n  let handleChange = (event) =\u003e {\n    setText(event.target.value)\n  }\n\n  return (\n    \u003cdiv\u003e\n      \u003cinput type=\"text\" value={text} onChange={handleChange} /\u003e\n    \u003c/div\u003e\n  )\n}\n\n/**\n * 构造初始化 ReactModel 相关的参数\n * 支持初始化多个 ReactModel\n */\nconst ReactModelArgs = [\n  {\n    Model: Model, // 必选参数，要注入的 React Model 对象\n    preloadedState: [], // 可选参数，要注入到 redux store 的预加载状态，\n    context: undefined, // 可选参数，要注入到 model 内部的 context 对象\n  },\n]\n\n/**\n * 初始化渲染\n */\nReactDOM.render(\n  \u003cProvider list={ReactModelArgs}\u003e\n    \u003cApp /\u003e\n  \u003c/Provider\u003e,\n  document.getElementById('root'),\n)\n```\n\n### 通过 react-class-component 启动\n\n除了通过 `Provider` 组件启动以外，在 React 中，还有另一种方式，通过 `provide`\n\n```tsx\nimport Controller from 'react-imvc/controller'\n// 引入 MODEL_CONTEXT 这个 symbol\nimport { MODEL_CONTEXT } from '@pure-model/core'\n// 引入 pure-model 的 class-component 适配器\nimport { provide } from '@pure-model/react'\n// 引入自定义 ModelContext\nimport { EnvContext } from './EnvContext'\n// 引入编写好的 ReactModel\nimport { TestModel } from './TestModel'\n\n// 通过装饰符 decorator 将 TestModel 注入 controller\n// 可以传递多个 Model 比如 @provide({ Model1, Model2, Model3 })\n@provide({ TestModel })\nexport default class MyComponent extends React.Component\u003cany, any\u003e {\n  /**\n   * 将 env 注入 EnvContext\n   * 可以通过 {...MyContext0.impl(), ...MyContext1.impl() } 追加多个 context value 注入\n   */\n  [MODEL_CONTEXT] = {\n    ...EnvContext.impl({\n      env: 'prod', // 设置 env 即可\n    }),\n  }\n\n  /**\n   * App 组件内部可以使用 TestModel.useState 等 api 了\n   * 并且 App 组件不会在 model 的 setupPreloadCallback 完成之前被渲染\n   */\n  render() {\n    return \u003cApp /\u003e\n  }\n}\n\n// 不喜欢，或不支持 decorator 的场景，可以使用 HOC 高阶函数的风格\nclass MyComponent extends React.Component\u003cany, any\u003e {\n  /**\n   * 将 env 注入 EnvContext\n   * 可以通过 {...MyContext0.impl(), ...MyContext1.impl() } 追加多个 context value 注入\n   */\n  [MODEL_CONTEXT] = {\n    ...EnvContext.impl({\n      env: 'prod', // 设置 env 即可\n    }),\n  }\n\n  /**\n   * App 组件内部可以使用 TestModel.useState 等 api 了\n   * 并且 App 组件不会在 model 的 setupPreloadCallback 完成之前被渲染\n   */\n  render() {\n    return \u003cApp /\u003e\n  }\n}\n\n// 可以传递多个 Model 比如 provide({ Model1, Model2, Model3 })(MyComponent)\nexport default provide({ TestModel })(MyComponent)\n```\n\n## Next.js 框架适配 API\n\n`@pure-model/next.js` 提供了对 `next.js` 框架的适配 API\n\npage options 参数如下：\n\n- `Models` 对象类型，value 为 `ReactModel`\n- `contexts` 接受一下类型的参数：\n  - 数组类型，value 为 `ModelContextValue`，通过 `Context.create(value)` 创建\n  - 函数类型，接受一个 `options` 参数，包含 `{ ctx?, isServer, getInitialProps }`，返回 `ModelContextValue` 数组\n    - `ctx` 为 `NextPageContext` 对象，可能存在，也可能不存在\n    - `getInitialProps` 为 boolean 类型，表示是在 `Page.getInitialProps` 里调用，还是在 `Page` 组件里调用，组件里调用时没有 `ctx` 对象\n    - `isServer` 为 boolean 类型，表示是否在服务端运行\n- `preload` 方法函数，接受两个参数 `models` 实例对象 和 `ctx` 上下文对象，可以在这里进行数据同步\n\n```typescript\n// 引入 page 函数\nimport { page } from '@pure-model/next.js'\n\n// 引入页面依赖的 Models 模块\nimport LayoutModel from '../../models/LayoutModel'\nimport IndexModel from './Model'\n\n// 引入页面的 View 组件\nimport View from './View'\n\n// 创建一个 Page\nconst Page = page({\n  // 传入所有 Models\n  Models: {\n    LayoutModel,\n    IndexModel,\n  },\n\n  /**\n   * 可选的 contexts 数组，可以注入 context value\n   * 改变 models 内部 setupContext(EnvContext) 获取的 context value\n   */\n  contexts: (options) =\u003e {\n    let ctx = options.ctx // ctx 为 NextPageContext 对象\n\n    // options.getInitialProps 为 boolean 值，判断是在\n    console.log('getInitialProps', options.getInitialProps)\n\n    return [\n      EnvContext.create({\n        env: 'prod',\n      }),\n    ]\n  },\n\n  /**\n   * 可选：配置 preload 方法\n   * 第一个参数为 models 实例\n   * 第二个参数为 NextPageContext\n   * 调用 model.actions 方法更新 model\n   * 调用 model.store.getState() 获取 model 里的 state\n   * 从 ctx 中获取 query/params, pathname 等参数，可传递给各个 models\n   * 各 models 之间也可以在 preload 方法里同步数据\n   * preload 方法先于 models 内部的 setupPreloadCallback(preloadCallback) 里的 preloadCallback 执行\n   */\n  preload: async ({ IndexModel }, ctx) =\u003e {\n    let tab = 'all'\n\n    if (Array.isArray(ctx.query.tab)) {\n      tab = ctx.query.tab.join('')\n    } else if (ctx.query.tab) {\n      tab = ctx.query.tab\n    }\n\n    IndexModel.actions.setSearchParams({\n      tab: tab,\n    })\n  },\n})\n\n// 用 Page 包裹 View 创建一个 NextPage 组件\nexport default Page(View)\n```\n\n## API 介绍\n\n`pure-model` 的 `setup*` 开头的 api，跟 `react-hooks` 和 `vue-composition-api` 一样，只能用在 `initializer` 函数中。\n\n可以封装自定义的 pure-model hooks `setupXXX` 进行逻辑和功能的复用。\n\n```javascript\n// 基础 api\nimport {\n  // 创建 model\n  createPureModel,\n\n  // 创建 store\n  setupStore,\n\n  // 创建 context\n  createModelContext,\n  // 合并 context value\n  mergeModelContext\n  // 使用 context\n  setupContext,\n  // ModelContextValue 包含的 symbol\n  MODEL_CONTEXT,\n\n  // 注册 model.preload 事件\n  setupPreloadCallback,\n  // 注册 model.start 事件\n  setupStartCallback,\n  // 注册 model.finish 事件\n  setupFinishCallback,\n\n  // 订阅 model store 内部的 state 状态\n  subscribe,\n  // 订阅 model store 内部的部分 state 状态\n  select,\n\n  // http 相关 api\n  setupFetch,\n  setupGetJSON,\n  setupPostJSON,\n} from '@pure-model/core'\n\n// react 组件适配 api\nimport {\n  // 创建绑定到 react 的 model\n  createReactModel,\n  // 注入 react model 用的 Provider 组件\n  Provider,\n  // 注入 react model 用的 provide 高阶函数\n  provide,\n  // 预加载多个 react model 的函数\n  preload,\n  // 在单个组件内使用 react-model 的 api\n  useReactModel,\n} from '@pure-model/react'\n\n// next.js 框架适配 api\nimport {\n  page\n} from '@pure-model/next.js'\n\n// immer 适配 api\nimport {\n  // 将 immer reducer 函数变成 plain reducer 函数\n  toReducer,\n  // 将 immer reducers 对象变成 plain reducers 对象\n  toReducers\n} from '@pure-model/immer'\n\n\n// 测试辅助套件 api\nimport { testHooks } from '@pure-model/test'\n\n// 内置辅助 model hooks api\nimport { setupCancel, setupSequence, setupInterval } from '@pure-model/hooks'\n\n```\n\n### 基础 API\n\n#### createPureModel(initializer, options?)\n\n创建一个 model\n\n- `initializer` 参数为函数类型，`() =\u003e { store, actions }` 返回 store + actions\n  - `initializer` 必须为同步的函数，才可以使用 `setup*` 的 pure-model hooks api\n- `options` 参数为对象类型\n  - `options.preloadedState` 注入预加载状态到 store，对应 redux `createStore` 里的 `preloadedState`\n  - `options.context` 注入 model context\n\n```javascript\nlet model = createPureModel(() =\u003e {\n  let { store, actions } = setupStore({\n    initialState: 0,\n    reducers: {\n      incre: (state) =\u003e state + 1,\n      decre: (state) =\u003e state - 1,\n    },\n  })\n\n  return { store, actions }\n})\n\n// 触发 setupPreloadCallback\nmodel.preload().then(() =\u003e {\n  // 触发 setupStartCallback\n  model.start()\n  // 触发 setupFinishCallback\n  model.finish()\n})\n\n// 访问 store\nmodel.store.getState()\nmodel.actions.incre()\n```\n\n`createPureModel` 返回的 model 包含以下结构\n\n- `model.store` 为 redux store，点击查看[store api](https://redux.js.org/api/store)\n- `model.actions` 为 `initializer` 函数返回的 `actions`\n- `model.preload()` 触发订阅了 `setupPreloadCallback` 的函数，必须在 start, finish 之前调用\n  - 当 `options.preloadedState` 有值时， 意味着 preload 已完成，`model.preload()` 不会生效，会直接跳过\n- `model.start()` 触发订阅了 `setupStartCallback` 的函数，必须在 preload 之后调用，finish 之前调用\n- `model.finish()` 触发订阅了 `setupFinishCallback` 的函数，必须在 preload, start 之后调用\n- `model.isPreloaded()` 返回 `boolean`，判断是否已 preload\n- `model.isStarted()` 返回 `boolean`，判断是否已 start\n- `model.isFinished()` 返回 `boolean`，判断是否已 finish\n\n`preload|start|finsih` 只在第一次调用时有效。\n\n#### setupStore({ name?, initialState, reducers, devtools?, logger? })\n\n创建 store，`setupStore` 只能用在 `initializer` 函数内部。\n\n- `options.name` 为可选参数，接收字符串类型，将会出现在 redux-devtools 的展示界面上\n- `options.initialState` 为必选参数，接收任意类型的纯数据，但不允许是函数或带原型的对象。\n- `options.reducers` 为必选参数， { key: reducer } 对象，可以为空对象\n- `options.devtools` 为可选参数，接收`boolean`类型，是否开启 redux-devtools（只在运行环境中支持 redux-devtools 时生效），默认为 true\n- `options.logger` 为可选参数，接收 `boolean` 类型，是否开启 redux-logger\n\n`setupStore` 的返回值为 { store, actions }，其中\n\n- `store` 为 redux store 对象，点击查看[store api](https://redux.js.org/api/store)\n- `actions` 为对 `reducers` 进行了 `bindActionCreators` 封装的对象，跟 `reducers` 拥有相同的 key 结构，但调用时去掉了 `state` 参数，并且会触发 store 内部更新。\n\n注意：setupStore 返回的 actions 跟最后 return 出去的 actions，并无强关联。\n\n- 可以不把 setupStore 返回的 actions return 到外部\n- 可以有选择的选取 setupStore 返回的 actions 暴露到外部的部分\n- 可以根据 setupStore 返回的 actions 构造异步的或者分组的 actions，打包到一起暴露出去\n- 暴露出去的 actions 函数调用时，可以不更新 store。\n- 暴露出去的 actions 是 pure-model 里的动作，它可以是 get，也可以是 set，甚至是 noop 什么都不做。\n- 暴露出去的 actions 本质上是一组树形结构的函数集合\n\n```javascript\nlet model = createPureModel(() =\u003e {\n  let { store, actions } = setupStore({\n    initialState: 0,\n    reducers: {\n      incre: (state) =\u003e state + 1,\n      decre: (state) =\u003e state - 1,\n      increBy: (state, step = 1) =\u003e state + step,\n    },\n  })\n\n  actions.incre()\n  actions.decre()\n  actions.increBy(1)\n\n  // 支持构造异步 action\n  let asyncIncreBy2 = async () =\u003e {\n    await delay(1000)\n    actions.increBy(2)\n  }\n\n  // 支持将 actions 攒成对象形式。\n  let group = {\n    decreBy3: () =\u003e actions.increBy(-3),\n    decreBy4: () =\u003e actions.increBy(-4),\n  }\n\n  // 支持构造不会更新 store 的 action\n  // 相当于 redux 里的 selector\n  let getCount = () =\u003e {\n    return store.getState()\n  }\n\n  return {\n    store,\n    // 打包最后暴露的 actions 结构\n    actions: {\n      ...actions,\n      getCount,\n      asyncIncreBy2,\n      group,\n    },\n  }\n})\n```\n\n#### setupModel(Model)\n\n从 `1.3` 版本开始，支持通过 `setupModel` 访问另一个 `Model` 的实例。\n\n被访问者角色的 `Model` 的 `setupPreloadCallback` 将先于访问者角色的 `Model` 的 `setupPreloadCallback` 调用，因此可以在 `setupPreloadCallback` 中通过 `model.store.getState()` 访问到已预加载的数据。\n\n```ts\nimport { setupModel, setupStore, setupPreloadCallback, createPureModel } from '@pure-model/core'\n\n/**\n * 通用 Model\n */\nconst CommonModel = createPureModel(() =\u003e {\n  const { store, actions } = setupStore({\n    initialState: {\n      isApp: false,\n      isProd: false,\n    },\n    reducers: {\n      update: (state, newState) =\u003e {\n        return {\n          ...state,\n          ...newState,\n        }\n      },\n    },\n  })\n\n  setupPreloadCallback(async () =\u003e {\n    actions.update({\n      isProd: true,\n    })\n  })\n\n  return {\n    store,\n    actions,\n  }\n})\n\nconst CounterModel = createPureModel(() =\u003e {\n  const { store, actions } = setupStore({\n    initialState: 0,\n    reducers: {\n      setCount: (_, newCount) =\u003e count,\n    },\n  })\n\n  const commonModel = setupModel(CommonModel)\n\n  setupPreloadCallback(async () =\u003e {\n    const commonModelState = commonModel.store.getState()\n    if (commonModelState.isProd) {\n      // do something\n    }\n  })\n\n  return {\n    store,\n    actions,\n  }\n})\n```\n\n#### createModelContext(initialValue) \u0026 setupContext(ModelContext) \u0026 mergeModelContext(...ModelContxtValue[])\n\n`createModelContext` 和 `setupContext` 跟 react-hooks 的 `React.createContext` 和 `React.useContext` 类似。\n\n`createModelContext(initialValue)` 传递 `initialValue` 初始化的值，并返回一个 `ModelContext` 对象。\n\n`setupContext(MyModelContext)` 在 `initializer` 函数里，访问 `ModelContext` 内部的值。\n\n`createModelContext` 返回的 `ModelContext` 具有一下属性/方法\n\n##### MyModelContext.create(injectedValue)\n\n创建包含 `injectedValue` 的 `ModelContextValue` 对象，可传递给 `createPureModel(initializer, options)` 的第二个参数 `options.context` ， 动态的注入想要变更的 context value。\n\n如果不进行 context value injection 注入，`setupContext` 将会返回 `ModelContext` 的 `initialValue`\n\n可以通过 `mergeModelContext(...modelContextValueList)` 将多个 `model context value` 合并到一起，传递给 `options.context` 配置.\n\n`ModelContextValue` 和 `ModelContext` 不是同一个概念。\n\n`ModelContext` 相当于一个 `Factory` 工厂，可以通过 `ModelContext.create` 创建多个 `ModelContextValue`\n\n`ModelContextValue` 则是一个 `{ [MODEL_CONTEXT]: { key: value } }` 对象，`MODEL_CONTEXT` 这个 `symbol` 标记了该对象是一个 `model context value`。\n\n```typescript\nimport { createPureModel, createModelContext, setupContext, mergeModelContext  } from '@pure-model/core'\n\n// 定义 CounterContext 的类型\ntype CounterContextType = {\n  count: number\n}\n\n// 创建 model context 并传递 initialValue\nlet CounterContext = createModelContext\u003cCounterContextType\u003e({\n  count: 0\n})\n\n\nlet counter = createPureModel(\n  () =\u003e {\n    // 通过 setupContext 获取到 CounterContxt 包含的 value\n    // 当无注入时，用默认值 initialValue，有注入时，使用注入的 context value\n    let { count } = setupContext(CounterContext)\n    return setupCounter(count)\n  },\n  {\n    // 动态注入 context\n    // mergeModelContext 可以合并多个 context v\n    context: mergeModelContext(\n      CounterContext.create({\n        count: 200\n      })\n      AnotherContext.create(...)\n    )\n  }\n)\n```\n\n##### MyModelContext.impl(injectedValue)\n\n`impl` 方法和 `create` 方法类似，实际上 `create` 内部依赖的 `impl` 方法。\n\n差别在于，`impl` 返回的是 `{ key: value }` 结构，而 `create` 返回的是 `{ [MODEL_CONTEXT]: { key: value } }`，多了一层 `MODEL_CONTEXT`。\n\n`create` 方法返回的结构，可直接用以所有接收 `options.context` 的参数位置。\n\n`impl` 方法返回的结构，需要再构造一个 `MODEL_CONTEXT` 的包装结构，才能用以 `options.context`。\n\n`impl` 的用途通常是，将一个 `object` 或者 `class` 标记为 `ModelContextValue`。\n\n```typescript\nimport { MODEL_CONTEXT, createModelContext, setupContext } from '@pure-model/core'\n\n// 定义 CounterContext 的类型\ntype CounterContextType = {\n  count: number\n}\n\n// 创建 model context 并传递 initialValue\nlet CounterContext = createModelContext\u003cCounterContextType\u003e({\n  count: 0,\n})\n\nclass Counter {\n  constructor(count = 0) {\n    this.count = count\n  }\n\n  [MODEL_CONTEXT] = {\n    // 可以通过 object spread 将多个 context 的 context value 展开到一个对象里\n    // 相当于进行了 mergeModelContext 操作\n    ...CounterContext.impl({\n      count: this.count,\n    }),\n  }\n}\n\nlet counter = createPureModel(\n  () =\u003e {\n    // 通过 setupContext 获取到 CounterContxt 包含的 value\n    // 当无注入时，用默认值 initialValue，有注入时，使用注入的 context value\n    let { count } = setupContext(CounterContext)\n    return setupCounter(count)\n  },\n  {\n    // new Counter 的实例包含 MODEL_CONTEXT 这个 key，可以作为 ModelContextValue 注入\n    context: new Counter(10),\n  },\n)\n```\n\n#### setupPreloadCallback(listener)\n\n`setupPreloadCallback(listener)` 类似于 react-hooks 的 `useEffect(f)` 注册一个事件，`listener` 它会在 `model.preload()` 时被调用。\n\n正如 `preload` 一词所暗示的，它的用途是预加载数据，支持 `async/await`，在 model.store 被消费前进行数据加载。\n\n可以理解为 `next.js` 的 `getInitialProps` 的功能定位。\n\n`setupPreloadCallback(listener)` 可以被使用多次，以及在 `custom hooks` 里使用，跟 `react-hooks` 类似。\n\n```javascript\ncreateReactModel(() =\u003e {\n  // 预加载数据，\n  // 通常用以获取首屏数据，以及支持 SSR\n  setupPreloadCallback(async () =\u003e {\n    let data = await postJSON('/api', params)\n    actions.updateXXX(data)\n  })\n})\n```\n\n#### setupStartCallback(listener)\n\n`setupStartCallback(listener)` 在注册了 `model.start()` 事件，在 `pure-model` 跟 `react component` 进行绑定时，相当于 `componentDidMount` 的生命周期。\n\n`setupStartCallback(listener)` 可以被使用多次，以及在 `custom hooks` 里使用，跟 `react-hooks` 类似。\n\n```javascript\ncreateReactModel(() =\u003e {\n  // 在 model.store 被 react component 消费后，继续更新\n  // 通常用以获取非首屏数据\n  setupStartCallback(async () =\u003e {\n    let data = await postJSON('/api', params)\n    actions.updateXXX(data)\n  })\n})\n```\n\n#### setupFinishCallback(listener)\n\n`setupFinishCallback(listener)` 注册了 `model.finish()` 事件，在 `pure-model` 跟 `react component` 进行绑定时，相当于 `componentWillUnmount` 的生命周期。\n\n`setupFinishCallback(listener)` 可以被使用多次，以及在 `custom hooks` 里使用，跟 `react-hooks` 类似。\n\n```javascript\ncreateReactModel(() =\u003e {\n  let tid: any\n  setupStartCallback(() =\u003e {\n    tid = setInterval(() =\u003e {\n      console.log('interval')\n    }, 1000)\n  })\n\n  // 在 model 不需要被消费时，清除定时器\n  setupFinishCallback(() =\u003e {\n    clearInterval(tid)\n  })\n})\n```\n\n#### subscribe(model, listener)\n\n`subscribe(model, listener)` 监听 model 内部的 state，在 state change 时触发 listener(state)\n\n为什么不直接使用 `model.store.subscribe(listener)` 函数？\n\n这是因为，`subscribe(model, listener)` 保证在 `model.preload()` 之前不触发 `listener`。\n\n而 `model.store.subscribe(listener)` 能监听到 `store` 的所有状态变化。\n\n可以按照具体的场景，选择两种不同的方式。\n\n```javascript\nlet model = createPureModel(() =\u003e {\n  let { store, actions } = setupStore({\n    initialState: 0,\n    reducers: {\n      incre: (state) =\u003e state + 1,\n      decre: (state) =\u003e state - 1,\n    },\n  })\n\n  setupPreloadCallback(() =\u003e {\n    actions.incre()\n  })\n\n  return { store, actions }\n})\n\n/**\n * 触发两次\n * 一次是 setupPreloadCallback 里的 actions.incre\n * 另一次是 model.start 之后的 model.actions.incre()\n */\nmodel.store.subscribe(() =\u003e {\n  console.log('store.subscribe', model.store.getState())\n})\n\n/**\n * 触发一次\n * model.start 之后的 model.actions.incre()\n */\nsubscribe(model, (state) =\u003e {\n  console.log('subscribe', state)\n})\n\n// 先 preload，再 start，再 incre\nmodel.preload().then(() =\u003e {\n  model.start()\n  model.actions.incre()\n})\n```\n\n#### select(options)\n\n`select(options)` 类似于 `subscribe` 但可以更精细地监听 model 内部状态\n\n- `options.model` 为要监听的 model 对象\n- `options.selector` 为 state =\u003e value 的函数，从 state 中摘取部分状态\n- `options.listener(selectedState)` 为监听函数，接收 selector 函数返回的结果\n- `options.compare` 为对比函数，当两次 selector(state) 值相等时，不会重复触发 listener，默认是 shallowEqual 的浅对比。\n\n简单用例如下所示：\n\n```typescript\ntype State = {\n  a: number\n  b: number\n}\nlet model = createPureModel(() =\u003e {\n  let initialState: State = {\n    a: 0,\n    b: 1,\n  }\n\n  let increA = (state: State) =\u003e {\n    return {\n      ...state,\n      a: state.a + 1,\n    }\n  }\n  let increB = (state: State) =\u003e {\n    return {\n      ...state,\n      b: state.b + 1,\n    }\n  }\n\n  // 交换 a/b 字段的值\n  let swap = (state: State) =\u003e {\n    return {\n      ...state,\n      a: state.b,\n      b: state.a,\n    }\n  }\n\n  let { store, actions } = setupStore({\n    initialState,\n    reducers: {\n      increA,\n      increB,\n      swap,\n    },\n  })\n\n  return { store, actions }\n})\n\nlet list: number[] = []\n\nselect({\n  model,\n  selector: (state: State) =\u003e state.a + state.b,\n  listener: (value) =\u003e {\n    list.push(value)\n  },\n})\n\nawait model.preload()\n\nmodel.start()\n\n// 会引起 selector 的值的变化\nmodel.actions.increA()\n// 不会引起 selector 的值的变化\nmodel.actions.swap()\n// 会引起 selector 的值的变化\nmodel.actions.increB()\n// list 在 select 内只会收集到 2 次变化，swap 操作带来的变更被 compare 对比捕获和忽略\nexpect(list).toEqual([2, 3])\n```\n\n### react 组件适配 api\n\npure-model 提供了适配 react 组件的 api，可以将 model 里的 state 和 actions 用到 react component 里\n\n#### createReactModel(initializer)\n\n`createReactModel(initializer)` 跟 `createPureModel` 类似，只不过它不是立即创建 model，而是创建一个 react-hooks api。\n\n其中，`initializer` 跟 `createPureModel(initializer)` 的 initializer 参数一致。\n\n`createPureModel(initializer)` 返回 `ReactModel` 对象，包含以下内容\n\n- `ReactModel.isReactModel` 为 `true`\n- `ReactModel.useState()` 在 `function component` 中使用，获取 model 内部的 store.getState() 并监听其变化，自动刷新视图\n- `ReactModel.useActions()` 在 `function component` 中使用，获取 model 内部的 actions 对象\n- `ReactModel.Provider` 初始化 `ReactModel` 的 `Provider` 组件，在子组件里使用 `useState/useActions` 时，需要再其父级或者根组件里，挂载 `Model.Provider` 组件。除非通过其它适配器的方式自动注入了 Provider。该组件接收的 props 如下\n  - `props.context?` 注入 model context\n  - `props.preloadedState` 注入 model store 的 preloadedState 状态\n- `ReactModel.preload(context?, preloadedState?) -\u003e { Provider, model, state }` 预加载函数，接收可选的 context 和 preloadeState 参数，返回：\n  - `Provider` 为加载过 setupPreloadCallback 数据的 `Provider` 组件，可用以 SSR 渲染 yngy\n  - `model` 为实例化的 Model，可以访问 `store/actions/preload/start/finish` 等属性和方法\n  - `state` 为 `model.preload` 后的 `model.store.getState()`，可用以传递到客户端，进行 `ReactDOM.hydrate` 等复用处理。\n\n#### Provider 组件\n\n`Provider` 组件跟 `ReactModel.Provider` 相似，只不过它没有绑定任意 `ReactModel`，而是用以管理多个 `ReactModel` 的 `ReactModel.Provider`\n\n将多个 `ReactModel` 及其 `props`，打包成一个数组，`{ Model, context?, preloadedState? }[]`，`Provider` 组件会批量进行组装 `ReactModel.Provider`。\n\n```tsx\nimport { Provider } from '@pure-model/react'\n\nReactDOM.render(\n  \u003cProvider\n    list={[\n      { Model: ReactModel0, context: ModelContext0, preloadedState: 10 },\n      { Model: ReactModel1, context: ModelContext0, preloadedState: -10 },\n    ]}\n  \u003e\n    \u003cApp /\u003e\n  \u003c/Provider\u003e,\n  container,\n)\n```\n\n#### preload({ Model, context?, preloadedState? }[])\n\n`preload` 跟 `Provider` 的关系，类似于 `ReactModel.preload` 和 `ReactModel.Provider` 的关系，只是它们可以处理多个。\n\n- `preload()` 返回的 `Provider` 是已经组合了多个 `ReactModel.Provider` 的产物，可以直接使用。\n- `preload()` 返回的 `stateList` 组合了多个 state\n- `preload()` 返回的 `modelList` 组合了多个 model\n\n```javascript\nlet { Provider, stateList, modelList } = await preload([\n  { Model: ReactModel0, context: Context0, preloadedState: 10 },\n  { Model: ReactModel1, context: Context1, preloadedState: -10 },\n])\n```\n\n#### useReactModel(ReactModel, options?)\n\n`useReactModel(ReactModel, options?)` 用以在单个组件内实例化 `ReactModel`，而上面的方式是在 `Provider/ReactContext` 层面实例化，让子组件共享同一个 model。\n\n- `options` 参数等同于 `createPureModel(initializer, options?)` 的 `options` 参数，可以参考其文档\n- `useReactModel` 返回的值是 `[state, actions]`，即 `ReactModel.useState/ReactModel.actions` 组装到一起。\n\n```jsx\nconst Test = () =\u003e {\n  let [state, actions] = useReactModel(MyReactModel, {\n    context: MyModelContext,\n    preloadedState: myPreloadedState,\n  })\n}\n```\n\n### immer 适配\n\n`@pure-model/immer` 模块提供了 `immer` 适配的 api，可以优化更新 state 的方式\n\n- `toReducer` 将 immer reducer 转换成普通的 reducer 函数\n- `toReducers` 将 immer reducers 转换成普通的 reducers 对象\n\n```typescript\nimport { toReducers, Draft } from '@pure-model/immer'\ntype State = {\n  count: number\n}\n\nlet initialState: State = {\n  count: 10,\n}\n\nlet model = createPureModel(() =\u003e {\n  // immer reducer 的 state 为 Draft 对象，可以直接 mutable 修改\n  // toReducer 将 immer reducer 转换成普通 reducer，可以分配给 setupStore\n  let decre = toReducer((state: Draft\u003cState\u003e) =\u003e {\n    state.count--\n  })\n\n  // toReducers 将一组 immer reducers 转换成普通的 reducers 对象\n  let reducers = toReducers({\n    incre: (state: Draft\u003cState\u003e) =\u003e {\n      state.count++\n    },\n    increBy: (state: Draft\u003cState\u003e, step: number = 1) =\u003e {\n      state.count += step\n    },\n  })\n\n  let store = setupStore({\n    initialState,\n    // 普通 reducers 分配给 setupStore\n    reducers: {\n      ...reducers,\n      decre,\n    },\n  })\n\n  return store\n})\n```\n\n### http 接口请求 api\n\n`@pure-model/core` 提供了 `http` 接口交互相关的 api\n\n- `setupFetch() -\u003e fetch(url, options) -\u003e response` 获取到朴素的 `fetch` 方法，返回 `response` 对象，可自行调用 `text|json` 等方法。fetch 相关文档见：https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch\n- `setupGetJSON() -\u003e getJSON(url, query, options) -\u003e json` 获取到 `getJSON` 方法，发送 `GET` 请求，返回 `json` 结果。`query` 参数为对象，将序列化成 `a=1\u0026b=2` 形式，拼接到 `url` 的查询字符串参数中，`options` 同 `fetch(url, options)` 的 `options`\n- `setupPostJSON() -\u003e postJSON(url, data, options) -\u003e json` 获取到 `postJSON` 方法，发送 `POST` 请求，返回 `json` 结果。`data` 参数为对象，将被 `JSON.stringify` 序列化并作为 `post body` 发送给接口，`options` 同 `fetch(url, options)` 的 `options`\n\n`url` 参数的补全规则如下：\n\n- 当 url 为绝对路径时，直接使用 url\n- 当 url 缺失了协议时（如以 `//` 开头），在 node.js 里补全 `http:` 协议，其它场景补全 `https:`。\n\n```typescript\nimport {\n  // 获取内部绑定了 env 的 fetch 函数\n  setupFetch,\n  // 获取基于 fetch 函数构造的 getJSON 函数\n  setupGetJSON,\n  // 获取基于 fetch 函数构造的 postJSON 函数\n  setupPostJSON,\n} from '@pure-model/core'\n\nlet model = createPureModel(() =\u003e {\n  let fetch = setupFetch()\n  let getJSON = setupGetJSON()\n  let postJSON = setupPostJSON()\n\n  let getUserInfo = async () =\u003e {\n    let data = await getJSON('/api', { a: 1, b: 2 })\n  }\n\n  let postUserInfo = async (params) =\u003e {\n    let data = await postJSON('/api', params)\n  }\n\n  let fetchX = async () =\u003e {\n    let response = await fetch('url', {\n      method: 'POST',\n      body: JSON.stringify({ a: 1, b: 2 }),\n    })\n    let json = await response.json()\n  }\n})\n```\n\n### 测试辅助套件 api\n\n`@pure-model/test` 提供了方便测试 `setup*` 这类 Hooks 函数的 api\n\n`testHook(fn, context)` 接收 `fn` 函数和 `context` 两个参数，返回 `fn` 函数的返回值。\n\n```typescript\nimport { testHook } from '@pure-model/test'\n\n// 获取到在 EnvContext 注入的 context value 背景下的 Hooks 结果\nlet fetch = testHook(\n  () =\u003e {\n    let fetch = setupFetch()\n    return { fetch }\n  },\n  EnvContext.create({\n    env,\n    platform,\n    fetch,\n  }),\n)\n\n// 后续可以测试 fetch 方法啦。\nfetch()\n```\n\n### 其它 API\n\n#### setupCancel\n\nsetupCancel 可以将一个 task 函数，包装成可以 cancel 取消的形态。\n\n`setupCancel(task, options?)` -\u003e `{ start, cancel }` 。setupCancel 返回 start 函数和 cancel 函数，start 函数接收跟 task 函数一样的参数类型，cancel 函数无参数和返回值。\n\n- task 参数为一个异步函数，必须返回 promise\n- options 为可选参数，可以传递一些 callbacks\n  - `options.onData(data)` 监听 data 事件，data 为 task 函数返回的数据类型\n  - `options.onError(error)` 监听 error 事件，error 为 task 函数运行出错的 error 对象\n  - `options.onCancel()` 监听 cancel 事件，调用 cancel 函数时触发。\n  - `options.onStart()` 监听 start 事件，调用 start 函数时触发。\n  - `options.onFinish()` 监听 finish 事件，不管 task 运行是成功，还是失败，或者被取消，finish 事件都会触发。\n\n可以基于 `setupPostJSON` 和 `setupCancel` 实现可取消的请求处理。\n\n```javascript\nimport { setupPostJSON } from '@pure-model/core'\nimport { setupCancel } from '@pure-model/hooks'\n\nconst model = createPureModel(() =\u003e {\n  let postJSON = setupPostJSON()\n\n  let productFetcher = setupCancel(\n    async (params) =\u003e {\n      let data = await postJSON('api/to/product', params)\n      return data\n    },\n    {\n      onData: (data) =\u003e {\n        // 更新 product\n        actions.setProduct(data.products)\n      },\n      onError: (error) =\u003e {\n        // 更新 error\n        actions.setError(error.message)\n      },\n      onStart: () =\u003e {\n        // 展示 loading\n        actions.showLoading()\n      },\n      onFinish: () =\u003e {\n        // 关闭 Loading\n        actions.hideLoading()\n      },\n      onCancel: () =\u003e {\n        // 取消\n        console.log('cancel')\n      },\n    },\n  )\n\n  return {\n    store,\n    actions: {\n      ...actions,\n      productFetcher,\n    },\n  }\n})\n\n// 触发 onStart\nmodel.actions.productFetcher.start({\n  productId: 0,\n})\n\n// 触发 onCancel 和 onFinish\nmodel.actions.productFetcher.cancel()\n```\n\n#### setupSequence\n\n`setupSequence(task, options?)` -\u003e `wrapper task function` 将异步的 task 函数，包装成数据触发顺序和调用顺序一致的形态。\n\n`setupSequence(task, options?)` 返回新的函数，该函数接收的参数和返回值跟 task 一致。\n\n- `options.onData(data)` 监听 data 事件，data 为 task 的返回值\n- `options.onError(error)` 监听 error 事件，error 为 task 运行时抛出的错误对象\n\n基于 `setupSequence` 我们可以更加简单的实现异步任务的顺序控制。\n\n```javascript\nimport { setupPostJSON } from '@pure-model/core'\nimport { setupSequence } from '@pure-model/hooks'\n\nlet model = createPureModel(() =\u003e {\n  let postJSON = setupPostJSON()\n  let fetchProduct = setupSequence(\n    async (id) =\u003e {\n      let data = await postJSON('api/to/product', { id })\n      return data\n    },\n    {\n      onData: (data) =\u003e {\n        actions.addProduct(data.product)\n      },\n      onError: (error) =\u003e {\n        console.log('error', error)\n      },\n    },\n  )\n\n  return {\n    store,\n    actions: {\n      ...actions,\n      fetchProduct,\n    },\n  }\n})[\n  // 不管 1, 2, 3, 4 个请求谁先返回，onData 总是按照调用顺序 1, 2, 3, 4 触发\n  (1, 2, 3, 4)\n].forEach(model.actions.fetchProduct)\n```\n\n#### setupInterval\n\n`setupInterval(options?)` -\u003e `{ start(period: number), stop, reset }`\n\nsetupInterval 接收一组 callbacks，返回 start 启动定时器函数，stop 停止定时器函数，reset 重置定时器内部 count 状态函数。\n\n- `options.onData(n:number)` 监听定时器的 data 事件，参数 n 为数字，将从 0 开始递增（若 reset 函数被调用，n 重新从 0 开始递增）\n- `options.onStart()` 监听定时器的 start 事件，在 start 函数调用时触发\n- `options.onStop()` 监听定时器的 stop 事件，在 stop 函数调用时触发（若调用时，定时器未启动，则不触发）\n- `options.onReset()` 监听定时器的 reset 事件，在 reset 函数调用时触发（reset 事件不包含 stop，不会停止定时器，仅仅重置状态）\n\n对于 setupInterval 的返回值 `{ start(period: number), stop, reset }`，有：\n\n- `start(period: number)` 根据给定的 period 周期数字，启动定时器。两次调用 start 将取消上一次的定时器（但不触发 onStop）并按照最新的 period 进行计时。\n- `stop()` 停用定时器\n- `reset()` 重置定时器状态\n\n通过 setInterval() 我们可以更简单地实现轮询接口等功能，配合 `setupStartCallback` 和 `setupFinishCallback` 可以自动启动和停用定时器，跟随 model 的生命周期\n\n```javascript\nimport { setupStartCallback, setupFinfishCallback } from '@pure-model/core'\nimport { setupPostJSON } from '@pure-model/core'\nimport { setupInterval } from '@pure-model/hooks'\n\nlet model = createPureModel(() =\u003e {\n  let postJSON = setupPostJSON()\n\n  let { start, stop, reset } = setupInterval({\n    onData: (n) =\u003e {\n      console.log('data', n)\n    },\n    onStart: () =\u003e {\n      console.log('start')\n    },\n    onStop: () =\u003e {\n      console.log('stop')\n    },\n    onReset: () =\u003e {\n      console.log('reset')\n    },\n  })\n\n  // 在 model 开始时，启动定时器\n  setupStartCallback(() =\u003e {\n    start(1000)\n  })\n\n  // 在 model 生命周期结束时，关闭定时器\n  setupFinishCallback(stop)\n\n  return {\n    store,\n    actions: {\n      ...actions,\n      fetchProduct,\n    },\n  }\n})[\n  // 不管 1, 2, 3, 4 个请求谁先返回，onData 总是按照调用顺序 1, 2, 3, 4 触发\n  (1, 2, 3, 4)\n].forEach(model.actions.fetchProduct)\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flucifier129%2Fpure-model","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flucifier129%2Fpure-model","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flucifier129%2Fpure-model/lists"}