Easy Vibe Coding 学习心得(四):后端之力——从数据库到全栈应用

一、从"前端"到"全栈"——认知的升级

1.1 前一篇的回顾

在上一篇文章中,我学习了前端设计的艺术。从 Figma 设计工具到组件库,从提示词美化到 Skills 插件,我做出了一个精美的霍格沃茨魔法画像页面。

那个项目能看、能聊、能变化表情,界面充满魔法世界的氛围。

但朋友试用后问了我几个问题:

“聊天记录换个设备就没了?”
“我想和朋友分享这个应用,怎么部署上线?”
“如果很多人同时用,服务器扛得住吗?”

那一刻我意识到:前端再美,也只是"面子";后端才是"里子"。

1.2 新的问题

我开始思考一个完整应用需要解决的后端问题:

数据持久化:

  • 用户数据存在哪里?
  • 如何保证数据安全?
  • 换设备后数据能同步吗?

用户系统:

  • 如何注册登录?
  • 如何管理用户权限?
  • 如何防止恶意攻击?

业务逻辑:

  • 复杂的计算放在哪里?
  • 如何调用第三方 API?
  • 如何处理并发请求?

部署运维:

  • 应用部署在哪里?
  • 如何监控运行状态?
  • 出问题了怎么排查?

带着这些问题,我进入了 Stage 3 的后端部分——学习如何构建全栈应用

二、后端技术选型:从简单到复杂

2.1 后端技术的演变

教程里对后端技术的演变讲得很清楚:

第一代:传统后端(2000-2010)

  • Java + Spring、.NET、PHP
  • 需要自己买服务器、配置环境
  • 部署复杂,运维成本高

第二代:云服务时代(2010-2020)

  • AWS、Azure、阿里云
  • 按需付费,弹性伸缩
  • 但仍需要运维知识

第三代:Serverless 时代(2020-至今)

  • Vercel、Zeabur、Supabase
  • 零运维,自动扩缩容
  • 按使用量付费,成本极低

Vibe Coding 时代的选择:

  • 前端托管:Vercel、Netlify、Zeabur
  • 后端服务:Supabase、Firebase、Appwrite
  • AI 能力:DeepSeek、SiliconFlow、Dify

2.2 我的技术栈选择

基于教程推荐和实际需求,我选择了以下技术栈:

需求 选择 理由
前端框架 Vue 3 学习曲线平缓,中文文档完善
UI 组件库 Element Plus 企业级组件,开箱即用
前端托管 Zeabur 国内访问快,免费额度够用
数据库 Supabase PostgreSQL 内核,功能完整
用户认证 Supabase Auth 内置多种登录方式
AI 文本 DeepSeek 性价比高,中文能力强
AI 图像 SiliconFlow 生成速度快,效果好

总成本:

  • 开发阶段:$0(全部使用免费额度)
  • 小规模使用:约 $10/月
  • 大规模使用:按需付费

三、Supabase 入门:5 分钟搭建后端

3.1 什么是 Supabase

教程里有个比喻很形象:

“Supabase 就是’开源版的 Firebase’,提供数据库、用户认证、文件存储、实时订阅等一站式后端服务。”

Supabase 提供什么?

  • Database:PostgreSQL 数据库
  • Auth:用户注册登录
  • Storage:文件存储
  • Edge Functions:后端函数
  • Realtime:实时数据推送

3.2 创建第一个项目

第一步:注册账号

  1. 访问 supabase.com
  2. 用 GitHub 账号登录
  3. 创建新项目

第二步:创建数据表

在 Supabase 控制台执行 SQL:

-- 创建博客文章表
CREATE TABLE posts (
  id BIGSERIAL PRIMARY KEY,
  title VARCHAR(255) NOT NULL,
  content TEXT,
  author_id UUID REFERENCES auth.users(id),
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

-- 创建行级安全策略
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- 允许任何人读取
CREATE POLICY "允许任何人读取" ON posts
  FOR SELECT USING (true);

-- 允许登录用户创建
CREATE POLICY "允许登录用户创建" ON posts
  FOR INSERT WITH CHECK (auth.role() = 'authenticated');

-- 允许作者更新自己的文章
CREATE POLICY "允许作者更新" ON posts
  FOR UPDATE USING (auth.uid() = author_id);

第三步:获取 API Key

在设置中找到:

  • Project URLhttps://xxx.supabase.co
  • Anon KeyeyJhbGc...(前端使用)
  • Service Role KeyeyJhbGc...(后端使用,不要泄露)

3.3 前端接入 Supabase

第一步:安装 SDK

npm install @supabase/supabase-js

第二步:创建客户端

// utils/supabase.js
import { createClient } from '@supabase/supabase-js'

const supabaseUrl = 'https://xxx.supabase.co'
const supabaseKey = 'your-anon-key'

export const supabase = createClient(supabaseUrl, supabaseKey)

第三步:实现用户登录

// 邮箱密码登录
async function signInWithEmail(email, password) {
  const { data, error } = await supabase.auth.signInWithPassword({
    email,
    password
  })

  if (error) {
    console.error('登录失败:', error.message)
    return null
  }

  return data.user
}

// 注册
async function signUp(email, password) {
  const { data, error } = await supabase.auth.signUp({
    email,
    password
  })

  if (error) {
    console.error('注册失败:', error.message)
    return null
  }

  return data.user
}

// GitHub 登录(一键登录)
async function signInWithGitHub() {
  const { data, error } = await supabase.auth.signInWithOAuth({
    provider: 'github',
    options: {
      redirectTo: window.location.origin
    }
  })
}

// 退出登录
async function signOut() {
  await supabase.auth.signOut()
}

// 获取当前用户
function getCurrentUser() {
  return supabase.auth.getUser()
}

第四步:监听登录状态

// 监听认证状态变化
supabase.auth.onAuthStateChange((event, session) => {
  if (event === 'SIGNED_IN') {
    console.log('用户已登录')
    // 更新 UI
  } else if (event === 'SIGNED_OUT') {
    console.log('用户已退出')
    // 更新 UI
  }
})

四、项目实战:AI 个人博客系统

4.1 项目背景

学完 Supabase 后,我决定做一个完整的全栈项目来实践所学。

项目目标:

  • 用户可以注册登录
  • 输入一个主题,AI 自动生成博客文章
  • 文章保存到数据库
  • 支持编辑、删除、分享

技术栈:

  • 前端:Vue 3 + Element Plus
  • 后端:Supabase(数据库 + 认证)
  • AI:DeepSeek(文章生成)
  • 部署:Zeabur

在这里插入图片描述

4.2 需求分析

核心功能:

  1. 用户系统:注册、登录、退出
  2. 文章生成:输入主题,AI 生成文章
  3. 文章管理:查看、编辑、删除
  4. 分享功能:生成分享链接

非功能需求:

  • 响应式设计,支持手机和电脑
  • 数据实时同步
  • 加载状态友好提示

4.3 数据库设计

数据表结构:

-- 用户资料表
CREATE TABLE profiles (
  id UUID REFERENCES auth.users PRIMARY KEY,
  username VARCHAR(50) UNIQUE,
  avatar_url TEXT,
  bio TEXT,
  created_at TIMESTAMP DEFAULT NOW()
);

-- 博客文章表
CREATE TABLE posts (
  id BIGSERIAL PRIMARY KEY,
  title VARCHAR(255) NOT NULL,
  content TEXT NOT NULL,
  summary VARCHAR(500),
  cover_image TEXT,
  author_id UUID REFERENCES profiles(id),
  view_count INTEGER DEFAULT 0,
  published BOOLEAN DEFAULT false,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

-- 文章标签表
CREATE TABLE tags (
  id BIGSERIAL PRIMARY KEY,
  name VARCHAR(50) UNIQUE NOT NULL
);

-- 文章标签关联表
CREATE TABLE post_tags (
  post_id BIGINT REFERENCES posts(id) ON DELETE CASCADE,
  tag_id BIGINT REFERENCES tags(id) ON DELETE CASCADE,
  PRIMARY KEY (post_id, tag_id)
);

-- 创建触发器:用户注册时自动创建资料
CREATE OR REPLACE FUNCTION handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
  INSERT INTO profiles (id, username, avatar_url)
  VALUES (NEW.id, NEW.email, NEW.raw_user_meta_data->>'avatar_url');
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER on_auth_user_created
  AFTER INSERT ON auth.users
  FOR EACH ROW EXECUTE FUNCTION handle_new_user();

4.4 前端实现

项目结构:

ai-blog-generator/
├── src/
│   ├── components/
│   │   ├── LoginModal.vue      # 登录弹窗
│   │   ├── PostEditor.vue      # 文章编辑器
│   │   ├── PostList.vue        # 文章列表
│   │   └── PostCard.vue        # 文章卡片
│   ├── composables/
│   │   ├── useAuth.js          # 认证逻辑
│   │   ├── usePosts.js         # 文章逻辑
│   │   └── useAI.js            # AI 生成逻辑
│   ├── views/
│   │   ├── Home.vue            # 首页
│   │   ├── Editor.vue          # 编辑器页面
│   │   └── Profile.vue         # 个人主页
│   ├── utils/
│   │   └── supabase.js         # Supabase 客户端
│   └── App.vue
├── package.json
└── vite.config.js

核心代码:

<!-- App.vue -->
<template>
  <div id="app">
    <Navbar :user="currentUser" @login="showLogin = true" @logout="handleLogout" />

    <main class="container">
      <router-view />
    </main>

    <LoginModal v-if="showLogin" @close="showLogin = false" @login="handleLogin" />
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { supabase } from './utils/supabase'
import Navbar from './components/Navbar.vue'
import LoginModal from './components/LoginModal.vue'

const currentUser = ref(null)
const showLogin = ref(false)

// 检查登录状态
onMounted(async () => {
  const { data: { session } } = await supabase.auth.getSession()
  currentUser.value = session?.user || null
})

// 处理登录
function handleLogin(user) {
  currentUser.value = user
  showLogin.value = false
}

// 处理退出
async function handleLogout() {
  await supabase.auth.signOut()
  currentUser.value = null
}
</script>
// composables/usePosts.js
import { ref } from 'vue'
import { supabase } from '../utils/supabase'

export function usePosts() {
  const posts = ref([])
  const loading = ref(false)
  const error = ref(null)

  // 获取所有文章
  async function fetchPosts() {
    loading.value = true
    error.value = null

    try {
      const { data, error: err } = await supabase
        .from('posts')
        .select(`
          *,
          profiles (username, avatar_url)
        `)
        .eq('published', true)
        .order('created_at', { ascending: false })

      if (err) throw err
      posts.value = data
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }

  // 创建文章
  async function createPost(title, content, summary, coverImage) {
    const { data: { user } } = await supabase.auth.getUser()
    if (!user) {
      error.value = '请先登录'
      return null
    }

    const { data, error: err } = await supabase
      .from('posts')
      .insert({
        title,
        content,
        summary,
        cover_image: coverImage,
        author_id: user.id
      })
      .select()
      .single()

    if (err) {
      error.value = err.message
      return null
    }

    await fetchPosts()
    return data
  }

  // 删除文章
  async function deletePost(id) {
    const { error: err } = await supabase
      .from('posts')
      .delete()
      .eq('id', id)

    if (err) {
      error.value = err.message
      return false
    }

    await fetchPosts()
    return true
  }

  return {
    posts,
    loading,
    error,
    fetchPosts,
    createPost,
    deletePost
  }
}
// composables/useAI.js
import { ref } from 'vue'

const DEEPSEEK_API_KEY = import.meta.env.VITE_DEEPSEEK_API_KEY

export function useAI() {
  const generating = ref(false)
  const generateError = ref(null)

  // 生成文章
  async function generateArticle(topic) {
    generating.value = true
    generateError.value = null

    try {
      const response = await fetch('https://api.deepseek.com/v1/chat/completions', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${DEEPSEEK_API_KEY}`
        },
        body: JSON.stringify({
          model: 'deepseek-chat',
          messages: [
            {
              role: 'system',
              content: '你是一位专业的博客作者,擅长撰写高质量的技术文章。请根据用户提供的主题,生成一篇结构清晰、内容详实的博客文章。文章应包含:标题、摘要、正文(分多个小节)、总结。使用 Markdown 格式。'
            },
            {
              role: 'user',
              content: `请以"${topic}"为主题,生成一篇博客文章。`
            }
          ],
          temperature: 0.7,
          max_tokens: 2000
        })
      })

      const data = await response.json()

      if (!response.ok) {
        throw new Error(data.error?.message || '生成失败')
      }

      const content = data.choices[0].message.content

      // 解析 Markdown,提取标题和摘要
      const lines = content.split('\n')
      const title = lines.find(line => line.startsWith('# '))?.replace('# ', '') || topic
      const summary = lines.find(line => line.length > 50 && line.length < 200)?.substring(0, 200) + '...' || ''

      return {
        title,
        content,
        summary,
        cover_image: await generateCoverImage(topic)
      }
    } catch (err) {
      generateError.value = err.message
      return null
    } finally {
      generating.value = false
    }
  }

  // 生成封面图(调用 SiliconFlow)
  async function generateCoverImage(topic) {
    const SILICONFLOW_API_KEY = import.meta.env.VITE_SILICONFLOW_API_KEY

    try {
      const response = await fetch('https://api.siliconflow.cn/v1/images/generations', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${SILICONFLOW_API_KEY}`
        },
        body: JSON.stringify({
          model: 'black-forest-labs/FLUX.1-schnell',
          prompt: `A beautiful blog cover image about ${topic}, minimalist style, high quality`,
          size: '1024x1024',
          n: 1
        })
      })

      const data = await response.json()
      return data.data?.[0]?.url || null
    } catch (err) {
      console.error('图片生成失败:', err)
      return null
    }
  }

  return {
    generating,
    generateError,
    generateArticle
  }
}

4.5 用户认证实现

<!-- components/LoginModal.vue -->
<template>
  <div class="modal-overlay" @click="$emit('close')">
    <div class="modal-content" @click.stop>
      <div class="modal-header">
        <h2>登录 / 注册</h2>
        <button class="close-btn" @click="$emit('close')">×</button>
      </div>

      <div class="modal-body">
        <!-- GitHub 一键登录 -->
        <button class="github-btn" @click="handleGitHubLogin">
          <svg class="github-icon" viewBox="0 0 24 24">
            <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
          </svg>
          使用 GitHub 登录
        </button>

        <div class="divider">
          <span>或</span>
        </div>

        <!-- 邮箱登录 -->
        <form @submit.prevent="handleEmailLogin">
          <div class="form-group">
            <label>邮箱</label>
            <input
              type="email"
              v-model="email"
              placeholder="your@email.com"
              required
            />
          </div>

          <div class="form-group">
            <label>密码</label>
            <input
              type="password"
              v-model="password"
              placeholder="至少 6 位"
              minlength="6"
              required
            />
          </div>

          <button type="submit" class="submit-btn" :disabled="loading">
            {{ isRegister ? '注册' : '登录' }}
          </button>
        </form>

        <p class="toggle-text">
          {{ isRegister ? '已有账号?' : '没有账号?' }}
          <a href="#" @click.prevent="isRegister = !isRegister">
            {{ isRegister ? '去登录' : '去注册' }}
          </a>
        </p>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { supabase } from '../utils/supabase'

const emit = defineEmits(['close', 'login'])

const email = ref('')
const password = ref('')
const isRegister = ref(false)
const loading = ref(false)

// GitHub 登录
async function handleGitHubLogin() {
  const { data, error } = await supabase.auth.signInWithOAuth({
    provider: 'github',
    options: {
      redirectTo: window.location.origin
    }
  })

  if (error) {
    alert('登录失败:' + error.message)
  }
}

// 邮箱登录
async function handleEmailLogin() {
  loading.value = true

  try {
    const { data, error } = isRegister.value
      ? await supabase.auth.signUp({ email: email.value, password: password.value })
      : await supabase.auth.signInWithPassword({ email: email.value, password: password.value })

    if (error) throw error

    emit('login', data.user)
    emit('close')
  } catch (error) {
    alert(error.message)
  } finally {
    loading.value = false
  }
}
</script>

<style scoped>
.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.7);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.modal-content {
  background: #1a1a2e;
  border-radius: 20px;
  padding: 30px;
  width: 90%;
  max-width: 400px;
  border: 1px solid rgba(255, 255, 255, 0.1);
}

