整体流程概览

  1. 后端(Spring Boot):编写API接口,处理业务逻辑,操作数据库,并返回JSON数据。
  2. 前端(uni-app):在页面中使用uni.request发起HTTP请求,调用后端API。
  3. 数据交互:前后端通过JSON格式进行数据通信。
  4. 跨域问题:解决开发过程中前端与后端不同端口访问的问题。

第一部分:Spring Boot 后端开发

步骤 1:创建项目与基础配置
  1. 使用 Spring Initializr 创建一个新的Spring Boot项目。
    • Project: Maven
    • Language: Java
    • Spring Boot: 选择最新稳定版
    • Dependencies: 至少添加 Spring WebLombok(简化代码)。

配置端口(可选,默认8080)。
src/main/resources/application.properties 中:

  1. server.port=8080
步骤 2:创建数据模型(Model)

创建一个简单的用户类。

src/main/java/com/hz/xxx/pojo/User.java

import lombok.Data;

@Data // Lombok 注解,自动生成 getter, setter, toString 等
public class User {
    private Long id;
    private String username;
    private String email;

        // 无参构造函数
    public User() {
    }

    // 有参构造函数
    public User(Long id, String username, String email) {
        this.id = id;
        this.username = username;
        this.email = email;
    }
}
步骤 3:创建控制器(Controller)

这是提供API接口的核心部分。

src/main/java/com/hz/xxx/controller/UserController.java

package org.example.springaa.controller;

import org.example.springaa.model.User;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

@RestController // 表明这是一个RESTful控制器,返回数据而非视图
@RequestMapping("/api/user") // 定义API的基础路径
@CrossOrigin(origins = "*") // 允许所有来源的跨域请求,解决开发时跨域问题
public class UserController {

    // 模拟一个内存数据库,用于演示
    private List<User> userList = new ArrayList<>();
    private Long nextId = 1L;

    // 构造函数,初始化一些测试数据
    public UserController() {
        userList.add(new User(nextId++, "张三", "zhangsan@example.com"));
        userList.add(new User(nextId++, "李四", "lisi@example.com")); // 修正了邮箱地址
    }

    // 1. 获取所有用户 - GET /api/user
    @GetMapping
    public List<User> getAllUsers() {
        return userList;
    }

    // 2. 根据ID获取用户 - GET /api/user/{id}
    @GetMapping("/{id}")
    public User getUserById(@PathVariable Long id) {
        return userList.stream()
                .filter(user -> user.getId().equals(id))
                .findFirst()
                .orElse(null); // 找不到返回null
    }

    // 3. 创建新用户 - POST /api/user
    @PostMapping
    public User createUser(@RequestBody User user) { // @RequestBody 将请求体的JSON绑定到User对象
        user.setId(nextId++);
        userList.add(user);
        return user; // 返回创建成功的用户信息(包含新生成的ID)
    }

    // 4. 更新用户 - PUT /api/user/{id}
    @PutMapping("/{id}")
    public User updateUser(@PathVariable Long id, @RequestBody User userDetails) {
        User existingUser = getUserById(id);
        if (existingUser != null) {
            existingUser.setUsername(userDetails.getUsername());
            existingUser.setEmail(userDetails.getEmail());
            return existingUser;
        }
        return null; // 用户不存在
    }

    // 5. 删除用户 - DELETE /api/user/{id}
    @DeleteMapping("/{id}")
    public String deleteUser(@PathVariable Long id) {
        User userToRemove = getUserById(id);
        if (userToRemove != null) {
            userList.remove(userToRemove);
            return "用户删除成功";
        }
        return "用户不存在";
    }
}

关键注解解释

  • @RestController:组合了@Controller@ResponseBody,直接返回对象数据,而不是视图名称。
  • @RequestMapping:映射HTTP请求到处理器方法。
  • @GetMapping, @PostMapping, @PutMapping, @DeleteMapping:分别映射GET、POST、PUT、DELETE请求。
  • @PathVariable:将URL中的模板变量绑定到方法参数。
  • @RequestBody:将HTTP请求体(JSON)绑定到方法参数对象。
  • @CrossOrigin:处理跨域请求。生产环境建议指定具体的前端域名,而不是"*"
