FJCT_ニフクラ mobile backend(mBaaS)お役立ちブログ

スマホアプリ開発にニフクラ mobile backend(mBaaS)。アプリ開発に役立つ情報をおとどけ!

NCMBでフォロー/フォロワー機能を作る(その2:Monacaでの画面解説と認証の実装まで)

f:id:mbaasdevrel:20210705184701j:plain

最近、NCMBを使ってユーザ同士がコミュニケーションできる仕組みを構築したいというニーズが出てきています。メッセージングアプリだけでなく、ゲームやフリマ、フォーラムアプリなど、多くのアプリでフォロー/フォロワーの仕組みが利用されます。

今回はコミュニケーション機能の基盤になるであろう、フォロー/フォロワー機能の実装を真剣に検討してみたので、その実装について解説します。前回はデータストアのスキーマについて解説しました。今回から2回に分けて、Monaca(JavaScript SDK)での実装例を解説します。

利用するライブラリ

今回はFramework7を使って実装しています。Framework7はコンポーネントが多く、Onsen UI同様にiOS/Androidそれぞれに合わせたUIを実現できます。

f:id:mbaasdevrel:20210705184423j:plain

画面について

今回の画面は全部で3つです。

ログイン/新規登録画面

f:id:mbaasdevrel:20210705184441j:plain

今回はログインと新規登録を一つの画面で行っています。

ユーザ一覧画面

f:id:mbaasdevrel:20210705184452j:plain

ユーザ(実際にはProfileクラス)を一覧表示する画面です。

ユーザ表示画面

f:id:mbaasdevrel:20210705184508j:plain

一覧で選択されたユーザ(実際にはProfile)を表示する画面です。フォローまたはアンフォローができます。該当ユーザのフォロワー、フォローしているユーザの一覧も表示されます。

Monacaプロジェクトのベースを作成

今回はMonacaでFramework7のCore Tab View テンプレートを選択しています。Monaca IDEからはフレームワークテンプレート > JavaScript > Framework7 Core Tab Viewと辿って選択します。

f:id:mbaasdevrel:20210705184532p:plain

今回編集するのは www/index.htmlwww/jswww/pages 以下になります。他は変更しませんので、今回は解説しません。

JavaScript SDKのインストール

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

f:id:mbaasdevrel:20210705184604p:plain

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

f:id:mbaasdevrel:20210705184613p:plain

js/routes.jsの編集

js/routes.js はFramework7のルーティングを設定するファイルになります。今回は前述の通り3画面になりますので、その指定を行います。後は全体の初期画面( www/index.html)と画面がなかった場合の表示( ./pages/404.html )になります。

const routes = [
  {
    path: '/',
    url: './index.html',
  },
  // ログイン画面
  {
    path: '/login',
    name: 'Login',
    componentUrl: './pages/login.html'
  },
  // 一覧画面
  {
    path: '/list',
    name: 'List',
    componentUrl: './pages/list.html'
  },
  // ユーザ表示画面
  {
    path: '/show/:id',
    name: 'Show',
    componentUrl: './pages/show.html'
  },
  // Default route (404 page). MUST BE THE LAST
  {
    path: '(.*)',
    url: './pages/404.html',
  },
];

NCMB SDKの初期化

NCMB SDKは js/app.js で初期化します。

const applicationKey = 'YOUR_APPLICATION_KEY';
const clientKey = 'YOUR_CLIENT_KEY';
const ncmb = new NCMB(applicationKey, clientKey);

www/index.htmlの編集

今回はタブは使わないので、 bodyタグ内のHTMLは次のようにシンプルな内容になります。 data-url で示している通り、最初に表示されるのは /list で指定されている画面になります。

<div id="app">
  <!-- Views/Tabs container -->
  <div class="views safe-areas">
    <!-- Your main view/tab, should have "view-main" class. It also has "tab-active" class -->
    <div id="view-home" class="view view-main view-init" data-url="/list">
    </div>
  </div>
</div>
<!-- この下にscriptタグが並びます -->

www/pages/list.htmlの内容

まず一覧画面を作成します。ここはデータストアから取得するProfileクラスのデータを一覧表示します。まだデータはないので、何も表示されませんが、画面の準備はしておきます。

<template>
  <div class="page">
    <div class="navbar">
      <div class="navbar-bg"></div>
      <div class="navbar-inner sliding">
        <div class="title">ユーザ一覧</div>
      </div>
    </div>
    <div class="page-content">
      <div class="data-table profiles">
        <table>
          <thead>
            <td>ユーザ名</td>
            <td>フォロー</td>
            <td>フォロワー</td>
          </thead>
          <tbody>
          </tbody>
        </table>
      </div>
    </div>
  </div>
