1

Trong bài đăng này, chúng ta sẽ tìm hiểu cách các ứng dụng Angular 2 có thể được xây dựng theo kiểu Phản ứng chức năng bằng thư viện RxJs là một phần của Angular 2. Bài đăng này dựa trên Quản lý Nhà nước trong Ứng dụng Angular 2 của Victor Savkin ( @victorsavkin ), kiểm tra xem nó có phải là nguồn gốc của những ý tưởng này. Ngoài ra, có kho lưu trữ mẫu này với một ví dụ về cách ứng dụng có thể được xây dựng bằng cách sử dụng kiểu này. Chúng ta hãy đi qua các chủ đề sau:

  • Độ khó của trạng thái xử lý trong các ứng dụng trang đơn
  • Khi nào cần một kiến ​​trúc giống như thông lượng?
  • Xây dựng ứng dụng Angular 2 giống như Flux bằng RxJs
  • Hành động ứng dụng
  • Làm thế nào để xây dựng một bộ điều phối hành động trong RxJs
  • Xây dựng trạng thái ứng dụng có thể quan sát được
  • Tiêu thụ vật quan sát bằng cách sử dụng ống Async
  • Nơi sử dụng RxJs: Thành phần thông minh và tinh khiết
  • So sánh với Redux
  • Kết luận

Bài đăng này được đọc tốt hơn sau khi trải qua Lập trình phản ứng chức năng cho các nhà phát triển Angular 2 - RxJs và Observables , trong đó một số toán tử RxJ được sử dụng trong bài đăng này được trình bày.

Độ khó của trạng thái xử lý trong các ứng dụng trang đơn

Có lẽ không phải tất cả các ứng dụng trang đơn đều có vấn đề quản lý nhà nước lớn. Hãy tưởng tượng một ứng dụng CRUD đơn giản được sử dụng để quản lý dữ liệu tham chiếu cho các đặc quyền bảo mật.

Đối với loại ứng dụng trang đơn đó, có thể sử dụng Angular 2 Forms và / hoặc NgModel, nếu bạn thích, là một cách tiếp cận sẽ mang lại kết quả tốt với độ phức tạp thấp liên quan.

Nhưng, có những trường hợp sử dụng mà cách tiếp cận này được biết là thiếu. Ví dụ phổ biến nhất là vấn đề truy cập tin nhắn chưa đọc nổi tiếng trong Facebook dẫn đến việc tạo ra kiến ​​trúc Flux (xem bài nói chuyện gốc  ở đây).

Khi nào cần một kiến ​​trúc giống như Flux?

Là một trong những thành viên cốt lõi của React (Pete Hunt - @floydophone ) nói trong Hướng dẫn về React, có lẽ bạn không cần Flux ! Khi bạn cần nó, bạn sẽ biết. Nếu bạn có một trường hợp sử dụng như sau, có lẽ bạn cần Flux:

  • nhiều phần trong ứng dụng của bạn hiển thị cùng một dữ liệu khác nhau. Ví dụ: ứng dụng triển vọng hiển thị một thông báo trong danh sách và cập nhật bộ đếm thư mục của các tin nhắn chưa đọc
  • một số dữ liệu có thể được chỉnh sửa đồng thời bởi cả người dùng bằng giao diện người dùng hoặc các sự kiện đến từ phụ trợ thông qua việc đẩy máy chủ
  • bạn có nhu cầu hoàn tác / làm lại ít nhất một phần trạng thái ứng dụng

Những trường hợp sử dụng này thực sự không thường xuyên, tùy thuộc vào loại ứng dụng.

Một kiến ​​trúc cho các UI phức tạp

Hãy nghĩ về giao diện người dùng Netflix, nơi một bộ phim có khả năng xuất hiện trong nhiều danh sách. Nếu bạn thêm một bộ phim vào mục yêu thích của bạn, hãy cuộn xuống và xem lại bộ phim đó trong một danh sách khác, bạn muốn xem nó được đánh dấu là mục yêu thích. Một lần nữa, cùng một dữ liệu xuất hiện trong nhiều phần của UI và một hành động cần phải ảnh hưởng đến toàn bộ UI một cách nhất quán Đây là trường hợp sử dụng điển hình cho Flux.

