全栈应用部署 #

架构概述 #

前后端分离架构将前端和后端分别部署,通过 API 进行通信。

架构图 #

text
┌─────────────────────────────────────────────────────┐
│              全栈应用架构                            │
├─────────────────────────────────────────────────────┤
│                                                     │
│  用户浏览器                                         │
│     │                                               │
│     ▼                                               │
│  ┌─────────────────────────────────────────────┐   │
│  │           前端应用 (React/Vue)               │   │
│  │           myapp-frontend.herokuapp.com       │   │
│  └─────────────────────────────────────────────┘   │
│     │                                               │
│     │ API 请求                                      │
│     ▼                                               │
│  ┌─────────────────────────────────────────────┐   │
│  │           后端 API (Node.js/Python)          │   │
│  │           myapp-api.herokuapp.com            │   │
│  └─────────────────────────────────────────────┘   │
│     │                                               │
│     ├──────────────────┬──────────────────┐        │
│     │                  │                  │        │
│     ▼                  ▼                  ▼        │
│  ┌─────────┐     ┌─────────┐     ┌─────────┐      │
│  │PostgreSQL│    │ Redis   │     │ S3      │      │
│  └─────────┘     └─────────┘     └─────────┘      │
│                                                     │
└─────────────────────────────────────────────────────┘

后端 API 部署 #

项目结构 #

text
api/
├── src/
│   ├── index.js
│   ├── routes/
│   ├── middleware/
│   └── models/
├── package.json
├── Procfile
└── app.json

CORS 配置 #

javascript
// src/index.js
const express = require('express');
const cors = require('cors');

const app = express();

const allowedOrigins = [
  'https://myapp-frontend.herokuapp.com',
  'https://myapp.com',
  'http://localhost:3000'
];

app.use(cors({
  origin: function (origin, callback) {
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

// API 路由
app.get('/api/health', (req, res) => {
  res.json({ status: 'ok' });
});

module.exports = app;

JWT 认证 #

javascript
// src/middleware/auth.js
const jwt = require('jsonwebtoken');

function authMiddleware(req, res, next) {
  const authHeader = req.headers.authorization;
  
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'No token provided' });
  }
  
  const token = authHeader.split(' ')[1];
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (error) {
    return res.status(401).json({ error: 'Invalid token' });
  }
}

module.exports = authMiddleware;

部署后端 #

bash
# 创建 API 应用
heroku create myapp-api

# 添加数据库
heroku addons:create heroku-postgresql:mini --app myapp-api
heroku addons:create heroku-redis:mini --app myapp-api

# 设置环境变量
heroku config:set JWT_SECRET=$(openssl rand -hex 32) --app myapp-api
heroku config:set CORS_ORIGIN=https://myapp-frontend.herokuapp.com --app myapp-api

# 部署
cd api
git push heroku main

前端部署 #

React 项目 #

bash
# 创建 React 项目
npx create-react-app frontend
cd frontend

环境变量配置 #

javascript
// src/config.js
const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:3001';

export { API_URL };

API 调用 #

javascript
// src/api/index.js
import { API_URL } from '../config';

async function fetchAPI(endpoint, options = {}) {
  const token = localStorage.getItem('token');
  
  const headers = {
    'Content-Type': 'application/json',
    ...options.headers,
  };
  
  if (token) {
    headers['Authorization'] = `Bearer ${token}`;
  }
  
  const response = await fetch(`${API_URL}${endpoint}`, {
    ...options,
    headers,
  });
  
  if (!response.ok) {
    throw new Error('API request failed');
  }
  
  return response.json();
}

export const api = {
  get: (endpoint) => fetchAPI(endpoint),
  post: (endpoint, data) => fetchAPI(endpoint, {
    method: 'POST',
    body: JSON.stringify(data),
  }),
  put: (endpoint, data) => fetchAPI(endpoint, {
    method: 'PUT',
    body: JSON.stringify(data),
  }),
  delete: (endpoint) => fetchAPI(endpoint, {
    method: 'DELETE',
  }),
};

部署 React 应用 #

json
// package.json
{
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test"
  },
  "homepage": "."
}
bash
# 创建前端应用
heroku create myapp-frontend

# 设置环境变量
heroku config:set REACT_APP_API_URL=https://myapp-api.herokuapp.com --app myapp-frontend

# 部署
cd frontend
git push heroku main

使用静态站点部署 #

bash
# 使用 buildpack 部署静态站点
heroku buildpacks:set mars/create-react-app --app myapp-frontend

