发布于 2026-01-05 4 阅读
0

构建一个全栈 Jamstack 应用程序

构建一个全栈 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 这个入门代码沙箱模板,准备开始编写剪贴簿应用程序。如果您更喜欢本地开发,则需要安装NodeNPM

运行以下命令将创建一个没有配置的 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>
Enter fullscreen mode Exit fullscreen mode

现在,前往Airtable创建一个免费账户,或者如果您已有账户,请直接登录。登录后,点击Add a base按钮从头开始创建一个新的数据库,并为其命名。这将创建一个包含一些主要字段的新数据库。您可以通过双击列来自定义表格。首先自定义表格名称,posts并添加以下列:

  • 图片网址
  • 图片说明 - 单行文本
  • cloudinaryId - 单行文本
  • 用户 ID - 单行文本
  • 日期 - 日期

底座应该看起来像这样:

初始表格,包含以下列:image、caption、cloudinaryId、userId 和 date

接下来,访问Airtable API并选择要集成的数据库。.env在根目录下创建一个文件,并将一些密钥添加为环境变量。要将 Airtable 连接到我们的应用程序,您需要在代码中使用以下密钥。

  • API_KEY:Airtable API 密钥。您可以在文档中找到它(勾选右上角的“显示 API 密钥”框),也可以在您的帐户页面中找到它。
  • BASE_ID您要集成的数据库的ID 。您可以在文档页面找到它。
  • TABLE_NAME:该数据库中的表名称(您可以使用一个数据库来存储多个表)。

包含密钥的 Airtable API 页面

/.env

AIRTABLE_API_KEY=<your-airtable-api-key>
AIRTABLE_BASE_ID=<your-airtable-base-id>
AIRTABLE_TABLE_NAME=<your-airtable-table-name>
Enter fullscreen mode Exit fullscreen mode

安装Airtable JavaScript SDK以便在 Next.js 应用程序中使用 Airtable。

npm install airtable
Enter fullscreen mode Exit fullscreen mode

此外,请安装以下依赖项以完成开发环境的设置。

npm install @auth0/nextjs-auth0 multer next-connect cloudinary
Enter fullscreen mode Exit fullscreen mode

要在应用程序中使用 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 }
Enter fullscreen mode Exit fullscreen mode

将 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! 😕' })
  }
}
Enter fullscreen mode Exit fullscreen mode

设置 Cloudinary

将剪贴簿照片上传到 Cloudinary 后,您需要将密钥存储secure_urlpublic_idAirtable 数据库中。请前往您的 Cloudinary控制面板,复制以下密钥并将其粘贴到.env文件中。

Cloudinary 控制面板,包含云名称、API 密钥和 API 密钥。

/.env

...
CLOUDINARY_CLOUD_NAME=<your-cloudinary-cloud-name>
CLOUDINARY_API_KEY=<your-cloudinary-api-key>
CLOUDINARY_API_SECRET=<your-cloudinary-api-secret>
Enter fullscreen mode Exit fullscreen mode

添加环境变量后,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
Enter fullscreen mode Exit fullscreen mode

将文件上传到 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
  }
}
Enter fullscreen mode Exit fullscreen mode

请尝试使用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 😕'
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

/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
Enter fullscreen mode Exit fullscreen mode

为帖子创建 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 }
Enter fullscreen mode Exit fullscreen mode

将应用程序封装起来,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
Enter fullscreen mode Exit fullscreen mode

现在,更新文件内部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])

  ...
}
Enter fullscreen mode Exit fullscreen mode

创建用于添加帖子的表单

创建一个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>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

此时,您就可以添加和显示您的剪贴簿帖子了。接下来,我们使用 Auth0 添加身份验证。

使用 Auth0 设置身份验证

如果您已经是Auth0用户,请登录您的帐户;或者立即创建一个免费帐户。创建一个新的常规 Web 应用程序。您将使用auth0-next.js SDK将 Auth0 连接到您的应用程序。当系统询问您使用的技术时,请选择Next.js。以下密钥用于配置 Auth0 与您的 Next.js 应用程序。转到“设置”选项卡,并将新的 Auth0 密钥添加到.env文件中。

Auth0 应用程序设置页面,需要 CLIENT_ID 和 CLIENT_SECRET。

/.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>
Enter fullscreen mode Exit fullscreen mode

在“设置”选项卡中向下滚动一点,您会找到“应用程序 URI”部分。添加以下回调 URL。如果您使用的是 codesandbox,请添加应用程序的基本 URL。

auth/[...auth0].js在目录内创建一个新文件/pages/api。任何添加到括号 ( []) 内的路由都将被视为动态路由

/src/pages/api/auth/[...auth0].js

import { handleAuth } from '@auth0/nextjs-auth0'

export default handleAuth()
Enter fullscreen mode Exit fullscreen mode

这将生成身份验证所需的路由(`<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
Enter fullscreen mode Exit fullscreen mode

/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) { ... }
Enter fullscreen mode Exit fullscreen mode

更新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
Enter fullscreen mode Exit fullscreen mode

为 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! 😕' })
  }
})
Enter fullscreen mode Exit fullscreen mode

/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
  }
}
Enter fullscreen mode Exit fullscreen mode

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()
      }
    ...
  }
}
Enter fullscreen mode Exit fullscreen mode

就这样!现在你只能看到你添加的帖子了。不登录的话,你将无法访问 API 路由。点击这里查看我的应用。如果遇到任何问题,请尝试使用这段源代码解决。如果你想下载,这里是GitHub 上的完整代码库

后续步骤

在本教程中,您已经构建了一个包含媒体管理和身份验证的全栈 Jamstack 应用。您还可以添加一些额外的功能,例如:

  • 更新和删除帖子。
  • 目前只能上传小尺寸图片。请找到一种方法,使应用程序能够上传大尺寸图片。
  • 您可以使用 Cloudinary 进行媒体转换。上传前优化图像,以节省云端空间。
  • 尝试显示用户个人资料,并包含更新或删除用户的途径。
  • 试着想出一些更奇特的办法来增加额外的功能,并与社区分享。

对于媒体管理,我更倾向于使用 Cloudinary。它允许你将所有媒体文件存储在与数据库不同的独立云端。Auth0 则为你的应用提供了一层额外的安全保障。你可以自定义登录/注册页面,使其更符合你的品牌形象。Airtable 是另一个我非常喜欢的强大工具,因为它拥有类似电子表格的用户界面。根据你的需求,你可以将任何数据库与 Jamstack 应用结合使用。正如之前提到的,你可以使用 Jamstack 开发全栈应用,并根据自身需求组合各种技术。

Hackmamba Jamstack 内容黑客马拉松活动创建的内容,由 Auth0 和 Cloudinary 提供支持。

文章来源:https://dev.to/hackmamba/build-a-full-stack-jamstack-application-5apm