官网地址: https://ngrx.io/docs
@ngrx/store
- Single source of truth(单一状态对象)
这个原则是整个单页应用的状态通过object tree(对象树)的形式存储在store 里面。这个定义十分抽象其实就是把所有需要共享的数据通过javascript 对象的形式存储下来
安装@ngrx/store
npm install @ngrx/store or yarn add @ngrx/store
配置
创建 state, action, reducer
import { Action } from '@ngrx/store';
export interface IAction extends Action {
payload?: any; // dispatch数据载体,可有可无,继承Action的type属性
}
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export const RESET = 'RESET';
const initialState = 0;
export function counterReducer(state: number = initialState, action: IAction) {
switch (action.type) {
case INCREMENT:
return state + 1;
case DECREMENT:
return state - 1;
case RESET:
return 0;
default:
return state;
}
}
注册store
import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { counterReducer } from './counter';
@NgModule({
imports: [
StoreModule.forRoot({ count: counterReducer }), // 注册store
],
})
export class AppModule {}
使用store
在组件或服务中注入store进行使用
// 组件级别
import { Component } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { Observable } from 'rxjs';
import { INCREMENT, DECREMENT, RESET } from './counter';
interface AppState {
count: number;
}
@Component({
selector: 'app-my-counter',
template: `
<button (click)="increment()">Increment</button>
<div>Current Count: {{ count$ | async }}</div>
<button (click)="decrement()">Decrement</button>
<button (click)="reset()">Reset Counter</button>
`,
})
export class MyCounterComponent {
count$: Observable<number>;
constructor(private store: Store<AppState>) { // 注入store
this.count$ = store.pipe(select('count')); // 从app.module.ts中获取count状态流
}
increment() {
this.store.dispatch({ type: INCREMENT });
}
decrement() {
this.store.dispatch({ type: DECREMENT });
}
reset() {
this.store.dispatch({ type: RESET });
}
}
ngrx/effects
目的:
为了抽取组件中的多余动作,让组件更单纯,复用性更好
方法:
为了达到以上目的,我们要先分辨出,“多余”的动作是什么?—什么是多余的 什么就是effect
说具体点儿,就是组件中 调取后台service部分的代码。
因为如果我们将service写在组件中,就会将此代码的复用性降低了,比如你在其他项目还是想利用此组件,但是另一个组件的调用后台的名称或方法并不一致,导致了组件没办法直接复用。
而有了effect的理念,我们将service方法直接抽离出来,根本不在组件中体现,这样组件就是一个很笨的组件,功能只涉及自己的业务,其他的都不管了。这种编程思路的根源是把所有的应用(或者组件)的逻辑想象成一个纯粹的对数据进行处理的函数(和外界的读写操作-- 这些读写操作就叫 Effects --都不属于这个函数的职责)以及一系列外部的读、写驱动构成。
function main(){
// 逻辑部分
var a = 2;
var b = 10;
var result = a * b;
// 写入 console 的 Effect
console.log('result is: ' + result);
// 操作 DOM 的 Effect
var resultElement = document.getElementById('result');
resultElement.textContent = result;
}
上面这段简单代码中前3行是代码的主要逻辑,接下来的几行代码都对外部世界产生了影响,所以他们都是 Effects ( Effect 这个词其实挺头疼,不知道中文那个词能比较形象的对应,“影响”感觉还是不到位)。那么我们接下来按照上面提到原则来改写这部分代码:逻辑部分不涉及任何对外部世界的影响。
// 程序的主体逻辑完全剥离 Effects,只是对数据做处理
function main(){
var a = 2;
var b = 10;
var result = a * b;
return {
DOM: result,
log: result
};
}
// 对于 Console 的影响写在这里
function logEffect(result){
console.log('result is: ' + result);
}
// 对于 DOM 的影响写在这里
function domEffect(result){
var resultElement = document.getElementById('result');
resultElement.textContent = result;
}
// 如何让数据和 effects 连接起来,这是一个粘合剂
function run(mainFn){
var sink = mainFn();
logEffect(sink.log);
domEffect(sink.DOM);
}
run(main);
状态、 Action 流 和 Effect
Redux 中的 Reducer 已经是一个纯函数,而且是完全的只对状态数据进行处理的纯函数。那么对于我们前面说的原则,Reducer 已经满足了。在发出某个 Action 之后,Reducer 会对状态数据进行处理然后返回。但一般来说,其实在执行 Action 后我们还是经常会可以称为 Effect 的动作,比如:进行 HTTP 请求,导航,写文件等等。而这些事情恰恰是 Redux 本身无法解决的,所以才有了诸如 Redux-Thunk 等中间件的产生。下面我们一起看看如何使用 @ngrx/effects 解决这个问题。
还是举一个小例子,比如登录注册这种经常用到的鉴权流程,我们一般有如下 Action :LOGIN、LOGIN_SUCCESS、LOGIN_FAIL、REGISTER、REGISTER_SUCCESS、REGISTER_FAIL 和 LOGOUT。
先拿 LOGIN 来说,我们希望流程是这个样子的:发出 LOGIN Action --> 使用登录 service 进行登录鉴权 --> 如果成功,发送 LOGIN_SUCCESS Action,如果失败,发送 LOGIN_FAIL Action。按原来的做法,我们至少需要在组件中的某个位置调用 service 进行 HTTP 请求,组件或者服务在 response 返回后决定发送 LOGIN_SUCCESS 或 LOGIN_FAIL 。
如果应用我们上面提到的 Effect 的概念,其实 Reducer 已经扮演了纯数据处理函数的角色,而 Action 在 @ngrx/effects 中是一个信号流,它扮演的是连接状态和要做的 Effect 中的粘合剂,就像上面代码中的 function run(mainFn) 一样。
@Injectable()
export class AuthEffects{
// 通过构造注入需要的服务和 action 信号流
constructor(private actions$: Actions, private authService: AuthService) { }
//用 @Effect() 修饰器来标明这是一个 Effect
@Effect()
login$: Observable<Action> = this.actions$ // action 信号流
.ofType(authActions.ActionTypes.LOGIN) // 如果是 LOGIN Action
.map(toPayload) // 转换成 action 的 payload 数据流
.switchMap((val:{username:string, password: string}) => {
// 调用服务
return this.authService.login(val.username, val.password);
})
// 如果成功发出 LOGIN_SUCCESS Action 交给其它 Effect 或者 Reducer 去处理
.map(user => new authActions.LoginSuccessAction({user: user}))
// 如果失败发出 LOGIN_FAIL Action 交给其它 Effect 或者 Reducer 去处理
.catch(err => of(new authActions.LoginFailAction(err.json())));
}
你可能会问,如果我们需要登录成功后导航到 /home 呢?导航也是effect,而 actions$ 是一个信号流,所以你完全可以定义一个 effect 监听 LOGIN_SUCCESS ,捕获到后就进行导航即可
@Effect()
navigateHome$: Observable<Action> = this.actions$
.ofType(actions.ActionTypes.LOGIN_SUCCESS)
.map(() => go(['/home']));
这样的话,其实组件都没有必要调用 Service 了,只需发出信号就好。
onSubmit({value, valid}){
if(!valid) return;
this.store$.dispatch(
new authActions.LoginAction({
username: value.username,
password: value.password
}));
}
那更复杂一些怎么办?比如我们登录后需要取得该登录用户的待办事项列表,那我们照猫画虎但写到 return this.todoService.getTodos(auth.user.id); 发现还需要访问 auth 啊,怎么破?
@Effect()
loadTodos$: Observable<Action> = this.actions$
.ofType(todoActions.ActionTypes.LOAD_TODOS)
.map(toPayload)
.switchMap(() => {
return this.todoService.getTodos(auth.user.id); // 这个auth怎么得到啊?
})
.map(todos => new todoActions.LoadTodosSuccessAction(todos))
.catch(err => of(new todoActions.LoadTodosFailAction(err.json())));
别忘了,ngrx 是基于 rxjs 的,非常善于合并和操作流,而 store 也是一个流,那就非常好办了,我们只需在 store 取得 auth 的最新值,然后合并这两个流就好了:
@Effect()
loadTodos$: Observable<Action> = this.actions$
.ofType(todoActions.ActionTypes.LOAD_TODOS)
.map(toPayload)
.withLatestFrom(this.store$.select('auth'))
.switchMap(([_, auth]) => {
return this.todoService.getTodos(auth.user.id);
})
.map(todos => new todoActions.LoadTodosSuccessAction(todos))
.catch(err => of(new todoActions.LoadTodosFailAction(err.json())));
这么做的理由
就是组件只是在dispatch action,而具体发生了什么,这个组件根本不关心。
这样做的好处
可以想象,你和队员合作,他可以独立去开发effect,你来写组件,你们的对接口就是action,具体实现已经分离。
RXJS
RxJS 是使用 Observables 的响应式编程的库,它使编写异步或基于回调的代码更容易
中文文档地址: https://cn.rx.js.org/
ramda
一款实用的 JavaScript 函数式编程库。
Ramda 主要特性如下:
Ramda 强调更加纯粹的函数式风格。数据不变性和函数无副作用是其核心设计理念。这可以帮助你使用简洁、优雅的代码来完成工作。
Ramda 函数本身都是自动柯里化的。这可以让你在只提供部分参数的情况下,轻松地在已有函数的基础上创建新函数。
Ramda 函数参数的排列顺序更便于柯里化。要操作的数据通常在最后面。
最后两点一起,使得将多个函数构建为简单的函数序列变得非常容易,每个函数对数据进行变换并将结果传递给下一个函数。Ramda 的设计能很好地支持这种风格的编程
ramda文档地址: http://ramda.cn/docs/#compose