Hexabase(ヘキサベース)は企業においても安心して利用できるBaaS(Backend as a Service)を提供しています。多くのBaaSがそうであるように、主にフロントエンド開発者に利用してもらいたいと考えています。そこで現在、TypeScript SDKの開発が進められています。
この記事ではv2系になって新しくなったHexabase TypeScript SDKのインストールと、データストア(クラウドデータベース)の通知機能を使ったチャットアプリの実装を紹介します。
デモ
コードは以下で公開しています。
インストール
インストールはnpmやyarnを使って行います。
# npmの場合
npm install @hexabase/hexabase-js
# yarnの場合
yarn add @hexabase/hexabase-js
インポート
インポートすると、 HexabaseClient
というオブジェクトが取得できます。
import { HexabaseClient, Item } from "@hexabase/hexabase-js";
初期化
HexabaseClientを初期化します。
// 本番閑居向け
const client = new HexabaseClient();
// 開発環境向け
const client = new HexabaseClient('dev');
なお、すでに認証済みだった場合は setToken
メソッドを使ってトークンをセットすることもできます。
client.setToken(token);
認証
Hexabaseでは業務利用を想定しているため、利用する際に認証情報が必須になります。最初はメールアドレスとパスワードで認証し、その後はトークンを使ってGraphQLにアクセスします。 client
を使って処理します。
初回の認証は次のようになります。emailとパスワード、またはトークンが必須です。
await client.login({email, password, token});
後はこの client
に対して処理を行います。
認証の判定
ログイン済みかどうかは client.currentUser
が null
かどうかで判定できます。今回はそれをステートに入れることで、ログイン判定と画面の出し分けを行っています。
const [loggedIn, setLoggedIn] = useState<boolean>(false);
useEffect(() => {
const token = localStorage.getItem('token');
if (token) init(token);
}, []);
const init = async (token: string) => {
await client.setToken(token);
await client.setWorkspace(process.env.NEXT_PUBLIC_WORKSPACE_ID!);
setLoggedIn(true);
}
return (
<>
{ loggedIn ?
<Chat client={client} />
: <Login success={success} />}
</>
)
認証画面
ログイン画面は components/login.tsx
にて実装しています。メールアドレスとパスワードで認証を行っています。認証が通れば、 success
に対してトークン文字列を送っています。
const [form, setForm] = useState<FormType>({
email: '',
password: '',
errorMessage: '',
});
const login = async (e: React.SyntheticEvent<HTMLFormElement> | React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
const bol = await client.login({email: form.email, password: form.password});
if (bol) {
params.success(client.tokenHxb);
} else {
setForm({...form, errorMessage: 'ログインに失敗しました。'});
}
};
チャットルーム一覧画面
認証が通ったら <Chat />
を表示します。これは components/chat.tsx
にて実装しています。Hexabase SDKのQueryを使って、チャットルーム一覧を取得しています。
const [rooms, setRooms] = useState<Item[]>([]);
useEffect(() => {
getRooms();
}, []);
const getRooms = async () => {
const rooms = await client.query(process.env.NEXT_PUBLIC_PROJECT_ID!)
.from(process.env.NEXT_PUBLIC_DATASTORE_CHAT_ID!)
.select('*');
setRooms(rooms);
}
一覧は画面に表示し、 Next.js の Linkを使って /{room.id}
で遷移できるようにします。
{rooms.map((room, i) => {
return (
<div key={i}>
<Link href={`/${room.id}`}>
{room.get<string>('name')}
</Link>
</div>
)
})}
チャット画面
チャット画面は app/[id]/page.tsx
にて実装しています。この画面にはURL直打ちでアクセスする可能性を考慮しています。まずルームの取得、そしてこれまでのメッセージ一覧を取得します。
// ルームのステート
const [room, setRoom] = useState<Item | undefined>(undefined);
// メッセージのステート
const [messages, setMessages] = useState<ItemHistory[]>([]);
// 新規入力されたメッセージのステート
const [comment, setComment] = useState<string>('');
// 最初にルームを取得する
useEffect(() => {
getRoom();
}, []);
// ルームを取得する処理
const getRoom = async () => {
// Hexabaseの初期化
await initHexabase();
// プロジェクト、データストア、アイテム(ルーム)の取得
const project = await client.currentWorkspace?.project(process.env.NEXT_PUBLIC_PROJECT_ID!);
const datastore = await project?.datastore(process.env.NEXT_PUBLIC_DATASTORE_CHAT_ID!);
const room = await datastore?.item(id);
if (!room) return;
// ルームのステートを更新
setRoom(room);
// これまでのメッセージを取得
setHistory(room);
// ルームの更新を監視
subscribe(room);
};
// Hexabaseの初期化処理
const initHexabase = async () => {
const token = localStorage.getItem('token');
if (!token) return;
await client.setToken(token);
await client.setWorkspace(process.env.NEXT_PUBLIC_WORKSPACE_ID!);
};
// これまでのメッセージを取得する処理
const setHistory = async (room: Item) => {
const histories = await room?.histories();
// 空文字列、または __refresh__ は除外
setMessages((histories || [])
.filter(history => history.comment !== '' && history.comment !== '__refresh__'));
}
// ルームの更新を監視する処理
const subscribe = (room: Item) => {
// 更新通知がきたら、これまでのメッセージを取得し直す
room?.subscribe('update', history => setHistory(room));
}
messages
は一覧表示します。ログインユーザーと投稿者が同じ場合は、データを削除できるようにします。
<List spacing={3}>
{messages.map((message, i) => {
return (
<ListItem key={i}>
{message.comment} by {message.user?.userName}
{message.user?.userName === client.currentUser?.userName && <Button onClick={() => remove(message)}>削除</Button>}
</ListItem>
)
})}
</List>
新規メッセージの投稿
フォームで新しいメッセージを入力します。
<form onSubmit={submit}>
<input type="text" id="comment" value={comment} onChange={e => setComment(e.target.value)} />
<button type="submit">送信</button>
</form>
submit
処理で、新しいメッセージを投稿します。
// メッセージを送信する処理
const submit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
e.preventDefault();
const history = room!.comment();
history.set('comment', comment);
await history.save();
setComment('');
};
保存されると、 subscribe
で登録した処理が実行され、 setHistory
でメッセージ一覧を更新します。
// ルームの更新を監視する処理
const subscribe = (room: Item) => {
// 更新通知がきたら、これまでのメッセージを取得し直す
room?.subscribe('update', history => setHistory(room));
}
メッセージの削除
メッセージの削除は一覧の remove
関数呼び出しで行います。メッセージを削除しても通知されないので、特別に __refresh__
というメッセージを送っています。このメッセージの通知を受け取ったら、一覧を強制リフレッシュしています。
// メッセージを削除する処理
const remove = async (history: ItemHistory) => {
await history.delete();
// 強制更新させるメッセージを送信
const deleteMessage = room!.comment();
deleteMessage.set('comment', '__refresh__');
await deleteMessage.save();
}
// ルームの更新を監視する処理
const subscribe = (room: Item) => {
// 更新通知がきたら、これまでのメッセージを取得し直す
room?.subscribe('update', history => setHistory(room));
}
// これまでのメッセージを取得する処理
const setHistory = async (room: Item) => {
const histories = await room?.histories();
// 空文字列、または __refresh__ は除外
setMessages((histories || [])
.filter(history => history.comment !== '' && history.comment !== '__refresh__'));
}
APIの呼び出し回数を減らすなら、メッセージのIDを一緒に送ってフィルタリングする方法でも良さそうです。
まとめ
SDKを使うことで、Hexabaseを使ったアプリに簡単なチャット機能を追加できます。ぜひWebアプリケーション開発に活用してください。
Hexabaseには他にもファイルストレージやスクリプトなどの機能があります。それらの機能も利用してください。