如何使用Next.js,Xata和Cloudinary构建完整的堆栈内容管理系统
#javascript #网络开发人员 #serverless #jamstack

在开发现代Web应用程序时,管理网站的内容始终具有挑战性。您可以通过数据库或硬编码的JSON文件管理内容。但是,在创建大型项目时,管理文件和数据库变得复杂。在这些情况下,使用Content Management System(CMS)是简单有效的方法之一。

CMS是用于与团队创建和管理内容的软件。有许多第三方CMS提供商,例如理智和Strapi。但是,有人曾经说过:“如果您想做正确的事,那就自己做。”
在本文中,我将教您如何使用最新的Web开发技术创建自己的CMS。您将学习使用数据库和云来管理您的Web应用程序的内容和数字资产。

final demo of the application - ecommerce cms

如果您想直接潜入代码。

先决条件

在进入本教程之前,您应该:

  • 对HTML和CSS有着深入的了解
  • 知道JavaScript的ES6 syntax
  • 了解反应及其工作原理

您将建造什么

您将构建一个CMS来管理电子商务网站的内容。有各种types of content management systems。在本教程中,我们将创建一个无头CM,其后端系统连接到数据库,用于使用Web界面管理内容。您将通过API端点公开前端的内容,并根据自己喜欢的数据使用数据。

您将使用的技术

  • Next.js - 用于构建Fullstack Jamstack应用程序的JavaScript框架。它扩展了编写无服务器代码的React的功能
  • Tailwind CSS - 使用类型标记
  • 的实用程序优先级CSS框架
  • Xataâ无服务器数据库,可让您创建jamstack应用程序,而不必担心部署或缩放问题
  • Cloudinary - 一个用于管理媒体资产的平台,例如Images

您需要CloudinaryXataNetlify帐户。这些服务提供了可以用于此项目的慷慨的免费层。

入门

为了帮助您,我创建了一个starter codesandbox;分叉并开始编码。

运行以下命令开始本地开发环境。

yarn create next-app my-ecommerce-cms -e https://github.com/giridhar7632/ecommerce-cms-starter
# or
npx create-next-app my-ecommerce-cms -e https://github.com/giridhar7632/ecommerce-cms-starter

入门代码包括所有基本依赖性,例如尾风CSS,XATA等。它还包含使用尾风CSS样式的几个现成的前端组件。
导航到项目目录并在安装依赖项后启动开发服务器:

cd my-ecommerce-app

yarn dev

现在,您将可以看到该应用在http://localhost:3000上运行。您还可以在CodeSandbox中查看预览。

preview of the starter project

入门代码

我已经在启动代码中创建了应用程序的UI,以使过程更简单。项目内部的主要内容是:

  1. **/****pages** 目录: next.js允许file system-based routing。它将此目录中的任何内容都视为路由,而/pages/api中的文件则为API端点
  2. **/components** 目录:如果您熟悉React,您可能已经知道一切都是可重复使用的组件。该目录包含布局,表单和所需的所有内容之类的组件。在文件夹中查看以了解结构
  3. **/pages/products.js** 文件:显示产品的路线。您可以在/components/Products文件夹中找到此处使用的组件
  4. **/pages/product** 目录:您可以创建动态路由,用于使用Next.js生成静态页面。您可以使用括号语法([slug])定义路由以使其动态。您可以在此文件夹中找到文件[id].js。因此,您可以将id作为该路线内的参数

让我们配置数据库并添加API端点以使UI Interactive。

连接到数据库

Xata是您将用于开发此CMS的无服务器数据库。使用XATA,您可以将所有数据存储在完整配置的数据库中,而不必担心部署和缩放。
XATA提供了一个CLI工具@xata.io/cli,用于直接从终端创建和管理数据库。

运行下面的命令以在全球安装XATA CLI并在项目中使用。

yarn add -g @xata.io/cli
# or
npm i -g @xata.io/cli

安装后,使用以下命令为XATA生成API键。

xata auth login

XATA将提示您从以下选项中进行选择:

Xata CLI asking to create API key

对于此项目,选择在浏览器中创建一个新的API键 ;当浏览器窗口弹出时,给API键一个名称。

Creating a new Xata API key inside browser

工作区象征您的团队,它们是创建数据库的XATA的起点。登录到帐户后,您可以创建一个。 数据库中的数据表格的形式组织,它们具有在数据库中形成严格架构的。

Preview of workspace and database in Xata

创建表

要将产品添加到数据库中,首先要构造一个表格及其用于数据结构的模式。 产品表将有几列,例如:

  • idâxata维护的自动列
  • name - 包含产品名称的字符串
  • description - 一个带有产品描述的字符串
  • price - 包含产品值的数字
  • stock
  • 剩下的项目数量
  • thumbnail产品的图像URL
  • media产品的媒体多个URL

