构建一个全栈 Jamstack 应用程序
随着浏览器的发展,许多用于设计灵活且可扩展的 Web 应用程序的现代方法也逐渐流行起来。Jamstack就是其中一种用于开发速度极快的 Web 应用的前沿技术。Jamstack 结合了 JavaScript、API 和 Markup 的功能,可以创建现代化的、浏览器友好的 Web 应用,这些应用无需任何 Web 服务器即可运行。在本教程中,您将创建一个自定义剪贴簿应用程序,让您可以随时随地存储和访问您的日常回忆。
查看您将创建的应用程序的实时演示。
在开始本教程之前:
- 你应该对如何使用 JavaScript 的 ES6 特性有一定的了解。
- 你应该具备使用 React 和 React Hooks 的初级经验。
- 如果您之前有过 React 设计系统方面的经验,那将很有帮助。
技术栈
Jamstack 的独特之处在于它允许您集成多种技术来创建全栈应用程序。您将使用以下技术来创建此剪贴簿:
- Next.js是一个 React 框架,它扩展了 React 的强大功能,可以轻松创建多页面应用程序。无需使用 Express 等后端框架,您就可以使用 Next.js 及其无服务器功能来开发应用程序的前端和后端。
- Chakra-UI提供了大量时尚且易于使用的 React 组件,可用于美化您的 Web 应用程序。
- 本应用将使用Airtable作为数据库解决方案。Airtable 是一款电子表格/数据库混合工具,拥有出色的 API,可轻松集成到您的应用程序中。
- Cloudinary是一个云端媒体管理平台,你可以在上面上传剪贴簿的照片。
- Auth0使您能够将用户身份验证系统集成到您的应用程序中。它使用 OAuth 2.0,并为您的应用程序提供安全的 OAuth 层。
Auth0 和 Cloudinary 都提供免费套餐。您可以创建一个帐户并免费使用它们来开发此应用程序。
目录
- 入门
- 将 Airtable 连接到您的应用程序
- 将 Airtable 与 Next.js 无服务器函数集成
- 将文件上传到 Cloudinary
- 为帖子创建 React 上下文
- 使用 Auth0 设置身份验证
- 后续步骤
入门
Fork 这个入门代码沙箱模板,准备开始编写剪贴簿应用程序。如果您更喜欢本地开发,则需要安装Node和NPM。
运行以下命令将创建一个没有配置的 Next.js 和 Chakra-UI 入门应用程序。
npx create-next-app --example with-chakra-ui <app-name>
# or
yarn create next-app --example with-chakra-ui <app-name>
现在,前往Airtable创建一个免费账户,或者如果您已有账户,请直接登录。登录后,点击Add a base按钮从头开始创建一个新的数据库,并为其命名。这将创建一个包含一些主要字段的新数据库。您可以通过双击列来自定义表格。首先自定义表格名称,posts并添加以下列:
- 图片网址
- 图片说明 - 单行文本
- cloudinaryId - 单行文本
- 用户 ID - 单行文本
- 日期 - 日期
底座应该看起来像这样:
接下来,访问Airtable API并选择要集成的数据库。.env在根目录下创建一个文件,并将一些密钥添加为环境变量。要将 Airtable 连接到我们的应用程序,您需要在代码中使用以下密钥。
API_KEY:Airtable API 密钥。您可以在文档中找到它(勾选右上角的“显示 API 密钥”框),也可以在您的帐户页面中找到它。BASE_ID:您要集成的数据库的ID 。您可以在文档页面找到它。TABLE_NAME:该数据库中的表名称(您可以使用一个数据库来存储多个表)。
/.env
AIRTABLE_API_KEY=<your-airtable-api-key>
AIRTABLE_BASE_ID=<your-airtable-base-id>
AIRTABLE_TABLE_NAME=<your-airtable-table-name>
安装Airtable JavaScript SDK以便在 Next.js 应用程序中使用 Airtable。
npm install airtable
此外,请安装以下依赖项以完成开发环境的设置。
npm install @auth0/nextjs-auth0 multer next-connect cloudinary
要在应用程序中使用 Auth0 和 Cloudinary,您需要分别安装 Auth0 Next.js SDK@auth0/nextjs-auth0和 Cloudinary SDK。Multercloudinary用于处理文件输入,而 Next-connect 用于处理 Next.js API 路由中的中间件。
将 Airtable 连接到您的应用程序
/utils现在,在该文件夹内创建一个新文件夹/src,并添加一个新文件Airtable.js。以下代码将您的应用连接到 Airtable 并检索数据。默认情况下,Airtable 返回不必要的数据。该minifyRecords函数返回包含必要数据的精简版记录。
/utils/Airtable.js
const Airtable = require('airtable')
// Authenticate
Airtable.configure({
apiKey: process.env.AIRTABLE_API_KEY
})
// Initialize a base
const base = Airtable.base(process.env.AIRTABLE_BASE_ID)
// Reference a table
const table = base(process.env.AIRTABLE_TABLE_NAME)
// To get an array of meaningful records
const minifyRecords = (records) =>
records.map((record) => ({
id: record.id,
fields: record.fields
}))
export { table, minifyRecords }
将 Airtable 与 Next.js 无服务器函数集成
使用API 路由,您可以在 Next.js 中构建自己的 API。添加到该/pages/api文件夹内的任何文件都将被视为 API 端点,而不是常规路由。您可以使用无服务器函数/api/*处理任何访问该端点的请求。让我们创建一个 API 端点来检索 Airtable 记录。
/src/pages/api/getPosts.js
// For retreving posts from Airtable
import { table, minifyRecords } from '../../utils/Airtable'
export default async (req, res) => {
try {
// get records from airtable
const records = await table.select({}).firstPage()
// send an array of minified records as a response
const minfiedRecords = minifyRecords(records)
res.status(200).json(minfiedRecords)
} catch (error) {
console.error(error)
res.status(500).json({ msg: 'Something went wrong! 😕' })
}
}
设置 Cloudinary
将剪贴簿照片上传到 Cloudinary 后,您需要将密钥存储secure_url到public_idAirtable 数据库中。请前往您的 Cloudinary控制面板,复制以下密钥并将其粘贴到.env文件中。
/.env
...
CLOUDINARY_CLOUD_NAME=<your-cloudinary-cloud-name>
CLOUDINARY_API_KEY=<your-cloudinary-api-key>
CLOUDINARY_API_SECRET=<your-cloudinary-api-secret>
添加环境变量后,cloudinary.js在/utils目录中创建一个新文件来设置 Cloudinary。
/utils/cloudinary.js
import { v2 as cloudinary } from 'cloudinary'
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET
})
export default cloudinary
将文件上传到 Cloudinary
您可以使用 Cloudinary SDK 的方法将照片上传到 Cloudinary uploader.upload。默认情况下,Next.js API 路由无法处理文件输入。因此,您需要使用 `<file>` 标签multer,它会将文件附加到请求中。您还需要使用next-connect`<middleware>` 标签来处理中间件函数(了解更多createPost.js)。在 `<file>`标签内创建一个新文件,/api用于上传图片。
/src/pages/api/createPost.js
// For creating a new record in Airtable
import nextConnect from 'next-connect'
import multer from 'multer'
import path from 'path'
import { table, minifyRecords } from '../../utils/Airtable'
import cloudinary from '../../utils/cloudinary'
// multer config
const upload = multer({
storage: multer.diskStorage({}),
fileFilter: (req, file, cb) => {
let ext = path.extname(file.originalname)
if (ext !== '.jpg' && ext !== '.jpeg' && ext !== '.png') {
cb(new Error('File type is not supported'), false)
return
}
cb(null, true)
}
})
const createPost = nextConnect({
onError(error, req, res) {
res.status(501).json({ error: error.message })
}
})
// Adds the middleware to Next-Connect
// this should be the name of the form field
createPost.use(upload.single('image'))
createPost.post(async (req, res) => {
// Uploading file to cloudinary
const result = await cloudinary.uploader.upload(req.file.path)
// Create a new record with required fields
const post = {
image: result.secure_url,
caption: req.body.caption,
cloudinaryId: result.public_id,
userId: req.body.userId
}
// Create a record with the above fields in Airtable
// the 'create' method accepts and returns an array of records
const newRecords = await table.create([{ fields: post }])
res.status(200).json(minifyRecords(newRecords)[0])
})
export default createPost
export const config = {
api: {
bodyParser: false
}
}
请尝试使用Postman或类似工具测试这些端点。如果遇到任何问题,请与提供的代码片段进行比较,或尝试通过互联网进行故障排除。
显示剪贴簿帖子
现在你已经有了 API,接下来让我们设计一个界面,用于在 Next.js 应用中显示数据。你可以使用getServerSidePropsNext.js 的服务器端渲染功能来显示来自 API 的数据。每次页面渲染时,Next.js 都会运行该函数中包含的代码。
您可以点击此处了解更多关于Next.js服务器端渲染的信息。
将以下代码添加到index.js文件中。帖子将以卡片形式显示在网格中。
/src/pages/index.js
import { Container } from '../components/Container'
import { Flex, Grid, Text } from '@chakra-ui/react'
import Card from '../components/Card'
export default function Index({ initialPosts }) {
return (
<Container minH="100vh">
<Flex flexDirection="column" justifyContent="center" alignItems="center">
<Flex w="100%" flexDirection="column" my={8}>
{!initialPosts.length ? (
<Flex
h={['30vh', '50vh']}
w="100%"
justifyContent="center"
alignItems="center"
>
<Text fontSize={['2xl', '3xl']} opacity="0.2">
No Posts Added
</Text>
</Flex>
) : (
<Grid
templateColumns={[
'repeat(1, 1fr)',
'repeat(2, 1fr)',
'repeat(3, 1fr)'
]}
gap={6}
m="0 auto"
w={['100%', '90%', '85%']}
>
{initialPosts.map((post) => {
return <Card post={post.fields} key={post.id} />
})}
</Grid>
)}
</Flex>
</Flex>
</Container>
)
}
export async function getServerSideProps(context) {
try {
const res = await fetch('http://localhost:3000/api/getPosts')
return {
props: {
initialPosts: await res.json()
}
}
} catch (error) {
console.log(error)
return {
props: {
err: 'Something went wrong 😕'
}
}
}
}
/src/components/Card.js
import { Box, Text, useColorModeValue } from '@chakra-ui/react'
const Card = ({ post }) =>
post ? (
<Box
w="100%"
p={4}
flex="1"
borderWidth="1px"
bg={useColorModeValue('white', 'gray.800')}
borderColor={useColorModeValue('gray.100', 'gray.700')}
rounded="lg"
shadow="md"
>
<Text textAlign="right" fontSize="sm" fontWeight="bold" mb={4}>
{new Date(post.date).toString().substr(0, 15)}
</Text>
<a href={post.image} target="_blank" rel="noopener noreferrer">
<img src={post.image} alt={post.cloudinaryId} loading="lazy" />
</a>
<Text fontSize="md" my={4} noOfLines={[3, 4, 5]} isTruncated>
{post.caption}
</Text>
</Box>
) : null
export default Card
为帖子创建 React 上下文
为 Posts 创建一个 React 上下文,以便在其他组件之间共享文章状态。context在内部创建一个新文件夹/src,并添加一个新文件posts.js。
/src/context/posts.js
import { createContext, useState } from 'react'
const PostsContext = createContext()
const PostsProvider = ({ children }) => {
const [posts, setPosts] = useState([])
const addPost = async (formData) => {
try {
// sending the form data
const res = await fetch('/api/createPost', {
method: 'POST',
body: formData
})
const newPost = await res.json()
// updating the posts state
setPosts((prevPosts) => [newPost, ...prevPosts])
} catch (error) {
console.error(error)
}
}
return (
<PostsContext.Provider
value={{
posts,
setPosts,
addPost
}}
>
{children}
</PostsContext.Provider>
)
}
export { PostsContext, PostsProvider }
将应用程序封装起来,PostsProvider以便在您的应用程序中使用此上下文。
/src/pages/_app.js
import { PostsProvider } from '../context/posts'
import theme from '../utils/theme'
function MyApp({ Component, pageProps }) {
return (
<PostsProvider>
<ChakraProvider resetCSS theme={theme}>
...
</ChakraProvider>
</PostsProvider>
)
}
export default MyApp
现在,更新文件内部posts的状态。此时,您可以看到卡片已填充了来自 Airtable 的数据。initialPostsindex.js
/src/pages/index.js
import { useContext, useEffect } from 'react'
import { PostsContext } from '../context/posts'
export default function Index({ initialPosts }) {
const { posts, setPosts } = useContext(PostsContext)
useEffect(() => {
setPosts(initialPosts)
}, [initialPosts, setPosts])
...
}
创建用于添加帖子的表单
创建一个form用于从网页添加帖子的组件。导入addPost您创建的函数以提交表单数据。在上传文件之前,您应该包含上传照片的预览。有关在 JavaScript 中处理文件输入的更多信息,请参阅相关文档。Chakra-UI 中的 Toast 元素用于在单独的模态框中显示消息。在本组件中,您将使用 Toast 来显示成功消息。
/src/components/AddPost.js
import { useContext, useRef, useState } from 'react'
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
ModalCloseButton,
Button,
FormControl,
FormLabel,
Input,
useDisclosure,
useToast
} from '@chakra-ui/react'
import { PostsContext } from '../context/posts'
export const AddPost = ({ children }) => {
const [image, setImage] = useState()
const [caption, setCaption] = useState('')
const [previewSource, setPreviewSource] = useState('')
const [fileInputState, setFileInputState] = useState('')
const { addPost } = useContext(PostsContext)
const { isOpen, onOpen, onClose } = useDisclosure()
const toast = useToast()
const initialRef = useRef()
const handleFileInput = (e) => {
const file = e.target.files[0]
setPreviewSource(URL.createObjectURL(file))
setImage(file)
setFileInputState(e.target.value)
}
const handleSubmit = (e) => {
e.preventDefault()
if (!image) return
let formData = new FormData()
formData.append('image', image)
formData.append('caption', caption)
addPost(formData)
toast({
title: 'Hurray!!! 🎉',
description: 'Post added ✌',
status: 'success',
duration: 1500,
isClosable: true
})
onClose()
setCaption('')
setFileInputState('')
setPreviewSource('')
}
return (
<>
<Button
fontWeight="medium"
size="md"
colorScheme="yellow"
_active={{
transform: 'scale(0.95)'
}}
onClick={onOpen}
>
{children}
</Button>
<Modal initialFocusRef={initialRef} isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader fontWeight="bold">Add Post</ModalHeader>
<ModalCloseButton />
<form onSubmit={handleSubmit}>
<ModalBody pb={6}>
<FormControl>
<FormLabel>Photo</FormLabel>
<input
type="file"
name="image"
ref={initialRef}
onChange={handleFileInput}
value={fileInputState}
required
/>
</FormControl>
{previewSource && (
<img
src={previewSource}
alt="chosen"
height="300px"
width="300px"
style={{ margin: '15px auto' }}
/>
)}
<FormControl mt={4}>
<FormLabel>Caption</FormLabel>
<Input
placeholder="Caption goes here..."
type="text"
value={caption}
onChange={(e) => setCaption(e.target.value)}
/>
</FormControl>
</ModalBody>
<ModalFooter>
<Button mr={4} onClick={onClose}>
Cancel
</Button>
<Button type="submit" colorScheme="yellow" mr={3}>
Create
</Button>
</ModalFooter>
</form>
</ModalContent>
</Modal>
</>
)
}
AddPost创建导航栏后,您就可以测试该组件了。您的导航栏左侧将显示标题,右侧将显示“添加文章”、“登录/注销”Navbar.js和“深色模式切换”按钮。请在/components文件夹中创建一个新文件。
/src/components/Navbar.js
import Link from 'next/link'
import { Button } from '@chakra-ui/button'
import { Flex, Text } from '@chakra-ui/layout'
import { DarkModeSwitch } from './DarkModeSwitch'
import { AddPost } from './AddPost'
const Navbar = () => {
return (
<Flex
justifyContent="space-between"
w="80%"
flexDirection={['column', 'row']}
m={4}
>
<Text mb={[4, 0]} textAlign="center" fontWeight="bold" fontSize="2xl">
@Scrapbook
</Text>
<Flex justifyContent="space-between">
<AddPost>Add Post</AddPost>
<a href="/api/auth/login">
<Button variant="solid" colorScheme="blue" mx={3} size="md">
Login
</Button>
</a>
<DarkModeSwitch />
</Flex>
</Flex>
)
}
export default Navbar
此时,您就可以添加和显示您的剪贴簿帖子了。接下来,我们使用 Auth0 添加身份验证。
使用 Auth0 设置身份验证
如果您已经是Auth0用户,请登录您的帐户;或者立即创建一个免费帐户。创建一个新的常规 Web 应用程序。您将使用auth0-next.js SDK将 Auth0 连接到您的应用程序。当系统询问您使用的技术时,请选择Next.js。以下密钥用于配置 Auth0 与您的 Next.js 应用程序。转到“设置”选项卡,并将新的 Auth0 密钥添加到.env文件中。
/.env
...
AUTH0_SECRET=<any secret string of length 32>
AUTH0_BASE_URL='http://localhost:3000'<Your application base URL>
AUTH0_ISSUER_BASE_URL=<URL of your tenant domain>
AUTH0_CLIENT_ID=<Your application's client Id>
AUTH0_CLIENT_SECRET=<Your application's client secret>
在“设置”选项卡中向下滚动一点,您会找到“应用程序 URI”部分。添加以下回调 URL。如果您使用的是 codesandbox,请添加应用程序的基本 URL。
- 允许的回调 URL: http://localhost:3000/api/auth/callback ,/api/auth/callback
- 允许的注销 URL: http://localhost: 3000
auth/[...auth0].js在目录内创建一个新文件/pages/api。任何添加到括号 ( []) 内的路由都将被视为动态路由。
/src/pages/api/auth/[...auth0].js
import { handleAuth } from '@auth0/nextjs-auth0'
export default handleAuth()
这将生成身份验证所需的路由(`<authorization>` /login、/logout`<authorization>` 和/callback`<authorization>`)。Auth0 在底层使用 React Context 管理用户的身份验证状态。
将文件内部组件包裹在 `<authorization>`/pages/_app.js标签中,即可在整个应用程序中UserProvider使用useUserAuth0 提供的钩子。
/src/pages/_app.js
import { UserProvider } from '@auth0/nextjs-auth0'
function MyApp({ Component, pageProps }) {
return (
<UserProvider>
<PostsProvider> ... </PostsProvider>
</UserProvider>
)
}
export default MyApp
/index.js现在,您可以在组件内部使用useUser钩子来获取用户信息。将用户作为 prop 传递给Navbar组件即可添加login/logout功能。此外,我们还可以显示You have to log in用户是否已登录。
/src/pages/index.js
...
import { useUser } from '@auth0/nextjs-auth0'
export default function Index({ initialPosts }) {
const { posts, setPosts } = useContext(PostsContext)
const { user, error, isLoading } = useUser()
useEffect(...)
if (isLoading) return <div>Loading...</div>
if (error) return <div>{error.message}</div>
return (
<Flex flexDirection="column" justifyContent="center" alignItems="center">
<Navbar user={user} />
{user ? (
<Flex w="100%" flexDirection="column" my={8}>
{!posts.length ? (
<Flex
h={['30vh', '50vh']}
w="100%"
justifyContent="center"
alignItems="center"
>
<Text fontSize={['2xl', '3xl']} opacity="0.2">
No Posts Added
</Text>
</Flex>
) : (
<Grid
templateColumns={[
'repeat(1, 1fr)',
'repeat(2, 1fr)',
'repeat(3, 1fr)'
]}
gap={6}
m="0 auto"
w={['90%', '85%']}
>
{posts.map((post) => {
console.log(post)
return <Card post={post.fields} key={post.id} />
})}
</Grid>
)}
</Flex>
) : (
<Flex
h={['30vh', '50vh']}
w="100%"
justifyContent="center"
alignItems="center"
>
<Text fontSize={['2xl', '3xl']} opacity="0.2">
You have to login
</Text>
</Flex>
)}
</Flex>
)
}
export async function getServerSideProps(context) { ... }
更新Navbar.js用户登录和注销功能。此外,您还应该确保只有登录用户才能添加剪贴簿帖子。
/src/components/Navbar.js
import { Button } from '@chakra-ui/button'
import { Flex, Text } from '@chakra-ui/layout'
import { DarkModeSwitch } from './DarkModeSwitch'
import { AddPost } from './AddPost'
const Navbar = ({ user }) => {
return (
<Flex
justifyContent="space-between"
w="80vw"
flexDirection={['column', 'row']}
m={4}
>
<Text mb={[4, 0]} textAlign="center" fontWeight="bold" fontSize="2xl">
@Scrapbook
</Text>
<Flex justifyContent="space-between">
{user && <AddPost>Add Post</AddPost>}
{user ? (
<a href="/api/auth/logout">
<Button variant="solid" colorScheme="blue" mx={4} size="md">
Logout
</Button>
</a>
) : (
<a href="/api/auth/login">
<Button variant="solid" colorScheme="blue" mx={4} size="md">
Login
</Button>
</a>
)}
<DarkModeSwitch />
</Flex>
</Flex>
)
}
export default Navbar
为 API 路由添加身份验证
只有经过身份验证的用户才能访问 API。您还必须将每个帖子与特定用户关联起来,并且仅显示属于该用户的帖子。Auth0 提供了获取用户信息的withApiAuthRequired方法getSession。
按如下方式更新 API 路由:
/pages/api/getPost.js
// For retreving posts from Airtable
import { getSession, withApiAuthRequired } from '@auth0/nextjs-auth0'
import { table, minifyRecords } from '../../utils/Airtable'
export default withApiAuthRequired(async (req, res) => {
const { user } = await getSession(req, res)
try {
const records = await table
.select({ filterByFormula: `userId= '${user.sub}'` })
.firstPage()
const minfiedItems = minifyRecords(records)
res.status(200).json(minfiedItems)
} catch (error) {
console.error(error)
res.status(500).json({ msg: 'Something went wrong! 😕' })
}
})
/pages/api/createPost.js
import nextConnect from 'next-connect'
import multer from 'multer'
import { table, minifyRecords } from '../../utils/Airtable'
import cloudinary from '../../utils/cloudinary'
import { getSession, withApiAuthRequired } from '@auth0/nextjs-auth0'
// multer config
const upload = multer({
storage: multer.diskStorage({}),
fileFilter: (req, file, cb) => {
let ext = path.extname(file.originalname)
if (ext !== '.jpg' && ext !== '.jpeg' && ext !== '.png') {
cb(new Error('File type is not supported'), false)
return
}
cb(null, true)
}
})
const createPost = nextConnect({
onError(error, req, res) {
res.status(501).json({ error: error.message })
}
})
// Adds the middleware to Next-Connect
createPost.use(upload.single('image'))
createPost.post(async (req, res) => {
const { user } = getSession(req, res)
// Uploading file to cloudinary
const result = await cloudinary.uploader.upload(req.file.path)
// Create a new record with required fields
const post = {
image: result.secure_url,
caption: req.body.caption,
cloudinaryId: result.public_id,
userId: user.sub
}
// Create a record with the above fields in Airtable
// the 'create' method accepts and returns an array of records
const newRecords = await table.create([{ fields: post }])
res.status(200).json(minifyRecords(newRecords)[0])
})
export default withApiAuthRequired(createPost)
export const config = {
api: {
bodyParser: false
}
}
Cookies要访问受保护的 API,您应该在请求中包含用户的上下文(例如)。否则,您将收到错误消息not_authenticated。请修改请求,将用户的会话令牌作为 cookie 包含fetch在内。getServerSideProps
/src/pages/index.js
...
export async function getServerSideProps(context) {
try {
const res = await fetch('http://localhost:3000/api/posts', {
headers: { Cookie: context.req.headers.cookie }
})
return {
props: {
initialPosts: await res.json()
}
...
}
}
就这样!现在你只能看到你添加的帖子了。不登录的话,你将无法访问 API 路由。点击这里查看我的应用。如果遇到任何问题,请尝试使用这段源代码解决。如果你想下载,这里是GitHub 上的完整代码库。
后续步骤
在本教程中,您已经构建了一个包含媒体管理和身份验证的全栈 Jamstack 应用。您还可以添加一些额外的功能,例如:
- 更新和删除帖子。
- 目前只能上传小尺寸图片。请找到一种方法,使应用程序能够上传大尺寸图片。
- 您可以使用 Cloudinary 进行媒体转换。上传前优化图像,以节省云端空间。
- 尝试显示用户个人资料,并包含更新或删除用户的途径。
- 试着想出一些更奇特的办法来增加额外的功能,并与社区分享。
对于媒体管理,我更倾向于使用 Cloudinary。它允许你将所有媒体文件存储在与数据库不同的独立云端。Auth0 则为你的应用提供了一层额外的安全保障。你可以自定义登录/注册页面,使其更符合你的品牌形象。Airtable 是另一个我非常喜欢的强大工具,因为它拥有类似电子表格的用户界面。根据你的需求,你可以将任何数据库与 Jamstack 应用结合使用。正如之前提到的,你可以使用 Jamstack 开发全栈应用,并根据自身需求组合各种技术。
为Hackmamba Jamstack 内容黑客马拉松活动创建的内容,由 Auth0 和 Cloudinary 提供支持。
文章来源:https://dev.to/hackmamba/build-a-full-stack-jamstack-application-5apm




