概要
VSCodeの拡張機能DoxygenPreviewerというのを作ってますが、その機能拡張時に勉強したことを書きます。
なにをしたか
- Doxygenのプレビューを出せるようになっていたが、リンクを飛んだりできなかったので飛べるようにした
どう実現したか
- extension側からWebview側のJavaScriptとの間で通信するようにした
- aタグのclickをトリガーに通信を実行するようにした
このように通信を行い、extensionとwebviewが連携してWebブラウザのような操作をある程度再現できるようにしました。
詳細
何が問題か
VSCodeはHTMLを表示するAPIを提供していますが、セキュリティのためにそれでできることはとても限られています。
たとえばCSSを読み込めなかったりですが、同様にaタグとかのリンクも動かないです。
この対策をググってみると、下記のstackoverflowにヒントがありました1。
VSCode: How to enable links in a webpage in the Webview
If you want to update the webview content itself and cannot do this within the webview itself for some reasons, use either a command uri or postMessage to call some function in your main extension's source that update's the webview's html
要するにpostMessage
でextensionにwebviewのhtmlを更新させろとのことです。
postMessageの使い方
VSCodeのAPIリファレンスにはpostMessageについてこう書いてあります。
Post a message to the webview content.
Postしたらどうやって受け取れとは書いてません。あと、まずやりたいのはwebviewからextensionにメッセージを投げることです。
これについては実はwebviewのサンプルページ(例のCat Coding)に説明があります。
Passing messages from a webview to an extension
acquireVsCodeApi
を使ってどうこう、という感じで、説明は面倒なのでコードを書きます。(私のコードから引用します。)
const vscode = acquireVsCodeApi();
function vscode_post_msg(command, data)
{
obj = {
command: command,
data: data,
}
vscode.postMessage(obj);
}
このmessage.jsを前回CSSを埋め込んだ要領でHTMLに埋め込み、実行させてextension側で受け取ります。
panel_obj.webview.onDidReceiveMessage( (ev) => {
console.log("receive : " + ev);
if (!ev.data) {
return;
}
const data = ev.data;
if (ev.command === "link") {
next_html_load(data, panel_obj, css_src, insert_script_str);
}
});
panel_obj
はwebviewのインスタンスです。このインスタンスはwebviewメンバーでonDidRecieveMessageメソッドを持っていて、これでメッセージの受信イベントを登録できるわけです。
リンクをクリックされたら通信を行う
これは従来のJavaScriptのテクニックなので筆者より読者の方が詳しいと思いますが一応書きます。
function link_message(href_str)
{
const line_id = href_str.replace(/^.+#/, "");
if (line_id.length > 2) {
var elm = document.getElementById(line_id);
if (elm) {
elm.scrollIntoView();
}
}
vscode_post_msg("link", href_str);
}
window.onload = () => {
var a_tags = document.getElementsByTagName("a");
for (var i = 0; i < a_tags.length; i+=1)
{
var element = a_tags.item(i);
if (!element.className === "el") {
continue;
}
const href_str = element.href;
element.addEventListener('click',
() => {
link_message(href_str);
})
}
}
やってることは:
-
window.onload
でページのロードを待ち -
getElementsByTagName
でaタグの要素を読み出し - リンクさせたいタグが
el
クラスなのでそれをチェックし -
addEventListener
でメッセージを送るイベントを登録する
こうしてextensionでメッセージを受信したらwebviewのhtmlを書き換えるようにして、リンクを飛んだように見せかけることができました。
その他
hrefの値に関して(蛇足)
よくわからないのですが、hrefのままだとリンクを飛ぼうとして飛ばない(iframeの中身が初期化される)みたいな動きをします。
ここで、hrefに設定される値(URI)をfile:///
にするとリンクを飛ばなくなりました。
webviewをスクロールさせる
これもJavaScriptのテクニックなので何も特別なことはないのですが。
上に示したlink_message()
の中でscrollIntoView()
を使っていますが、これにより指定した要素までスクロールさせることができます。
extensionからwebviewにメッセージを送る
用意だけしてまだ使ってないのですが、下記のようにしてできます。
const obj = {
command: "scroll",
data: scrl_pos,
}
panel_obj.webview.postMessage(obj);
window.addEventListener('message', event => {
const message = event.data; // The JSON data our extension sent
switch (message.command) {
}
});
postMessage()
を使うのは同じですね。
addEventListener
をtypemessage
で使うことでなぜextensionからのメッセージを受け取ることができるのか、そもそもaddEventListener
ってなに状態ですが、このようにしてextensionからwebviewにメッセージを送れます。
まとめ
主にextensionとwebviewとのメッセージのやりとりと、あとはJavaScriptのいくつかのテクニックに触れました。JavaScriptはいろんな機能を持っているんだなあと思いました。あと、VSCodeのサンプルページはもうちょっと目次をちゃんと書いてほしいと思いました。しかしサンプルページをもっとよく読めば更なる発見があるかもしれません。
-
なんか違う解決策で喜んでいる気がしますが、とにかくこの方針で解決させました。 ↩