# 或使用 heroku-static-buildpack
# 创建 static.json
json
// static.json
{
  "root": "build/",
  "https_only": true,
  "error_page": "error.html",
  "routes": {
    "/**": "index.html"
  }
}

Vue 项目部署 #

项目配置 #

javascript
// vue.config.js
module.exports = {
  publicPath: process.env.NODE_ENV === 'production' ? '/' : '/',
  outputDir: 'dist',
  devServer: {
    proxy: {
      '/api': {
        target: process.env.VUE_APP_API_URL || 'http://localhost:3001',
        changeOrigin: true
      }
    }
  }
};

环境变量 #

text
# .env.production
VUE_APP_API_URL=https://myapp-api.herokuapp.com

部署 Vue 应用 #

bash
# 创建应用
heroku create myapp-frontend

# 设置 buildpack
heroku buildpacks:set heroku/nodejs --app myapp-frontend

# 部署
git push heroku main

统一域名部署 #

子域名配置 #

text
┌─────────────────────────────────────────────────────┐
│              统一域名部署                            │
├─────────────────────────────────────────────────────┤
│                                                     │
│  myapp.com                                          │
│     │                                               │
│     ├── www.myapp.com ──► 前端应用                  │
│     │                                               │
│     └── api.myapp.com ──► 后端 API                  │
│                                                     │
└─────────────────────────────────────────────────────┘

配置自定义域名 #

bash
# 前端域名
heroku domains:add www.myapp.com --app myapp-frontend

# API 域名
heroku domains:add api.myapp.com --app myapp-api

# DNS 配置
# www CNAME myapp-frontend.herokuapp.com
# api CNAME myapp-api.herokuapp.com

单应用部署(前后端同仓库) #

项目结构 #

text
myapp/
├── client/          # 前端代码
│   ├── src/
│   ├── public/
│   └── package.json
├── server/          # 后端代码
│   ├── src/
│   └── package.json
├── package.json
└── Procfile

根 package.json #

json
{
  "name": "myapp",
  "scripts": {
    "install": "npm install --prefix server && npm install --prefix client",
    "build": "npm run build --prefix client",
    "start": "npm start --prefix server",
    "heroku-postbuild": "npm run build"
  }
}

后端服务静态文件 #

javascript
// server/src/index.js
const express = require('express');
const path = require('path');

const app = express();

// API 路由
app.use('/api', require('./routes'));

// 服务静态文件
app.use(express.static(path.join(__dirname, '../../client/build')));

app.get('*', (req, res) => {
  res.sendFile(path.join(__dirname, '../../client/build/index.html'));
});

module.exports = app;

Procfile #

text
web: npm start
release: npm run db:migrate

认证流程 #

前端登录 #

javascript
// src/components/Login.js
import { useState } from 'react';
import { api } from '../api';

function Login() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    
    try {
      const { token, user } = await api.post('/api/auth/login', {
        email,
        password
      });
      
      localStorage.setItem('token', token);
      // 更新用户状态
    } catch (error) {
      console.error('Login failed:', error);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <button type="submit">Login</button>
    </form>
  );
}

后端认证 #

javascript
// server/src/routes/auth.js
const router = require('express').Router();
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');

router.post('/login', async (req, res) => {
  const { email, password } = req.body;
  
  const user = await db.query('SELECT * FROM users WHERE email = $1', [email]);
  
  if (!user || !(await bcrypt.compare(password, user.password_hash))) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  
  const token = jwt.sign(
    { id: user.id, email: user.email },
    process.env.JWT_SECRET,
    { expiresIn: '7d' }
  );
  
  res.json({ token, user: { id: user.id, email: user.email } });
});

module.exports = router;

最佳实践 #

1. 环境变量管理 #

bash
# 前端环境变量
REACT_APP_API_URL=https://api.myapp.com

# 后端环境变量
DATABASE_URL=postgres://...
REDIS_URL=redis://...
JWT_SECRET=xxx
CORS_ORIGIN=https://myapp.com

2. 构建优化 #

json
// package.json
{
  "scripts": {
    "build": "NODE_ENV=production npm run build:client && npm run build:server",
    "build:client": "cd client && npm run build",
    "build:server": "cd server && npm run build"
  }
}

3. 监控配置 #

bash
# 为前端和后端分别添加监控
heroku addons:create newrelic:wayne --app myapp-frontend
heroku addons:create newrelic:wayne --app myapp-api
heroku addons:create papertrail:choklad --app myapp-frontend
heroku addons:create papertrail:choklad --app myapp-api

下一步 #

全栈应用部署完成后,接下来学习 生产环境最佳实践 了解生产环境优化!

最后更新:2026-03-28