嗨,各位跃跃欲试的开发者们!我是雪狼。咱们前几篇文章,从 Serverless 的概念,到边缘计算的威力,再到 Serverless Functions 和数据库的「江湖门派」,理论知识的基石已经给你夯得实实的了

但光说不练,终究是「纸上谈兵」。古人云:「纸上得来终觉浅,绝知此事要躬行。」 这话一点没错!

现在,是时候让我们亲手「添砖加瓦」,从零开始,一步步构建并部署属于咱们自己的第一座「全栈 Serverless 大厦」了。我们将通过一个功能完整的「在线留言板」应用,亲身体验现代化 Angular 与 Serverless 平台(以 Vercel 为例)相结合,能爆发出多么惊人的开发效率和多么丝滑的部署流程。

准备好了吗?系好安全带,跟着雪狼,咱们要发车了!这趟旅程,保证让你彻底打通任督二脉,轻松迈出成为真正全栈开发者的第一步

我们的目标:一个麻雀虽小,五脏俱全的「雪狼留言板」#

咱们这次要搭建的,是一个功能虽然简单,但却能完整体现全栈 Serverless 精髓的「留言板」 。它就像一只麻雀,虽然小巧,但五脏俱全,能让你对整个开发流程有一个清晰的认知。

  • 前端呈现:咱们依然用最爱的 Angular。一个独立的组件,包含一个表单,用于让来访者留下「昵称」 和「留言」 ,下方则是一个列表,实时展示所有历史留言,营造一种亲切的互动氛围。

  • 后端驱动:后端逻辑将由两个精悍的 Serverless Functions 来支撑。这两个函数会和你的 Angular 前端代码和谐地住在同一个代码仓库里

    • POST /api/messages:专门负责接收前端提交过来的新留言,并妥善存入数据库。

    • GET /api/messages:供前端调用,用于获取所有历史留言,让你的留言板充满「故事」。

  • 数据心脏:我们将采用 Vercel KV 作为数据库,这是一个基于 Redis 的、对 Serverless 环境极度友好的键值对数据库,读写速度快如闪电,完美契合咱们的需求。

  • 部署哲学:通过简单的 git push 命令,实现前端和后端代码的「一键部署」 到 Vercel 的全球网络,真正体验到「无拘无束」 的丝滑流程。

准备好了吗?咱们就从这只「麻雀」开始,开启你的全栈 Serverless 之旅!

第一步:初始化 Angular 项目 —— 「万丈高楼平地起」#

万丈高楼平地起」 ,咱们的全栈 Serverless 之旅,就从 Angular 项目的初始化开始。首先,请确保你的开发环境已经安装了最新版的 Node.js 和 Angular CLI。

# --ssr=false 咱们暂时不启用服务端渲染,保持简单
ng new my-guestbook --standalone --style=scss --ssr=false
# 然后,进入到咱们的项目目录
cd my-guestbook

为了让教程更聚焦核心逻辑,咱们暂时「偷个懒」,把所有的前端工作都直接在 src/app/app.component.ts 这个根组件里完成。在实际项目中,当然会进行更细致的组件拆分。

安装核心依赖:

在项目目录下,咱们还需要安装一些必要的依赖。

# @vercel/kv 是咱们本地开发时用于模拟连接 Vercel KV 数据库的客户端(或者在 Vercel 平台环境下直接使用)
# 它提供了一种方便的方式来操作键值对数据
npm install @vercel/kv 
# @vercel/node 为咱们的 Serverless Functions 提供了类型定义,让 TypeScript 开发更顺畅
# 它是开发依赖,只在开发时需要
npm install -D @vercel/node 

编写前端代码 (src/app/app.component.ts) —— 打造留言板的「门面」:

现在,咱们来编写 Angular 的根组件代码。它将负责展示留言列表和提交新留言的表单。

