Webviewでブラウジングっぽいことを実現する

# 概要
VSCodeの拡張機能[DoxygenPreviewer](https://qiita.com/hakua-doublemoon/items/c328a7bf0bc7a1fbef14)というのを作ってますが、その機能拡張時に勉強したことを書きます。

## なにをしたか

* Doxygenのプレビューを出せるようになっていたが、リンクを飛んだりできなかったので飛べるようにした

## どう実現したか

* extension側からWebview側のJavaScriptとの間で通信するようにした
* aタグのclickをトリガーに通信を実行するようにした

このように通信を行い、extensionとwebviewが連携してWebブラウザのような操作をある程度再現できるようにしました。

# 詳細
## 何が問題か

VSCodeはHTMLを表示するAPIを提供していますが、セキュリティのためにそれでできることはとても限られています。
たとえば[CSSを読み込めなかったりですが](https://qiita.com/hakua-doublemoon/items/cfb638fe1ab43ca42cb8#html%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B)、同様にaタグとかのリンクも動かないです。
この対策をググってみると、下記のstackoverflowにヒントがありました[^1]。

[VSCode: How to enable links in a webpage in the Webview](https://stackoverflow.com/questions/56820226/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](https://code.visualstudio.com/api/extension-guides/webview#passing-messages-from-a-webview-to-an-extension)
`acquireVsCodeApi`を使ってどうこう、という感じで、説明は面倒なのでコードを書きます。（[私のコードから引用します](https://github.com/hakua-doublemoon/DoxygenPreviewer)。）

```script/insert_script/message.js
const vscode = acquireVsCodeApi();
function vscode_post_msg(command, data)
{
    obj = {
        command: command,
        data:    data,
    }
    vscode.postMessage(obj);
}
```

このmessage.jsを前回CSSを埋め込んだ要領でHTMLに埋め込み、実行させてextension側で受け取ります。

```src/extension.ts
    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のテクニックなので筆者より読者の方が詳しいと思いますが一応書きます。

```script/insert_script/message.js
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にメッセージを送る

用意だけしてまだ使ってないのですが、下記のようにしてできます。

```src/extension.ts
        const obj = {
            command: "scroll",
            data:    scrl_pos,
        }
        panel_obj.webview.postMessage(obj);
```
```script/insert_script/message.js
window.addEventListener('message', event => {
    const message = event.data; // The JSON data our extension sent
    switch (message.command) {
    }
});
```
`postMessage()`を使うのは同じですね。
`addEventListener`をtype`message`で使うことでなぜextensionからのメッセージを受け取ることができるのか、そもそも`addEventListener`ってなに状態ですが、このようにしてextensionからwebviewにメッセージを送れます。

# まとめ
主にextensionとwebviewとのメッセージのやりとりと、あとはJavaScriptのいくつかのテクニックに触れました。JavaScriptはいろんな機能を持っているんだなあと思いました。あと、VSCodeのサンプルページはもうちょっと目次をちゃんと書いてほしいと思いました。しかしサンプルページをもっとよく読めば更なる発見があるかもしれません。

[^1]: なんか違う解決策で喜んでいる気がしますが、とにかくこの方針で解決させました。