首页 关于 文章 作品集 工具 联系
返回文章列表

Flutter + Node.js 全栈实战:从零搭建 RESTful API 后端服务

在全栈开发中,Flutter 负责构建精美的跨平台客户端界面,而 Node.js 则是搭建高性能后端 API 服务的利器。这篇文章记录了我在项目中从零搭建 Node.js 后端并对接 Flutter 客户端的完整过程,涵盖项目结构设计、数据库连接、中间件配置、鉴权机制以及 Flutter 端的 HTTP 请求封装。

技术栈选型

在开始之前,先明确这次实战使用的技术栈。后端采用 Node.js + Express + MongoDB,这是目前最成熟的 JavaScript 全栈组合之一。Express 提供了轻量而灵活的路由框架,MongoDB 作为 NoSQL 数据库在处理非结构化数据时非常方便。鉴权方面使用 JWT (JSON Web Token),密码加密使用 bcryptjs

Flutter 端使用 dio 作为 HTTP 客户端,它在拦截器、请求取消、FormData 支持等方面远超 Dart 原生的 http 包。

为什么选 Node.js?因为 Flutter 开发者通常已经熟悉 JavaScript/Dart,Node.js 可以让前后端语言统一,降低学习成本。而且 npm 生态极其丰富,几乎所有你需要的中间件都能找到现成的包。

一、后端项目初始化

首先创建项目目录并初始化:

mkdir my-flutter-backend
cd my-flutter-backend
npm init -y

# 安装核心依赖
npm install express mongoose dotenv cors jsonwebtoken bcryptjs

# 安装开发依赖
npm install --save-dev nodemon

安装完成后,项目目录结构如下:

my-flutter-backend/
├── src/
│   ├── config/
│   │   └── db.js          # 数据库连接
│   ├── middleware/
│   │   └── auth.js        # JWT 鉴权中间件
│   ├── models/
│   │   └── User.js        # 用户模型
│   ├── routes/
│   │   ├── auth.js        # 认证路由
│   │   └── posts.js       # 文章路由
│   ├── controllers/
│   │   ├── authController.js
│   │   └── postController.js
│   ├── utils/
│   │   └── response.js    # 统一响应格式
│   └── app.js             # Express 应用入口
├── .env
├── server.js              # 启动入口
└── package.json

二、数据库连接配置

.env 文件中配置环境变量:

PORT=3000
MONGODB_URI=mongodb://localhost:27017/flutter_app
JWT_SECRET=your_super_secret_key_here
JWT_EXPIRES_IN=7d

数据库连接文件 src/config/db.js

const mongoose = require('mongoose');

const connectDB = async () => {
  try {
    const conn = await mongoose.connect(process.env.MONGODB_URI);
    
  } catch (error) {
    console.error(`数据库连接失败: ${error.message}`);
    process.exit(1);
  }
};

module.exports = connectDB;

三、用户模型设计

使用 Mongoose 定义用户 Schema,包含邮箱、密码(加密存储)、昵称、头像等字段:

const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const userSchema = new mongoose.Schema({
  username: {
    type: String,
    required: [true, '请输入用户名'],
    unique: true,
    trim: true,
    minlength: [2, '用户名至少2个字符'],
    maxlength: [20, '用户名最多20个字符']
  },
  email: {
    type: String,
    required: [true, '请输入邮箱'],
    unique: true,
    lowercase: true,
    match: [/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/, '邮箱格式不正确']
  },
  password: {
    type: String,
    required: [true, '请输入密码'],
    minlength: 6,
    select: false  // 默认查询时不返回密码
  },
  avatar: {
    type: String,
    default: ''
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
});

// 保存前自动加密密码
userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next();
  const salt = await bcrypt.genSalt(10);
  this.password = await bcrypt.hash(this.password, salt);
  next();
});

// 实例方法:验证密码
userSchema.methods.matchPassword = async function(enteredPassword) {
  return await bcrypt.compare(enteredPassword, this.password);
};

module.exports = mongoose.model('User', userSchema);

四、JWT 鉴权中间件

鉴权中间件负责从请求头中提取 Token 并验证其有效性,将解码后的用户信息挂载到 req.user 上供后续路由使用:

const jwt = require('jsonwebtoken');
const User = require('../models/User');

