📲 第十一章:LIFF SDK 初始化與 Line 隱形登入機制

在前面的章節,我們講完了純網頁的排版與部署。但在現代的商業應用中,最讓老闆與客戶頭痛的就是「使用者註冊轉化率」。 如果一個打卡系統,每次都要員工輸入「帳號、密碼」才能打卡,員工一定會抱怨連連,甚至每天忘記密碼。

為了解決這個問題,台灣市佔率最高的 Line 提供了一個終極核武器:LIFF (LINE Front-end Framework)。 只要把你的網站掛載在 Line 的聊天室裡面點開,Line 就會「偷偷把這個使用者的 Line ID 和大頭貼」傳送給你的網站。 使用者完全不需要輸入任何字,就能瞬間完成登入!

這堂課,我們不談理論,直接剖析 真實營運中的打卡系統 Source Code。 我們將教你如何用 Vibe Coding 的方式,指揮 AI 幫你寫出這個極度複雜、充滿坑洞的初始化與驗證流程!


🔑 實戰 1:在 React 中引入並初始化 LIFF SDK

LIFF 不是一個可以用 npm install 隨便裝一裝就沒事的套件。 因為它牽涉到跟 Line APP 內建瀏覽器的底層溝通,它的初始化時間(liff.init())是非同步的,而且如果初始化失敗,整個網頁都會崩潰。

💡 Vibe Prompt 實戰 1:打造安全的 LIFF 初始化 Hook

我們不能把初始化的邏輯隨便寫在某個按鈕裡面。我們需要一個全域的自訂 Hook (useLiff),讓整個應用程式隨時可以知道:「Line 準備好了嗎?」。

[!IMPORTANT] 請複製以下 Prompt 傳送給 AI:

我正在開發一個 Line LIFF 網頁應用程式 (React)。 請幫我寫一個自訂 Hook:useLiff()。 需求如下: 1. 使用 @line/liff 套件。 2. 狀態管理:isReady (預設 false)、liffError (預設 null)、profile (使用者的 Line 資料,預設 null)。 3. 在 useEffect 中執行 liff.init({ liffId: '你的LIFF_ID' })。 4. 如果初始化成功,檢查 liff.isLoggedIn()。如果已登入,使用 liff.getProfile() 取得使用者大頭貼與名稱,並存入 profile 狀態。 5. 如果未登入,呼叫 liff.login() 強制登入。 請給我完整的程式碼,並在 catch 區塊加上詳細的防呆錯誤處理。

🤖 AI 生成的企業級 LIFF 初始化 Hook:

import { useState, useEffect } from 'react';
import liff from '@line/liff';

export function useLiff() {
  const [isReady, setIsReady] = useState(false);
  const [liffError, setLiffError] = useState(null);
  const [profile, setProfile] = useState(null);

  useEffect(() => {
    // 定義一個非同步函數來執行初始化
    const initLiff = async () => {
      try {
        console.log("⏳ 開始初始化 LIFF SDK...");
        
        // 1. 初始化 SDK (請務必將 LIFF_ID 放在環境變數中)
        await liff.init({ liffId: import.meta.env.VITE_LIFF_ID });
        console.log("✅ LIFF 初始化成功!");
        
        // 2. 檢查使用者是否已經登入 Line
        // (如果在 Line APP 裡面打開,這裡通常預設就是 true)
        if (liff.isLoggedIn()) {
          console.log("✅ 使用者已登入,正在抓取個人資料...");
          const userProfile = await liff.getProfile();
          setProfile(userProfile);
          console.log("👋 歡迎回來:", userProfile.displayName);
        } else {
          // 3. 如果是用外部瀏覽器打開且未登入,強制跳轉到 Line 的登入頁面
          console.log("⚠️ 使用者未登入,準備重導向...");
          liff.login();
        }
        
        // 4. 全部搞定,標記為 Ready
        setIsReady(true);
        
      } catch (error) {
        console.error("❌ LIFF 初始化發生致命錯誤:", error);
        setLiffError(error.message);
      }
    };

    // 執行初始化
    initLiff();
  }, []); // 空陣列確保只在網頁打開時執行一次

  // 回傳所有狀態給外面的元件使用
  return { isReady, liffError, profile };
}

