显然,要开始一个项目最好的方式是直接在github上找一个模版然后直接开始魔改。我们必须要考虑前面的技术选型,最关键的地方是数据库、部署。
比较难过的是,我找了一圈,没有比较符合要求的项目,没有办法,我只能给大家搓一个出来,项目地址是:https://github.com/Shiinama/next-cloudflare-template
接下来大家就跟着我走,这一章的主要目的是把项目和本地数据库跑起来,把基础设施讲一遍,并且通过最常用的Google登录去跑通这个本地流程。
基础背景知识的补充是在:
登录wrangler
在开始使用 Cloudflare Workers 和 D1 数据库之前,我们需要先登录 Wrangler CLI 工具。Wrangler 是 Cloudflare 官方提供的命令行工具,用于开发、测试和部署 Cloudflare Workers。
1. 安装 Wrangler
如果你还没有安装 Wrangler,可以通过 npm 安装:
npm install -g wrangler
```
或者在项目中使用 pnpm:
```bash
pnpm add -D wrangler
```
### 2\. 登录 Cloudflare 账户
安装完成后,需要登录你的 Cloudflare 账户:
```bash
pnpm wrangler login
```
执行此命令后,会打开浏览器窗口,提示你登录 Cloudflare 账户并授权 Wrangler 访问。按照提示完成授权过程。
### 3\. 验证登录状态
登录成功后,可以验证登录状态:
```bash
pnpm wrangler whoami
```
这个命令会显示你当前登录的 Cloudflare 账户信息,包括邮箱和账户 ID。
现在你已经成功登录 Wrangler,可以开始创建和管理 Cloudflare 资源了。
## 创建Cloudflare数据库
我们需要先创建一个本地的 D1 数据库:
```bash
pnpm wrangler d1 create demo
```
这个命令会创建一个名为 “demo” 的 D1 数据库。执行后会看到一些配置信息,需要将这些信息替换掉到我们的 `wrangler.toml` 文件中的对应信息。
就像这样:
```json
{
"d1_databases": [
{
"binding": "DB",
"database_name": "demo",
"database_id": "faac1a9d-d012-4e93-b30f-ba990b24928e"
}
]
}
```
用这个命令查看一下创建成功与否
```base
pnpm wrangler d1 list
```
> 别用我的,你没用我的账号登录也没意义
### 2\. 初始化数据库结构
我们是通过Drizzle来管理D1数据库。
所以我们要去配置`lib/db/schema.ts`,项目中的我是已经直接配好的,参考的是[next auth drizzle](https://authjs.dev/getting-started/adapters/drizzle)下的sqlite数据库结构配置。
`drizzle.config.ts`是直接写好的,直接执行`pnpm drizzle-kit generate`(生成SQL迁移文件),在`migrations`文件夹下生成了SQL,像是这样。
```sql
CREATE TABLE `account` (
`userId` text NOT NULL,
`type` text NOT NULL,
`provider` text NOT NULL,
`providerAccountId` text NOT NULL,
`refresh_token` text,
`access_token` text,
`expires_at` integer,
`token_type` text,
`scope` text,
`id_token` text,
`session_state` text,
PRIMARY KEY(`provider`, `providerAccountId`),
FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `authenticator` (
`credentialID` text NOT NULL,
`userId` text NOT NULL,
`providerAccountId` text NOT NULL,
`credentialPublicKey` text NOT NULL,
`counter` integer NOT NULL,
`credentialDeviceType` text NOT NULL,
`credentialBackedUp` integer NOT NULL,
`transports` text,
PRIMARY KEY(`userId`, `credentialID`),
FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE UNIQUE INDEX `authenticator_credentialID_unique` ON `authenticator` (`credentialID`);--> statement-breakpoint
CREATE TABLE `session` (
`sessionToken` text PRIMARY KEY NOT NULL,
`userId` text NOT NULL,
`expires` integer NOT NULL,
FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `user` (
`id` text PRIMARY KEY NOT NULL,
`name` text,
`email` text,
`emailVerified` integer,
`image` text
);
--> statement-breakpoint
CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);--> statement-breakpoint
CREATE TABLE `verificationToken` (
`identifier` text NOT NULL,
`token` text NOT NULL,
`expires` integer NOT NULL,
PRIMARY KEY(`identifier`, `token`)
);
```
紧接着我们执行D1的数据库迁移命令`pnpm wrangler d1 migrations apply demo --local`, 在迁移成功后,再执行`pnpm wrangler d1 execute demo --command "SELECT name FROM sqlite_master WHERE type='table';"`,此时就可以看到本地表已经创建成功了。

> 需要理解的是:我们使用`drizzle`去做生成迁移SQL文件、代码层的数据库管理,最终是通过D1的迁移脚本,把结构同步到真实数据库。
最后我们创建一个函数:
```typescript
// lib/db/index.ts
import { getRequestContext } from '@cloudflare/next-on-pages'
import { drizzle } from 'drizzle-orm/d1'
import * as schema from './schema'
export const createDb = () => drizzle(getRequestContext().env.DB, { schema })
export type Db = ReturnType<typeof createDb>
```
`getRequestContext`是获取在`ServerLess`环境中获取变量的方法,我们后续使用就可以直接通过`createDb`去操作数据库了。
> 在完成前面的数据库配置后,我们就需要验证一下这个数据库是否真的可用,我们直接走一下Google的登录流,看是否表中正常插入数据了。
### 3\. 配置Next Auth
首先来到`lib/auth.ts`,这里首先关注`AUTH_SECRET`,我们可以直接使用`npm exec auth secret`,会自动创建AUTH\_SECRET 到.env.local里,这是NextAuth的脚手架提供的能力。(也可以直接采用openssl rand -base64 32)。
而`DrizzleAdapter`是一个中间层,里面其实就是处理各种表和SQL的操作。
`providers: [Google]`表示我们引入了Google这一个登录的提供服务商。
```typescript
import { DrizzleAdapter } from '@auth/drizzle-adapter'
import NextAuth from 'next-auth'
import Google from 'next-auth/providers/google'
import { accounts, sessions, users, verificationTokens } from './db/schema'
import { createDb } from '@/lib/db'
export const { handlers, signIn, signOut, auth } = NextAuth(() => {
const db = createDb()
return {
secret: process.env.AUTH_SECRET,
adapter: DrizzleAdapter(db, {
usersTable: users,
accountsTable: accounts,
sessionsTable: sessions,
verificationTokensTable: verificationTokens
}),
providers: [Google],
session: {
strategy: 'jwt'
}
}
})
```
### 4\. 配置Google登录
配置google cloud大部分部分都是填资料,网上教程非常多:
[https://developers.google.com/identity/protocols/oauth2?hl=zh-cn](https://developers.google.com/identity/protocols/oauth2?hl=zh-cn)
[https://console.cloud.google.com/](https://console.cloud.google.com/)
在注册好账号和应用之后,点击`API和服务` -> `Oauth权限页面` -> `客户端` -> `创建客户端`,就来到了创建应用的地方,填写方式如图,
* 应用名称:会在Google别人的账号登录的时候显示
* 回调地址:[http://localhost:3000/api/auth/callback/google(本地开发环境)](http://localhost:3000/api/auth/callback/google%EF%BC%88%E6%9C%AC%E5%9C%B0%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%EF%BC%89)
* 授权的JavaScript来源:[http://localhost:3000](http://localhost:3000)
在生产环境中,需要将回调地址更改为你的实际域名,例如:[https://yourdomain.com/api/auth/callback/google](https://yourdomain.com/api/auth/callback/google)
这个回调地址非常重要,它必须与NextAuth配置中的回调URL完全匹配,否则Google会拒绝认证请求。

可以看到会有两个秘钥,在图的马赛克位置,将其填入到`.env`文件中的对应字段内,这部分由`next-auth`库自动读取,是`约定命名`。
```
AUTH_GOOGLE_ID=""
AUTH_GOOGLE_SECRET=""
```
接下来就是登录部分,有关文件是`components/login`、`app/api/auth/[...nextauth]/route.ts`。

点一下登录,走一些流程之后,我们可以看到在控制台里面多了google的回调:

这个回调是由nextauth处理的,\[\[…nextauth\]代表的是捕捉后面的所有路由,而这就是对应的NextAuth提供的内置路由。
> 请直接参考[文档](https://next-auth.js.org/getting-started/rest-api)。
这些路由构成了 NextAuth.js 的核心Server API,处理身份验证、会话管理和\`CSRF 保护等。
这显然也就跟重定向回调地址对上了,这代表的是/api/auth/callback/:provider,处理google回调验证参数。
### 5\. 回到数据库
那么当我们成功登录之后,就可以到检查数据库的步骤了,我们用更友好的`GUI`的方式去检查本地数据库。
`"db:studio:local": "tsx scripts/db-studio-local.ts"`, 这段`package.json`里的脚本会执行 `db-studio-local` 的脚本,以通过设置环境变量的方式,去控制启动本地/远程数据库。
```typescript
import { execSync } from 'child_process'
import { join } from 'path'
import { existsSync, readdirSync } from 'fs'
import { platform } from 'os'
function findSqliteFile(): string | null {
const basePath = join('.wrangler', 'state', 'v3', 'd1', 'miniflare-D1DatabaseObject')
if (!existsSync(basePath)) {
console.error(`Base path does not exist: ${basePath}`)
return null
}
try {
function findFile(dir: string): string | null {
const files = readdirSync(dir, { withFileTypes: true })
for (const file of files) {
const path = join(dir, file.name)
if (file.isDirectory()) {
const found = findFile(path)
if (found) return found
} else if (file.name.endsWith('.sqlite')) {
return path
}
}
return null
}
return findFile(basePath)
} catch (error) {
console.error('Error finding SQLite file:', error)
return null
}
}
function main() {
const sqliteFilePath = findSqliteFile()
if (!sqliteFilePath) {
console.error('Could not find SQLite database file. Make sure you have run the local database first.')
process.exit(1)
}
console.log(`Found SQLite database at: ${sqliteFilePath}`)
// Set environment variable and run drizzle-kit studio
const command =
platform() === 'win32'
? `set "LOCAL_DB_PATH=${sqliteFilePath}" && drizzle-kit studio`
: `LOCAL_DB_PATH="${sqliteFilePath}" drizzle-kit studio`
try {
execSync(command, { stdio: 'inherit' })
} catch (error) {
console.error('Failed to run drizzle-kit studio:', error)
process.exit(1)
}
}
main()
```
```typescript
// drizzle.config.ts
import type { Config } from 'drizzle-kit'
const { LOCAL_DB_PATH, DATABASE_ID, CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID } = process.env
// Use better-sqlite driver for local development
export default LOCAL_DB_PATH
? ({
schema: './lib/db/schema.ts',
dialect: 'sqlite',
dbCredentials: {
url: LOCAL_DB_PATH
}
} satisfies Config)
: ({
schema: './lib/db/schema.ts',
out: './migrations',
dialect: 'sqlite',
driver: 'd1-http',
dbCredentials: {
databaseId: DATABASE_ID!,
token: CLOUDFLARE_API_TOKEN!,
accountId: CLOUDFLARE_ACCOUNT_ID!
}
} satisfies Config)
```
可以看到已经成功的插入数据了

那么其实到这还是不够的,我们要在代码中查出数据,验证ORM是否可用。
```typescript
// actions/test.ts
'use server'
import { createDb } from '@/lib/db'
import { users } from '@/lib/db/schema'
export async function getUsersTest() {
const db = createDb()
const data = await db.select().from(users)
return data
}
export async function getTableSchemas() {
const db = createDb()
const tables = await db.run(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`)
return tables
}
```
这里是一个server actions文件(直接获得数据的文件),Next.js中有两种主流的方式来处理数据请求:
1. **Route Handlers**:在App Router中,通过导出与HTTP方法同名的函数(GET、POST、PUT等),Next.js会自动将这些函数映射到对应的API端点。
```typescript
// app/api/users/route.ts
export async function GET() {
const users = await fetchUsers()
return Response.json(users)
}
```
2. **Server Actions**:允许你直接在组件或模块中定义服务器端函数,并从客户端直接调用它们,无需创建API路由。
```typescript
// actions/users.ts
'use server'
export async function getUsers() {
const users = await fetchUsers()
return users
}
```
Server Actions提供了更直接的数据获取方式,减少了API路由的样板代码。
> 在全栈应用的开发中,只有需要给外部调用的路由使用`Route Handlers`才是有必要的。
我们可以直接调用函数,进行请求
```tsx
'use client'
import { getTableSchemas, getUsersTest } from '@/actions/test'
import { Button } from '@/components/ui/button'
export const TextButton = () => {
return (
<Button
onClick={async () => {
const data = await getTableSchemas()
const users = await getUsersTest()
console.log('Data:', users, data)
}}
>
Test
</Button>
)
}
```
结果如下:

## 结束
那么到这里,其实我们已经把项目和本地数据库跑起来了,那么下一章那我们就直接开始远程项目的部署。