Skip to content

Commit

Permalink
支持 middleware & 增加官方调试 middleware (#27)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
temper357 authored and alvinhui committed Aug 22, 2019
1 parent 51aaac5 commit 52e39cd
Show file tree
Hide file tree
Showing 22 changed files with 716 additions and 262 deletions.
26 changes: 0 additions & 26 deletions .eslintrc

This file was deleted.

18 changes: 18 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const { eslintTS, deepmerge } = require('@ice/spec');

module.exports = deepmerge(eslintTS, {
env: {
jest: true
},
rules: {
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/interface-name-prefix": 1,
},
settings: {
"react": {
"pragma": "React",
"version": "detect"
}
},
});
42 changes: 17 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ npm install @ice/store --save

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

* Minimal API: Contains 3 APIs, which is easily learnable in 5 minutes.
* Minimal API: Contains 5 APIs, which is easily learnable in 5 minutes.
* Predictable: Uses unidirectional data flow (similar to Redux) and allows state mutation only inside actions, allowing data flow to be traced easily.
* 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.
* Optimal performance: Decreases the number of view components that rerender when the state changes by creating multiple stores.
* Built in async status: Records loading and error status of async actions, simplifying the rendering logic in the view layer.

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

### applyMiddleware

Apply middleware to all the store if the second parameter is not specified,
otherwise apply middleware the store by namespace.

* Parameters
- middlewares {array} middleware array to be applied
- namespace {string} store namespace
* Return value
- void

### useStores

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

### toJS
### getState

Recursively convert proxified state object to plain javaScript type.
Get the latest state of individual store by namespace.

* Parameters
- value {any} value of any javaScript type
- namespace {string} store namespace
* Return value
- {any} javaScript value of any type

#### Example
- {object} the latest state of the store

```javascript
// store.js
export default {
value: {
a: 1,
b: 2,
}
};

// view.jsx
import IceStore, { toJS } from '@ice/store';
const { value } = useStore('foo');

const a = toJS(value);
console.log(a);

```
## Advanced use

### async actions' executing status
Expand Down
183 changes: 151 additions & 32 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ $ npm install @ice/store --save

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

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

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


## Todos

- [ ] 增加调试工具
- [ ] 支持 middleware

## 实现原理

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

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

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

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

### applyMiddleware

给所有 store 或者指定 namespace 的 store 注册 middleware,如果不指定第 2 个参数,给所有 store 注册 middleware,如果指定第 2 个参数,则给指定 namespace 的 store 注册 middleware,详细用法见[注册方式](#注册方式)

* 参数
- middlewares {array} 待注册的 middleware 数组
- namespace {string} store 的命名空间
* 返回值
-

### useStores

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

### toJS
### getState

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

* 参数
- value {any} 任意 javaScript 类型值
- namespace {string} store 的命名空间
* 返回值
- {any} 去 Proxy 后的 javaScript 类型

#### 示例

```javascript
// store.js
export default {
value: {
a: 1,
b: 2,
}
};

// view.jsx
import IceStore, { toJS } from '@ice/store';
const { value } = useStore('foo');

const a = toJS(value);
console.log(a);

```
- {object} store 的 state 对象

## 高级用法

Expand Down Expand Up @@ -319,6 +304,140 @@ return (
);
```

### 中间件

### 背景

如果你有使用过服务端的框架如 Express 或者 koa,应该已经熟悉了中间件的概念,在这些框架中,中间件用于在框架 `接收请求``产生响应` 间插入自定义代码,这类中间件的功能包含在请求未被响应之前对数据进行加工、鉴权,以及在请求被响应之后添加响应头、打印 log 等功能。


在状态管理领域,Redux 同样实现了中间件的机制,用于在 `action 调用``到达 reducer` 之间插入自定义代码,中间件包含的功能有打印 log、提供 thunk 与 promise 异步机制、日志上报等。


icestore 支持中间件的目的与 Redux 类似,也是为了在 action 调用前后增加一种扩展机制,增加诸如打印 log、埋点上报、异步请求封装等一系列能力,不同的是 icestore 已支持异步机制,因此不需要额外通过中间件方式支持。

### 中间件 API

在中间件 API 的设计上,`icestore` 借鉴了 koa 的 API,见如下:

```javascript
async (ctx, next) => {
// action 调用前逻辑

const result = await next();

// action 调用后逻辑

return result;
}
```

如果用户定义的 action 中有返回值,中间件函数必须将下一个中间件的执行结果返回,以保证中间件链式调用完成后能拿到 action 的返回值。

#### ctx API

对于中间件函数的第一个 ctx 参数,从上面能拿到当前的 store 与当前调用 action 的信息,ctx 对象中包含的详细参数如下:

* ctx.action - 当前调用的 action 对象
* 类型:{object}
* 默认值:无
* ctx.action.name - 当前调用的 action 方法名
* 类型:{string}
* 默认值:无
* ctx.action.arguments - 当前调用的 action 方法参数数组
* 类型:{array}
* 默认值:无
* ctx.store - 当前 store 对象
* 类型:{object}
* 默认值:无
* ctx.store.namespace - 当前 store 的 namespace
* 类型:{string}
* 默认值:无
* ctx.store.getState - 获取当前 store 最新 state 的方法
* 类型:{function}
* 参数:无

调用方式如下:

```javascript
const {
action, // 当前调用的 action 对象
store, // 当前 store 对象
} = ctx;

const {
name, // 当前调用的 action 方法名
arguments, // 当前调用的 action 方法参数数组
} = action;

const {
namespace, // 当前 store namespace
getState, // 获取当前 store state 方法
} = store;
```

### 注册方式

由于 `icestore` 的多 store 设计,`icestore` 支持给不同的 store 单独注册 middleware,
方式如下:

1. 全局注册 middleware
* 全局注册的 middleware 对所有 store 生效

```javascript
import Icestore from '@ice/store';
const stores = new Icestore();
stores.applyMiddleware([a, b]);
```

2. 指定 store 注册 middleware
* store 上最终注册的 middleware 将与全局注册 middleware 做合并

```javascript
stores.applyMiddleware([a, b]);
stores.applyMiddleware([c, d], 'foo'); // store foo 中间件为 [a, b, c, d]
stores.applyMiddleware([d, c], 'bar'); // store bar 中间件为 [a, b, d, c]
```

## 调试

icestore 官方提供 logger 中间件,可以方便地跟踪触发 action 名以及 action 触发前后 state 的 diff 信息,提升问题排查效率。

### 使用方式

在注册 store 之前,使用 `applyMiddleware` 方法将 logger 中间件加入到中间件队列中

```javascript
import todos from './todos';
import Icestore from '@ice/store';
import logger from '@ice/store-logger';

const icestore = new Icestore();

const middlewares = [];

// 线上环境不开启调试中间件
if (process.env.NODE_ENV !== 'production') {
middlewares.push(logger);
}

icestore.applyMiddleware(middlewares);
icestore.registerStore('todos', todos);
```

注册成功后,当 `store` 中的 action 被调用时,在浏览器的 DevTools 中将能看到实时的日志:

<img src="https://user-images.githubusercontent.com/5419233/63344463-13184300-c383-11e9-96da-2de3b41f6e9b.png" width="250" />

日志中包含以下几个部分:

* Store Name: 当前子 store 对应的 namespace
* Action Name: 当前触发的 action 名
* Added / Deleted / Updated: state 变化的 diff
* Old state: 更新前的 state
* New state: 更新后的 state


## 测试

由于所有的 state 和 actions 都封装在一个普通的 JavaScript 对象中,可以在不 mock 的情况下很容易的给 store 写测试用例。
Expand Down
20 changes: 20 additions & 0 deletions examples/todos/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "todos",
"version": "1.0.0",
"private": true,
"main": "src/index.js",
"dependencies": {
"@ice/store": "^0.2.x",
"@ice/store-debugger": "^0.0.x",
"react": "16.8.6",
"react-dom": "16.8.6"
},
"devDependencies": {
"ice-scripts": "^2.0.0"
},
"scripts": {
"start": "ice-scripts dev",
"build": "ice-scripts build",
"test": "ice-scripts test"
}
}
15 changes: 15 additions & 0 deletions examples/todos/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge,chrome=1">
<meta name="viewport" content="width=device-width">
<title>todos app</title>
</head>

<body>
<div id="ice-container"></div>
</body>

</html>
Loading

0 comments on commit 52e39cd

Please sign in to comment.