import { Component, inject, signal, OnInit } from '@angular/core';
import { HttpClient, HttpClientModule } from '@angular/common/http'; // 别忘了导入 HttpClientModule
import { ReactiveFormsModule, FormGroup, FormControl, Validators } from '@angular/forms';
import { CommonModule } from '@angular/common'; // NgIf, NgFor 等指令需要
// 定义留言的数据结构,保持前后端一致性
interface Message {
  name: string;
  message: string;
  timestamp: number; // 留言时间戳,用于排序和唯一标识
}
@Component({
  selector: 'app-root',
  standalone: true, // 独立组件,现代 Angular 的推荐做法
  imports: [ReactiveFormsModule, HttpClientModule, CommonModule], // 导入必要的模块
  template: `
    <main class="guestbook-container">
      <h1>雪狼的留言板</h1>
      <form [formGroup]="form" (ngSubmit)="onSubmit()" class="message-form">
        <input formControlName="name" placeholder="你的昵称" required>
        <textarea formControlName="message" placeholder="留下你的足迹..." required></textarea>
        <button type="submit" [disabled]="form.invalid">提交留言</button>
      </form>
      <section class="messages-list">
        @for (msg of messages(); track msg.timestamp) {
          <div class="message-item">
            <p>"{{ msg.message }}"</p>
            <span> —— {{ msg.name }} ({{ formatTimestamp(msg.timestamp) }})</span>
          </div>
        } @empty {
          <p class="empty-message">还没有留言呢,快来抢个「沙发」吧!</p>
        }
      </section>
    </main>
  `,
  styles: [`
    /* 留言板的整体样式 */
    .guestbook-container {
      max-width: 700px;
      margin: 40px auto;
      padding: 30px;
      background-color: #f9f9f9;
      border-radius: 12px;
      box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      color: #333;
    }
    .guestbook-container h1 {
      text-align: center;
      color: #2c3e50;
      margin-bottom: 30px;
      font-size: 2.2em;
      font-weight: 600;
    }
    /* 表单样式 */
    .message-form {
      display: flex;
      flex-direction: column;
      gap: 15px;
      margin-bottom: 30px;
      padding: 20px;
      border: 1px solid #e0e0e0;
      border-radius: 8px;
      background-color: #ffffff;
      box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
    }
    .message-form input,
    .message-form textarea {
      padding: 12px 15px;
      border: 1px solid #c0c0c0;
      border-radius: 6px;
      font-size: 1em;
      transition: border-color 0.3s ease, box-shadow 0.3s ease;
    }
    .message-form input:focus,
    .message-form textarea:focus {
      border-color: #007bff;
      box-shadow: 0 0 8px rgba(0, 123, 255, 0.2);
      outline: none;
    }
    .message-form textarea {
      resize: vertical;
      min-height: 80px;
    }
    .message-form button {
      padding: 12px 25px;
      background-color: #007bff;
      color: white;
      border: none;
      border-radius: 6px;
      font-size: 1.1em;
      cursor: pointer;
      transition: background-color 0.3s ease, transform 0.2s ease;
    }
    .message-form button:hover:not(:disabled) {
      background-color: #0056b3;
      transform: translateY(-2px);
    }
    .message-form button:disabled {
      background-color: #cccccc;
      cursor: not-allowed;
    }
    /* 留言列表样式 */
    .messages-list {
      display: flex;
      flex-direction: column;
      gap: 20px;
    }
    .message-item {
      background-color: #ffffff;
      padding: 20px;
      border-radius: 8px;
      box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
      border-left: 5px solid #007bff;
    }
    .message-item p {
      margin: 0 0 10px 0;
      font-size: 1.1em;
      line-height: 1.6;
      color: #555;
    }
    .message-item span {
      display: block;
      text-align: right;
      font-size: 0.9em;
      color: #888;
    }
    .empty-message {
      text-align: center;
      color: #888;
      font-style: italic;
      padding: 20px;
      background-color: #fff;
      border-radius: 8px;
      box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
    }
  `]
})
export class AppComponent implements OnInit {
  private http = inject(HttpClient);
  // 使用 signal 来管理留言列表,这是 Angular 17+ 的新特性,更高效响应式
  messages = signal<Message[]>([]);
  // 使用 Reactive Forms 来构建表单,并进行基本验证
  form = new FormGroup({
    name: new FormControl('', Validators.required),
    message: new FormControl('', Validators.required),
  });
  ngOnInit() {
    // 组件初始化时,加载一次历史留言
    this.loadMessages();
  }
  // 从后端 API 加载留言的方法
  loadMessages() {
    this.http.get<Message[]>('/api/messages')
      .subscribe({
        next: data => {
          // 对留言按时间戳降序排序,最新的显示在最上面
          this.messages.set(data.sort((a, b) => b.timestamp - a.timestamp));
        },
        error: err => console.error('加载留言失败:', err)
      });
  }
  // 提交表单时调用的方法
  onSubmit() {
    if (this.form.valid) {
      // 发送 POST 请求到咱们的 Serverless Function API
      this.http.post('/api/messages', this.form.value)
        .subscribe({
          next: () => {
            this.loadMessages(); // 提交成功后,重新加载留言列表,更新 UI
            this.form.reset();   // 清空表单,方便用户继续留言
            alert('留言成功,感谢你的足迹!');
          },
          error: err => {
            console.error('提交留言失败:', err);
            alert('提交失败,请稍后再试!');
          }
        });
    } else {
      // 如果表单无效,触发表单验证,让错误信息显示出来
      this.form.markAllAsTouched();
    }
  }
  // 格式化时间戳,让时间显示更友好
  formatTimestamp(timestamp: number): string {
    const date = new Date(timestamp);
    return date.toLocaleString('zh-CN', { 
      year: 'numeric', 
      month: '2-digit', 
      day: '2-digit', 
      hour: '2-digit', 
      minute: '2-digit' 
    });
  }
}