.github-btn {
  width: 100%;
  padding: 12px;
  background: #24292e;
  color: white;
  border: none;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  cursor: pointer;
  font-size: 1em;
  transition: all 0.3s ease;
}

.github-btn:hover {
  background: #2f363d;
}

.github-icon {
  width: 20px;
  height: 20px;
  fill: white;
}

.divider {
  display: flex;
  align-items: center;
  margin: 20px 0;
  color: #888;
}

.divider::before,
.divider::after {
  content: '';
  flex: 1;
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}

.divider span {
  padding: 0 10px;
}

.form-group {
  margin-bottom: 15px;
}

.form-group label {
  display: block;
  margin-bottom: 5px;
  color: #888;
}

.form-group input {
  width: 100%;
  padding: 10px 15px;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 8px;
  color: white;
}

.submit-btn {
  width: 100%;
  padding: 12px;
  background: linear-gradient(135deg, #667eea, #764ba2);
  color: white;
  border: none;
  border-radius: 10px;
  cursor: pointer;
  font-size: 1em;
}

.toggle-text {
  text-align: center;
  margin-top: 15px;
  color: #888;
}

.toggle-text a {
  color: #667eea;
}
</style>

4.6 部署上线

第一步:推送到 GitHub

git init
git add .
git commit -m "Initial commit: AI Blog Generator"
git remote add origin git@github.com:username/ai-blog-generator.git
git push -u origin main

第二步:部署到 Zeabur

  1. 访问 zeabur.com
  2. 用 GitHub 账号登录
  3. 点击 “New Project”
  4. 选择 “Deploy from GitHub”
  5. 选择项目仓库
  6. 配置环境变量:
    • VITE_SUPABASE_URL
    • VITE_SUPABASE_ANON_KEY
    • VITE_DEEPSEEK_API_KEY
    • VITE_SILICONFLOW_API_KEY
  7. 点击 “Deploy”

第三步:配置自定义域名(可选)

  1. 在 Zeabur 控制台添加域名
  2. 在域名服务商处添加 CNAME 记录
  3. 等待 DNS 生效

4.7 成果展示

功能清单:

  • ✅ 用户注册登录(GitHub / 邮箱)
  • ✅ 输入主题,AI 自动生成文章
  • ✅ 文章保存到数据库
  • ✅ 文章列表展示
  • ✅ 文章详情查看
  • ✅ 作者个人主页
  • ✅ 响应式设计

性能数据:

  • 文章生成:平均 5-8 秒
  • 页面加载:<1 秒
  • 首屏渲染:<500ms

成本估算:

  • Supabase:免费额度(500MB 数据库,50000 月活用户)
  • Zeabur:免费额度(每月 100 万积分)
  • DeepSeek:约 ¥0.01/篇(按 token 计费)
  • SiliconFlow:约 ¥0.02/张(按生成次数计费)

五、踩过的坑和解决方案

5.1 行级安全策略(RLS)

问题: 创建文章时报错 “permission denied for table posts”。

原因: Supabase 默认开启行级安全策略,需要显式配置权限。

解决:

-- 允许登录用户创建文章
CREATE POLICY "允许登录用户创建" ON posts
  FOR INSERT WITH CHECK (auth.role() = 'authenticated');

-- 允许作者更新自己的文章
CREATE POLICY "允许作者更新" ON posts
  FOR UPDATE USING (auth.uid() = author_id);

-- 允许任何人读取已发布的文章
CREATE POLICY "允许任何人读取" ON posts
  FOR SELECT USING (published = true);

5.2 API Key 泄露风险

问题: 把 API Key 写在前端代码里,有泄露风险。

解决:

// 使用环境变量
const DEEPSEEK_API_KEY = import.meta.env.VITE_DEEPSEEK_API_KEY

// .env 文件(不要提交到 Git)
VITE_DEEPSEEK_API_KEY=sk-xxxxx
VITE_SILICONFLOW_API_KEY=sk-xxxxx

// .gitignore
.env
.env.local

更好的方案: 使用 Supabase Edge Functions 作为代理层。

// edge-functions/generate-article/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'

serve(async (req) => {
  const { topic } = await req.json()

  const response = await fetch('https://api.deepseek.com/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${Deno.env.get('DEEPSEEK_API_KEY')}`
    },
    body: JSON.stringify({
      model: 'deepseek-chat',
      messages: [
        {
          role: 'system',
          content: '你是一位专业的博客作者...'
        },
        {
          role: 'user',
          content: `请以"${topic}"为主题,生成一篇博客文章。`
        }
      ]
    })
  })

  const data = await response.json()
  return new Response(JSON.stringify(data), {
    headers: { 'Content-Type': 'application/json' }
  })
})

