使用 Next.js 15、React 19 與 Vercel 打造高效能 LINE LIFF 應用程式


本篇教學將引導您如何結合最新的 Next.js 15、React 19 技術,並透過 Vercel 平台,一步步開發與部署功能完善、效能卓越的 LINE LIFF 應用程式。
前言
隨著 LINE 在台灣的普及率持續攀升,LINE Front-end Framework (LIFF) 成為開發者打造與 LINE 深度整合的網頁應用程式的利器。在 2025 年的今天,結合最新的 Next.js 15、React 19、以及 Vercel 部署平台,我們可以用前所未有的效率和性能打造出色的 LIFF 應用程式。
本文將帶您一步步使用最新版本的工具,從零開始建立一個現代化的 LINE LIFF 應用程式。
技術堆疊概覽
在開始之前,讓我們先了解本次教學將使用的技術堆疊:
- @line/create-liff-app (v1.1.5):LINE 官方提供的 LIFF 應用程式腳手架工具
- @line/liff (v2.26.1):最新版 LIFF SDK
- Next.js 15:支援 React 19 的全端 React 框架
- React 19:帶來 Server Components 和新鉤子的最新版本
- App Router:Next.js 的新一代路由系統
- TypeScript:提供型別安全的開發體驗
- Vercel:專為 Next.js 優化的部署平台
建立開發環境
1. 使用 Create LIFF App 快速開始
首先,我們使用 LINE 官方提供的 @line/create-liff-app
來建立專案:
npx @line/create-liff-app@latest my-liff-app
執行後會出現一系列問題,以下是推薦的選擇:
? Which template do you want to use? › nextjs
? JavaScript or TypeScript? › TypeScript
? Please enter your LIFF ID: › [輸入您的 LIFF ID,稍後可修改]
? Which package manager do you want to use? › npm
2. 更新 next.config.ts 檔
Next.js 設定檔範例(next.config.ts)
以下是專案最佳化後的 next.config.ts
設定,涵蓋圖片優化、多語系、路由導向與轉發,以及實驗性功能等:
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
reactStrictMode: true,
// 圖片優化設定
images: {
domains: ["images.unsplash.com", "cdn.example.com"],
formats: ["image/webp", "image/avif"],
},
// 自動導向設定
async redirects() {
return [
{
source: "/old-path",
destination: "/new-path",
permanent: true,
},
];
},
// 自訂路由轉發
async rewrites() {
return [
{
source: "/api/:slug*",
destination: "https://api.example.com/:slug*",
},
];
},
// 啟用實驗性功能
experimental: {
// 為認證錯誤提供更好的支援
authInterrupts: true,
},
};
export default nextConfig;
設定 LIFF 環境
1. 在 LINE Developers 建立 LIFF 應用程式
注意:必須使用 LINE Business(企業)帳號建立 Provider,個人帳號無法新增 LIFF ID。
- 登入 LINE Developers Console
- 建立新的 Provider(如果還沒有的話,且必須為企業帳號)
- 建立 LINE Login Channel
- 在 LIFF 標籤中點擊「Add」
- 設定以下資訊:
- LIFF app name:您的應用程式名稱
- Size:選擇 Full(全螢幕)
- Endpoint URL:暫時填入
https://example.com
(稍後會更新) - Scope:勾選需要的權限(建議至少選擇 profile)
2. 更新環境變數
在專案根目錄建立 .env.local
檔案:
NEXT_PUBLIC_LIFF_ID=您的_LIFF_ID
實作 LIFF Provider(App Router 版本)
1. 建立 LIFF Provider 元件
建立 app/providers/LiffProvider.tsx
:
'use client';
import React, { createContext, useContext, useEffect, useState, useCallback } from 'react';
import { Liff } from '@line/liff';
interface LiffContextType {
liff: Liff | null;
liffError: string | null;
isLoggedIn: boolean;
isInClient: boolean;
isReady: boolean;
}
const LiffContext = createContext<LiffContextType>({
liff: null,
liffError: null,
isLoggedIn: false,
isInClient: false,
isReady: false,
});
export const useLiff = () => useContext(LiffContext);
interface LiffProviderProps {
children: React.ReactNode;
liffId: string;
}
export const LiffProvider: React.FC<LiffProviderProps> = ({ children, liffId }) => {
const [liff, setLiff] = useState<Liff | null>(null);
const [liffError, setLiffError] = useState<string | null>(null);
const [isReady, setIsReady] = useState(false);
const initLiff = useCallback(async () => {
try {
const liffModule = await import('@line/liff');
const liffInstance = liffModule.default;
console.log('初始化 LIFF...');
await liffInstance.init({ liffId });
console.log('LIFF 初始化成功');
setLiff(liffInstance);
setIsReady(true);
} catch (error) {
console.error('LIFF 初始化失敗:', error);
setLiffError((error as Error).toString());
setIsReady(true);
}
}, [liffId]);
useEffect(() => {
if (typeof window !== 'undefined') {
initLiff();
}
}, [initLiff]);
const value = {
liff,
liffError,
isLoggedIn: liff?.isLoggedIn() || false,
isInClient: liff?.isInClient() || false,
isReady,
};
return <LiffContext.Provider value={value}>{children}</LiffContext.Provider>;
};
2. 更新 Root Layout
更新 app/layout.tsx
:
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
import { LiffProvider } from './providers/LiffProvider';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: 'My LIFF App',
description: '使用 Next.js 15 和 React 19 打造的 LIFF 應用程式',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="zh-TW">
<body className={inter.className}>
<LiffProvider liffId={process.env.NEXT_PUBLIC_LIFF_ID || ''}>
{children}
</LiffProvider>
</body>
</html>
);
}
建立 LIFF 功能元件
1. 使用者資料元件
建立 app/components/UserProfile.tsx
:
'use client';
import { useState, useEffect } from 'react';
import { useLiff } from '../providers/LiffProvider';
import { Profile } from '@liff/get-profile';
export function UserProfile() {
const { liff, isLoggedIn, isReady } = useLiff();
const [profile, setProfile] = useState<Profile | null>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (isReady && isLoggedIn && liff) {
fetchProfile();
}
}, [isReady, isLoggedIn, liff]);
const fetchProfile = async () => {
if (!liff) return;
setLoading(true);
try {
const userProfile = await liff.getProfile();
setProfile(userProfile);
} catch (error) {
console.error('無法取得使用者資料:', error);
} finally {
setLoading(false);
}
};
const handleLogin = () => {
if (liff) {
liff.login();
}
};
const handleLogout = () => {
if (liff) {
liff.logout();
window.location.reload();
}
};
if (!isReady) {
return <div>載入中...</div>;
}
if (!isLoggedIn) {
return (
<div className="text-center p-8">
<p className="mb-4">請先登入 LINE</p>
<button
onClick={handleLogin}
className="bg-green-500 text-white px-6 py-2 rounded-md hover:bg-green-600 transition"
>
使用 LINE 登入
</button>
</div>
);
}
if (loading) {
return <div>載入使用者資料...</div>;
}
return (
<div className="text-center p-8">
{profile && (
<div className="mb-6">
<img
src={profile.pictureUrl}
alt="Profile"
className="w-24 h-24 rounded-full mx-auto mb-4"
/>
<h2 className="text-xl font-bold">{profile.displayName}</h2>
<p className="text-gray-600 text-sm">{profile.userId}</p>
{profile.statusMessage && (
<p className="text-gray-500 mt-2">{profile.statusMessage}</p>
)}
</div>
)}
<button
onClick={handleLogout}
className="bg-red-500 text-white px-6 py-2 rounded-md hover:bg-red-600 transition"
>
登出
</button>
</div>
);
}
2. LIFF 功能展示元件
建立 app/components/LiffFeatures.tsx
:
'use client';
import { useState } from 'react';
import { useLiff } from '../providers/LiffProvider';
export function LiffFeatures() {
const { liff, isInClient } = useLiff();
const [scanResult, setScanResult] = useState<string>('');
const sendMessage = async () => {
if (!liff || !liff.isInClient()) {
alert('此功能只能在 LINE 應用程式內使用');
return;
}
try {
await liff.sendMessages([
{
type: 'text',
text: '這是從 LIFF 應用程式發送的訊息!',
},
]);
alert('訊息已發送');
} catch (error) {
console.error('發送訊息失敗:', error);
alert('發送訊息失敗');
}
};
const scanCode = async () => {
if (!liff || !liff.isInClient()) {
alert('此功能只能在 LINE 應用程式內使用');
return;
}
try {
const result = await liff.scanCodeV2();
setScanResult(result.value || '無法讀取');
} catch (error) {
console.error('掃描失敗:', error);
alert('掃描失敗');
}
};
const shareMessage = async () => {
if (!liff) return;
try {
await liff.shareTargetPicker([
{
type: 'text',
text: '來自 LIFF 應用程式的分享訊息!',
},
]);
} catch (error) {
console.error('分享失敗:', error);
alert('分享失敗');
}
};
return (
<div className="p-8">
<h2 className="text-2xl font-bold mb-6">LIFF 功能展示</h2>
<div className="space-y-4">
<div>
<button
onClick={sendMessage}
disabled={!isInClient}
className="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 transition disabled:bg-gray-400"
>
發送訊息到聊天室
</button>
{!isInClient && (
<p className="text-sm text-gray-500 mt-1">僅限 LINE 應用程式內使用</p>
)}
</div>
<div>
<button
onClick={scanCode}
disabled={!isInClient}
className="bg-purple-500 text-white px-4 py-2 rounded-md hover:bg-purple-600 transition disabled:bg-gray-400"
>
掃描 QR Code
</button>
{scanResult && (
<p className="mt-2 p-2 bg-gray-100 rounded">掃描結果:{scanResult}</p>
)}
</div>
<div>
<button
onClick={shareMessage}
className="bg-green-500 text-white px-4 py-2 rounded-md hover:bg-green-600 transition"
>
分享訊息
</button>
</div>
</div>
</div>
);
}
3. 主頁面整合
更新 app/page.tsx
:
import { UserProfile } from './components/UserProfile';
import { LiffFeatures } from './components/LiffFeatures';
export default function Home() {
return (
<main className="min-h-screen bg-gray-50">
<div className="max-w-4xl mx-auto py-8">
<h1 className="text-3xl font-bold text-center mb-8">
My LIFF App
</h1>
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
<UserProfile />
</div>
<div className="bg-white rounded-lg shadow-md p-6">
<LiffFeatures />
</div>
</div>
</main>
);
}
利用 React 19 新功能
1. 使用 Server Components 優化效能
建立 app/components/ServerInfo.tsx
:
// 這是一個 Server Component(預設)
async function getServerData() {
// 模擬從伺服器取得資料
const data = {
serverTime: new Date().toISOString(),
version: '1.0.0',
};
return data;
}
export default async function ServerInfo() {
const data = await getServerData();
return (
<div className="bg-blue-50 p-4 rounded-md">
<h3 className="font-semibold mb-2">伺服器資訊</h3>
<p className="text-sm">時間:{data.serverTime}</p>
<p className="text-sm">版本:{data.version}</p>
</div>
);
}
2. 使用 useActionState 處理表單
建立 app/components/ContactForm.tsx
:
'use client';
import { useActionState } from 'react';
import { sendMessage } from '../actions/contact';
const initialState = {
message: '',
error: '',
};
export function ContactForm() {
const [state, formAction, isPending] = useActionState(
sendMessage,
initialState
);
return (
<form action={formAction} className="space-y-4">
<div>
<label htmlFor="name" className="block text-sm font-medium">
姓名
</label>
<input
type="text"
id="name"
name="name"
required
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
/>
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium">
訊息
</label>
<textarea
id="message"
name="message"
required
rows={4}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
/>
</div>
<button
type="submit"
disabled={isPending}
className="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700 disabled:bg-gray-400"
>
{isPending ? '傳送中...' : '傳送訊息'}
</button>
{state.error && (
<p className="text-red-600 text-sm">{state.error}</p>
)}
{state.message && (
<p className="text-green-600 text-sm">{state.message}</p>
)}
</form>
);
}
建立對應的 Server Action app/actions/contact.ts
:
'use server';
export async function sendMessage(prevState: any, formData: FormData) {
const name = formData.get('name') as string;
const message = formData.get('message') as string;
// 這裡可以整合 LINE Notify 或其他通知服務
console.log('收到聯絡訊息:', { name, message });
// 模擬 API 呼叫
await new Promise(resolve => setTimeout(resolve, 1000));
return {
message: '訊息已成功送出!',
error: '',
};
}
部署到 Vercel
1. 準備部署
確保您的專案已經推送到 GitHub:
git init
git add .
git commit -m "Initial commit"
git branch -M main
git remote add origin https://github.com/您的帳號/my-liff-app.git
git push -u origin main
2. 在 Vercel 部署
- 前往 Vercel 並使用 GitHub 帳號登入
- 點擊「New Project」
- 匯入您的 GitHub repository
- 設定環境變數:
- 新增
NEXT_PUBLIC_LIFF_ID
並填入您的 LIFF ID
- 新增
- 點擊「Deploy」
3. 更新 LIFF Endpoint URL
部署完成後,複製 Vercel 提供的網址(例如:https://my-liff-app.vercel.app
),回到 LINE Developers Console 更新 LIFF 的 Endpoint URL。
本地開發技巧
1. 使用 ngrok 進行本地測試
由於 LIFF 需要 HTTPS,我們可以使用 ngrok 來建立安全通道:
# 安裝 ngrok
npm install -g ngrok
# 啟動 Next.js 開發伺服器
npm run dev
# 在另一個終端機視窗執行
ngrok http 3000
將 ngrok 提供的 HTTPS 網址更新到 LIFF 的 Endpoint URL,即可在 LINE 應用程式內測試。
2. 偵錯模式
建立 app/components/DebugInfo.tsx
:
'use client';
import { useLiff } from '../providers/LiffProvider';
export function DebugInfo() {
const { liff, isInClient, isLoggedIn, liffError } = useLiff();
if (process.env.NODE_ENV === 'production') {
return null;
}
return (
<div className="fixed bottom-4 right-4 bg-black text-white p-4 rounded-lg text-xs max-w-xs">
<h4 className="font-bold mb-2">LIFF Debug</h4>
<p>環境: {isInClient ? 'LINE App' : 'External Browser'}</p>
<p>登入狀態: {isLoggedIn ? '已登入' : '未登入'}</p>
<p>LIFF 版本: {liff?.getVersion() || 'N/A'}</p>
<p>語言: {liff?.getLanguage() || 'N/A'}</p>
{liffError && <p className="text-red-400">錯誤: {liffError}</p>}
</div>
);
}
最佳實踐
1. 錯誤處理
建立統一的錯誤處理機制:
// app/utils/liff-error-handler.ts
export function handleLiffError(error: unknown): string {
if (error instanceof Error) {
// 根據錯誤類型提供友善的錯誤訊息
if (error.message.includes('INIT_FAILED')) {
return 'LIFF 初始化失敗,請檢查網路連線';
}
if (error.message.includes('UNAUTHORIZED')) {
return '權限不足,請重新登入';
}
return error.message;
}
return '發生未知錯誤';
}
2. 效能優化
利用 Next.js 15 的新功能:
// 使用 dynamic import 減少初始載入時間
const LiffFeatures = dynamic(
() => import('./components/LiffFeatures').then(mod => mod.LiffFeatures),
{
loading: () => <p>載入功能中...</p>,
ssr: false // LIFF 功能只在客戶端執行
}
);
Next.js 15 App Router 與 Server/Client Components 實戰
Next.js 15 推出的 App Router 架構,讓開發者可以更靈活地混用 Server Components 與 Client Components,達到效能與互動性的最佳平衡。
Server Component(預設): 只在伺服器執行,可直接存取資料庫、API,不會被送到瀏覽器端。
// app/types/liff.ts
import { Liff } from '@line/liff';
export interface LiffContextType {
liff: Liff | null;
liffError: string | null;
isLoggedIn: boolean;
isInClient: boolean;
isReady: boolean;
}
export interface LiffMessage {
type: 'text' | 'image' | 'video';
text?: string;
originalContentUrl?: string;
previewImageUrl?: string;
}
進階功能
1. 整合 LINE Pay
// app/components/LinePayButton.tsx
'use client';
import { useLiff } from '../providers/LiffProvider';
export function LinePayButton({ amount, orderId }: { amount: number; orderId: string }) {
const { liff, isInClient } = useLiff();
const handlePayment = async () => {
if (!liff || !isInClient) {
alert('請在 LINE 應用程式內使用 LINE Pay');
return;
}
// 呼叫後端 API 建立 LINE Pay 交易
const response = await fetch('/api/linepay/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount, orderId }),
});
const { paymentUrl } = await response.json();
// 導向 LINE Pay 付款頁面
liff.openWindow({
url: paymentUrl,
external: true,
});
};
return (
<button
onClick={handlePayment}
className="bg-green-600 text-white px-6 py-3 rounded-lg font-semibold"
>
使用 LINE Pay 付款
</button>
);
}
2. Rich Menu 整合
// app/hooks/useRichMenu.ts
import { useEffect } from 'react';
import { useLiff } from '../providers/LiffProvider';
export function useRichMenu(richMenuId: string) {
const { liff, isInClient } = useLiff();
useEffect(() => {
if (liff && isInClient && richMenuId) {
// 切換 Rich Menu
liff.switchRichMenu(richMenuId);
}
}, [liff, isInClient, richMenuId]);
}
結語
透過本教學,您已經學會如何使用最新的技術堆疊建立一個功能完整的 LINE LIFF 應用程式。結合 Next.js 15 的強大功能、React 19 的創新特性,以及 Vercel 的便利部署,開發 LIFF 應用程式變得前所未有的簡單和高效。
重點回顧
- 使用 @line/create-liff-app 快速建立專案基礎
- 升級到最新版本確保使用最新功能和效能優化
- App Router 整合充分利用 Server Components 的優勢
- React 19 新功能如 useActionState 簡化表單處理
- Vercel 部署實現零配置的全球部署
下一步
- 探索更多 LIFF API 功能,如藍牙連接、地理位置等
- 整合後端服務,建立完整的應用程式生態系
- 優化使用者體驗,加入更多互動功能
- 實作進階的安全機制,保護使用者資料
希望這篇教學對您有所幫助,祝您開發愉快!如有任何問題,歡迎在留言區討論。
參考資源
Thanks for reading!
Found this article helpful? Share it with others or explore more content.