2023年11月06日
使用OceanBase和TypeScript创建无头CMS
如果您计划开始撰写博客或在网站上展示您的产品,您有两个主要选择。您可以使用 HTML、CSS 和 JavaScript 从头开始编写所有内容,创建数据库和界面来管理您的内容。然而,如果您不是经验丰富的程序员,这可能会很具有挑战性。更有效的选择是使用内容管理系统(CMS),它为您提供了管理内容和设计网站的工具,而无需费力。
有许多可用的 CMS,每种都有其优势。WordPress 是最受欢迎的 CMS,以其用户友好的界面和庞大的插件生态系统而闻名。Joomla 和 Drupal 为复杂网站提供了更强大的平台,尽管它们需要一些技术专长。对于初学者来说,Squarespace 和 Wix 是创建视觉吸引人的网站的理想选择,无需编写代码。
然而,这些 CMS 通常与呈现层绑定,限制了您的内容在其服务的平台上的传播。如果您希望通过移动应用、智能设备或语音助手等各种渠道触达受众,传统的 CMS 可能会显得力不从心。现代网络不仅包括网站,还包括移动和物联网设备、智能手表和车载娱乐系统。
什么是无头 CMS?
进入无头 CMS 的世界。无头 CMS 采用不同的方法,将内容管理与内容呈现分离。可以将其想象成一个指挥家,将您的内容传递到各种平台,而不受单一工具的限制。
无头 CMS 是一种后端内容存储库系统,通过 RESTful API(应用程序编程接口)或 GraphQL 促进内容存储、修改和传递。它被称为“无头”,因为它将“头”(前端或呈现层)与“身体”(后端或内容存储库)分离。
无头 CMS 带来了许多好处。它赋予开发人员使用其首选技术栈进行前端开发的自由,促进独特和定制的用户体验。它支持全渠道内容传递,在当今多设备世界中至关重要。然而,它并非没有挑战。缺乏视觉前端可能会使非技术用户感到不够直观。
它还需要一个熟练的开发团队来构建客户端,并且某些功能,如 SEO 和预览功能,可能需要额外的工作来实现。尽管存在这些挑战,但在正确的手中,无头 CMS 凭借其灵活性和适应性,成为一种强大的工具。
构建无头 CMS
既然我们已经了解了 CMS 的格局,现在是时候动手并深入本文的核心了:使用 TypeScript 和 OceanBase 构建无头 CMS。
你可能会问,为什么选择这个特定的技术栈?
让我们从 TypeScript 开始。它是 JavaScript 的超集,引入了静态类型,这是 JavaScript 传统上缺乏的特性。静态类型增强了代码的可靠性、可预测性和可维护性,使 TypeScript 成为我们构建强大无头 CMS 技术栈的重要组成部分。
您可能已经看到过 博客文章 将 JavaScript 称为“最受欢迎的后端语言”。但实际上,纯 JavaScript 很少用于后端开发。开发人员更倾向于 TypeScript,JavaScript 的静态类型兄弟。之所以偏爱 TypeScript,原因在于其独特的功能增强了代码的可靠性和可维护性,使其成为后端开发的更合适选择。
接下来是 OceanBase,我在大多数项目中选择的数据库。作为企业级高性能关系数据库,OceanBase 为无头 CMS 提供了强大的支持。其高可扩展性和可靠性确保我们的内容管理系统能够处理大量数据和流量,使其成为大规模应用的可靠选择。
最后,我选择了 Express.js 来构建 API 服务器。Express 是一个灵活的 Node.js Web 应用程序框架,为 Web 和移动应用程序提供了强大的功能集。其简单性和灵活性使其成为 CMS 项目的理想选择,使我们能够在最小的麻烦下构建强大的 API。
该项目的代码库已上传到这个 GitHub 仓库。您可以克隆并进行调试,或者使用 GitPod 部署它并查看它的运行情况。
设置项目
在开始之前,请确保您的计算机上已安装以下内容:
- Node.js:这是我们将用于运行服务器端代码的 JavaScript 运行时。
- TypeScript:正如我们之前讨论的那样,TypeScript 是带有静态类型检查的 JavaScript 超集。
- OceanBase:要在项目中使用 OceanBase,您首先需要一个运行中的 OceanBase 集群。您有几种选择。您可以在本地环境中安装 OceanBase,在云中启动虚拟机来运行它,或者使用 AWS 市场中的 OceanBase Cloud 来在几次点击中设置您的集群。设置好 OceanBase 服务器后,不要忘记获取用户名、密码、主机名和端口以供后续使用。
首先,让我们初始化我们的项目。打开终端,导航到您想要的目录,并运行以下命令来创建一个新的 Node.js 项目:
$ mkdir oceanbase-typescript && cd oceanbase-typescript $ npm init -y
这将在您的项目中创建一个新的目录,并在其中初始化一个新的 Node.js 应用程序。
接下来,让我们安装必要的依赖项。我们将需要 express
用于我们的服务器,TypeScript 用于编写我们的代码,以及一些其他包:
$ npm install express typescript ts-node nodemon
在这个项目中,我们将使用 TypeORM 作为对象关系映射(ORM)来将我们的项目与 OceanBase 连接。TypeORM 支持各种各样的数据库,包括 MySQL、Postgresql 和 MongoDB。由于 TypeORM 尚未为 OceanBase 提供驱动程序,而 OceanBase 兼容 MySQL,我们将使用 MySQL 驱动程序来连接我们的 OceanBase。
要使用 TypeORM 和 MySQL 驱动程序,我们需要在我们的项目中安装它们。运行以下命令:
$ npm install typeorm mysql
现在,让我们在项目的根目录中创建一个 src/index.ts
文件。这将是我们应用程序的入口点。以下是我们的 Express 服务器的基本设置:
import express from 'express'; const app = express(); const port = process.env.PORT || 3000; app.get('/', (req, res) => { res.send('Hello World!'); }); app.listen(port, () => { console.log(`Server is running on port ${port}`); });
为了运行服务器,我们将使用 ts-node
与 nodemon
结合,以确保我们的服务器在我们进行更改时重新启动。在您的 package.json
中添加一个 start
脚本:
"scripts": { "start": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/index.ts", // ... other scripts }
现在,您可以通过运行以下命令来启动服务器:
npm run start
您应该会看到控制台输出 Server is running on port 3000
。现在,向服务器发出 GET 请求,地址为 http://localhost:3000
,您应该会看到显示“Hello World!”。
就是这样!您现在已经设置了一个基本的 TypeScript 和 express.js 项目。在下一节中,我们将连接到我们的 OceanBase 数据库,并开始构建 CMS 功能。
连接到 OceanBase
我们的 CMS 将包括博客文章、标签和用户。其中每个对应于我们应用程序中的一个实体。让我们创建每个实体。
在 src/entity
目录中创建以下文件:
User.ts
BlogPost.ts
Tag.ts
让我们从 User.ts
开始。在我们的 CMS 中,用户将拥有一个 ID、一个名称和他们撰写的博客文章列表。以下是您可以定义此实体的方式:
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm" @Entity() export class User { @PrimaryGeneratedColumn() id: number @Column() firstName: string @Column() lastName: string @Column() age: number }
接下来,我们将在 Tag.ts
中定义 Tag
实体。标签将拥有一个 ID、一个名称和拥有此标签的博客文章列表。
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, } from "typeorm"; import { BlogPost } from "./BlogPost"; @Entity() export class Tag { @PrimaryGeneratedColumn() id: number; @Column() name: string; @ManyToMany(() => BlogPost, blogPost => blogPost.tags) blogPosts: BlogPost[]; }
最后,我们将在 BlogPost.ts
中定义 BlogPost
实体。博客文章将拥有一个 ID、一个标题、一个 slug(用于 URL)、文章内容、作者的引用以及标签列表。
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from "typeorm"; import { Tag } from "./Tag"; @Entity() export class BlogPost { @PrimaryGeneratedColumn() id: number; @Column() title: string; @Column() content: string; @Column() author: string; @Column() excerpt: string; @Column({ unique: true }) slug: string; @Column({ type: 'date' }) publishDate: Date; @Column({ type: 'enum', enum: ['DRAFT', 'PUBLISHED', 'ARCHIVED'], default: 'DRAFT' }) status: string; @ManyToMany(() => Tag, tag => tag.blogPosts, { cascade: true }) @JoinTable() tags: Tag[]; }
现在,我们已经设置了我们的实体,下一步是将我们的应用程序连接到 OceanBase 数据库。为此,我们将创建一个 DataSource
对象来处理连接。
让我们来看看 src/data-source.ts
文件:
import "reflect-metadata" import { DataSource } from "typeorm" import { User } from "./entity/User" import {Tag} from './entity/Tag' import { BlogPost } from "./entity/BlogPost" require('dotenv').config() export const AppDataSource = new DataSource({ type: "mysql", host: process.env.OB_HOST, port: Number(process.env.OB_PORT), username: process.env.OB_USER, password: process.env.OB_PASSWORD, database: process.env.OB_DB, synchronize: true, logging: false, entities: [User, Tag, BlogPost], migrations: [], subscribers: [], }).initialize() 接下来,我们创建一个新的`DataSource`对象。该对象配置为连接到与OceanBase兼容的MySQL数据库。我们提供了必要的连接细节: * `host`:您的OceanBase集群运行的主机。 * `port`:要连接的端口。 * `username`和`password`:用于验证数据库的凭据。 * `database`:要连接的数据库的名称。 最后,我们指定了之前创建的实体。这些是TypeORM将在我们的OceanBase数据库中创建的表。 现在,无论我们在应用程序中需要与数据库交互的地方,都可以导入这个`AppDataSource`并使用它。通过这种方式,我们确保我们的应用程序始终使用相同的数据库连接。 请记住,将`.env`文件中的占位符替换为您实际的OceanBase连接详细信息。`.env`文件应该如下所示: ```sh OB_HOST=your-oceanbase-host OB_PORT=your-oceanbase-port OB_USER=your-oceanbase-username OB_PASSWORD=your-oceanbase-password OB_DB=your-oceanbase-database
通过这样的设置,我们的应用程序现在已经准备好从OceanBase中存储和检索数据了。在接下来的部分中,我们将开始为我们的CMS构建API端点。
为CMS设置CRUD操作
我们的CMS的核心将是CRUD(创建、读取、更新、删除)操作。这些操作将允许我们管理我们的内容,包括创建博客文章、添加标签和检索文章。在本节中,我们将创建几个模块来处理这些操作。
添加标签
我们将创建的第一个模块是addTag.ts
,它将允许我们向我们的CMS添加新标签。该模块导入了AppDataSource
和Tag
实体,并导出了一个异步函数addTag
,该函数以标签名称作为参数。
import { AppDataSource } from "../data-source"; import { Tag } from "../entity/Tag"; export async function addTag(name: string) { const dataSource = await AppDataSource; const tagRepository = dataSource.manager.getRepository(Tag); const newTag = new Tag(); newTag.name = name; const result = await tagRepository.save(newTag); return result; }
该函数首先等待AppDataSource
初始化,然后获取Tag
实体的存储库。它创建一个新的Tag
实例,将提供的名称分配给它,并将这个新标签保存到存储库。然后函数返回此操作的结果。
创建文章
接下来,我们将创建createPost.ts
,它将允许我们创建博客文章。该模块类似于addTag.ts
,但它还处理与文章关联的标签的创建。
import { AppDataSource } from "../data-source"; import { BlogPost } from "../entity/BlogPost"; import { Tag } from "../entity/Tag"; export async function createPost(title: string, content: string, author: string, slug: string, publishDate: Date, status: string, tagNames: string[]) { const dataSource = await AppDataSource; const postRepository = dataSource.manager.getRepository(BlogPost); const tagRepository = dataSource.manager.getRepository(Tag); const tags = []; for (const tagName of tagNames) { let tag = await tagRepository.findOne({ where: { name: tagName } }); if (!tag) { tag = new Tag(); tag.name = tagName; tag = await tagRepository.save(tag); } tags.push(tag); } const newPost = new BlogPost(); newPost.title = title; newPost.content = content; newPost.author = author; newPost.slug = slug; newPost.publishDate = publishDate; newPost.status = status; newPost.tags = tags; return await postRepository.save(newPost); }
该函数接受多个参数:title
、content
、author
、slug
、publishDate
、status
和tagNames
。它初始化AppDataSource
并获取BlogPost
和Tag
的存储库。然后,它为每个提供的标签名称创建一个新的Tag
,如果它尚不存在,并将其保存到Tag
存储库。
然后,使用提供的参数和Tag
实例数组创建一个新的BlogPost
。将文章保存到BlogPost
存储库,并函数返回此操作的结果。
获取所有文章
getAllPosts.ts
模块允许我们从我们的CMS中检索所有博客文章。它导出了一个异步函数getAllPosts
,该函数从BlogPost
存储库中获取并返回所有文章。
import { AppDataSource } from "../data-source"; import { BlogPost } from "../entity/BlogPost"; export async function getAllPosts() { const dataSource = await AppDataSource; return await dataSource.manager.getRepository(BlogPost).find(); }
同样,getAllTags.ts
模块导出了一个异步函数getAllTags
,该函数从Tag
存储库中获取并返回所有标签。
import { AppDataSource } from "../data-source"; import { Tag } from "../entity/Tag"; export async function getAllTags() { const dataSource = await AppDataSource; return await dataSource.manager.getRepository(Tag).find(); }
根据Slug获取文章
getPostBySlug.ts
模块导出了一个异步函数getPostBySlug
,该函数根据其slug获取并返回文章。
import { AppDataSource } from "../data-source"; import { BlogPost } from "../entity/BlogPost"; export async function getPostBySlug(slug: string) { const dataSource = await AppDataSource; return await dataSource.manager.getRepository(BlogPost).findOne({where: {slug}}); }
根据标签获取所有文章
getPostsByTag.ts
模块导出了一个异步函数getPostsByTag
,该函数根据特定标签获取并返回所有与之关联的文章。
import { AppDataSource } from "../data-source"; import { BlogPost } from "../entity/BlogPost"; import { Tag } from "../entity/Tag"; export async function getPostsByTag(tagName: string) { const dataSource = await AppDataSource; const tagRepository = dataSource.manager.getRepository(Tag); const tag = await tagRepository.findOne({ where: { name: tagName } }); if (!tag) { throw new Error(`No tag found with the name ${tagName}`); } return await dataSource.manager.getRepository(BlogPost).find({ where: { tags: tag } }); }
该函数接受tagName
作为参数。它初始化AppDataSource
并获取Tag
的存储库。然后搜索具有提供名称的标签。如果找不到这样的标签,则抛出错误。如果找到标签,则函数从BlogPost
存储库中获取并返回与该标签关联的所有BlogPost
实体。
通过这些文件,我们已经为我们的CMS设置了基本的CRUD操作。这些操作允许我们在我们的CMS中创建、检索和管理博客文章和标签,利用了TypeScript和OceanBase的强大功能。
在Express中设置API服务器
Express.js框架将处理我们的CMS的HTTP请求和响应。在src/index.ts
文件中,我们首先导入了必要的模块和函数:
import express, { Request, Response , NextFunction} from 'express'; import { AppDataSource } from './data-source'; import { addTag } from './modules/addTag'; import { createPost } from './modules/createPost'; import { getAllPosts } from './modules/getAllPosts'; import { getAllTags } from './modules/getAllTags'; import { getPostBySlug } from './modules/getPostBySlug'; import { getPostsByTag } from './modules/getPostsByTag';
接下来,我们创建了一个Express应用程序,并将端口设置为3000
。我们还使用了express.json()
中间件来解析传入的JSON负载:
const app = express(); const port = 3000; app.use(express.json());
然后,我们为我们的CMS定义了几个路由:
GET /posts
:此路由从CMS中获取所有博客文章,并将它们发送到HTTP响应中。
app.get('/posts', async (req: Request, res: Response) => { const posts = await getAllPosts(); res.json(posts); });
POST /posts
:此路由创建一个新的博客文章。它期望在HTTP请求体中提供文章的详细信息。
app.post('/posts', async (req: Request, res: Response, next: NextFunction) => { try { const { title, content, author, slug, publishDate, status, tagNames } = req.body; await createPost(title, content, author, slug, publishDate, status, tagNames); res.sendStatus(201); } catch (error) { next(error); } });
POST /tags
:此路由创建一个新的标签。它期望在HTTP请求体中提供标签名称。
app.post('/tags', async (req: Request, res: Response, next: NextFunction) => { try { const { name } = req.body; await addTag(name); res.sendStatus(201); } catch (error) { next(error); } });
GET /tags
:此路由从CMS中获取所有标签,并将它们发送到HTTP响应中。
app.get('/tags', async (req: Request, res: Response, next: NextFunction) => { try { const tags = await getAllTags(); res.json(tags); } catch (error) { next(error); } });
GET /posts/:slug
:此路由根据其slug获取博客文章,并将其发送到HTTP响应中。
app.get('/posts/:slug', async (req: Request, res: Response, next: NextFunction) => { try { const { slug } = req.params; const post = await getPostBySlug(slug); if (post) { res.json(post); } else { res.sendStatus(404); } } catch (error) { next(error); } });
GET /posts/tag/:tagName
:此路由获取与特定标签关联的所有博客文章,并将它们发送到HTTP响应中。
app.get('/posts/tag/:tagName', async (req: Request, res: Response, next: NextFunction) => { try { const { tagName } = req.params; const posts = await getPostsByTag(tagName); res.json(posts); } catch (error) { next(error); } });
我们还添加了一个错误处理中间件,以捕获在处理请求过程中可能发生的任何错误:
app.use((error: Error, req: Request, res: Response, next: NextFunction) => { console.error(error); res.status(500).json({ error: 'Internal server error' }); });
最后,我们启动Express服务器,并添加事件监听器来处理优雅的关闭:
app.listen(port, () => { console.log(`Server is running on port ${port}`); }); process.on('SIGINT', async () => { const conn = await AppDataSource; await conn.close(); console.log('Database connection closed'); process.exit(0); }); process.on('unhandledRejection', (reason, promise) => { console.log('Unhandled Rejection at:', promise, 'reason:', reason); process.exit(1); });
就是这样!通过这些步骤,我们已经使用Express.js、TypeScript和OceanBase为我们的CMS设置了API服务器。
测试API
现在我们的API服务器已经设置好了,是时候来测试一下了。我们将使用VS Code插件Thunder Client来向我们的服务器发出HTTP请求并检查其响应。
输入以下命令来启动服务器:
npm run start
让我们从测试我们创建的POST /tags
端点开始,以向我们的CMS添加新标签。打开Postman并创建一个新请求。将请求类型设置为POST
,URL设置为http://localhost:3000/tags
。在请求体中,选择raw
和JSON
,并输入一个新的标签名称,如下所示:
{ "name": "Technology" }
点击“发送”。如果一切正常,您应该会收到一个201 Created状态码。
接下来,让我们测试POST /posts
端点。创建一个新的POST
请求到http://localhost:3000/posts
。在请求体中,输入一个新的博客帖子,包括标题、内容、作者、slug、发布日期、状态和一个标签名称的数组:
{ "title": "CMS的未来", "content": "这是一篇关于CMS未来的博客文章。", "author": "约翰·多", "slug": "the-future-of-cms", "publishDate": "2023-12-01", "status": "PUBLISHED", "tagNames": ["Technology", "CMS"] }
点击“发送”。您应该会收到一个201 Created
状态码。
现在,让我们测试GET /posts
端点。创建一个新的GET
请求到http://localhost:3000/posts
,然后点击“发送”。您应该会收到一个200 OK
状态码,并在响应体中看到所有博客帖子的列表。
类似地,通过创建一个新的GET
请求到http://localhost:3000/tags
来测试GET /tags
端点。您应该会收到一个200 OK
状态码,并在响应体中看到所有标签的列表。
要测试GET /posts/tag/:tagName
端点,创建一个新的GET
请求到http://localhost:3000/posts/tag/Technology
。您应该会收到一个200 OK
状态码,并看到与标签“Technology”相关的所有博客帖子的列表。
最后,要测试GET /posts/:slug
端点,创建一个新的GET
请求到http://localhost:3000/posts/the-future-of-ai
。您应该会收到一个200 OK
状态码,并看到slug为“the-future-of-ai”的博客帖子的详细信息。
恭喜!如果您的所有请求都返回了预期的响应,那么您的API服务器就正常工作了。您已成功使用TypeScript和OceanBase构建了一个无头CMS,并配备了一个使用Express.js的强大API服务器。
结论
构建一个无头CMS可能看起来是一项艰巨的任务,但正如我们在本教程中所看到的,借助正确的工具和坚实的结构,这是完全可以实现的。使用TypeScript和OceanBase,我们创建了一个灵活且可扩展的CMS,可以在各种平台上提供内容。
OceanBase是一个分布式、高性能的关系型数据库,它是我们CMS的支撑。它的高可扩展性和与MySQL的兼容性使其成为这类项目的绝佳选择。OceanBase不仅高效地处理大量数据,还提供了可靠性,确保我们的CMS能够处理大规模的应用程序。
TypeScript的静态类型增强了我们代码的可靠性、可预测性和可维护性,使其成为我们构建强大的无头CMS的有力助手。Express.js是一个轻量灵活的Node.js Web应用程序框架,使我们能够轻松构建一个时尚的API服务器。
本指南展示了您如何在TypeScript项目中使用OceanBase。这证明了OceanBase的多功能性和健壮性,证明它可以成为从小型项目到大型企业解决方案的广泛应用的绝佳选择。
同样,您可以在这个GitHub仓库上找到项目的源代码。该仓库也可以成为您心中任何其他TypeScript/OceanBase项目的起点。