iT邦幫忙

2025 iThome 鐵人賽

DAY 30
0
Modern Web

30 天製作工作室 SaaS 產品 (前端篇)系列 第 30

Day 30: 30天打造SaaS產品前端篇-完賽心得與技術棧總回顧

  • 分享至 

  • xImage
  •  

30 天旅程回顧

終於來到了鐵人賽的最後一天!回顧這 30 天的挑戰,感觸良多。

我在軟體開發領域已經工作八年,從最初的後端工程師,到後來的全端工程師,再到區塊鏈開發,甚至成立了自己的工作室,接了不少案子。但在這次的鐵人賽挑戰中,第一次從零開始打造一個完整的 SaaS 產品,才發現有太多需要注意和學習的地方。

以前在公司工作時,大多是進入一個已經建立好框架和架構的環境,專注在功能開發和優化上。但這次從頭打造 Kyo System,從前端的 Landing Page 設計、用戶認證流程、到數據視覺化儀表板還有OTP服務,每一個環節都需要自己做決策、選擇技術棧、考慮效能和使用者體驗。

這個過程雖然充滿挑戰,但也讓我在前端開發的理解上更進一步。更重要的是,成功地為工作室打造出一套可以實際應用的 SaaS 產品架構,這是這次挑戰最大的收穫。

前端技術棧完整回顧

技術選型決策回顧

1. React 18 + TypeScript

// TypeScript 帶來的型別安全
interface OTPVerification {
  phoneNumber: string;
  code: string;
  requestId: string;
}

const verifyOTP = async (data: OTPVerification): Promise<AuthResponse> => {
  // 編譯時期就能發現型別錯誤,大幅減少 runtime error
}

選擇原因:

  • React 18 的 Concurrent Features 提升使用者體驗
  • TypeScript 提供強型別檢查,減少 bug
  • 龐大的生態系統與社群支援

2. Vite 建構工具

// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vendor-react': ['react', 'react-dom'],
          'vendor-mantine': ['@mantine/core', '@mantine/hooks'],
          'vendor-charts': ['recharts'],
        },
      },
    },
  },
})

選擇原因:

  • 開發時的 HMR 速度極快
  • 原生 ES modules 支援
  • 簡潔的設定檔

實際收穫:

  • 開發體驗極佳,改動即時反映
  • 打包體積可控,透過 code splitting 優化載入速度
  • 部署到 CDN 後,首頁載入時間 < 1.5s

3. Mantine UI 元件庫

<Stack spacing="md">
  <TextInput
    label="手機號碼"
    placeholder="+886 912345678"
    required
    icon={<IconPhone size={16} />}
  />
  <Button loading={isLoading} fullWidth>
    發送驗證碼
  </Button>
</Stack>

選擇原因:

  • 元件設計現代且一致
  • 內建暗黑模式支援
  • 完整的 TypeScript 定義
  • 豐富的 Hooks 工具

實際收穫:

  • 開發速度快,元件品質高
  • 主題系統靈活,輕鬆客製化品牌風格
  • 內建的 accessibility 支援,無障礙功能開箱即用

4. Zustand 狀態管理

interface AppStore {
  user: User | null;
  setUser: (user: User | null) => void;
  notifications: Notification[];
  addNotification: (notification: Notification) => void;
}

const useAppStore = create<AppStore>((set) => ({
  user: null,
  setUser: (user) => set({ user }),
  notifications: [],
  addNotification: (notification) =>
    set((state) => ({
      notifications: [...state.notifications, notification],
    })),
}))

選擇原因:

  • API 簡潔,學習曲線平緩
  • 沒有 Provider 地獄
  • Bundle size 小 (< 1KB)
  • 完美的 TypeScript 支援

實際收穫:

  • 狀態管理邏輯清晰,容易維護
  • 效能優異,不需要額外的優化
  • 可以輕鬆整合 Redux DevTools

5. TanStack Query (React Query)

const { data, isLoading, error } = useQuery({
  queryKey: ['otp-stats', dateRange],
  queryFn: () => fetchOTPStats(dateRange),
  staleTime: 5 * 60 * 1000, // 5 分鐘
  refetchInterval: 30 * 1000, // 30 秒自動刷新
})

選擇原因:

  • 強大的快取機制
  • 自動重試與背景更新
  • 樂觀更新支援
  • 內建 loading 與 error 狀態

實際收穫:

  • 大幅減少手動管理 loading state 的程式碼
  • 快取策略讓 API 請求次數減少 60%
  • 使用者體驗提升,資料總是保持新鮮

