This documentation is available as Markdown for AI agents and LLMs. See the full Markdown index or append .md to any documentation URL.

处理平台差异

在本教程中,了解在创建通用应用时如何处理原生和 Web 之间的平台差异。


Android、iOS 和网页的功能不同。在我们的情况中,Android 和 iOS 都可以使用 react-native-view-shot 库截取屏幕截图。然而,网页浏览器无法做到这一点。

🌐 Android, iOS, and the web have different capabilities. In our case, both Android and iOS can capture a screenshot with the react-native-view-shot library. However, web browsers cannot.

在本章中,我们将学习如何处理 Web 浏览器的屏幕截图,以便我们的应用在所有平台上具有相同的功能。

🌐 In this chapter, we'll learn how to handle capturing screenshots for web browsers so our app has the same functionality on all platforms.

观看:在通用 Expo 应用中处理平台差异
观看:在通用 Expo 应用中处理平台差异

通过使用 dom-to-image 实现平台特定的截图捕捉来处理 Android、iOS 和网页之间的平台差异。


1

安装并导入 dom-to-image

🌐 Install and import dom-to-image

要在网页上截取屏幕截图并将其保存为图片,我们将使用一个名为 dom-to-image 的第三方库。它可以截取任意 DOM 节点的截图,并将其转换为矢量(SVG)或光栅(PNG 或 JPEG)图片。

🌐 To capture a screenshot on the web and save it as an image, we'll use a third-party library called dom-to-image. It takes a screenshot of any DOM node and turns it into a vector (SVG) or raster (PNG or JPEG) image.

停止开发服务器,然后运行以下命令安装该库:

🌐 Stop the development server and run the following command to install the library:

Terminal
- npm install dom-to-image
- yarn add dom-to-image
- pnpm add dom-to-image
- bun add dom-to-image

安装后,确保重启开发服务器,并在终端中按 W 。

2

添加特定平台的代码

🌐 Add platform-specific code

使用 React Native 的 Platform 模块,我们可以实现特定平台的行为。在 src/app/(tabs)/index.tsx 中:

🌐 Using Platform module from React Native, we can implement platform-specific behavior. Inside src/app/(tabs)/index.tsx:

  1. 从 react-native 导入 Platform 模块。
  2. 从 dom-to-image 导入 domtoimage 库。
  3. 更新 onSaveImageAsync() 函数以检查当前平台是否具有 Platform.OS 属性的 'web'。如果是 'web',我们将使用 domtoimage.toJpeg() 方法将当前的 <View> 转换并捕获为 JPEG 图片。否则,我们将继续使用为本地平台添加的相同逻辑。
src/app/(tabs)/index.tsx
import * as ImagePicker from 'expo-image-picker'; import * as MediaLibrary from 'expo-media-library'; import { useEffect, useRef, useState } from 'react'; import { ImageSourcePropType, View, StyleSheet, Platform } from 'react-native'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { captureRef } from 'react-native-view-shot'; import domtoimage from 'dom-to-image'; import Button from '@/components/button'; import ImageViewer from '@/components/image-viewer'; import IconButton from '@/components/icon-button'; import CircleButton from '@/components/circle-button'; import EmojiPicker from '@/components/emoji-picker'; import EmojiList from '@/components/emoji-list'; import EmojiSticker from '@/components/emoji-sticker'; const PlaceholderImage = require('@/assets/images/background-image.png'); export default function Index() { const [selectedImage, setSelectedImage] = useState<string | undefined>(undefined); const [showAppOptions, setShowAppOptions] = useState<boolean>(false); const [isModalVisible, setIsModalVisible] = useState<boolean>(false); const [pickedEmoji, setPickedEmoji] = useState<ImageSourcePropType | undefined>(undefined); const [permissionResponse, requestPermission] = ImagePicker.useMediaLibraryPermissions(); const imageRef = useRef<View>(null); useEffect(() => { if (!permissionResponse?.granted) { requestPermission(); } }, []); const pickImageAsync = async () => { let result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ['images'], allowsEditing: true, quality: 1, }); if (!result.canceled) { setSelectedImage(result.assets[0].uri); setShowAppOptions(true); } else { alert('You did not select any image.'); } }; const onReset = () => { setShowAppOptions(false); }; const onAddSticker = () => { setIsModalVisible(true); }; const onModalClose = () => { setIsModalVisible(false); }; const onSaveImageAsync = async () => { if (Platform.OS !== 'web') { try { const localUri = await captureRef(imageRef, { height: 440, quality: 1, }); await MediaLibrary.saveToLibraryAsync(localUri); if (localUri) { alert('Saved!'); } } catch (e) { console.log(e); } } else { try { const dataUrl = await domtoimage.toJpeg(imageRef.current, { quality: 0.95, width: 320, height: 440, }); let link = document.createElement('a'); link.download = 'sticker-smash.jpeg'; link.href = dataUrl; link.click(); } catch (e) { console.log(e); } } }; return ( <GestureHandlerRootView style={styles.container}> <View style={styles.imageContainer}> <View ref={imageRef} collapsable={false}> <ImageViewer imgSource={PlaceholderImage} selectedImage={selectedImage} /> {pickedEmoji && <EmojiSticker imageSize={40} stickerSource={pickedEmoji} />} </View> </View> {showAppOptions ? ( <View style={styles.optionsContainer}> <View style={styles.optionsRow}> <IconButton icon="refresh" label="Reset" onPress={onReset} /> <CircleButton onPress={onAddSticker} /> <IconButton icon="save-alt" label="Save" onPress={onSaveImageAsync} /> </View> </View> ) : ( <View style={styles.footerContainer}> <Button theme="primary" label="Choose a photo" onPress={pickImageAsync} /> <Button label="Use this photo" onPress={() => setShowAppOptions(true)} /> </View> )} <EmojiPicker isVisible={isModalVisible} onClose={onModalClose}> <EmojiList onSelect={setPickedEmoji} onCloseModal={onModalClose} /> </EmojiPicker> </GestureHandlerRootView> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#25292e', alignItems: 'center', }, imageContainer: { flex: 1, }, footerContainer: { flex: 1 / 3, alignItems: 'center', }, optionsContainer: { position: 'absolute', bottom: 80, }, optionsRow: { alignItems: 'center', flexDirection: 'row', }, });
修复 dom-to-image TypeScript 模块错误

由于我们使用的是 TypeScript,所以在导入 domtoimage 库之后,我们需要添加类型定义。我们可以通过在项目目录的根目录下创建一个 types.d.ts 文件并添加声明语句来实现这一点:

🌐 We need to add a type definition after importing the domtoimage library since we're using TypeScript. We can do this by creating a file types.d.ts in the root of our project directory and adding the declaration statement:

types.d.ts
declare module 'dom-to-image';

在网页浏览器中运行该应用时,我们现在可以保存截图:

🌐 On running the app in a web browser, we can now save a screenshot:

概括

🌐 Summary

Chapter 8: Handle platform differences

The app does everything we set out for it to do, so it's time to shift our focus toward the purely aesthetic..

In the next chapter, we will customize the app's status bar, splash screen, and app icon.

下一个:Chapter 9: Configure status bar, splash screen and app icon