怎么样?前端代码是不是既熟悉又清晰?咱们的 Angular 应用只负责好 UI 展示和数据交互,关于数据到底存在哪里,怎么存取的,它完全不用操心。这种「各司其职」 的感觉,是不是很棒?

第二步:创建 Serverless API —— 留言板的「智慧大脑」#

好了,前端的「门面」已经搭好,现在咱们来给留言板加上「智慧大脑」 —— 后端 API。还记得咱们前面说的「单一仓库,统一工作流」 吗?咱们就在 Angular 项目的隔壁,直接创建 Serverless Functions!

在你的项目根目录下,创建一个名为 api 的文件夹。然后,在里面创建一个 messages.ts 文件。这个文件就是咱们留言板的核心后端逻辑。

重点提示:Vercel KV 的配置

在你继续编写代码之前,你需要在 Vercel 上创建一个 KV 数据库实例。

  1. 登录 Vercel Dashboard。

  2. 导航到你的项目。

  3. 点击「Storage」选项卡。

  4. 选择「KV」。

  5. 点击「Create New KV Database」,根据提示创建。

  6. 创建成功后,Vercel 会提供一组环境变量(KV_URL, KV_REST_API_URL, KV_REST_API_TOKEN, KV_REST_API_READ_ONLY_TOKEN)。这些环境变量,在 Vercel 部署时会自动注入到你的 Serverless Functions 中,本地开发时 vercel dev 也会帮你自动处理。

编写后端函数 (api/messages.ts) —— 我们的留言处理中心:

import { kv } from '@vercel/kv'; // 引入 Vercel KV 客户端
import type { VercelRequest, VercelResponse } from '@vercel/node'; // 引入 Vercel Functions 的类型定义
// 这是 Vercel Serverless Function 的标准导出方式,一个默认函数
export default async function handler(
  request: VercelRequest, // 请求对象,包含了所有 HTTP 请求信息
  response: VercelResponse, // 响应对象,用于构造并返回 HTTP 响应
) {
  // 定义在 KV 数据库中存储留言列表的键名
  const GUESTBOOK_KEY = 'guestbook_messages';
  // --- 处理 POST 请求:前端提交新留言了 ---
  if (request.method === 'POST') {
    // 从请求体中解析出昵称和留言内容
    const { name, message } = request.body;
    // 简单的参数校验,防止恶意或空数据
    if (!name || !message) {
      return response.status(400).json({ error: '昵称和留言都是必填项哦!' });
    }
    // 构造新的留言对象,加上时间戳方便排序和管理
    const newMessage = { name, message, timestamp: Date.now() };
    try {
      // 使用 Vercel KV 的 lpush 命令,将新留言推入到名为 GUESTBOOK_KEY 的列表的头部
      // kv.lpush 是 Redis 的命令,这里 kv 客户端做了封装
      await kv.lpush(GUESTBOOK_KEY, JSON.stringify(newMessage));
      // 成功创建,返回 201 Created 状态码和新留言对象
      return response.status(201).json(newMessage);
    } catch (error) {
      console.error('保存留言失败:', error);
      return response.status(500).json({ error: '留言保存失败,请稍后再试!' });
    }
  }
  // --- 处理 GET 请求:前端要获取所有历史留言了 ---
  if (request.method === 'GET') {
    try {
      // 从 KV 数据库中获取名为 GUESTBOOK_KEY 的列表的所有留言
      // LRANGE 0 -1 代表获取列表中的所有元素
      const messages = await kv.lrange(GUESTBOOK_KEY, 0, -1);
      // Vercel KV 存储的是字符串,所以需要将它们解析回 JSON 对象
      // 并按照时间戳降序排列,最新的留言显示在最上面
      const parsedMessages = messages
        .map(m => JSON.parse(m as string))
        .sort((a, b) => b.timestamp - a.timestamp);
      // 返回 200 OK 状态码和解析后的留言列表
      return response.status(200).json(parsedMessages);
    } catch (error) {
      console.error('获取留言失败:', error);
      return response.status(500).json({ error: '获取留言失败,请稍后再试!' });
    }
  }
  // 如果是其他不支持的 HTTP 方法,咱们就返回 405 Method Not Allowed
  return response.status(405).send('Method Not Allowed');
}

怎么样?这个 Serverless Function 是不是既简单又清晰?它完美地完成了留言的增和查功能,而你完全不需要关心服务器的启动、端口的监听、路由的配置。所有这些底层细节,都被 Vercel 这样的平台「魔法般」 地抽象掉了。这就是「大道至简」 的魅力!

第三步:本地「全栈」开发 —— 体验「黑科技」 vercel dev 的魔法#

好了,前端代码和后端函数都已就绪。那么问题来了:如何在咱们的本地开发环境里,让 localhost:4200 运行的 Angular 应用,能够顺畅地调用到我们刚刚写的、还安安静静躺在 api/ 文件夹里的 Serverless 函数呢?

答案就是 —— Vercel CLI 提供的「黑科技」命令:vercel dev

首先,如果你还没安装 Vercel CLI,请在命令行中运行:

npm install -g vercel # 全局安装 Vercel CLI

然后,在你的项目根目录,也就是 my-guestbook 文件夹下,勇敢地敲下这行命令:

vercel dev

你会看到命令行输出一连串的信息,然后……见证奇迹的时刻到了! Vercel CLI 会智能地替你完成一系列操作:

  1. 它会智能识别出这是一个 Angular 项目,并自动在后台替你运行 ng serve,启动你的前端开发服务器。

  2. 它会扫描你的 api/ 目录,并为你本地启动一个临时的 Serverless 运行时,将你的 messages.ts 函数加载进去。

  3. 最关键的是,它会建立一个统一的本地开发入口(通常是 localhost:3000)。当你访问 / 时,它会智能地把请求转发给你的 Angular 应用;而当你的 Angular 应用请求 /api/messages 时,它又会毫不犹豫地把请求路由到你本地运行的 api/messages.ts 函数。

现在,打开你的浏览器,访问 localhost:3000!你会发现,你的全栈留言板应用,前端和后端都在本地完美地运行起来了!你可以在表单中输入内容,提交后,留言会立即显示在列表中,而且数据已经安全地存储到了本地模拟的 Vercel KV 中。这种「浑然天成」 的本地开发体验,是不是让你直呼「魔法」 ?!

这就是 Vercel 平台的强大之处:它不仅是部署工具,更是强大的开发伴侣,让本地全栈开发变得前所未有的丝滑