Flux / Redux có phải là giải pháp duy nhất cho các trường hợp sử dụng như thế này không?

Ngoài ra còn có các tùy chọn khác ngoài nguyên tử duy nhất của tùy chọn trạng thái được trình bày trong phần còn lại của bài đăng này. Ví dụ: bạn có thể xây dựng ứng dụng xung quanh khái niệm dịch vụ dữ liệu quan sát được. Đó có lẽ chỉ là một phiên bản khác của Flux.

Tuy nhiên, giả sử bạn tình cờ gặp một trong những trường hợp này trong một phần của ứng dụng của mình: làm thế nào chúng ta có thể xây dựng một nguyên tử ứng dụng trạng thái giống như Redux trong Angular 2?

Xây dựng ứng dụng Flux Angular 2 bằng RxJs

Như trong các cách tiếp cận Flux khác, tất cả bắt đầu với các Tác vụ UI. Người dùng tương tác với trang và do đó, nhiều phần của ứng dụng muốn phản hồi tương tác đó.

Hành động ứng dụng

Một hành động là một thông báo xác định những gì đã xảy ra trong UI: Một Todo đã được thêm, xóa, bật. Để làm cho loại này an toàn, hãy tạo một lớp cho mỗi hành động:

export class AddTodoAction {
    constructor(public newTodo: Todo) {

    }
}

export class ToggleTodoAction {
    constructor(public todo: Todo) {

    }
}

Và sau đó, hãy xác định một loại kết hợp Kiểu chữ là một liên kết của tất cả các loại hành động được xác định:

export type Action = LoadTodosAction | AddTodoAction | ToggleTodoAction | DeleteTodoAction | StartBackendAction | EndBackendAction;

Như chúng ta có thể thấy, các Hành động chỉ là các POJO vận chuyển dữ liệu cần thiết cho các phần khác nhau của ứng dụng để tự điều chỉnh.

Nhưng, làm thế nào một hành động có thể được gửi đến nhiều phần của ứng dụng cần nó?

Sử dụng bộ điều phối hành động

Một hành động được gửi qua người điều phối hành động. Điều này được đưa vào bất kỳ nơi nào của ứng dụng cần gửi các hành động, điển hình là các thành phần giống như Bộ điều khiển hoặc Thông minh như TodoList trong ứng dụng mẫu:

export class TodoList {
    constructor(@Inject(dispatcher) private dispatcher: Observer<Action>) {
      ...
    }
}

Bạn có thể tự hỏi dispatchertên bên trong @Injectchú thích là gì, đây chỉ là tên mã thông báo để xác định một loại thuốc tiêm cụ thể, nhiều hơn về điều này sau. Điều quan trọng cần biết bây giờ là bộ điều phối có thể được sử dụng để gửi bất kỳ hành động nào đến phần còn lại của ứng dụng:

onToggleTodo(todo: Todo) {
    this.dispatcher.next(new ToggleTodoAction(todo));
    ...
}

Như chúng ta sẽ thấy sau, bộ điều phối được xây dựng trong một vài dòng RxJ. Nhưng, trước khi xem cách nó được xây dựng bên trong, hãy xem các phần khác của ứng dụng có thể phản ứng như thế nào với một hành động.

Xác định trạng thái ứng dụng

Hãy bắt đầu bằng cách xác định trạng thái ứng dụng trông như thế nào:

export interface ApplicationState {
    todos: List

   ,
    uiState: UiState
}

Như chúng ta có thể thấy, trạng thái ứng dụng bao gồm một danh sách các mục việc cần làm là dữ liệu của ứng dụng, cộng với một thể hiện của UiState. Hãy xem UiState:

export class UiState {
    constructor(public actionOngoing: boolean, public message:string) {
    }
}

UiStatechứa bất kỳ trạng thái nào trong UI ngoài dữ liệu. Ví dụ: thông báo hiện đang được hiển thị cho người dùng hoặc cờ cho biết nếu một số hành động đang diễn ra trong phần phụ trợ.

Giới thiệu trạng thái ứng dụng quan sát được

Bây giờ chúng ta đã biết trạng thái của ứng dụng trông như thế nào, chúng ta cần tưởng tượng một luồng bao gồm các trạng thái khác nhau mà ứng dụng có theo thời gian: trạng thái ứng dụng có thể quan sát được.