步骤 4:运行与测试

运行 xxxApplication.java 中的main方法。后端服务将在 http://localhost:8080 启动。

可以使用 PostmanApifox 等工具测试API是否正常工作。

  • GET http://localhost:8080/api/user
  • POST http://localhost:8080/api/user (Body: raw, JSON)
{
    "username": "王五",
    "email": "wangwu@example.com"
}

第二部分:uni-app 前端开发

步骤 1:发起网络请求 (uni.request)

uni-app 提供了 uni.request 方法用于发起HTTP请求。它是对原生XMLHttpRequest的封装,用法类似微信小程序的wx.request

基本语法

uni.request({
    url: 'http://your-api-url',
    method: 'GET', // 或 POST, PUT, DELETE
    data: { ... }, // 请求参数(GET请求是Query,POST请求是Body)
    header: {
        'content-type': 'application/json' // 默认值,根据后端要求设置
    },
    success: (res) => {
        // 请求成功回调,res.data 是服务器返回的数据
        console.log(res.data);
    },
    fail: (err) => {
        // 请求失败回调
        console.error(err);
    },
    complete: () => {
        // 请求完成回调(无论成功失败都会执行)
    }
});
步骤 2:编写前端页面调用接口

假设我们在 pages/index/index.vue 中调用后端接口。

pages/index/index.vue

<template>
    <view class="container">
        <!-- 操作按钮区域 -->
        <view class="action-section">
            <button class="btn-primary" @click="toggleUserList">
                {{ showUserList ? '隐藏用户列表' : '获取用户列表' }}
            </button>
            <button class="btn-secondary" @click="toggleCreateForm">
                {{ showCreateForm ? '取消创建' : '创建新用户' }}
            </button>
        </view>

        <!-- 用户列表区域 -->
        <view v-if="showUserList" class="user-list-section">
            <view class="section-header">
                <text class="section-title">用户列表 ({{ userList.length }})</text>
                <button class="btn-refresh" @click="getUsers" size="mini">
                    <text class="refresh-text">刷新</text>
                </button>
            </view>
            <view class="user-list">
                <view v-for="user in userList" :key="user.id" class="user-card">
                    <view class="user-info">
                        <text class="user-name">{{ user.username }}</text>
                        <text class="user-email">{{ user.email }}</text>
                    </view>
                    <button class="btn-delete" @click="deleteUser(user.id)" size="mini">删除</button>
                </view>
                <view v-if="userList.length === 0" class="empty-state">
                    <text class="empty-text">暂无用户数据</text>
                    <button class="btn-refresh" @click="getUsers">重新加载</button>
                </view>
            </view>
        </view>

        <!-- 新增用户表单区域 -->
        <view v-if="showCreateForm" class="form-section">
            <view class="section-header">
                <text class="section-title">添加新用户</text>
                <button class="btn-close" @click="closeCreateForm" size="mini">
                    <text class="close-text">关闭</text>
                </button>
            </view>
            <view class="form-card">
                <view class="input-group">
                    <text class="input-label">用户名</text>
                    <input 
                        v-model="newUser.username" 
                        placeholder="请输入用户名" 
                        class="input-field"
                        placeholder-class="placeholder"
                        focus
                    />
                </view>
                
                <view class="input-group">
                    <text class="input-label">邮箱地址</text>
                    <input 
                        v-model="newUser.email" 
                        placeholder="请输入邮箱地址" 
                        class="input-field"
                        placeholder-class="placeholder"
                        type="email"
                    />
                </view>
                
                <view class="form-actions">
                    <button class="btn-cancel" @click="closeCreateForm">取消</button>
                    <button class="btn-submit" @click="submitUser" :disabled="!isFormValid">
                        {{ isSubmitting ? '提交中...' : '确认添加' }}
                    </button>
                </view>
            </view>
        </view>

        <!-- 默认提示 -->
        <view v-if="!showUserList && !showCreateForm" class="welcome-section">
            <text class="welcome-title">用户管理系统</text>
            <text class="welcome-subtitle">请选择上方操作开始使用</text>
            <view class="feature-cards">
                <view class="feature-card">
                    <text class="feature-icon">👥</text>
                    <text class="feature-title">查看用户</text>
                    <text class="feature-desc">点击"获取用户列表"查看所有用户信息</text>
                </view>
                <view class="feature-card">
                    <text class="feature-icon">➕</text>
                    <text class="feature-title">添加用户</text>
                    <text class="feature-desc">点击"创建新用户"添加新的用户账号</text>
                </view>
            </view>
        </view>
    </view>
