
Amplify DataStore を使ってオフライン対応Webアプリを作成する
先々週の2019年12月7日、Amplifyの新機能「Amplify DataStore」が発表されました!早速試します。
やりたいこと
・デバイスがオフラインのときも、ユーザはキャッシュデータにアクセスできる
・デバイスがオフラインのときも、データ作成および変更を中断させない
・デバイスがオンラインに戻ると、自動でバックエンドに再接続し、データを同期して、競合がある場合は解決する
環境
・Angular: 8.3.7
・amplify-cli: 4.6.0
手順
1. Angularプロジェクトを作成
$ ng new amplify-datastore --style=scss --routing
$ cd amplify-datastore
2. Amplifyの利用準備
$ npx amplify-app@latest
Angular6以上の場合、src/polyfills.tsに以下を追加
(window as any).global = window;
(window as any).process = {
env: { DEBUG: undefined },
};
3. 必要なAmplifyライブラリをインストール
$ npm i @aws-amplify/core @aws-amplify/datastore
4. AWSアカウントと紐付ける設定の作成
$ amplify init
5. Amplify APIを追加
$ amplify add api
6. GraphQLスキーマを追加
amplify/backend/api/amplifyDatasource/schema.graphql
enum PostStatus {
ACTIVE
INACTIVE
}
type Post @model {
id: ID!
title: String!
rating: Int!
status: PostStatus!
}
7. モデルジェネレータ起動ッ
$ npm run amplify-modelgen
これでCFn設定とかアプリ用のモデル定義が作られたと思う、たぶん
8. src/main.tsで設定をインポート
import Amplify from '@aws-amplify/core';
import awsconfig from './aws-exports';
Amplify.configure(awsconfig);
9. コンポーネントはこんな感じで作った
src/app/app.component.html
<div>
<div>
<button (click)="createPost()">NEW</button>
<button (click)="deletePosts()">DELETE ALL</button>
</div>
<table border="1">
<thead>
<tr>
<td>id</td>
<td>title</td>
<td>version</td>
<td>actions</td>
</tr>
</thead>
<tbody>
<tr *ngFor="let post of (posts | async)">
<td>{{post.id.substring(0, 8)}}</td>
<td>{{post.title}}</td>
<td>{{post._version}}</td>
<td>
<button (click)="updatePost(post.id)">update</button>
<button (click)="deletePost(post.id)">delete</button>
</td>
</tr>
</tbody>
</table>
</div>
src/app/app.component.ts
import {Component, OnDestroy, OnInit} from '@angular/core';
import {Post, PostStatus} from '../models';
import {DataStore, Predicates} from '@aws-amplify/datastore';
import {from, Observable} from 'rxjs';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit, OnDestroy {
posts: Observable<any[]>;
private sub: ZenObservable.Subscription;
ngOnInit() {
this.posts = from(DataStore.query(Post, Predicates.ALL));
this.sub = DataStore.observe(Post as any).subscribe(msg => {
this.getPosts();
});
}
ngOnDestroy() {
this.sub.unsubscribe();
}
createPost() {
DataStore.save(
new Post({
title: `Created! ${Date.now()}`,
rating: 1,
status: PostStatus.ACTIVE
})
);
}
private getPost(id: string) {
return from(DataStore.query(Post as any, id));
}
private getPosts() {
this.posts = from(DataStore.query(Post, Predicates.ALL));
}
updatePost(id: string) {
this.getPost(id).subscribe(item => {
DataStore.save(
Post.copyOf(item as any, updated => {
updated.title = `Updated! ${Date.now()}`;
updated.rating = 4;
})
);
});
}
deletePost(id: string) {
this.getPost(id).subscribe(item => DataStore.delete(item));
}
deletePosts() {
DataStore.delete(Post, Predicates.ALL);
}
}
自動生成のPostモデルには_versionのゲッターがないので、Postじゃなくてanyで取り回す。本来的にはフロントで出すような情報じゃないからだろうけど、デモとしてのわかりやすさ優先でむりやり出す。
この状態でバックエンドなしでそこそこ動く。_versionはバックエンドの方で作られる値なんで空白。
10. バックエンドのリソースを作成
$ npm run amplify-push
これでAppSyncリソースが作られた。
あとDynamoDBにテーブルが作られた。
アプリを動かしたらば
DynamoDBにレコードが記録された。
ウェ〜イ🎉🎉🎉
トラブルシューッ
Cannot find module 'stream'. とか Cannot find name 'Buffer'. のとき
DataStore - Sync error subscription failed Connection failed: Buffer is not defined のとき
src/polyfills.tsに以下を追加
global.Buffer = global.Buffer || require('buffer').Buffer;
参考URL
感想
オフラインからの復帰で変更の競合が解決されるさまをgifにとったけど、noteのアップロード上限がそれを許さなかった。南無三。
オフライン処理って自力で書いたことないけど大変なことになるのは必至。こんなふうにさくっと外部化して、本当にやるべきことちゃんとやるっていうのはいいですね。