处理平台差异
在本教程中,了解在创建通用应用时如何处理原生和 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.

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:
- npm install dom-to-image注意: 这里使用
dom-to-image库仅作示例。在生产应用中,你可能需要探索更适合你具体使用场景的其他解决方案或 API。
安装完成后,确保重启开发服务器,并在终端中按 w 。
2
添加特定平台的代码
🌐 Add platform-specific code
使用 React Native 的 Platform 模块,我们可以实现特定平台的行为。在 app/(tabs)/index.tsx 中:
🌐 Using Platform module from React Native, we can implement platform-specific behavior. Inside app/(tabs)/index.tsx:
- 从
react-native导入Platform模块。 - 从
dom-to-image导入domtoimage库。 - 更新
onSaveImageAsync()函数以检查当前平台是否具有Platform.OS属性的'web'。如果是'web',我们将使用domtoimage.toJpeg()方法将当前的<View>转换并捕获为 JPEG 图片。否则,我们将继续使用为本地平台添加的相同逻辑。
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/ImageViewer'; import IconButton from '@/components/IconButton'; import CircleButton from '@/components/CircleButton'; import EmojiPicker from '@/components/EmojiPicker'; import EmojiList from '@/components/EmojiList'; import EmojiSticker from '@/components/EmojiSticker'; 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] = MediaLibrary.usePermissions(); 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:
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
这个应用完成了我们设定的所有功能,所以现在是把注意力转向纯粹美学的时候了。
在下一章中,我们将自定义应用的状态栏、启动画面和应用图标。