最近、NCMBを使ってユーザ同士がコミュニケーションできる仕組みを構築したいというニーズが出てきています。メッセージングアプリだけでなく、ゲームやフリマ、フォーラムアプリなど、多くのアプリでフォロー/フォロワーの仕組みが利用されます。
今回はコミュニケーション機能の基盤になるであろう、フォロー/フォロワー機能の実装を真剣に検討してみたので、その実装について解説します。前回はデータストアのスキーマについて解説しました。前回の認証までの実装に続けて、今回はユーザ一覧とフォロー機能の実装を、Monaca(JavaScript SDK)で行っていきます。
ユーザプロフィール一覧の表示
pages/list.html
にて、ユーザプロフィールを一覧表示します。これは前回の認証チェックの後に実行します。
// 画面表示前に実行されるイベント $on('pageBeforeIn', async (e, page) => { // 認証状態のチェック if (!(await checkAuth())) { // false が返ってきたらログイン画面に遷移 return $f7router.navigate({name: 'Login'}); } const Profile = ncmb.DataStore('Profile'); const ary = await Profile.limit(100).fetchAll(); // 取得したプロフィールデータをHTMLに描画 showProfiles.bind(page)(ary); // 描画した一覧をタップした際のイベント page.$el.find('.profiles tbody tr').on('click', (e) => changeScreen(e, ary)); });
showProfiles
関数は次のようになります。テーブルタグの中に一覧を表示していきます。
// プロフィールデータを描画する関数 const showProfiles = function(ary) { this.$el.find('.profiles tbody').html(ary.map(profile => ` <tr data-object-id="${profile.objectId}"> <td nowrap>${profile.displayName}</td> <td nowrap>${profile.follows_count}</td> <td nowrap>${profile.followers_count}</td> </tr> `).join('')); }
一覧をタップした際のイベント
一覧されたプロフィールデータをタップすると、 changeScreen 関数が呼ばれます。これはタップされたプロフィールデータを使って pages/show.html
に遷移する関数です。
// 一覧をタップした際のイベント const changeScreen = async function(e, ary) { // タップしたデータのオブジェクトIDを取得 const objectId = $(e.target).parents('tr').data('object-id'); // 対象になるプロフィールデータを取得 const profile = ary.filter(m => m.objectId === objectId)[0]; $f7router.navigate(`/show/${objectId}`, {props: { profile }}); }
pages/show.html の編集
ユーザ表示を行う pages/show.html
の画面はまずユーザ情報の表示があります。さらにフォロー、フォロー解除用のボタンを表示します。
<template> <div class="page"> <div class="navbar"> <div class="navbar-bg"></div> <div class="navbar-inner sliding"> <div class="left"> <a href="/list" class="link"> <i class="f7-icons">chevron_left</i> 戻る </a> </div> <div class="title">${ profile.displayName }</div> </div> </div> <div class="page-content"> <div class="list"> <ul> <li> <div class="item-content"> <div class="item-title"> <div class="item-header">表示名</div> ${ profile.displayName } </div> </div> </li> <li> <div class="item-content"> <div class="item-title"> <div class="item-header">フォロワー数</div> ${ profile.followers_count } </div> </div> </li> <li> <div class="item-content"> <div class="item-title"> <div class="item-header">フォロー数</div> ${ profile.follows_count } </div> </div> </li> <li class="no-me"> <a href="#" class="list-button follow" @click=${follow}>フォローする</a> <a href="#" class="list-button color-red unfollow" @click=${unfollow}>フォロー解除</a> </li> </ul> </div> <div class="block-title">フォロワー</div> <div class="list"> <ul class="followers"> </ul> </div> <div class="block-title">フォロー</div> <div class="list"> <ul class="follows"> </ul> </div> </div> </div> </template>
このフォロー、フォロー解除用のボタンはユーザの状態によって表示または非表示になります。
- 自分のプロフィールだった場合
ボタンを両方とも非表示 - まだフォローしていない場合
フォロー解除ボタンは非表示 - すでにフォローしている場合
フォローするボタンを非表示
これを実装すると、次のようになります。
const { profile } = props; const user = ncmb.User.getCurrentUser(); // 画面表示前に実行されるイベント $on('pageBeforeIn', async (e, page) => { const user = ncmb.User.getCurrentUser(); // 自分のプロフィールだったらボタンを非表示に if (profile.user.objectId === user.objectId) { $('.no-me').hide(); } // フォロー済みかチェック const Follow = ncmb.DataStore('Follow') const isFollow = await Follow .equalTo('follows', profile.objectId) .fetch(); if (isFollow.objectId) { // フォローしている page.$el.find('.follow').hide(); } else { // フォローしていない page.$el.find('.unfollow').hide(); } // フォロー/フォロワー一覧を取得 const { followers, follows } = await getFollow(profile.user); // フォロワーを表示 showUser.bind(page)(followers, 'followers'); // フォローしているユーザを表示 showUser.bind(page)(follows, 'follows') });
指定したユーザのフォロー/フォロワーを取得する getFollow
関数は次のようになります。Followクラスを特定ユーザで絞り込む場合は、次のように行います。ポインターとしてユーザが保存されていますので、その形式にした上で検索を実行します。
// 指定したユーザのフォロー/フォロワーを取得する const getFollow = async (user) => { // ポインターの作成 const pointer = { __type: 'Pointer', className: 'user', objectId: user.objectId }; const Follow = ncmb.DataStore('Follow'); return await Follow .equalTo('user', pointer) // ポインターで検索 .fetch(); }
フォロー(フォロワー)ユーザを一覧表示する showUser
関数は次のようになります。
// フォロー/フォロワーを一覧表示する関数 const showUser = async function(ids, name) { // 表示対象がなければ何もしない if (ids.length === 0) return; // 表示対象のobjectIdでProfileクラスを検索します const Profile = ncmb.DataStore('Profile'); const profiles = await Profile .in('objectId', ids) .limit(1000) .fetchAll(); // 返ってきたプロフィールデータを指定されたクラスに描画します this.$el.find(`.${name}`).html(profiles.map(profile => ` <li> <div class="item-content"> <a data-object-id="${profile.objectId}" href="#">${profile.displayName}</a> </div> </li> `).join('')); // リストをタップした際の処理 this.$el.find(`.${name} li`).on('click', e => { // タップされたデータのobjectIdを特定します const objectId = $(e.target).data('object-id'); // タップされたプロフィールデータを特定します const profile = profiles.filter(p => p.objectId === objectId)[0]; // 画面遷移します return $f7router.navigate(`/show/${objectId}`, {props: { profile }}); }); }
フォロー処理
新しくユーザをフォローする処理です。これはフォロー対象(プロフィールデータ)のobjectIdをFollowクラスに保存します。と、同時にフォロー対象はフォローされたという処理が必要になります。これはACL的に処理ができないので、スクリプト機能で実行します。
// フォロー処理 const follow = async () => { // 自分のフォローデータを取得 let follow = await getFollow(user); // フォロー対象を follows フィールドに追加 await follow .addUnique('follows', profile.objectId) .update(); // データを更新(再度取得) follow = await getFollow(user); // 自分のフォロー数をアップデート const myProfile = await getProfile(); await myProfile .set('follows_count', follow.follows.length) .update(); // フォロー対象のデータ更新はスクリプトで実行 await ncmb.Script .data({ follow_id: profile.objectId, follower_id: myProfile.objectId, action: 'add' }) .exec('POST', 'follower.js'); $f7.dialog.alert(`${profile.displayName}をフォローしました`); }
ここでの注意点として、新しくフォローしたユーザのデータについては addUnique
で追加しています。これにより多重処理になったとしても、複数回同じIDが追加されることはありません。そして自分のプロフィールデータに対してフォローカウント数をアップデートします。addUnique関数は特殊なオペレーションになるので、followsフィールドは配列データではなくなってしまいます。そのため、一度データを取得し直しています。
そしてスクリプトに対してフォロー対象とフォロワー(自分)のobjectIdを送っています。また、追加と削除の2つの処理が考えられるので、追加のアクションである指定して add
を送っています。スクリプトの実装は後述します。
フォロー解除
同様にフォロー解除の処理です。フォローの解除は remove
関数で行います。これも配列操作用の特殊なオペレーションです。後は基本的にフォロー追加の時と同じ流れになりますが、スクリプトを呼び出す際のアクションが remove
としているのが違いになります。
// フォロー解除処理 const unfollow = async () => { // 自分のフォローデータを取得 let follow = await getFollow(user); // フォロー解除対象を follows フィールドに追加 await follow .remove('follows', profile.objectId) .update(); // データを更新(再度取得) follow = await getFollow(user); // 自分のフォロー数をアップデート const myProfile = await getProfile(); await myProfile .set('follows_count', follow.follows.length) .update(); // フォロー解除対象のデータ更新はスクリプトで実行 const res = await ncmb.Script .data({ follow_id: profile.objectId, follower_id: myProfile.objectId, action: 'remove' }) .exec('POST', 'follower.js'); $f7.dialog.alert(`${profile.displayName}のフォローを外しました`); }
スクリプトの実装
では follower.js
というスクリプトの実装です。内容は次の通りになります。指定したフォローデータに対して、新しくフォロワーのIDを追加、または削除します。その上で、プロフィールデータを最新のものにアップデートしておきます。
const NCMB = require('ncmb'); const applicationKey = 'YOUR_CLIENT_KEY'; const clientKey = 'YOUR_APPLICATION_KEY'; const userName = 'ADMIN_USER_NAME'; const password = 'ADMIN_PASSWORD'; const ncmb = new NCMB(applicationKey, clientKey); module.exports = async (req, res) => { await ncmb.User.login(userName, password); // 更新対象のフォローデータを取得 let follow = await getFollow(req.body.follow_id); // フォローデータがなければ終了 if (!follow.objectId) return res.send({ error: `No follow ${req.body.follow_id}` }); // アクションによって処理分け switch (req.body.action) { case 'add': // フォローされた際の処理 follow.addUnique('followers', req.body.follower_id) break; case 'remove': // アンフォローされた時の処理 follow.remove('followers', req.body.follower_id) break; } // 更新 await follow.update(); // データ更新(再取得) follow = await getFollow(req.body.follow_id); // プロフィールを更新 const Profile = ncmb.DataStore('Profile'); const profile = new Profile; await profile .set('objectId', follow.profile.objectId) // 更新対象のobjectIdを設定 .set('followers_count', follow.followers.length) // フォロワー数を更新 .set('follows_count', follow.follows.length) // フォロー数を更新 .update(); res.send({ follow }); } // フォローデータを返す関数 function getFollow(objectId) { const Follow = ncmb.DataStore('Follow'); return Follow .equalTo('profile', { __type: 'Pointer', className: 'Profile', objectId }).fetch(); }
コード
コードはNCMBMania/follower_app_monaca: Monacaで実装したフォロー/フォロワー機能ですにアップしてあります。実装時の参考にしてください。
まとめ
これでMonacaアプリにおけるフォロー/フォロワー機能の実装が完了です。この仕組みは汎用的に利用できますので、皆さんのアプリにおいてコミュニケーション機能が必要になった際に参考にしてください。
現状のスクリプトは誰でも実行できる状態になっています。これは、仕組みが分かっていて、かつobjectIdが分かっている場合にフォロー操作ができることを意味します。もしセキュアにしたい場合には、次のような工夫が考えられます。
- ログインユーザのセッショントークンをスクリプトに送る
- スクリプトでセッショントークンを利用してユーザ情報が取得できるか確認
- 取得できる場合は、リクエストされたフォローデータのユーザ情報と比較
- フォローデータのユーザ情報と一致した場合は、正しいリクエストであると判定して処理を行う
このような処理を追加すれば、よりセキュアにスクリプトが実行できるでしょう。実装時の参考にしてください。