5.3 数据库关联查询

问题: 查询文章时,同时获取作者信息,怎么写?

解决: 使用 Supabase 的关联查询。

const { data, error } = await supabase
  .from('posts')
  .select(`
    *,
    profiles (
      username,
      avatar_url
    )
  `)
  .eq('published', true)

5.4 图片存储方案

问题: AI 生成的图片 URL 会过期,怎么办?

解决: 下载到 Supabase Storage。

// 下载并保存图片
async function saveCoverImage(url, postId) {
  const response = await fetch(url)
  const blob = await response.blob()

  const { data, error } = await supabase.storage
    .from('cover-images')
    .upload(`posts/${postId}.jpg`, blob)

  if (error) throw error

  // 获取公开访问 URL
  const { data: { publicUrl } } = supabase.storage
    .from('cover-images')
    .getPublicUrl(`posts/${postId}.jpg`)

  return publicUrl
}

六、关键收获

6.1 全栈思维:从"页面"到"系统"

以前我只关注前端页面:

  • “这个页面怎么布局”
  • “这个动画怎么做”

现在我学会了系统思考:

  • “数据存在哪里”
  • “如何保证数据安全”
  • “用户如何认证”
  • “如何部署上线”

全栈不是什么都懂,而是能把各个部分连接起来。

