TUTORIAL

Serverless 部署轻量站点

Serverless 部署轻量站点

Serverless 部署轻量站点

传统部署要租服务器、装环境、配 Nginx、管证书、监控负载——一台机器从配置到上线少说半天,流量突增还得手动扩容。Serverless 把这些全免了:你只管上传代码,平台自动分配资源、自动扩缩容、自动续证书、按调用次数计费。这是轻量化站点的终极部署方案,也是本系列的收官篇。我们将用 Vercel 和 Cloudflare Pages 实操部署一个静态站点 + Serverless Function。

一、Serverless 的核心概念与适用场景

Serverless 不是"没有服务器",而是"服务器对你不可见"——平台帮你管服务器,你只写函数。每个函数被请求时才启动,处理完就释放,按实际执行次数和时长计费,没请求时零成本。这跟传统"常驻进程"模式截然不同:传统是"先开机器等请求",Serverless 是"来请求才启机器"。对流量波动大的站点,这种模式能省一大笔钱。

// Serverless Function 长这样:一个普通函数
// 文件:api/contact.js(Vercel 约定 api/ 目录下的文件自动变 API)
export default async function handler(req, res) {
  // 只处理 POST
  if (req.method !== 'POST') {
    return res.status(405).json({ code: 405, msg: '仅支持 POST' });
  }

  const { name, phone, message } = req.body;

  // 1. 参数校验
  if (!name || !phone) {
    return res.status(400).json({ code: 400, msg: '姓名和电话必填' });
  }

  // 2. 防垃圾提交(基于 IP 限流,用环境变量存的 Redis 配置)
  const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
  const count = await redis.incr(`rate:${ip}`);
  if (count === 1) await redis.expire(`rate:${ip}`, 3600);  // 1 小时窗口
  if (count > 5) {
    return res.status(429).json({ code: 429, msg: '提交过于频繁,请稍后再试' });
  }

  // 3. 存入数据库(连接用环境变量,密钥不进代码)
  await db.query(
    'INSERT INTO contacts (name, phone, message, ip, created_at) VALUES (?, ?, ?, ?, NOW())',
    [name, phone, message, ip]
  );

  // 4. 发邮件通知(用第三方服务)
  await mailer.send({
    to: process.env.ADMIN_EMAIL,
    subject: '新咨询:' + name,
    body: `电话:${phone}\n留言:${message}`
  });

  // 5. 返回结果
  res.status(200).json({ code: 0, msg: '提交成功,我们会尽快联系您' });
}

// 环境变量在平台后台配置(Vercel: Settings → Environment Variables)
// process.env.DB_HOST / DB_USER / DB_PASS / ADMIN_EMAIL
// 代码里只有变量名,真实值在平台加密存储

Serverless Function 的特点是"无状态"——每次调用都是独立的,函数内部不能存全局变量(下次调用可能换台机器)。状态数据必须放外部存储(数据库、Redis、对象存储)。尧图踩过的坑:把验证码存在函数全局变量里,结果用户每次请求拿到不同机器,验证码对不上。改成存 Redis 后问题消失。

二、Vercel 部署静态站点 + API

Vercel 是 Next.js 母公司,对静态站点和 Serverless Function 支持最完善。部署流程极简:连 GitHub 仓库 → 选框架 → 自动构建部署。之后每次 push 代码自动触发部署,连 CI/CD 都省了。免费额度对中小站点够用:100GB 流量/月、100GB-Hours 函数执行/月。

# 项目结构
ldpk-site/
├── public/              # 静态资源(直接复制到输出)
│   ├── images/
│   └── robots.txt
├── api/                 # Serverless Function 目录
│   ├── contact.js       # → POST /api/contact
│   └── sitemap.js       # → GET /api/sitemap
├── index.html           # 静态首页
├── about.html           # 静态关于页
├── vercel.json          # Vercel 配置
└── package.json

# vercel.json 关键配置
{
  "builds": [
    { "src": "*.html", "use": "@vercel/static" },
    { "src": "api/*.js", "use": "@vercel/node" }
  ],
  "routes": [
    { "src": "/contact", "dest": "/api/contact" },
    { "src": "/sitemap.xml", "dest": "/api/sitemap" },
    { "src": "/(.*)", "dest": "/$1" }
  ],
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "X-Frame-Options", "value": "SAMEORIGIN" },
        { "key": "Strict-Transport-Security", "value": "max-age=31536000" }
      ]
    }
  ]
}