</template>

そして画面を表示した際に、認証状態をチェックします。もしログインしていないようであれば、 www/pages/login.html に遷移します。

<script>
  export default async function (props, {$f7, $f7router, $on, $onBeforeMount, $onMounted, $onBeforeUnmount, $onUnmounted }) {
    // 画面表示前に実行されるイベント
    $on('pageBeforeIn', async (e, page) => {
      // 認証状態のチェック
      if (!(await checkAuth())) {
        // false が返ってきたらログイン画面に遷移
        return $f7router.navigate({name: 'Login'});
      }
      // 省略
    });
  }
</script>

関数 checkAuth の内容は次のようになります。認証情報はlocalStorageに保存されますが、そのセッション有効性はチェックされません。そこで、認証状態を localStorage から復元した後、セッションの有効性をチェックしています(適当なクラス名のデータストアにアクセスします)。これが失敗する場合はセッションが無効になっていると判断して、認証無効(false)を返します。

checkAuth は js/app.js に定義しています。

// 認証状態をチェックする関数
// 認証が問題なければ true / ログインしていない or セッションに問題がある場合は false
const checkAuth = async () => {
  // 現在のログインユーザを取得
  const user = ncmb.User.getCurrentUser();
  // データがない場合は false を返す
  if (!user) return false;
  try {
    // セッションの有効性チェック
    await ncmb.DataStore('Test').fetch();
    // 問題なければ true
    return true;
  } catch (e) {
    // セッションに問題がある場合は false
    return false;
  }
}

www/pages/login.html の編集

続いてログイン/新規登録画面についてです。こちらは以下3つの情報を入力します。

  • 表示名
    displayName
  • ログインID
    userName
  • パスワード
    password
<template>
  <div class="page" data-name="home">
    <!-- Top Navbar -->
    <div class="navbar">
      <div class="navbar-bg"></div>
      <div class="navbar-inner">
        <div class="title sliding">ログイン</div>
      </div>
    </div>
    <!-- Scrollable page content-->
    <div class="page-content">
      <div class="list">
        <form id="login">
          <ul>
            <li class="item-content item-input">
              <div class="item-inner">
                <div class="item-title item-label">表示名(登録時のみ)</div>
                <div class="item-input-wrap">
                  <input type="text" name="displayName" placeholder="ニフクラ 太郎" />
                </div>
              </div>
            </li>
            <li class="item-content item-input">
              <div class="item-inner">
                <div class="item-title item-label">ユーザ名</div>
                <div class="item-input-wrap">
                  <input type="text" name="userName" placeholder="Your username" />
                </div>
              </div>
            </li>
            <li class="item-content item-input">
              <div class="item-inner">
                <div class="item-title item-label">パスワード</div>
                <div class="item-input-wrap">
                  <input type="password" name="password" placeholder="Your password" />
                </div>
              </div>
            </li>
          </ul>
        </form>
      </div>
      <div class="list">
        <ul>
          <li>
            <a href="#" @click=${signInOrLogin} class="item-link list-button login-button">新規登録 & ログイン</a>
          </li>
        </ul>
      </div>
    </div>
  </div>
</template>

f:id:mbaasdevrel:20210705184701j:plain

ログイン/新規登録処理について

上記のテンプレートで定義されている通り、ボタンを押したタイミングで signInOrLogin 関数が呼ばれます。Framework7ではexport default の中に実装していきます。

<script>
export default (props, { $f7, $update, $f7router, $ }) => {
  // この中に記述します。

  return $render;
}
</script>

signInOrLogin の内容は次のようになります。

// 新規登録 & ログインボタンをタップした際のイベント
const signInOrLogin = async () => {
  // 入力値をオブジェクト化(app.jsにて定義)
  const params = serializeForm('form#login');
  // ユーザ登録処理    
  try {
    await registerUser(params);
  } catch (e) {
    // すでに同じ名前で登録されている場合はエラー
    // 今回は無視して次に進みます
  }
  try {
    // ログインと権限設定の処理
    await loginUser(params);
    // 前の画面に戻る
    $f7router.back();
  } catch (e) {
    // ログイン失敗したらアラート
    $f7.dialog.alert('ログイン失敗しました。ID、パスワードを確認してください');
  }
}

