NCMBとMonaca、Framework7を使ってチャットアプリを作る（その3：チャット処理）

NCMBとMonacaを使ってチャットアプリを作ります。以前、PieSocketを使ったチャットアプリを公開したのですが、現在○○は無料での利用枠がなくなっています。そこで今回は[goofmint/ruby-websockets-chat-demo](https://github.com/goofmint/ruby-websockets-chat-demo)をベースにチャットアプリを作ります。これはごく単純なWebScoketサーバーで、Herokuにデプロイできるものです。Herokuの無料枠内での利用が可能です。

[前回の記事では認証周りを解説しました](https://qiita.com/goofmint/items/f48282a1b995fa6007da)。今回はチャット処理について解説します。

## コードについて

今回のコードは[NCMBMania/MonacaChatDemo](https://github.com/NCMBMania/MonacaChatDemo)にアップロードしてあります。実装時の参考にしてください。

## WebSocketサーバーのデプロイ

チャットサーバーをデプロイします。Herokuに対応していますので、クリック1つでデプロイできます。

[goofmint/ruby-websockets-chat-demo](https://github.com/goofmint/ruby-websockets-chat-demo)

デプロイした際のURLが `https://CHAR-SERVER.herokuapp.com` だとしたら、`https` を `wss` に変更して `config.json` にて `websocketUrl` キーで設定します。 `wss` はWebSocketのSSL/TLS版です。

```json
{
	"applicationKey": "YOUR_APPLICATION_KEY",
	"clientKey": "YOUR_CLIENT_KEY",
	"websocketUrl": "wss://CHAR-SERVER.herokuapp.com"
}
```

## WebSocket接続の設定

NCMBの初期化を行った `js/app.js` にて、同じくWebSocketの初期化を行います。その際、状態を把握するのに使える関数も指定しておきます（必須ではありません）。

```js
const webSocket = new WebSocket(config.websocketUrl);
webSocket.onopen = onOpen;
webSocket.onclose = onClose;
webSocket.onerror = onError;
window.webSocket = webSocket;

// 接続した時に呼ばれるイベント
const onOpen = (e) => {
}

// エラーが出た際に呼ばれるイベント
const onError  = (e) => {
  console.log(e);
};

// 接続を閉じた時に呼ばれるイベント
const onClose = (e) => {
}
```

## チャット画面について

チャット画面はFramework7のメッセージ機能を使うので、HTMLはとてもシンプルです。

```html
<div class="page" data-name="chat">
	<div class="navbar">
		<div class="navbar-bg"></div>
		<div class="navbar-inner sliding">
			<div class="title">チャット</div>
		</div>
	</div>
	<div class="toolbar messagebar">
		<form id="chat">
			<div class="toolbar-inner">
				<div class="messagebar-area">
					<textarea name="text" class="resizable" placeholder="Message"></textarea>
				</div>
				<a class="link send-link" href="#" @click=${sendMessage}>Send</a>
			</div>
		</form>
	</div>
	<div class="page-content">
		<div class="messages">
		</div>
	</div>
</div>
```

![FireShot Capture 485 - Monaca × Framework7 × NCMB - localhost 20220803154354 - 485.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/c4e7c5fe-f566-79a7-4b68-504dce362455.jpeg)


### 変数の準備

画面を構築するのに必要な変数、NCMBのデータストアに保存する際に使う変数を準備します。

```js
export default (props, { $on, $update, $tick, $onMounted }) => {
	// データストアに保存する用のクラス名（DBで言うテーブル名相当）
	const Message = ncmb.DataStore('Message');
	// ログインユーザー
	const user = ncmb.User.getCurrentUser();
	// データ保存する際のACL（アクセス権限）
	const acl = new ncmb.Acl;
	acl
		.setPublicReadAccess(true)        // 誰でも閲覧可能
		.setUserWriteAccess(user, true);  // ユーザのみ編集・削除可能
	
	let messagebar;           // メッセージ入力欄用
	let messages = [];        // チャットメッセージ（描画用）
	let messageObjects = [];  // チャットメッセージ（データストアから取得）
```

### チャットメッセージを受け取った際の処理を用意

チャットメッセージをWebSocketサーバー経由で受け取った際の処理を記述します。これはWebSocketサーバーが送信する内容を解析し、後述する `addMessage` 関数にデータを送ります。 `unescapeHtml` 関数は○○より拝借しています。

```js
// マウントされた際に実行
$onMounted(() => {
	// WebSocketでメッセージを受け取った際に呼び出される関数
	window.webSocket.onmessage = (e) => {
		// 文字列のメッセージをJSONとしてパース
		const message = JSON.parse(e.data);
		// ユーザー情報はHTMLをアンエスケープした上でパース
		message.user = JSON.parse(unescapeHtml(message.user));
		// チャットメッセージを追加
		addMessage(message);
	};
});

// HTMLをアンエスケープする関数
const unescapeHtml = (target) => {
	if (typeof target !== 'string') return target;
	const patterns = {
		'&lt;': '<',
		'&gt;': '>',
		'&amp;': '&',
		'&quot;': '"',
		'&#x27;':  '\'',
		'&#x60;': '`'
	};
	return target.replace(/&(lt|gt|amp|quot|#x27|#x60);/g, match => {
		return patterns[match];
	});
};
```

### 画面の初期化処理

画面が初期化された際には `page:init` イベントが呼ばれます。ここでFramework7のメッセージ機能を利用します。処理がちょっと長いですが、コメントを参照してください。

```js
// ページが初期化される際に呼ばれる関数
$on('page:init', async () => {
	// チャットメッセージの作成
	messages = app.messages.create({
		el: '.messages',
		scrollMessagesOnEdge: true,
		// 最後のメッセージか判定する関数
		lastMessageRule: function (message, previousMessage, nextMessage) {
			if (message.isTitle) return false;
			if (!nextMessage || nextMessage.type !== message.type || nextMessage.name !== message.name) return true;
			return false;
		},
		// メッセージに吹き出しを付けるか判定する関数
		tailMessageRule: function (message, previousMessage, nextMessage) {
			if (message.isTitle) return false;
			if (!nextMessage || nextMessage.type !== message.type || nextMessage.name !== message.name) return true;
			return false;
		}
	});
	// データストアからメッセージを取得
	messageObjects = await getMessages();
	// チャットメッセージに追加
	messageObjects.forEach(addMessage);
	// メッセージ入力欄を作成
	messagebar = app.messagebar.create({
		el: '.messagebar'
	});
});
```

#### メッセージの取得

`getMessages` 関数はNCMBのデータストアから既存メッセージを取得する処理です。

```js
// NCMBにあるチャットメッセージを取得する関数
const getMessages = () => {
	return Message
		.include('user') // ユーザー情報も取得
		.limit(100)      // 100件まで
		.fetchAll();
}
```

#### メッセージの追加処理

取得したメッセージは `addMessage` 関数にて `messages` に追加していきます。この関数はWebSocketサーバーからメッセージを受け取った際にも利用されます。

```js
// チャット表示用のメッセージを追加する関数
const addMessage = (message) => {
	// 詳細は以下を参照
	// https://framework7.io/docs/messages#single-message-parameters
	messages.addMessage({
		text: message.message,
		type: message.user.objectId === user.objectId ? 'sent' : 'received',
		name: message.displayName,
		avatar: '/assets/icons/person.png',
	});
};
```

### メッセージの送信

メッセージを送信すると `sendMessage` 関数が呼ばれます。ここではNCMBのMessageクラスにデータを保存した上で、メッセージをWebSocketサーバーに送信しています。WebSocketでは文字列のみ扱えますので、JSON.stringifyを使って文字列化しています。

```js
// メッセージ送信処理
const sendMessage = async () => {
	// 入力内容を取得
	const params = app.form.convertToData($('#chat'));
	// Messageクラスのインスタンス（DBでいう行相当）を作成
	const message = new Message;
	// データを適用して保存
	await message
		.set('user', user)
		.set('acl', acl)
		.set('message', params.text)
		.save();
	// WebSocketで通知
	window.webSocket.send(JSON.stringify(message));
}
```

![FireShot Capture 486 - Monaca × Framework7 × NCMB - localhost 20220803154430 - 486.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/0c2c6c5a-38eb-c354-2d94-a5c5cde6e917.jpeg)


これでチャットアプリの完成です。

## まとめ

今回はチャット画面周りを解説しました。WebSocketサーバーを自分で立てれば、簡単なチャットアプリを作成できます。WebSocketサーバーだけではメッセージが保存されませんが、NCMBのデータストアを組み合わせることで、チャットメッセージを残しておけます。

Framework7のメッセージ機能は画像を投稿、表示できるなど多機能なものになっています。今回のチャットアプリをカスタマイズして実装してみてください。