理解如何应用 LeanCloud 创立数据库并应用 Next.js 创立带有服务端的应用程序。

前言

通过本教程您将理解到:

  • 应用 LeanCloud 作为收费数据库
  • 应用 Next.js 开发一个蕴含前后端的利用
  • 将利用公布到 Vercel
  • 应用 Tailwind 轻松设置款式

咱们将创立一个用于影视剧打分利用,我将它部署在 rec.zehao.me,残缺源码我放在 2eha0/records

创立 Next.js 利用

应用 Next.js 官网模板创立我的项目

& npx create-next-app --example with-tailwindcss my-app

该指标曾经为您配置好以下内容:

  • Next.js 最新版本
  • TypeScript
  • Tailwind CSS & 主动去除未应用的类名
  • Next.js API 路由示例

创立前端组件

当初咱们能够开始创立组件了,pages/index.tsx 是利用的入口文件,咱们先来批改整体布局

// pages/index.tsximport type { NextPage } from 'next'import Head from 'next/head'const Home: NextPage = () => {  return (    <div className='mx-[3.5rem] min-w-[15rem] max-w-full sm:mx-auto sm:w-[30rem] font-sans'>      <Head>        <title>我看过的</title>        <meta name="viewport" content="width=device-width" />        <link rel="icon" href="/favicon.ico" />      </Head>      <h1 className='text-slate-300 flex justify-between items-center text-xl sm:text-5xl my-8 sm:my-20'>        <span>我看过的</span>        <span className='text-xs sm:text-xl'>电影 / 动漫 / 剧 / 书</span>      </h1>    </div>  )}export default Home

接下来,咱们须要为利用增加一个卡片组件,用于显示影视作品的信息,新建 components/card.tsx 文件

// components/card.tsximport Image from 'next/image'export const Card: React.FC<Props> = (props) => {  return (    <section className='relative before:content-[""] before:border-l-2 before:absolute before:inset-y-0 before:-left-9 before:translate-x-[0.44em] pb-10 first:before:top-1 last:before:bottom-10'>      <p className='text-slate-400 text-xs mb-2 sm:text-base sm:mb-3 relative'>        2022/04/02        <i className='absolute w-4 h-4 rounded-full bg-slate-200 -left-9 top-1/2 translate-y-[-50%]' />      </p>      <div className="flex items-start">        <div className="flex-1 mr-2">          <p className='text-md mb-2 sm:text-2xl sm:mb-3 leading-6 text-slate-900'>            鬼灭之刃            <span className='text-slate-400'>(2019)</span>          </p>          <p className='text-xs sm:text-base text-slate-700'>            <span className='text-slate-400'>评分:</span>            <big className='font-bold text-blue-500'> 还行</big>          </p>          <p className='text-xs sm:text-base text-slate-700'>            <span className='text-slate-400'>分类:</span>            动漫          </p>          <div className="bg-white text-xs text-slate-500 leading-2 mt-4 sm:text-base">            老套的降级打怪式剧情,但动画制作的品质还不错,适宜下饭          </div>        </div>        <div className='flex-none w-1/6 rounded-md sm:w-[5rem] sm:rounded-xl overflow-hidden bg-slate-100 relative aspect-[85/113]'>          <Image            src='https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2551222097.webp'            layout='fill'            objectFit="cover"            alt='鬼灭之刃'            className="hover:opacity-75 duration-300 ease-in-out"          />        </div>      </div>    </section>  )}

图片咱们应用了 next/image 组件,咱们须要批改一下 next.config.js 文件,增加图片域名配置

// next.config.jsmodule.exports = {  reactStrictMode: true,  images: {    domains: [      'img1.doubanio.com',      'img2.doubanio.com',      'img3.doubanio.com',      'img9.doubanio.com',    ],  },}

而后咱们能够增加 <Card /> 组件到 pages/index.tsx 中,看一下成果