serializeFormjs/app.js の中に定義しています。フォームの内容をオブジェクト化して、アクセスしやすくする関数です。

// フォームの内容をオブジェクト化します
const serializeForm = (ele) => {
  // フォームを取得します
  const f = new URLSearchParams(new FormData($(ele)[0]));
  // フォームの内容をオブジェクトに入れ直します
  const params = {};
  for (const values of f) {
    params[values[0]] = values[1];
  }
  return params;
}

registerUser は次のようになります。入力されたユーザIDとパスワードでユーザ登録処理を実行します。

// ユーザ登録処理
const registerUser = (params) => {
  const user = new ncmb.User;
  // 入力値をセットして、ユーザ登録処理を実行
  return user
    .set('userName', params.userName)
    .set('password', params.password)
    .signUpByAccount();
}

この処理はすでにユーザが登録されていた場合、エラーになります。今回は登録とログイン画面を共通のものにしているので、既存ユーザのログイン処理時には必ずエラーを起こすでしょう。しかし、catchでエラーを補足し、無視しています。すでに上記で記述済みですが、以下の処理部分になります。

// ユーザ登録処理    
try {
  await registerUser(params);
} catch (e) {
  // すでに同じ名前で登録されている場合はエラー
  // 今回は無視して次に進みます
}

そしてユーザ登録処理の後に、ログイン処理を実行します。getProfile (後述)の返り値の有無によって、新規登録か既存ユーザかの判別を行っています。既存ユーザの場合は、すぐに処理を終了します。新規ユーザ登録者の場合には初期データとして、 ProfileFollow クラスを作成しています。詳細はコードを参考にしてください。

const loginUser = async (params) => {
  // ログイン処理
  const user = await ncmb.User.login(params.userName, params.password);
  const profile = await getProfile();
  // 新規データか判断
  if (profile.objectId) return;
  // ログインしたら、ACL(アクセス権限を設定)
  const acl = new ncmb.Acl;
  acl
    .setPublicReadAccess(true)         // 誰でも閲覧可能
    .setRoleWriteAccess('Admin', true) // 管理者権限があれば書き込み可能
    .setUserWriteAccess(user, true);   // 自分も編集可能
  await profile
    .set('user', user)
    .set('displayName', params.displayName)
    .set('follows_count', 0)                 // 初期値
    .set('followers_count', 0)               // 初期値
    .set('acl', acl)
    .save();
  // フォロー/フォロワーの情報が入るクラス
  const Follow = ncmb.DataStore('Follow');
  const follow = new Follow;
  return follow
    .set('profile', profile) // リレーション
    .set('user', user)       // リレーション
    .set('follows', [])      // 初期値
    .set('followers', [])    // 初期値
    .set('acl', acl)
    .save();
}

getProfile 関数は次のようになります。これは js/app.js に定義しています。

// ユーザプロフィールを返します。なければ新規データを返します。
const getProfile = async () => {
  // ログインユーザを取得
  const user = ncmb.User.getCurrentUser();
  // Profileクラス(検索用)を準備
  const Profile = ncmb.DataStore('Profile');
  // 自分のデータを検索します
  const profile = await Profile
    .equalTo('user.objectId', user.objectId)
    .fetch();
  // データがあれば(objectIdがあれば)データを、なければ新規Profileを返します
  return profile.objectId ? profile : new Profile;
}

管理者ユーザの作成

NCMBの管理画面にログインして、Adminグループに所属するユーザを作成します。まず Admin グループを作成します。

f:id:mbaasdevrel:20210705184730p:plain

そしてAdminグループが選ばれている状態で新しい会員を追加してください。こうすることで、該当ユーザをAdminグループに追加できます。

f:id:mbaasdevrel:20210705184739p:plain

コード

コードはNCMBMania/follower_app_monaca: Monacaで実装したフォロー/フォロワー機能ですにアップしてあります。実装時の参考にしてください。

まとめ

今回はMonacaアプリとしての画面に関する説明と、認証画面までを作成しました。次回はユーザ一覧と、詳細表示、そしてフォロー/アンフォロー機能について解説します。

中津川 篤司

中津川 篤司

NCMBエヴァンジェリスト。プログラマ、エンジニアとしていくつかの企業で働き、28歳のときに独立。 2004年、まだ情報が少なかったオープンソースソフトの技術ブログ「MOONGIFT」を開設し、毎日情報を発信している。2013年に法人化、ビジネスとエンジニアを結ぶDXエージェンシー「DevRel」活動をスタート。