關鍵學習點深入探討

1. Landing Page 與轉換率優化

學到的關鍵概念:

// A/B Testing 架構
const LandingPage = () => {
  const variant = useABTest('hero-cta-test', ['variant-a', 'variant-b'])

  return (
    <Hero>
      {variant === 'variant-a' ? (
        <Button size="xl">免費試用 30 天</Button>
      ) : (
        <Button size="xl">立即開始</Button>
      )}
    </Hero>
  )
}

實踐經驗:

  • Hero Section 的 CTA 按鈕顏色改為對比色,點擊率提升 25%
  • 加入社會證明 (客戶 Logo、使用人數),信任度提升
  • 優化首屏載入速度到 1.2s,跳出率降低 15%
  • 使用 Hotjar 熱力圖分析使用者行為,調整版面配置

轉換漏斗優化:

造訪首頁 (100%)
  → 滾動到 Features (70%)
  → 點擊 CTA (12%)
  → 註冊頁面 (10%)
  → 完成註冊 (7%)

優化後:
造訪首頁 (100%)
  → 滾動到 Features (75%)
  → 點擊 CTA (18%)
  → 註冊頁面 (15%)
  → 完成註冊 (11%)

2. 認證系統與 MFA 安全

JWT Token 管理策略:

// Token 自動刷新機制
const setupTokenRefresh = () => {
  const token = getAccessToken()
  if (!token) return

  const { exp } = jwtDecode(token)
  const expiresIn = exp * 1000 - Date.now()
  const refreshTime = expiresIn - 5 * 60 * 1000 // 提前 5 分鐘

  setTimeout(async () => {
    try {
      const newToken = await refreshAccessToken()
      setAccessToken(newToken)
      setupTokenRefresh() // 遞迴設定下一次刷新
    } catch (error) {
      // Token 刷新失敗,導向登入頁
      logout()
    }
  }, refreshTime)
}

TOTP 多因素認證實作:

// 使用 speakeasy 生成 TOTP secret
const generateMFASecret = () => {
  const secret = speakeasy.generateSecret({
    name: 'Kyo System',
    issuer: 'Kyo OTP',
    length: 32,
  })

  return {
    secret: secret.base32,
    qrCode: secret.otpauth_url,
  }
}

// 驗證 TOTP code
const verifyTOTP = (secret: string, token: string) => {
  return speakeasy.totp.verify({
    secret,
    encoding: 'base32',
    token,
    window: 2, // 允許前後 2 個時間窗口
  })
}

安全性提升:

  • 實作 CSRF Token 防護
  • 使用 HTTP-only Cookie 儲存 Refresh Token
  • 實作帳號登入異常偵測
  • 密碼強度檢查 (最少 8 碼,包含大小寫、數字、特殊字元)

3. 即時通訊與 WebSocket

Socket.io 客戶端架構:

// WebSocket Hook
const useWebSocket = () => {
  const [socket, setSocket] = useState<Socket | null>(null)
  const [connected, setConnected] = useState(false)

  useEffect(() => {
    const token = getAccessToken()
    const newSocket = io(WS_URL, {
      auth: { token },
      transports: ['websocket', 'polling'],
      reconnection: true,
      reconnectionDelay: 1000,
      reconnectionAttempts: 5,
    })

    newSocket.on('connect', () => {
      setConnected(true)
      console.log('WebSocket connected')
    })

    newSocket.on('disconnect', () => {
      setConnected(false)
      console.log('WebSocket disconnected')
    })

    newSocket.on('notification', (data) => {
      showNotification(data)
    })

    setSocket(newSocket)

    return () => {
      newSocket.close()
    }
  }, [])

  return { socket, connected }
}

即時通知系統:

// 通知管理
interface Notification {
  id: string
  type: 'success' | 'error' | 'warning' | 'info'
  title: string
  message: string
  timestamp: Date
  read: boolean
}

const NotificationCenter = () => {
  const { socket } = useWebSocket()
  const { notifications, addNotification, markAsRead } = useNotificationStore()

  useEffect(() => {
    if (!socket) return

    socket.on('otp:sent', (data) => {
      addNotification({
        type: 'success',
        title: 'OTP 已發送',
        message: `已發送 OTP 到 ${data.phoneNumber}`,
      })
    })

    socket.on('otp:verified', (data) => {
      addNotification({
        type: 'success',
        title: 'OTP 驗證成功',
        message: `用戶 ${data.userId} 已完成驗證`,
      })
    })
  }, [socket])

  return (
    <NotificationList
      notifications={notifications}
      onMarkAsRead={markAsRead}
    />
  )
}