// pages/index.tsximport type { NextPage } from 'next'import Head from 'next/head'import { Card } from '../components/card'const Home: NextPage = () => {  return (    <div className='mx-[3.5rem] min-w-[15rem] max-w-full sm:mx-auto sm:w-[30rem] font-sans'>      <Head>        <title>我看过的</title>        <meta name="viewport" content="width=device-width" />        <link rel="icon" href="/favicon.ico" />      </Head>      <h1 className='text-slate-300 flex justify-between items-center text-xl sm:text-5xl my-8 sm:my-20'>        <span>我看过的</span>        <span className='text-xs sm:text-xl'>电影 / 动漫 / 剧 / 书</span>      </h1>      <div>        <Card />        <Card />        <Card />      </div>    </div>  )}export default Home

至此利用的外观曾经初见雏形了,接下来咱们为 Card 增加一些 props,首先咱们来定义 props 的类型,咱们在根目录下新建一个 types.ts 文件

// types.tsexport type Record = {  date: string  title: string  score: 1 | 2 | 3 | 4 | 5  comment?: string  year: number  img: string  type: 'movie' | 'tv' | 'anime' | 'book'}

之所以放在根目录,是因为等一下创立 API 时也会用到这个类型

接下来咱们批改一下 Card 组件,将数据局部替换成 props

// components/card.tsximport Image from 'next/image'import { Record } from '../types'type Props = Recordconst Score: React.FC<Pick<Props, 'score'>> = ({ score }) => {  switch (score) {    case 1:      return <big className='font-bold text-black-500'> 不看也罢</big>    case 2:      return <big className='font-bold text-green-500'> 无聊</big>    case 3:      return <big className='font-bold text-blue-500'> 还行</big>    case 4:      return <big className='font-bold text-violet-500'> 值得一看</big>    case 5:      return <big className='font-bold text-orange-500'> 神作!</big>  }}const renderType = (type: Props['type']) => {  const typeMap = {    movie: '电影',    tv: '剧',    book: '书',    anime: '动漫',  }  return typeMap[type] ?? '未知'}export const Card: React.FC<Props> = (props) => {  return (    <section className='relative before:content-[""] before:border-l-2 before:absolute before:inset-y-0 before:-left-9 before:translate-x-[0.44em] pb-10 first:before:top-1 last:before:bottom-10'>      <p className='text-slate-400 text-xs mb-2 sm:text-base sm:mb-3 relative'>        { new Date(props.date).toLocaleDateString() }        <i className='absolute w-4 h-4 rounded-full bg-slate-200 -left-9 top-1/2 translate-y-[-50%]' />      </p>      <div className="flex items-start">        <div className="flex-1 mr-2">          <p className='text-md mb-2 sm:text-2xl sm:mb-3 leading-6 text-slate-900'>            { props.title }            <span className='text-slate-400'>({props.year})</span>          </p>          <p className='text-xs sm:text-base text-slate-700'>            <span className='text-slate-400'>评分:</span>            <Score score={ props.score } />          </p>          <p className='text-xs sm:text-base text-slate-700'>            <span className='text-slate-400'>分类:</span>            { renderType(props.type) }          </p>          <div className="bg-white text-xs text-slate-500 leading-2 mt-4 sm:text-base">            { props.comment }          </div>        </div>        <div className='flex-none w-1/6 rounded-md sm:w-[5rem] sm:rounded-xl overflow-hidden bg-slate-100 relative aspect-[85/113]'>          <Image            src={ props.img }            layout='fill'            objectFit="cover"            alt={ props.title }            className="hover:opacity-75 duration-300 ease-in-out"          />        </div>      </div>    </section>  )}

设置 LeanCloud Storage

LeanCloud 是一个 BaaS(Backend as a Service)^Backend as a Service: [后端即服务] 平台,倡议注册国际版 LeanCloud,可免实名认证

