1
头图
Learn how to create a database with LeanCloud and an application with a server with Next.js.

foreword

Through this tutorial you will learn:

  • Use LeanCloud as a free database
  • Develop an application with front and back ends using Next.js
  • Publish the app to Vercel
  • Easy styling with Tailwind

We will create an application for scoring film and television dramas, I will deploy it in rec.zehao.me , and I will put the complete source code in 2eha0/records

https://rec.zehao.me

Create a Next.js application

Create a project using the Next.js official template

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

The goal has already configured the following for you:

  • Next.js latest version
  • TypeScript
  • Tailwind CSS & Automatically strip unused class names
  • Next.js API Routing Example

Create front-end components

Now we can start creating components, pages/index.tsx is the entry file of the application, let's modify the overall layout first

 // pages/index.tsx
import 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

Next, we need to add a card component to the application to display the information of film and television works, create a new components/card.tsx file

 // components/card.tsx
import 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>
  )
}

We used the next/image component for the picture, we need to modify the next.config.js file and add the picture domain name configuration

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

Then we can add the <Card /> component to pages/index.tsx to see the effect

 // pages/index.tsx
import 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

So far, the appearance of the application has taken shape. Next, we add some props for Card cf888f2dff3b42b33a14fd99d5f9cc89---. First, let's define the type of props , we are in the root directory Create a new types.ts file

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

The reason why it is placed in the root directory is that this type will also be used when creating an API later.

Next, let's modify the Card component and replace the data part with props

 // components/card.tsx
import Image from 'next/image'
import { Record } from '../types'

type Props = Record

const 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>
  )
}

Set up LeanCloud Storage

LeanCloud is a BaaS (Backend as a Service)^ Backend as a Service: [Backend as a Service ] platform, it is recommended to register the international version of LeanCloud , which can be exempted from real-name authentication

First, we need to create a Class in Data Storage

  • Name the Class Records
  • Add img , title , type , comment and type fields, their types are String
  • Add year , score fields and set their type to Number

Records Class

Create read data API

Now let's create an API for reading data from LeanCloud

First we need to install the LeanCloud JS SDK

 $ npm install leancloud-storage --save

Then we need to add the configuration information of LeanCloud to .env.local , the configuration information can be found in "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}"

New pages/api/records.ts

 // pages/api/records.ts
import 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)
  }
}

Then we modify pages/index.tsx and get data from /api/records interface

 // pages/index.tsx
import 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

Deploy to Vercel

Our application is ready to run locally, let's deploy it to Vercel next.

  1. Commit our code to a git repository (eg Github, GitLab, BitBucket)
  2. Import the Next.js project into Vercel
  3. Setting environment variables during import
  4. Click "Deploy"

Vercel will automatically detect that you are using Next.js and enable the correct settings for your deployment. Finally, your application is deployed at a URL like xxx.vercel.app.

adding data

Now that our application is running on the public network, we can try to add a few pieces of data on LeanCloud, and then refresh the page to see if it can be displayed normally.

Summarize

In this tutorial, we were able to create a Next.js application that displays a list of data dynamically fetched from LeanCloud with Tailwind CSS beautification.

This article is reproduced from my blog https://www.zehao.me/full-stack-app-nextjs-leancloud-tailwind/

张泽豪
235 声望4 粉丝