我想用 React Native Expo 创建聊天应用程序。我使用 expo-image-picker 拍照并将其上传到我使用 apollo 构建的 api graphql。我使用 apollo-upload-client 上传图片,但我发现该包不支持此 18.0.0 版本的 React Native。 我收到 Apollo 错误:响应不成功:当我尝试上传时收到状态代码 400
这是我的 apollo 客户端实例:
import fetch from "cross-fetch";
import {
ApolloClient,
NormalizedCacheObject,
InMemoryCache,
} from "@apollo/client";
import { GraphQLWsLink } from "@apollo/client/link/subscriptions";
import { ApolloLink, HttpLink, split } from "@apollo/client/core";
import { getMainDefinition } from "@apollo/client/utilities";
import { createClient } from "graphql-ws";
import { AuthStorage } from "./utils/AuthStorage";
import createUploadLink from "apollo-upload-client/createUploadLink.mjs";
export const apolloClient = async (
token?: string
): Promise<ApolloClient<NormalizedCacheObject>> => {
const API_URI = process.env.EXPO_PUBLIC_API_URI;
const API_URI_WS = process.env.EXPO_PUBLIC_URI_WS;
const user = token ? token : (await AuthStorage.isAuth())?.token;
const httpLink: HttpLink = new HttpLink({
uri: API_URI,
fetch,
headers: {
authorization: `Bearer ${user}`,
},
});
const wsLink: GraphQLWsLink = new GraphQLWsLink(
createClient({
url: API_URI_WS as string,
lazy: true,
connectionParams: {
authentication: `Bearer ${user}`,
},
})
);
const uploadLink = createUploadLink({
uri: API_URI,
fetch,
headers: {
authorization: `Bearer ${user}`,
},
});
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === "OperationDefinition" &&
definition.operation === "subscription"
);
},
wsLink,
httpLink
);
const link = ApolloLink.split(
(operation) => {
const definition = getMainDefinition(operation.query);
return (
definition.kind === "OperationDefinition" &&
definition.operation === "mutation" &&
!!definition.selectionSet?.selections.find(
(selection) =>
selection.kind === "Field" && selection.name.value === "upload"
)
);
},
uploadLink as any,
splitLink
);
const client = new ApolloClient({
link: link,
cache: new InMemoryCache(),
});
return client;
};
这是上传带有消息的图像的突变:
export const SEND_MESSAGE_MOBILE = gql`
mutation SendMessageDiscussGroupMobile(
$discussionId: Float!
$userId: Float!
$messageInput: MessageInput!
$data: [Upload!]
$receiverId: Float
$discussGroupId: Float
) {
sendMessageDiscussGroupMobile(
discussionId: $discussionId
userId: $userId
messageInput: $messageInput
data: $data
receiverId: $receiverId
discussGroupId: $discussGroupId
) {
theme
createdAt
updatedAt
User {
lastname
firstname
id
photo
status
}
Receiver {
firstname
lastname
id
photo
status
}
DiscussGroup {
groupName
coverPhoto
id
}
messages {
id
User {
id
lastname
firstname
photo
status
}
content
files {
name
extension
url
id
}
receiverId
discussGroupId
createdAt
updatedAt
}
id
}
}
`;
这是拍照上传的方法:
const cameraPress = async () => {
try {
await requestCameraPermissionsAsync();
let result = await launchCameraAsync({
cameraType: CameraType.back,
allowsEditing: false,
aspect: [1, 1],
quality: 1,
});
if (!result.canceled) {
const reponse = await fetch(result.assets[0].uri);
const blob = await reponse.blob();
const file = new File([blob], result.assets[0].fileName as string, {
type: result.assets[0].mimeType,
});
await sendMessage([file], message);
}
} catch (error) {
console.log(error);
}
};
大家也有同样的问题 "@apollo/client": "^3.9.7", "apollo-upload-client": "^18.0.0", "世博会": "~50.0.17", "expo-image-picker": "~14.7.1",
带有斑点的结果: https://prnt.sc/JmTZy7RNsIoA https://prnt.sc/MkXxL00C6mjS
FormData 的结果: https://prnt.sc/YZcwDlJ9NgEK https://prnt.sc/93Fw0icDvv1P
import { ApolloClient, InMemoryCache, ApolloLink, NormalizedCacheObject, Observable } from '@apollo/client'
import { onError } from '@apollo/client/link/error';
import AsyncStorage from '@react-native-async-storage/async-storage';
import createUploadLink from "apollo-upload-client/createUploadLink.mjs";
import { parseHeaders } from '@apollo/client/link/http/parseAndCheckHttpResponse';
import { router } from "expo-router";
// utils
import toaster from './toaster';
export const uploadFetch = (url: string, options: any) =>
new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onload = () => {
const opts: any = {
status: xhr.status,
statusText: xhr.statusText,
headers: parseHeaders(xhr.getAllResponseHeaders() || ''),
};
opts.url =
'responseURL' in xhr
? xhr.responseURL
: opts.headers.get('X-Request-URL');
const body = 'response' in xhr ? xhr.response : (xhr as any).responseText;
resolve(new Response(body, opts));
};
xhr.onerror = () => {
reject(new TypeError('Network request failed'));
};
xhr.ontimeout = () => {
reject(new TypeError('Network request failed'));
};
xhr.open(options.method, url, true);
Object.keys(options.headers).forEach((key) => {
xhr.setRequestHeader(key, options.headers[key]);
});
if (xhr.upload) {
xhr.upload.onprogress = options.onProgress;
}
options.onAbortPossible(() => {
xhr.abort();
});
xhr.send(options.body);
});
const customFetch = (uri: any, options: any) => {
if (options.onProgress) {
return uploadFetch(uri, options);
}
return fetch(uri, options);
};
// @ts-ignore
const apiUrl = process.env.EXPO_PUBLIC_API_URL;
// @ts-ignore
const httpLink = new createUploadLink({ uri: apiUrl});
const authMiddleware = new ApolloLink((operation, forward) => {
return new Observable((observer) => {
const fetchToken = async () => {
try {
const token = await AsyncStorage.getItem('authToken');
observer.next(token);
observer.complete();
} catch (error) {
observer.error(error);
}
};
fetchToken();
}).flatMap((token) => {
operation.setContext(({ headers = {} }) => ({
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
},
}));
return forward(operation);
});
});
const errorLink = onError(({ graphQLErrors, networkError }: any) => {
if (graphQLErrors) {
if (graphQLErrors?.[0]?.extensions?.validation) {
const errors = graphQLErrors?.[0]?.extensions?.validation;
Object.keys(errors).forEach((key) => {
errors[key].forEach((error: any) => {
toaster(error);
});
});
return;
}
if (graphQLErrors?.[0]?.extensions?.reason) {
const errors = graphQLErrors?.[0]?.extensions?.reason;
toaster(errors);
return;
}
graphQLErrors.forEach(({ message }: {message: string}) => {
if (message == 'Access denied. The team is deactivated.'
// && ! user?.is_impersonated
) {
router.push("/login")
} else if (message == 'This action is unauthorized.'
// && !user?.is_impersonated
) {
toaster(`GraphQL Error: ${message}`);
} else {
toaster(`GraphQL Error: ${message}`);
}
});
}
if (graphQLErrors) {
const {ApolloError} = graphQLErrors;
toaster(ApolloError || graphQLErrors[0].message)
}
if (networkError) {
toaster(`Network Error: ${networkError.message}`)
}
});
const client: ApolloClient<NormalizedCacheObject> = new ApolloClient({
link: ApolloLink.from([errorLink, authMiddleware.concat(httpLink) ]),
cache: new InMemoryCache(),
});
export {client};
图Ql突变
export const UPDATE_AVATAR = gql`
mutation UpdateProfile( $avatar: Upload!) {
updateProfile(input: {avatar: $avatar}) {
avatar_url
}
}
`;
示例代码在这里
const [image, setImage] = useState<string | null>(null);
const pickImage = async (useLibrary: boolean) => {
let result;
const options: ImagePicker.ImagePickerOptions = {
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [4, 3],
quality: 1,
allowsMultipleSelection: false,
};
if (useLibrary) {
result = await ImagePicker.launchImageLibraryAsync(options);
} else {
await ImagePicker.requestCameraPermissionsAsync();
result = await ImagePicker.launchCameraAsync(options);
}
if (!result.canceled) {
uploadFile(result);
setImage(result.assets[0].uri);
}
};
const uploadFile = async (file) => {
const response = await fetch(file.assets[0].uri);
const blob = await response.blob();
// or FormData
const formData = new FormData();
formData.append("avatar", {
uri: file.assets[0].uri,
type: "image/jpeg",
name: "name.jpeg",
fileName: "name.jpeg",
size: file.assets[0].size,
});
try {
const data = await updateProfile({
// or variables: {avatar: blob} - result the same one
variables: {
avatar: blob,
},
});
} catch (e) {
console.error("Error Occured", e);
}
};