【ハンズオン資料】Monaca × NCMBで掲示板アプリを作ってみよう


Monacaを使って何かアプリを作ってみたいと思っても、いざとなるとアイデアが出てこないかもしれません。また、最初から大きなアプリを作ろうと思うと、何から手を付けて良いのか分からないことでしょう。

今回はNCMB（ニフクラmobile backend）と組み合わせて、掲示板アプリを開発します。

## 記事

こちらのハンズオン資料は下記のMonacaプレス記事をベースにしています。

- [掲示板アプリを作ってみよう【その1：画面の説明と認証まで】 – モナカプレス](https://press.monaca.io/atsushi/6318)
- [掲示板アプリを作ってみよう【その2：スレッドとコメントの投稿】 – モナカプレス](https://press.monaca.io/atsushi/6321)

## ベースコード

下記のURLをMonaca IDEにてインポートしてください。

```
https://github.com/NCMBMania/monaca_forum_handson/archive/refs/heads/main.zip
```

## 利用している技術、ライブラリ

このアプリで利用している技術やライブラリは次の通りです。

- [Onsen UI](https://ja.onsen.io/)
- [ニフクラmobile backend](https://mbaas.nifcloud.com/)

今回はMonacaでOnsen UIの最小限のテンプレートを選択しています。

![](https://press.monaca.io/wp-content/uploads/2020/11/daily_app_13.png)

## 利用するNCMBの機能

- 会員管理
    - 匿名認証
    - ロール（グループ）管理
- データストア
    - Threadクラス
        - データ登録
        - データ削除
        - データ検索
    - Commentクラス
        - データ登録
        - データ削除
        - データ検索
- ファイルストア
  - 画像登録
  - 画像取得

## 仕様

このアプリの画面は次の通りです。

### スレッド一覧画面

掲示板のスレッドが一覧表示される画面です。この画面が最初に表示されます。

![](https://press.monaca.io/wp-content/uploads/2020/12/monaca-internal-survey-8.png)

#### 新規スレッド入力ダイアログ

ツールバーの上にある + アイコンをタップすると新しくスレッドを立てるための入力ダイアログが表示されます。ここでは以下の情報を入力します。

- スレッドのタイトル
- スレッドの概要
- 画像（オプション）

![](https://press.monaca.io/wp-content/uploads/2020/12/monaca-internal-survey-7.png)

### スレッド詳細画面

スレッド一覧画面をタップするとスレッド詳細画面が表されます。ここではスレッドの内容と、そのスレッドに対するコメントが一覧表示されます。

![](https://press.monaca.io/wp-content/uploads/2020/12/monaca-internal-survey-3.png)

#### コメント入力ダイアログ

画面下にあるアイコンをタップするとコメントを入力するダイアログが表示されます。ここでは以下の情報を入力します。

- コメント本文
- 画像（オプション）

![](https://press.monaca.io/wp-content/uploads/2020/12/monaca-internal-survey-4.png)

### データの権限

スレッド、コメントともに誰でも閲覧可能です。また、作成者は削除可能です。adminグループに所属しているユーザーも削除可能です。

### ユーザーデータの権限

ユーザーデータは自分のみ閲覧可能です。

## ニフクラ mobile backendのキーを取得

[ニフクラ mobile backend](https://mbaas.nifcloud.com/)にユーザー登録、またはログインしてアプリを作成します。その結果として、次の2つのキーが取得できます。

- アプリケーションキー
- クライアントキー

## ライブラリのインストール

ではここから開発していきます。今回はOnsen UIのテンプレートを利用しています。外部フレームワーク（VueやReactなど）は用いず、素のJavaScriptのものを選択しています。

外部ライブラリとして、ncmbを追加します。JavaScriptライブラリですので、JS/CSSコンポーネントの追加と削除から行ってください。

![](https://press.monaca.io/wp-content/uploads/2020/11/daily_app_11.png)

読み込むファイルとしてncmb.min.jsをチェックするのを忘れないでください。

![](https://press.monaca.io/wp-content/uploads/2020/11/daily_app_9.png)

## NCMBの初期化

`www/js/app.js` にて、下記のようにニフクラmobile backendを初期化します。上記で紹介したアプリケーションキーとクライアントキーを適用してください。

```js
// NCMBを初期化します
const applicationKey = 'YOUR_APPLICATION_KEY';    // 自分のアプリケーションキーと書き換えてください
const clientKey = 'YOUR_CLIENT_KEY';              // 自分のクライアントキーと書き換えてください
const ncmb = new NCMB(applicationKey, clientKey); // 初期化
```

## index.html の紹介

Monacaアプリで一番初めに読み込まれる `www/index.html` では、ナビゲーションバー `ons-navigator` だけを指定します。最初に表示する画面は list.html となっています。

```html
<ons-navigator id="nav" page="list.html"></ons-navigator>
```

## スレッド一覧画面の紹介

スレッド一覧画面 `list.html` のHTMLを下記に紹介します。スレッドを表示するための `ons-list` と、スレッドを立てる際のダイアログ `ons-dialog` が主な画面構成要素です。

```html
<ons-page>
  <ons-toolbar>
    <div class="center">掲示板</div>
    <div class="right">
      <ons-toolbar-button>
        <ons-icon icon="md-plus" id="open"></ons-icon>
      </ons-toolbar-button>
    </div>
  </ons-toolbar>
  <ons-list>
    <ons-list-header>スレッド一覧</ons-list-header>
    <span id="threads"></span>
  </ons-list>
  <ons-dialog direction="up">
    <div style="text-align: center;">
      <p>
        <ons-input id="title" modifier="underbar" placeholder="スレッドのタイトル" float></ons-input>
      </p>
      <p>
        <textarea class="textarea" id="body" cols="20" rows="3" placeholder="スレッドの説明"></textarea>
      </p>
      <p>
        <input type="file" id="image" accept="image/*" />
      </p>
      <p style="margin-top: 30px;">
        <ons-button id="add">スレッドを立てる</ons-button>
      </p>
    </div>
  </ons-dialog>
</ons-page>
```

### スレッド詳細画面の紹介

スレッド詳細画面 `thread.html` は次のようなHTMLとなっています。スレッドの内容を表示する部分、コメント一覧の `ons-list` 、そしてコメントを追加する際のダイアログがあります。加えて画像をタップした際に拡大表示するためのダイアログもあります。

```html
<ons-page>
  <ons-toolbar>
    <div class="left"><ons-back-button>Back</ons-back-button></div>
    <div class="center thread-title"></div>
  </ons-toolbar>
  <ons-card>
    <img class="caption" src="http://placehold.jp/100x100.png"/>
    <p class="thread-body"></p>
  </ons-card>
  <ons-list id="comments">
  </ons-list>
  <ons-fab position="bottom right" id="comment-button">
    <ons-icon icon="md-comment"></ons-icon>
  </ons-fab>
  <ons-dialog class="comment" direction="up">
    <div style="text-align: center;">
      <form id="comment-form">
        <p>
          <textarea class="textarea" id="body" cols="20" rows="3" placeholder="コメント"></textarea>
        </p>
        <p>
          <input type="file" id="image" accept="image/*" />
        </p>
        <p style="margin-top: 30px;">
          <ons-button id="add">コメントする</ons-button>
        </p>
      </form>
    </div>
  </ons-dialog>
  <ons-dialog class="image" direction="up">
    <div style="text-align: center;">
      <ons-card>
        <img src="" class="square-image" />
      </ons-card>
      <p style="margin-top: 30px;">
        <ons-button id="close">閉じる</ons-button>
      </p>
    </div>
  </ons-dialog>
</ons-page>
```

### ニフクラ mobile backendの管理画面での作業

認証はアプリを立ち上げた際に実行します。今回はNCMBの匿名認証を利用します。まずNCMBの管理画面にて匿名認証を有効にしてください。

![](https://press.monaca.io/wp-content/uploads/2020/12/monaca-internal-survey-11-2.png)

次にニフクラmobile backendの管理画面にて、管理グループを作成しておきます。管理画面の会員管理にてロールを作成します。今回は分かりやすいように `admin` としておきます。

![](https://press.monaca.io/wp-content/uploads/2020/11/daily_app_3.png)

### 認証判定

続いて `js/app.js` に次のような関数があるのを確認してください。これはMonacaアプリが準備完了したら呼ばれる関数です。

```js
// Monacaアプリの準備が完了すると呼ばれるメソッド
ons.ready(async () => {
});
```

この中に、次のようにコードを記述します（説明は後述）。

```js
// Monacaアプリの準備が完了すると呼ばれるメソッド
ons.ready(async () => {
  // 現在ログインしているユーザを取得（最初は null）
  let user = ncmb.User.getCurrentUser();
  // ログイン判定
  if (user) {
    // ログインしている場合
    try {
      // セッションの有効性確認
      await ncmb.DataStore('Test').fetch();
    } catch (e) {
      // エラーの場合は空にする
      user = null;
    }
  }
  // ログインしていない場合は匿名認証実行
  if (!user) {
    user = await ncmb.User.loginAsAnonymous();
  }
  // 管理者グループのロールを取得 
  const role = await ncmb.Role
    .equalTo('roleName', 'admin')
    .fetch();
  // 管理者グループに所属するユーザを取得
  const users = await role.fetchUser();
  // 自分が所属しているかどうかで管理者フラグを立てる
  window.admin = users.map(u => u.objectId)
    .indexOf(user.objectId) > -1;
});
```

まず最初に現在ログイン中のユーザデータを取得します（ `ncmb.User.getCurrentUser()` ）。これが空の場合は未ログインになります。

もしあった場合には適当なデータストア（今回はTest）のデータを取得しようとします。これによってセッションが有効であるかどうかチェックしています。セッションが有効期限切れであったり、管理画面でユーザデータを削除していた場合はセッションが無効になってエラーになります。その場合にはユーザデータを削除して、匿名認証を実行します（`ncmb.User.loginAsAnonymous()`）。

認証した後は、管理者グループ（admin）を取得して、さらに所属ユーザを取得します。その中に自分のID（`objectId`）があれば、管理者グループに所属しているということです。結果は `admin` という変数に入れています。

これで認証処理の完成です。

## スレッドの作成

スレッド一覧画面 `list.html` を開発します。まず画面初期化時にイベントリスナーを追加します。

```js
// 記述済み：画面初期化時の処理
ons.getScriptPage().onInit = function() {
  // プラスボタンを押した時の処理
  document.querySelector('#open').onclick = openDialog.bind(this);
  // スレッド追加ボタンを押した時の処理
  document.querySelector('#add').onclick = addThread.bind(this);
}
```

次に画面が表示された際のイベント処理です。ここではスレッド一覧を読み込みます。

```js
// 記述済み：画面表示時の処理
ons.getScriptPage().onShow = function() {
  // スレッド一覧を読み込む
  showThread.bind(this)();
}
```

![](https://press.monaca.io/wp-content/uploads/2020/12/monaca-internal-survey-8.png)

### スレッド一覧の表示について

`showThread` の実装についてです。まずスレッドの一覧を読み込みます。

```js
// 記述済み：スレッドを表示する処理
async function showThread() {
  // スレッド一覧の読み込み
  this.threads = await getThread();
```

getThreadはNCMBのThreadクラスからデータを取得します。

**記述してください**

```js
// スレッド一覧をNCMBから取得する処理
async function getThread() {
  const Thread = ncmb.DataStore('Thread');
  return await Thread
    .order('createDate', true)
    .limit(100)
    .fetchAll();
}
```

そして取得したスレッドを `ons-list` の中に出力します。

```js
// 記述済み：スレッド表示対象のDOM
const dom = this.querySelector('#threads');
const html = [];
this.threads.forEach(thread => {
  // 一覧用のDOMを準備
  html.push(`
    <ons-list-item modifier="chevron" tappable data-object-id="${thread.objectId}">
      <div class="left">
        <img id="img-${thread.objectId}" data-name="${thread.get('image')}" class="square-image list-item__thumbnail" src="http://placehold.jp/30x30.png">
      </div>
      <div class="center">
        <span class="list-item__title">
          ${thread.get('title')} ${deletable(thread)}
        </span>
        <span class="list-item__subtitle">
          ${thread.get('body')}
        </span>
      </div>
    </ons-list-item>
  `);
});
// DOMを追加
dom.innerHTML = html.join('');
```

ユーザに削除権限があるかどうか判定してHTMLを返す deletable はスレッド詳細画面でも使えるので `js/app.js` に記述してあります。

```js
// 記述済み：削除可能か判定する関数
function deletable(obj) {
  let bol = false;
  // 管理者であれば常に削除可能
  if (window.admin) {
    bol = true;
  } else {
    // 管理者でない場合は自分に削除権限があるかチェック
    const user = ncmb.User.getCurrentUser();
    bol = user && obj.acl[user.objectId] && obj.acl[user.objectId].write;
  }
  // 削除可能な場合はゴミ箱アイコンを表示
  return bol ? `<ons-icon data-object-id="${obj.objectId}" class="delete" icon="fa-trash"></ons-icon>` : '';
}
```

HTMLを描画したら、イベントリスナーを追加します。

```js
addEvent.bind(this)();
```

今回は以下のイベントを追加しています。

- スレッドをタップしたらスレッド詳細画面に遷移する
- 画像をNCMBから読み込んで表示する
- 削除アイコンをタップしたらスレッドを削除する

```js
// 記述済み：スレッドのイベント設定
function addEvent() {
  // スレッドをタップした際のイベント
  this.querySelectorAll('ons-list-item').forEach(d => {
    d.onclick = () => {
      document.querySelector('#nav').pushPage('thread.html', { data: {
        thread: this.threads.filter(t => t.objectId === d.dataset.objectId)[0]
      }});
    }
  });
  // 画像を読み込む
  this.querySelectorAll('ons-list-item img').forEach(d => {
    loadImage.bind(this)(d.dataset.name, `#${d.getAttribute('id')}`);
  });
  // 削除アイコンをタップした際のイベント
  this.querySelectorAll('.delete').forEach(d => {
    d.onclick = () => {
      deleteThread.bind(this)(d);
    }
  });
}
```

画像を読み込む処理はスレッド詳細でも利用するので `js/app.js` に定義してあります。

**記述してください**

```js
async function loadImage(name, className) {
  // 指定されたクラスがなければ終了
  if (!this.querySelector(className)) return;
  // NCMBから画像をダウンロード（バイナリなので2つ目の引数に blob を指定）
  const blob = await ncmb.File.download(name, 'blob');
  // FileReaderを用意
  const fileReader = new FileReader();
  fileReader.onload = () => {
    // 写真を読み込んだら、指定されたDOMに適用
    this.querySelector(className).src = fileReader.result;
  };
  // Blobを読み込む
  fileReader.readAsDataURL(blob);
}
```

ここまでで画面の初期表示が完了します。

### イベント処理

スレッド一覧におけるイベント処理は次の通りです。

- ツールバーにある + ボタンをタップ
- スレッド追加ボタンを押した時のイベント
- スレッドをタップした際のイベント
- 削除アイコンをタップした際のイベント

#### ツールバーにある + ボタンをタップ

これはスレッド追加用のダイアログを表示するだけです。

```js
// 記述済み：スレッド追加用ダイアログの表示
function openDialog() {
  this.querySelector('ons-dialog').show();
}
```

![](https://press.monaca.io/wp-content/uploads/2020/12/monaca-internal-survey-7.png)

#### スレッド追加ボタンを押した時のイベント

これは入力された内容を取得して、NCMBにスレッドを登録します。コードのコメントを参照してください。

注意点としては、ACL（アクセス権限）として、誰でも読み込み可能としています。そしてadminグループに所属するユーザ、またはユーザ自身は編集、削除権限を付与しています。ファイル `file` がある場合は、それをNCMBのファイルストアにアップロードしています。これは `ncmb.File.upload` の1行だけで完了します。このファイルについてもACLで制御されています。データを登録した後はダイアログを非表示にして、スレッド詳細画面 `thread.html` に遷移しています。

**記述してください**

```js
// スレッドを追加する処理
async function addThread() {
  // 変数の準備
  const title = this.querySelector('#title').value;
  const body  = this.querySelector('#body').value;
  const file = this.querySelector('#image').files[0];
  const user = ncmb.User.getCurrentUser();
  // 権限の設定
  // 全体に読み込み権限
  // adminまたは自分に編集・削除権限
  const acl = new ncmb.Acl();
  acl
    .setPublicReadAccess(true)
    .setRoleWriteAccess('admin', true)
    .setUserWriteAccess(user, true);
  // 画像があればアップロード
  if (file) {
    await ncmb.File.upload(file.name, file, acl);
  }
  // スレッドクラスを作成
  const Thread = ncmb.DataStore('Thread');
  const thread = new Thread;
  // 値を設定して保存
  await thread
    .set('title', title)
    .set('body', body)
    .set('image', file.name)
    .set('acl', acl)
    .save();
  // ダイアログを閉じる
  this.querySelector('ons-dialog').hide();
  // スレッド画面へ移動
  document.querySelector('#nav').pushPage('thread.html', { data: { thread }});
}
```

#### スレッドをタップした際のイベント

スレッドをタップした際には、スレッドを追加した時と同様にスレッド詳細画面 `thread.html` に遷移しています。

```js
// 記述済み：スレッドをタップした際のイベント
this.querySelectorAll('ons-list-item').forEach(d => {
  d.onclick = () => {
    document.querySelector('#nav').pushPage('thread.html', { data: {
      thread: this.threads.filter(t => t.objectId === d.dataset.objectId)[0]
    }});
  }
});
```

#### 削除アイコンをタップした際のイベント

削除アイコンをタップした際にはスレッドの削除処理を実行しています。この時、クラウド側でACLをチェックしていますので、不正ユーザが削除を実行しようと思っても権限がなければ削除できません。


**記述してください**

```js
// スレッドを削除する処理
async function deleteThread(dom) {
  // 削除前の確認
  const res = await ons.notification.confirm('スレッドは一度削除すると元には戻せません。削除してよろしいですか？');
  if (res === 0) return; // キャンセルの場合
  // 削除対象のデータを設定
  const Thread = ncmb.DataStore('Thread');
  const thread = new Thread;
  thread.objectId = dom.dataset.objectId;
  // 削除実行
  await thread.delete();
  // 表示を更新
  showThread.bind(this)();
}
```

## スレッド詳細画面について

ここまででスレッド一覧画面が完成です。続いてスレッド詳細画面の実装を解説します。

![](https://press.monaca.io/wp-content/uploads/2020/12/monaca-internal-survey-2.png)

### 画面初期化時のイベント

画面初期化時には以下のイベントを設定しています。

- コメントダイアログ表示ボタンを押した時
- コメントするボタンを押した時
- 画像ダイアログの閉じるボタンを押した時

```js
// 記述済み：画面初期化時の処理
ons.getScriptPage().onInit = function() {
  // コメントダイアログ表示ボタンを押した時の処理
  this.querySelector('#comment-button').onclick = openModal.bind(this);
  // コメントするボタンを押した時の処理
  this.querySelector('#add').onclick = addComment.bind(this);
  // 画像ダイアログを閉じる際の処理
  this.querySelector('#close').onclick = closeDialog.bind(this);
}
```

### 画面表示時のイベント

画面表示時には、前の画面から受け取ったスレッド情報を表示します。また、画像があれば追加表示したり、既存のコメントを一覧表示します。

```js
// 記述済み：スレッド画面表示時の処理
ons.getScriptPage().onShow = function() {
  // スレッドの内容を画面に表示
  const { thread } = this.data;
  for (const key in thread) {
    const value = thread[key];
    const dom = this.querySelector(`.thread-${key}`);
    if (dom) {
      dom.innerHTML = value;
    }
  }
  // 画像があれば表示
  if (thread.get('image')) {
    loadImage.bind(this)(thread.get('image'), `.caption`)
  }
  // コメントを表示
  showComments.bind(this)();
}
```

### コメント一覧の表示について

`showComments` の実装についてです。まずコメントの一覧を読み込みます。

```js
// 記述済み
// 表示対象のスレッド
const thread = this.data.thread;
// スレッドをキーとしてコメントを取得
const ary = await getComments(thread);
```

getCommentsはNCMBのCommentクラスからデータを取得します。

**記述してください**

```js
// NCMBからコメントを取得する処理
async function getComments(thread) {
  const Commnet = ncmb.DataStore('Comment');
  return await Commnet
    .equalTo('thread', {
      __type: 'Pointer',
      className: 'Thread',
      objectId: thread.objectId
    })
    .order('createDate', false)
    .limit(100)
    .fetchAll();
}
```

そして取得したコメントを `ons-list` の中に出力します。

```js
// 記述済み：表示対象のDOM
const dom = this.querySelector('#comments');
// 表示用HTMLを蓄積しておく
const html = [];
ary.forEach(comment => {
  // HTMLの組み立て
  html.push(`
    <ons-list-item>
      <div class="left">
        ${addImage.bind(this)(comment)}
      </div>
      <div class="center">
        <span class="list-item__title">${comment.get('body')}</span>
        <span class="list-item__subtitle">
          ${ago(comment.get('createDate'))}
          ${deletable(comment)}
        </span>
      </div>
    </ons-list-item>
  `);
});
// HTMLを流し込み
dom.innerHTML = html.join('');
```

ユーザに削除権限があるかどうか判定してHTMLを返す deletable はすでに紹介した通り `js/app.js` に定義してあります。HTMLを描画したら、イベントリスナーを追加します。

```js
// 記述済み
addEvent.bind(this)();
```

今回は以下のイベントを追加しています。

- 画像をNCMBから読み込んで表示する
- 削除アイコンをタップしたらコメントを削除する

```js
// コメントのイベント設定
function addEvent() {
  // コメントの画像に対するイベント
  this.querySelectorAll('.square-image').forEach(d => {
    // プレイスホルダーから実際の画像に差し替え
    loadImage.bind(this)(d.dataset.name, `#${d.getAttribute('id')}`);
    // クリック時に画像の拡大表示を行うイベントを設定
    d.onclick = () => {
      showImage.bind(this)(d)
    }
  });
  // 削除アイコンをクリックした際のイベント
  this.querySelectorAll('.delete').forEach(d => {
    d.onclick = () => {
      deleteComment.bind(this)(d);
    }
  });
}
```

画像を読み込む関数 `loadImage` はすでに紹介済みです。 `js/app.js` に定義してあります。ここまでで画面の初期表示が完了します。

### イベント処理

スレッド詳細におけるイベント処理は次の通りです。

- コメントダイアログ表示ボタンを押した時
- コメントするボタンを押した時
- 画像ダイアログを閉じる時
- 削除アイコンをクリックした時

#### コメントダイアログ表示ボタンを押した時

この時にはコメント入力用のダイアログを表示するだけです。

```js
// 記述済み：コメント用のダイアログを表示
function openModal() {
  this.querySelector('ons-dialog.comment').show();
}
```

![](https://press.monaca.io/wp-content/uploads/2020/12/monaca-internal-survey-4.png)

#### コメントするボタンを押した時

この時にはNCMBにコメントを追加します。実装内容はスレッドの時とほとんど変わりません。該当するスレッドを登録して、関連づけるのを忘れないでください。

**記述してください**

```js
// コメントを追加する処理
async function addComment() {
  // 変数の準備
  const body = this.querySelector('#body').value;
  const file = this.querySelector('#image').files[0];
  const { thread } = this.data;
  const user = ncmb.User.getCurrentUser();

  // 権限設定
  // 全体に読み込み権限付与
  // adminまたは自分に削除権限付与
  const acl = new ncmb.Acl();
  acl
    .setPublicReadAccess(true)
    .setRoleWriteAccess('admin', true)
    .setUserWriteAccess(user, true);
  // ファイルが指定されている場合はアップロード
  if (file) {
    await ncmb.File.upload(file.name, file, acl);
  }
  // コメントクラスの準備
  const Comment = ncmb.DataStore('Comment');
  const comment = new Comment;
  // 値を設定
  await comment
    .set('body', body)
    .set('thread', thread)
    .set('image', file ? file.name : null)
    .set('acl', acl)
    .save();
  // フォームをリセット
  this.querySelector('#comment-form').reset();
  // ダイアログを非表示
  this.querySelector('ons-dialog.comment').hide();
  // 表示を更新
  showComments.bind(this)();
}
```

#### 画像ダイアログを閉じる時

この時には画像ダイアログを非表示にするだけです。

```js
// 記述済み：閉じるボタンを押した時の処理
function closeDialog() {
  this.querySelector('ons-dialog.image').hide();
}
```

#### 削除アイコンをクリックした時

削除アイコンをタップした際には該当データをCommentクラスから削除します。こちらもスレッドと同様にACL管理されていますので、不正ユーザによるデータ操作は防げますので安心してください。

**記述してください**

```js
// コメントを削除する処理
async function deleteComment(dom) {
  // 削除前の確認
  const res = await ons.notification.confirm('コメントは一度削除すると元には戻せません。削除してよろしいですか？');
  if (res === 0) return; // キャンセルの場合
  // 削除対象のデータを設定
  const Comment = ncmb.DataStore('Comment');
  const comment = new Comment;
  comment.objectId = dom.dataset.objectId;
  // 削除実行
  await comment.delete();
  // 表示を更新
  showComments.bind(this)();
}
```

ここまででスレッド詳細画面が完成となります。

## まとめ

これで掲示板アプリが完成です。今回は次のような画面を持ったアプリを開発しました。

- スレッド一覧画面
- スレッド詳細画面

また、ニフクラ mobile backendの次の機能を利用しました。

- 会員管理
    - 匿名認証
    - ロール（グループ）管理
- データストア
    - Threadクラス
        - データ登録
        - データ削除
        - データ検索
    - Commentクラス
        - データ登録
        - データ削除
        - データ検索
- ファイルストア
  - 画像登録
  - 画像取得

今回のデータの保存と取得、認証といった機能はどのようなアプリでも使える機能だと思います。今後のアプリ開発に応用してください。