React Native Expo推送通知图片在Cron任务中返回Null问题排查
问题说明
我用React Native Expo开发的应用,其他功能正常,但Expo推送通知存在一个问题:通过PHP Cron任务脚本发送通知时,点击通知能正常打开页面,但接收的图片数据为Null;而用Postman手动调用同一个脚本时,图片链接可正常读取,应用中能显示图片,控制台里图片值是有效链接。
我怀疑问题出在这段代码里——根据经验,Cron任务脚本可能无法正确获取document_root来检查文件是否存在:
$image_link_check = $documentRoot . "/admin/uploads/news/" .$latest_news['image']; if(file_exists($image_link_check)) { $image = $imageBaseUrl . "/news/" . $latest_news['image']; } else { $image = NULL; }
接收页面代码(NewsDetailsScreen.js)
import { StyleSheet, SafeAreaView, Image, Button, View, Platform, StatusBar, ScrollView, TouchableOpacity, Text, } from 'react-native'; import React, { useState, useEffect, useContext } from 'react'; import AppText from '../components/AppText'; import colors from '../config/colors'; import { openBrowserAsync } from 'expo-web-browser'; import { CommonActions, useNavigation } from '@react-navigation/native'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { useFocusEffect } from '@react-navigation/native'; import themeContext from '../theme/themeContext'; export default function NewsDetailsScreen({ route }) { const news = route.params; const navigation = useNavigation(); const theme = useContext(themeContext); const [author, setAuthor] = useState(); const [darkModeOn, setDarkModeOn] = useState(false); const retrieveDarkMode = async () => { try { const jsonValue = await AsyncStorage.getItem('darkModeOn'); if (jsonValue !== null) { const parsedValue = JSON.parse(jsonValue); setDarkModeOn(parsedValue); } } catch (e) { console.log('error: ', e); } }; const displayAuthor = (author) => { if ( author !== 'Editor' && author !== 'F1 Desk' && author !== 'Motorsport Network' && author !== 'RACER Staff' && author !== 'GPblog' && author !== '' ) { setAuthor('By ' + author); } else { setAuthor(''); } }; useEffect(() => { displayAuthor(news.author); console.log( 'news: ------------------------------------------------------------------ ', news.image ); }); useFocusEffect( React.useCallback(() => { retrieveDarkMode(); return () => {}; }, []) ); const styles = StyleSheet.create({ author: { color: colors.lightBlue, marginTop: 10, }, button: { marginTop: 20, marginBottom: 20, }, date: { marginTop: 10, fontSize: 15, color: darkModeOn ? colors.white : colors.lightBlack, }, description: { marginTop: 10, color: darkModeOn ? colors.white : colors.lightBlack, }, detailsContainer: { padding: 20, }, image: { width: '100%', height: 330, }, mainContainer: { marginTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0, backgroundColor: theme.backgroundColor, }, source: { marginTop: 10, color: darkModeOn ? colors.white : colors.lightBlack, }, title: { fontSize: 24, fontWeight: '500', color: darkModeOn ? colors.white : colors.lightBlack, }, backText: { color: darkModeOn ? colors.white : colors.lightBlack, fontSize: 35, paddingLeft: 5, paddingBottom: 10, position: 'absolute', left: 10, bottom: '-15%', }, }); return ( <ScrollView style={styles.scrollView}> <SafeAreaView style={styles.mainContainer}> <TouchableOpacity onPress={() => { navigation.dispatch(CommonActions.goBack()); }} style={{ width: '100%', height: 45, flexDirection: 'row' }} activeOpacity={1} > <Text style={styles.backText}>‹</Text> </TouchableOpacity> {news.image !== null ? ( <Image style={styles.image} source={{ uri: news.image, }} /> ) : ( <Image style={styles.image} source={require('../assets/f1ianIcon300x300.png')} /> )} <SafeAreaView style={styles.detailsContainer}> <AppText style={styles.title}>{news.title}</AppText> <AppText style={styles.description}> {news.description} </AppText> {news.author !== null && ( <AppText style={styles.author}>{author}</AppText> )} <AppText style={styles.date}>{news.pubDate}</AppText> <AppText style={styles.source}> From {news.feedName} </AppText> <View style={styles.button}> <Button title="Read Full Article" onPress={() => openBrowserAsync(news.link)} /> </View> </SafeAreaView> </SafeAreaView> </ScrollView> ); }
PHP脚本相关片段
// 站点链接生成 if(array_key_exists('HTTPS', $_SERVER) && $_SERVER["HTTPS"] == "on") { $site_link = "https://".$_SERVER['HTTP_HOST'] . "" . $base_directory; } else { $site_link = "http://".$_SERVER['HTTP_HOST'] . "" . $base_directory; } $base_directory = ""; $imageBaseUrl = $site_link . "/admin/uploads"; // 获取最新新闻 $latest_news = find_the_latest_single_news_by_subject_id($f1_subject); $found_feed = find_feed_info_by_feed_id($latest_news['feed_id']); // 遍历设备发送通知 while($found_devices_assoc = mysqli_fetch_assoc($found_devices)) { $token = $found_devices_assoc['token']; // 处理标题 $title = $latest_news['title']; $title = str_replace("'", "'", $title); $title = str_replace(""", "'", $title); $source = ucwords($found_feed['feed_name']); // 处理作者 $author = str_replace(""", '"', $latest_news['author']); $author = str_replace("'", "'", $author); $author = str_replace(".com", "", $author); $author = trim($author); if(strtolower($author) != strtolower($found_feed['feed_name'])) { $author = $author; } else { $author = NULL; } // 处理描述 $description = str_replace(""", '"', $latest_news['description']); $description = str_replace("'", "'", $description); $description = str_replace("\n", " ", $description); // 处理日期 $pubDate = date("F j, Y", $latest_news['pubDate']) . " at " . date("g:i a", $latest_news['pubDate']); $insertDateTime = date("F j, Y", $latest_news['insert_date']) . " at " . date("g:i a", $latest_news['insert_date']); // 处理图片 $image = ""; if($latest_news['image'] === NULL || $latest_news['image'] === "") { $image = NULL; } else { $image_link_check = $documentRoot . "/admin/uploads/news/" . $latest_news['image']; echo "image_link_check: " . $image_link_check . "<br><br>"; if(file_exists($image_link_check)) { $image = $imageBaseUrl . "/news/" . $latest_news['image']; } else { $image = NULL; } } // 发送通知逻辑 if($found_devices_assoc['notificationOn'] == 1) { $json_payload = array( 'to' => $token, 'sound' => 'default', 'title' => $source, 'body' => $title, "data" => [ "screen" => "NewsDetailsScreen", "id" => $latest_news['id'], "feedName" => $source, "pubDate" => $pubDate, "title" => $title, "description" => $description, "link" => $latest_news['link'], "author" => $author, "image" => $image, "insertDate" => $insertDateTime ] ); $data = json_encode($json_payload); var_dump($data); $ch = curl_init(); curl_setopt_array($ch, array( CURLOPT_URL => "https://exp.host/--/api/v2/push/send", CURLOPT_RETURNTRANSFER => true, CURLOPT_ENCODING => "", CURLOPT_MAXREDIRS => 10, CURLOPT_TIMEOUT => 30, CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_POSTFIELDS => $data, CURLOPT_HTTPHEADER => array( "Accept: application/json", "Accept-Encoding: gzip, deflate", "Content-Type: application/json", "cache-control: no-cache", "host: exp.host" ), )); $response = curl_exec($ch); $err = curl_error($ch); if ($response === FALSE) { die('Oops! FCM Send Error: ' . curl_error($ch)); } curl_close($ch); if ($err) { echo "cURL Error #:" . $err; } else { echo $response; } } }
解决方案
核心问题是Cron环境下$_SERVER变量不全,导致$documentRoot和$site_link获取错误,进而图片检查失败。
1. 修复$documentRoot的获取
Cron运行时没有Web服务器的环境变量,不能依赖$_SERVER['DOCUMENT_ROOT'],直接用绝对路径定义:
// 替换原来的$documentRoot赋值,改成服务器上的真实绝对路径 $documentRoot = '/var/www/your-site-folder'; // 示例路径,替换为你的网站根目录
或者用脚本所在目录推导根目录:
// 假设脚本在网站根目录下的cron文件夹里,用此方式获取根目录 $documentRoot = dirname(__DIR__);
2. 修复$site_link的生成
Cron环境下$_SERVER['HTTP_HOST']不存在,直接硬编码站点域名:
// 替换原来的站点链接生成逻辑 $site_link = "https://your-domain.com"; // 替换为你的实际域名 $base_directory = ""; $imageBaseUrl = $site_link . "/admin/uploads";
3. 可选:跳过文件存在检查(如果数据库路径可靠)
如果数据库里的image字段只会存储真实存在的图片文件名,可以直接生成链接,省去文件存在检查步骤:
if($latest_news['image'] === NULL || $latest_news['image'] === "") { $image = NULL; } else { $image = $imageBaseUrl . "/news/" . $latest_news['image']; }
内容的提问来源于stack exchange,提问作者f1ian
相关产品推荐
相关产品推荐