效能優化:

  • 實作訊息去重機制,避免重複通知
  • 使用 Virtual List 渲染大量通知,避免效能問題
  • 實作通知聲音與桌面通知 (需用戶授權)
  • 自動清理 7 天前的已讀通知

4. 數據視覺化儀表板

Recharts 進階應用:

// 自訂 Tooltip
const CustomTooltip = ({ active, payload }: any) => {
  if (!active || !payload?.length) return null

  return (
    <Paper p="sm" shadow="md">
      <Text size="sm" weight={500}>
        {format(new Date(payload[0].payload.date), 'yyyy/MM/dd')}
      </Text>
      <Group spacing="xs" mt="xs">
        <Badge color="blue">發送: {payload[0].value}</Badge>
        <Badge color="green">成功: {payload[1].value}</Badge>
        <Badge color="red">失敗: {payload[2].value}</Badge>
      </Group>
    </Paper>
  )
}

// 互動式圖表
const OTPStatsChart = ({ data }: { data: ChartData[] }) => {
  const [activeIndex, setActiveIndex] = useState<number | null>(null)

  return (
    <ResponsiveContainer width="100%" height={400}>
      <LineChart data={data}>
        <CartesianGrid strokeDasharray="3 3" />
        <XAxis dataKey="date" />
        <YAxis />
        <Tooltip content={<CustomTooltip />} />
        <Legend />
        <Line
          type="monotone"
          dataKey="sent"
          stroke="#228be6"
          strokeWidth={2}
          activeDot={{ r: 8 }}
        />
        <Line
          type="monotone"
          dataKey="success"
          stroke="#40c057"
          strokeWidth={2}
        />
        <Line
          type="monotone"
          dataKey="failed"
          stroke="#fa5252"
          strokeWidth={2}
        />
      </LineChart>
    </ResponsiveContainer>
  )
}

即時數據更新:

// 使用 TanStack Query 的 refetchInterval
const { data: stats } = useQuery({
  queryKey: ['dashboard-stats'],
  queryFn: fetchDashboardStats,
  refetchInterval: 30000, // 30 秒自動刷新
  refetchIntervalInBackground: false, // 背景不刷新,節省資源
})

// WebSocket 即時更新
useEffect(() => {
  if (!socket) return

  socket.on('stats:updated', (newStats) => {
    queryClient.setQueryData(['dashboard-stats'], newStats)
  })
}, [socket])

效能優化技巧:

  • 使用 useMemo 快取圖表數據計算
  • 大數據集使用數據抽樣 (顯示每小時而非每分鐘)
  • 圖表懶加載,只在可視區域才渲染
  • 使用 Web Worker 處理大量數據計算

5. 測試策略

單元測試 (Vitest):

// Component 測試
describe('OTPForm', () => {
  it('should validate phone number format', async () => {
    const { getByRole, getByText } = render(<OTPForm />)

    const input = getByRole('textbox', { name: /phone/i })
    const submitButton = getByRole('button', { name: /send/i })

    // 輸入無效號碼
    fireEvent.change(input, { target: { value: '123' } })
    fireEvent.click(submitButton)

    expect(getByText(/invalid phone number/i)).toBeInTheDocument()
  })

  it('should call API when form is valid', async () => {
    const mockSendOTP = vi.fn()
    const { getByRole } = render(<OTPForm onSubmit={mockSendOTP} />)

    fireEvent.change(getByRole('textbox'), {
      target: { value: '+886912345678' },
    })
    fireEvent.click(getByRole('button'))

    await waitFor(() => {
      expect(mockSendOTP).toHaveBeenCalledWith('+886912345678')
    })
  })
})

E2E 測試 (Playwright):

// 完整用戶流程測試
test('user can complete OTP verification flow', async ({ page }) => {
  // 1. 造訪首頁
  await page.goto('http://localhost:5173')

  // 2. 點擊登入
  await page.click('text=登入')

  // 3. 輸入手機號碼
  await page.fill('input[name="phoneNumber"]', '+886912345678')
  await page.click('button:has-text("發送驗證碼")')

  // 4. 等待 API 回應
  await page.waitForSelector('text=驗證碼已發送')

  // 5. 輸入驗證碼
  await page.fill('input[name="code"]', '123456')
  await page.click('button:has-text("驗證")')

  // 6. 驗證成功後應導向儀表板
  await expect(page).toHaveURL(/.*dashboard/)
  await expect(page.locator('h1')).toContainText('儀表板')
})