const protect = async (req, res, next) => {
  let token;

  // 从 Header 中获取 Bearer Token
  if (req.headers.authorization &&
      req.headers.authorization.startsWith('Bearer')) {
    try {
      token = req.headers.authorization.split(' ')[1];
      const decoded = jwt.verify(token, process.env.JWT_SECRET);
      req.user = await User.findById(decoded.id).select('-password');
      next();
    } catch (error) {
      return res.status(401).json({
        success: false,
        message: 'Token 无效或已过期'
      });
    }
  }

  if (!token) {
    return res.status(401).json({
      success: false,
      message: '未提供认证 Token'
    });
  }
};

module.exports = protect;

五、统一响应格式

为了让 Flutter 端更容易处理响应,后端统一返回格式化的 JSON 数据:

const successResponse = (res, data, statusCode = 200) => {
  res.status(statusCode).json({
    success: true,
    data,
    timestamp: new Date().toISOString()
  });
};

const errorResponse = (res, message, statusCode = 500) => {
  res.status(statusCode).json({
    success: false,
    message,
    timestamp: new Date().toISOString()
  });
};

module.exports = { successResponse, errorResponse };

六、Express 应用入口

将所有组件组装到一起,注册路由和中间件:

const express = require('express');
const cors = require('cors');
const morgan = require('morgan');

const authRoutes = require('./routes/auth');
const postRoutes = require('./routes/posts');

const app = express();

// 中间件
app.use(cors({
  origin: process.env.CLIENT_URL || '*',
  credentials: true
}));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
app.use(morgan('dev'));

// 路由
app.use('/api/v1/auth', authRoutes);
app.use('/api/v1/posts', postRoutes);

// 健康检查
app.get('/api/health', (req, res) => {
  res.json({ status: 'ok', time: new Date().toISOString() });
});

// 404 处理
app.use((req, res) => {
  res.status(404).json({ success: false, message: '接口不存在' });
});

// 全局错误处理
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({
    success: false,
    message: '服务器内部错误'
  });
});

module.exports = app;

七、Flutter 端对接

Flutter 端使用 dio 封装 HTTP 请求。首先定义一个统一的 API 客户端,支持自动注入 Token、统一错误处理:

import 'package:dio/dio.dart';

class ApiClient {
  static final ApiClient _instance = ApiClient._internal();
  factory ApiClient() => _instance;

  late final Dio _dio;

  ApiClient._internal() {
    _dio = Dio(BaseOptions(
      baseUrl: 'http://10.0.2.2:3000/api/v1', // Android 模拟器
      connectTimeout: Duration(seconds: 10),
      receiveTimeout: Duration(seconds: 10),
      headers: {'Content-Type': 'application/json'},
    ));

    // 请求拦截器:自动注入 Token
    _dio.interceptors.add(InterceptorsWrapper(
      onRequest: (options, handler) async {
        final token = await _getToken();
        if (token != null) {
          options.headers['Authorization'] = 'Bearer $token';
        }
        return handler.next(options);
      },
      onError: (error, handler) {
        // 401 自动跳转登录
        if (error.response?.statusCode == 401) {
          // 清除本地 Token 并跳转登录页
        }
        return handler.next(error);
      },
    ));
  }

  Future<String?> _getToken() async {
    // 从 SharedPreferences 或 secure_storage 读取
    return null;
  }

  // GET 请求
  Future<Response> get(String path, {Map<String, dynamic>? params}) {
    return _dio.get(path, queryParameters: params);
  }

  // POST 请求
  Future<Response> post(String path, {dynamic data}) {
    return _dio.post(path, data: data);
  }

  // PUT 请求
  Future<Response> put(String path, {dynamic data}) {
    return _dio.put(path, data: data);
  }

  // DELETE 请求
  Future<Response> delete(String path) {
    return _dio.delete(path);
  }
}

在 Flutter 端调用登录接口的示例:

Future<bool> login(String email, String password) async {
  try {
    final response = await ApiClient().post('/auth/login', data: {
      'email': email,
      'password': password,
    });

    if (response.data['success']) {
      final token = response.data['data']['token'];
      // 保存 Token 到本地安全存储
      await _saveToken(token);
      return true;
    }
    return false;
  } on DioException catch (e) {
    // 统一错误处理
    final message = e.response?.data['message'] ?? '网络异常';
    showToast(message);
    return false;
  }
}

总结

这篇文章覆盖了 Flutter + Node.js 全栈项目后端部分的核心内容:项目结构设计、数据库连接、Mongoose 模型定义、JWT 鉴权中间件、统一响应格式,以及 Flutter 端的 Dio HTTP 客户端封装。整个后端代码不到 300 行,却足以支撑一个完整的移动应用后端服务。

接下来的文章中,我会继续分享文件上传接口的设计、分页查询优化、以及 WebSocket 实时通信的实现。敬请期待。