blog

typescriptでパワフルなウェブチャットルームを作ろう。

プロフィール: genal-chat\n\nプロジェクトインターフェース\n\n機能\nモバイル互換性\nユーザー情報の変更\nグループチャット/プライベートチャット\nグループ作成/グループ参加/グ...

Apr 26, 2020 · 6 min. read
シェア

簡単

空き時間にチャットルームを作ってフロントエンドのスキルを固めたいと思い、2020年6月24日にAstroChatの開発の旅を始めました。

このプロジェクトは完全なタイプスクリプトで開発されており、これは将来の機能反復の基礎となるものです。もちろん、私はタイプスクリプトが大好きです。

プロジェクトインターフェース

特徴

  • モバイル対応
  • ユーザー情報の変更
  • グループ/プライベートチャット
  • グループの作成 / グループへの参加 / グループからの退出 / ファジー検索グループ
  • 友達追加/友達削除/ファジー検索ユーザー
  • メッセージページング
  • 顔文字
  • イメージ送信/プレビュー
  • オンライン統計
  • カスタム背景
  • 再接続アラート

技術概要

  • Typescript: JavaScript のスーパーセットで、最大の利点は型システムを提供し、コードの可読性と保守性を向上させることです。
  • Vue 2.6.x: フロントエンド・プログレッシブ・フレームワーク。
  • Socket/io: リアルタイム通信、websocketサードパーティライブラリ.
  • Vuex: Vue.jsアプリケーション専用に開発された状態管理モデル。
  • Nestjs: 効率的でスケーラブルなNode.jsサーバーサイドアプリケーションを構築するためのフレームワークで、TypeScriptで書かれ、OOP1、FP2、FRP3の概念を組み合わせています。
  • Typeorm:最新のJavaScript機能をサポートし、データベースを使用するあらゆるアプリケーションの開発を支援する追加機能を提供します。
  • ES6+:ES6+の構文を使用し、アロー関数、async/await、その他の構文がとても使いやすくなっています。
  • SASS(SCSS):CSSの前処理言語としてSCSSを使用することで、少ないコードで最も効率的な方法で複雑なデザインを作成できます。

タイプスクリプトを使う理由

  1. チャットルームには複雑なデータ構造が含まれるため、タイプスクリプトを使用することで、開発プロセス中にコンパイラに警告を出すことで、多くの低レベルのエラーを回避することができます。
  2. タイプスクリプトの型定義により、システムのデータ構造が一目でわかり、コードの保守性と可読性が大幅に向上しました。

データベースのテーブル構造設計

データベースは以下の6つのテーブルを使用します。

  • user ユーザーテーブル
  • グループ グループテーブル
  • user_group user_group 中間テーブル
  • group_message グループ・メッセージ・テーブル
  • user_friend user_friend 中間テーブル
  • friend_message プライベートチャットメッセージテーブル

真ん中の表は、グループ/友人とユーザーとのつながりを確立するために使われます。以下は私が描いたマインドマップです。これを読めば、その謎が理解できると思います。

WebSocket作成ロジック

ユーザールームの作成

チャットルームに入室したユーザーは、自動的にpublicという名前のWebSocketルームと、ユーザーのidにちなんだ名前のWebSocketルームに入室することになります。ルームの概念を理解していない場合、ルームにいる人だけがルームからのブロードキャストを受信することができると仮定することができます。

グループチャットルームの作成

groupIdをWebSocketルームの名前とし、新しいユーザがグループに参加するたびに、グループルームに新しいユーザの詳細とともにユーザの参加イベントをブロードキャストし、他のユーザは新しいユーザの情報を保存します。新しいユーザがメッセージを送信すると、他のユーザはそのメッセージのuserIdから対応するユーザの詳細を見つけることができます。これにより、他のユーザーは、メッセージが送信されるとすぐに、そのメッセージの所有者を知ることができます。

プライベートチャットルームの作成

バディ追加リクエストが開始されるたびに、ユーザーの userId とバディの userId を連結した文字列を WebSocket ルーム名として使用して、プライベートチャットルームが作成されます。

バックエンドアーキテクチャ

バックエンドはnestjsを使用しています。nestjsは近年急速に成長しているnode.jsフレームワークです:

  1. TypeScriptで作られており、通常のES6と互換性があります。
  2. nestjsの@nestjs/websocketsパッケージは、WebSocketイベントの処理をカプセル化します。

バックエンドのロジックコードです。

  1. nestjsによるWebSocket接続の確立
