作为云原生网络开发人员,我们希望建立易于扩展的应用程序。不过,数据库往往是一个痛点。我们想要PostgreSQL等关系型数据库的结构和数据完整性,但我们也想要MongoDB等NoSQL数据库的易扩展性。直到最近,我们还不得不在其中选择一个。
幸运的是,CockroachDB提供了两个世界的最佳选择。它是一个强大的关系型数据库,完全支持SQL,并具有你所期望的企业级数据库的所有功能。它支持PostgreSQL的线程协议,所以你可以用大多数支持Postgres的工具和库来使用它。但这还不是全部:它提供了出色的可扩展性,所以你不再需要在关系型数据完整性和NoSQL可扩展性之间做出选择。而且,如果你使用CockroachDB Serverless,你就不必担心自己托管和扩展数据库的问题--这样你就可以把精力放在构建优秀的应用上。
本教程将告诉你如何从零开始,使用TypeScript、React、Prisma、Netlify无服务器函数和CockroachDB构建一个现代云原生网络应用。
CockroachDB & Typescript应用
我们将建立一个模拟的游戏排行榜。以下是最终应用程序的外观。
我们将用TypeScript创建整个应用程序--包括后端和前端。
我们将使用React构建用户界面,并使用Create React App生成项目。该应用程序将有两个主要屏幕:上图所示的排行榜,以及一个让我们添加新的排行榜项目的管理屏幕。
我们将首先在CockroachDB无服务器数据库中存储所有游戏者和排行榜数据。我们将把整个应用托管在Netlify上,并使用Netlify的无服务器函数来读取和写入我们的数据库。最后,我们将使用出色的Prisma ORM,从TypeScript中为我们的数据提供类型安全的访问。
让我们潜心研究并开始吧如果你想看看这个应用程序的完整代码,你可以在GitHub上找到它。在本教程中,我们不会展示每一行代码,而是解释创建应用程序和数据库所需的步骤,然后强调你需要从资源库中理解的代码的关键部分。
应用开发的先决条件
在进行下一步之前,请确保你已经安装了Node.js的最新LTS版本。你可以在Node.js官方下载页面上找到Windows、macOS和Linux的安装程序。
在整个教程中,我假设你之前已经使用过React,并且你对构建你的React组件感到满意。如果你是React的新手,我推荐官方的React入门教程。
最后,你应该知道并理解现代JavaScript。如果你已经知道TypeScript,也会有帮助,但如果你不知道也没关系。TypeScript是JavaScript的超集,所以你可以跟着它走,边走边学。但这不是一个TypeScript教程,所以如果你想深入了解TypeScript,我推荐你去看官方的TypeScript语言教程。
创建项目
Netlify内置支持由Create React App生成的应用程序,所以我们将用它来生成我们的项目结构。打开一个终端或命令提示符,切换到你通常保存项目的目录,并运行以下命令。
npx create-react-app cockroachdb-typescript --template typescript
Create React App将需要几分钟的时间来运行。当它完成后,你会得到以下目录结构。
cockroachdb-typescript
├── node_modules
├── public
└── src
cockroachdb-typescript 将是我们的主项目目录--所以在本教程的剩余部分,当你运行终端命令时,确保你在 目录中。cockroachdb-typescript
Create React App还在public 和src 子目录中生成了几个文件。我们将在稍后建立我们的应用程序的React用户界面时重新审视这些文件。但首先,让我们创建一个数据库和无服务器函数,让我们的用户界面可以与之互动。
创建一个CockroachDB无服务器数据库
如果我们想创建一个排行榜,我们就需要一个地方来存储我们的数据。我们将使用CockroachDB Serverless来保持简单。CockroachDB Serverless在云端启动了一个CockroachDB集群,所以我们不需要担心安装、维护或扩展我们的数据库。最重要的是,安装工作非常快速和简单。
- 如果你还没有,请注册一个CockroachDB云账户。
- 登录到你的CockroachDB账户。
- 在集群页面上,选择创建集群。
- 在创建集群页面上,选择无服务器。
- 选择创建你的免费集群。
一旦你完成了账户注册并创建了一个新的集群,CockroachDB就会向你展示一个像下面这样的连接字符串。
注意,CockroachDB只提供一次密码,所以在选择REVEAL_PASSWORD后要记下它。复制密码并将其保存在一个安全的地方,因为我们以后会用到它。
接下来,选择CockroachDB无服务器仪表板右上方的连接按钮。
你会看到三组说明:一组是安装CockroachDB客户端,一组是安装CA证书,还有一组是连接到你的CockroachDB集群。确切的说明会根据你所运行的操作系统而有所不同。在Mac上,你会看到类似这样的内容。
运行前两组命令,下载CockroachDB客户端和CA证书。保持第三条命令的可访问性--我们稍后会需要它,还有我们之前透露的密码。
创建数据库表和设置Prisma
创建了集群后,现在是时候设置我们的排行榜所需的表,并配置Prisma ORM。
Prisma带有一个迁移工具,可以自动创建和更新你的数据库表--但它还不兼容CockroachDB。不过没关系!我们可以自己创建我们需要的表。我们可以通过一个SQL脚本来创建我们需要的表。
你可以在已经完成的github项目中找到它,网址是database/schema.sql 。 下面是它的样子。
DROP DATABASE IF EXISTS leaderboard;
CREATE DATABASE leaderboard;
USE leaderboard;
CREATE TABLE players (
id SERIAL,
name TEXT NOT NULL,
email TEXT NOT NULL,
CONSTRAINT "players_pkey" PRIMARY KEY (id ASC)
);
CREATE TABLE player_scores (
id SERIAL,
player_id INTEGER NOT NULL,
score INTEGER NOT NULL,
CONSTRAINT "player_scores_pkey" PRIMARY KEY (id ASC),
CONSTRAINT fk_player FOREIGN KEY (player_id) REFERENCES players(id)
);
CREATE INDEX player_scores_player_id on player_scores(player_id);
CREATE INDEX player_scores_score on player_scores(score);
INSERT INTO players (name, email) VALUES ('Test Player 1', 'test_player_1@example.com');
INSERT INTO players (name, email) VALUES ('Test Player 2', 'test_player_2@example.com');
INSERT INTO players (name, email) VALUES ('Test Player 3', 'test_player_3@example.com');
INSERT INTO players (name, email) VALUES ('Test Player 4', 'test_player_4@example.com');
INSERT INTO players (name, email) VALUES ('Test Player 5', 'test_player_5@example.com');
该脚本首先创建了一个新的数据库,然后创建了存储球员和比赛分数的表,接着创建了索引,以确保我们创建的应用程序能够快速查询和排序球员的分数。
请注意,除了创建表格外,我还添加了一些球员样本。我这样做是因为创建球员已经超出了典型排行榜应用程序的范围。大多数排行榜的重点是创建和显示现有球员的条目。
我们将使用以下命令来运行Cockroach SQL客户端并执行我们的脚本。我们以前见过它。这是我们之前在CockroachDB仪表板上查看我们的连接信息时看到的第三条命令。你所需要做的就是在结尾处添加-f schema.sql 。它看起来会像这样。
cockroach sql --url 'postgresql://**<your-db-username>:<your-db-password>**@free-tier.gcp-us-central1.cockroachlabs.cloud:26257/defaultdb?sslmode=verify-full&sslrootcert='$HOME'/.postgresql/root.crt&options=--cluster%3D**<your-database-name>**' -f schema.sql
注意,你需要在运行该命令之前插入你的数据库的实际用户名/密码/名称。
输出结果应该是这样的。
CREATE TABLE
Time: 98ms
CREATE TABLE
Time: 402ms
CREATE INDEX
Time: 1.164s
CREATE INDEX
Time: 819ms
这就是用CockroachDB创建一个游戏排行榜数据库的全部内容。
在我们使用Prisma之前,我们必须要求Prisma根据我们的数据库表生成自己的模式文件。
Prisma希望在你的项目的prisma 子目录下有一个叫做schema.prisma 的文件。你可以在GitHub上找到这个完整的文件。如果你从头开始,你不需要整个文件。相反,你可以从前两个部分开始。
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
这里,你可以看到Prisma期望在DATABASE_URL 环境变量中找到一个连接字符串。在我们项目的根目录下创建一个.env 文件是向Prisma提供它所期望的连接字符串的最简单的方法。.env 文件应该有一个单行。
DATABASE_URL= postgresql://<username>:<password>@<host>:<port>/<database>.leaderboard
你可以在CockroachDB无服务器仪表盘中选择 "连接"按钮,然后打开 "连接参数"标签,找到用户名、主机、端口和数据库值。
请注意,仪表板中的数据库名称将以.defaultdb 结尾。当在你的.env 文件中(以及在Netlify上)输入时,将.defaultdb 改为.leaderboard ,以确保我们的应用程序连接到我们之前运行schema.sql 时创建的排行榜数据库。
虽然一般来说,将.env 文件提交到源代码控制中不是一个好主意,但你可以在我们部署到Netlify后,通过在Netlify仪表板上设置一个环境变量,使连接字符串对Prisma可用。我们会在讨论这个问题时介绍。
一旦你把连接字符串添加到.env 文件中,在项目的根目录下运行以下命令。
npm install --save @prisma/client@3.6.0
npx prisma db pull
然后,Prisma将对数据库进行反省,并生成其模式的其余部分。
MacBook-Air-2018
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "angry-porcupine-3367.defaultdb", schema "public" at "free-tier.gcp-us-central1.cockroachlabs.cloud:26257"
Introspecting based on datasource defined in prisma/schema.prisma …
✔ Introspected 2 models and wrote them into prisma/schema.prisma in 3.30s
Run prisma generate to generate Prisma Client.
如果你现在看一下schema.prisma ,你会看到Prisma已经添加了它在内省过程中了解的模式信息。
model player_scores {
id BigInt @id(map: "primary") @default(autoincrement())
player_id BigInt
score BigInt
players players @relation(fields: [player_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "fk_player")
@@index([player_id], map: "player_scores_player_id")
@@index([score], map: "player_scores_score")
}
model players {
id BigInt @id(map: "primary") @default(autoincrement())
name String
email String
player_scores player_scores[]
}
接下来,运行npx prisma generate 。Prisma将创建并提供针对你的数据库模式的定制TypeScript客户端的使用说明。
至此,我们已经创建了一个游戏排行榜数据库,并生成了一个完全类型安全的客户端来访问它。
创建无服务器函数并部署到Netlify
我们剩下最后一步:创建Netlify函数,使我们的React前端能够与CockroachDB数据库通信,然后将应用程序部署到Netlify。
让我们从安装TypeScript的Netlify函数辅助库开始。在项目的根目录下运行以下命令。
npm install --save @netlify/functions
如果我们把函数放在主项目目录的netlify/functions 子目录下,Netlify会自动识别我们的函数,所以我们就这样做吧。
如果你要从头开始创建你的函数,而不是使用GitHub上的代码,请在项目的根目录下创建一个netlify 子目录,然后在其中创建一个functions 目录。为排行榜应用程序创建三个TypeScript Netlify函数。getScores.ts,addScore.ts, 和getPlayers.ts, 如图所示。
cockroachdb-typescript
├── .netlify
├── database
├── netlify
│ └── functions
│ ├── addScore.ts
│ ├── getPlayer.ts
│ └── getScores.ts
├── node_modules
└── prisma
让我们看看它们是如何工作的,首先是getScores.ts 。
import { Handler } from '@netlify/functions'
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient();
const handler: Handler = async (event, context) => {
// load all scores from the database and include player name
// from the players table.
const allScores = await prisma.player_scores.findMany({
include: {
players: {
select: {
name: true
}
}
},
orderBy: {
score: 'desc'
}
});
return {
statusCode: 200,
body: JSON.stringify(allScores.map(score => (
// flatten player name into score entry
{ id: score.id, name: score.players.name, score: score.score }
))
, (key, value) =>
// need to add a custom serializer because CockroachDB IDs map to
// JavaScript BigInts, which JSON.stringify has trouble serializing.
typeof value === 'bigint'
? value.toString()
: value
)
}
}
export { handler }
这是我们的Netlify函数中最复杂的,所以让我们关注几个关键领域。首先,注意对prisma.scores.findMany 的调用,在这里我们使用 Prisma 为我们生成的类型安全的客户端。
我们还传递了一个对象作为参数,它有两个属性:include 和orderBy 。orderBy 的工作是预期的:它确保我们将收到一个按分数降序排序的顶级分数列表。include 属性指示Prisma将player_scores 表与球员表连接起来,这样我们就可以在排行榜上包括球员名字。
一旦getScores.ts ,通过JSON返回分数,我们就平铺该对象。否则,球员的名字会出现在一个嵌套对象中,像这样。
{
id: "70354",
score: 250
"players": {
"name": "Player 1"
}
}
扁平化数据后,该对象看起来像这样。
{
id: "70354",
score: 250,
name: "Player 1"
}
我们可以在我们的React前端轻松地处理这个嵌套对象。但在服务器端重新格式化数据是一个很好的做法,因为在生产应用中,服务器端和客户端的代码往往是由不同的团队编写的。我们的前端代码不应该需要知道像这样的低级实现细节。我们还提供了一个自定义的JSON序列化函数。这很重要,因为许多CockroachDB的ID字段足够大,JavaScript将其翻译成BigInt,这打破了JSON.strinfigy'的默认序列化行为。
接下来,让我们看一下addScore.ts 函数。
import { Handler } from '@netlify/functions'
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient();
interface ScoreEntry {
playerId: string,
score: string
}
const handler: Handler = async (event, context) => {
if(event.body) {
const newScore = JSON.parse(event.body) as ScoreEntry;
await prisma.player_scores.create({
data: {
player_id: BigInt(newScore.playerId),
score: parseInt(newScore.score)
},
});
return {
statusCode: 200,
body: JSON.stringify(newScore)
};
}
return {
statusCode: 500
};
}
export { handler }
我们从我们的React前端将发送的POST 请求的正文中提取球员的ID和得分。注意,我们已经创建了一个名为ScoreEntry 的TypeScript接口,让编译器知道我们在解析请求体时将得到的数据的形状。当我们调用prisma.player_scores.create 时,我们也将播放器的ID转换回BigInt。
最后,我们有getPlayers.ts 。它读取了数据库中的所有球员,所以我们的React前端将能够生成一个球员列表,让我们选择在管理页面上为哪个球员添加新的排行榜分数。
import { Handler } from '@netlify/functions'
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient();
const handler: Handler = async (_event, _context) => {
// load all players from the database
const allPlayers = await prisma.players.findMany();
return {
statusCode: 200,
body: JSON.stringify(allPlayers, (_key, value) =>
// need to add a custom serializer because CockroachDB IDs map to
// JavaScript BigInts, which JSON.stringify has trouble serializing.
typeof value === 'bigint'
? value.toString()
: value
)
}
}
export { handler }
使用React和Typescript创建一个前端
我们的排行榜需要一个用户界面。早些时候,我们使用Create React App来生成我们的项目目录,所以我们需要的许多部分都已经到位了。
我们将使用React Router在我们的应用程序中的页面之间进行导航。现在通过在你的项目根目录下运行以下程序来安装React Router和其TypeScript类型定义。
npm install --save react-router-dom@6.1.1
在GitHub仓库中,我在项目的public/index.html 文件中加入了Bootstrap 5,以使UI更具视觉吸引力,在public/index.html 的<head> 部分加入了以下一行。
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
crossorigin="anonymous">
当然,如果你愿意,你也可以使用Tailwind或其他任何CSS框架。
设置React Router已经超出了本文的范围,如果你对它的工作原理感到好奇,可以看看GitHub上的成品应用程序的src/App.tsx 文件。
打开src 子文件夹,你会看到以下结构。
├── src
├── App.css
├── App.test.tsx
├── App.tsx
├── index.css
├── index.tsx
├── logo.svg
├── react-app-env.d.ts
├── reportWebVitals.ts
└── setupTests.ts
我们的Leaderboard的两个关键部分在src目录中的两个新组件文件中:Leaderboard.tsx 和Admin.tsx 。
让我们先看一下Leaderboard.tsx 。
import React, { useState, useEffect } from 'react';
import { Link } from "react-router-dom";
interface Leader {
id: number,
name: string,
score: number
}
function renderLeader(leader: Leader) {
return <tr key={leader.id}>
<td>{leader.name}</td>
<td>{leader.score}</td>
</tr>
}
export default function Leaderboard() {
const [leaders, setLeaders] = useState([] as Leader[]);
useEffect(() => {
fetch('/.netlify/functions/getScores')
.then(response => response.json() as Promise<Leader[]>)
.then(data => setLeaders(data));
}, []);
return <>
<h2>Leaderboard</h2>
{leaders.length === 0 ?
<div>No leader scores to display. Would you like to <Link to="admin">add one</Link>?</div>
:
<table className="table leader-table">
<thead>
<tr>
<th>Name</th>
<th>Score</th>
</tr>
</thead>
<tbody>
{leaders.map(l => renderLeader(l))}
</tbody>
</table>
}
</>
}
我们首先创建一个Leader 接口,让TypeScript知道排行榜条目是什么样子的。然后,renderLeader 辅助函数为排行榜表生成行。
排行榜的其余部分是一个标准的React函数组件。注意useEffect 钩子,因为它是使该组件工作的部分。在这种情况下,该钩子使用浏览器内置的Fetch API来调用我们之前创建的Netlify函数,加载排行榜的分数,并通过调用setLeaders 来更新排行榜的状态。这反过来又导致React重新渲染组件并显示排行榜。
接下来,让我们看看Admin.tsx 组件,它使添加新的排行榜条目成为可能。
import React, {useEffect, useState} from 'react';
interface Player {
id: string,
name: string,
}
export default function Admin() {
const [players, setPlayers] = useState([] as Player[]);
const [score, setScore] = useState("");
const [message, setMessage] = useState("");
const [selectedPlayer, setSelectedPlayer] = useState<Player | undefined>(undefined);
useEffect(() => {
fetch('/.netlify/functions/getPlayers')
.then(response => response.json() as Promise<Player[]>)
.then(data => {
setPlayers(data);
if(data && data.length > 0) {
setSelectedPlayer(data[0]);
}
});
}, []);
const selectPlayer = (players: HTMLSelectElement) => {
setSelectedPlayer({
id: players.value,
name: players.options[players.selectedIndex].text
});
}
const addScore = () => {
const scoreEntry = {
playerId: selectedPlayer?.id,
score: score
}
fetch("/.netlify/functions/addScore",
{
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
method: "POST",
body: JSON.stringify(scoreEntry)
})
.then(function(res){
setMessage(`Leaderboard score of ${score} added for player ${selectedPlayer?.name}`);
})
.catch(function(res){
setMessage(`Unable to add score for player ${selectedPlayer?.name}`);
})
}
return <>
<h2>Admin</h2>
<h4>Add a Leaderboard Entry</h4>
{players.length === 0 ?
<div>Loading Players...</div>
:
<>
<div className="mb-3 score-entry">
<label>Player</label>
<select
className="form-select"
aria-label="player selection"
value={selectedPlayer?.id}
onChange={e => selectPlayer(e.target)}>
{players.map(p => <option key={p.id} value={p.id}>
{p.name}
</option>)}
</select>
</div>
<div className="mb-3 score-entry">
<label>Score</label>
<input type="text"
className="form-control"
aria-label="score entry"
value={score}
onChange={e => setScore(e.target.value)}/>
</div>
<div>
<button className="btn btn-primary" onClick={addScore}>Add Score</button>
</div>
</>
}
<div className="admin-message">{message}</div>
</>
}
这个组件有点复杂,但没有什么是我们不能处理的。它的开始与排行榜组件类似:通过调用一个Netlify函数,在一个效果钩中加载数据并更新React组件。
一旦玩家列表加载完毕,该组件将显示一个选择框和一个文本框,允许应用程序的用户选择一个玩家,输入一个分数,然后按下一个按钮,通过addScore 函数将该玩家的排行榜分数条目添加到数据库中。这个函数接收页面上输入的数据,并使用Fetch API来调用我们之前检查过的后端Netlify函数。
我们唯一缺少的是认证。在这里,我们省略了它以保持简单。但在生产应用中,你会希望使用类似Netlify Identity的东西来防止陌生人在你的排行榜上添加虚假的条目。
将应用程序部署到Netlify
现在剩下的就是部署到Netlify了。虽然本教程中没有一个完整的Netlify指南,但有几个简单的步骤可以让你快速启动和运行。
首先,把你写的所有代码--或者如果你愿意,就把这个项目的完整代码分叉--推送到你自己的GitHub仓库。然后,使用GitHub登录到Netlify。
登录后,你会在Netlify的仪表板上看到一个从Git上新建网站的按钮。
选择这个按钮可以直接从你的GitHub仓库创建一个网站。它甚至会运行Create React App来构建你的前端,然后自动部署你的Netlify功能。
一旦你的应用程序启动并运行,Netlify将向你显示访问它的URL。
还有一些我们需要做的事情--还记得我之前让你为Prisma设置的DATABASE_URL 环境变量吗?我们需要一种方法来为我们部署在Netlify上的函数提供这个变量。
Netlify让这一切变得简单。在你的应用程序的仪表板上,选择网站设置,然后在左边的菜单中,选择构建和部署,然后选择环境。这可以让你设置DATABASE_URL 环境变量。
输入你先前在设置Prisma模式时使用的相同连接字符串。
还有最后一步。Netlify在部署我们的应用程序时,将环境变量捆绑到每个函数中。然而,Netlify在我们设置DATABASE_URL 环境变量之前就部署了我们的应用程序,所以如果我们现在尝试运行我们的应用程序,我们会得到一个难看的错误信息。幸运的是,我们所要做的就是触发一个新的部署。
在我们Netlify应用程序的仪表板上,点击生产部署。然后,当下一个屏幕加载时,点击触发部署的下拉菜单,选择部署网站。
等待部署完成,然后导航到你的应用程序的Netlify URL。你还不会看到任何球员的分数,所以点击导航栏中的管理链接并添加一些。
一旦你部署到Netlify,你也可以使用Netlify CLI的netlify dev 命令在本地运行你的整个应用程序,这样你就不需要部署来测试无服务器功能的变化。
总结
就这样吧!虽然有很多模板需要完成,但我们现在已经可以在CockroachDB的支持下构建出色的React应用了,而且最重要的是,我们使用的是无服务器函数和无服务器数据库,所以我们不必担心管理或扩展服务器的问题。
如果你还没有,为什么不注册一个免费的CockroachDB无服务器账户,用它来开始构建你的下一个伟大的应用呢?