Khi các hành động mới được kích hoạt, luồng này sẽ phát ra các giá trị mới phản ánh kết quả của các hành động đó: todos được thêm, bật, xóa, v.v. Điều chúng tôi đang xem xét là:

let applicationStateObs: Observable<ApplicationState>;

Sau này chúng ta sẽ thấy làm thế nào chúng ta có thể tạo ra một thứ có thể quan sát như vậy ... nó sẽ chỉ mất một vài dòng RxJ. Hiện tại, hãy giả sử rằng trạng thái ứng dụng có thể quan sát được đã tồn tại và nó có thể được tiêm ở bất cứ đâu trong ứng dụng:

export class App {
    constructor(@Inject(state) private state: Observable<ApplicationState>) {
        ...
    }
}

Điều này có nghĩa là bất kỳ phần nào của ứng dụng muốn phản ứng với trạng thái mới đều có thể được tiêm và đăng ký vào nó. Phần đó của ứng dụng không biết hành động nào đã kích hoạt sự xuất hiện của nhà nước mới; nó chỉ biết rằng trạng thái mới đã đến, và quan điểm cần được cập nhật để phản ánh nó.

Cách sử dụng trạng thái ứng dụng có thể quan sát

Chúng ta có thể đăng ký trạng thái ứng dụng có thể quan sát được như bất kỳ quan sát nào khác. Hãy nói rằng chúng tôi muốn lấy danh sách các mã thông báo và chuyển nó vào mẫu. Chúng tôi có thể đăng ký vào biến thành viên trong danh sách todos có thể quan sát được:

state.subscribe(newState => this.todos = newState.todos );

Đây sẽ là một cách để làm điều đó, nhưng Angular 2 cung cấp một cách tốt hơn.

Tiêu thụ vật quan sát bằng cách sử dụng ống Async

Một cách khác để tiêu thụ vật quan sát là sử dụng asyncống Angular 2 . Ống này sẽ đăng ký để có thể quan sát và trả về kết quả cuối cùng của nó:

<ul id="todo-list">
    <li *ngFor="#todo of todos| async">
    ...
    </li>
</ul>

Ở đây chúng ta có thể thấy rằng danh sách các todos đến từ một todosquan sát. Điều này có thể quan sát được xác định thông qua một phương thức getter trong lớp trình điều khiển và được lấy từ trạng thái ứng dụng có thể quan sát được bằng cách sử dụng maptoán tử:

get todos() {
        return this.state.map((state: ApplicationState) => state.todos);
    }

Như chúng ta có thể thấy, thật đơn giản để xây dựng một ứng dụng Flux một khi bộ điều phối và trạng thái ứng dụng có thể quan sát được:

  • tiêm bộ điều phối bất cứ nơi nào một hành động cần phải được kích hoạt
  • tiêm trạng thái ứng dụng bất cứ nơi nào trong ứng dụng cần phản ứng với trạng thái ứng dụng mới

Bây giờ chúng ta hãy xem làm thế nào hai cấu trúc này có thể được xây dựng bằng RxJ.

Xây dựng một bộ điều phối hành động

Bộ điều phối thực sự chỉ là một xe buýt sự kiện truyền thống: chúng tôi muốn sử dụng nó để kích hoạt các sự kiện và muốn một phần của ứng dụng có thể đăng ký các hành động mà nó phát ra.

Để thực hiện điều này, cách đơn giản nhất là sử dụng Chủ đề RxJs, thực hiện cả giao diện Quan sát và Giao diện. Điều này có nghĩa là chúng tôi không chỉ có thể đăng ký Chủ đề mà còn sử dụng nó để phát ra các giá trị.

Làm cho thuốc tiêm

Người điều phối được tiêm bất cứ nơi nào trên ứng dụng cần một tên tiêm. Hãy bắt đầu bằng cách tạo tên như vậy bằng cách sử dụng OpaqueToken:

export const dispatcher = new OpaqueToken("dispatcher");

Mã thông báo này sau đó có thể được sử dụng để đăng ký Chủ thể trong hệ thống tiêm phụ thuộc Angular 2. Hãy thêm dòng này vào câu lệnh bootstrap của ứng dụng:

bootstrap(App, [
    ...
    provide(dispatcher, {useValue: new Subject

   ()}),
    ]);

Điều này có nghĩa là bất cứ khi nào hệ thống tiêm phụ thuộc được yêu cầu một cái gì đó được đặt tên dispatcher, Chủ đề này sẽ được tiêm.

Tránh súp sự kiện trong khi sử dụng bộ điều phối

Mặc dù người điều phối là một Chủ thể, tốt hơn hết là đánh dấu loại của nó khi tiêm chỉ là Người quan sát:

constructor(@Inject(dispatcher) private dispatcher: Observer

   ) {
    ...
}

Điều này là do chúng tôi thực sự chỉ muốn bộ điều phối được sử dụng trong mã ứng dụng để gửi các sự kiện, ví dụ:

this.dispatcher.next(new DeleteTodoAction(todo));

Chúng tôi muốn tránh hầu hết mã ứng dụng để vô tình đăng ký trực tiếp vào bộ điều phối, thay vì đăng ký trạng thái ứng dụng.

Có thể có các trường hợp sử dụng hợp lệ để đăng ký trực tiếp vào người điều phối, nhưng hầu hết thời gian này có thể không có ý định. Với bộ điều phối được xác định, chúng ta hãy xem làm thế nào chúng ta có thể xác định trạng thái ứng dụng có thể quan sát được.

Xây dựng trạng thái ứng dụng có thể quan sát bằng RxJs

Trước tiên, hãy xác định trạng thái ban đầu cho ứng dụng, một lần nữa sử dụng tên tiêm của initialState:

provide(initialState, {useValue: {todos: List([]), uiState: initialUiState}}),

Điều này có nghĩa là bất cứ khi nào tên initialStateđược yêu cầu tiêm, đối tượng được xác định ở trên được truyền vào: nó chứa một danh sách trống các mã thông báo và một số trạng thái UI không dữ liệu ban đầu.

Xác định trạng thái ứng dụng

Trạng thái ứng dụng có thể quan sát có thể được xây dựng như sau:

function applicationStateFactory(initialState, actions: Observable<Action>): Observable<ApplicationState> {
    ...
}

Trạng thái ứng dụng là một chức năng của cả trạng thái ban đầu và luồng hành động xảy ra theo thời gian:

  • giá trị đầu tiên của trạng thái có thể quan sát được là chính trạng thái ban đầu
  • sau đó hành động đầu tiên xảy ra và trạng thái ban đầu được chuyển đổi tương ứng và trạng thái quan sát được phát ra trạng thái mới
  • hành động tiếp theo được kích hoạt, một trạng thái mới được phát ra

Tính toán trạng thái ứng dụng mới

Mỗi trạng thái mới là kết quả của việc áp dụng hành động mới cho trạng thái trước đó, trông rất giống một reducehoạt động chức năng . Bước đầu tiên để tính toán trạng thái ứng dụng mới là xác định một loạt các hàm giảm, kiểu Redux. Ở đây chúng tôi có chức năng giảm tốc cho tất cả các hành động liên quan đến việc cần làm:

function calculateTodos(state: List

   , action) {
    if (!state) {
        return List([]);
    }
    if (action instanceof  LoadTodosAction) {
        return List(action.todos);
    }
    else if (action instanceof AddTodoAction) {
        return state.push(action.newTodo);
    }
    else if (action instanceof ToggleTodoAction) {
        return toggleTodo(state, action);
    }
    else if (action instanceof DeleteTodoAction) {
        let index = state.findIndex((todo) => todo.id === action.todo.id);
        return state.delete(index);
    }
    else {
        return state;
    }
}

Đây là một hàm giảm điển hình: lấy trạng thái và hành động, tính toán trạng thái tiếp theo của danh sách việc cần làm. Một hàm khử tương tự
calculateUiStatecó thể được định nghĩa cho UiStatemột phần của trạng thái ứng dụng (xem tại đây ).

Sử dụng các bộ giảm tốc để tạo ra một luồng trạng thái ứng dụng mới

Bây giờ chúng ta có các hàm giảm, chúng ta cần xác định luồng có thể quan sát mới, lấy luồng hành động và trạng thái ban đầu và tạo trạng thái mới.

