iT邦幫忙

2024 iThome 鐵人賽

DAY 10
0
Modern Web

使用Nextjs及Xata全端開發 打造屬於你的討論版SaaS系列 第 10

[Day10] 來接Database吧 - Xata初始化及Nextjs的SSR

  • 分享至 

  • xImage
  •  

先來定義服務要的Schema

在專案的src/lib/database/schema.json 建立以下檔案

{
  "tables": [
    {
      "name": "services",
      "columns": [
        { "name": "name", "type": "string" }, // 服務名稱
        {
          "name": "topLinks", // 頂端連結
          "type": "json",
          "notNull": true,
          "defaultValue": "[]"
        },
        {
          "name": "headLinks", // 標題下方的連結
          "type": "json",
          "notNull": true,
          "defaultValue": "[]"
        },
        { "name": "description", "type": "text" }, // 描述
      ]
    },
    {
      "name": "threads",
      "columns": [
        {
          "name": "title", //討論串標題
          "type": "string",
          "defaultValue": "Untitled"
        },
        {
          "name": "name", // 發文者名稱
          "type": "string",
          "defaultValue": "anonymous"
        },
        { "name": "content", "type": "text" }, // 內文
        { "name": "image", "type": "file" }, // 圖片
        { "name": "youtubeID", "type": "string" }, // 影片
        { "name": "replyAt", "type": "datetime" }, // 討論串最後回覆時間 
        { "name": "userId", "type": "string" }, // 發文者ID
        { "name": "userIp", "type": "string" } // 發文者IP
      ]
    },
    {
      "name": "replies",
      "columns": [
        {
          "name": "name", // 回應者名稱 
          "type": "string",
          "defaultValue": "anonymous"
        },
        { "name": "content", "type": "text" }, // 內文 
        { "name": "image", "type": "file" }, // 圖片 
        { "name": "youtubeID", "type": "string" }, // 影片
        { "name": "thread", "type": "link", "link": { "table": "threads" } }, // 關聯到Thead 用來確定reply是reply到哪一個thread上
        { "name": "userId", "type": "string" }, // 發文者ID
        { "name": "userIp", "type": "string" } // 發文者IP
      ]
    }
  ]
}

建立scripts

接著我們需要建立兩個script

在你的package.json內 建立兩個script

  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "xata:gen": "xata codegen --output src/xata.ts", // 用來將現在的db schema同步到本地
    "xata:upload": "xata schema upload src/lib/database/schema.json" // 用來將定義好的schema推送到DB上
  }

然後我們下指令

yarn xata:upload

他會告訴你哪些table及column被增加跟刪除

https://ithelp.ithome.com.tw/upload/images/20240830/2016879667Mn47jLDS.png

然後將現在的DB 在本地端建立一個client

yarn xata:gen

你應該可以注意到你本地端有一個被修改的檔案

在src/xata.ts

// Generated by Xata Codegen 0.30.0. Please do not edit.
import { buildClient } from "@xata.io/client";
import type {
  BaseClientOptions,
  SchemaInference,
  XataRecord,
} from "@xata.io/client";

const tables = [
  {
    name: "services",
    columns: [
      { name: "name", type: "string" },
      { name: "topLinks", type: "json", notNull: true, defaultValue: "[]" },
      { name: "headLinks", type: "json", notNull: true, defaultValue: "[]" },
      { name: "description", type: "text" },
    ],
  },
  {
    name: "threads",
    columns: [
      { name: "title", type: "string", defaultValue: "Untitled" },
      { name: "name", type: "string", defaultValue: "anonymous" },
      { name: "content", type: "text" },
      { name: "image", type: "file" },
      { name: "youtubeID", type: "string" },
      { name: "replyAt", type: "datetime" },
      { name: "userId", type: "string" },
      { name: "userIp", type: "string" },
    ],
    revLinks: [{ column: "thread", table: "replies" }],
  },
  {
    name: "replies",
    columns: [
      { name: "name", type: "string", defaultValue: "anonymous" },
      { name: "content", type: "text" },
      { name: "image", type: "file" },
      { name: "youtubeID", type: "string" },
      { name: "thread", type: "link", link: { table: "threads" } },
      { name: "userId", type: "string" },
      { name: "userIp", type: "string" },
    ],
  },
] as const;

export type SchemaTables = typeof tables;
export type InferredTypes = SchemaInference<SchemaTables>;

export type Services = InferredTypes["services"];
export type ServicesRecord = Services & XataRecord;

export type Threads = InferredTypes["threads"];
export type ThreadsRecord = Threads & XataRecord;

export type Replies = InferredTypes["replies"];
export type RepliesRecord = Replies & XataRecord;

export type DatabaseSchema = {
  services: ServicesRecord;
  threads: ThreadsRecord;
  replies: RepliesRecord;
};

const DatabaseClient = buildClient();

const defaultOptions = {
  databaseURL:
    "https://EricWu-s-workspace-2lkpjt.ap-southeast-2.xata.sh/db/ithome",
};