在XATA中,您可以使用浏览器或CLI构造表。现在,让我们用CLI构建桌子。本课程的数据库架构已经在启动代码的schema.json文件中。

/schema.json

{
  "formatVersion": "1",
  "tables": [
    {
      "name": "products",
      "columns": [
        {
          "name": "name",
          "type": "string"
        },
        {
          "name": "description",
          "type": "text"
        },
        {
          "name": "price",
          "type": "float"
        },
        {
          "name": "stock",
          "type": "int"
        },
        {
          "name": "thumbnail",
          "type": "string"
        },
        {
          "name": "media",
          "type": "multiple"
        }
      ]
    }
  ]
}

现在,执行以下命令以使用给定的架构填充数据库。

xata init --schema=schema.json --codegen=utils/xata.js

Xata database creation process in CLI

要创建一个新数据库,您必须首先回答终端中的几个问题。然后,CLI从文件中读取架构并创建数据库。它还生成了用于使用XATA客户端的/utils/xata.js文件。您将使用此客户端与XATA进行通信。

使用xata random-data命令在数据库内生成一些随机数据。在浏览器内打开您的Xata workspace以查看您的数据库。

Xata database with randomly generated data

创建产品数据

现在,让我们开发与产品数据交互的API端点。在/pages/api目录中,添加一个名为products的文件夹和称为createProduct.js的文件夹。您将使用此文件来处理POST请求在数据库中创建产品。

/pages/api/products/createProduct.js

import { getXataClient } from "../../../utils/xata";
const xata = getXataClient();
const handler = async (req, res) => {
  // create method is used to create records in database
  try {
    await xata.db.products.create({ ...req.body });
    res.json({ message: "Success 😁" });
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
};
export default handler;

Xata Client提供了访问数据库的方法;您可以使用表的名称来执行例如xata.db.<table-name>之类的操作。您还可以使用表的create()在数据库内创建记录。
您可以在/components/ProductForm中看到以添加所需产品数据的形式。我使用react-hook-form以多个步骤处理表单数据。

Form for creating a new product inside the database

将图像上传到云

在将新产品添加到数据库之前,您将首先将图像上传到Cloudinary,并使用产品数据存储图像链接。拥有云帐户后,请按照以下步骤启用图像上传。
将媒体上传到Cloudinary有两种方法:

  1. 签名的预设
  2. 未签名的预设。 创建一个未签名的预设,允许用户将图像上传到您的云。 转到您的Cloudinary仪表板并导航到Settings > Upload > Add upload preset

Adding a new upload preset inside Cloudinary

在上载预设中配置名称签名模式文件夹。添加文件夹是可选的,但我建议您将所有上传的图像放在一个地方。您可以在their documentation中了解有关Cloudinary上传预设的更多信息。

Configuring the new upload preset inside cloudinary

保存后,转到上一页,您可以在其中找到新的未签名预设。

The new preset added

使用新预设,您现在可以从前端上传图像到云。 /components/ProductForm目录中的ThumbnailUpload.js文件将处理图像上传,并将图像URL添加到产品数据中。在文件中添加以下代码。

/components/ProductForm/ThumbnailUpload.js

import React, { useState } from "react";
import Button from "../common/Button";

const ThumbnailUpload = ({ defaultValue, setValue }) => {
  const [imageSrc, setImageSrc] = useState(defaultValue);
  const [loading, setLoading] = useState(false);
  const [uploadData, setUploadData] = useState();
  const handleOnChange = (changeEvent) => {
    // ...
  };

  const handleUpload = async (uploadEvent) => {
    uploadEvent.preventDefault();
    setLoading(true);
    const form = uploadEvent.currentTarget;
    const fileInput = Array.from(form.elements).find(
      ({ name }) => name === "file"
    );
    try {
      const formData = new FormData();
      // specifying cloudinary upload preset
      formData.append("upload_preset", "vnqoc9iz");
      for (const file of fileInput.files) {
        formData.append("file", file);
      }
      const res = await fetch(
        "https://api.cloudinary.com/v1_1/scrapbook/image/upload",
        {
          method: "POST",
          body: formData,
        }
      );
      const data = await res.json();
      setImageSrc(data.secure_url);
      // adding the thumbnail URL to te main form data
      setValue("thumbnail", data.secure_url);
      setUploadData(data);
    } catch (error) {
      console.log(error);
    }
    setLoading(false);
  };

  return (
    <form onSubmit={handleSubmit}>
      // ...
    </form>
  );
};
export default ThumbnailUpload;

上面的代码执行以下操作:

  • 从表单输入中提取文件,与Form data一起向Cloudinary API发送帖子请求,然后返回文件的公共URL
  • 使用setValue react-hook-form的方法将图像URL添加到产品表单数据
  • ,还更新组件的状态以使UI Interactive

同样,您可以使用Promise.all方法添加多个图像上传来处理产品媒体。

/components/ProductForm/MediaUpload.js

// ...
const MediaUpload = ({ defaultValues = [], setValue }) => {
  const [imageSrc, setImageSrc] = useState([...defaultValues]);
  const [loading, setLoading] = useState(false);
  const [uploadedData, setUploadedData] = useState(false);
  const handleOnChange = (changeEvent) => {
    // ...
  };

  const handleUpload = async (uploadEvent) => {
    uploadEvent.preventDefault();
    setLoading(true);
    const form = uploadEvent.currentTarget;
    const fileInput = Array.from(form.elements).find(
      ({ name }) => name === "file"
    );
    try {
      // adding upload preset
      const files = [];
      for (const file of fileInput.files) {
        files.push(file);
      }
      const urls = await Promise.all(
        files.map(async (file) => {
          const formData = new FormData();
          formData.append("file", file);
          formData.append("upload_preset", "vnqoc9iz");
          const res = await fetch(
            "https://api.cloudinary.com/v1_1/scrapbook/image/upload",
            {
              method: "POST",
              body: formData,
            }
          );
          const data = await res.json();
          return data.secure_url;
        })
      );
      setImageSrc(urls);
      setValue("media", urls);
      setUploadedData(true);
    } catch (error) {
      console.log(error);
    }
    setLoading(false);
  };
  return <form onSubmit={handleUpload}>...</form>;
};
export default MediaUpload;

现在,您应该通过向您创建的端点发送帖子请求将产品添加到数据库中。

/components/Product/AddProduct.js

import { Close } from "../common/icons/Close";
import ProductForm from "../ProductForm";

const AddProduct = ({ props }) => {
  const [isOpen, setIsOpen] = useState(false);
  const handleClose = () => setIsOpen(false);
  const handleOpen = () => setIsOpen(true);
  const onFormSubmit = async (data) => {
    try {
      await fetch(`${baseUrl}/api/products/createProduct`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(data),
      }).then(() => {
        handleClose();
        window.location.reload();
      });
    } catch (error) {
      console.log(error);
    }
  };

  return (
    // ...
  );
};

