Monaca, ncmb, OnsenUIを使用したアプリ開発で詰まったこと

2020-11-24

こんにちは、0371です。

今回はMonaca, ncmb, OnsenUIを使用したアプリ開発を行ったので、その備忘録を残したいと思います。
特にncmbが特殊だったので、その辺りを重点的に書いていきます。

サービスの説明

Monacaとは、HTML&CSS, javascriptでスマートフォンアプリを作成することができるアプリ開発のサービスです。
iOSAndroidの両方に対応するアプリを作成できます。 クラウドIDEで開発することも可能です。

Monaca
"https://ja.monaca.io/"

ncmbはアプリ開発のバックエンドの機能を、クラウドで提供するサービスです。
mBaaS(mobile backend as a Service)に分類されます。 正式名称は「ニフクラ mobile backend」です。

ニフクラ mobile backend
"https://mbaas.nifcloud.com/"

OnsenUIiOSAndroidの両方に対応するUIを実現できるUIコンポーネントです。

Onsen UI "https://ja.onsen.io/"

これら3つのサービスを利用し、スケジュール管理アプリを制作することになりました。
アプリの作成にあたり、Monacancmbの利用は必須条件となっています。

プロジェクトの期間は1ヶ月ですが、ドキュメントの整備なども含めるので、開発は実質20時間程度です。
4人チームでの開発で、ドキュメント作成担当が2人、プログラム担当が2人でした。
人数が少ないので兼任する場面もありました。

各サービスの良かったところ

アプリ制作において、使用したサービスの良かったところを書いていきます。

  1. MonacaにクラウドIDEがあり、かつncmbとの連携が簡単であるため、環境構築の手間が省けた。

ローカルな環境でアプリ開発をするためには、「エディターを用意して〜。必要なパッケージをインストールして〜。」という作業をしなくても済むのは、とても良かったと思います。
Monaca側のドキュメントでncmbとの連携方法が詳しく書かれていたのと、スターターセットのようなサンプルコードも用意されていたので、時短になりました。
環境構築でハマってしまったら、最初から心が折れていたと思います。(白目)

  1. ncmbにサンプルコードをコピペするだけで実現可能なメールアドレス認証があった(デメリットあり)。

スケジュール管理アプリには、ユーザー登録処理が必要なので、その辺りをコピペするだけで気軽に実現できるサンプルコードがあったのも助かりました。
ただし、ncmbで提供しているソースコードはブラックボックスになっている処理があるため、UIの変更はできるが、ユーザー登録処理の変更は難しい(自分にはできませんでした)というデメリットがあります。
そのため、サインアップ・ログイン処理でかなりの時間を費やしてしまいました。 例として、メールアドレス会員登録ページのテンプレートを挙げます。

<!DOCTYPE html>

~~~ 割愛 ~~~

<link rel="stylesheet" href="../../css/style.css" type="text/css" media="all" />
<script type="text/javascript" src="../../system/config.js" charset="UTF-8"></script>
<script src="https://code.jquery.com/jquery.min.js"></script>
<script type="text/javascript" src="../../system/mailAddress.js" charset="UTF-8"></script>
<script>
  var set = null;

  window.onload = function() {
    set = new Set();
  }

  function check() {
    set.checkForm();
  }
</script>

~~~ 割愛 ~~~
    <div id="contents">
      <form name="mailAddressForm">
        <div class="unit">
              <p>パスワードを入力してください。</p>
          <input type="password" name="password">
        </div>
        <div class="unit">
              <p>確認の為もう一度入力してください。</p>
          <input type="password" name="checkPassword">
        </div>
      </form>
      <div class="align-r">
         <a class="btn font-l" onclick="check()">登録する</a>
      </div>
~~~ 割愛 ~~~

CSSjavascriptの参照先がncmbのサーバー内のファイルを参照しているので、中身が見れません。
"https://github.com/NIFCLOUD-mbaas/ncmb_js" に公開されているソースコードを覗いてみても、処理を行っているファイルを見つけることができませんでした。(自分の目が節穴だったら教えてください)
そのため、このページ内でパスワード以外の項目(名前や所属クラスなど)を入力させてDBに保存させたくても、どのようにコードを書き換えればいいかがわからなかったです。

