Hexabaseは企業向けのBaaS(Backend as a Service)を提供しています。クラウドデータベースがあり、テーブルを作成してAPI経由でCRUD操作が可能です。他、ファイルストレージや認証、リアルタイム通知機能があります。
今回はHexabaseの提供する機能を使って、掲示板アプリを作成します。掲示板アプリは、スレッドの一覧表示、スレッドの詳細表示、スレッドの新規作成、コメントの投稿ができるものです。
Hexabaseの構造について
まず、Hexabaseの構造について簡単に説明します。
Hexabaseでは、一番上位のオブジェクトが「ワークスペース」です。ワークスペースの中には、複数の「プロジェクト」を作成することができます。そして、各プロジェクトには、複数の「データストア」を設定できます。データストアは、「テーブル」に相当します。
フォーラム用のHexabase設計
今回は、以下のように設計しました。
- ワークスペース:Demo
- プロジェクト:Forum
- データストア:Thread
ワークスペース「Demo」の中に、プロジェクト「Forum」を作成し、その中にデータストア「Thread」を設定しました。これで、フォーラムを構築するための基本的な枠組みができあがりました。
また、プロジェクト「Forum」では、通知設定を有効にしておきました。これにより、新しい投稿があった際に、関係者に通知が届くようになります。
Hexabaseのフィールド設定
Hexabaseでは、自分でデータベース設計を行います。今回は、Threadを以下のようにフィールド作成しました。
フィールド名 | 型 |
---|---|
id | 自動採番 |
Title | 文字列型 |
level | 選択肢 |
body | テキストエリア |
pubDate | 日付・時刻型 |
attachment | 添付ファイル |
フレームワークとルーティングについて
このフォーラムアプリケーションでは、Framework7をフレームワークとして利用しています。Framework7は、モバイルアプリケーションの開発に特化したフレームワークで、UIコンポーネントやルーティング機能などを提供しています。
アプリケーションの画面遷移は、以下のようなルーティング設定によって管理されています。
このルーティング設定により、URLパスに応じた画面の表示が可能になります。例えば、 "/home" にアクセスすると、 "./pages/home.html" が表示されます。また、 "/threads/:id" のように、パラメータを含むパスも定義できます。
const routes = [
{
path: "/",
url: "./index.html",
},
// ログイン
{
path: "/home",
name: "Home",
componentUrl: "./pages/home.html"
},
// スレッド一覧
{
path: "/threads",
name: "Threads",
componentUrl: "./pages/threads.html"
},
// スレッド詳細
{
path: "/threads/:id",
name: "Thread",
componentUrl: "./pages/thread.html"
},
// スレッド新規作成
{
path: "/form",
name: "Form",
componentUrl: "./pages/form.html"
},
// Default route (404 page). MUST BE THE LAST
{
path: "(.*)",
url: "./pages/404.html",
},
];
ライブラリの読み込みと設定
index.htmlでは、Framework7のViewとdayjs(日付操作ライブラリ)、HexabaseのSDKを読み込んでいます。
<div id="app">
<!-- Views/Tabs container -->
<div class="views tabs safe-areas">
<div id="app">
<div class="views tabs safe-areas">
<div id="view-home" class="view view-init view-main" data-url="/home">
</div>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.10.7/dayjs.min.js" integrity="sha512-bwD3VD/j6ypSSnyjuaURidZksoVx3L1RPvTkleC48SbHCZsemT3VKMD39KknPnH728LLXVMTisESIBOAb5/W0Q==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="js/hexabase.js"></script>
また、画像の表示で blob://
を使用するため、CSP(Content Security Policy)を設定しています。
<!-- blob: を追加します -->
<meta http-equiv="Content-Security-Policy"
content="default-src * data: gap: blob: content: https://ssl.gstatic.com; style-src * "unsafe-inline"; script-src * "unsafe-inline" "unsafe-eval"">
この設定により、 blob://
プロトコルを使用した画像の表示が可能になります。
Hexabase SDKの初期化
まず、js/app.js
でHexabase SDKを初期化します。
通常は引数不要ですが、開発環境などを指定する場合は引数を渡します。
// Hexabaseの初期化
const { HexabaseClient } = hexabase;
const client = new HexabaseClient();
ログイン処理について
ログイン処理はpages/home.html
に実装されています。
メールアドレスとパスワードを入力し、ログインボタンを押すと以下の処理が実行されます。
入力されたメールアドレスとパスワードを使ってログインを試み、成功すればワークスペースを設定し、一覧画面に遷移します。
// ログインボタンを押した時の処理
const login = async () => {
// 入力値を受け取る
const email = $f7.$el.find("#email").val();
const password = $f7.$el.find("#password").val();
try {
// ログイン
const res = await client.login({ email, password });
await client.setWorkspace("demo");
const text = res ? "ログインしました" : "ログイン失敗しました";
// トースト表示
$f7.toast.create({
text,
position: "top",
closeTimeout: 500,
}).open();
// 一覧画面に遷移
$f7router.navigate({
name: "Threads",
});
} catch (e) {
console.log(e);
}
};
スレッド一覧の取得
スレッド一覧はpages/threads.html
で処理されます。
画面が表示されると、以下の処理が実行され、データストア"Thread"からレコードデータを取得します。
取得したデータは$update()
でHTMLに反映されます。HTMLではtableタグを使って一覧を表示しています。
let records = []; // スレッド一覧用
// 画面が表示されたら呼び出されるイベント
$on("pageAfterIn", async (e, page) => {
const project = await client.currentWorkspace.project("Forum");
const thread = await project.datastore("Thread");
// スレッド一覧の取得
records = await thread.items();
// 画面を更新
$update();
});
<table>
<thead>
<tr>
<th>#</th>
<th>配信日</th>
<th>タイトル</th>
<th>重要度</th>
</tr>
</thead>
<tbody>
${records.map(r => $h`
<tr @click=${()=> click(r)}>
<td>${parseInt(r.get('id'))}</td>
<td>${dayjs(r.get('pubDate')).format('MM月DD日 hh:mm')}</td>
<td>${r.get('Title')}</td>
<td>${r.get('level') ? r.get('level').ja : ''}</td>
</tr>
`)}
</tbody>
</table>
行をタップするとclick
関数が呼ばれ、スレッド詳細画面に遷移します。
// スレッド一覧をタップした際のイベント
const click = (record) => {
// スレッド詳細画面に遷移
$f7router.navigate(/threads/${record.id}
, {
props: {
record,
},
});
}
データの新規作成
新規スレッドを作成するには、pages/threads.html
のヘッダーにある+アイコンをタップし、/form
に遷移します。
<div class="right">
<a href="/form" class="link">
<i class="f7-icons">plus</i>
</a>
</div>
フォームについて
pages/form.html
ではフォームを表示し、タイトル、日付、重要度、本文、添付ファイルを入力できます。
<form id="report">
<div class="list">
<ul>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-input-wrap">
<input type="text" name="Title" placeholder="掲示板のタイトル" placeholder="掲示板のタイトル" />
<span class="input-clear-button"></span>
</div>
</div>
</li>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-input-wrap">
<input type="date" name="pubDate" placeholder="2023-03-26" />
</div>
</div>
</li>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-input-wrap">
<select name="level">
<option value="通常" selected>通常</option>
<option value="至急">至急</option>
<option value="重要">重要</option>
</select>
</div>
</div>
</li>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-input-wrap">
<textarea name="body" class="resizable"></textarea>
</div>
</div>
</li>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-input-wrap">
<input type="file" name="attachment" multiple />
</div>
</div>
</li>
<li>
<a href="#" @click=${save} class="list-button">保存する</a>
</li>
</ul>
</div>
</form>
保存ボタンを押すとsave
関数が呼ばれ、入力データをHexabaseのデータストアアイテムに変換し、save
メソッドでデータを保存します。
// 新しいスレッドを保存するイベント
const save = async (e) => {
// フォームデータをオブジェクトに変換
const item = await formToObject($(e.target).parents("form")[0]);
try {
// レコードの登録
const res = await item.save();
// データが登録できればメッセージ表示
const text = res ? "保存しました" : "保存失敗しました";
$f7.toast.create({
text,
position: "top",
closeTimeout: 2000,
}).open();
$f7router.back();
} catch (e) {
console.log(e);
}
};
formToObject
関数では、日付型の変換やファイルオブジェクトの作成を行っています。
// フォームのデータをオブジェクトに変換する関数
const formToObject = async (form) => {
// プロジェクト・データストアを取得
const project = await client.currentWorkspace.project("Forum");
const thread = await project.datastore("Thread");
// 新規アイテムを生成
const item = await thread.item();
// フォームの入力値をアイテムに適用する
const ary = Array.prototype.slice.call(form.elements);
for (const ele of ary) {
const { name, type, value } = ele;
// ファイルは別処理
if (type !== "file") {
// 日付は日付型に変換
if (name === "pubDate") {
item.set(name, new Date(value));
} else {
item.set(name, value);
}
continue;
}
// ファイルはHexabaseのファイルオブジェクトに変換
const files = ele.files.map(file => {
const f = item.file();
f
.set("name", file.name)
.set("data", file);
return f;
});
if (files.length > 0) item.set("attachment", files);
}
return item;
}
スレッド一覧からの画面遷移
スレッドをタップしたら、スレッド詳細画面に遷移します。これは pages/threads.html
に定義しています。
// スレッド一覧をタップした際のイベント
const click = (record) => {
// スレッド詳細画面に遷移
$f7router.navigate(/threads/${record.id}
, {
props: {
record,
},
});
}
スレッド詳細画面
pages/thread.html
についてです。この画面では前の画面から送られてきたスレッド詳細の表示と、コメントの取得を行っています。
<div class="page-content">
<div class="list media-list">
<ul>
<li>
<div class="item-content">
<div class="item-inner">
<div class="item-title-row">
<div class="item-title">${record.get('title')}</div>
<div class="item-after">${dayjs(record.get('pubDate')).format('MM月DD日 HH:mm')}</div>
</div>
<div class="item-subtitle">${record.get('level') ? record.get('level').ja : ''}</div>
<div class="item-text">${showText(record.get('body'))}</div>
</div>
</div>
</li>
<li>
${attachments.map(a => $h`
<div class="item-content">
<div class="item-inner">
<img src="${a}" width="100%" />
</div>
</div>
`)}
</li>
</ul>
</div>
<div class="block-title">コメント</div>
<div class="list media-list">
<ul>
${comments.map(c => $h`
<li>
<div class="item-content">
<div class="item-inner">
<div class="item-title-row">
<div class="item-title">${c.user.userName}</div>
<div class="item-after">${dayjs(c.createdAt).format('MM月DD日 HH:mm')}</div>
</div>
<div class="item-text">${c.comment}</div>
</div>
</div>
</li>
`)}
</ul>
</div>
<div class="list inset">
<ul>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-input-wrap">
<textarea id="text" class="resizable"></textarea><br />
<button class="button" @click=${addComment}>コメントする</button>
</div>
</div>
</li>
</ul>
</div>
</div>
コメントの取得
コメントは、画面を表示した際に取得します。Hexabaseにはコメント通知機能があるので、アイテムに対するsubscribe
メソッドを使ってリアルタイム通知を受け取ります。
既存のコメントはrecord.histories()
で取得できます。
export default (props, { $f7, $f7router, $update, $onMounted }) => {
const { record } = props; // 前の画面から送られてくるスレッドデータ
let comments = []; // コメント一覧用
let attachments = []; // 添付ファイル用
// 新しいコメントが追加された際のリアルタイム通知を受け取る
record.subscribe("update", comment => {
if (comment.comment === "") return;
comments.push(comment);
$update();
});
// 画面をマウントした際に実行されるイベント
$onMounted(() => {
initView(); // 初期表示用
getAttachments(); // 添付ファイルの取得
});
// 初期表示用関数
const initView = async () => {
// コメントの取得
comments = (await record.histories())
.filter(c => c.comment !== "") // 空文字は除外
.reverse(); // 逆順に
// 画面更新
$update();
};
// 省略
};
添付ファイルの取得
添付ファイル(今回は画像のみ)は、getAttachments
関数で取得します。download
メソッドを使うと、レコードのdata
プロパティにファイルのデータが入ります。
// 添付ファイルの取得
const getAttachments = async () => {
const attachment = record.get("attachment");
if (!attachment || attachment.length === 0) return;
try {
// 添付ファイルのダウンロード
await Promise.all(attachment.map(a => a.download()));
attachments = attachment.map(a => URL.createObjectURL(a.data));
$update();
} catch (e) {
console.log(e);
}
}
ダウンロードした内容は Blob
型になっているので、 URL.createObjectURL
を使って blob:〜
形式に変換しています。表示はテンプレートの attachments
にて処理しています。以下は該当部分の抜粋です。
<li>
${attachments.map(a => $h`
<div class="item-content">
<div class="item-inner">
<img src="${a}" width="100%" />
</div>
</div>
`)}
</li>
コメントの登録
新しいコメントを入力してボタンを押すと、addComment
関数が呼ばれます。この関数ではコメントを登録するのみで、非同期処理にする必要はありません。データが保存されればsubscribe
メソッドが呼ばれるので、そこで画面の更新を行います。
// コメントを登録するイベント
const addComment = async () => {
// 入力値の取得
const text = $("#text").val();
// コメント投稿
const comment = record.comment();
comment
.set("comment", text)
.save();
$("#text").val(""); // リセット
};
登録したコメントはHexabaseでも確認できます。
まとめ
Hexabase SDKを使えば、データストアからデータを取得したり、登録したりするのは簡単です。Hexabaseではデータの型があるので、間違ったデータ型で登録しようとするとエラーになるので安心です。
外部システムと連携したり、よりスマホやタブレットから便利に使うためにMonacaアプリとHexabaseを組み合わせて利用してください。