簡単
空き時間にチャットルームを作ってフロントエンドのスキルを固めたいと思い、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を使用することで、少ないコードで最も効率的な方法で複雑なデザインを作成できます。
タイプスクリプトを使う理由
- チャットルームには複雑なデータ構造が含まれるため、タイプスクリプトを使用することで、開発プロセス中にコンパイラに警告を出すことで、多くの低レベルのエラーを回避することができます。
- タイプスクリプトの型定義により、システムのデータ構造が一目でわかり、コードの保守性と可読性が大幅に向上しました。
データベースのテーブル構造設計
データベースは以下の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フレームワークです:
- TypeScriptで作られており、通常のES6と互換性があります。
- nestjsの@nestjs/websocketsパッケージは、WebSocketイベントの処理をカプセル化します。
バックエンドのロジックコードです。
- 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 '接続に成功した!'
 }
}
- グローバルミドルウェアをカプセル化し、開発中のデバッグを容易にします。
// middleware.js
export function logger(req, res, next) {
 const { method, path } = req;
 console.log(`${method} ${path}`);
 next();
};
// main.js 
グローバルミドルウェアを使う
app.use(logger)
- nestjsの静的リソース設定
// main.js
静的リソースの設定
app.useStaticAssets(join(__dirname, '../public/', 'static'), {
 prefix: '/static/', 
});
- 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: 