现在,尝试使用表单添加新产品。您可以看到上传到云和XATA中创建的记录的文件。

Xata database records created using the UI

查询产品数据

用于显示所有产品,您将使用客户端渲染。为了显示每种产品的详细信息,让我们使用动态路由使用服务器端渲染。创建一个用于获取所有产品数据的API端点getProducts.js

âtrong警告:建议不要在客户端编程中使用XATA。从浏览器中检索数据时,它可能会显示您的API密钥。因此,我们将从API路线内的XATA获取所有数据并将其交付给客户端。

/pages/api/products/getProducts.js

import { getXataClient } from "../../../utils/xata";
const xata = getXataClient();
const handler = async (req, res) => {
  // getMany or getAll method can be used to create records in database
  try {
    const data = await xata.db.products.getAll();
    res.json({ message: "Success 😁", data });
  } catch (error) {
    res.status(500).json({ message: error.message, data: [] });
  }
};
export default handler;

在这里,您使用表的getAll方法一次获取所有记录。有关从表获取数据的更多方法,请参阅Xata docs
/pages/Products.js文件中,添加以下代码以获取API的数据。

 /pages/Products.js

import { useEffect, useState } from "react";
// ...

function Products() {
  const [loading, setLoading] = useState(false);
  const [products, setProducts] = useState([]);
  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      try {
        const res = await fetch(`/api/products/getProducts`);
        const { data } = await res.json();
        setProducts(data);
      } catch (error) {
        console.log(error);
      }
      setLoading(false);
    };
    fetchData();
  }, []);
  return (
    // ...
  );
}

// ...

在上面的代码中,您正在从之前创建的API中获取useEffect挂钩内部的数据。保存您的代码,然后转到浏览器。您可以看到以表格形式显示的产品数据。

Displaying all the product data in the UI, fetching from database

单击详细信息时,您将被重定向到/product/<some-product-id>。到目前为止,您会看到一个空页面;让我们继续创建添加数据。

生成静态页面以获取产品详细信息

您可以在/pages目录内找到/product文件夹。如前所述,我添加了[id].js文件以生成动态路由。您将使用路线的参数访问产品ID。有关动态的更多信息,请参阅Next.js documentation
在以下代码中,首先,您要获取所有项目以添加getStaticPaths的路径。然后,在getStaticProps中,您可以从路由参数(params)获得id,并将单个产品数据传递到页面上。