無障礙測試 (axe-core):

import { axe, toHaveNoViolations } from 'jest-axe'

expect.extend(toHaveNoViolations)

test('should not have accessibility violations', async () => {
  const { container } = render(<App />)
  const results = await axe(container)

  expect(results).toHaveNoViolations()
})

測試覆蓋率目標:

  • 核心業務邏輯: 90%+
  • UI 元件: 70%+
  • E2E 關鍵流程: 100%

遇到的挑戰與解決方案

挑戰 1: 技術選型的取捨

問題: 在 Mantine 與 Ant Design、Chakra UI 之間猶豫不決

考量因素:

  • Bundle size
  • TypeScript 支援度
  • 客製化難度
  • 社群活躍度

最終決策: Mantine

  • Bundle size 適中 (tree-shaking 後約 150KB)
  • 完整的 TypeScript 定義
  • 主題系統靈活
  • 文件清楚,範例豐富

學到的經驗:
不要盲目追求「最流行」的方案,要根據專案需求選擇最適合的工具。做 POC (Proof of Concept) 是很好的驗證方式。

挑戰 2: 效能優化的實踐

問題: 初版儀表板載入時間超過 5 秒,使用者體驗不佳

分析過程:

# 使用 Vite Bundle Analyzer
npm run build -- --analyze

# 發現問題:
# 1. 所有依賴打包在一起 (vendor.js 3.5MB)
# 2. 沒有 code splitting
# 3. 圖表庫沒有 tree-shaking

解決方案:

  1. Code Splitting:
// 路由層級的 lazy loading
const Dashboard = lazy(() => import('./pages/Dashboard'))
const Analytics = lazy(() => import('./pages/Analytics'))
const Settings = lazy(() => import('./pages/Settings'))

const App = () => (
  <Suspense fallback={<LoadingScreen />}>
    <Routes>
      <Route path="/dashboard" element={<Dashboard />} />
      <Route path="/analytics" element={<Analytics />} />
      <Route path="/settings" element={<Settings />} />
    </Routes>
  </Suspense>
)
  1. Manual Chunks:
// vite.config.ts
build: {
  rollupOptions: {
    output: {
      manualChunks: {
        'vendor-react': ['react', 'react-dom', 'react-router-dom'],
        'vendor-ui': ['@mantine/core', '@mantine/hooks', '@mantine/form'],
        'vendor-charts': ['recharts'],
        'vendor-query': ['@tanstack/react-query'],
      },
    },
  },
}
  1. 動態載入圖表:
// 只在需要時載入圖表元件
const ChartsSection = lazy(() => import('./components/ChartsSection'))

const Analytics = () => {
  const [showCharts, setShowCharts] = useState(false)

  return (
    <div>
      <Button onClick={() => setShowCharts(true)}>
        顯示圖表分析
      </Button>

      {showCharts && (
        <Suspense fallback={<Skeleton height={400} />}>
          <ChartsSection />
        </Suspense>
      )}
    </div>
  )
}

優化結果:

  • 首次載入時間: 5.2s → 1.8s (降低 65%)
  • First Contentful Paint: 3.1s → 0.9s
  • Largest Contentful Paint: 5.2s → 1.5s
  • Bundle size: 3.5MB → 1.2MB (gzipped: 350KB)

挑戰 3: 使用者體驗的平衡

問題: 安全性與便利性的取捨

具體情境:

  • 為了安全,Access Token 有效期設為 15 分鐘
  • 使用者反應頻繁需要重新登入,體驗不佳

解決方案:

  1. 無感刷新機制:
// Axios Interceptor 自動刷新 token
axios.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config

    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true

      try {
        const newToken = await refreshAccessToken()
        setAccessToken(newToken)
        originalRequest.headers.Authorization = `Bearer ${newToken}`
        return axios(originalRequest)
      } catch (refreshError) {
        // Refresh token 也過期,導向登入頁
        logout()
        return Promise.reject(refreshError)
      }
    }

    return Promise.reject(error)
  }
)
  1. 記住我功能:
// 延長 Refresh Token 有效期
const login = async (credentials: LoginData, rememberMe: boolean) => {
  const response = await api.post('/auth/login', {
    ...credentials,
    refreshTokenTTL: rememberMe ? '30d' : '7d',
  })

  if (rememberMe) {
    localStorage.setItem('rememberMe', 'true')
  }
}
  1. 活動偵測:
// 偵測使用者活動,延長 session
const useActivityDetection = () => {
  useEffect(() => {
    let lastActivity = Date.now()

    const updateActivity = () => {
      lastActivity = Date.now()
    }

    const checkInactivity = setInterval(() => {
      const inactiveTime = Date.now() - lastActivity

      if (inactiveTime > 30 * 60 * 1000) { // 30 分鐘無活動
        logout()
      }
    }, 60 * 1000) // 每分鐘檢查

    window.addEventListener('mousemove', updateActivity)
    window.addEventListener('keypress', updateActivity)

    return () => {
      clearInterval(checkInactivity)
      window.removeEventListener('mousemove', updateActivity)
      window.removeEventListener('keypress', updateActivity)
    }
  }, [])
}

結果:

  • 使用者滿意度提升
  • 安全性維持高標準
  • Session 管理更智慧

給自己的筆記

如何開始自己的 SaaS 專案

基於這 30 天的經驗,我學到:

1. 從 MVP 開始

✅ DO:
- 專注核心功能 (對 Kyo System 來說就是 OTP 發送與驗證)
- 選擇熟悉的技術棧
- 快速上線,收集使用者反饋

❌ DON'T:
- 一開始就想做完美的架構
- 追求最新、最酷的技術
- 過度設計,想太多邊緣情境

2. 建立良好的開發習慣

Git Commit 規範:

feat: 新功能
fix: 修復 bug
docs: 文件更新
style: 程式碼格式調整
refactor: 重構
test: 測試相關
chore: 雜項 (依賴更新等)

範例:
feat(auth): add MFA support with TOTP
fix(dashboard): resolve chart rendering issue on Safari

Code Review 檢查清單:

  • [ ] 是否有足夠的錯誤處理?
  • [ ] 是否考慮了 loading 狀態?
  • [ ] 是否有安全性問題? (XSS, CSRF)
  • [ ] 是否影響效能?
  • [ ] 是否易於測試?
  • [ ] 是否符合專案的程式碼風格?

3. 重視使用者體驗

// 好的 UX 設計
const SendOTPButton = () => {
  const [loading, setLoading] = useState(false)
  const [countdown, setCountdown] = useState(0)

  const handleSend = async () => {
    setLoading(true)

    try {
      await sendOTP()
      showNotification({ message: '驗證碼已發送' })
      setCountdown(60) // 60 秒後才能再次發送
    } catch (error) {
      showNotification({
        color: 'red',
        message: error.message || '發送失敗,請稍後再試',
      })
    } finally {
      setLoading(false)
    }
  }

  useEffect(() => {
    if (countdown > 0) {
      const timer = setTimeout(() => setCountdown(countdown - 1), 1000)
      return () => clearTimeout(timer)
    }
  }, [countdown])

  return (
    <Button
      onClick={handleSend}
      loading={loading}
      disabled={countdown > 0}
    >
      {countdown > 0 ? `重新發送 (${countdown}s)` : '發送驗證碼'}
    </Button>
  )
}

UX 關鍵點:

  • 提供即時反饋 (loading, success, error)
  • 防止重複提交
  • 清楚的錯誤訊息
  • 適當的 loading 狀態
  • 鍵盤快捷鍵支援

前端學習路徑建議

根據我的經驗,推薦以下學習路徑:

Level 1: 基礎
├─ HTML/CSS/JavaScript
├─ Git 版本控制
├─ Chrome DevTools
└─ 基礎演算法與資料結構

Level 2: 框架入門
├─ React 基礎
├─ TypeScript
├─ 狀態管理 (Zustand/Redux)
└─ 路由 (React Router)

Level 3: 進階開發
├─ 建構工具 (Vite/Webpack)
├─ 測試 (Vitest, Playwright)
├─ 效能優化
└─ 無障礙設計

Level 4: 專業領域
├─ 伺服器端渲染 (Next.js)
├─ 效能監控與分析
└─ 安全性最佳實踐

Level 5: 全端與架構
├─ Node.js 後端
├─ 雲端服務 (AWS/GCP)
├─ CI/CD
└─ 系統設計

推薦資源:

避免的常見陷阱

1. 過度工程化

❌ 不好的做法 (過度抽象):
// 為了一個簡單的 button 創建複雜的抽象層
const ButtonFactory = createFactory({
  strategies: {
    primary: PrimaryButtonStrategy,
    secondary: SecondaryButtonStrategy,
  },
  decorators: [LoadingDecorator, DisabledDecorator],
  middleware: [ValidationMiddleware, AnalyticsMiddleware],
})

✅ 好的做法 (簡單直接):
const Button = ({ variant, loading, onClick, children }) => (
  <button
    className={`btn btn-${variant}`}
    disabled={loading}
    onClick={onClick}
  >
    {loading ? <Spinner /> : children}
  </button>
)