Trong một bài viết trước , chúng tôi đã giới thiệu một số toán tử RxJ thường được sử dụng. Đã đến lúc sử dụng toán tử đầu tiên, scantoán tử. Toán tử này nhận một luồng và tạo một luồng mới cung cấp đầu ra của hàm giảm theo thời gian.

let appStateObservable = actions.scan( (state: ApplicationState, action) => {

   let newState: ApplicationState = {
       todos: calculateTodos(state.todos, action),
       uiState: calculateUiState(state.uiState, action)
   };

   return newState;

} , initialState);

Những gì chúng ta đang làm ở đây là lấy trạng thái hiện tại của ứng dụng, bắt đầu với trạng thái ban đầu và sau đó liên tục tính toán trạng thái mới là kết quả của trạng thái trước đó và chính hành động đó.

Đầu ra của scancũng là một quan sát có giá trị là một số trạng thái của ứng dụng theo thời gian.

Và đó là nó! Như các tài liệu Redux đã đề cập, một sự thay thế cho Redux là một vài dòng RxJ! Nhưng, vẫn còn một vài kết thúc lỏng lẻo mà chúng ta sẽ xem xét.

Ngăn chặn các chức năng giảm tốc được gọi nhiều lần cho một hành động

Trong khi gỡ lỗi một ứng dụng được xây dựng như thế này, bạn sẽ nhận thấy rằng nếu bạn thêm nhiều người tiêu dùng trạng thái ứng dụng (như nhiều asyncđường ống), các chức năng giảm tốc sẽ được nhấn nhiều lần, mỗi lần cho mỗi thuê bao.

Điều này là do mỗi mặc định có thể quan sát được sinh ra một chuỗi xử lý riêng biệt, xem bài viết trước để biết thêm chi tiết.

Mặc dù về cơ bản không có gì sai, vì đơn giản để gỡ lỗi, bạn có thể muốn đảm bảo rằng các chức năng giảm tốc chỉ được kích hoạt một lần cho mỗi hành động được gửi đi, như trong Redux.

Đối với điều này, chúng tôi giới thiệu toán tử RxJs thứ hai mà chúng tôi sẽ sử dụng, toán tử chia sẻ (xem tại đây để biết thêm về điều đó):

return appStateObservable.share();

Có một kết thúc lỏng lẻo khác, đó là cách sử dụng trạng thái ứng dụng có thể quan sát được trong kịch bản khởi động ứng dụng.

Đảm bảo rằng trạng thái ứng dụng có thể quan sát được có thể được sử dụng khi khởi động ứng dụng

Bạn có thể muốn tiêm trạng thái ứng dụng có thể quan sát được và sử dụng nó ở những nơi trong ứng dụng của bạn, nơi toàn bộ ứng dụng của bạn chưa được thiết lập hoàn toàn.

Ví dụ, tại thời điểm khởi động ứng dụng, như ở đây . Điều này sẽ không hoạt động theo cách bạn mong đợi vì có thể không phải tất cả các thuê bao đều có dây tại thời điểm giá trị trạng thái ứng dụng đầu tiên trả về.

Giải pháp cho việc này là làm cho trạng thái ứng dụng có thể quan sát được một luồng mà khi đăng ký luôn trả về giá trị cuối cùng, ngay cả khi nó được phát ra trong quá khứ trước khi đăng ký diễn ra.

Đối với điều này, chúng ta cần phải bọc trạng thái đơn giản có thể quan sát thành
BehaviourSubject:

function wrapIntoBehaviorSubject(init, obs) {
    const res = new BehaviorSubject(init);
    obs.subscribe(s => res.next(s));
    return res;
}

return wrapIntoBehaviorSubject(initialState, appStateObservable);

Điều này sẽ đảm bảo rằng các thuê bao sẽ luôn nhận được ít nhất giá trị trạng thái ban đầu.

Tất cả cùng nhau ngay bây giờ

Đây là một ví dụ hoạt động về cách xây dựng trạng thái ứng dụng có thể quan sát được (xem thêm ở đây ):

export function applicationStateFactory(initialState: ApplicationState, actions: Observable

   ): Observable

     {

    let appStateObservable = 
        actions.scan( (state: ApplicationState, action) => {

        console.log("Processing action " + action.getName());

        let newState: ApplicationState = {
          todos: calculateTodos(state.todos, action),
            uiState: calculateUiState(state.uiState, action)
        };

        console.log({todos: newState.todos.toJS(),uiState: newState.uiState});

        return newState;

    } , initialState).share();

    return wrapIntoBehaviorSubject(initialState, appStateObservable);
}

