最近、NCMBを使ってユーザ同士がコミュニケーションできる仕組みを構築したいというニーズが出てきています。メッセージングアプリだけでなく、ゲームやフリマ、フォーラムアプリなど、多くのアプリでフォロー/フォロワーの仕組みが利用されます。
今回はコミュニケーション機能の基盤になるであろう、フォロー/フォロワー機能の実装を真剣に検討してみたので、その実装について解説します。前回はデータストアのスキーマについて解説しました。今回から2回に分けて、Monaca(JavaScript SDK)での実装例を解説します。
- 利用するライブラリ
- 画面について
- Monacaプロジェクトのベースを作成
- JavaScript SDKのインストール
- js/routes.jsの編集
- NCMB SDKの初期化
- www/index.htmlの編集
- www/pages/list.htmlの内容
- www/pages/login.html の編集
- 管理者ユーザの作成
- コード
- まとめ
利用するライブラリ
今回はFramework7を使って実装しています。Framework7はコンポーネントが多く、Onsen UI同様にiOS/Androidそれぞれに合わせたUIを実現できます。
画面について
今回の画面は全部で3つです。
ログイン/新規登録画面
今回はログインと新規登録を一つの画面で行っています。
ユーザ一覧画面
ユーザ(実際にはProfileクラス)を一覧表示する画面です。
ユーザ表示画面
一覧で選択されたユーザ(実際にはProfile)を表示する画面です。フォローまたはアンフォローができます。該当ユーザのフォロワー、フォローしているユーザの一覧も表示されます。
Monacaプロジェクトのベースを作成
今回はMonacaでFramework7のCore Tab View テンプレートを選択しています。Monaca IDEからはフレームワークテンプレート > JavaScript > Framework7 Core Tab Viewと辿って選択します。
今回編集するのは www/index.html
と www/js
、www/pages
以下になります。他は変更しませんので、今回は解説しません。
JavaScript SDKのインストール
外部ライブラリとして、ncmbを追加します。JavaScriptライブラリですので、JS/CSSコンポーネントの追加と削除から行ってください。
読み込むファイルとしてncmb.min.jsをチェックするのを忘れないでください。
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>
ログイン/新規登録処理について
上記のテンプレートで定義されている通り、ボタンを押したタイミングで 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、パスワードを確認してください'); } }
serializeForm
は js/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
(後述)の返り値の有無によって、新規登録か既存ユーザかの判別を行っています。既存ユーザの場合は、すぐに処理を終了します。新規ユーザ登録者の場合には初期データとして、 Profile
と Follow
クラスを作成しています。詳細はコードを参考にしてください。
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
グループを作成します。
そしてAdminグループが選ばれている状態で新しい会員を追加してください。こうすることで、該当ユーザをAdminグループに追加できます。
コード
コードはNCMBMania/follower_app_monaca: Monacaで実装したフォロー/フォロワー機能ですにアップしてあります。実装時の参考にしてください。
まとめ
今回はMonacaアプリとしての画面に関する説明と、認証画面までを作成しました。次回はユーザ一覧と、詳細表示、そしてフォロー/アンフォロー機能について解説します。