/pages/product/[id].js

import React from "react";
import { getXataClient } from "../../utils/xata";
// ...

const xata = getXataClient();
// props passed in the server while generating the page
function Product({ product }) {
  return (
    // ...
  );
}
export default Product;

// fetching data from xata in server for generating static pages
export async function getStaticProps({ params }) {
  // getting filtered data from Xat
  const data = await xata.db.products
    .filter({
      id: params.id,
    })
    .getFirst();
  return {
    props: { product: data },
  };
}
// pre-rendering all the static paths
export async function getStaticPaths() {
  const products = await xata.db.products.getAll();
  return {
    paths: products.map((item) => ({
      params: { id: item.id },
    })),
    // whether to run fallback incase if user requested a page other than what is passed inside the paths
    fallback: true,
  };
}

杀死终端,然后再次运行yarn dev命令。打开浏览器,尝试查看任何产品的详细信息。现在,您可以看到该页面准备好数据。这是我的:

Statically generated product details page

更新产品

让我们添加更新产品的功能。创建一个API路由,以更新无服务器功能的数据。创建一个新文件updateProduct.js,以更新产品API目录中的数据。就像创建一样,您可以使用表的update方法在XATA中编辑记录。 update方法将使用id识别记录,并使用data更新列。

/pages/api/products/updateProduct.js

import { getXataClient } from "../../../utils/xata";
const xata = getXataClient();
const handler = async (req, res) => {
  // using update method to update records in database
  const { id, ...data } = req.body;
  try {
    await xata.db.products.update(id, { ...data });
    res.json({ message: "Success 😁" });
  } catch (error) {
    console.log(error);
    res.status(500).json({ message: error.message });
  }
};
export default handler;

导航到UpdateProduct.js内部的UpdateProduct.js组件以添加功能以更新产品数据。您将与产品ID和更新的数据一起向API端点发送 PUT 请求。

/components/Product/UpdateProduct.js

import { baseUrl } from "../../utils/config";
// ...

const UpdateProduct = ({ product, ...props }) => {
  // ...
  const onFormSubmit = async (data) => {
    try {
      await fetch(`${baseUrl}api/products/updateProduct`, {
        method: "PUT",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ id: product.id, ...data }),
      }).then(() => {
        handleClose();
        window.location.reload();
      });
    } catch (error) {
      console.log(error);
    }
  };

  return (
    // ...
  );
};
export default UpdateProduct;

尝试使用表单更新产品数据。您将在“产品详细信息”页面和XATA表中看到更新的数据。

删除产品

要从数据库中删除特定记录,您可以将delete方法与产品的id一起使用。在/page/api/products文件夹中创建一个新文件deleteProduct.js

/pages/api/products/deleteProduct.js

import { getXataClient } from "../../../utils/xata";
const xata = getXataClient();
const handler = async (req, res) => {
  // use delete method for deleting the records in database
  const { id } = req.body;
  try {
    await xata.db.products.delete(id);
    res.json({ message: "Success 😁" });
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
};
export default handler;

现在,在DeleteProduct.js组件中添加handleDelete函数。

/components/Product/DeleteProduct.js

import { baseUrl } from "../../utils/config";
// ...

const DeleteProduct = ({ productId }) => {
  // ...
  const handleDelete = async () => {
    try {
      await fetch(`${baseUrl}/api/products/deleteProduct`, {
        method: "DELETE",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ id: productId }),
      }).then(() => {
        handleClose();
        window.location.replace("/products");
      });
    } catch (error) {
      console.log(error);
    }
  };

  return (
    // ...
  );
};
export default DeleteProduct;

您可以尝试从“产品详细信息”页面删除产品。如果有效的话,您会很好。

部署到Netlify

要在整个Internet上提供此应用程序,请将其部署到云服务。 Netlify是该项目的理想服务提供商,因为它支持XATA和Next.js开箱即用。使用npm在全球安装NetLify CLI,以从您的终端部署该应用程序。使用您的NetLify帐户登录到CLI。

# installing the Netlify CLI globally
npm i -g netlify-cli

# logging into your Netlify account
ntl login

成功登录后,尝试在项目文件夹中运行ntl init以配置NetLify。回答提示的问题,您将拥有已部署的网站的URL。这里是mine

Netlify configuration using netlify-cli

如果您遇到任何问题,请在this guide之后尝试修复它们。

结论

在本教程中,您已经成功地使用了Next.js,Xata和Cloudinary创建了一个CRUD应用程序。由于Jamstack结合了几种用于标记和API的技术,您还可以使用本教程中使用的其他服务。您可以通过使用您首选的框架创建电子商务网站的前端来进一步获取本教程。

资源