🔍 深度解析: 這是所有 Line 機器人開發者的夢魘,但 AI 幫我們完美處理了。 這裡面有一個極度重要的機制:liff.login()。 當你把這個網址丟給老闆,老闆如果用電腦的 Chrome 瀏覽器打開(而不是在 Line 裡面打開),liff.isLoggedIn() 就會是 false。 這時候這段程式碼會非常聰明地自動跳轉到 Line 的綠色登入畫面,要求老闆輸入帳號密碼。這樣就完美兼顧了「Line 內建瀏覽器」與「外部瀏覽器」的兩種情境!


🚫 終極避坑指南:LIFF 初始化卡死在迴圈中

這是一個價值十萬元的 Debug 經驗! 很多新手照著官方文件寫了 liff.login(),結果部署上去後發現:網頁在外部瀏覽器打開時,會陷入「無限登入迴圈」,一直閃爍重新載入!

為什麼?因為當你呼叫 liff.login() 時,Line 會把網頁跳轉到驗證中心,驗證完後再把網頁「重新載入」一次。 如果你的網址設定錯誤,Line 無法正確把網址加上驗證參數帶回來,你的網頁就會以為「我又沒登入了」,然後再次呼叫 liff.login()

💡 Vibe Prompt 實戰 2:讓 AI 幫你檢查 LIFF Callback 網址

[!IMPORTANT] 請複製以下 Prompt 傳送給 AI:

我的 React 網頁在外部瀏覽器呼叫 liff.login() 時,發生了無限重新載入的死迴圈。 請問這是不是因為 LIFF Console 後台的 "Endpoint URL" 設定有問題? 請告訴我,當我部署在 Vercel (網址是 https://my-punch.vercel.app/) 時,我在 Line Developers 後台的 Endpoint URL 應該填什麼? 另外,請幫我修改剛才的 liff.login() 語法,明確加上 redirectUri 參數以防止這個 Bug。

🤖 AI 的緊急煞車防爆措施:

這絕對是 Callback URL 沒有對齊造成的無限迴圈!

【Line Developers 後台設定檢查】:
你的 Endpoint URL 必須精準填寫:`https://my-punch.vercel.app/`
(⚠️ 注意:如果你是用 React Router 的 Hash 模式,後面帶有 /#/ 的話會引發大災難,Line 預設會把 # 後面的東西吃掉!)

【程式碼防爆修改】:
與其讓 liff 自己猜要跳回哪裡,我們直接強制指定 `redirectUri`:
// 修改前的危險寫法:
// liff.login();

// ✅ 修改後的最安全寫法:
liff.login({ 
  // 明確告訴 Line,驗證完後要把使用者丟回哪一個網址!
  redirectUri: window.location.href 
});

🔐 實戰 3:身份的二次驗證 (JWT Token)

好,現在我們拿到了使用者的 profile (包含姓名和大頭貼)。我們可以把這個姓名直接傳給我們的後端 (FastAPI),跟後端說:「王小明打卡了!」對吧?

絕對不行!這是最嚴重的資安漏洞!

如果你的後端 API 只認「名字」,駭客只要寫一個假網頁,傳送一個「總經理」的名字給你的後端,後端就會以為總經理打卡了! 在真實的商業系統中,前端傳給後端的絕對不能是明文名字,而必須是一張**「無法偽造的通行證 (ID Token)」**。

💡 Vibe Prompt 實戰 3:取得 Line 的 JWT Token

[!IMPORTANT] 請複製以下 Prompt 傳送給 AI:

在 LIFF 應用中,我要把使用者的打卡資料送到我的後端伺服器 (FastAPI)。 為了防止駭客偽造身份,我不能只傳 liff.getProfile() 拿到的名字。 請教我如何使用 liff.getIDToken() 來取得安全的 JWT 憑證。 請寫一個函數 punchIn(),裡面示範如何取得 Token,並用 fetch() 把 Token 放在 HTTP Header (Authorization: Bearer) 中送到後端 API (/api/punch)。 請附上詳細的資安註解。

🤖 AI 的企業級防偽打卡功能:

export const punchIn = async () => {
  try {
    // 1. 取得 Line 簽發的安全通行證 (JWT Token)
    // 這串文字是經過 Line 伺服器加密簽章的,駭客絕對無法自己偽造一模一樣的 Token
    const idToken = liff.getIDToken();
    
    if (!idToken) {
      throw new Error("無法取得安全憑證,請重新登入!");
    }

    console.log("🔐 取得通行證準備打卡...");

    // 2. 打 API 送資料給後端
    const response = await fetch('https://你的後端網址.com/api/punch', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        // 🔥 這是精華!把通行證放在 Header 裡面傳給後端
        'Authorization': `Bearer ${idToken}`
      },
      body: JSON.stringify({
        action: 'clock_in',
        timestamp: new Date().toISOString()
        // ⚠️ 注意:我們完全不需要傳送姓名或 Line ID 給後端!
        // 後端收到 Token 後,會自己去問 Line 伺服器:「這個 Token 是誰的?」
      })
    });

    const result = await response.json();
    if (response.ok) {
      alert("✅ 打卡成功!");
    } else {
      alert("❌ 打卡失敗:" + result.message);
    }
    
  } catch (error) {
    console.error("打卡發生異常:", error);
    alert("系統異常,請聯絡管理員。");
  }
};

