【Misskey】SNSの投稿をWebサイトに埋め込むためのウィジェットを作った話【Vue.js】
分散型SNSであり、(貴重な?)国産のOSSとしても有名なMisskeyの最新バージョン v2024.9.0 では、Misskeyの投稿やタイムラインなどを埋め込んで表示できるようになりました。埋め込み機能は考慮すべきことが多く、実装に時間がかかります。実際、埋め込みウィジェットの初版を作ってから約1年半かかって実装にこぎつけました。
完成品(画像)
ここでは、Misskeyの埋め込み機能の実装が難しい点を確認したり、実装までの過程を振り返ったりしていきたいと思います。
Misskeyの投稿を埋め込むには何が必要か
MFMのレンダリング
Misskeyは、フロントエンドの高いカスタマイズ性が特徴です。それは投稿そのものにおいても同じで、MFMという独自の言語を使用して書式を設定できるようになっています。
ここで、MFMをちょっと見てみましょう。例えば、こんなふうに書くと…
$[fg.color=f00 赤字]
$[bg.color=ff0 黄背景]
$[rotate.deg=30 misskey]
こんなふうに表示されます(もちろん構文をネストすることも可能です)。これを駆使してアート作品を作るMFMアートなんてものもあり、Misskeyのメインコンテンツの一つとして親しまれています。
Misskeyの埋め込みウィジェットを公式で実装するにあたって、MFMを正しく表示できるのはマスト。しかし、同時に実装難易度の高い要素でもあります。
リッチなUI
埋め込みウィジェットと実際のクライアントのUIが大きくかけ離れていると、それはそれでガッカリもの。できるだけ揃えたいところです。
さらに、できることなら、Webクライアントと同じようにカスタマイズ性に優れた埋め込みウィジェットを実現したいです。例えば、埋め込みウィジェットのカラーモードを固定したり、外枠を角丸にするかどうかを指定したりと、各種パラメータを埋め込むウェブサイト側で制御することで、ウェブサイトに溶け込みやすいデザインにできるようにする等が挙げられます。
セキュリティ
埋め込みウィジェット内でログイン処理などが行われ、その内部からインタラクションを行えるのはセキュリティ的にまずい。そのため、既存の仕組みとまた別でロジックを組む必要があります。
どうやって実装しようとしたか
これらをすべて実現しつつ、なるべくメンテナンス性の良さそうな実装を見つけるのに相当苦労しました。ここからは、時系列順に私が試した実装アプローチを見ていきましょう。
① 完全SSR
MisskeyのWebクライアント本体は、クライアントに配信されたJavaScriptが画面全体を描画するSPA方式を取っています。しかしそれと共通化するとセキュリティ上の懸念があるということから、バックエンドでpugを駆使してSSR(サーバーサイドレンダリング)することを試みました。
HTMLの骨子はすべてサーバーサイドで構築したあと、必要なインタラクション部分に、Webクライアントとは独立したJavaScriptコードを用意するという方式です。
この方式の開発断念直前の画像。
なかなか健闘してそう?
しかし、これでは従来の仕組みやフロントエンドの資産をうまく活かすことができずメンテナンスコストが莫大になることや、SPA主体のアプリケーションでSSR実装に対応することに限界を感じ、この方式での開発は停滞しました。
② 既存のSPAに統合
次に、既存のWebクライアントの起動処理の大部分(クライアントの設定を読み出して適用する動作やアカウント情報の初期化など)をバイパスすることで、コンポーネントの大部分を流用しつつ埋め込み機能を実現する方式を試しました。
ここで制作したコンポーネントはほぼ現行の方式に流用しているので、
見た目はあまり変わりません
この方法では、Webクライアントの機能をほぼそのまま利用しつつ、埋め込みページで読み込まれた場合の条件分岐を適宜追加して余計な機能が起動するのを抑制するという方法を取っていました。
しかし、これでは、Webクライアントに条件分岐が大量に追加されることになるほか、今後の開発においても埋め込みで表示される場合のハンドリングを常に意識しておく必要があり、これまたメンテナンスコストが高そうだということで悩みのタネとなっていました。
③ 埋め込み用・Webクライアント用でSPAを分割し、一部のスクリプトのみを流用
結局いろいろあって、③の方式に落ち着きました。フルスクラッチで開発しなおした① → ②とは違って② → ③は連続しており、②をベースにして改良していきました。この改良ではMisskeyプロジェクトリーダーのしゅいろさんが大部分の設計を担当しました。
この方式では、通常のWebクライアント(frontend
)と埋め込み用フロントエンド(frontend-embed
)を完全に分離し、双方に共通な部分をfrontend-shared
パッケージに入れて呼び出すというアプローチを取っています。
packages/
├─ frontend/
│ ├─ src/
│ │ ├─ components/
│ │ ├─ scripts/
│ │ ├─ ...
│
├─ frontend-shared/
│ ├─ src/
│ │ ├─ components/
│ │ ├─ scripts/
│ │ ├─ ...
│
├─ frontend-embed/
│ ├─ src/
│ │ ├─ components/
│ │ ├─ scripts/
│ │ ├─ ...
これにより、コア部分のロジックを共通化しつつ、Webクライアントとは完全に切り離されて独立したアプリケーションとして埋め込みウィジェットの実装を実現しました。
また、frontend
とfrontend-embed
は同じ構成の技術スタック(Vue + Vite)となっているため、frontend
のコンポーネントをコピーして流用することができ、結果としてMFMなどのMisskey独自のリッチな要素の再現もほぼ完璧に実現することに成功しています。
こうして、去る2024年9月9日に、埋め込みウィジェットが正式にMisskeyに取り込まれました。①の実装(2023年4月6日)から起算すると約1年5ヶ月かかって実装されたことになります。ものすごい作業量でしたが、設計を試行錯誤したりすることでノウハウを学ぶこともできましたし、また「埋め込み機能」の実装の難しさも痛感することができました。
結果的に、合計で10,000行近くを書き換える大規模なPRになりました(これ単体でMisskeyの1回分のリリースくらいに相当することもあるような量です)。
その後…
Misskey v2024.9.0が正式リリースされるまでにも、埋め込みウィジェット関連の整備やパフォーマンス改善を継続的に行いました。ここではその一部を紹介します。
ドキュメント整備
埋め込みウィジェットは、Misskey Webクライアントに内蔵している埋め込みウィジェットカスタマイザーを使えば基本的にすべてのパラメータを視覚的に編集することができるようになっています。
だいたいの場合これで事足りる…?
しかし、より深くカスタマイズしたい場合や埋め込みコードを動的に生成したい場合などにドキュメントがあるのは嬉しい…ということで、Misskeyプロジェクトの公式サイトにドキュメンテーションを追加しました。
カスタマイザーのUI内テキストに比べて多くの文章が書けるので、より踏み込んだ仕様や、非推奨の仕様をあえて残している理由などをドキュメントにまとめて、より柔軟に埋め込み機能を活用していただけるようにしました。
非推奨の値を無効化せずあえて残している理由は
ちゃんとあるのでした・・・(画像下部のアコーディオン)
さらなるパフォーマンス改善
Misskeyでは、他のSNSにシェアした際などに表示されるOGPタグや、他のActivityPub互換サーバーから問い合わせがあった際に適切なレスポンスを返すため、一部のHeadタグはサーバーサイドレンダリングされるようになっています(冒頭の①のアプローチはこの仕様をフルに活用しようとしたものです)。ここに埋め込みで使用する一部のAPIのレスポンスを埋め込むことで、追加で発生するAPIリクエストをなるべく減らし、さらなるパフォーマンス改善を実現することができました(これもしゅいろさん発案・実装で、私は最終チェックを行いました)。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- metaタグやlinkタグが並ぶ 省略 -->
+ <script type="application/json" id="misskey_meta" data-generated-at="1727265997824">{ "name": "サーバー名", "description": "説明文" }</script>
+ <script type="application/json" id="misskey_embedCtx" data-generated-at="1727265997824">{ "note": { "id": "9ylcw1dzlczd0ch4", "text": "hoge" } }</script>
<script src="/assets/boot.js"></script>
</head>
<body>
<noscript><p>JavaScriptを有効にしてください<br>Please turn on your JavaScript</p></noscript>
<!-- Vueがマウントされるまでの間をつなぐローディング画面 -->
<div id="splash">
<img id="splashIcon" src="/favicon.ico">
<div id="splashSpinner">
<svg class="spinner bg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1,0,0,1,12,12)">
<circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
</g>
</svg>
<svg class="spinner fg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1,0,0,1,12,12)">
<path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
</g>
</svg>
</div>
</div>
</body>
</html>
Head内に、misskey_meta
とmisskey_embedCtx
を追加。それぞれ、
- サーバーのメタデータ(サーバーの説明文やサーバーの設定などが入ったもの)
- 埋め込みコンテキスト(パスに応じて絶対に必要になるリソースが入っている)
です。これらは、JavaScriptを用いて同期的に呼び出すことができます。
import * as Misskey from 'misskey-js';
const metaEl = document.getElementById('misskey_meta');
const meta = JSON.parse(metaEl?.textContent) as Misskey.entities.MetaDetailed;
これにより、APIリクエストを最大2回削減することができ、大幅な読み込み時間短縮に繋がりました。
まとめ
このように、今回はMisskeyに埋め込み機能を実装した話をさせていただきました。個人的にこれまで経験した中で過去最大規模の機能追加となり、貴重な体験ができたと感じています。
今後このアップデートが浸透していくことで、より便利に・楽しくMisskeyを使っていただけると大変嬉しく思います!
Discussion