Vue.js + TypeScriptで作るLIFF v2アプリ

# 2020/07/02追記
記事を公開した当日に公式のnpmパッケージが公開されました。
https://developers.line.biz/ja/news/2020/07/01/published-liff-npm-package/

上記パッケージを使用することでLIFF SDKの導入と同時に型の情報を得ることが出来るので、本記事の内容よりそちらを推奨します。

# はじめに
まず、LIFFとはLINEが提供するウェブアプリのプラットフォームです。現在の最新バージョンは2系となっており、LIFF v2と表記されます。
以下は概要の引用ですが、LIFFを使用することでLINEの機能を活かしたWebアプリを開発することが出来ます。
https://developers.line.biz/ja/docs/liff/overview/

> LINE Front-end Framework（LIFF）は、LINEが提供するウェブアプリのプラットフォームです。このプラットフォームで動作するウェブアプリを、LIFFアプリと呼びます。
> LIFFアプリを使うと、LINEのユーザーIDなどをLINEプラットフォームから取得できます。LIFFアプリではこれらを利用して、ユーザー情報を活用した機能を提供したり、ユーザーの代わりにメッセージを送信したりできます。

本記事ではLIFFアプリをVue.js + TypeScriptで作成します。

# 手順

本記事では以下の手順でLIFFアプリを作成します。

1. チャネルを作成する
1. Vue.jsのプロジェクトを作成する
1. LIFFアプリを開発する

1.の手順は主に `LINE Developers` 上の話を、2.の手順では `Vue CLI` でVue.jsのプロジェクトを作成する話をします。
上記手順は読み飛ばしても大丈夫という方は3.のLIFFアプリを開発する部分だけをお読みいただけると幸いです。

# チャネルを作成する

## ngrokを起動

LIFFアプリを作成するためにはWebアプリをhttpsで公開する必要があります。
S3などにホスティングしても良いのですが、本記事ではngrokを使用してローカルで動くVue.jsアプリを公開します。
※ngrokのセットアップは主題から外れるため割愛します。

```console
ngrok http 8080
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314315/6b00cb71-7371-6e2c-8749-ca1b85b694a0.png)


`https://xxx.ngrok.io`はLIFFのエンドポイントURLとして使用するので控えておきます。


## プロバイダーを作成する

1. [LINE Developers](https://developers.line.biz/ja/)にログインする
2. プロバイダーを作成する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314315/c6cac117-68fd-ec71-ffea-c14aac5fcaf0.png)

## チャネルを作成する

作成したプロバイダーに`LINEログイン`チャネルを追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314315/7ec71abb-e10e-cc70-3be8-323e91e3fa3b.png)


## LIFFアプリを追加する

チャネルにLIFFアプリを追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314315/9931e49d-dae9-475e-dbd1-879b44dccea6.png)


各設定項目については[公式のドキュメント](https://developers.line.biz/ja/docs/liff/registering-liff-apps/#registering-liff-app)に詳しく書かれています。
エンドポイントURLには前述の手順で控えておいたngrokのURL、`https://xxx.ngrok.io`を指定します。

ここまでの手順で以下のようなLIFFアプリが作れているかと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314315/4f14e82d-227e-e434-6678-c9747a44d373.png)

`LIFF ID` はアプリ開発時に必要になります。ユーザーは `LIFF URL` にアクセスすることでこのLIFFアプリを開くことが出来ます。


# Vue.jsのプロジェクトを作成する

## プロジェクトの作成

本記事では`Vue CLI`を使用してプロジェクトを作成します。
（プロジェクト名は適宜置き換えてください）

```console
vue create liff-app
```

プロジェクトの設定は任意のもので構いませんが、本記事では以下のような設定で作成しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314315/543b07f6-5618-1020-d7d3-721ebdfc9b53.png)


## ngrokからアクセスできるようにする

以下の設定を`package.json`に追加します。

```json:package.json
  "vue": {
    "devServer": {
      "disableHostCheck": true
    }
  }
```

## 動作確認

以下のコマンドでVue.jsアプリを起動し、`https://xxx.ngrok.io`からアクセスできることを確認します。

```console
npm run serve
```

ngrok経由でVue.jsアプリを開ければ、LIFFアプリとして実行することが出来ます。
前項の手順で作成したLIFF URL、`https://liff.line.me/xxx`にアクセスすると以下のように表示されると思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314315/2034b460-e4e9-27ab-964e-4074041a2391.png)


# LIFFアプリを開発する

## LIFF SDKを組み込む

LINE SDKはCDN経由でのみ配布されています。以下のscriptタグを`index.html`に追加します。

```html:index.html
<script charset="utf-8" src="https://static.line-scdn.net/liff/edge/2/sdk.js"></script>
```

この状態でLIFF SDKは使用可能な状態となりますが、せっかくTypeScriptで使用するので型の情報が欲しくなります。
幸運なことに[liff-type](https://www.npmjs.com/package/liff-type)という素晴らしいパッケージが公開されているのでこちらを使用します。

ドキュメントにある通り以下のコマンドでインストールし、

```
npm i -D liff-type
```

`tsconfig.json`ファイルに以下の内容を追記します。
※追記内容は環境に応じて適宜読み替えてください

```json:tsconfig.json
{
  "compilerOptions": {
    "types": ["liff-type"]
  }
}
```

正しく設定が行えればインテリセンスが利いた快適なLIFFアプリ開発環境が作れると思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314315/aebc779e-8f46-152c-ee05-8cb51d7a508e.png)


