經過前面的努力,目前手機 APP (React Native + Expo) 已經建立好跑者的個人設定與課表介面,而我們的後端大腦 (Spring Boot + Gemini) 也已經安穩地部署在 GCP Cloud Run 上。
今天,我們要搭建跨越這兩端的橋樑:前後端 API 串接。
我們選擇在使用度高的 Axios 作為 HTTP 請求工具,並透過實體手機上的 Expo Go 進行真機測試。
在請 AI 幫忙寫串接程式碼之前,有兩個開發 React Native 專屬的網路觀念必須先掌握:
以前寫 Web 前端呼叫不同網域的 API 時,最怕遇到紅字 CORS policy blocked。但在 React Native 開發原生 APP 時,網路請求是直接走手機原生的網路層(iOS 的 NSURLSession / Android 的 OkHttp),完全沒有瀏覽器的安全沙盒限制,因此不會有 CORS 問題!你可以大方地直接呼叫雲端 HTTPS 網址。
如果今天沒有把後端推上 Cloud Run,而是讓 Spring Boot 跑在電腦本機 (localhost:8080)。請特別注意:當你用實體手機開啟 Expo Go 時,APP 裡的 localhost 指的是「手機自己」,而不是你的電腦! 如果要連到電腦,必須確保兩者在同一個 Wi-Fi 下,並將網址改為電腦的區域網路 IP(例如 http://192.168.1.100:8080)。不過我們已經把後端部署到 GCP Cloud Run 了!直接填上真實的 HTTPS 網址,可以避開區域網路與 IP 切換的大坑。
apiService.ts)在終端機安裝 Axios:
npm install axios
與其在每個元件裡面散落著 axios.post(...),好的實務做法是建立一個獨立的 apiService.ts。因此我下了以下Prompt:
「我正在使用 Expo 開發 React Native APP。請幫我用 axios 封裝一個用來呼叫 Cloud Run AI 課表 > API 的非同步函式
generateMarathonPlan。
需求:
- 設定 Base URL 支援環境變數注入(預設為我的 Cloud Run 網址)。
- 設定 30 秒的 Request Timeout(讓雲端 Gemini 有足夠時間運算)。
- 包含完善的 TypeScript 型別定義與 try-catch 錯誤處理。」
產出了的 API 封裝層 src/services/apiService.ts:
import axios from 'axios';
// 隱藏實際網址,建議透過環境變數 EXPO_PUBLIC_API_URL 注入
export const BASE_URL = process.env.EXPO_PUBLIC_API_URL || 'https://<YOUR-CLOUD-RUN-URL>';
const apiClient = axios.create({
baseURL: BASE_URL,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
});
export const generateMarathonPlan = async (payload) => {
try {
const response = await apiClient.post('/generate', payload);
return response.data;
} catch (error) {
console.error('API 呼叫失敗:', error.response?.data || error.message);
throw new Error(error.response?.data?.message || '無法喚醒 AI 教練,請檢查網路連線。');
}
};
滿懷期待地在實體手機 Expo Go 上發出請求後,手機畫面卻彈出了非預期的錯誤訊息:
這個錯誤訊息很眼熟——正是我們在 Day 11 後端設計的 AI Guardrail(領域防護網)!為什麼我們傳入賽事資料還會被擋下來?
把錯誤訊息交給 AI 進行跨端比對:
「我在手機 Expo Go 點選課表產生按鈕 時觸發了
DOMAIN_NOT_SUPPORTED錯誤。 這是後端回傳的訊息:『教練我的眼裡只有東京馬拉松和跑鞋!問這什麼無關的問題,請出門左轉去找別人!』 請幫我查看是哪邊錯誤導致!」
比對了後端的 GenerateScheduleRequest.java 與 Prompt 範本:
targetRace(目標賽事)與 currentLevel(跑者當前體能)。height、weight 等欄位,後端收不到 currentLevel,導致填入 Prompt 範本時「學員體能」呈現空白。Gemini 判定學員資訊不足以規劃安全訓練,因此直接回傳 isRelevant: false 拒絕回答!找到原因後,立即將 Payload 組合成符合後端需求的格式:
const payload = {
targetRace: "東京馬拉松 全程馬拉松",
currentLevel: `跑者 ${profile.nickname},身高 ${profile.height} cm,體重 ${profile.weight} kg,目前每週可訓練 ${weeklyDays} 天,具備基礎體能與慢跑習慣。`
};
調整完後,再次透過 Expo Go 點擊發送——成功通過驗證!後端 Gemini 順利回傳了完整的 JSON 結構化課表!
在測試過程中,發現原本「每次儲存個人檔案都要重新呼叫一次 AI 課表」的體驗不太合理。因此 AI 進行架構重構**:**
「目前個人檔案頁面在點擊儲存時會直接發送 AI 請求,這不符合真實 App 的使用體驗。請幫我重構成兩階段流程:
- 個人檔案頁:專注管理跑者的暱稱、身高、體重與即時 BMI 計算,點擊儲存立即存入本地狀態(Zustand),不發送 AI 請求。
- 課表頁:提供賽事快速標籤(預設東京馬拉松系列避免打錯)與每週天數點選。點擊生成時才整合 Profile 數據呼叫 Cloud Run API,並加入
ActivityIndicator讀取動畫與disabled防連點機制。」
AI 重構了兩個頁面,不僅讓職責更加分明,在 Expo Go 上的操作流暢度也大幅提升!完成後如下:
跑者基本檔案:
課表產生:
當看著實體手機 Expo Go 上成功渲染出 AI 產出的 4 週課表時,其實也發現到 AI 課表上的邏輯硬傷:
這正是軟體開發與 AI 結合有趣的地方,並且也能夠考驗對產品的 Know How 程度!雖然我不是非常專業的跑步教練,但透過網路上的前輩分享,了解到具體的訓練計劃究竟是如何,才對此次 AI 的生成產生疑問 ——「串通 API 只是打通技術管道,如何透過專業領域知識調教出真正具備運動科學深度的 Prompt,才是產品的核心價值。」(這部分我們可到後續章節再來調教 AI 大腦!)
今天透過人機協作完成了幾項任務:
目前我們已經在 Expo Go 成功拿到了這包珍貴的課表資料。明天(Day 18)將正式深入 Zustand 全域狀態管理與持久化儲存,教大家如何把這份雲端課表化為手機的全域記憶,讓各個分頁都能隨時存取!敬請期待!