原則: KISS (Keep It Simple, Stupid) - 優先選擇簡單的解決方案

2. 忽略效能

❌ 效能陷阱:
const Dashboard = () => {
  // 每次 render 都重新創建 function
  const handleClick = () => {
    console.log('clicked')
  }

  // 每次 render 都重新計算
  const expensiveValue = calculateExpensiveValue(data)

  return <ChildComponent onClick={handleClick} value={expensiveValue} />
}

✅ 效能優化:
const Dashboard = () => {
  // 使用 useCallback 快取 function
  const handleClick = useCallback(() => {
    console.log('clicked')
  }, [])

  // 使用 useMemo 快取計算結果
  const expensiveValue = useMemo(
    () => calculateExpensiveValue(data),
    [data]
  )

  return <ChildComponent onClick={handleClick} value={expensiveValue} />
}

3. 安全性疏忽

❌ 安全漏洞:
// XSS 漏洞
<div dangerouslySetInnerHTML={{ __html: userInput }} />

// CSRF 漏洞
fetch('/api/transfer', {
  method: 'POST',
  body: JSON.stringify({ amount: 1000 }),
})

✅ 安全實踐:
// 使用安全的 HTML sanitizer
import DOMPurify from 'dompurify'
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />

// 加入 CSRF token
fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'X-CSRF-Token': getCsrfToken(),
  },
  body: JSON.stringify({ amount: 1000 }),
})

4. 忽略錯誤處理

❌ 缺乏錯誤處理:
const UserProfile = () => {
  const { data } = useQuery({ queryKey: ['user'], queryFn: fetchUser })

  return <div>{data.name}</div> // data 可能是 undefined!
}

✅ 完整的錯誤處理:
const UserProfile = () => {
  const { data, isLoading, error } = useQuery({
    queryKey: ['user'],
    queryFn: fetchUser,
  })

  if (isLoading) return <Skeleton />
  if (error) return <ErrorMessage error={error} />
  if (!data) return <EmptyState />

  return <div>{data.name}</div>
}

Kyo System 前端架構總結

最終架構圖

┌─────────────────────────────────────────────────────────────┐
│                      Kyo System Frontend                     │
└─────────────────────────────────────────────────────────────┘
                              │
        ┌─────────────────────┼─────────────────────┐
        │                     │                     │
        ▼                     ▼                     ▼
   ┌─────────┐          ┌─────────┐          ┌─────────┐
   │ Landing │          │   Auth  │          │Dashboard│
   │  Page   │          │  Pages  │          │  Pages  │
   └─────────┘          └─────────┘          └─────────┘
        │                     │                     │
        └─────────────────────┼─────────────────────┘
                              │
        ┌─────────────────────┼─────────────────────┐
        │                     │                     │
        ▼                     ▼                     ▼
   ┌─────────┐          ┌─────────┐          ┌─────────┐
   │  UI     │          │  State  │          │  Data   │
   │  Layer  │          │ Manager │          │  Layer  │
   └─────────┘          └─────────┘          └─────────┘
        │                     │                     │
   ┌────┴────┐           ┌────┴────┐          ┌────┴────┐
   │ Mantine │           │ Zustand │          │  React  │
   │   UI    │           │         │          │  Query  │
   └─────────┘           └─────────┘          └─────────┘
                              │
                              │
                    ┌─────────┴─────────┐
                    │                   │
                    ▼                   ▼
              ┌──────────┐        ┌──────────┐
              │   HTTP   │        │ WebSocket│
              │   API    │        │  Socket  │
              └──────────┘        └──────────┘
                    │                   │
                    └─────────┬─────────┘
                              │
                              ▼
                    ┌──────────────────┐
                    │   Backend API    │
                    │  (Fastify + TS)  │
                    └──────────────────┘

模組化設計

