ブラウザの画面をもう片方の画面に転送するものを作った。
めっちゃ簡単にWeb上の画面を他の端末に再現するものを作った。
— 弩.net@Coqやっていき (@isyumi_net) December 23, 2018
ユーザーテストとかに使えそう。
Android・PCはWebRTCで
iPhoneはWebSocketで画面を転送する。 pic.twitter.com/smxdYBwRiE
動機
AWSがWebSocketのリバプロに対応してくれた。
これまでWebSocketでいろいろ作った。楽しかった。しかし、一つ問題があった。WebSocketを使うとインフラ・デプロイの自動化が不可能になる点だ。HTTPと違いユーザーが使っている間はずっと接続が維持される。つまり、ユーザーが一人でも残っていたらサーバーを捨てれないのだ。
そこで、Nginxなどを使って自前でWebSocketリバースプロキシを立てるのがこれまでの習慣だった。当然、そのサーバーにパッチを当てたりするのが嫌になる。
この度、それをAWS側が巻き取ってくれることになった。ありがたい。せっかくなので前々から作ろうと思っていたこのシステムを作った。
用途
ユーザーテストとかに使えると思う。
あと、無許可でお客さんに対してやるのはやめましょう。
動作要件
送信画面は数行のJSを入れられればなんでもいい。
WebSocketサーバーは別のドメインになっても問題ない。
受信画面は送信画面と同じドメインに存在する義務がある。これは主に相対アドレス解決の問題である。無理なら手元にプロキシサーバーを立ててhosts.txtに入れればよいだろう。WordPressを使うなら、共通ヘッダーに受信用スクリプトを入れて、固定ページに送信用スクリプトを入れればいいだろう。
やっていること
- 画面の変更を検知
- innerHTMLを取得
- それをWebSocketで別のブラウザに転送
- innerHTMLをサニタイズ
- 表示
画面の変更検知
- DOMの変更検知
- Touchイベント
- InputEvent
- Scrollイベント
MutationObserver
DOMの変更を検知するもの。以前はDOMNodeInsertedなどを使っていたが、現代ではこのAPIを使うべきだ。このAPIはコールバック関数が非同期で起動されるのでユーザーにやさしい。
Touchイベント
Start/End/Moveイベントを拾うことでスワイプをたどることができる。
毎度毎度clientYとかscreenYとかpageXとか、どれを使えばいいんだっけとググる羽目になるので、正解を書いておく。clientXとclientYだ。
InputEvent
MutationObserverとinnerHTMLだけではinput要素の中を取れないようだ。どのハンドラを使うべきかだが、
- onChangeだとフォーカスが外れるまで発火しない
- onKeyPressだと最新のvalueが取れない
という点を考慮し、今のところonKeyUpイベントをハンドルするのが正解ではないかという気がする。
一つ問題がある。DOMの中の特定の要素を一意に指すことができるグッドな方法がないことだ。一般的にXPATHが使われる。しかし、JSにはあるDOMのXPATHを取り出すAPIもXPATHからDOMを取り出すAPIもない。
そこでその要素は同じタグの何番目というXPATHもどきを作った。
String getPath(HtmlElement element) {
var tagName = element.tagName.toLowerCase();
var tags = document.querySelectorAll(tagName);
var elementIndex = tags.indexOf(element);
return "${tagName}/${elementIndex}";
}
これなら画面を超えて一意に特定のDOM要素を指すことができる。
Scrollイベント
当たり前の話だが、目上の人のwindowのscrollイベントをハンドリングするときはpassiveを指定するのがマナーです。
innerHTMLの送信
おそらく
- HEADタグの中身
- BODYタグの中身
- 現在のスクロール位置
- 現在のURL
を1セットで送るのが合理的なようだ。理由は↓。
サニタイズ
まず、innerHTMLをDOMに変換したい。いきなりwindow.documentに入れてしまうと大変なことになる。
DocumentFragmentというブラウザのAPIを使うのが正しいと思う。自分はDartで書いたのでJSの詳細なAPIは存じ上げないが、nsTreeSanitizerというのを正しく設定しないとダメなようだ。Dartの場合はtreeSanitizer: NodeTreeSanitizer.trustedでおk。
次にHEADのDocumentFragmentにbase要素を入れる。こうすることで送信画面と同じCSS・imgを見に行ってくれる。
次にスクリプトを削除する
- スクリプトタグを全部消す
- 各要素のonで始まるattributeを全部消す
- Link rel="serviceworker"を前部消す
で大丈夫だと思う。違ったらだれか教えてください。
準備ができたらnodeを入れ替えてScroll位置を設定すれば大丈夫。