🔍 深度解析: 這就是為什麼這套系統可以賣幾十萬的原因。 「零信任架構 (Zero Trust)」 是現代 SaaS 系統的標配。前端是極度不安全的,任何人都可以竄改你瀏覽器裡的資料。 但 liff.getIDToken() 產生出來的那串亂碼,包含了 Line 伺服器的數位簽章。你的後端只要解開這個簽章,就能百分之百確定:「這個請求,確實是從王小明本人的 Line APP 裡面發出來的」。這就是隱形登入的最高境界!


⏳ 實戰 4:把 LIFF 狀態與畫面完美結合

我們寫好了 Hook 和打卡功能,現在要把這些東西套用到網頁畫面上。 在 LIFF 還沒準備好之前,畫面應該要是什麼樣子? 如果是在外部瀏覽器打開,然後跳轉回來,畫面會不會閃爍?

💡 Vibe Prompt 實戰 4:打造體驗滿分的過場畫面 (Splash Screen)

在 APP 開發中,我們通常會有一個啟動畫面 (Splash Screen)。 我們用 React 和 Tailwind 來實作它。

[!IMPORTANT] 請複製以下 Prompt 傳送給 AI:

我現在要在 App.jsx 中使用剛才寫的 useLiff() hook。 需求: 1. 呼叫 const { isReady, liffError, profile } = useLiff(); 2. 如果 liffError 有值,顯示一個滿版的紅色錯誤畫面。 3. 如果 isReady 為 false,顯示一個深色質感的滿版 Loading 畫面 (包含旋轉的圖示和 "系統初始化中..." 文字)。 4. 如果 isReady 為 true,才顯示主畫面 (包含使用者的頭像、姓名,以及一個「上班打卡」的按鈕)。 請給我完整的 App.jsx 程式碼。

🤖 AI 的高質感應用程式入口:

import React from 'react';
import { useLiff } from './hooks/useLiff';
import { punchIn } from './api'; // 剛才寫的打卡函數

export default function App() {
  const { isReady, liffError, profile } = useLiff();

  // 情境 1:發生致命錯誤 (例如沒有設定 LIFF ID)
  if (liffError) {
    return (
      <div className="min-h-screen bg-zinc-900 flex items-center justify-center p-6">
        <div className="bg-red-500/20 border border-red-500 text-red-100 p-6 rounded-2xl max-w-sm text-center">
          <h2 className="text-xl font-bold mb-2">⚠️ 系統載入失敗</h2>
          <p className="text-sm opacity-80">{liffError}</p>
        </div>
      </div>
    );
  }

  // 情境 2:正在跟 Line 伺服器連線中 (Splash Screen)
  if (!isReady) {
    return (
      <div className="min-h-screen bg-zinc-900 flex flex-col items-center justify-center">
        {/* Tailwind 的 animate-spin 可以讓任何東西旋轉 */}
        <div className="w-12 h-12 border-4 border-emerald-500 border-t-transparent rounded-full animate-spin mb-4"></div>
        <p className="text-emerald-500 font-bold tracking-widest animate-pulse">
          SYSTEM INITIALIZING...
        </p>
      </div>
    );
  }

  // 情境 3:連線成功!顯示主畫面
  return (
    <div className="min-h-screen bg-zinc-900 text-white p-6">
      <header className="flex items-center gap-4 mb-10">
        {/* 顯示 Line 頭像 */}
        {profile?.pictureUrl ? (
          <img 
            src={profile.pictureUrl} 
            alt="avatar" 
            className="w-16 h-16 rounded-full border-2 border-emerald-500 shadow-lg shadow-emerald-500/20"
          />
        ) : (
          <div className="w-16 h-16 rounded-full bg-zinc-800 border-2 border-zinc-700"></div>
        )}
        
        <div>
          <p className="text-zinc-400 text-sm">早安,辛苦了</p>
          <h1 className="text-2xl font-bold">{profile?.displayName || '員工'}</h1>
        </div>
      </header>

      <main>
        {/* 霸氣的打卡按鈕 */}
        <button 
          onClick={punchIn}
          className="w-full bg-emerald-500 text-zinc-900 font-bold text-xl py-6 rounded-2xl shadow-[0_0_30px_rgba(16,185,129,0.3)] active:scale-95 transition-transform"
        >
          👆 立即打卡
        </button>
      </main>
    </div>
  );
}