6.2 数据思维:从"静态"到"动态"

以前我的应用数据是静态的:

  • 写死在 HTML 里
  • 存在 LocalStorage

现在我的应用数据是动态的:

  • 存在云端数据库
  • 多设备同步
  • 实时更新

数据是应用的核心,设计好数据结构,应用就成功了一半。

6.3 安全思维:从"信任"到"验证"

以前我相信用户不会乱来:

  • 前端验证就够了
  • API Key 直接写代码里

现在我学会了防御性编程:

  • 后端验证必不可少
  • 行级安全策略保护数据
  • API Key 放在服务端

安全不是功能,而是底线。

6.4 工程思维:从"本地"到"线上"

以前我只在本地运行:

  • “在我电脑上能跑就行”

现在我学会了工程化部署:

  • 环境变量管理
  • CI/CD 流程
  • 日志监控
  • 错误追踪

只有线上稳定运行的代码,才是有价值的代码。

七、下一步计划

7.1 短期优化(1 周内)

  • 添加文章评论功能
  • 添加点赞/收藏功能
  • 添加文章搜索功能
  • 优化移动端体验

7.2 中期优化(1 个月内)

  • 添加付费订阅功能(Stripe)
  • 添加文章统计分析(阅读量、来源)
  • 添加 RSS 订阅
  • 添加 SEO 优化