src/
├── app/
│   ├── App.tsx                 # 應用程式進入點
│   ├── Router.tsx              # 路由設定
│   └── providers/              # Context Providers
│       ├── QueryProvider.tsx   # React Query
│       ├── ThemeProvider.tsx   # Mantine Theme
│       └── AuthProvider.tsx    # 認證 Context
│
├── pages/
│   ├── landing/
│   │   ├── LandingPage.tsx
│   │   ├── HeroSection.tsx
│   │   ├── FeaturesSection.tsx
│   │   └── PricingSection.tsx
│   │
│   ├── auth/
│   │   ├── LoginPage.tsx
│   │   ├── RegisterPage.tsx
│   │   └── MFASetupPage.tsx
│   │
│   └── dashboard/
│       ├── DashboardPage.tsx
│       ├── AnalyticsPage.tsx
│       └── SettingsPage.tsx
│
├── components/
│   ├── ui/                     # 共用 UI 元件
│   │   ├── Button.tsx
│   │   ├── Input.tsx
│   │   └── Card.tsx
│   │
│   ├── charts/                 # 圖表元件
│   │   ├── LineChart.tsx
│   │   ├── BarChart.tsx
│   │   └── PieChart.tsx
│   │
│   └── layout/                 # 版面元件
│       ├── Header.tsx
│       ├── Sidebar.tsx
│       └── Footer.tsx
│
├── features/                   # 功能模組
│   ├── otp/
│   │   ├── components/
│   │   │   ├── OTPForm.tsx
│   │   │   └── OTPVerifyForm.tsx
│   │   ├── hooks/
│   │   │   └── useOTP.ts
│   │   └── api/
│   │       └── otpApi.ts
│   │
│   ├── notifications/
│   │   ├── components/
│   │   │   └── NotificationCenter.tsx
│   │   ├── hooks/
│   │   │   └── useWebSocket.ts
│   │   └── store/
│   │       └── notificationStore.ts
│   │
│   └── analytics/
│       ├── components/
│       │   └── StatsCard.tsx
│       └── hooks/
│           └── useAnalytics.ts
│
├── stores/                     # Zustand stores
│   ├── authStore.ts
│   ├── uiStore.ts
│   └── notificationStore.ts
│
├── hooks/                      # 共用 Hooks
│   ├── useAuth.ts
│   ├── useLocalStorage.ts
│   └── useDebounce.ts
│
├── lib/                        # 工具函數
│   ├── api/
│   │   ├── client.ts          # Axios instance
│   │   └── endpoints.ts       # API endpoints
│   │
│   ├── utils/
│   │   ├── format.ts          # 格式化函數
│   │   ├── validation.ts      # 驗證函數
│   │   └── storage.ts         # LocalStorage 封裝
│   │
│   └── constants/
│       ├── routes.ts          # 路由常數
│       └── config.ts          # 設定檔
│
└── types/                      # TypeScript 型別定義
    ├── api.ts
    ├── models.ts
    └── common.ts

可擴展性設計

1. 功能模組化

每個功能 (feature) 都是獨立的模組,包含:

  • Components: UI 元件
  • Hooks: 業務邏輯
  • API: API 呼叫
  • Store: 狀態管理

優點:

  • 高內聚低耦合
  • 易於測試
  • 可以獨立開發與部署
  • 未來可以拆成微前端

2. 共用元件庫

建立內部元件庫 @kyong/kyo-ui:

// packages/kyo-ui/src/Button/Button.tsx
export const Button = ({ variant, size, loading, children, ...props }) => {
  return (
    <button
      className={clsx(
        'kyo-button',
        `kyo-button--${variant}`,
        `kyo-button--${size}`,
        { 'kyo-button--loading': loading }
      )}
      {...props}
    >
      {loading && <Spinner />}
      {children}
    </button>
  )
}

// 在應用中使用
import { Button } from '@kyong/kyo-ui'

優點:

  • 統一的設計語言
  • 跨專案共用
  • 獨立版本控制
  • 可以單獨測試與發布

3. API 層抽象

// lib/api/client.ts
export const apiClient = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  timeout: 10000,
})

// lib/api/endpoints.ts
export const endpoints = {
  auth: {
    login: '/auth/login',
    register: '/auth/register',
    refresh: '/auth/refresh',
  },
  otp: {
    send: '/otp/send',
    verify: '/otp/verify',
  },
} as const

// features/otp/api/otpApi.ts
export const otpApi = {
  send: (data: SendOTPData) =>
    apiClient.post(endpoints.otp.send, data),

  verify: (data: VerifyOTPData) =>
    apiClient.post(endpoints.otp.verify, data),
}

優點:

  • API 呼叫集中管理
  • 易於 mock 測試
  • 可以輕鬆切換後端
  • 型別安全

4. 環境設定管理

// lib/constants/config.ts
export const config = {
  apiUrl: import.meta.env.VITE_API_URL,
  wsUrl: import.meta.env.VITE_WS_URL,
  environment: import.meta.env.MODE,
  isDevelopment: import.meta.env.DEV,
  isProduction: import.meta.env.PROD,

  features: {
    enableMFA: import.meta.env.VITE_ENABLE_MFA === 'true',
    enableAnalytics: import.meta.env.VITE_ENABLE_ANALYTICS === 'true',
  },
}