Chúng ta có thể sử dụng chức năng xuất xưởng này để tạo trạng thái ứng dụng có thể quan sát được theo cách sau:

bootstrap(App, [
...
provide(state, {useFactory: applicationStateFactory, deps: [new Inject(initialState), new Inject(dispatcher)]})
]);

Và, điều này kết thúc cách thức một trạng thái ứng dụng có thể quan sát được. Chúng ta hãy nhớ rằng một khi hệ thống ống nước ban đầu này được đưa ra, vấn đề thực sự là phải đưa bộ điều phối và trạng thái ứng dụng vào nơi chúng ta cần, và chủ yếu là viết các hàm giảm tốc mới.

Nơi sử dụng trạng thái và bộ điều phối - Thành phần thông minh so với nguyên chất

Nói chung, bộ điều phối và trạng thái quan sát được chỉ nên được tiêm trong các thành phần thông minh. Các thành phần đó không cần phải có bất kỳ biến trạng thái nào, vì chúng có thể tiêu thụ trạng thái có thể quan sát trực tiếp bằng cách sử dụng asyncđường ống.

Không phải ứng dụng không có trạng thái, mà là ứng dụng không có trạng thái. Trạng thái được quản lý bởi thư viện RxJs và không ở cấp độ của ứng dụng.

Các thành phần nguyên chất nên sử dụng bộ điều phối và trạng thái như thế nào?

Các thành phần thuần túy có thể nhận được các vật thể quan sát dưới dạng luồng đầu vào, nhưng chúng không nên được đưa vào bộ điều phối hoặc trạng thái có thể quan sát được, vì điều này sẽ liên kết chúng với ứng dụng cụ thể này, khiến chúng không thể tái sử dụng.

Nếu một thành phần thuần túy muốn gửi một hành động, thay vào đó, nó đưa ra một sự kiện thông qua EventEmittervà đó là thành phần thông minh sẽ đăng ký vào trình phát sự kiện đó và trong phản hồi gửi một hành động.

So sánh ứng dụng Xây dựng góc 2 với Redux

Hãy nhớ rằng chúng ta sẽ cần phải biết RxJ để sử dụng Angular 2 vì Observables là một phần của API Angular 2 cho những thứ như Biểu mẫu hoặc HTTP.

Và, theo các tài liệu của Redux , khái niệm tương tự như Redux có thể được triển khai trong RxJs bằng cách sử dụng toán tử quét và một vài toán tử nữa, như chúng ta vừa thấy. Ngoài ra, RxJs đã được xuất xưởng với Angular 2.

Vì vậy, nó có vẻ có ý nghĩa khi gặp các trường hợp sử dụng yêu cầu Flux thực hiện chúng trong RxJ thay vì Redux. Sử dụng triển khai Redux được thực hiện trong RxJs như ngrx cũng là một cách hay.

Cuối cùng, các khái niệm vững chắc xung quanh Redux quan trọng hơn chính thư viện.

Kết luận

Vẫn còn sớm để cộng đồng Angular 2 biết ứng dụng Angular 2 điển hình sẽ trông như thế nào và trạng thái sẽ được xử lý như thế nào.

Với tất cả các tùy chọn này, thật dễ dàng để đánh mất một cái gì đó quan trọng. Chắc chắn có nhiều cách để làm các ứng dụng giống Flux trong Angular 2, nhưng câu hỏi chính là chúng ta có thực sự cần một kiến ​​trúc giống Flux cho toàn bộ ứng dụng của mình không? Hoặc, chỉ cho một màn hình nhất định hoặc trường hợp sử dụng nâng cao hơn?

Đó là một câu hỏi đáng được hỏi khi thiết kế kiến ​​trúc của một ứng dụng cụ thể, vì câu trả lời có thể phụ thuộc vào ứng dụng.

Một cách tốt để đánh giá xem Flux có lợi hay không là hướng dẫn React How-To , câu hỏi này cũng có một số thông tin và bài nói chuyện Flux gốc .

|