Skip to content

Commit 52e39cd

Browse files
temper357alvinhui
authored andcommitted
支持 middleware & 增加官方调试 middleware (#27)
* feat: support middleware && add debug middleware * feat: add README of icestore-debug * feat: delete next in last middleware * fix: lint * fix: test * feat: improve test * fix: lint * feat: middleware API simplify * feat: update API according to PR review * fix: lint and test * feat: delete Proxy * fix: lint * feat: remove state diff * feat: update API according to latest PR review * chore: remove unused dependency * test: add action value * feat: split util functions to individual files * feat: support action return value from middleware * chore: remove unused util * fix: util path * feat: split util functions to individual files * feat: update README * feat: remove toJS in icestore-logger * feat: move middleware logic to store * fix: loading and error status * fix: PR review * feat: add test * fix: not throw error * feat: add test * feat: update README * feat: README fix
1 parent 51aaac5 commit 52e39cd

File tree

22 files changed

+716
-262
lines changed

22 files changed

+716
-262
lines changed

.eslintrc

Lines changed: 0 additions & 26 deletions
This file was deleted.

.eslintrc.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const { eslintTS, deepmerge } = require('@ice/spec');
2+
3+
module.exports = deepmerge(eslintTS, {
4+
env: {
5+
jest: true
6+
},
7+
rules: {
8+
"@typescript-eslint/explicit-member-accessibility": "off",
9+
"@typescript-eslint/explicit-function-return-type": "off",
10+
"@typescript-eslint/interface-name-prefix": 1,
11+
},
12+
settings: {
13+
"react": {
14+
"pragma": "React",
15+
"version": "detect"
16+
}
17+
},
18+
});

README.md

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ npm install @ice/store --save
2222

2323
`icestore` is a lightweight React state management library based on hooks. It has the following core features:
2424

25-
* Minimal API: Contains 3 APIs, which is easily learnable in 5 minutes.
25+
* Minimal API: Contains 5 APIs, which is easily learnable in 5 minutes.
2626
* Predictable: Uses unidirectional data flow (similar to Redux) and allows state mutation only inside actions, allowing data flow to be traced easily.
27-
* Optimal performance: Decreases the number of view components that rerender when the state changes by creating multiple stores. Rerendering only occurs when the current state is different from the previous state.
27+
* Optimal performance: Decreases the number of view components that rerender when the state changes by creating multiple stores.
2828
* Built in async status: Records loading and error status of async actions, simplifying the rendering logic in the view layer.
2929

3030
The data flow is as follows:
@@ -173,6 +173,17 @@ Register store config to the global store instance.
173173
* Return value
174174
- {object} store instance
175175

176+
### applyMiddleware
177+
178+
Apply middleware to all the store if the second parameter is not specified,
179+
otherwise apply middleware the store by namespace.
180+
181+
* Parameters
182+
- middlewares {array} middleware array to be applied
183+
- namespace {string} store namespace
184+
* Return value
185+
- void
186+
176187
### useStores
177188

178189
Hook to use multiple stores.
@@ -191,34 +202,15 @@ Hook to use a single store.
191202
* Return value
192203
- {object} single store instance
193204

194-
### toJS
205+
### getState
195206

196-
Recursively convert proxified state object to plain javaScript type.
207+
Get the latest state of individual store by namespace.
197208

198209
* Parameters
199-
- value {any} value of any javaScript type
210+
- namespace {string} store namespace
200211
* Return value
201-
- {any} javaScript value of any type
202-
203-
#### Example
212+
- {object} the latest state of the store
204213

205-
```javascript
206-
// store.js
207-
export default {
208-
value: {
209-
a: 1,
210-
b: 2,
211-
}
212-
};
213-
214-
// view.jsx
215-
import IceStore, { toJS } from '@ice/store';
216-
const { value } = useStore('foo');
217-
218-
const a = toJS(value);
219-
console.log(a);
220-
221-
```
222214
## Advanced use
223215

224216
### async actions' executing status

README.zh-CN.md

Lines changed: 151 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ $ npm install @ice/store --save
2222

2323
`icestore` 是基于 React Hooks 实现的轻量级状态管理框架,有以下核心特点:
2424

25-
* **极简 API**:只有 3 个 API,简单上手,使用方便,不需要学习 Redux 里的各种概念。
25+
* **极简 API**:只有 5 个 API,简单上手,使用方便,不需要学习 Redux 里的各种概念。
2626
* **React Hooks**:拥抱 Hooks 的使用体验,同时也是基于 React Hooks 实现。
2727
* **集成异步状态**:记录异步 action 的执行状态,简化 view 组件中对于 loading 与 error 状态的渲染逻辑。
28-
* **性能优化**:通过多 store 的去中心化设计,减少单个 state 变化触发重新渲染的组件个数,同时改变 state 时做 diff,进一步减少不必要的渲染
28+
* **性能优化**:通过多 store 的去中心化设计,减少单个 state 变化触发重新渲染的组件个数,从而减少不必要的渲染
2929
* **单向数据流**:与 Redux 一样使用单向数据流,便于状态的追踪与预测。
3030

3131
### 兼容性
@@ -137,11 +137,6 @@ ReactDOM.render(<Todo />, document.getElementById('root'));
137137
完整示例展示在这个 [sandbox](https://codesandbox.io/s/icestore-hs9fe)
138138

139139

140-
## Todos
141-
142-
- [ ] 增加调试工具
143-
- [ ] 支持 middleware
144-
145140
## 实现原理
146141

147142
`icestore` 数据流示意图如下:
@@ -171,7 +166,7 @@ ReactDOM.render(<Todo />, document.getElementById('root'));
171166

172167
### 不要在 action 之外直接修改 state
173168

174-
`icestore` 的架构设计中强制要求对state的变更只能在 action 中进行。在 action 之外的对 state的修改将直接 throw 错误。这个设计的原因是在 action 之外修改 state 将导致 state 变更逻辑散落在 view 中,变更逻辑将会难以追踪和调试。
169+
`icestore` 的架构设计中强制要求对 state 的变更只能在 action 中进行。在 action 之外的对 state 的修改不生效。这个设计的原因是在 action 之外修改 state 将导致 state 变更逻辑散落在 view 中,变更逻辑将会难以追踪和调试。
175170

176171
```javascript
177172
// store.js
@@ -214,6 +209,16 @@ ReactDOM.render(<Todo />, document.getElementById('root'));
214209
* 返回值
215210
- {object} store 实例
216211

212+
### applyMiddleware
213+
214+
给所有 store 或者指定 namespace 的 store 注册 middleware,如果不指定第 2 个参数,给所有 store 注册 middleware,如果指定第 2 个参数,则给指定 namespace 的 store 注册 middleware,详细用法见[注册方式](#注册方式)
215+
216+
* 参数
217+
- middlewares {array} 待注册的 middleware 数组
218+
- namespace {string} store 的命名空间
219+
* 返回值
220+
-
221+
217222
### useStores
218223

219224
同时使用多个 store 的 hook。
@@ -232,34 +237,14 @@ ReactDOM.render(<Todo />, document.getElementById('root'));
232237
* 返回值
233238
- {object} store 的配置对象
234239

235-
### toJS
240+
### getState
236241

237-
递归将 Proxy 化的 state 对象转化成普通的 javaScript 对象
242+
获取单个 store 的最新 state 对象。
238243

239244
* 参数
240-
- value {any} 任意 javaScript 类型值
245+
- namespace {string} store 的命名空间
241246
* 返回值
242-
- {any} 去 Proxy 后的 javaScript 类型
243-
244-
#### 示例
245-
246-
```javascript
247-
// store.js
248-
export default {
249-
value: {
250-
a: 1,
251-
b: 2,
252-
}
253-
};
254-
255-
// view.jsx
256-
import IceStore, { toJS } from '@ice/store';
257-
const { value } = useStore('foo');
258-
259-
const a = toJS(value);
260-
console.log(a);
261-
262-
```
247+
- {object} store 的 state 对象
263248

264249
## 高级用法
265250

@@ -319,6 +304,140 @@ return (
319304
);
320305
```
321306

307+
### 中间件
308+
309+
### 背景
310+
311+
如果你有使用过服务端的框架如 Express 或者 koa,应该已经熟悉了中间件的概念,在这些框架中,中间件用于在框架 `接收请求``产生响应` 间插入自定义代码,这类中间件的功能包含在请求未被响应之前对数据进行加工、鉴权,以及在请求被响应之后添加响应头、打印 log 等功能。
312+
313+
314+
在状态管理领域,Redux 同样实现了中间件的机制,用于在 `action 调用``到达 reducer` 之间插入自定义代码,中间件包含的功能有打印 log、提供 thunk 与 promise 异步机制、日志上报等。
315+
316+
317+
icestore 支持中间件的目的与 Redux 类似,也是为了在 action 调用前后增加一种扩展机制,增加诸如打印 log、埋点上报、异步请求封装等一系列能力,不同的是 icestore 已支持异步机制,因此不需要额外通过中间件方式支持。
318+
319+
### 中间件 API
320+
321+
在中间件 API 的设计上,`icestore` 借鉴了 koa 的 API,见如下:
322+
323+
```javascript
324+
async (ctx, next) => {
325+
// action 调用前逻辑
326+
327+
const result = await next();
328+
329+
// action 调用后逻辑
330+
331+
return result;
332+
}
333+
```
334+
335+
如果用户定义的 action 中有返回值,中间件函数必须将下一个中间件的执行结果返回,以保证中间件链式调用完成后能拿到 action 的返回值。
336+
337+
#### ctx API
338+
339+
对于中间件函数的第一个 ctx 参数,从上面能拿到当前的 store 与当前调用 action 的信息,ctx 对象中包含的详细参数如下:
340+
341+
* ctx.action - 当前调用的 action 对象
342+
* 类型:{object}
343+
* 默认值:无
344+
* ctx.action.name - 当前调用的 action 方法名
345+
* 类型:{string}
346+
* 默认值:无
347+
* ctx.action.arguments - 当前调用的 action 方法参数数组
348+
* 类型:{array}
349+
* 默认值:无
350+
* ctx.store - 当前 store 对象
351+
* 类型:{object}
352+
* 默认值:无
353+
* ctx.store.namespace - 当前 store 的 namespace
354+
* 类型:{string}
355+
* 默认值:无
356+
* ctx.store.getState - 获取当前 store 最新 state 的方法
357+
* 类型:{function}
358+
* 参数:无
359+
360+
调用方式如下:
361+
362+
```javascript
363+
const {
364+
action, // 当前调用的 action 对象
365+
store, // 当前 store 对象
366+
} = ctx;
367+
368+
const {
369+
name, // 当前调用的 action 方法名
370+
arguments, // 当前调用的 action 方法参数数组
371+
} = action;
372+
373+
const {
374+
namespace, // 当前 store namespace
375+
getState, // 获取当前 store state 方法
376+
} = store;
377+
```
378+
379+
### 注册方式
380+
381+
由于 `icestore` 的多 store 设计,`icestore` 支持给不同的 store 单独注册 middleware,
382+
方式如下:
383+
384+
1. 全局注册 middleware
385+
* 全局注册的 middleware 对所有 store 生效
386+
387+
```javascript
388+
import Icestore from '@ice/store';
389+
const stores = new Icestore();
390+
stores.applyMiddleware([a, b]);
391+
```
392+
393+
2. 指定 store 注册 middleware
394+
* store 上最终注册的 middleware 将与全局注册 middleware 做合并
395+
396+
```javascript
397+
stores.applyMiddleware([a, b]);
398+
stores.applyMiddleware([c, d], 'foo'); // store foo 中间件为 [a, b, c, d]
399+
stores.applyMiddleware([d, c], 'bar'); // store bar 中间件为 [a, b, d, c]
400+
```
401+
402+
## 调试
403+
404+
icestore 官方提供 logger 中间件,可以方便地跟踪触发 action 名以及 action 触发前后 state 的 diff 信息,提升问题排查效率。
405+
406+
### 使用方式
407+
408+
在注册 store 之前,使用 `applyMiddleware` 方法将 logger 中间件加入到中间件队列中
409+
410+
```javascript
411+
import todos from './todos';
412+
import Icestore from '@ice/store';
413+
import logger from '@ice/store-logger';
414+
415+
const icestore = new Icestore();
416+
417+
const middlewares = [];
418+
419+
// 线上环境不开启调试中间件
420+
if (process.env.NODE_ENV !== 'production') {
421+
middlewares.push(logger);
422+
}
423+
424+
icestore.applyMiddleware(middlewares);
425+
icestore.registerStore('todos', todos);
426+
```
427+
428+
注册成功后,当 `store` 中的 action 被调用时,在浏览器的 DevTools 中将能看到实时的日志:
429+
430+
<img src="https://user-images.githubusercontent.com/5419233/63344463-13184300-c383-11e9-96da-2de3b41f6e9b.png" width="250" />
431+
432+
日志中包含以下几个部分:
433+
434+
* Store Name: 当前子 store 对应的 namespace
435+
* Action Name: 当前触发的 action 名
436+
* Added / Deleted / Updated: state 变化的 diff
437+
* Old state: 更新前的 state
438+
* New state: 更新后的 state
439+
440+
322441
## 测试
323442

324443
由于所有的 state 和 actions 都封装在一个普通的 JavaScript 对象中,可以在不 mock 的情况下很容易的给 store 写测试用例。

examples/todos/package.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "todos",
3+
"version": "1.0.0",
4+
"private": true,
5+
"main": "src/index.js",
6+
"dependencies": {
7+
"@ice/store": "^0.2.x",
8+
"@ice/store-debugger": "^0.0.x",
9+
"react": "16.8.6",
10+
"react-dom": "16.8.6"
11+
},
12+
"devDependencies": {
13+
"ice-scripts": "^2.0.0"
14+
},
15+
"scripts": {
16+
"start": "ice-scripts dev",
17+
"build": "ice-scripts build",
18+
"test": "ice-scripts test"
19+
}
20+
}

examples/todos/public/index.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<!DOCTYPE html>
2+
<html>
3+
4+
<head>
5+
<meta charset="utf-8" />
6+
<meta http-equiv="x-ua-compatible" content="ie=edge,chrome=1">
7+
<meta name="viewport" content="width=device-width">
8+
<title>todos app</title>
9+
</head>
10+
11+
<body>
12+
<div id="ice-container"></div>
13+
</body>
14+
15+
</html>

0 commit comments

Comments
 (0)