回避策として、メールアドレスでログインさせた後に、user_idを持たないユーザーに対して、名前や所属クラスを入力させるページに飛ばし、二段階入力をさせる仕様に変更しました。

以下のjavascriptでログイン後に遷移する画面の分岐を行っています。
javascript初心者なので、あまり良いコードではないかもしれませんが、参考になりそうなところは使用してください。
ncmb.User.loginWithMailAddressncmb独自の定義された関数です。

// ログイン処理
function login_authentication(){
  // HTMLのinputタグのvalueから値を取ってくる。 
  let login_email = document.getElementById("login_email").value;
  let login_password = document.getElementById("login_password").value;
  
  ncmb.User.loginWithMailAddress(login_email, login_password)
  .then(function(data){
    // 成功時処理 
    // ログインしたユーザーにuser_idが付与されていない時
    if(!ncmb.User.getCurrentUser().user_id){
      window.location.href = 'userCreate.html';
    } else {
      // セッションストレージにログイン中のユーザーのIDを保存する。
      sessionStorage.setItem('currentUserId', user_id);
      window.location.href = 'schedule.html'; 
    };
  })
  .catch(function(err){
    // エラー処理
    alert("ログインに失敗しました。\nお手数をおかけしますが、もう一度ログインを行ってください。"+err);
    // ログイン画面に遷移
    window.location.href = 'login.html'; 
  });
}

そして、遷移後にもう一度ユーザー情報を追加する処理を行います。
ユーザー情報入力画面でinputタグに情報を入力させ、遷移後のユーザー情報確認画面で送信ボタンを押すことで、DB処理を行っています。
以下は、ユーザー情報入力画面のHTMLファイルの一部です。

<form class="" name="mailAddressForm">
   <!-- 名前 -->
   <label class="" for="name">名前を入力してください。</label>
   <input id="user_add_name" type="text" name="user_add_name" required>

   <!-- クラス・教員 -->
   <label class="mr-2" for="_class">クラス・教員を選択してください。</label>
   <select id="user_add_class" name="user_add_class">
      // データストアからフィールドの値を取得してoptionタグを生成
   </select>

   <!-- 委員会 -->
   <label class="mr-2" for="comittee">委員会を選択してください。</label>
   <select id="user_add_committee" name="user_add_committee">
      // データストアからフィールドの値を取得してoptionタグを生成
   </select>
</form>

これを会員管理のDBに追加する処理は以下の通りです。
ユーザー情報確認画面で処理を行っています。
user_idに関しては別の処理で追加しています。

// ログインしているユーザーのデータを取得する
myAccount = ncmb.User.getCurrentUser();

// ログインユーザーの情報を更新する(ログアウトしなくてもOKだった)
// 更新扱いなので.update()を使用
myAccount
   .set("name", name)
   .set("class_id", _class)
   .set("committee_id", committee)
   .update()
   .then(function(user) {
      // 成功時処理
      alert("新規登録に成功");
      window.location.href = 'schedule.html';
   })
   .catch(function(error) {
      alert("新規登録に失敗!次のエラー発生:" + error);
});

全くスマートではないので、良い方法があったらぜひTwitterのDMで教えてください(滝汗)

  1. OnsenUIを使用することで、スマホ用のデザインが簡単に作れる。(デメリットあり)

こちらもデメリットがありましたが、ササっとアプリを構築したい方にはとても良いと思いました。
OnsenUI独自のタグを使用して、スマホ対応のUIを簡単に作成することができます。
ドキュメントのソースコードを見れば、どういう動きをするのかがわかるので敷居が低いです。
OnsenUIには、"ons-splitter-content" というコンポーネントが存在します。
これは非常に良いもので、一つのHTMLファイルの中にいくつかの画面レイヤーを作成できるコンポーネントです。
Androidでいうインテントのような挙動をします。
実際に動かした方が伝わると思うので、上記のリンクから挙動を試してみてください。

初めはこのons-splitter-contentを使用して開発を進めていました。
理由としては、画面の遷移前から遷移後へ値を受け渡す方法がわからなかったためです。
ons-splitter-contentであれば画面が遷移したように見えるだけで、実際には遷移していないため、値を受け渡す必要がなく、実装が楽だと感じたため採用しました。

