摘要
在數位時代,開發高效能的網頁應用程式對於企業與個人開發者而言愈加重要。本文將介紹如何使用 React Native Web 打造出色的網頁應用體驗。 歸納要點:
- 深入分析 React Native Web 的性能優化,透過程式碼分割和減少 DOM 操作等技巧提升效能。
- 探討 React Native Web 的架構與可擴展性,並與 Next.js 和 Gatsby 等框架的整合方式進行比較。
- 提供跨平台測試策略及最佳實務,幫助開發者有效管理程式碼與維護網頁應用程式。
我們在研究許多文章後,彙整重點如下
網路文章觀點與我們總結
- 許多人在生活中面臨壓力和焦慮,這是普遍的現象。
- 學會管理情緒對於提高生活品質至關重要。
- 簡單的冥想或深呼吸技巧可以幫助減輕緊張感。
- 建立良好的社交支持系統有助於應對困難時期。
- 定期運動不僅能改善身體健康,還能提升心情。
- 尋求專業心理諮詢是一種勇敢且有效的解決方式。
在忙碌而充滿挑戰的生活中,我們每個人都可能會遭遇壓力與焦慮,這讓人覺得孤單和無助。透過一些簡單的方法如冥想、運動,以及與朋友或家人的交流,我們可以更好地管理自己的情緒。此外,不要害怕尋求專業幫助,這反而是一種智慧的表現。我們並不需要獨自面對這些挑戰,有時候分享與傾訴便能讓我們感到釋放與安慰。
觀點延伸比較:管理情緒方法 | 有效性 | 最新趨勢 | 權威觀點 |
---|---|---|---|
冥想 | 高 | 使用專業應用程式(如 Headspace, Calm)幫助引導冥想 | 研究顯示每日10分鐘的冥想可顯著減少焦慮 |
深呼吸技巧 | 中等 | 結合瑜伽和呼吸練習效果更佳 | 專家建議使用4-7-8呼吸法來緩解壓力 |
社交支持系統 | 高 | 線上社群與實體聚會並行,擴大支持網絡的可能性 | 心理學家強調人際關係對心理健康的重要性 |
定期運動 | 高 | 科技健身器材及APP越來越受歡迎,如Peloton、Nike Training Club,提供個性化訓練計劃 | 運動醫學報告指出,即使是短時間的運動也能立即改善心情 |
尋求專業心理諮詢 | 高 | 隨著遠距療法興起,更多人選擇線上諮詢服務,如Talkspace、BetterHelp | 許多臨床心理師推薦在困難時期主動尋求幫助 |
React Native 一直以來被用來建立網頁應用程式,而 Meta、Twitter (X) 和 Flipkart 則是運用這一方法的企業範例。對於我們的案例研究而言,理解其他開發者可能面臨的背景至關重要。客戶已經擁有一個基於 React Native 的 Android 和 iOS 應用程式,他們希望將其產品以 Telegram Web App 格式再建立一個版本。我們之前曾在另一個應用程式上進行過類似的專案,但從未發布過。這段經驗成為我們此次開發工作的基礎。
React Native for Web:跨平台網頁應用開發的潛力和挑戰
這裡有一個簡單的提醒:Telegram 網頁應用程式是一個在其獨立網頁檢視中執行的網頁應用。你可以使用 React 建構它,並與 Tamagui 分享樣式和導航。移動應用已經完全使用 React Native 開發。為了避免從頭編寫程式碼,我們決定使用 react-native-web。文件將 React Native for Web 描述為一種相容層,它介於 React DOM 和 React Native 之間,可以在新的及現有的網頁和多平台應用程式中使用。簡而言之,這是一個庫,使你能夠將 React Native 的程式碼作為網頁應用執行。在這裡了解更多。
由於技術原因,我們無法向你展示啟發此次實驗的應用程式。因此,在本文中,我們將撰寫一個簡單的點選遊戲,探索不同的樣式、使用觸覺反饋、收集個人資料以及運用主題。我們的程式碼版本可能與文件有所不同,因為我們在精確遵循時遇到了一些問題。
**專案1具體說明:** React Native for Web 的未來潛力與挑戰 - 因為 React Native for Web 致力於提供跨平台開發方案,它在網頁應用開發中的效能、可擴充套件性及安全性,特別是與原生網路技術相比,一直是業界關注的焦點。值得深入探討的是,React Native for Web 在處理複雜 UI、動畫及效能最佳化方面的表現,以及它如何與其他框架和庫整合。隨著 WebAssembly 的進步,React Native for Web 面臨著新的競爭壓力,需要探索基於 WebAssembly 的跨平台開發解決方案,以維持其市場優勢。
**專案2具體說明:** Tamagui 與 React Native for Web 的結合 - 作為一個基於 React Native 的 UI 框架,Tamagui 在效能最佳化、動態樣式和可擴充套件性上顯示出其優勢。值得探討的是 Tamagui 如何與 React Native for Web 結合,在網頁應用開發中實現與原生應用同樣高效能和良好使用者體驗,以及如何透過 Tamagui 的強項來簡化複雜的網頁應用開發過程。Tamagui 與 React Native for Web 結合帶來的新挑戰也需深入研究,例如如何有效地處理不同平台間差異,以及如何確保靈活性以促進高品質的網頁應用開發。
要開始使用,您需要一個 React Native 應用程式。在您想建立應用的目錄中,執行以下命令:
npx react-native init react-native-web-example
Test React Native Web
我們還需要建立 index.web.js(您可以從 script 標籤中看到它)。請在專案的根目錄下於 index.js 層級建立這個檔案,並將以下程式碼放置在其中:index.js
import { AppRegistry } from "react-native"; import name from "./app.json"; import App from "./App"; import { enableExperimentalWebImplementation } from "react-native-gesture-handler"; enableExperimentalWebImplementation(true); AppRegistry.registerComponent(name, () => App); AppRegistry.runApplication(name, { initialProps: {}, rootTag: document.getElementById("app-root"), });
React Native Web 的設定與解決方案
基本上,這裡發生的事情與 index.js 中相同,不過除了註冊元件外,我們還找到了一個 id 為 ′app-root′ 的 div,並在其中渲染應用程式。至於 enableExperimentalWebImplementation(true) - 這不是程式碼的必要部分。在開發過程中,我們在使用 ′react-native-gesture-handler′ 時遇到了問題。這一附加選項幫助我們解決了該問題。接下來,你需要一個構建工具,因為 Metro 在這種情況下無法提供幫助。在 react-native-web 網頁上有一個示範 webpack 配置,你也可以透過安裝 react-native-reanimated 獲得它(我們會在所有包含 React Native 程式碼的專案中安裝它)。但對我們來說那並不奏效。我們使用了 Vite 和針對 react-native-web 的外掛 - vite-plugin-react-native-web 作為構建工具。接下來,在專案根目錄建立 vite.config.js 並新增以下程式碼:
// vite.config.js import reactNativeWeb from "vite-plugin-react-native-web"; import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import commonjs from "vite-plugin-commonjs"; export default defineConfig({ commonjsOptions: { transformMixedEsModules: true }, plugins: [ reactNativeWeb(), react({ babel: { plugins: [ "react-native-reanimated/plugin", "@babel/plugin-proposal-class-properties", "@babel/plugin-proposal-export-namespace-from", ], }, }), commonjs(), ], });
我忘了提到,以下這些套件需要在此之前安裝: vite 、 @vitejs/plugin-react 、 vite-plugin-commonjs 、 vite-plugin-react-native-web 以及 babel-plugin-react-native-web。如果您也想使用 react-native-reanimated ,則還需要新增這些套件: react-native-reanimated 和 @babel/plugin-proposal-export-namespace-from。您的 babel.config.js 應該會如下所示: babel.config.js
module.exports = { presets: ["module:@react-native/babel-preset"], plugins: [ "@babel/plugin-proposal-export-namespace-from", "react-native-reanimated/plugin", ], };
我們使用了來自一個純粹的 React Native 專案的 babel.config.js,版本為 0.74.5,並添加了一些外掛。如果您正在使用不同版本的 React Native,請僅參考這些外掛。接下來,將以下命令新增至 package.json 中的 scripts 部分。
{ ... "scripts": { ... "web:dev": "vite dev", "web:build": "vite build", "web:preview": "vite build && vite preview" ... } ... }
現在我們執行應用程式,檢查一切是否正常運作。在我們的 App.tsx 中,目前只有文字。因此我們得到了以下結果:
克服跨平台開發挑戰,讓 Telegram 應用程式在 React Native 上完美執行
我們也在 iOS 和 Android 上測試這個應用,以確保一切運作正常。然後,我們開始撰寫應用程式。我們已經有了應用的基礎,但它相當簡單。我們希望為 Telegram 帶來更有趣的功能,因為這個即時通訊軟體擁有許多與客戶互動的可能性和功能,但需要訪問許可權。根據文件,這可以透過全域物件 window 來實現,然後使用 window.Telegram.WebApp。與 React.js 應用不同,在 React Native 中我們並沒有這樣的物件(window)。當我們嘗試訪問它時,TypeScript 會報錯。在使用 react-native-web 開發網頁應用時,可以獲得對 window 的訪問。接下來的一部分雖然不太理想,但為了方便,我們需要手動指定型別並宣告全域物件 window。在專案的根目錄中建立 global.d.ts 檔案,並寫入以下內容:
1. 跨平台挑戰:突破 React Native 的限制
在開發 Telegram 應用時,我們面臨了一個典型問題:缺乏 window 物件使得無法存取 Telegram 的 WebApp 功能。這是許多開發者在進行跨平台應用開發時常遇到的挑戰。
2. 解決方案:
作者提出了一個巧妙的方法,即利用 react-native-web 和手動宣告 global.d.ts 檔案來解決此問題,顯示出他對 React Native 和 TypeScript 的深刻理解。
3. 進階思考:
此情況引發更深層次的思考——在跨平台開發中如何有效處理不同平台之間的差異,以及如何平衡原生功能和跨平台效率。
讓我們首先在 head 標籤中新增一段指令碼,以將我們的迷你應用連線到 Telegram 客戶端。
... ...
global.d.ts} 的內容翻譯如下:
```typescript
// 這個檔案定義了全域性的 TypeScript 型別,提供了一些常用的型別宣告。
declare global {
interface Window {
// 設定一個名為 ′myApp′ 的全域性變數,其型別為任何。
myApp: any;
}
// 定義一個 ′fetchData′ 函式,返回 Promise 型別的資料。
function fetchData(url: string): Promise<any>;
}
// 將此檔案標記為模組,以便 TypeScript 能夠正確解析它。
export {
type TelegramTheme = { bg_color: string; text_color: string; hint_color: string; link_color: string; button_color: string; button_text_color: string; secondary_bg_color: string; header_bg_color: string; accent_text_color: string; section_bg_color: string; section_header_text_color: string; section_separator_color: string; subtitle_text_color: string; destructive_text_color: string; }; type WebAppUser = { id: number; is_bot: boolean; first_name: string; last_name: string; username: string; is_premium: boolean; photo_url: string; }; type WebappData = { user: WebAppUser; }; type TelegramHapticFeedback = { impactOccurred: ( style: "light" | "medium" | "rigid" | "heavy" | "soft", ) => void; notificationOccurred: (type: "error" | "success" | "warning") => void; }; type TelegramWebapp = { initData: string; initDataUnsafe: WebappData; version: string; platform: string; themeParams: TelegramTheme; headerColor: string; backgroundColor: string; expand: () => void; close: () => void; HapticFeedback: TelegramHapticFeedback; }; type Window = { Telegram?: { WebApp: TelegramWebapp; }; }; declare var window: Window;
在這個檔案中,我們已經指定了從 Telegram.WebApp 獲取的必要資料型別,並聲明瞭一個全域的 window 物件。但請記住,我們也在編寫一個移動應用程式。因此,我們不會直接使用 window 物件,以避免出錯。相反,我們將建立一個全域的 TelegramConfig 物件,在其中寫入所有來自 Telegram 的資料。對於移動部分,我們將建立一個 MockConfig,讓我們自行輸入所有資料。由於我們不會收到任何來自 Telegram 的資料,這些配置將是靜態的。請建立一個 src/config.ts 檔案並編寫:config.ts
import { Platform } from "react-native"; export const MockConfig = { themeParams: { bg_color: "#000", secondary_bg_color: "#1f1f1f", section_bg_color: "#000", section_separator_color: "#8b8b8b", header_bg_color: "#2c2c2c", text_color: "#fff", hint_color: "#949494", link_color: "", button_color: "#358ffe", button_text_color: "", accent_text_color: "#0f75f1", section_header_text_color: "", subtitle_text_color: "", destructive_text_color: "", }, initDataUnsafe: { user: { username: "MockUser", is_premium: false, photo_url: "", first_name: "", last_name: "", id: 0, }, }, } as TelegramWebapp; export const config = () => { if (Platform.OS !== "web") { return MockConfig; } if (window.Telegram?.WebApp.initData) { return window.Telegram?.WebApp; } else { return MockConfig; } };
以 MockConfig 建立彈性資料獲取機制,並展示 Telegram 客戶端功能
在這裡,我們為我們的移動或網頁應用建立一個 MockConfig,以防 Telegram 客戶端沒有資料。接下來,將撰寫一個配置函式,如果 Telegram 的資料可用,則返回該資料;否則返回 MockConfig。我們將利用這一點來獲取資料。現在讓我們使用配置/tg 中的主題設定和使用者資料編寫一個簡單的點選器。要明確的是,我們在本文中並不試圖建立一個複雜的應用程式。更重要的是向您展示如何無障礙地使用 Telegram 客戶端及其功能/選項。
讓我們執行以下命令,以便在應用程式內部放置導航庫。雖然這對於本範例並非必要,但我們還是為您準備好了 🫶
pnpm add @react-navigation/native-stack @react-navigation/native react-native-screens
我們還將新增動畫套件:
pnpm add react-native-reanimated react-native-gesture-handler
讓我們進行以下設定以連線動畫。前往 babel.config.js:babel.config.js
module.exports = { presets: ["module:@react-native/babel-preset"], // add plugins here plugins: [ "@babel/plugin-proposal-export-namespace-from", "react-native-reanimated/plugin", ], };
為 iOS 安裝 pods:確保您已經安裝了 CocoaPods。可以透過在終端中執行以下命令來進行安裝:
```bash
sudo gem install cocoapods
```
接下來,導航到您的 Xcode 專案目錄並初始化 CocoaPods。在終端中輸入:
```bash
cd /path/to/your/project
pod init
```
這將會建立一個名為 `Podfile` 的檔案。開啟該檔案並新增您需要的 pods,例如:
```ruby
platform :ios, ′10.0′
use_frameworks!
target ′YourApp′ do
pod ′Alamofire′, ′ ̄> 5.4′
pod ′SwiftyJSON′, ′ ̄> 5.0′
end
```
儲存更改後,返回終端並執行以下命令以安裝 pods:
```bash
pod install
```
完成後,您應該會看到一條成功資訊。請注意,以後都要使用 `.xcworkspace` 檔案來開啟您的專案,而不是原始的 `.xcodeproj` 檔案。這樣才能正確地使用所安裝的庫和框架。
cd ios && pod install && cd ..
接下來,建立以下資料夾:src、src/components、src/screens,以及其中的檔案:src/RootNavigator.tsx、src/screens/HomeScreen.tsx、src/utils.ts、src/components/index.ts、src/components/Coin.tsx、src/components/Header.tsx、src/components/Progress.tsx 和 src/components/Screen.tsx。在 src 資料夾內,我們將會有如下結構:— componentsHomeScreen.tsxCoin.tsxProgress.tsxindex.tsHeader.tsx。— screensHomeScreen.tsx。— utils.ts— App.tsx— RootNavigator.tsx。因此,我們已經為各個元件建立了檔案。現在讓我們開始填充這些檔案並建立元件本身。首先從標頭開始:前往 src/components/Header.tsx 目錄中的 Header 檔案。
import { Image, StyleSheet, Text, View } from "react-native"; import { config } from "../../config"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import React from "react"; type HeaderProps = { amount: number; }; export const Header: React.FC = ({ amount }) => { const insets = useSafeAreaInsets(); const paddingTop = Math.max(20, insets.top); const { username, photo_url } = config().initDataUnsafe.user; return ( {amount} @{username} {photo_url ? ( ) : ( )} ); }; const styles = StyleSheet.create({ header: { backgroundColor: config().themeParams?.header_bg_color, paddingHorizontal: 20, flexDirection: "row", alignItems: "center", paddingBottom: 20, justifyContent: "space-between", }, amountRow: { flexDirection: "row", alignItems: "center", }, text: { fontSize: 24, fontWeight: "600", color: config().themeParams?.text_color, }, userInfo: { flexDirection: "row", alignItems: "center", gap: 20, }, username: { color: config().themeParams.accent_text_color, fontSize: 18, }, image: { backgroundColor: config().themeParams.button_color, height: 50, width: 50, justifyContent: "center", alignItems: "center", borderRadius: 50, }, icon: { height: 30, width: 30, tintColor: config().themeParams.text_color, }, });
如上面的範例所示,我們使用 config().themeParams 方法來獲取標頭顏色的主題設定,以及使用者資訊,例如使用者名稱和 photo_url。不過,如文件中所述,photo_url 可能不存在,因此我們需要新增一個檢查來確認它是否存在(在這種情況下,我們將輸出一個佔位符)。在專案的根目錄中建立一個 assets/icons 資料夾,用以儲存我們的圖片。在這個應用程式中,我們只需要兩張圖片:一張是使用者照片的佔位符,以及另一張是我們將會點選的硬幣影象。
現在我們已經完成了標頭的部分,接下來讓我們進入下一個元件:硬幣本身。僅僅插入一張圖片並點選它並不有趣。因此,我們將新增一些動畫效果:數字出現和消失的動畫,這將相當簡單,以及硬幣旋轉的動畫。src/components/Coin.tsx
import React, { useState } from "react"; import { Dimensions, GestureResponderEvent, Image, Platform, Pressable, StyleSheet, Text, View, } from "react-native"; import Animated, { SlideOutUp, useAnimatedStyle, useSharedValue, withTiming, } from "react-native-reanimated"; import { generateUuid } from "../utils"; import { config } from "../../config"; import { useHaptics } from "../useHaptics"; import { ImpactFeedbackStyle } from "expo-haptics"; //animated component to have ability use animated style from Reanimated package const AnimatedButton = Animated.createAnimatedComponent(Pressable); const sensitivity = Platform.OS == "web" ? 0.1 : 0.2; const animationConfig = { duration: 100, }; /** * @prop onClick - what happened on click the coin * @prop disabled - when coin can be clicked or not */ type CoinProps = { onClick: () => void; disabled?: boolean; }; export const Coin: React.FC = ({ disabled, onClick }) => { const [number, setNumber] = useState< { id: string; x: number; y: number } | undefined >(undefined); const [showNumber, setShowNumber] = useState(false); const width = Dimensions.get("window").width - 50; //setting coin size based on window and check web compatibility const size = width > 1000 ? 1000 : width; const center = size / 2; //shared values to use in coin animation const rotateX = useSharedValue(0); const rotateY = useSharedValue(0); const { impactOccurred } = useHaptics(); const handlePressIn = async (e: GestureResponderEvent) => { await impactOccurred(ImpactFeedbackStyle.Light); const { locationX, locationY } = e.nativeEvent; //getting rotate amount by x axis const deltaX = locationX - center; //getting rotate amount by y axis const deltaY = locationY - center; if (Platform.OS === "web") { rotateY.value = deltaX * sensitivity; rotateX.value = -deltaY * sensitivity; } else { rotateY.value = withTiming(deltaX * sensitivity, animationConfig); rotateX.value = withTiming(-deltaY * sensitivity, animationConfig); } //set number position && unique id to have no problems with keys setNumber({ id: generateUuid(), x: locationX, y: locationY }); }; const handlePressOut = (e: GestureResponderEvent) => { setShowNumber(true); if (Platform.OS === "web") { rotateX.value = 0; rotateY.value = 0; } else { rotateX.value = withTiming(0, animationConfig); rotateY.value = withTiming(0, animationConfig); } onClick(); // use timeout to not remove element on render start setTimeout(() => { //set values undefined to launch exiting animation setNumber(undefined); setShowNumber(false); }, 10); }; //style to define coin rotation const rotateStyle = useAnimatedStyle( () => ({ position: "relative", transform: [ { rotateY: `${rotateY.value}deg`, }, { rotateX: `${rotateX.value}deg`, }, ], }), [rotateX, rotateY], ); return ( {!!number && showNumber && ( +1 )} ); }; const styles = StyleSheet.create({ container: { position: "relative", }, text: { fontSize: 26, fontWeight: "600", //getting text color from Telegram client color: config().themeParams?.hint_color, }, });
React Native 動畫:打造生動的硬幣翻轉體驗
所有動畫會播放兩次——當你按下硬幣時和放開時。我們來更詳細地分析這段程式碼。建立 `rotateX` 和 `rotateY`(SharedValue)以及 `rotateStyle`(AnimatedStyle)。在 `rotateStyle` 中,我們觀察 SharedValue 的變化,並將其改變以符合我們點選硬幣的位置。接著,我們把動畫樣式本身傳遞給 AnimatedButton,這是使用 createAnimatedComponent 函式及其 Pressable 引數所獲得的。根據 `rotateX` 和 `rotateY` 的值,硬幣會向一側或另一側傾斜。當按鈕被按下時,X 軸和 Y 軸的旋轉角度會發生改變。在此之後,我們獲取觸控發生位置的坐標。我們將這些坐標減去元素的中心點,以找到增量delta。然後,我們將該增量乘以一個靈敏度值(可介於 0 到 1 之間),從而得到 X 軸和 Y 軸的傾斜角度。我們將 X 與 Y 的壓力值寫入一個數字,以便用於顯示飛翔的數字。
這段程式碼展示瞭如何使用 React Native 的 Animated API 來實現精緻的硬幣翻轉動畫,並根據觸控位置進行靈活調整。它利用 `SharedValue` 和 `AnimatedStyle` 建立平滑且可控的動畫效果。我們也可以進一步探討其應用與效能最佳化潛力:
* **創新應用:**
* 除了硬幣翻轉,此技術還可以應用於其他互動式元素,例如卡片翻轉、立體影象旋轉,以及遊戲中的角色動作。
* 藉由結合其他動畫技巧,如 `spring` 和 `timing`,可以實現更豐富的動畫體驗,例如讓硬幣彈跳、旋轉並最終落地。
* **效能最佳化:**
* 對於頻繁觸控互動,可以考慮使用 `useNativeDriver` 選項來提升動畫執行效率。
* 利用 `Animated.parallel` 或 `Animated.sequence` 串聯多個動畫,提高複雜動畫流程管理效率。
透過以上探討,不僅強調了基礎技術細節,也對未來可能的新穎應用和演進趨勢提供了深刻見解,使讀者對這些技術有更全面理解與期許。
釋放按鈕時的動畫邏輯與平台差異
上述所有動作都是按下按鈕時撰寫邏輯所必需的。我們的程式碼還需要涵蓋按鈕釋放時發生的事情。我們採取的第一步是將動畫值重新設定為 0,以便硬幣回到其原始位置。值得注意的是,不同平台對於按鈕被點選和釋放有不同的條件。React Native Reanimated 在網頁上的動畫可能會出現問題,因此在某些情況下,它們可能需要重新渲染。這需要額外的驗證,因為我們使用了 withTiming 元件(它確保值不會在當前時刻變化,而是在 animationConfig 中指定的時間進行變化)。在 React Native 中實作動畫效果,讓數字上升並呈現硬幣翻轉
接下來,我們呼叫 onClick 方法,並將其傳遞至 props,以便在點選時執行相應的操作。在 `setTimeout` 中(這是必要的,以確保元素能夠及時顯示數字),我們移除 number 和 showNumber 的值,以觸發動畫,當該元素離開 DOM 樹時。由於我們已經提到過圖形的動畫,因此將使用一個簡單的 `Animated.View` 及其屬性 - exiting,用以實現從 Reanimated 庫中渲染器退出時的動畫效果。當圖形退出時,我們現在擁有了一個展示硬幣上升的動畫。我們還將數字的 x 和 y 值傳入樣式中,以便將其放置在按鈕被點選的位置。現在讓我們繼續進入 Progress.tsx 檔案 src/components/Progress.tsx。import React, { useState } from "react"; import { StyleSheet, Text, View } from "react-native"; import { config } from "../../config"; type ProgressProps = { max?: number; amount: number; }; export const Progress: React.FC = ({ max = 3500, amount }) => { const [width, setWidth] = useState(0); return ( setWidth(e.nativeEvent.layout.width)}> {amount} / {max} {amount} / {max} ) ; }; const styles = StyleSheet.create({ container: { height: 70, borderColor: config().themeParams.accent_text_color, backgroundColor: config().themeParams.section_bg_color, borderWidth: 2, borderRadius: 70, overflow: "hidden", position: "relative", justifyContent: "center", }, progress: { height: "100%", backgroundColor: config().themeParams.accent_text_color, width: 200, borderRadius: 20, position: "absolute", justifyContent: "center", overflow: "hidden", }, text: { fontWeight: "700", fontSize: 24, color: config().themeParams.accent_text_color, textAlign: "center", }, progressText: { textAlign: "center", color: config().themeParams.text_color, }, });
這部分並不複雜。我們只是透過 props 傳遞最大值和當前值(數量)。隨著數量的增加,進度值也會相應提升。我們還使用配置中的顏色,這些顏色可以從引數中獲得,或是來自使用者的 Telegram 主題設定。現在讓我們來建立一個簡單的 Screen.src/components/Screen.tsx。
import React from "react"; import { StyleSheet, View, ViewProps } from "react-native"; import { config } from "../../config"; const styles = StyleSheet.create({ screen: { flex: 1, backgroundColor: config().themeParams?.secondary_bg_color, }, }); export const Screen: React.FC = ({ children, style }) => { return {children}; };
一切都在我們的 HomeScreen:src/screens/HomeScreen.tsx 中匯聚。
import { StatusBar, StyleSheet, View } from "react-native"; import { Coin, Header, Progress, Screen } from "../components"; import { config } from "../../config"; import { useState } from "react"; import { useSafeAreaInsets } from "react-native-safe-area-context"; const MAX_CLICK_AMOUNT = 3500; export const HomeScreen = () => { //total amount of coins const [amount, setAmount] = useState(0); //amount of clicks const [clickedAmount, setClickedAmount] = useState(0); const insets = useSafeAreaInsets(); const paddingBottom = Math.max(20, insets.bottom); //what happened when we press coin const handleClick = () => { setAmount(prev => prev + 1); setClickedAmount(prev => prev + 1); }; return ( <> = MAX_CLICK_AMOUNT} onClick={handleClick}> <> ); }; const styles = StyleSheet.create({ screen: { flex: 1, gap: 20, }, coin: { flex: 1, backgroundColor: config().themeParams.bg_color, alignItems: "center", justifyContent: "center", }, footer: { padding: 20, }, });
再次強調,這一切都相當簡單。唯一可能引起一些疑問的地方是 clickedAmount 和 amount。這兩個值基本上是一樣的,那麼為什麼我們需要它們呢?答案如下:amount - 所有使用者互動的數量;clickedAmount - 使用者點選按鈕的次數。amount 需要儲存在某處,而 clickedAmount 應該隨著時間重置,因為我們會給使用者更多的點選機會。我們尚未規定這項功能,因此如果你願意,可以自行嘗試實驗。現在讓我們把所有這些放入 RootNavigator,並將導航器本身放進 App.tsx 中。src/RootNavigator.tsx
import { createNativeStackNavigator } from "@react-navigation/native-stack"; import { HomeScreen } from "./screens/HomeScreen"; const RootStack = createNativeStackNavigator(); export const RootNavigator = () => { return ( ); };
// 在這段程式碼中,我們將使用 React 來建立一個簡單的應用程式。
import React, { useState } from ′react′;
function App() {
const [count, setCount] = useState(0); // 使用 useState 鉤子來管理計數器狀態
return (
<div>
<h1>計數器</h1>
<p>當前計數: {count}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
<button onClick={() => setCount(count - 1)}>減少</button>
</div>
);
}
export default App;
import React, { useEffect } from "react"; import { SafeAreaProvider } from "react-native-safe-area-context"; import { NavigationContainer } from "@react-navigation/native"; import { RootNavigator } from "./RootNavigator"; import { config } from "../config"; export default function App() { useEffect(() => { config().expand(); }, []); return ( ); }
在 App.tsx 中,我們在 useEffect 中呼叫 expand 方法。這個方法用於在 Telegram 上執行時將應用程式擴充套件至全螢幕。請參考連結中的程式碼庫以檢視最終的程式碼樣貌。
探索 Telegram 客戶端的額外功能與觸覺反饋
所以我們有一個具備基本功能的常規點選器。我想指出的是,在這個例子中,我們是從 `initDataUnsafe` 獲取使用者資料的。這並不是最佳解決方案,因為根據文件說明,最好是讓 `initData` 失敗並使用來自 Telegram-bot 的 `ApiKey`。不過,由於我們的例子僅僅是一個示範,因此這種選擇已經足夠了。同時,很明顯在移動應用中使用使用者模擬(moc)並不是一個好主意。將身份驗證處理和顯示分開進行,或者從客戶賬號登入,是更合理的做法。總體來說,對此問題可以長篇大論,但我們留給你自己的想像空間——只需克隆該程式碼庫,自由探索即可。現在讓我們看看 Telegram 客戶端在額外功能方面能提供什麼。為了達到這個目的,我們將使用 WebApp 庫中的觸覺反饋(Haptic Feedback)。需要注意的是,它在移動應用中無法運作,所以我們會採取以下措施。讓我們從 Haptic 函式庫開始。我們使用 expo-haptics,因為它的引數大致與 Telegram 的 HapticFeedback 相似。由於我們的專案是用純 React Native 撰寫,因此我們會先安裝 expo,然後再安裝 expo-haptics:pnpx install-expo-modules@latest pnpx expo install expo-haptics cd ios && pod install。接下來,我們來撰寫一個 hook 作為包裝器:src/useHaptics.ts
import { useEffect, useState } from "react"; import { Platform } from "react-native"; import { impactAsync, notificationAsync, NotificationFeedbackType, ImpactFeedbackStyle, } from "expo-haptics"; type Haptics = { impactOccurred: (style: ImpactFeedbackStyle) => Promise; notificationOccurred: (type: NotificationFeedbackType) => Promise; }; export const useHaptics = () => { const [haptics, setHaptics] = useState({ impactOccurred: async _ => {}, notificationOccurred: async _ => {}, }); useEffect(() => { if (Platform.OS == "web") { if (window.Telegram?.WebApp.HapticFeedback) { setHaptics(window.Telegram.WebApp.HapticFeedback); return; } } const impact = async (style: ImpactFeedbackStyle) => await impactAsync(style); const notification = async (type: NotificationFeedbackType) => await notificationAsync(type); setHaptics({ impactOccurred: impact, notificationOccurred: notification }); }, []); return haptics; };
這樣一來,我們就可以在 Telegram 迷你應用程式和我們的常規應用中使用 HapticFeedback。剩下的唯一工作就是在點選硬幣時新增觸覺反饋,就這樣。至於觸覺反饋,你也可以嘗試建立一個資料庫來儲存結果,但這部分已經在你那邊 😉 剩下的唯一任務就是將應用部署到 Telegram 上。不過,這部分我們會在下一篇文章中討論。
參考來源
相關討論