通用基础项目

    3. 从模版开始

    发布时间
    April 26, 2025
    阅读时间
    5 min read
    作者
    Felix
    访问
    公开阅读

    显然,要开始一个项目最好的方式是直接在github上找一个模版然后直接开始魔改。我们必须要考虑前面的技术选型,最关键的地方是数据库部署

    比较难过的是,我找了一圈,没有比较符合要求的项目,没有办法,我只能给大家搓一个出来,项目地址是:https://github.com/Shiinama/next-cloudflare-template

    接下来大家就跟着我走,这一章的主要目的是把项目和本地数据库跑起来,把基础设施讲一遍,并且通过最常用的Google登录去跑通这个本地流程。

    基础背景知识的补充是在:

    Drizzle

    D1

    登录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';"`,此时就可以看到本地表已经创建成功了。
    
    ![d1-execute](https://ik.imagekit.io/ixou4q6nu/d1-execute.png)
    
    > 需要理解的是:我们使用`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会拒绝认证请求。
    
    ![google-login](https://ik.imagekit.io/ixou4q6nu/sercet.png)
    
    可以看到会有两个秘钥,在图的马赛克位置,将其填入到`.env`文件中的对应字段内,这部分由`next-auth`库自动读取,是`约定命名`。
    
    ```
    AUTH_GOOGLE_ID=""
    AUTH_GOOGLE_SECRET=""
    ```
    
    接下来就是登录部分,有关文件是`components/login`、`app/api/auth/[...nextauth]/route.ts`。
    
    ![](https://ik.imagekit.io/ixou4q6nu/login-demo.png)
    
    点一下登录,走一些流程之后,我们可以看到在控制台里面多了google的回调:
    
    ![](https://ik.imagekit.io/ixou4q6nu/google-callback.png)
    
    这个回调是由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)
    ```
    
    可以看到已经成功的插入数据了
    
    ![](https://ik.imagekit.io/ixou4q6nu/drizzle-gui.png)
    
    那么其实到这还是不够的,我们要在代码中查出数据,验证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>
      )
    }
    ```
    
    结果如下:
    
    ![](https://ik.imagekit.io/ixou4q6nu/console-test.png)
    
    ## 结束
    
    那么到这里,其实我们已经把项目和本地数据库跑起来了,那么下一章那我们就直接开始远程项目的部署。
    

    评论

    加入讨论

    0 条评论
    登录后评论

    还没有评论,来占个沙发吧。