首先,咱们须要在 Data Storage 中创立一个 Class

  • 将 Class 命名为 Records
  • 增加 imgtitletypecommenttype 字段,它们的类型都是 String
  • 增加 yearscore 字段,将他们的类型设置为 Number

创立读取数据 API

当初咱们来创立一个 API 用于读取 LeanCloud 中的数据

首先咱们须要装置 LeanCloud JS SDK

$ npm install leancloud-storage --save

而后咱们须要将 LeanCloud 的配置信息增加到 .env.local 中,配置信息能够在 "Settings" -> "App keys" 中找到

LEANCLOUD_APP_ID="{replace-your-app-id}"LEANCLOUD_APP_KEY="{replace-to-your-app-key}"LEANCLOUD_SERVER_URL="{replace-to-your-server-url}"

新建 pages/api/records.ts

// pages/api/records.tsimport AV from 'leancloud-storage'import { Record } from '../../types'import type { NextApiRequest, NextApiResponse } from 'next'export default async function handler(  _req: NextApiRequest,  res: NextApiResponse<Record[]>) {  try {    const {      LEANCLOUD_APP_ID: appId,      LEANCLOUD_APP_KEY: appKey,      LEANCLOUD_SERVER_URL: serverURL,    } = process.env    if (!appId || !appKey || !serverURL) {      res.status(500).json({ error: 'Missing Leancloud config' } as any)      return    }    AV.init({ appId, appKey, serverURL })    const query = new AV.Query('Record')    const data = await query.find()    const records: Record[] = data.reverse().map(x => {      const json = x.toJSON()      return {        date: json.createdAt,        title: json.title,        score: json.score,        comment: json.comment,        year: json.year,        img: json.img,        type: json.type,      }    })    res.status(200).json(records)  } catch (e: any) {    res.status(500).json(e)  }}

接着咱们批改一下 pages/index.tsx,从 /api/records 接口获取数据

// pages/index.tsximport type { NextPage } from 'next'import Head from 'next/head'import { useEffect, useState } from 'react'import { Card } from '../components/card'import { Record } from '../types'const Home: NextPage = () => {  const [ records, setRecords ] = useState<Record[] | null>(null)  useEffect(() => {    fetch('/api/records')      .then(res => res.json())      .then(setRecords)  }, [])  const renderCards = () => {    if (!records) {      return null    }    return records.map(x => <Card key={ `${x.date}${x.title}${x.year}` } { ...x } />)  }  return (    <div className='mx-[3.5rem] min-w-[15rem] max-w-full sm:mx-auto sm:w-[30rem] font-sans'>      <Head>        <title>我看过的</title>        <meta name="viewport" content="width=device-width" />        <link rel="icon" href="/favicon.ico" />      </Head>      <h1 className='text-slate-300 flex justify-between items-center text-xl sm:text-5xl my-8 sm:my-20'>        <span>我看过的</span>        <span className='text-xs sm:text-xl'>电影 / 动漫 / 剧 / 书</span>      </h1>      <div>        { renderCards() }      </div>    </div>  )}export default Home

部署到 Vercel

咱们的利用曾经能够在本地运行了,下一步让咱们把它部署到 Vercel 上。

  1. 将咱们的代码提交到 git 仓库(如 Github、GitLab、BitBucket)
  2. 将 Next.js 我的项目导入 Vercel
  3. 在导入期间设置环境变量
  4. 点击“Deploy”

Vercel 将自动检测您正在应用 Next.js 并为您的部署启用正确的设置。最初,您的应用程序部署在相似 xxx.vercel.app 的 URL 上。

增加数据

当初咱们的利用曾经运行在公网上了,咱们能够在 LeanCloud 上尝试增加几条数据,而后刷新页面看看是否可能失常显示。

总结

在本教程中,咱们可能创立一个 Next.js 应用程序,通过 Tailwind CSS 丑化界面,显示从 LeanCloud 动静获取的数据列表。

  • 查看源码
  • 查看演示
本文转载自我的博客 https://www.zehao.me/full-sta...