✌️

liff.init は Vue の createApp より前で実行しよう

2021/10/13に公開

前提

Vue.js (3.x 系)で liff アプリを作成するとき、以下のように liff.init していました。

RootPage.vue
<template>
  <router-view />
</template>

<script lang="ts">
import liff from '@line/liff/dist/lib';
import { defineComponent, onMounted } from 'vue';
import { useRouter } from 'vue-router';

export default defineComponent({
  name: 'RootPage',
  setup() {
    const router = useRouter();

    onMounted(async () => {
      await liff.init({ liffId: 'xxxxxxxxx' });

      if (!liff.isLoggedIn()) {
        await router.push({
          name: 'page1',
        });

      } else {
        await router.push({
          name: 'page2',
        });
      }
    });
  },
});
</script>

通常の動作はこれで問題ありませんが、クエリパラメータを付与して liff アプリに遷移したときに問題が起こりました。

期待していた遷移の挙動

まず liff アプリに下記のようにクエリパラメータを付与してアクセスします。

https://liff.line.me/xxxxxxxxxxxxx?hoge=hoge&fuga=fuga

すると、liff サーバによってエンドポイントに指定していたアドレスにリダイレクトされます。この時、もともと付与されていたクエリパラメータは URL エンコードされた状態で liff.state というパラメータ内に保持されます。これは、liff アプリにおける1次リダイレクトです。(詳細は公式ドキュメントを参照してください。)

https://myliffapp.com/index.html?liff.state=%3Fhoge%3Dhoge%26fuga%3Dfuga

その後、Vue が起動し、前述した liff.init が呼ばれます。このとき、内部的にliff.stateパラメータを通常のクエリパラメータとして再解釈した URL にリダイレクトされます。これが、2次リダイレクトにあたります。

https://myliffapp.com/index.html?hoge=hoge&fuga=fuga

実際の挙動

実際にはliff.initの後、下記のような URL に遷移されてしまいました。

https://myliffapp.com/index.html?hoge

なぜか第1パラメータの hoge のキーだけが残る謎挙動。頭を抱えました。

仮説

VueRouter と liff.init の両方が URL を管理しようとして衝突しているのではないか、 という仮説を立てました。
ドキュメントにも下記のように記載があります。

liff.init()メソッドが返すPromiseオブジェクトがresolveする前に、サーバーやフロントエンド側の処理などでURLを変更しないようにしてください。

この仮説に基づき、VueRouter が動作し始める前にliff.init してみました。

結果

下記のように変更したところ、クエリパラメータが期待通りに取れるようになりました。

main.ts
import liff from '@line/liff/dist/lib';
import { createApp } from 'vue';

import App from './App.vue';
import router from './router';

async function main() {

  // VueRouter が動き始める前に liff.init する
  await liff.init({ liffId: 'xxxxxxxx' });

  const app = createApp(App);
  app.use(router);
  app.mount('#app');
}

main();
RootPage.vue
<template>
  <router-view />
</template>

<script lang="ts">
import liff from '@line/liff/dist/lib';
import { defineComponent, onMounted } from 'vue';
import { useRouter } from 'vue-router';

export default defineComponent({
  name: 'Base',
  setup() {
    const router = useRouter();

    onMounted(async () => {
      // ここでは liff.init しない!!
      // await liff.init({ liffId: 'xxxxxxxxx' });

      // この時点で正しく2次リダイレクトされ、クエリパラメータは正常に読み取れる。

      if (!liff.isLoggedIn()) {
        await router.push({
          name: 'page1',
        });

      } else {
        await router.push({
          name: 'page2',
        });
      }
    });
  },
});
</script>

まとめ

ネット上では onMounted 内で liff.init している例が散見されるので、同じ部分でハマっている方も多い気がします。react など他のフレームワークでも同様にパスの制御が衝突してしまう可能性があるので、liff.initのタイミングには注意したほうがよさそうです。

その他

Qiita でも同じ内容で投稿しています。

Discussion