// 使用 feature flag
const SettingsPage = () => {
  return (
    <div>
      <h1>設定</h1>

      {config.features.enableMFA && (
        <MFASettings />
      )}

      {config.features.enableAnalytics && (
        <AnalyticsSettings />
      )}
    </div>
  )
}

未來展望

Kyo System 前端後續功能規劃

Q4 2025

  • [ ] 多語系支援 (i18n)
  • [ ] 暗黑模式優化
  • [ ] 進階數據視覺化 (更多圖表類型)
  • [ ] 匯出報表功能 (PDF, Excel)

Q1 2026

  • [ ] 行動端 App (React Native)
  • [ ] 離線模式支援 (PWA)
  • [ ] 即時協作功能
  • [ ] AI 數據分析助手

Q2 2026

  • [ ] Monorepo 優化
  • [ ] 效能監控系統
  • [ ] A/B Testing 平台

前端技術演進方向

1. React 19 與 Server Components

// 未來可能的架構
// app/dashboard/page.tsx (Server Component)
async function DashboardPage() {
  // 在 server 端獲取資料
  const stats = await fetchDashboardStats()

  return (
    <div>
      <h1>儀表板</h1>
      {/* Client Component 處理互動 */}
      <StatsChart data={stats} />
    </div>
  )
}

優點:

  • 減少客戶端 JavaScript
  • 更好的 SEO
  • 更快的初次載入

2. TypeScript 5.x 新特性

// 使用 const type parameters
function processData<const T extends readonly string[]>(data: T) {
  return data
}

const result = processData(['a', 'b', 'c'] as const)
// type: ['a', 'b', 'c']

// 使用 satisfies operator
const config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
} satisfies Config

3. 效能優化技術

Web Vitals 監控:

import { onCLS, onFID, onLCP } from 'web-vitals'

onCLS(console.log)  // Cumulative Layout Shift
onFID(console.log)  // First Input Delay
onLCP(console.log)  // Largest Contentful Paint

// 上傳到分析服務
function sendToAnalytics(metric) {
  fetch('/api/analytics', {
    method: 'POST',
    body: JSON.stringify(metric),
  })
}

Resource Hints:

<!-- DNS Prefetch -->
<link rel="dns-prefetch" href="https://api.example.com">

<!-- Preconnect -->
<link rel="preconnect" href="https://api.example.com">

<!-- Prefetch -->
<link rel="prefetch" href="/dashboard">

<!-- Preload -->
<link rel="preload" href="/fonts/main.woff2" as="font" crossorigin>

挑戰心得

30 天的鐵人賽挑戰終於來到尾聲。

回顧這段旅程,從第一天的專案規劃、技術選型,到最後完成一個可以實際運作的 SaaS 產品前端,這個過程充滿了學習與成長。

雖然我已經有八年的開發經驗,但這次從零開始打造 SaaS 產品的經驗,讓我對前端開發有了更深層的理解。以前在公司工作時,框架和架構都已經建立好了,這次自己從頭設計,才發現有太多細節需要考量:

  • Landing Page 如何設計才能提高轉換率?
  • 認證系統如何在安全與便利之間取得平衡?
  • 如何優化前端效能,讓使用者體驗更好?
  • 如何建立可維護、可擴展的架構?

這些問題在既有專案中可能不會遇到,但在從零開始時,每一個決策都需要深思熟慮。

持續學習的重要性

技術日新月異,前端領域更是如此。React 19、TypeScript 5.x、新的建構工具、新的框架,層出不窮。保持學習的熱情,持續精進自己的技能,是我們避免被AI淘汰的方法。

這次鐵人賽對我來說,不只是一個挑戰,更是一個整理知識、深化理解的過程。透過寫作,我對前端技術有了更系統化的認識,加上自己在前端這條路上走得很淺,也發現了許多之前忽略的細節和需要學習的知識。

最後的話

Kyo System 的前端架構已經成功建立,這套架構不僅可以用在 OTP 服務,未來也可以擴展到工作室的其他 SaaS 產品。這是這次鐵人賽最大的成果。

前端開發是一個既有挑戰又有趣的領域。從設計美觀的介面,到優化使用者體驗,再到打造高效能的應用,每一個環節都值得深入研究。


上一篇
Day 29: 30天打造SaaS產品前端篇-完整系統整合與端到端測試
系列文
30 天製作工作室 SaaS 產品 (前端篇)30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言