MonacaとNCMBで目安箱アプリを作ってみよう

こちらは[目安箱アプリを作ってみよう【その1：画面の説明と匿名認証まで】 – モナカプレス](https://press.monaca.io/atsushi/6208)と[目安箱アプリを作ってみよう【その2：意見の投稿と一覧画面の作成】 – モナカプレス](https://press.monaca.io/atsushi/6222)をハンズオン用に再編集したものです。

## 体験できるもの

- Monacaを使ったアプリ開発
- Onsen UIを使ったネイティブアプリ風UIの作成法
- NCMBの匿名認証
- NCMBのデータストアへのデータ保存

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

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

- Onsen UI
- ニフクラ mobile backend

## ベースアプリ

ベースアプリは [NCMBMania/Monaca_Survey_App: Monacaの目安箱アプリ開発ハンズオンのベースです。](https://github.com/NCMBMania/Monaca_Survey_App) になります。下記URLをZipインポートとして指定してください。

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

## 仕様

このアプリの仕様、機能は次の通りです。

### 入力画面

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

目安箱に要望を投稿する画面です。この画面が最初に表示されます。入力項目は次の通りです。

- 要望

この画面ではログインしているかどうかを確認します。ログインしていなかった場合、匿名認証を実行します。

### ご意見一覧画面

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

目安箱に投稿された要望を閲覧する画面です。通常ユーザは自分の投稿した要望だけを一覧できます。管理者グループのユーザは他のユーザの投稿も見られます。自分の投稿であれば、タップして編集画面に遷移します。編集画面は入力画面を利用しています。

### 要望データの権限

要望データは本人と管理者ユーザだけが閲覧できます。また、要望の編集が行えるのは本人のみです。

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

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

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

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

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

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

<img src="https://press.monaca.io/wp-content/uploads/2020/11/daily_app_11.png" alt="" />

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

<img src="https://press.monaca.io/wp-content/uploads/2020/11/daily_app_9.png" alt="" />

## NCMBの初期化

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

```js
// NCMBの初期化
const applicationKey = 'YOUR_APPLICATION_KEY';
const clientKey = 'YOUR_CLIENT_KEY';
window.ncmb = new NCMB(applicationKey, clientKey);
```

## index.html の紹介

Monacaアプリで一番最初に読み込まれる `www/index.html` では、タブバーのOnsen UIとなっています。読み込むファイルは、意見の投稿を行う `form.html` と 閲覧を行う `view.html` となっています。

```html
<ons-page>
  <ons-tabbar position="auto">
    <ons-tab label="投稿" page="form.html" active>
    </ons-tab>
    <ons-tab label="閲覧" page="view.html">
    </ons-tab>
  </ons-tabbar>
</ons-page>
```

## 入力画面実装

入力画面 `form.html` のHTMLを下記に紹介します。基本的に日報の入力項目を表示する部分と、入力した内容を表示する `#reports` があるだけです。

```html
<!-- 記載済み -->
<ons-page>
  <ons-toolbar>
    <div class="center">入力フォーム</div>
  </ons-toolbar>
  <div style="text-align: center; margin-top: 60px;">
    <ons-row>
      <ons-col>
        <input type="hidden" id="objectId" />
        <textarea id="body" class="textarea" rows="5" cols="40" placeholder="ご意見を入力してください"></textarea>
      </ons-col>
    </ons-row>
    <p style="margin-top: 30px;">
      <ons-button id="post">目安箱に投稿する</ons-button>
    </p>
  </div>
</ons-page>
```

### JavaScriptについて

JavaScriptでは画面を表示した時にログインされているかどうかをチェックします。この処理は入力画面が表示された際に呼ばれる処理の `ons.getScriptPage().onShow` の中に実装します。

```js
// 記載済み
/*
  この画面が表示される度に実行されるメソッド
*/
ons.getScriptPage().onShow = async function() {
  // 匿名認証実行
  await loginCheck();
  if (this.data && this.data.objectId) {
    const post = await fetchPost(this.data.objectId);
    this.querySelector('#body').value = post.body;
    this.querySelector('#objectId').value = post.objectId;
  }
}
```

ログイン状態の確認はニフクラ mobile backendの会員管理機能を利用します。まず `ncmb.User.getCurrentUser()` でログインしているユーザの情報を取得します。これが空の場合には未ログイン状態なので、匿名認証処理を実行します。

**www/js/app.js に実装してください**

```js
/*
  認証状態の確認を行い、ログインしていなかった場合は匿名認証を実行する
*/
async function loginCheck() {
  const user = ncmb.User.getCurrentUser();
  if (!user) {
    // 未ログインの場合
    await ncmb.User.loginAsAnonymous();
    return true;
  }
  try {
    // セッションの有効性を確認
    await ncmb.DataStore('Test').fetch();
  } catch (e) {
    // セッションが不正だった場合は匿名認証し直し
    await ncmb.User.loginAsAnonymous();
  }
  return true;
}
```

匿名認証はユーザIDやパスワードといった情報なしで認証機能を提供します。デバイス個別のIDを生成し、それを認証に利用します。IDはセッションが切れると別なものが生成されてしまうので注意してください。目安箱は匿名の方が入力しやすいので、今回は匿名認証を利用しています。

もしユーザ情報が取得できたとしても、そのセッションが有効かどうかは分かりません。それを確かめるために、適当なクラス（今回はTestクラス。存在しなくても大丈夫です）へリクエストします。これが失敗する場合にはセッションが無効（有効期限切れなど）となっています。その場合には再度匿名認証を実行します。

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

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

<img src="https://press.monaca.io/wp-content/uploads/2020/11/daily_app_3.png" alt="" />

この `admin` グループを表示している状態を会員を追加したり、既存会員のobjectIdを使ってグループに追加できます。この管理グループにいる場合、他のユーザのデータも見られるようになります。これは匿名認証のユーザでも指定できます。

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


また、アプリ設定画面にて匿名認証を有効にしてください。これはアプリ設定をクリックした後の基本の一番下に設定があります。

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

## 目安箱への投稿

匿名認証が終わったら、目安箱への投稿を実装していきます。これは投稿ボタン `ons.getScriptPage().onInit` を押した際のイベントとして実装していきます。

```js
/*
  初期化時に一度だけ実行されるメソッド
*/
ons.getScriptPage().onInit = async function() {
  this.querySelector('#post').onclick = () => postSurvey.bind(this)();
}

/*
  目安箱への投稿を行う
*/
async function postSurvey() {
  // この中に実装します
}
```

処理の内容です。まず投稿に必要な情報を収集します。

```js
// 実装済み
const body = this.querySelector('#body');
const objectId = this.querySelector('#objectId').value;
if (body.value.trim() === '') {
  ons.notification.alert('ご意見を入力してください');
  return;
}
```

次にニフクラ mobile backendのデータストアに保存する準備をします（別関数 insertPost として定義しています）。詳細はコメントを参照してください。注意点としてACL（アクセス権限）として、データを入力した本人は読み書き可能、adminグループに対して読み込み権限を付与しておきます。こうすることで、adminグループに所属しているユーザはデータ閲覧が可能になります。

```js
async function insertPost(body, objectId) {
  // 投稿用のクラス（Post）を準備
  const Post = ncmb.DataStore('Post');
  // Postクラスのインスタンス（DBでいう行相当）を準備
  const post = new Post();
  // 本文を設定
  post.set('body', body);

  // ACL（アクセス権限）の準備
  const acl = new ncmb.Acl();
  const user = ncmb.User.getCurrentUser();
  acl
    .setUserReadAccess(user, true)
    .setUserWriteAccess(user, true)
    .setRoleReadAccess('admin', true);
  post.set('acl', acl);
  // 保存
  await post.save();
}
```

後は保存処理を実行します。

```js
// 実装済み
try {
  await insertPost(body.value, objectId);
  ons.notification.alert('ご意見ありがとうございました！');
  body.value = '';
} catch (e) {
  ons.notification.alert('エラーが発生しました。もう一度お試しください。');
}
```

## 目安箱の投稿表示

次に目安箱への投稿を表示する画面を作成します。これは `view.html` に実装します。HTMLは次のようになっています。

```html
<ons-navigator id="nav">
  <ons-toolbar>
    <div class="center">意見閲覧</div>
  </ons-toolbar>
  <div style="text-align: center; margin-top: 60px;">
    <ons-list id="posts">
    </ons-list>
  </div>
</ons-navigator>
```

この画面が表示されたタイミングで、投稿されているデータを取得して表示します。つまり `ons.getScriptPage().onShow` での処理になります。

```js
/*
  この画面が表示される度に実行されるメソッド（実装済み）
*/
ons.getScriptPage().onShow = async function() {
  // 匿名認証実行
  await loginCheck();
  const posts = await loadPosts();
  showPosts.bind(this)(posts);
}
```

実際の処理は次のようになります。まずニフクラ mobile backendからデータを取得します。

```js
async function loadPosts() {
  return await ncmb.DataStore('Post')
    .order('createDate')
    .fetchAll();
}
```

そして取得したデータを `ons-list-item` で表示します。この時、自分に書き込み権限があるかどうかを判断して、それによって編集画面に遷移できるかどうかを判定しています。

```js
// 実装済み
function showPosts(posts) {
  const user = ncmb.User.getCurrentUser();
  this.querySelector('#posts').innerHTML = '';
  // データを順番に表示
  posts.forEach(p => {
    // <ons-list-item>を準備
    const i = document.createElement('ons-list-item');
    // 本文を準備
    let text = p.body;
    // 自分に書き込み権限があるかチェック
    if (p.acl[user.objectId] && p.acl[user.objectId].write) {
      // あれば編集用アイコンの表示
      text = `${p.body} ✏️`;
      // 編集イベントの設定
      i.setAttribute('tappable', true);
      i.onclick = () => {
        // form.htmlに遷移
        this.querySelector('#nav').pushPage('form.html', {data: { objectId: p.objectId}});
      }
    }
    // 画面に表示
    i.appendChild(document.createTextNode(text));
    this.querySelector('#posts').appendChild(i);
  });
}
```

自分が編集できるデータをタップした際には、投稿画面 `form.html` に遷移します。この時、ニフクラ mobile backendのデータ管理上のユニークIDであるobjectIdを次の画面に送っています。


## 投稿の編集処理

編集処理を実装する際には、すでに実装されている保存処理とうまく流用しながら実装していくのがいいでしょう。そうすることで余分な開発が減ります。まず、一覧画面から送られてきた投稿データを表示する部分を作ります。これは `ons.getScriptPage().onShow` の中に実装します。

HTMLのhiddenを使ってobjectIdを隠しておきます。こうすることで新規保存なのか更新なのかを区別します。

```js
/*
  この画面が表示される度に実行されるメソッド
*/
ons.getScriptPage().onShow = async function() {
  // 匿名認証実行
  await loginCheck();
  if (this.data && this.data.objectId) {
    const post = await fetchPost(this.data.objectId);
    this.querySelector('#body').value = post.body;
    this.querySelector('#objectId').value = post.objectId;
  }
}

// 実装してください
async function fetchPost(objectId) {
  return await ncmb.DataStore('Post')
    .equalTo('objectId', objectId)
    .fetch();
}
```

### 更新処理

新規保存と更新の違いは次の通りです。

- 新規の場合は `save` メソッド。更新の場合は `update` メソッド。
- 更新の場合は objectId あり。新規の場合はなし。
- 更新の場合は一覧画面に移動

これらの仕様を反映した保存処理が次の通りになります。詳細はコメントを参照してください。

```js
// 更新してください
async function insertPost(body, objectId) {
  const Post = ncmb.DataStore('Post');
  const post = new Post();
  if (objectId) {
    post.set('objectId', objectId);
  }
  post.set('body', body);

  const user = ncmb.User.getCurrentUser();
  const acl = new ncmb.Acl();
  acl
    .setUserReadAccess(user, true)
    .setUserWriteAccess(user, true)
    .setRoleReadAccess('admin', true);
  post.set('acl', acl);
  await (objectId ? post.update() : post.save());
}
```

## まとめ

ここまでで目安箱アプリが完成となります。今回は次のような画面を持ったアプリを開発しました。

- ご意見投稿画面
- ご意見一覧画面
- ご意見編集画面

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

- 会員管理
  - 匿名認証
- データストア
  - Postクラス
    - データ登録
    - データ更新
    - データ検索

今回のデータの保存と取得、更新といった機能はどのようなアプリでも使える機能だと思います。また匿名認証はIDやパスワードの入力が不要な分、ユーザストレスの少ない認証なのでお勧めです。今後のアプリ開発に応用してください。