Electronで埋め込みページをwebViewで挿入し、webView内にあるリンク(target="_blank")を同一windowで開く方法

Electronアプリを作成している中で、以下のようなシチュエーションにぶち当たりました。

```
ステップ１：Electronアプリ内でリンクをクリック
↓
ステップ２：新しいElectron windowでリンク先のページをナビゲーションバー付きで開く
↓
ステップ３：ステップ２で開いたwindow内にあるリンク（target="_blank"付き）をクリックした際に、新しいwindowで開くのではなく2個目に開いたwindow内で表示させたい
```

Electronの公式サイトでは以下3つの方法が紹介されていましたが、今回はWebViewsを利用した実装方法を紹介します。

1. Iframes
1. WebViews
1. BrowserViews

ただしWebViewsはdeprecatedなので、将来的にはBrowserViewsに移行したい。
参考：https://www.electronjs.org/docs/latest/tutorial/web-embeds

ちなみにアプリは、Electron + webpack + react で開発しています。


## WebViewsの使い方
使い方自体はとっても簡単で、`<webview>`をHTML内に挿入するだけです。

```
 <webview id="hoge" src="https://www.hogehoge"></webview>
```
埋め込む画面の高さ・幅を変えたい場合はstyleタグを入れます。

```
 <webview id="hoge" src="https://www.hogehoge"
    style="width:200px; height:150px"></webview>
```

## ナビゲーションバーの作り方
今回はシンプルに「戻る」「進む」のみを作成しています。
cssも少量なので直接HTML内に書いています。
WebViewsと合わせるとこんな感じ。
```navigation.html
<html>
  <head>
    <style>
      .header {
        top: 0;
        width: 100%;
        border:1px solid #ccc;
        display: flex;
        align-items: center;
      }

      button {
        margin:5px;
      }
    </style>
  </head>
  <body style="height: 100%; margin:0">
      <div>
        <nav class="header">
            <button onclick="goBackPage()" id="back">&lt;</button>
            <button onclick="goForwardPage()" id="forward">&gt;</button>
        </nav>
    
        <div>
          <webview id="hoge" src="https://www.hogehoge"
                   style="width:200px; height:150px"
          ></webview>
        </div>
      </div>
  </body>
</html>
```

Electron内でとあるリンクをクリックした際に、このHTMLを読み込んで新しいWindowで表示させる感じです。

### 「戻る」「進む」ボタンの機能実装
scriptもシンプルなのでnavigation.html内に入れちゃってます。

```
  <script>
    const gameView = document.getElementById('hoge')
    const backButton = document.getElementById('back')
    const forwardButton = document.getElementById('forward')

    const goBackPage = () => {
      if(gameView.canGoBack()) {
        gameView.goBack()
      }
    }

    const goForwardPage = () => {
      if(gameView.canGoForward()) {
        gameView.goForward()
      }
    }

    // 戻るや進むページがない時に disableにしています
    const check = () => {
      backButton.disabled = !gameView.canGoBack();
      forwardButton.disabled = !gameView.canGoForward();
    }

    const goBackToGame = () => {
      gameView.goToIndex(0)
    }

    gameView.addEventListener("dom-ready", () => {
      check()
    })

    gameView.addEventListener("did-frame-navigate", () => {
      check()
    })
    gameView.addEventListener("did-navigate-in-page", () => {
      check()
    })


  </script>
```

## Electron側での読み込み
このようにして作成したHTMLをElectronのmainプロセスで読み込んで表示させました。
```main.ts
    // ipcRender.onを使ってrendererでリンクがクリックされた際にこのイベントがよばれる形です。
    // argumentとかpreloadとかは色々省略しています。
    ipcMain.on("some-event" (e) => {
        const width = 1400;
        const height = 1300;
    
        const win = new BrowserWindow({
          width,
          height,
          title,
          webPreferences: {
            devTools: isDev,
            preload,
            contextIsolation: false,
            nodeIntegrationInSubFrames: true,
            webviewTag: true,
          },
        });
    
        win.webContents.loadFile('./navigation.html'); // ここでHTMLを読み込み
        
        //....
    })
    
```

ここまでで一旦、ステップ２まで実装することができました。
ステップ３なんて簡単だろーって思っていましたが、結局一番時間がかかりました。。。
```
ステップ１：Electronアプリ内でリンクをクリック
↓
ステップ２：新しいelectron windowでリンク先のページをナビゲーションバー付きで開く
↓
ステップ３：ステップ２で開いたwindow内にあるリンク（target="_blank"付き）をクリックした際に、新しいwindowで開くのではなく2個目に開いたwindow内で表示させたい
```

まず前提として、WebViewsではポップアップがデフォルトでfalseになっているので、target="_blank"付きのリンクをクリックしても開きません。ポップアップを有効にするためには、`allowpopups`をwebviewタグにつけてあげる必要があります。
```
<webview id="hoge" src="https://www.hogehoge"
         style="width:200px; height:150px"
         allowpopups
></webview>
```
これでリンク自体は開くようになりますが、これだと新しいwindowで開くことになります。
今回はあくまでもステップ２で生成されたwindow内でページ遷移をしたかったのです。

調べてみたところ、どうやらmainプロセスでwebContentsを使って制御ができるみたいです。
実際に追加したものとしては以下のような感じです。

```main.ts
    // ipcMain.onを使ってrendererでリンクがクリックされた際にこのイベントがよばれる形です。
    // argumentとかpreloadとかは色々省略しています。
    ipcMain.on("some-event" (e) => {
        const width = 1400;
        const height = 1300;
    
        const win = new BrowserWindow({
          width,
          height,
          title,
          webPreferences: {
            devTools: isDev,
            preload,
            contextIsolation: false,
            nodeIntegrationInSubFrames: true,
            webviewTag: true,
          },
        });
    
        win.webContents.loadFile('./navigation.html');

        //　以下追加
        win.webContents.on('will-attach-webview', (_, webPreferences, params) => {
          webPreferences.contextIsolation = false;
          webPreferences.nodeIntegrationInSubFrames = true;
          webPreferences.preload = preload;

          params.src = url;
        });

        win.webContents.on('did-attach-webview', (_, webViewContents) => {
          webViewContents.setWindowOpenHandler(({ url }) => {
            webViewContents.loadURL(url);
            return {
              action: 'deny',
            };
          });
        });
        
        //....
    })
    
```

色々設定はしていますが、肝心の新しいwindowで開かないようにするために大事なのはここです。
```
        win.webContents.on('did-attach-webview', (_, webViewContents) => {
          webViewContents.setWindowOpenHandler(({ url }) => {
            webViewContents.loadURL(url);
            return {
              action: 'deny',
            };
          });
        });
```

`action: deny` とすることで新しいwindowで開かないように設定できました。
`allowpopups`でtarget付きのリンクを開くことは許容するけど、新しいwindowはやめてね、みたいな感じになっているのだと理解しています。

上記以外にも、ipcRendererを使った方法や、aタグ内の`target="_blank"`を取り除いちゃう方法なども発見しました。
参考にした記事: http://var.blog.jp/archives/86316642.html

今回はwebpackも利用しているのでなるべくconfigをいじらずにできる方法を模索してたどり着きましたが、
きっともっといい方法あるはず！
そしていずれは、BrowserViewsに移行したいと思っています。

何かのお役に立てれば幸いです :bow: