[Angular 2 – Meteor]. 2. Thao tác và xử lý dữ liệu trong Angular 2 Meteor (sử dụng RxJS – MongoObservable).

Với các ứng dụng thì ngoài việc xây dựng View thì việc còn lại của chúng ta là xử lý logic với dữ liệu, và trong Meteor thuần, chúng ta sẽ phải sử dụng Mongo Collection để tạo các Collection để lưu trữ dữ liệu. Tương tự như bảng dữ liệu trong SQL thì ở NoSQL sẽ là Collection. Trong bài viết này chúng ta sẽ tìm hiểu về tương tác dữ liệu trong Angular 2 Meteor, và sử dụng những khái niệm mới như là RxJS (Observable) trong xử lý dữ liệu.

Ở Angular 2, nhóm phát triển kết hợp khá nhiều các công nghệ mới để càng ngày càng hoàn thiện Meteor hơn, và việc sử dụng RxJS trong thao tác dữ liệu cũng không ngoại lệ. Theo như những thông tin mình mới hóng được, thì Apolo là một khái niệm mới mà Meteor sắp đưa vào sử dụng, đó chính là GraphQL, được phát triển bởi Facebook và hỗ trợ thao tác trên nhiều hệ quản trị CSDL khác nhau. Chi tiết hơn chúng ta sẽ nghiên cứu thêm trong các bài viết tới và khi Meteor team đem Apolo ra sử dụng một cách chính thức. Quay lại với bài viết hôm nay, chúng ta sẽ tập trung tìm hiểu về một khái niệm gọi là 3-way data binding và sử dụng RxJS để thao tác dữ liệu trong Meteor. Chúng ta sẽ tìm hiểu dựa theo tutorial về Angular 2 Meteor.

3-Way Data binding:

3-way.png
3-Way data binding (Chỉnh sửa từ: http://angular.itacademy.dk/data-binding/)

Hay còn được gọi với cái tên Full Stack Reactivity. Là tương tác dữ liệu giữa server-side – client-side, mỗi client có 1 Database riêng (sử dụng MiniMongo), và client sẽ subscribe dữ liệu từ server, và khi có thay đổi thì mọi thứ sẽ tự động cập nhật. Từ server – client – render ra view và ngược lại, dữ liệu trên view (form) thay đổi, thì sẽ cập nhật ở Database local và sau đó là gửi lên server để đồng bộ cho tất cả. Toàn bộ các xử lý trên đã được Meteor lo cho chúng ta, từ việc tạo Socket để kết nối client và server cho tới việc tạo DDP để đồng bộ dữ liệu, từ đó chúng ta có được một ứng dụng Real-time mà không cần phải tốn công tạo Socket và các thứ cần thiết để có được 1 ứng dụng Real-time.

Meteor đã sử dụng Mongo.Collection để làm điều đó. Tuy nhiên, với thời đại công nghệ phát triển vùn vụt như hiện nay cũng kèo theo việc Meteor kết hợp thêm với các công nghệ khác để ngày càng hoàn thiện hơn, cụ thể là Angular 2 sử dụng Observable (Observer design pattern) trong xử lý dữ liệu, Meteor cũng đua đòi học theo sử dụng Observable, và từ đó chúng ta có meteor-rxjs và cụ thể hơn trong trường hợp này đó chính là MongoObservable 😀 :D..

RxJS và MongoObservable

wallpaper-angular2-rxjs
RxJS + Angular (http://slides.com/wassimchegham/angular-2-data-flow-functional-reactive-programming-observables-rxjs#/)

Sau khi kết hợp với Angular thì Angular 2 Meteor team cũng viết ra một package gọi là meteor-rxjs để thay vì sử dụng callback hay là Promise như trước, thì giờ sẽ là sử dụng Observable.

Thật ra thì Observable cũng giống như Promise, giống nhau gần như là hoàn toàn, chỉ có cái là Promise thì khi resolve, thì coi như là đã kết thúc một lời hứa, còn với Observable thì nó có thể thực hiện tiếp, vì Observable không sử dụng resolve – reject mà thay vào đó là onNext, onComplete. Khi thực hiện xong cho tác vụ xử lý thì chúng ta sẽ gọi onNext để các Observers có thể subscrible. Và khi áp dụng Observable trong Mongo Collectio, mỗi khi Collection thay đổi thì callback next sẽ được gọi, còn với complete thì sẽ không được gọi tới. Có thể sẽ được gọi khi chúng ta unsubscribe nhưng trong khi chúng ta sử dụng thì nó sẽ không gọi complete, đơn thuần chỉ là vì chúng ta sử dụng Collection, chờ sự thay đổi của Collection một cách liên tục, nên callback next sẽ được gọi liên tục.

Trong khuôn khổ bài viết này chúng ta sẽ chỉ xoay quanh MongoObservable và các bạn có thể xem thêm về Observable và RxJS tại đậy.

Với Array, chúng ta xử lý 1 đống dữ liệu không tuân theo một quy luật nhất định nào, còn với Observable thì mọi thứ chúng ta làm sẽ được liên tưởng cùng với 1 cái stream. Là data stream, event stream hay là những gì stream thì chúng ta đều có thể sử dụng, và để giải thích cặn kẽ thì không thể lời một lời 2 được nên mình xin hẹn Observable – RxJS trong 1 topic riêng. Trong bài này chúng ta sẽ chỉ nghĩ về việc sử dụng Observable cho xử lý luồng dữ liệu bất đồng bộ.

Khai báo Collection:

Tiếp theo chúng ta sẽ tạo Collection sử dụng MongoObservable, và như đã đề cập ở các bài viết trước, thư mục client thì chỉ client dùng được, thư mục server thì chỉ server dùng được, và những tài nguyên dùng chung sẽ nằm trong thư mục both, chúng ta sẽ tạo các Collection ở trong này để client và server đều có thể dùng (khai báo 1 chỗ, tuy nhiên việc 2 database sẽ do Meteor lo cho các bạn nhé, nên chỉ việc khai báo và dùng như bình thường, đi sâu hơn vào các bài trong các bạn sẽ thấy rõ hơn 😀 :D..).

Thêm file both/collections/parties.collection.ts với nội dung:

//Vị trí file: both/collections/parties.collection.ts

import { MongoObservable } from 'meteor-rxjs';

export const Parties = new MongoObservable.Collection('parties');

Chúng ta import MongoObservable từ package đã cài và sử dụng nó để tạo 1 Collection có tên là parties. Các bạn cứ liên tưởng rằng nó đang tạo 1 cái bảng parties trong Database cũng ok. Sau khi tạo xong, thì chúng ta sử dụng 1 khái niệm khá là đặc biệt đó là CommonJS, đã được Meteor lo :3.. các bạn chỉ cần dùng thôi. Khi các bạn export, thì ở 1 vị trí khác chúng ta có thể import nó vào và sử dụng, Typescript compile file .ts sang ES5, và đăng ký cái vừa export trong module CommonJS với tên chính là đường dẫn tương đối của file trong ứng dụng. Trong trường hợp này là ‘both/collections/parties.collection’.

Và không để các bạn chờ lâu, chúng ta sẽ tiến hành import Collection vào một ví trí khác và sử dụng nó.

Đầu tiên chúng ta sẽ thêm nó vào Component mà chúng ta đã định nghĩa trong bài trước: AppComponent. Bước 1 sẽ là import Collection vào bằng đoạn code: import { Parties } from '../../../both/collections/parties.collection'; và vì chúng ta sử dụng MongoObservable nên chúng ta cũng phải import Observable từ rxjs vào để sử dụng. File AppComponent sau khi import các thứ cần thiết và đọc dữ liệu từ Collection thì sẽ có nội dung như sau (Vì bay giờ chúng ta sử dụng Collection để lấy dữ liệu nên dòng khai báo dữ liệu trong constructor chúng ta sẽ bỏ đi):

//Vị trí file: client/imports/app/app.component.ts

import { Component } from '@angular/core';
import template from './app.component.html';

import { Parties } from '../../../both/collections/parties.collection';
import { Observable } from 'rxjs/Observable';

@Component({
selector: 'app',
template
})
export class AppComponent {
parties: Observable<any[]>;

constructor() {
this.parties = Parties.find({}).zone();
}
}

Ở trong đoạn code trên, chúng ta khai báo lại biến parties với kiểu dữ liệu mới đó là Observable, Observable này sẽ handle cho 1 mảng dữ liệu bất kỳ. find là một phương thức trong Mongo Collection, nhưng trong trường hợp này thì nó là phương thức của 1 Observable và trả về 1 Observable Cursor để chúng ta duyệt và lấy dữ liệu. Cuối cùng còn 1 thứ lạ lẫm đó chính là zone, phương thức này thay thế cho việc gửi request kiểm tra thay đổi dữ liệu của Meteor trước đây, và bây giờ, khi sử dụng Zone, mọi thứ sẽ được cập nhật một cách nhanh chóng và tự dộng. Tức là khi có thay đổi về dữ liệu thì Zone lắng nghe và thực hiện cập nhật lại trên View.

Tới đây, khi chúng ta start app sẽ thấy lỗi như vầy trên trình duyệt EXCEPTION: Error in ./AppComponent class AppComponent - inline template:0:15 caused by: Cannot find a differ supporting object '[object Object]' of type 'object'. NgFor only supports binding to Iterables such as Arrays.. Lý do chính là vì sau khi gọi phương thức zone, chúng ta sẽ nhận được 1 Observable, nên khi trên View sử dụng vòng lặp ngFor cho biến parties thì sẽ bào lỗi Object không phải là Iterator. Và để giải quyết vấn đề này chúng ta sẽ sử dụng 1 Pipe được Angular 2 xây dựng sẵn. Đó là async pipe. Về việc sử dụng thì: observable_or_promise_expression | async, và trong trường hợp này template của chúng ta sẽ có nội dung như sau:

<!-- Vị trí file: client/imports/app/app.component.ts -->
<div>
<ul>
<li>
{{party.name}}
{{party.description}}
{{party.location}}
</li>
</ul>
</div>

AsyncPipe sẽ subscribe tới 1 Observable hoặc Promise và trả về dữ liệu mới nhất được gửi tới. Và trong trường hợp này chính là 1 Observable parties.

Một điều các bạn cũng nên biết là AsyncPipe sẽ tự động unsubscribe khi Component bị hủy. Sau này, khi chúng ta subscribe chúng ta đều phải unsubscribe thật cẩn thận, để tránh bị leak memory.

Như vậy là chúng ta đã hoàn thiện việc khai báo và cài đặt các thứ để có thể tạo và sử dụng MongoObservable Collection, một việc cuối cùng không quá quan trọng nhưng vẫn nên làm đó chính là tạo Model. Mục đích của việc này là để cho mọi thứ tường minh hơn và cụ thể hơn, Typescript đã cho phép chúng ta gắn kiểu dữ liệu cho biến, thì việc tạo Model cho nó cũng chính là việc tận dụng tối đa những gì mà các công nghệ hỗ trợ và cả các thông báo cảnh báo hoặc là báo lỗi cũng sẽ được Typescript hỗ trợ khi có Model.

Chắc hẳn một điều là khi các bạn đọc code tới phần xử lý data, thì việc biết rõ các thuộc tính của 1 field trong 1 Collection sẽ dễ dàng hơn rất là nhiều trong việc ghi và đọc dữ liệu từ Database đúng không? 😀 :D.

Chúng ta sẽ tạo các model sử dụng từ khóa interface mà Typescript cung cấp. Tạo file CollectionObject.ts trong thư mục both/models:

//Vị trí file: both/models/collection-object.model.ts

export interface CollectionObject {
_id?: string;
}

Lý do chúng ta có file này là vì trong MongoDB, tất cả các field đều được phân biệt với nhau dựa vào một khóa chính là khóa _id, có thể chúng ta gán giá trị cho thuộc tính này luôn hoặc mặc định khi insert dữ liệu thì MongoDB sẽ tạo một chuỗi ký tự ngẫu nhiên và không bị trùng lặp cho thuộc tính này. Vì vậy, với mọi đối tượng dữ liệu chúng ta đều cần phải có 1 thuộc tính _id để đồng bộ với thuộc tính trong Database.

Thực ra thì có thể không có thuộc tính này thì mọi thứ vẫn bình thường, chỉ là khi sử dụng tính năng Model này của Typescript thì những trường hợp không đồng bộ thuộc tính sẽ được Typescript cảnh báo hoặc báo lỗi. Tuy nhiên có thì vẫn hơn keke.. Và như vậy chúng ta sẽ tạo và export nó để cho các Model khác có thể kế thừa và sử dụng thuộc tính _id này.

Tiếp theo là model cho 1 đối tượng dữ liệu sẽ được lưu trong Collection, và trong ví dụ của chúng ta thì đó chính là 1 model Party.

//Vị trí file: both/models/party.model.ts

import { CollectionObject } from './collection-object.model';

export interface Party extends CollectionObject {
name: string;
description: string;
location: string;
}

Như đã trình bày ở trên thì các Model sau này của chúng ta sẽ cần phải có trường _id để đồng bộ với dữ liệu trong MongoDB, và như đoạn code thì chúng ta sẽ import CollectionObject để kế thừa và sử dụng thuộc tính _id của nó. Cú pháp kế thừa trong Typescript cũng tương tự như các ngôn ngữ lập trình hiện đại như là Java, C#… (Giống C# hơn đấy, đồ Microsoft mà :v :v…), tiếp đó là tạo các thuộc tính cho đối tượng Party với các kiểu dữ liệu tương ứng, và cuối cùng là export ra để có thể sử dụng Model này ở 1 nơi khác.

Sau khi có Model thì chúng ta sẽ có một số thay đổi trong Component, thêm Model giúp cho mọi thứ tường minh hơn, và khi đó AppComponent sẽ có nội dung như sau:

//Vị trí file: client/imports/app/app.component.ts

import { Component } from '@angular/core';
import template from './app.component.html';

import { Parties } from '../../../both/collections/parties.collection';
import { Observable } from 'rxjs/Observable';

import { Party } from '../../../both/models/party.model';

@Component({
selector: 'app',
template
})
export class AppComponent {
parties: Observable<Party[]>;

constructor() {
this.parties = <Observable<Party[]>>Parties.find().zone();
}
}

Thay kiểu dữ liệu cho Observable (trước đây là any – Kiểu dữ liệu bất kỳ, còn bây giờ là Party). Và dòng this.parties = Parties.find().zone(); chỉ là để ép kiểu dữ liệu cho kết quả trả về.

Không chỉ mỗi trong Component thay đổi, mà chúng ta cũng cần thêm kiểu dữ liệu khi tạo Collection, để Collection đó là một Collection chuyên biệt cho Model này:

//Vị trí file: both/collections/parties.collection.ts

import { MongoObservable } from 'meteor-rxjs';

import { Party } from '../models/party.model';

export const Parties = new MongoObservable.Collection<Party>('parties');

Và như vậy là chúng ta đã hoàn tất mọi chuyện, việc cuối cùng trong bài viết này chính là thêm dữ liệu vào ứng dụng.

Thêm dữ liệu vào ứng dụng:

Để thêm dữ liệu vào ứng dụng chúng ta sẽ có 2 nơi có thể thêm dữ liệu vào cho ứng dụng, 1 là thêm trực tiếp vào Database bằng tay (dùng shell console), 2 là thêm vào ứng dụng khi khởi động server (server ghi dữ liệu vào Database).

  • Thêm dữ liệu vào từ Console:

Khi chúng ta start app bằng câu lệnh: meteor, mặc định meteor sẽ sử dụng local MongoDB để lưu dữ liệu, trong thực tế thì người ta sẽ sử dụng 1 server database riêng nếu có nhu cầu, và sẽ cho meteor kết nối tới server database đó dựa vào “Connection String” của DB server đó. Còn ở hiện tại thì chúng ta đang sử dụng local MongoDB, được meteor cài sẵn bên trong nó, và để truy cập vào để xem xóa sửa trong đó thì chúng ta phải gọi 1 câu lệnh trên terminal (command line) thông qua meteor đó là: meteor mongo. Và kết quả truy cập sẽ như hình sau:

mongo-meteor
Mongo in Meteor

Và để thêm data vào chúng ta sẽ gõ câu lệnh sau:

db.parties.insert({ name: "A new party", description: "From the mongo console!", location: "In the DB" });

Người ta gọi cái này là code trên shell do MongoDB cung cấp, và một số lệnh shell mà các bạn cần quan tâm đó là insert, update và remove. Chi tiết hơn các bạn có thể xem tại đây.

Nếu các bạn muốn xóa 1 field nào trong Collection, thì cũng khá là đơn giản, chỉ cần find để tìm id của field đó, và dùng câu lệnh remove, ví dụ chúng ta find ra 1 object có id là: "_id" : ObjectId("5843982d1386bbb8b009a2a2" thì chúng ta chỉ cần gõ: db.parties.remove({ _id: ObjectId("5843982d1386bbb8b009a2a2") }) thì Mongo sẽ xóa field đó đi và trả về kết quả: WriteResult({ "nRemoved" : 1 }) nếu xóa thành công.

Trong câu lệnh trên người ta sử dụng db.., đây là một format trong shell của MongoDB và chỉ cần dòng lệnh insert trên là chúng ta có một Object trong parties Collection. nếu insert thành công thì nó sẽ trả về: WriteResult({ "nInserted" : 1 }) hoặc “nInserted” : nếu insert n object. Và mỗi object sẽ có 1 thuộc tính _id như mình đã nói:

demo-mongo

Đó là cách mà chúng ta thêm dữ liệu từ Console mà mongo shell cung cấp, tuy nhiên đó chỉ là insert, các bạn cần xem thêm các chức năng khác như là update, remove, find, findOne… để có thể sử dụng trong quá trình dev và test. Bây giờ để xem có gì thay đổi hay không, thì các bạn chỉ cần start app và xem kết quả ở trang localhost.

demo-result01
Kết quả chạy thử 1
  • Thêm dữ liệu vào khi start ứng dụng:

Ở trên chúng ta đã thêm dữ liệu vào ứng dụng từ Console, tuy nhiên trong một số trường hợp chúng ta có những dữ liệu tĩnh cần khởi tạo và lưu vào Database ngay khi ứng dụng khởi động, để có thể sử dụng trong cả ứng dụng. Và cũng không có gì là quá phức tạp, chúng ta chỉ cần tạo 1 hàm, import Collection vào để thực hiện thao tác thêm data, sau đó export hàm đó ra, bắt sự kiện app startup và gọi hàm đó để nó thực thi tác vụ thêm data.

Chúng ta sẽ tạo 1 file fixtures trong thư mục server:

//Vị trí: server/imports/fixtures/parties.ts

import { Parties } from '../../../both/collections/parties.collection';
import { Party } from '../../../both/models/party.model';

export function loadParties() {
if (Parties.find().cursor.count() === 0) {
const parties: Party[] = [{
name: 'Dubstep-Free Zone',
description: 'Can we please just for an evening not listen to dubstep.',
location: 'Palo Alto'
}, {
name: 'All dubstep all the time',
description: 'Get it on!',
location: 'Palo Alto'
}, {
name: 'Savage lounging',
description: 'Leisure suit required. And only fiercest manners.',
location: 'San Francisco'
}];

parties.forEach((party: Party) => Parties.insert(party));
}
}

Hàm loadParties trên thực hiện kiểm tra, nếu trong Database chưa có dữ liệu, thì sẽ thực hiện thêm dữ liệu vào bằng phương thức insert của MongoObservable Collection. Sau khi đã có hàm loadParties, chúng ta sẽ thực hiện chức năng tiếp theo đó chính là bắt sự kiện startup của ứng dụng và gọi hàm loadParties trên.

Tạo file main.ts trong thư mục server -> import hàm loadParties đã export ở trên, bắt sự kiện startup và gọi loadParties:

import { Meteor } from 'meteor/meteor';

import { loadParties } from './imports/fixtures/parties';

Meteor.startup(() => {
loadParties();
});

Và như vậy là chúng ta đã hoàn thành việc thêm dữ liệu từ server bằng MongoObservable Collection.

demo-result02.png
Kết quả chạy thử 2

Đây chỉ là các cách thêm dữ liệu dùng trong mục đích test khi dev, còn thực chất trong tương lai khi áp dụng thực tế, thì dữ liệu được gửi lên từ client và được server xử lý, kiểm tra và lưu lại, cái này thì tùy thuộc vào yêu cầu của ứng dụng.

Tổng kết

Qua bài viết này, chúng ta được biết thêm một số thứ mà Meteor và Angular đem vào kết hợp sử dụng, và chúng ta cũng đã sử dụng trong project tutorial về sơ bộ cũng hiểu được cách thức hoạt động cũng như sử dụng nó như thế nào, thì để hiểu sâu hơn thì chắc chắn cả mình và các bạn sẽ phải bỏ thêm nhiều thời gian để làm việc với nó, gặp vấn đề, giải quyết nó, thì mới có thể hiểu rõ hơn về nguyên lý hoạt động của nó như thế nào và khi kết hợp thì sẽ xảy ra những vấn đề gì.

Tóm lại, chúng ta đã biết được cách thức hoạt động của Meteor, kết hợp Meteor và Angular để tạo View hiển thị dữ liệu thông qua các khái niệm Module, Component, Directive… và trong bài viết này chính là việc thao tác xử lý dữ liệu sử dụng MongoDB – MongoObservable Collection, và cả luồng chạy của dữ liệu trong ứng dụng Angular 2 Meteor khi cập nhật từ server -> client -> View và ngược lại. Như vậy là đã đầy đủ những khái niệm cơ bản trong lập trình Web sử dụng Meteor, và đi sâu hơn, các bài viết tiếp theo trong series, chúng ta sẽ tìm hiểu kỹ hơn về những thứ đặc trưng hơn của cả Meteor và Angular 2 và áp dụng chúng trong ứng dụng thực tế. Sẽ có những thứ mới mẻ khác đang chờ các bạn ở các bài viết tiếp theo, hẹn gặp lại mọi người trong bài chia sẻ kế tiếp của mình: CRUD (Create – Read – Update – Delete) data từ client và Router/Navigation.

Hồ Quốc Toản

Tài liệu tham khảo:

[1] 2 minutes introduction to RxJS
[2] Angular — Introduction to Reactive Extensions (RxJS)
[3] AsyncPipe
[4] 3-Way Data binding – Angular 2 meteor

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s