Electron単体でGoogle APIを叩く

既に[Node.jsのOAuthを使ってGoogleDriveAPIを叩いてみた](https://qiita.com/maximum80/items/645cae98c1094a80976c)という記事がありますが、GUIアプリでわざわざ別のブラウザから認証コードをコピペしてくるのも面倒なので内蔵WebViewを使ってよろしくやってみたメモ。

IPCは使っていないですがUIにVue.jsとvue-routerを使っているので若干その知識が必要になります。

ソースはこちら
https://github.com/kuinaein/electron-drive/compare/bc77d0d...88a3c37

大まかな流れは次の通り。

1. Google Cloud Console で OAuth クライアント ID を作成
1. electron-vue テンプレートを使って新規 Electron プロジェクトを作成
1. ユーザー認証のための WebView 画面を作成
1. Google API (今回は Drive API) を使う画面を作成

# Google Cloud Console で OAuth クライアント ID を作成

このやり方はあちこちで言及されているのでざっと流します。

1. Google Cloud Console にログインし、[APIとサービス ＞ 認証情報](https://console.cloud.google.com/apis/credentials)を開く
1. 認証情報を作成 ＞ OAuth クライアント ID
1. アプリケーションの種類は「その他」を選択
1. できたクライアント ID の行の編集ボタン（鉛筆マーク）をクリック
1. 「JSONをダウンロード」をクリック
1. client_secret.json 等の名前で保存しておく

# electron-vue テンプレートを使って新規 Electron プロジェクトを作成

今回は UI ライブラリとして Vue.js 、HTML テンプレートに Pug を利用することにします。

```bash
sudo npm install -g vue-cli
# プロジェクト名はお好きに
vue init simulatedgreg/electron-vue electron-drive
cd electron-drive
yarn install
yarn add googleapis pug-html-loader
````

プロジェクト設定はデフォルトのままでOKです。ただ ESLint を入れるとコーディング規則違反でコンパイルエラーになるので、小物を作るときは外したほうがいいかもしれません。（ちょっと癖がある……）

# ユーザー認証のための WebView 画面を作成

最初に client_secret.json を src/ あたりに移しておきます。

まず認証画面を表示するのですが、今回は vue-router を使っているので `renderer/router/index.js` にルート定義を書きます。

```js:renderer/router/index.js
const REDIRECT_URI = 'http://localhost:11451/'

export default new Router({
  routes: [
    {
      path: '/',
      name: 'login',
      component: require('@/components/LoginPage').default,
      meta: { redirectUri: REDIRECT_URI }
      // name: 'landing-page',
      // component: require('@/components/LandingPage').default
    },
    // ...
```

REDIRECT_URI はダミーなので localhost ならポート番号は何番でも良いです。

`renderer/components/LoginPage.vue` は Google のログインページを表示するだけなので、全面WebViewです。

```vue:renderer/components/LoginPage.vue
<template lang="pug">
webview(ref="webview" :src="url" @will-navigate="onWillNavigate")
</template>

<style scoped>
webview {
  display: inline-flex;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}
</style>

<script>
const { google } = require('googleapis')
const OAuth2 = google.auth.OAuth2

const clientSecret = require('@/../client_secret.json').installed

export default {
  data () {
    return { url: 'about:blank' }
  },
  mounted () {
    if (process.env.NODE_ENV === 'development') {
      window.webview = this.$refs.webview
    }
    const auth = new OAuth2(clientSecret.client_id,
      clientSecret.client_secret,
      // clientSecret.redirect_uris[0])
      this.$route.meta.redirectUri)
    this.url = auth.generateAuthUrl({scope: 'https://www.googleapis.com/auth/drive'})
  },
  methods: {
    onWillNavigate (ev) {
      const url = '' + ev.url
      if (url.startsWith(this.$route.meta.redirectUri)) {
        // 念の為止める。 stop() は効かない模様
        this.$refs.webview.loadURL('about:blank')
        const parser = new URL(url)
        const code = parser.searchParams.get('code')
        this.$router.push({name: 'drive', query: {code}})
      }
    }
  }
}
</script>
```

認証が通ってリダイレクトされたタイミングで `will-navigate` イベントが発火するので、ここで `webview.loadURL()` を呼んで WebView を止めます。
そのあと `$router.push()` で API を利用する画面に飛ばします。このときに取得した認証コードを渡しています。

# Google API (今回は Drive API) を使う画面を作成

こちらもまずルート定義を入れます。

```js:renderer/router/index.js
    {
      path: '/drive',
      name: 'drive',
      component: require('@/components/DrivePage').default,
      meta: { redirectUri: REDIRECT_URI }
    },
```

次いでビューを書きます。今回はマイドライブのファイル一覧を表示するだけのシンプルなものです。

```vue:renderer/components/DrivePage.vue
<template lang="pug">
div
  div(v-if="null === files") ロード中...
  ul(v-else)
    li(v-for="f of files" :key="'file-' + f.id") {{f.name}}
</template>

<script>
const { google } = require('googleapis')
const OAuth2 = google.auth.OAuth2

const clientSecret = require('@/../client_secret.json').installed

export default {
  data () {
    return { files: null }
  },
  mounted () {
    const auth = new OAuth2(clientSecret.client_id,
      clientSecret.client_secret,
      this.$route.meta.redirectUri)
    auth.getToken(this.$route.query.code).then(res => {
      auth.credentials = res.tokens

      const drive = google.drive({version: 'v3', auth})
      return drive.files.list({
        q: "'root' in parents and trashed = false"})
    }).then(res => {
      this.files = res.data.files
    }).catch(err => {
      console.log(err)
      alert('エラー！: ' + err)
    })
  }
}
</script>
```

APIを呼ぶためにはまず認証コードを元にトークンを得る必要があるので、最初にそれを行っています。
あとは OAuth オブジェクトにトークンをセットして API を呼ぶだけ。

意外と簡単でした……。あくまで実験用なので auth オブジェクトの持ち方とか雑ですが。