export class XataClient extends DatabaseClient<DatabaseSchema> {
  constructor(options?: BaseClientOptions) {
    super({ ...defaultOptions, ...options }, tables);
  }
}

let instance: XataClient | undefined = undefined;

export const getXataClient = () => {
  if (instance) return instance;

  instance = new XataClient();
  return instance;
};

然後我們就可以來接看看Xata了

我們到先到xata的後台去建立一筆service

切到services這張表

https://ithelp.ithome.com.tw/upload/images/20240830/201687968aRiil8roy.png

然後點選Add a record

然後根據你的需要塞入內容

https://ithelp.ithome.com.tw/upload/images/20240830/2016879633KfWGgdZN.png

注意topLinks及headLinks必須要是正確的JSON

[
  {
    "name": "Nextjs",
    "url": "https://nextjs.org/"
  }
]

然後建立取得service的function

在以下路徑建立檔案

src/lib/database/service.ts

import { XataClient, ServicesRecord } from "../../xata";

interface IGetThreads {
  serviceId: string;
}

export const getService = async ({
  serviceId,
}: IGetThreads): Promise<ServicesRecord | null> => {
  try {
    const xata = new XataClient({
      branch: serviceId,
      apiKey: process.env.XATA_API_KEY,
    });

    return await xata.db.services.getFirst();
  } catch (error) {
    console.error(error);
    return null;
  }
};

使用這個function來做SSR

修改你的src/app/service/[serviceId]/page.tsx檔案

// 增加這兩行
import { getService } from "@/lib/database/service";
import { notFound } from "next/navigation";

然後把我們原本的假資料換掉

// const service: IService = {
//   id: params.serviceId,
//   name: "My Service",
//   topLinks: [{ name: "Nextjs", url: "https://nextjs.org/" }],
//   headLinks: [
//     {
//       name: "鐵人賽",
//       url: "https://ithelp.ithome.com.tw/users/20168796/ironman/7445",
//     },
//     { name: "ithome", url: "https://ithelp.ithome.com.tw/" },
//   ],
//   description: "This is an example service providing various utilities.",
// };

const service = await getService({ serviceId: params.serviceId });
if (!service) {
    return notFound();
}

你會發現Title的service及PostCard的型別有錯
這是很正常的

修改這裡的PostCard

<PostCard description={service.description || ""} />

到Title將IService換掉

import Link from "next/link";
import { ExternalLink, Link2 } from "lucide-react";
import { ServicesRecord } from "@/xata";

export interface ILinkItem {
  name: string;
  url: string;
}

interface TitleProps {
  service: ServicesRecord;
}

export default function Title({ service }: TitleProps) {
  return (
    <>
      <div className="absolute top-2 right-2 flex items-center space-x-2 text-xs">
        {service.topLinks.map((link: ILinkItem, index: number) => (
          <a
            key={index}
            href={link.url}
            target="_blank"
            className="text-gray-400 hover:text-gray-600 flex items-center"
          >
            {link.name} <ExternalLink className="ml-1 h-3 w-3" />
          </a>
        ))}
        <Link
          href={`/service/${service.id}`}
          className="text-gray-400 hover:text-gray-600 flex items-center"
        >
          Homepage <Link2 className="ml-1 h-3 w-3" />
        </Link>
      </div>
      <h1 className="text-3xl font-bold text-center mb-2 mt-6 text-black">
        {service.name}
      </h1>
      <div className="flex justify-center mb-2 space-x-2">
        {service.headLinks.map((link: ILinkItem, index: number) => (
          <Link
            key={index}
            href={link.url}
            passHref
            target="_blank"
            className="text-blue-500 text-md py-1 px-2 rounded shadow-md border-2 border-blue-400 hover:bg-blue-500 hover:border-blue-500 hover:text-white transition duration-300"
          >
            {link.name}
          </Link>
        ))}
      </div>
    </>
  );
}

現在啟動服務吧

yarn dev

來看看現在的樣子

http://localhost:3000/service/main

這個網址去

你會發現畫面出現了

你可以去修改database裡面service的內容 看看畫面會怎麼變化

總結

欸? 我不用接API喔

對 因為我們使用SSR 資料是先在後端準備好 變成html之後直接拿給前端 因此不需要接api

const service = await getService({ serviceId: params.serviceId });
  if (!service) {
    return notFound();
  }

你應該可以注意到這裡我們使用了await 這在原本的React是不被允許的

為什麼我們是到/service/main這裏

你可以將main換成其他的字 會notfound

這是因為我們

const xata = new XataClient({
   branch: serviceId, //這裏根據serviceId指定了branch的名稱
   apiKey: process.env.XATA_API_KEY,
});

我們現在只有main一個branch
因此只會有一個service 其他的都會notfound

明天我們繼續接thread及reply的database


上一篇
[Day 9] 準備我們的資料庫 - Xata
下一篇
[Day11]要串接新增及讀取Thread - Nextjs的api及Xata的DB串接
系列文
使用Nextjs及Xata全端開發 打造屬於你的討論版SaaS30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言