## LIFFアプリを初期化する

LIFF SDKの機能を使用するためには、はじめに`liff.init()`を実行する必要があります。
作法として正しいのかは分かりませんが、今はアプリ全体でliffの機能を使用したいため`App.vue`の`created`のタイミングで実行する処理を入れてみます。

なお、`liff.init`の実行にはLIFF IDが必要となります。


```typescript:App.vue
@Component
export default class App extends Vue {
  @Prop({ type: Boolean, default: false })
  loggedIn = false;

  created() {
    liff.init({
      liffId: 'ここにLIFF IDを入れる'
    })
    .then(() => {
      this.loggedIn = liff.isLoggedIn();
    })
  }
}
```


## LIFFアプリが動作している環境を取得する

```vuejs
<template>
  <div class="liff-data">
    <table>
      <tr>
        <th>OS</th>
        <td>{{ os }}</td>
      </tr>
      <tr>
        <th>Language</th>
        <td>{{ language }}</td>
      </tr>
      <tr>
        <th>LIFF SDK Version</th>
        <td>{{ sdkVersion }}</td>
      </tr>
      <tr>
        <th>LINE Version</th>
        <td>{{ lineVersion }}</td>
      </tr>
      <tr>
        <th>isInClient</th>
        <td>{{ isInClient }}</td>
      </tr>
      <tr>
        <th>isLoggedIn</th>
        <td>{{ isLoggedIn }}</td>
      </tr>
    </table>
  </div>
</template>

<script>
import { Component, Vue } from "vue-property-decorator";

@Component
export default class LiffData extends Vue {
  get os() {
    return liff.getOS();
  }

  get language() {
    return liff.getLanguage();
  }

  get lineVersion() {
    return liff.getLineVersion();
  }

  get sdkVersion() {
    return liff.getVersion();
  }

  get isInClient() {
    return liff.isInClient();
  }

  get isLoggedIn() {
    return liff.isLoggedIn();
  }
}
</script>
```


## ユーザのプロフィールを取得する

```vuejs
<template>
  <div class="profile">
    <table>
      <tr>
        <th>IDトークンの生成URL</th>
        <td>{{ token.iss }}</td>
      </tr>
      <tr>
        <th>ユーザーID</th>
        <td>{{ token.sub }}</td>
      </tr>
      <tr>
        <th>チャネルID</th>
        <td>{{ token.aud }}</td>
      </tr>
      <tr>
        <th>トークンの有効期限</th>
        <td>{{ token.exp }}</td>
      </tr>
      <tr>
        <th>IDトークンの生成時間</th>
        <td>{{ token.iat }}</td>
      </tr>
      <tr>
        <th>ユーザー認証時間</th>
        <td>{{ token.auth_time }}</td>
      </tr>
      <tr>
        <th>nonce</th>
        <td>{{ token.nonce }}</td>
      </tr>
      <tr>
        <th>認証方法</th>
        <td>{{ token.amr }}</td>
      </tr>
      <tr>
        <th>表示名</th>
        <td>{{ token.name }}</td>
      </tr>
      <tr>
        <th>プロフィールの画像URL</th>
        <td>{{ token.picture }}</td>
      </tr>
      <tr>
        <th>メールアドレス</th>
        <td>{{ token.email }}</td>
      </tr>
    </table>
  </div>
</template>

<script>
import { Component, Vue } from "vue-property-decorator";

@Component
export default class Profile extends Vue {
  get token() {
    return liff.getDecodedIDToken();
  }
}
</script>
```

## QRコードリーダを表示する

2020/07/01現在、QRコードリーダーの表示はiOS版LINEバージョン9.19.0以降では動作しないため、関数が存在するかを確認してから使用する必要があります。

参考：https://developers.line.biz/ja/docs/liff/developing-liff-apps/#opening-qr-code-reader

```vuejs
<template>
  <div class="qr">
    {{ scanText }}
    <button v-on:click="qrScan">
      <span>QRコード読み取り</span>
    </button>
  </div>
</template>

<script>
import { Component, Vue } from "vue-property-decorator";

@Component
export default class QR extends Vue {
  scanText = "";

  qrScan() {
    if (liff.scanCode) {
      liff.scanCode().then(result => {
        const stringifiedResult = JSON.stringify(result);
        this.scanText = stringifiedResult;
      });
    }
  }
}
</script>
```

## メッセージを送信する

2020/07/01現在、LIFFアプリからメッセージを送信する方法は現在のトークに送信する方法とターゲットピッカーを表示して、ユーザが選んだ相手に送信する方法の2種類存在します。
後者のターゲットを選択する方法はLINE 10.3.0以降でサポートされる機能のため、動作環境で使用可能かを確認してから送信します。

```vuejs
<template>
  <div class="message">
    <input v-model="message" />
    <button v-on:click="sendMessage">
      <span>送信</span>
    </button>
  </div>
</template>

<script>
import { Component, Vue } from "vue-property-decorator";

@Component
export default class Message extends Vue {

  message = 'LIFFへようこそ！';

  sendMessage() {
    liff.sendMessages([{
      'type': 'text',
      'text': this.message
    }]).then(function() {
      window.alert('Message sent');
    }).catch(function(error) {
      window.alert('Error sending message: ' + error);
    });
  }
}
</script>
```