</template>

<script>
    const BASE_URL = 'http://localhost:8080/api/user';

    export default {
        data() {
            return {
                userList: [],
                newUser: {
                    username: '',
                    email: ''
                },
                isSubmitting: false,
                showUserList: false,
                showCreateForm: false
            }
        },
        computed: {
            isFormValid() {
                return this.newUser.username.trim() && 
                       this.newUser.email.trim() && 
                       !this.isSubmitting;
            }
        },
        methods: {
            // 切换用户列表显示
            toggleUserList() {
                this.showCreateForm = false;
                this.showUserList = !this.showUserList;
                if (this.showUserList) {
                    this.getUsers();
                }
            },

            // 切换创建表单显示
            toggleCreateForm() {
                this.showUserList = false;
                this.showCreateForm = !this.showCreateForm;
                if (!this.showCreateForm) {
                    this.resetForm();
                }
            },

            // 关闭创建表单
            closeCreateForm() {
                this.showCreateForm = false;
                this.resetForm();
            },

            // 重置表单
            resetForm() {
                this.newUser = { username: '', email: '' };
                this.isSubmitting = false;
            },

            async getUsers() {
                try {
                    uni.showLoading({
                        title: '加载中...',
                        mask: true
                    });
                    
                    const result = await new Promise((resolve, reject) => {
                        uni.request({
                            url: BASE_URL,
                            method: 'GET',
                            success: (res) => resolve(res),
                            fail: (err) => reject(err)
                        });
                    });
                    
                    uni.hideLoading();
                    
                    if (result.statusCode === 200) {
                        this.userList = result.data;
                        uni.showToast({
                            title: `已加载 ${this.userList.length} 个用户`,
                            icon: 'success'
                        });
                    } else {
                        uni.showToast({
                            title: `获取失败: ${result.statusCode}`,
                            icon: 'none'
                        });
                    }
                    
                } catch (error) {
                    uni.hideLoading();
                    console.error('请求异常:', error);
                    uni.showToast({
                        title: '网络异常',
                        icon: 'none'
                    });
                }
            },

            async createUser() {
                if (!this.isFormValid) return;
                
                this.isSubmitting = true;
                
                try {
                    const result = await new Promise((resolve, reject) => {
                        uni.request({
                            url: BASE_URL,
                            method: 'POST',
                            data: {
                                username: this.newUser.username,
                                email: this.newUser.email
                            },
                            header: {
                                'Content-Type': 'application/json'
                            },
                            success: (res) => resolve(res),
                            fail: (err) => reject(err)
                        });
                    });
                    
                    if (result.statusCode === 200) {
                        uni.showToast({
                            title: '用户创建成功',
                            icon: 'success'
                        });
                        this.resetForm();
                        this.showCreateForm = false;
                    } else {
                        uni.showToast({
                            title: '创建失败',
                            icon: 'none'
                        });
                    }
                } catch (error) {
                    console.error('创建用户异常:', error);
                    uni.showToast({
                        title: '创建失败',
                        icon: 'none'
                    });
                } finally {
                    this.isSubmitting = false;
                }
            },

            async deleteUser(userId) {
                uni.showModal({
                    title: '确认删除',
                    content: '确定要删除这个用户吗?此操作不可撤销。',
                    confirmColor: '#FF3B30',
                    success: async (res) => {
                        if (res.confirm) {
                            try {
                                const result = await new Promise((resolve, reject) => {
                                    uni.request({
                                        url: `${BASE_URL}/${userId}`,
                                        method: 'DELETE',
                                        success: (res) => resolve(res),
                                        fail: (err) => reject(err)
                                    });
                                });
                                
                                if (result.statusCode === 200) {
                                    uni.showToast({
                                        title: '删除成功',
                                        icon: 'success'
                                    });
                                    this.getUsers();
                                }
                            } catch (error) {
                                console.error('删除用户异常:', error);
                                uni.showToast({
                                    title: '删除失败',
                                    icon: 'none'
                                });
                            }
                        }
                    }
                });
            },

            submitUser() {
                this.createUser();
            }
        }
    }