// chat.gateway.ts
@WebSocketGateway()
export class ChatGateway {
 // socket接続フック
 async handleConnection(client: Socket): Promise<string> {
 let userRoom = client.handshake.query.userId;
 // デフォルトで公開ルームに接続される
 client.join('public');
 // ユーザーIDに基づくユーザー専用メッセージルーム
 if(userRoom) {
 client.join(userRoom);
 }
 return '接続に成功した!'
 }
}
  1. グローバルミドルウェアをカプセル化し、開発中のデバッグを容易にします。
// middleware.js
export function logger(req, res, next) {
 const { method, path } = req;
 console.log(`${method} ${path}`);
 next();
};
// main.js 
グローバルミドルウェアを使う
app.use(logger)
  1. nestjsの静的リソース設定
// main.js
静的リソースの設定
app.useStaticAssets(join(__dirname, '../public/', 'static'), {
 prefix: '/static/', 
});
  1. nestjs カスタム例外フィルタ
// http-exception.filter.ts
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter<HttpException> {
 catch(exception: HttpException, host: ArgumentsHost) {
 const ctx = host.switchToHttp();
 const response = ctx.getResponse();
 const request = ctx.getRequest();
 const status = exception.getStatus();
 const exceptionRes: any = exception.getResponse();
 const {
 error,
 message,
 } = exceptionRes;
 // リクエストエラーの場合、以下のフォーマットがフロントエンドに返される。
 response.status(status).json({
 status,
 timestamp: new Date().toISOString(),
 path: request.url,
 error,
 message,
 });
 }
}

フロントエンドアーキテクチャ

ページの初期化

初期化では、WebSocket接続関数を呼び出し、全ユーザのグループ情報と全フレンド情報を取得し、WebSocket通信のルールを確立して対応するルームに参加し、vuexを使って最新データをディスパッチします。

データ処理

グループのデータタイプ

//  
interface Group {
 groupId: string;
 userId: string; // グループオーナーID
 groupName: string;
 notice: string;
 messages: GroupMessage[];
 createTime: number;
}

友人のデータ型

//  
interface Friend {
 userId: string;
 username: string;
 avatar: string;
 role?: string;
 tag?: string;
 messages: FriendMessage[];
 createTime: number;
}

以前は、すべてのグループ/フレンドデータをオブジェクトの配列[ friend1 , friend2 ... ]で管理していました。 しかし、データが大量になると、グループ/フレンドデータのクエリや更新に非常に負荷がかかります。フレンドの名前やアバターが変わるたびに、情報を更新するために配列を走査しなければなりません。

そこで、チャットルームのコードをオブジェクト構造で最適化しました。groupId/userIdがキーで、値は対応するグループ/フレンドのデータです。

gather = {
 'userId': {
 userId: 'userId'
 username: 'xxx'
 messages: [];
 ...
 }
}

各グループとユーザーは一意な ID を持っているので、重複を心配する必要はありません。この構造では、データの更新はとても簡単です。更新が必要なidを取得して、対応する値でgather.idを上書きするだけです。

vuex

チャットルームでは、データの即時更新やvueコンポーネント間のデータの同期が必要で、このようなビジネスシナリオへの対応はvuexの強みです。 vuexのアクションにWebSocket接続を確立する関数を記述し、ユーザがログインに成功した後に接続関数を呼び出すようにしました。

// actions.ts
const actions: ActionTree<ChatState, RootState> = {
 // WebSocketを初期化する
 async connectSocket({commit, state, dispatch, rootState}, callback) {
 // WebSocket接続確立
 socket.on('connect', async () => {
 // グループメッセージタイムに登録する
 socket.on('groupMessage', (res: any) => {
 console.log('on groupMessage', res)
 if (!res.code) {
 // グループメッセージを処理する
 commit(ADD_GROUP_MESSAGE, res.data)
 }
 })
 }
 }

vuex-classライブラリは、typescriptでvuexを開発するための素晴らしい接着剤です。vuex-classを使えば、vuexコンポーネントでvuexメソッドを呼び出すのは、このように書くだけです:

// GenalChat.vue
import { namespace } from 'vuex-class'
const appModule = namespace('app')
export default class GenalChat extends Vue {
 @appModule.Getter('user') user: User;
 @appModule.Action('login') login: Function;
}

技術概要

🖖🏻 現在、チャットルームは完全なチャット機能を備えています。同時に、友人が私を励ますために星を与えるような、よりクールな機能を開発し続けるつもりです!

オンラインアドレス

github:

Read next