しかし、世の中そんなに甘くはありませんでした。
ons-splitter-contentを使用した状態でデータストアから値を取得し、それをwindow.onload関数でHTMLoptionタグを生成する処理を行うと、生成に失敗しました。
エラーが出ないため原因は特定できませんでしたが、おそらく画面のレイヤーの下層(アクセス時に最初にユーザーが目にする画面ではない別の画面)で、window.onload関数を使用しタグの生成を行おうとすると失敗してしまうようです。

回避策として、値の受け渡しにはセッションストレージを使用し、ons-splitter-contentを廃止してHTMLファイルを独立させました。
セッションストレージはブラウザにある一時的な保存領域のようなもので、値の保存や取り出しがとても簡単です。
もう少し早くセッションストレージの存在を知っていれば、開発期間にゆとりが持てたかもしれません。(涙)

ons-splitter-contentの廃止によって、OnsenUIを使用する意味が薄れてきたこともあり、結局Bootstrap4に切り替えました。(とほほ)

OnsenUIBootstrap4よりもtailwindcssのような、javascriptを使わないcssフレームワークの方が予期せぬ問題が生じにくいかもしれませんね。

各サービスのつまづきポイント

プログラミング力が少ない我々は、各サービスの恩恵を受けるよりも、デメリットの方が多かったように思えます。

  1. 当たり前だけど、クラウドIDEを使用するときは、二台のパソコンで1つのアカウントにアクセスしてはいけない

一度だけ犯したミスなのですが、家に持ち帰ってMonacaのクラウドIDEを使用して作業をしていて、ブラウザのタブを切り忘れたまま、学校で作業をしようとします。
すると、家の方のタブで開きっぱなしになっているソースファイルは編集ができなくなります。(当たり前ですけども)
ですので、別PCで作業し終了する時は、必ずMonacaのタブを切ったことを確認しましょう。

  1. 当たり前だけど、gitは使った方がいい

gitに関する知識が薄いチームメンバーがいたため、gitを使用しないで開発を進めていました。
しかし、gitを学習する手間よりも、いちいち手作業でファイルを同期する手間の方が多かったため、gitは使用した方がいいです。
Monacaにもgitと連携できる機能があるようなので、プロジェクトの立ち上げの時点で活用するといいと思います。

  1. サンプルアプリのソースコードはよく読み込んでおくといい

これは、世の中にあまりMonacancmbを使用したアプリ開発をしている記事がないからです。
比較的マイナーな技術を扱うときには、公式が提供しているソースコードをよく読んでおいた方がいいです。
処理と動作がわかれば、スムーズにコードを書いていくことができると思います。 特に初心者の方は。(自戒)

  1. ncmbで不具合が生じていると、コードは合ってるのに処理がおかしくなることがある

データストア内のレコードの削除を行おうとしたときにこの現象に合いました。
普段からクラウドサービスを触っている人からすると普通のことなのかもしれませんが、クラウドサービスは障害発生の頻度が高いように思えます。(それが良いか悪いかの話ではなく)
そのため、ncmbに処理をお願いするコードを実行する前に、公式の障害情報を確認すると良いでしょう。
自分たちは、障害情報に気がつかずに「なぜうまくいかないんだ...」と無駄に時間を過ごしてしまいました。
障害が発生している間は、別の作業をしましょう。

その他

その他、開発をしているときに得た知識を羅列します。

  1. データストアに格納できる型は、文字列、配列、数値、日付、真偽値、緯度経度、オブジェクトである。

日付は 2013-09-06T01:51:03.606Z のような形で格納されます。

  1. 会員管理の更新は、更新したいユーザがログイン中でも可能

一度ログアウトしないとデータの更新ができないかなと思っていましたが、ログアウトしなくても可能でした。

  1. すでに作成されている会員管理のレコードに何かの項目を追加したいときは、saveメソッドではなく、updateメソッドを使用する。

なんでもかんでもsaveだと思ってました。(白目)

  1. メールアドレスの会員管理にはデフォルトで、objectId, userName, password, mailAddress, authData, sessionInfo, mailAddressConfirm, temporaryPassword, createDate, updateDate, acl の列がある