</script>

<style>
    .container {
        padding: 30rpx;
        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        min-height: 100vh;
    }
    
    .action-section {
        display: flex;
        gap: 20rpx;
        margin-bottom: 40rpx;
    }
    
    .btn-primary {
        flex: 1;
        background: linear-gradient(45deg, #007AFF, #5856D6);
        color: white;
        border: none;
        border-radius: 16rpx;
        padding: 24rpx;
        font-size: 32rpx;
        font-weight: 600;
        box-shadow: 0 8rpx 24rpx rgba(0, 122, 255, 0.3);
    }
    
    .btn-secondary {
        flex: 1;
        background: linear-gradient(45deg, #34C759, #30B04C);
        color: white;
        border: none;
        border-radius: 16rpx;
        padding: 24rpx;
        font-size: 32rpx;
        font-weight: 600;
        box-shadow: 0 8rpx 24rpx rgba(52, 199, 89, 0.3);
    }
    
    .section-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 30rpx;
    }
    
    .section-title {
        font-size: 36rpx;
        font-weight: 700;
        color: #1C1C1E;
    }
    
    .btn-refresh, .btn-close {
        background: #8E8E93;
        color: white;
        border: none;
        border-radius: 12rpx;
        padding: 16rpx 24rpx;
        font-size: 24rpx;
    }
    
    .refresh-text, .close-text {
        color: white;
    }
    
    .user-list-section, .form-section {
        background: white;
        border-radius: 24rpx;
        padding: 40rpx 30rpx;
        margin-bottom: 40rpx;
        box-shadow: 0 12rpx 48rpx rgba(0, 0, 0, 0.1);
        animation: slideDown 0.3s ease;
    }
    
    @keyframes slideDown {
        from {
            opacity: 0;
            transform: translateY(-20rpx);
        }
        to {
            opacity: 1;
            transform: translateY(0);
        }
    }
    
    .user-list {
        max-height: 600rpx;
        overflow-y: auto;
    }
    
    .user-card {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 30rpx;
        background: #F8F9FA;
        border-radius: 16rpx;
        margin-bottom: 20rpx;
        border-left: 8rpx solid #007AFF;
    }
    
    .user-info {
        flex: 1;
    }
    
    .user-name {
        display: block;
        font-size: 32rpx;
        font-weight: 600;
        color: #1C1C1E;
        margin-bottom: 8rpx;
    }
    
    .user-email {
        display: block;
        font-size: 28rpx;
        color: #8E8E93;
    }
    
    .btn-delete {
        background: linear-gradient(45deg, #FF3B30, #FF2D55);
        color: white;
        border: none;
        border-radius: 12rpx;
        padding: 16rpx 24rpx;
        font-size: 24rpx;
        font-weight: 500;
        min-width: 120rpx;
    }
    
    .empty-state {
        text-align: center;
        padding: 80rpx 0;
    }
    
    .empty-text {
        font-size: 32rpx;
        color: #8E8E93;
        display: block;
        margin-bottom: 30rpx;
    }
    
    .form-card {
        background: #F8F9FA;
        border-radius: 20rpx;
        padding: 40rpx 30rpx;
    }
    
    .input-group {
        margin-bottom: 40rpx;
    }
    
    .input-label {
        display: block;
        font-size: 30rpx;
        font-weight: 600;
        color: #1C1C1E;
        margin-bottom: 20rpx;
    }
    
    .input-field {
        background: white;
        border: 2rpx solid #E5E5EA;
        border-radius: 16rpx;
        padding: 28rpx 24rpx;
        font-size: 32rpx;
        color: #1C1C1E;
        transition: all 0.3s ease;
        box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
    }
    
    .input-field:focus {
        border-color: #007AFF;
        box-shadow: 0 4rpx 20rpx rgba(0, 122, 255, 0.2);
        transform: translateY(-2rpx);
    }
    
    .placeholder {
        color: #C7C7CC;
        font-size: 32rpx;
    }
    
    .form-actions {
        display: flex;
        gap: 20rpx;
        margin-top: 40rpx;
    }
    
    .btn-cancel {
        flex: 1;
        background: #8E8E93;
        color: white;
        border: none;
        border-radius: 16rpx;
        padding: 28rpx;
        font-size: 34rpx;
        font-weight: 600;
    }
    
    .btn-submit {
        flex: 2;
        background: linear-gradient(45deg, #007AFF, #5856D6);
        color: white;
        border: none;
        border-radius: 16rpx;
        padding: 28rpx;
        font-size: 34rpx;
        font-weight: 600;
        box-shadow: 0 8rpx 24rpx rgba(0, 122, 255, 0.3);
    }
    
    .btn-submit:disabled {
        background: linear-gradient(45deg, #C7C7CC, #AEAEB2);
        box-shadow: none;
    }
    
    .welcome-section {
        text-align: center;
        padding: 100rpx 0;
    }
    
    .welcome-title {
        display: block;
        font-size: 48rpx;
        font-weight: 700;
        color: white;
        margin-bottom: 20rpx;
    }
    
    .welcome-subtitle {
        display: block;
        font-size: 32rpx;
        color: rgba(255, 255, 255, 0.8);
        margin-bottom: 60rpx;
    }
    
    .feature-cards {
        display: flex;
        flex-direction: column;
        gap: 30rpx;
    }
    
    .feature-card {
        background: rgba(255, 255, 255, 0.1);
        backdrop-filter: blur(10px);
        border-radius: 20rpx;
        padding: 40rpx 30rpx;
        border: 1rpx solid rgba(255, 255, 255, 0.2);
    }
    
    .feature-icon {
        display: block;
        font-size: 60rpx;
        margin-bottom: 20rpx;
    }
    
    .feature-title {
        display: block;
        font-size: 36rpx;
        font-weight: 600;
        color: white;
        margin-bottom: 10rpx;
    }
    
    .feature-desc {
        display: block;
        font-size: 28rpx;
        color: rgba(255, 255, 255, 0.8);
    }
    
    /* 滚动条样式 */
    ::-webkit-scrollbar {
        width: 8rpx;
    }
    
    ::-webkit-scrollbar-track {
        background: #F1F1F1;
        border-radius: 8rpx;
    }
    
    ::-webkit-scrollbar-thumb {
        background: #C1C1C1;
        border-radius: 8rpx;
    }
</style>

第三部分:关键问题与优化

跨域问题(CORS)
  • 问题:当uni-app运行在浏览器(H5)或小程序开发者工具上时,其地址(如http://localhost:9000)与后端Spring Boot(http://localhost:8080)端口不同,浏览器会因同源策略阻止请求。
  • 解决方案
    • 后端解决(推荐):使用Spring Boot的@CrossOrigin注解(如上文代码所示)或配置一个全局的CORS过滤器。

全局CORS配置(替代@CrossOrigin):

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**") // 针对所有/api/开头的路径
                .allowedOrigins("*") // 允许所有来源,生产环境应指定具体域名
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*");
    }
}

前端代理(仅开发环境):在 manifest.json 的 H5 配置中设置代理,可以避免浏览器跨域。

"h5": {
    "devServer": {
        "proxy": {
            "/api": {
                "target": "http://localhost:8080",
                "changeOrigin": true,
                "pathRewrite": {
                    "^/api": "/api"
                }
            }
        }
    }
}

这样,前端请求 /api/user 会被代理到 http://localhost:8080/api/user

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