Angular 2
ngrx
Поиск…
Вступление
Полный пример: Вход / выход пользователя
Предпосылки
Эта тема не касается Redux и / или Ngrx:
- Вы должны быть довольны Redux
- По крайней мере, понять основы RxJs и наблюдаемого шаблона
Сначала давайте определим пример с самого начала и поиграем с некоторым кодом:
Как разработчик, я хочу:
- Имейте интерфейс
IUser
который определяет свойстваUser
- Объявите действия, которые мы будем использовать позже, чтобы манипулировать
User
вStore
- Определить начальное состояние
UserReducer
- Создайте редуктор
UserReducer
- Импортируйте наш
UserReducer
в наш основной модуль, чтобы создатьStore
- Используйте данные из
Store
для отображения информации в нашем представлении
Предупреждение о спойлере : если вы хотите попробовать демо сразу или прочитать код, прежде чем мы начнем, вот Plunkr ( встроенный просмотр или просмотр ).
1) Определить интерфейс IUser
Мне нравится разделить мои интерфейсы на две части:
- Свойства, которые мы получим с сервера
- Свойства, которые мы определяем только для UI (например, при нажатии кнопки)
И вот интерфейс IUser
мы будем использовать:
user.interface.ts
export interface IUser {
// from server
username: string;
email: string;
// for UI
isConnecting: boolean;
isConnected: boolean;
};
2) Объявить действия для манипулирования User
Теперь нам нужно подумать о том, какие действия должны выполнять наши редукторы .
Скажем здесь:
user.actions.ts
export const UserActions = {
// when the user clicks on login button, before we launch the HTTP request
// this will allow us to disable the login button during the request
USR_IS_CONNECTING: 'USR_IS_CONNECTING',
// this allows us to save the username and email of the user
// we assume those data were fetched in the previous request
USR_IS_CONNECTED: 'USR_IS_CONNECTED',
// same pattern for disconnecting the user
USR_IS_DISCONNECTING: 'USR_IS_DISCONNECTING',
USR_IS_DISCONNECTED: 'USR_IS_DISCONNECTED'
};
Но прежде чем мы будем использовать эти действия, позвольте мне объяснить, почему нам понадобится служба для отправки некоторых из этих действий для нас:
Скажем, что мы хотим подключить пользователя. Таким образом, мы будем нажимать кнопку входа в систему, и вот что произойдет:
- Нажмите на кнопку
- Компонент поймает событие и вызовет
userService.login
- Метод
userService.login
dispatch
событие для обновления нашего свойства хранилища:user.isConnecting
- Вызывается HTTP-вызов (мы будем использовать
setTimeout
в демо, чтобы имитировать поведение async ) - Как только
HTTP
вызов будет завершен, мы отправим другое действие, чтобы предупредить наш магазин о том, что пользователь зарегистрирован
user.service.ts
@Injectable()
export class UserService {
constructor(public store$: Store<AppState>) { }
login(username: string) {
// first, dispatch an action saying that the user's tyring to connect
// so we can lock the button until the HTTP request finish
this.store$.dispatch({ type: UserActions.USR_IS_CONNECTING });
// simulate some delay like we would have with an HTTP request
// by using a timeout
setTimeout(() => {
// some email (or data) that you'd have get as HTTP response
let email = `${username}@email.com`;
this.store$.dispatch({ type: UserActions.USR_IS_CONNECTED, payload: { username, email } });
}, 2000);
}
logout() {
// first, dispatch an action saying that the user's tyring to connect
// so we can lock the button until the HTTP request finish
this.store$.dispatch({ type: UserActions.USR_IS_DISCONNECTING });
// simulate some delay like we would have with an HTTP request
// by using a timeout
setTimeout(() => {
this.store$.dispatch({ type: UserActions.USR_IS_DISCONNECTED });
}, 2000);
}
}
3) Определите начальное состояние UserReducer
user.state.ts
export const UserFactory: IUser = () => {
return {
// from server
username: null,
email: null,
// for UI
isConnecting: false,
isConnected: false,
isDisconnecting: false
};
};
4) Создайте редуктор UserReducer
Редуктор принимает 2 аргумента:
- Текущее состояние
-
Action
типаAction<{type: string, payload: any}>
Напоминание: редуктор должен быть инициализирован в какой-то момент
Поскольку мы определили состояние по умолчанию нашего редуктора в части 3), мы сможем использовать его так:
user.reducer.ts
export const UserReducer: ActionReducer<IUser> = (user: IUser, action: Action) => {
if (user === null) {
return userFactory();
}
// ...
}
Надеюсь, есть более простой способ написать, что, используя нашу factory
функцию для возврата объекта и внутри редуктора, используйте значение параметров по умолчанию (ES6):
export const UserReducer: ActionReducer<IUser> = (user: IUser = UserFactory(), action: Action) => {
// ...
}
Затем нам нужно обработать все действия в нашем редукторе: СОВЕТ : используйте функцию ES6 Object.assign
чтобы сохранить наше состояние неизменным
export const UserReducer: ActionReducer<IUser> = (user: IUser = UserFactory(), action: Action) => {
switch (action.type) {
case UserActions.USR_IS_CONNECTING:
return Object.assign({}, user, { isConnecting: true });
case UserActions.USR_IS_CONNECTED:
return Object.assign({}, user, { isConnecting: false, isConnected: true, username: action.payload.username });
case UserActions.USR_IS_DISCONNECTING:
return Object.assign({}, user, { isDisconnecting: true });
case UserActions.USR_IS_DISCONNECTED:
return Object.assign({}, user, { isDisconnecting: false, isConnected: false });
default:
return user;
}
};
5) Импортируйте наш UserReducer
в наш основной модуль, чтобы создать Store
app.module.ts
@NgModule({
declarations: [
AppComponent
],
imports: [
// angular modules
// ...
// declare your store by providing your reducers
// (every reducer should return a default state)
StoreModule.provideStore({
user: UserReducer,
// of course, you can put as many reducers here as you want
// ...
}),
// other modules to import
// ...
]
});
6) Используйте данные из Store
для отображения информации на наш взгляд
Теперь все готово с логической стороны, и нам просто нужно отобразить то, что мы хотим, в двух компонентах:
-
UserComponent
: [Dumb component] Мы просто передадим объект пользователя из магазина с использованием свойства@Input
иasync
pipe. Таким образом, компонент получит пользователя только после его доступности (иuser
будет иметь типIUser
а не типObservable<IUser>
!) -
LoginComponent
[Интеллектуальный компонент] Мы будем непосредственно вводитьStore
в этот компонент и работать только сuser
в качествеObservable
.
user.component.ts
@Component({
selector: 'user',
styles: [
'.table { max-width: 250px; }',
'.truthy { color: green; font-weight: bold; }',
'.falsy { color: red; }'
],
template: `
<h2>User information :</h2>
<table class="table">
<tr>
<th>Property</th>
<th>Value</th>
</tr>
<tr>
<td>username</td>
<td [class.truthy]="user.username" [class.falsy]="!user.username">
{{ user.username ? user.username : 'null' }}
</td>
</tr>
<tr>
<td>email</td>
<td [class.truthy]="user.email" [class.falsy]="!user.email">
{{ user.email ? user.email : 'null' }}
</td>
</tr>
<tr>
<td>isConnecting</td>
<td [class.truthy]="user.isConnecting" [class.falsy]="!user.isConnecting">
{{ user.isConnecting }}
</td>
</tr>
<tr>
<td>isConnected</td>
<td [class.truthy]="user.isConnected" [class.falsy]="!user.isConnected">
{{ user.isConnected }}
</td>
</tr>
<tr>
<td>isDisconnecting</td>
<td [class.truthy]="user.isDisconnecting" [class.falsy]="!user.isDisconnecting">
{{ user.isDisconnecting }}
</td>
</tr>
</table>
`
})
export class UserComponent {
@Input() user;
constructor() { }
}
login.component.ts
@Component({
selector: 'login',
template: `
<form
*ngIf="!(user | async).isConnected"
#loginForm="ngForm"
(ngSubmit)="login(loginForm.value.username)"
>
<input
type="text"
name="username"
placeholder="Username"
[disabled]="(user | async).isConnecting"
ngModel
>
<button
type="submit"
[disabled]="(user | async).isConnecting || (user | async).isConnected"
>Log me in</button>
</form>
<button
*ngIf="(user | async).isConnected"
(click)="logout()"
[disabled]="(user | async).isDisconnecting"
>Log me out</button>
`
})
export class LoginComponent {
public user: Observable<IUser>;
constructor(public store$: Store<AppState>, private userService: UserService) {
this.user = store$.select('user');
}
login(username: string) {
this.userService.login(username);
}
logout() {
this.userService.logout();
}
}
Поскольку Ngrx
является слиянием концепций Redux
и RxJs
, RxJs
может быть трудно понять входы. Но это мощный шаблон, который позволяет вам, как мы видели в этом примере, иметь реактивное приложение, и вы можете легко поделиться своими данными. Не забывайте, что есть Plunkr, и вы можете разветвить его, чтобы сделать свои собственные тесты!
Надеюсь, это было полезно, даже если тема довольно длинная, ура!