objectIdはハッシュ生成で、autoincrementではありませんでした。
そのため、独自でIDを作成する必要があります。
何か列に追加したい場合は、set()update()をしましょう。

  1. MonacaのクラウドIDEのエントリーポイントはindex.htmlで固定

最初のトップ画面などはindex.htmlに作成しましょう。

  1. データストアにはデフォルトで、objectId, createDate, updateDate, acl の列がある

aclというのは、読み書きなどのアクセス許可のことです。

  1. ncmbのポインターの使い方

ポインターとは、クラス同士を1対1の関係で結びつけるものです。
以下のコードは次のような処理を想定しています。

まず、セッションストレージから入力値を取得する。
データストアからテーブルを参照し、インスタンスを生成する。
スケジュールインスタンスに入力値をセットして保存する。
保存後に、スケジュールテーブルに新しく保存されたレコードを取得する。
取得したレコードのobjectIdを使用してnew_scheduleインスタンスを生成する。
会員管理テーブルのインスタンスにnew_scheduleインスタンスをセットする。
会員管理テーブルのインスタンスの保存に成功したとき、画面遷移する。

この順番で処理を行っています。 あまり参考にならないかもしれませんが。(白目)

//セッションストレージから入力値を取得する。
      var begin_date_value = sessionStorage.getItem('begin_date_value');
      var begin_time_value = sessionStorage.getItem('begin_time_value');
      var end_date_value = sessionStorage.getItem('end_date_value');
      var end_time_value = sessionStorage.getItem('end_time_value');
      var title_value = sessionStorage.getItem('title_value');
      var remark_value = sessionStorage.getItem('remark_value');
      var category_value = sessionStorage.getItem('category_value');
      var range_value = sessionStorage.getItem('range_value');
      var user_objectId = sessionStorage.getItem("user_objectId");

      var category_array = category_value.split(',');
      var category_objectId = category_array[0];
      var category_name = category_array[1];


// データストアからテーブルを参照し、インスタンスを生成する。
function suchedule_add_onclick(){
  //スケジュールテーブル参照
  var Schedule = ncmb.DataStore("schedule");
  var schedule_instance = new Schedule();

  //ユーザスケジュールテーブル参照(中間テーブル)
  var User_schedule = ncmb.DataStore("user_schedule");
  var user_schedule_instance = new User_schedule();

  //カテゴリテーブル参照
  var Category = ncmb.DataStore("category");
  var category_instance = new Category({objectId:category_objectId});

  // スケジュールインスタンスに入力値をセットして保存する。
  schedule_instance.set("begin_datetime",begin_date_value+"T"+begin_time_value)
          .set("end_datetime",end_date_value+"T"+end_time_value)
          .set("title",title_value)
          .set("remark",remark_value)
          .set("category",category)
          .save()
          .then(function(res){
            // 保存後の処理
            // 保存後に、スケジュールテーブルに新しく保存されたレコードを取得する。
            Schedule.order("createDate",true)
                    .fetch()
                    .then(function(order_suchedule){
                      // 取得したレコードの`objectId`を使用してnew_scheduleインスタンスを生成する。
                      var new_schedule_objectId = order_suchedule.get("objectId");
                      var new_schedule = new Schedule({objectId:new_schedule_objectId});
                      // 会員管理テーブルのインスタンスに`new_schedule`インスタンスをセットする。
                      user_schedule_instance.set("user_objectId",user_objectId)
                                   .set("schedule",new_schedule)
                                   .save()
                                   .then(function(res){
                                      // 会員管理テーブルのインスタンスの保存に成功したとき、画面遷移する。
                                      alert("スケジュールを追加しました。");
                                      window.location.href = 'schedule.html';
                                }).catch(function(err){
                                  alert(err);
                                })
                    })
                    .catch(function(err){
                      alert(err);
                    });

          })
          .catch(function(err){
             alert(err);
          });
  }

まとめ

結果として、納期に間に合いませんでした。(滝汗) 調査の時間が不足していたこと、ネットの情報が少なかったこと、自分たちの実力が足りなかったことなどが原因として挙げられます。

ただし、経験として得たものは多くあります。 次回の開発に活かして行きたいです。