GraphQL 教程 - 如何使用 AWS AppSync 和 AWS Amplify 管理图像和文件的上传和下载
如何使用 GraphQL 结合 AWS AppSync、AWS Amplify 和 Amazon S3 创建和查询图像和文件
存储和查询图像和视频等文件是大多数应用程序的常见要求,但如何使用 GraphQL 来实现这一点?
一种方案是使用 Base64 编码图片,并在变异过程中以字符串形式发送。这种方法存在一些缺点,例如编码后的文件比原始二进制文件更大、计算成本高昂,以及编码和解码的复杂性增加。
另一种选择是使用单独的服务器(或 API)来上传文件。这是首选方法,也是我们将在本教程中介绍的技术。
要查看或试用最终的示例项目,请单击此处。
工作原理
通常,您需要做几件事才能完成这项工作:
- GraphQL API
- 用于保存文件的存储服务或数据库
- 用于存储 GraphQL 数据的数据库,包括对文件位置的引用
以电子商务应用程序中的产品架构为例:
type Product {
id: ID!
name: String!
description: String
price: Int
image: ?
}
我们该如何使用这个image
字段,并让它在我们的应用中存储和引用图像呢?让我们看看它如何与存储在 Amazon S3 中的图像配合使用。
使用 Amazon S3 有两种主要访问类型:私有和公共。
公开访问意味着任何拥有文件 URL 的人都可以随时查看或下载它。在这个用例中,我们可以将图片 URL 引用为 GraphQL schema 中的 image 字段。由于图片 URL 本身就是公开的,所以我们不关心谁可以查看图片。
私有访问意味着只有从您的应用调用 API 的用户才能查看或下载该文件。在本用例中,我们只会将对图像键(即images/mycoolimage.png
)的引用存储为 GraphQL 模式中的 image 字段。使用此键,我们可以在需要时从 S3 获取一个临时签名 URL,以便随时查看此图像。
在本教程中,您将学习如何执行这两项操作。
创建客户端
在本教程中,我将使用 React 编写客户端代码,但您可以使用 Vue、Angular 或任何其他 JavaScript 框架,因为我们将要编写的 API 调用不是特定于 React 的。
创建一个新的客户端项目,进入目录并安装 amplify 和 uuid 依赖项:
npx create-react-app gqlimages
cd gqlimages
npm install aws-amplify @aws-amplify/ui-react uuid
公共访问
我们将创建的第一个示例是具有公共图像访问权限的 GraphQL API。
我们将要使用的 GraphQL 类型是Product
带有image
字段的。我们希望此产品的图片是公开的,以便任何查看该应用的用户(无论是否登录)都可以共享并查看它。
我们将使用的 GraphQL 模式如下:
type Product @model {
id: ID!
name: String!
description: String
price: Int
image: String
}
我们如何实现这个 API?
对于突变
- 将图像存储在 S3 中
- 发送一个突变,使用图像参考以及其他产品数据在 GraphQL API 中创建产品
查询
- 通过 GraphQL API 查询产品数据。由于图片 URL 是公开的,我们可以立即渲染图片字段。
创建服务
要构建此 API,我们需要以下内容:
- 用于存储图像的 S3 存储桶
- GraphQL API 用于存储图像引用和有关类型的其他数据
- 身份验证服务用于验证用户身份(仅在将文件上传到 S3 时需要)
我们要做的第一件事是创建身份验证服务。为此,我们将初始化一个 Amplify 项目并添加身份验证。
如果您尚未安装和配置 Amplify CLI,请单击此处查看视频演示。
amplify init
amplify add auth
? Do you want to use the default authentication and security configuration? Default configuration
? How do you want users to be able to sign in when using your Cognito User Pool? Username
? What attributes are required for signing up? Email
接下来,我们将创建存储服务(Amazon S3):
amplify add storage
? Please select from one of the below mentioned services: Content (Images, audio, video, etc.)
? Please provide a friendly name for your resource that will be used to label this category in the project: gqls3
? Please provide bucket name: <YOUR_UNIQUE_BUCKET_NAME>
? Who should have access: Auth and guest users
? What kind of access do you want for Authenticated users?
❯◉ create/update
◉ read
◉ delete
? What kind of access do you want for Guest users?
◯ create/update
❯◉ read
◯ delete
? Do you want to add a Lambda Trigger for your S3 Bucket? N
最后,我们将创建 GraphQL API:
amplify add api
? Please select from one of the below mentioned services (Use arrow keys): GraphQL
? Provide API name: (gqls3)
? Choose an authorization type for the API: API key
? Do you have an annotated GraphQL schema? N
? Do you want a guided schema creation? Y
? What best describes your project: Single object with fields
? Do you want to edit the schema now? Y
出现提示时,使用以下内容更新位于/amplify/backend/api/gqls3/schema.graphql的架构:
type Product @model {
id: ID!
name: String!
description: String
price: Int
image: String
}
接下来,我们可以使用以下命令部署 API:
amplify push
? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target javascript
? Enter the file name pattern of graphql queries, mutations and subscriptions src/graphql/**/*.js
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes
? Enter maximum statement depth [increase from default if your schema is deeply nested] 2
接下来,我们将配置index.js来识别 Amplify 应用程序:
import Amplify from 'aws-amplify'
import config from './aws-exports'
Amplify.configure(config)
现在服务已经部署完毕,我们需要更新 S3 存储桶以拥有一个公共/images文件夹,以便任何人都可以查看该文件夹中存储的任何内容。
警告:将 S3 文件夹公开时,请确保不要在其中存储任何敏感或私人信息,因为该文件夹完全公开,任何人都可以查看。在本例中,我们模拟了一个电商应用,其中包含一些将发布在主网站上的产品公开图片。
打开 S3 控制台https://s3.console.aws.amazon.com并找到您在上一步中创建的存储桶。
接下来,单击“权限”选项卡来更新存储桶策略。
将策略更新如下。您需要将Resource字段更新为您的存储桶的资源名称(即,arn:aws:s3:::gqlimages6c6fev-dev
需要将其替换为您的存储桶的名称):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::gqlimages6c6fev-dev/public/images/*"
}
]
}
从客户端应用程序与 API 交互
现在后端已经创建,我们如何与它交互以从中上传和读取图像?
下面是我们不仅可以将文件保存到我们的 API 还可以在 UI 中查询和呈现它们的代码。
主要有两个功能:
createProduct
- 将产品图像上传到 S3,并将产品数据以 GraphQL 突变形式保存到 AppSynclistProducts
- 查询所有产品的 GraphQL API
import React, { useEffect, useState } from 'react';
import { Storage, API, graphqlOperation } from 'aws-amplify'
import { v4 as uuid } from 'uuid'
import { withAuthenticator } from '@aws-amplify/ui-react'
import { createProduct as CreateProduct } from './graphql/mutations'
import { listProducts as ListProducts } from './graphql/queries'
import config from './aws-exports'
const {
aws_user_files_s3_bucket_region: region,
aws_user_files_s3_bucket: bucket
} = config
function App() {
const [file, updateFile] = useState(null)
const [productName, updateProductName] = useState('')
const [products, updateProducts] = useState([])
useEffect(() => {
listProducts()
}, [])
// Query the API and save them to the state
async function listProducts() {
const products = await API.graphql(graphqlOperation(ListProducts))
updateProducts(products.data.listProducts.items)
}
function handleChange(event) {
const { target: { value, files } } = event
const fileForUpload = files[0]
updateProductName(fileForUpload.name.split(".")[0])
updateFile(fileForUpload || value)
}
// upload the image to S3 and then save it in the GraphQL API
async function createProduct() {
if (file) {
const extension = file.name.split(".")[1]
const { type: mimeType } = file
const key = `images/${uuid()}${productName}.${extension}`
const url = `https://${bucket}.s3.${region}.amazonaws.com/public/${key}`
const inputData = { name: productName , image: url }
try {
await Storage.put(key, file, {
contentType: mimeType
})
await API.graphql(graphqlOperation(CreateProduct, { input: inputData }))
} catch (err) {
console.log('error: ', err)
}
}
}
return (
<div style={styles.container}>
<input
type="file"
onChange={handleChange}
style={{margin: '10px 0px'}}
/>
<input
placeholder='Product Name'
value={productName}
onChange={e => updateProductName(e.target.value)}
/>
<button
style={styles.button}
onClick={createProduct}>Create Product</button>
{
products.map((p, i) => (
<img
style={styles.image}
key={i}
src={p.image}
/>
))
}
</div>
);
}
const styles = {
container: {
width: 400,
margin: '0 auto'
},
image: {
width: 400
},
button: {
width: 200,
backgroundColor: '#ddd',
cursor: 'pointer',
height: 30,
margin: '0px 0px 8px'
}
}
export default withAuthenticator(App);
要启动应用程序,请运行npm start
。
要查看完整的项目代码,请单击此处并打开
src/Products.js
文件。
私人通道
我们将创建的下一个示例是具有私有图像字段的类型的 GraphQL API。
此图片仅供使用我们应用的用户访问。如果有人尝试直接获取此图片,则无法查看。
对于图像字段,我们将创建一个 GraphQL 类型,它包含从 S3 存储桶创建和读取私有文件所需的所有信息,包括存储桶名称和区域以及我们想要从存储桶中读取的密钥。
我们将要使用的 GraphQL 类型是一个User
带有avatar
字段的类型。我们希望这个头像图片是私密的,只有登录到该应用的用户才能看到。
我们将使用的 GraphQL 模式如下:
type User @model {
id: ID!
username: String!
avatar: S3Object
}
type S3Object {
bucket: String!
region: String!
key: String!
}
我们如何实现 API 来实现这一点?
对于突变
- 将图像存储在 S3 中
- 发送一个突变,使用图像引用以及其他用户数据在 GraphQL API 中创建用户
查询
- 从API查询用户数据(包括图片引用)
- 在另一个 API 调用中从 S3 获取图像的签名 URL
要构建此应用程序,我们需要以下内容:
- 身份验证服务,用于验证用户身份
- 用于存储图像的 S3 存储桶
- GraphQL API 用于存储图像引用和有关类型的其他数据
构建应用程序
如果您没有在前面的示例中构建应用程序,请返回并构建上述项目(创建身份验证服务、GraphQL API 和 S3 存储桶)以继续。
我们现在可以更新位于/amplify/backend/api/gqls3/schema.graphql的模式并添加以下类型:
type User @model {
id: ID!
username: String!
avatar: S3Object
}
type S3Object {
bucket: String!
region: String!
key: String!
}
接下来,我们可以部署更改:
amplify push
? Do you want to update code for your updated GraphQL API Yes
? Do you want to generate GraphQL statements (queries, mutations and
subscription) based on your schema types? This will overwrite your cu
rrent graphql queries, mutations and subscriptions Yes
从客户端应用程序与 API 交互
现在后端已经创建,我们如何与它交互以从中上传和读取图像?
下面是我们不仅可以将文件保存到我们的 API 还可以在 UI 中查询和呈现它们的代码。
主要功能有三项:
createUser
-(将用户图像上传到 S3 并将用户数据以 GraphQL 变异形式保存到 AppSync)fetchUsers
- 查询所有用户的 GraphQL APIfetchImage
- 获取图像的签名 S3 URL,以便我们呈现它并将其呈现在 UI 中。
import React, { useState, useReducer, useEffect } from 'react'
import { withAuthenticator } from 'aws-amplify-react'
import { Storage, API, graphqlOperation } from 'aws-amplify'
import { v4 as uuid } from 'uuid'
import { createUser as CreateUser } from './graphql/mutations'
import { listUsers } from './graphql/queries'
import { onCreateUser } from './graphql/subscriptions'
import config from './aws-exports'
const {
aws_user_files_s3_bucket_region: region,
aws_user_files_s3_bucket: bucket
} = config
const initialState = {
users: []
}
function reducer(state, action) {
switch(action.type) {
case 'SET_USERS':
return { ...state, users: action.users }
case 'ADD_USER':
return { ...state, users: [action.user, ...state.users] }
default:
return state
}
}
function App() {
const [file, updateFile] = useState(null)
const [username, updateUsername] = useState('')
const [state, dispatch] = useReducer(reducer, initialState)
const [avatarUrl, updateAvatarUrl] = useState('')
function handleChange(event) {
const { target: { value, files } } = event
const [image] = files || []
updateFile(image || value)
}
async function fetchImage(key) {
try {
const imageData = await Storage.get(key)
updateAvatarUrl(imageData)
} catch(err) {
console.log('error: ', err)
}
}
async function fetchUsers() {
try {
let users = await API.graphql(graphqlOperation(listUsers))
users = users.data.listUsers.items
dispatch({ type: 'SET_USERS', users })
} catch(err) {
console.log('error fetching users')
}
}
async function createUser() {
if (!username) return alert('please enter a username')
if (file && username) {
const { name: fileName, type: mimeType } = file
const key = `${uuid()}${fileName}`
const fileForUpload = {
bucket,
key,
region,
}
const inputData = { username, avatar: fileForUpload }
try {
await Storage.put(key, file, {
contentType: mimeType
})
await API.graphql(graphqlOperation(CreateUser, { input: inputData }))
updateUsername('')
console.log('successfully stored user data!')
} catch (err) {
console.log('error: ', err)
}
}
}
useEffect(() => {
fetchUsers()
const subscription = API.graphql(graphqlOperation(onCreateUser))
.subscribe({
next: async userData => {
const { onCreateUser } = userData.value.data
dispatch({ type: 'ADD_USER', user: onCreateUser })
}
})
return () => subscription.unsubscribe()
}, [])
return (
<div style={styles.container}>
<input
label="File to upload"
type="file"
onChange={handleChange}
style={{margin: '10px 0px'}}
/>
<input
placeholder='Username'
value={username}
onChange={e => updateUsername(e.target.value)}
/>
<button
style={styles.button}
onClick={createUser}>Save Image</button>
{
state.users.map((u, i) => {
return (
<div
key={i}
>
<p
style={styles.username}
onClick={() => fetchImage(u.avatar.key)}>{u.username}</p>
</div>
)
})
}
<img
src={avatarUrl}
style={{ width: 300 }}
/>
</div>
)
}
const styles = {
container: {
width: 300,
margin: '0 auto'
},
username: {
cursor: 'pointer',
border: '1px solid #ddd',
padding: '5px 25px'
},
button: {
width: 200,
backgroundColor: '#ddd',
cursor: 'pointer',
height: 30,
margin: '0px 0px 8px'
}
}
export default withAuthenticator(App)
要启动应用程序,请运行npm start
。
文章来源:https://dev.to/dabit3/graphql-tutorial-how-to-manage-image-file-uploads-downloads-with-aws-appsync-aws-amplify-hga要查看或试用最终的示例项目,请单击此处。
我叫Nader Dabit。我是 Amazon Web Services 的开发倡导者,负责AWS AppSync和AWS Amplify等项目。我专注于跨平台和云端应用程序开发。