7.3 长期规划

  • 支持多语言
  • 添加协作编辑功能
  • 开发桌面应用(Electron)
  • 开发小程序版本

八、写给同样想学习的你

8.1 关于"后端太难"

很多人觉得后端很难,我刚开始也这么想。

但学完后我发现:后端的核心就三件事

  1. 存数据(数据库)
  2. 认用户(认证授权)
  3. 调接口(业务逻辑)

用 Supabase 这样的工具,这三件事都变得非常简单。

工具选对了,难度就降下来了。

8.2 关于"学不完"

技术永远学不完,关键是学以致用

我的建议是:

  • 先确定一个想做的项目
  • 遇到不会的技术再去学
  • 学了马上用到项目里

项目驱动学习,效率最高。

8.3 关于"做不好"

我做的第一个全栈应用,漏洞百出:

  • 数据库没有安全策略
  • API Key 直接暴露
  • 错误处理一塌糊涂

但没关系,先做出来,再慢慢优化

每个优秀的开发者,都是从写出烂代码开始的。

8.4 最后的话

学完后端这部分,我最大的感受是:

全栈开发不再是少数人的专利,而是每个前端开发者都能掌握的技能。

Supabase 这样的工具,让后端开发变得像调用 API 一样简单。

我们不需要成为数据库专家,不需要精通运维,只需要专注于业务逻辑和用户体验。

这就是 Vibe Coding 的力量——让复杂的事情变简单。

完成比完美更重要。

这句话我会一直记着。


下一篇:《学习心得(五):综合实战——我的 AI 原生应用上线了》


⚠️ 免责声明:本文由 AI 智能体辅助创作,内容仅供参考。文中涉及的代码示例、技术方案请在实际应用前自行验证。观点仅代表作者个人,不构成任何形式的投资或技术决策建议。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