はじめに
この記事は、ミロゴス Advent Calendar 2023 14日目の投稿です。
本記事では、LIFF開発をするにあたって必要な内容を紹介できればと思います。
LIFFとは
LINE Front-end Framework(LIFF)は、LINEヤフー株式会社が提供するウェブアプリのプラットフォームです。
このプラットフォームで動作するウェブアプリを、LIFFアプリと呼びます。
LIFFアプリを使うと、LINEのユーザーIDなどをLINEプラットフォームから取得できます。
LIFFアプリではこれらを利用して、ユーザー情報を活用した機能を提供したり、ユーザーの代わりにメッセージを送信したりできます。
内容
環境構築
この記事では、liffの他に下記二つのライブラリを使用します。
- nextjs
- chakraui
環境構築をこちらの記事で紹介しているので、ご参照下さい。
パッケージの追加
- LIFF SDKのインストール
npm install --save @line/liff
サーバーの準備
LIFFを始めるにあたって、サーバーを用意する必要があります。
そのため、今回はngorkというローカルサーバーを全世界に公開できる便利なツールを使用します。
ngrokの登録
- ngrokのサインアップ
- ngrokのインストール
brew install ngrok/ngrok/ngrok
- 認証トークンをデフォルトのngrok.yml 構成ファイルへ追加
ngrok config add-authtoken xxxxxxxxxx
- 設定ファイルの内容を表示
cat ~/Library/Application\ Support/ngrok/ngrok.yml
Webサーバーの公開
ローカルのポート3000で実行されているウェブサーバーを外部に公開します。
ngrok http 3000
ngrok (Ctrl+C to quit)
Build better APIs with ngrok. Early access: ngrok.com/early-access
Session Status online
Account xxx@xxx.com (Plan: Free)
Update update available (version 3.5.0, Ctrl-U to update)
Version 3.3.4
Region Japan (jp)
Latency 7ms
Web Interface http://127.0.0.1:4040
Forwarding https://xxxxxxxxxx.ngrok-free.app -> http://localhost:3000
Connections ttl opn rt1 rt5 p50 p90
329 1 0.00 0.00 0.04 5.78
HTTP Requests
-------------
LIFFの作成
LIFFを始めるための必要な準備をします。
チャネルを作成
LINE Developersコンソールへログイン
必要であれば、新規アカウント作成をします。
新規チャネルの作成
- 「プロバイダー」 > 「チャネル」から「新規チャネルの作成」を押下します。
- LINEログインを選択します。
- チャネル内のLIFFに関する設定を入力します。
- 必須項目を選択・入力し、LINE開発規約に同意チェックを入れます。
- アプリタイプについては、ウェブアプリを選択します。
- 必須項目を選択・入力し、LINE開発規約に同意チェックを入れます。
LIFFの追加
作成したチャネルにLIFFアプリを追加します。
- 作成したチャネルからLIFFタブを選択し、追加ボタンを押下します。
- 必須の項目を入力します。
- エンドポイントURLには、ngrokで作成したURLを入力します。
- 項目の詳しい解説につきましては、以下の公式ガイドで確認ください。
LIFFの使い方
全体構成
今回、使用する全体の構成になります。
ソースコードにつきましては、以下の記事で作成したものを少し改変したものとなります。
ソースコード一覧
ディレクトリ構成
.
├── README.md
├── components
│ └── Index
│ ├── MainContent.tsx
│ └── constants.ts
├── constants
│ └── index.ts
├── hooks
│ └── useLiffInitialization.ts
├── next-env.d.ts
├── node_modules
├── package-lock.json
├── package.json
├── pages
│ ├── _app.tsx
│ └── index.tsx
├── public
│ └── images
│ ├── image_1.png
│ ├── image_2.png
│ └── image_3.png
├── tsconfig.json
├── types
│ └── index.ts
├── utils
│ └── sample-data.ts
└── .env
ソースコード
export const TITLE_FIRST = 'タイトル1';
export const TITLE_SECOND = 'タイトル2';
export const TITLE_THIRD = 'タイトル3';
export const IMAGE_PATH_FIRST = '/images/image_1.png';
export const IMAGE_PATH_SECOND = '/images/image_2.png';
export const IMAGE_PATH_THIRD = '/images/image_3.png';
export const IMAGE_ID_VALIDATION_MESSAGE = '画像を選択してください。';
export const MESSAGE_FIELD_VALIDATION_MESSAGE =
'メッセージを入力してください。';
export const MESSAGE_FIELD_MAX_LENGTH = 100;
export const MESSAGE_FIELD_MAX_LENGTH_VALIDATION_MESSAGE =
'メッセージは100文字以内で入力してください。';
export const SUBMIT_BUTTON_TEXT = '送信';
import { Liff } from '@/types';
import { CheckIcon } from "@chakra-ui/icons";
import {
Box,
Button,
Flex,
FormControl,
FormErrorMessage,
Heading,
Image,
Text,
Textarea,
VStack,
} from "@chakra-ui/react";
import React, { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import * as FormConstants from "./constants";
export interface MainContentProps {
liffObject: Liff;
}
const MainContent = (props: MainContentProps) => {
const { liffObject } = props;
const images = [
{
id: 1,
title: FormConstants.TITLE_FIRST,
src: FormConstants.IMAGE_PATH_FIRST,
},
{
id: 2,
title: FormConstants.TITLE_SECOND,
src: FormConstants.IMAGE_PATH_SECOND,
},
{
id: 3,
title: FormConstants.TITLE_THIRD,
src: FormConstants.IMAGE_PATH_THIRD,
},
];
const [selectedImageId, setSelectedImageId] = useState(null as number | null);
console.log("選択した画像ID", selectedImageId);
const {
register,
handleSubmit,
control,
setValue,
formState: { errors },
} = useForm();
useEffect(() => {
setValue("imageId", selectedImageId);
}, [selectedImageId, setValue]);
const onSubmit = (data) => {
const imageId = data.imageId;
const message = data.message;
console.log("取得した画像ID", imageId);
console.log("取得したメッセージ", message);
};
return (
<>
<Flex alignItems="center" justifyContent="center">
<VStack spacing="4" align="start" padding="0">
<VStack align="start">
<Heading size="lg">画像の選択</Heading>
<Heading fontSize="14px">
画像をクリックすると対象の画像IDを取得できます。
</Heading>
</VStack>
<FormControl isInvalid={!!errors.imageId}>
<Controller
name="imageId"
control={control}
defaultValue=""
rules={{
required: FormConstants.IMAGE_ID_VALIDATION_MESSAGE,
}}
render={({ field }) => (
<input
type="hidden"
value={field.value || ""}
onChange={field.onChange}
/>
)}
/>
{typeof errors.imageId?.message === "string" && (
<FormErrorMessage>{errors.imageId.message}</FormErrorMessage>
)}
</FormControl>
{images.map((image, index) => (
<React.Fragment key={index}>
<Text>{image.title}</Text>
<Box
position="relative"
key={image.id}
borderRadius="md"
onClick={() => {
setSelectedImageId(image.id);
}}
marginBottom={index === images.length - 1 ? "30px" : "0"}
>
<Image
src={image.src}
alt={`Image ${image.id}`}
width="100%"
opacity={selectedImageId === image.id ? 1 : 0.6}
/>{" "}
{selectedImageId === image.id && (
<CheckIcon
position="absolute"
top="5px"
right="5px"
w={6}
h={6}
color="green.400"
/>
)}{" "}
</Box>
</React.Fragment>
))}
</VStack>
</Flex>
<Flex alignItems="center" justifyContent="center">
<form onSubmit={handleSubmit(onSubmit)}>
<FormControl isInvalid={!!errors.message} width="500px">
{typeof errors.message?.message === "string" && (
<FormErrorMessage>{errors.message.message}</FormErrorMessage>
)}
<VStack align="start" marginBottom="20px">
<Heading size="lg">メッセージの入力</Heading>
<Heading fontSize="14px">入力された内容を取得します。</Heading>
</VStack>
<Textarea
{...register("message", {
required: FormConstants.MESSAGE_FIELD_VALIDATION_MESSAGE,
maxLength: {
value: FormConstants.MESSAGE_FIELD_MAX_LENGTH,
message:
FormConstants.MESSAGE_FIELD_MAX_LENGTH_VALIDATION_MESSAGE,
},
})}
height="200px"
border="2px solid black"
marginBottom="20px"
/>
<Button
type="submit"
width="100%"
backgroundColor="black"
color="white"
>
{FormConstants.SUBMIT_BUTTON_TEXT}
</Button>
</FormControl>
</form>
</Flex>
</>
);
};
export default MainContent;
import { Liff } from '@/types/index';
import { useEffect, useState } from 'react';
// LIFFの初期化に必要なプロパティを定義した型
export interface LiffInitializationProps {
// LINE Developers Consoleで取得したアプリのLIFF ID
liffId: string;
}
// LIFFの初期化結果を表す型
export interface LiffInitialization {
liffObject: Liff | null;
isLiffError: boolean;
}
const useLiffInitialization = (
props: LiffInitializationProps
): LiffInitialization => {
const { liffId } = props;
const [liffObject, setLiffObject] = useState<Liff | null>(null);
const [isLiffError, setIsLiffError] = useState<boolean>(false);
// useEffectを使って初回レンダリング時やliffIdが変更されたときに実行される処理
useEffect(() => {
const initializeLiff = async () => {
try {
const liffModule = await import('@line/liff').then(
(module) => module.default
);
console.log('LIFF init...');
await liffModule.init({ liffId: liffId }); // LIFFの初期化を行う
console.log('LIFF init success.');
// ログインしていない場合、ログイン処理を実行
if (!liffModule.isLoggedIn()) {
console.log('User not yet logged in.');
liffModule.login();
}
await liffModule.ready; // LIFFの準備完了を待つ
console.log('LIFF ready.');
setLiffObject(liffModule); // LIFFオブジェクトをReactステートにセット
} catch (error) {
console.log('LIFF init failed.');
setIsLiffError(true);
}
};
initializeLiff(); // 上記の初期化処理を実行
}, [liffId]); // liffIdが変更されたときに再実行するように指定
// レンダリング結果としてLIFFオブジェクトとエラーフラグを返す
return { liffObject, isLiffError };
};
export default useLiffInitialization;
export const LIFF_ID = process.env.NEXT_PUBLIC_LIFF_ID || '';
import MainContent from '@/components/Index/MainContent';
import { LIFF_ID } from '@/constants/index';
import useLiffInitialization from '@/hooks/useLiffInitialization';
import { Box, Flex, Text } from '@chakra-ui/react';
const IndexPage = () => {
const liffId: string = LIFF_ID;
const { liffObject, isLiffError } = useLiffInitialization({ liffId }); // LIFFの初期化を行うカスタムフックから結果を受け取る
// もし初期化中にエラーが発生した場合、エラーメッセージを表示
if (isLiffError) {
return (
<Flex align="center" justify="center" height="100vh">
<Box>
<Text>エラーが発生しました</Text>
</Box>
</Flex>
);
}
// もしLIFFオブジェクトがまだ取得できていない場合、何も表示せずに終了
if (!liffObject) {
return null;
}
return <MainContent liffObject={liffObject} />;
};
export default IndexPage;
import liff from '@line/liff';
export type Liff = typeof liff;
NEXT_PUBLIC_LIFF_ID=(作成したLIFF ID)
LIFF IDのセット
作成したチャネルのLIFF IDを呼び出せるように設定します。
- env ファイルへLIFF_IDを追加
NEXT_PUBLIC_LIFF_ID=(作成したLIFF ID)
.env ファイル内で NEXT_PUBLIC_LIFF_ID という環境変数に LIFF ID を設定しています。
この設定は、環境変数としてアプリケーション内で使えるようになります。
- NEXT_PUBLIC_LIFF_ID を LIFF_ID として出力
export const LIFF_ID = process.env.NEXT_PUBLIC_LIFF_ID || '';
環境変数 NEXT_PUBLIC_LIFF_ID を LIFF_ID としてエクスポートしています。
デフォルト値として空の文字列を使用しています。
LIFFの初期化についてのReactフック
このReactフックは、LIFFの初期化を行うための処理を提供します。
以下は、このフックが行っている主な処理です。
useLiffInitialization.ts
- 初期化プロパティの型定義
export interface LiffInitializationProps {
liffId: string; // LINE Developers Consoleで取得したアプリのLIFF ID
}
フックのプロパティとして、LINE Developers Consoleで取得したアプリのLIFF ID が必要です。
- 初期化結果の型定義
export interface LiffInitialization {
liffObject: Liff | null; // 初期化されたLIFFオブジェクト
isLiffError: boolean; // 初期化中にエラーが発生したかどうかを示すフラグ
}
フックはLIFFの初期化結果を提供し、初期化されたLIFFオブジェクトとエラーフラグが含まれます。
- Reactフックの実装
const useLiffInitialization = (
props: LiffInitializationProps
): LiffInitialization => {
// ...(省略)
useEffect(() => {
const initializeLiff = async () => {
try {
// LIFFモジュールの動的インポート
const liffModule = await import('@line/liff').then(
(module) => module.default
);
// LIFFの初期化
await liffModule.init({ liffId: liffId });
// ログインしていない場合、ログイン処理を実行
if (!liffModule.isLoggedIn()) {
liffModule.login();
}
// LIFFの準備完了を待つ
await liffModule.ready;
// LIFFオブジェクトをReactステートにセット
setLiffObject(liffModule);
} catch (error) {
// 初期化中にエラーが発生した場合
setIsLiffError(true);
}
};
initializeLiff(); // 初期化処理を実行
}, [liffId]); // liffIdが変更されたときに再実行するように指定
// レンダリング結果としてLIFFオブジェクトとエラーフラグを返す
return { liffObject, isLiffError };
};
フックはuseEffectを使用して、初回レンダリング時やLIFF IDが変更されたときに LIFF を初期化します。
初期化が成功したら準備完了まで待ち、LIFFオブジェクトをReactステートにセットします。
エラーが発生した場合は、エラーフラグがセットされます。
このフックを使用することで、LIFFを簡単かつ効果的にReactアプリケーションに統合できます。
LIFFオブジェクトのセットとメインページの表示について
LIFF(LINE Frontend Framework)の初期化が成功した後に、liffObjectを使用してメインコンテンツを表示するReactページを示しています。
- LIFF IDの取得
const liffId: string = LIFF_ID;
LINE Developers Consoleで取得したアプリのLIFF ID が LIFF_ID として定義され、それを liffId に代入しています。
- LIFFの初期化
const { liffObject, isLiffError } = useLiffInitialization({ liffId });
カスタムフック useLiffInitialization を使用して、LIFFの初期化を行います。
liffObject は初期化されたLIFFオブジェクトで、isLiffError は初期化中にエラーが発生したかどうかを示すフラグです。
- エラーの表示
if (isLiffError) {
// エラーメッセージを表示
return (
<Flex align="center" justify="center" height="100vh">
<Box>
<Text>エラーが発生しました</Text>
</Box>
</Flex>
);
}
もし初期化中にエラーが発生した場合、エラーメッセージを表示します。
- LIFFオブジェクトが取得できない場合の処理
if (!liffObject) {
// LIFFオブジェクトがまだ取得できていない場合は何も表示せずに終了
return null;
}
もし LIFFオブジェクトがまだ取得できていない場合は、何も表示せずに終了します。
- メインコンテンツの表示
return <MainContent liffObject={liffObject} />;
LIFFオブジェクトが取得できた場合は、MainContent コンポーネントに liffObject を渡してメインのコンテンツを表示します。
画面表示
以下の画面が正常に表示されればOKです。
さいごに
LIFFは、LINEプラットフォームを活用した柔軟なアプリケーションの開発を可能にするツールです。
このツールを通じて、開発者はLINEユーザーとの対話を強化し、新しい機能やサービスを提供することができます。
機能面については数多くあるため、別の機会で紹介できればと思います。