# 部署:连 GitHub 后自动部署,或用 CLI 手动部署
npm i -g vercel
vercel        # 部署到预览环境(生成临时域名)
vercel --prod # 部署到生产环境(绑定自定义域名)
// api/sitemap.js —— 动态生成站点地图
// 静态站点 + 动态 sitemap,两全其美
export default async function handler(req, res) {
  const baseUrl = 'https://ldpk.cn';
  // 从数据库拉所有文章 URL
  const [articles] = await db.query('SELECT slug, updated_at FROM articles WHERE status = 1');

  const urls = [
    { loc: '/', priority: '1.0', changefreq: 'daily' },
    { loc: '/about.html', priority: '0.8', changefreq: 'monthly' },
    { loc: '/cases.html', priority: '0.9', changefreq: 'weekly' },
    ...articles.map(a => ({
      loc: `/article/${a.slug}.html`,
      lastmod: a.updated_at.toISOString().split('T')[0],
      priority: '0.7',
      changefreq: 'monthly'
    }))
  ];

  // 拼成 XML
  const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls.map(u => `  <url>
    <loc>${baseUrl}${u.loc}</loc>
    ${u.lastmod ? `<lastmod>${u.lastmod}</lastmod>` : ''}
    <changefreq>${u.changefreq}</changefreq>
    <priority>${u.priority}</priority>
  </url>`).join('\n')}
</urlset>`;

  res.setHeader('Content-Type', 'application/xml');
  res.status(200).send(xml);
}

Vercel 会给每个部署生成一个预览域名,方便上线前验收。生产环境绑定自定义域名(如 ldpk.cn)后,平台自动配 SSL 证书、自动续期、自动启用全球 CDN。尧图用 Vercel 部署了十几个客户官网,从代码提交到上线全程自动化,运维工作量几乎为零。

三、边缘函数与全球加速

Vercel 的 Function 跑在单一区域(如香港),国内用户访问仍有延迟。Cloudflare Pages 的边缘函数(Workers)跑在全球 300+ 节点上,用户请求被路由到最近的节点处理,延迟降到 50ms 以内。对全球用户站点,边缘函数是更优选择。

// Cloudflare Pages 边缘函数(跑在全球节点)
// 文件:functions/api/contact.js(约定 functions/ 目录)
export async function onRequestPost({ request, env }) {
  const { name, phone, message } = await request.json();

  // 参数校验
  if (!name || !phone) {
    return new Response(JSON.stringify({ code: 400, msg: '参数缺失' }), {
      headers: { 'Content-Type': 'application/json' }
    });
  }

  // 防垃圾:用 Cloudflare KV(边缘键值存储)做限流
  const ip = request.headers.get('CF-Connecting-IP');
  const key = `rate:${ip}`;
  const count = parseInt(await env.RATE_LIMIT.get(key) || '0');
  if (count >= 5) {
    return new Response(JSON.stringify({ code: 429, msg: '提交过于频繁' }), {
      status: 429,
      headers: { 'Content-Type': 'application/json' }
    });
  }
  await env.RATE_LIMIT.put(key, count + 1, { expirationTtl: 3600 });

  // 写入 D1(Cloudflare 边缘 SQLite 数据库)
  await env.DB.prepare(
    'INSERT INTO contacts (name, phone, message, ip, created_at) VALUES (?, ?, ?, ?, ?)'
  ).bind(name, phone, message, ip, Date.now()).run();

  // 用边缘邮件服务发通知
  await fetch('https://api.mailchannels.net/tx/v1/send', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      personalizations: [{ to: [{ email: env.ADMIN_EMAIL }] }],
      from: { email: 'noreply@ldpk.cn', name: '尧图建站' },
      subject: '新咨询:' + name,
      content: [{ type: 'text/plain', value: `电话:${phone}\n留言:${message}` }]
    })
  });

  return new Response(JSON.stringify({ code: 0, msg: '提交成功' }), {
    headers: { 'Content-Type': 'application/json' }
  });
}

# wrangler.toml(Cloudflare 配置)
name = "ldpk-site"
compatibility_date = "2024-01-01"

[[d1_databases]]
binding = "DB"                  # 函数里用 env.DB
database_name = "ldpk-contacts"
database_id = "xxx"

[[kv_namespaces]]
binding = "RATE_LIMIT"          # 函数里用 env.RATE_LIMIT
id = "xxx"

[vars]
ADMIN_EMAIL = "service@ldpk.cn" # 非敏感变量可放这,敏感的用 secrets

边缘函数的"全球分布"带来一个特殊优势:天然抗 DDoS——攻击流量被分散到全球节点,单点不会被压垮。尧图有个客户做活动页,预计流量峰值大,用 Cloudflare 边缘函数部署,活动当天扛住 50 万 PV 零延迟零故障,成本不到 10 块(按调用次数计费)。这是传统服务器架构很难做到的。

Serverless 部署把"上线一个网站"的门槛降到了历史最低——不用买服务器、不用配环境、不用管证书、不用愁扩容,push 代码即上线。对轻量化站点来说,这是当前最优解。理解了 Serverless Function 的无状态特性、Vercel/Cloudflare 的部署流程、边缘函数的全球加速优势,你就能用最小成本把站点部署到全球。至此,"轻量化站点开发"系列完结:从 SSG 原理到 Markdown 转站、性能优化、无后端方案、Serverless 部署,一套完整的轻量化建站技能你已全部掌握。尧图的实践证明,这套方案能让建站成本降低 70%、上线速度提升 5 倍,是中小企业的最优选择。

返回教程列表