如何構建網頁應用程式:第一部分 - 使用 React Native Web 開發


摘要

在數位時代,開發高效能的網頁應用程式對於企業與個人開發者而言愈加重要。本文將介紹如何使用 React Native Web 打造出色的網頁應用體驗。 歸納要點:

  • 深入分析 React Native Web 的性能優化,透過程式碼分割和減少 DOM 操作等技巧提升效能。
  • 探討 React Native Web 的架構與可擴展性,並與 Next.js 和 Gatsby 等框架的整合方式進行比較。
  • 提供跨平台測試策略及最佳實務,幫助開發者有效管理程式碼與維護網頁應用程式。
總之,掌握 React Native Web 的各項技術要素,不僅能提升應用的性能,也為未來的發展趨勢鋪平道路。

嗨,大家好!意外地,開發團隊再次回來帶來一個新的實驗。這次,我們想與您分享我們使用 React Native 為 iOS、Android、網頁和 Telegram 建立應用程式的經驗。

我們在研究許多文章後,彙整重點如下
網路文章觀點與我們總結
  • 許多人在生活中面臨壓力和焦慮,這是普遍的現象。
  • 學會管理情緒對於提高生活品質至關重要。
  • 簡單的冥想或深呼吸技巧可以幫助減輕緊張感。
  • 建立良好的社交支持系統有助於應對困難時期。
  • 定期運動不僅能改善身體健康,還能提升心情。
  • 尋求專業心理諮詢是一種勇敢且有效的解決方式。

在忙碌而充滿挑戰的生活中,我們每個人都可能會遭遇壓力與焦慮,這讓人覺得孤單和無助。透過一些簡單的方法如冥想、運動,以及與朋友或家人的交流,我們可以更好地管理自己的情緒。此外,不要害怕尋求專業幫助,這反而是一種智慧的表現。我們並不需要獨自面對這些挑戰,有時候分享與傾訴便能讓我們感到釋放與安慰。

觀點延伸比較:
管理情緒方法有效性最新趨勢權威觀點
冥想使用專業應用程式(如 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 上。不過,這部分我們會在下一篇文章中討論。

參考來源


MD

專家

相關討論

❖ 相關專欄