第四步:git push,全球部署 —— 「一键升空,星辰大海」#

本地开发体验已经足够丝滑,现在,是时候让咱们的「留言板航母」 全球起航了!借助 Git 和 Vercel,部署一个全栈 Serverless 应用,就像发射火箭一样简单,但效果却是「星辰大海」 。

  1. 初始化 Git 并推送到 GitHub

    如果你之前没有对项目进行 Git 初始化,那么现在来操作一下:

    
    git init # 初始化本地 Git 仓库
    
    git add . # 添加所有文件到暂存区
    
    git commit -m "feat: initial guestbook app with Angular and Serverless Functions" # 提交代码
    
    
    
    # 接下来,你需要在 GitHub/GitLab/Bitbucket 上创建一个空的远程仓库
    
    # 然后将本地仓库关联到远程仓库并推送
    
    # 例如,如果你的远程仓库地址是 git@github.com:your-username/my-guestbook.git
    
    git remote add origin git@github.com:your-username/my-guestbook.git
    
    git push -u origin main # 推送到 main 分支

    完成这一步,你的代码就已经躺在云端的 Git 仓库里了。

  2. 在 Vercel 上创建项目,并「一键部署」

    • 访问你的 Vercel Dashboard (vercel.com/dashboard),登录后,点击「Add New... -> Project」 。

    • Vercel 会提示你导入一个 Git 仓库。选择你刚刚推送的 GitHub/GitLab/Bitbucket 仓库并导入。

    • Vercel 的智能之处就在于此:它会自动识别出你的项目是「Angular」 框架,并为你自动填好构建命令(ng build)和输出目录(通常是 dist/你的项目名/browser)。你几乎不需要手动配置什么!

    • 关键一步:配置环境变量! 在部署前,务必在 Vercel 项目的「Environment Variables」 (环境变量)部分,添加你在第一步创建 Vercel KV 数据库时得到的那四个环境变量:KV_URL, KV_REST_API_URL, KV_REST_API_TOKENKV_REST_API_READ_ONLY_TOKEN。这些是你的 Serverless Function 连接数据库的「通行证」 ,一定要填入,否则数据库将无法连接!

    • 一切就绪,点击「Deploy」 按钮!

文生图:Vercel的部署界面截图,显示了从GitHub导入项目,自动识别为Angular框架,以及部署成功后燃放烟花的动画。截图应清晰展示Vercel Dashboard的界面元素和部署流程。风格:真实的软件截图。

几分钟的等待之后(通常非常快!),你会看到 Vercel 部署成功的页面,并给你一个公开的 URL。现在,恭喜你!你的全栈留言板应用,已经成功部署在了 Vercel 的全球 CDN 网络上,并且拥有一个可无限扩展、按需付费的 Serverless 后端。无论用户身在何处,都能以极快的速度访问你的应用,并留下他们的「足迹」。是不是感觉酷毙了?!

结语 —— 「道不远人,人之为道而远人」#

各位前端的同仁们,我们刚刚所做的,几乎就是现代全栈 Serverless 开发的全部核心流程。从本地的 ng new 到云端的 git push,再到全球部署成功,整个过程行云流水,没有任何与「服务器」相关的繁琐操作。

这正是 Angular 与 Serverless 组合的魅力所在。它打破了传统开发的壁垒,让前端开发者能以前所未有的低成本,去构建、去实验、去发布一个功能完整、具备生产级别的产品。你不再需要「一人分饰多角」,而是可以凭借你前端的「内功」 ,轻松驾驭后端的力量。

正如《中庸》所言:

道不远人,人之为道而远人。

其意为:

真理(规律、方法)离我们不远,只是我们自己去做远离真理的事情。

全栈 Serverless 开发的「」 ,就在那里,它简化了复杂,拉近了我们与构建完整应用之间的距离。如果我们墨守成规,反而会觉得它很遥远。

现在,你已经亲手实践了全栈 Serverless 的力量。通往「全栈」 的道路,从未如此平坦而充满机遇。现在,轮到你去创造了,去把你的奇思妙想变成现实吧!雪狼相信你!