📱 實戰 5:在 Line 內建瀏覽器中開啟的「特權」

當你的網頁跑在 Line 裡面時,你不只是拿到姓名而已。 Line 還給了你一個超強的 API:liff.closeWindow()

你可以想像一個情境:員工點開 Line 的群組連結,跳出打卡畫面。按完打卡後,畫面出現「打卡成功」,然後... 然後他不知道怎麼辦,只能手動按左上角的 X 關閉網頁。 這非常不流暢! 如果我們能在打卡成功後,程式碼自動幫他把網頁關掉,回到 Line 聊天室,那這個體驗就跟原生的 APP 一模一樣!

💡 Vibe Prompt 實戰 5:自動關閉視窗的完美體驗

[!IMPORTANT] 請複製以下 Prompt 傳送給 AI:

我希望在員工點擊「打卡成功」的 alert 之後,網頁能夠自動關閉,讓他回到 Line 的聊天室。 我知道可以使用 liff.closeWindow()。 但為了防止在「外部電腦瀏覽器」執行這行程式碼導致報錯,請幫我加上判斷邏輯。 請告訴我如何修改剛才的 punchIn() 函數。

🤖 AI 的極致體驗優化:

// 在 punchIn 函數的成功區塊中加入:

if (response.ok) {
  alert("✅ 打卡成功!系統將為您關閉視窗。");
  
  // 🔥 神級防呆判斷:
  // liff.isInClient() 會檢查這個網頁是不是正在 Line APP 的內部瀏覽器裡面跑
  if (liff.isInClient()) {
    // 如果是,直接霸氣關閉整個視窗,退回聊天室!
    liff.closeWindow();
  } else {
    // 如果是在電腦版 Chrome 打開的,就幫他跳轉到一個感謝頁面,或是重新整理
    window.location.reload();
  }
}

✅ 本章總結與終極架構心法

在這個高達 6000 字的超深解析中,我們完全破解了 Line 打卡系統的前端命脈。 這不只是一段程式碼,這是一整套高商業價值的解決方案

回顧我們建立的這套隱形登入流:

  1. 全域 Hook (useLiff):優雅地處理非同步的初始化,並把狀態發送給整個系統。
  2. 防呆重新導向 (liff.login({redirectUri})):解決了最常發生的無限重新載入死迴圈。
  3. 零信任資安 (getIDToken):絕對不傳明文姓名,而是傳送擁有數位簽章的 JWT,徹底封殺駭客偽造打卡的可能。
  4. 載入體驗 (Splash Screen):用 Tailwind 的動畫,填補了與 Line 伺服器連線時的等待空白。
  5. 原生體驗 (closeWindow):打完卡自動關閉視窗,讓使用者覺得這不是一個網頁,而是一個內建功能。

當你學會了這套 LIFF 架構,你就不只能做打卡系統了。 你可以做「餐廳的桌邊掃碼點餐系統」、「美髮沙龍的預約系統」、「電商的會員綁定卡」。 只要有這套登入流,使用者的轉換率將會大幅提升!

但現在我們的畫面還是稍微陽春了一點(只有一個按鈕)。 在下一章:第十二章:Glassmorphism 與科技感儀表板介面,我們將把這個打卡頁面,升級成價值連城的賽博龐克風格,讓員工每天打卡都像是在啟動鋼鐵人的戰衣!我們下堂課見!

解鎖完整教學內容

本章為付費內容。加入專案即可解鎖超過 5000 字的深度解析,包含 10 個以上神級 Prompt 與真實 Source Code 範例!