HarmonyOS_ArkTs_API(5)

发布于:2025-05-30 ⋅ 阅读:(21) ⋅ 点赞:(0)

HarmonyOS_ArkTs_API(5)

概述

客户端服务端集成架构是此应用的核心技术框架,连接鸿蒙HarmonyOS ArkTS客户端与Spring Boot系统。该架构采用了现代化的分层设计和松耦合原则,确保前后端高效协作,同时保持各自的独立性和可维护性。本文档详细介绍了前后端通信机制、数据传输格式、错误处理策略和性能优化方案。

架构设计

整体架构

梦想生活规划师应用采用经典的客户端-服务器架构,但针对鸿蒙生态特点进行了优化:

┌─────────────────────┐      ┌──────────────────────────────┐
│  鸿蒙前端(HarmonyOS) │◄────►│         网关层(Gateway)       │
└─────────────────────┘      └──────────────────┬───────────┘
                                                │
                                                ▼
                             ┌──────────────────────────────┐
                             │      服务层(Service Layer)    │
                             └──────────────────┬───────────┘
                                                │
                                                ▼
                             ┌──────────────────────────────┐
                             │      数据层(Data Layer)       │
                             └──────────────────────────────┘

通信协议

前后端通信基于标准HTTP协议,采用RESTful API设计原则:

  • 请求方法:使用GET、POST、PUT、DELETE等语义化方法
  • 资源路径:采用名词复数形式,如/dreams, /tasks
  • 参数传递:查询参数、路径参数和请求体配合使用
  • 状态码:遵循标准HTTP状态码语义
  • 内容协商:默认使用application/json格式

客户端实现

HTTP请求库

前端使用自定义封装的HTTP工具类实现与服务器通信:

// HttpUtil.ets
import http from '@ohos.net.http';
import { AppConfig } from '../config/AppConfig';

// HTTP请求方法枚举
export enum RequestMethod {
  GET = 'GET',
  POST = 'POST',
  PUT = 'PUT',
  DELETE = 'DELETE'
}

// 请求参数类型定义
export type RequestData = Record<string, any>;
export type QueryParams = Record<string, string | number | boolean>;
export type RequestHeaders = Record<string, string>;

/**
 * 通用HTTP请求函数
 * @param method 请求方法
 * @param url 请求路径(相对路径)
 * @param data 请求数据
 * @param params 查询参数
 * @param headers 请求头
 * @returns 请求响应
 */
export async function request<T>(
  method: RequestMethod,
  url: string,
  data?: RequestData,
  params?: QueryParams,
  headers?: RequestHeaders
): Promise<T> {
  try {
    // 构建完整URL
    let fullUrl = `${AppConfig.BASE_API_URL}${url}`;
    
    // 添加查询参数
    if (params && Object.keys(params).length > 0) {
      const searchParams = new URLSearchParams();
      for (const key in params) {
        searchParams.append(key, params[key].toString());
      }
      fullUrl += `?${searchParams.toString()}`;
    }
    
    console.info(`发起 ${method} 请求: ${fullUrl}`);
    
    // 创建HTTP请求对象
    const httpRequest = http.createHttp();
    
    // 配置请求选项
    const options: http.HttpRequestOptions = {
      method: method,
      readTimeout: 60000, // 60秒超时
      header: {
        'Content-Type': 'application/json',
        ...headers
      },
      connectTimeout: 60000,
      extraData: data ? JSON.stringify(data) : undefined
    };
    
    // 发送请求
    const response = await httpRequest.request(fullUrl, options);
    
    // 请求完成后释放资源
    httpRequest.destroy();
    
    // 检查HTTP状态码
    if (response.responseCode >= 200 && response.responseCode < 300) {
      // 解析响应数据
      const result = response.result;
      if (typeof result === 'string') {
        return JSON.parse(result) as T;
      }
      return result as T;
    } else {
      // 处理HTTP错误
      const error = new Error(`HTTP错误: ${response.responseCode}`);
      error['code'] = response.responseCode;
      error['response'] = response;
      throw error;
    }
  } catch (error) {
    console.error(`请求失败: ${error instanceof Error ? error.message : String(error)}`);
    throw error;
  }
}

API服务层

前端API服务层封装了与后端的所有交互:

// ApiService.ets
import { request, RequestMethod, QueryParams } from '../utils/HttpUtil';
import { Dream, Task, User, Post, Comment, PageResponse } from '../model/CommonTypes';

/**
 * 用户认证相关API
 */
export async function login(username: string, password: string): Promise<User> {
  try {
    const response = await request<any>(
      RequestMethod.POST,
      '/auth/login',
      { username, password }
    );
    
    if (response.token) {
      // 存储认证令牌
      await saveAuthToken(response.token);
    }
    
    return response.user;
  } catch (error) {
    console.error(`登录失败: ${error instanceof Error ? error.message : String(error)}`);
    throw new Error(`登录失败: ${error instanceof Error ? error.message : String(error)}`);
  }
}

/**
 * 梦想相关API
 */
export async function createDream(dream: Dream): Promise<Dream> {
  try {
    return request<Dream>(RequestMethod.POST, '/dreams', dream);
  } catch (error) {
    console.error(`创建梦想失败: ${error instanceof Error ? error.message : String(error)}`);
    throw new Error(`创建梦想失败: ${error instanceof Error ? error.message : String(error)}`);
  }
}

export async function getDreamsByUserId(userId: number): Promise<Dream[]> {
  try {
    return request<Dream[]>(RequestMethod.GET, `/dreams/user/${userId}`);
  } catch (error) {
    console.error(`获取用户梦想失败: ${error instanceof Error ? error.message : String(error)}`);
    throw new Error(`获取用户梦想失败: ${error instanceof Error ? error.message : String(error)}`);
  }
}

/**
 * 任务相关API
 */
export async function getTasksByDreamId(dreamId: number): Promise<Task[]> {
  try {
    return request<Task[]>(RequestMethod.GET, `/tasks/dream/${dreamId}`);
  } catch (error) {
    console.error(`获取梦想任务失败: ${error instanceof Error ? error.message : String(error)}`);
    throw new Error(`获取梦想任务失败: ${error instanceof Error ? error.message : String(error)}`);
  }
}

/**
 * 社区相关API
 */
export async function getPosts(page: number = 0, size: number = 10): Promise<PageResponse<Post>> {
  try {
    return request<PageResponse<Post>>(
      RequestMethod.GET, 
      '/posts', 
      undefined, 
      { page, size } as QueryParams
    );
  } catch (error) {
    console.error(`获取帖子列表失败: ${error instanceof Error ? error.message : String(error)}`);
    throw new Error(`获取帖子列表失败: ${error instanceof Error ? error.message : String(error)}`);
  }
}

代码自动生成器

为了简化前后端API对接,我们开发了API代码生成工具:

// ApiGenerator.ts
import { ClassDeclaration, MethodDeclaration } from 'typescript';
import { generateApiMethod } from './CodeGen';

/**
 * 从控制器生成API服务
 * @param controllerClass 后端控制器类定义
 * @returns 生成的TypeScript API服务代码
 */
export function generateApiService(controllerClass: ClassDeclaration): string {
  const methods = controllerClass.getMethods();
  
  let apiServiceCode = `
import { request, RequestMethod, QueryParams } from '../utils/HttpUtil';
import { ${generateImports(controllerClass)} } from '../model/CommonTypes';

/**
 * ${controllerClass.getName()} API服务
 * 自动从后端控制器生成
 */
`;

  // 为每个控制器方法生成对应的前端API方法
  methods.forEach(method => {
    apiServiceCode += generateApiMethod(method, controllerClass);
  });
  
  return apiServiceCode;
}

/**
 * 生成所需的导入类型
 */
function generateImports(controllerClass: ClassDeclaration): string {
  // 收集返回类型和参数类型
  const typeSet = new Set<string>();
  
  controllerClass.getMethods().forEach(method => {
    const returnType = method.getReturnTypeNode()?.getText();
    if (returnType) {
      extractTypes(returnType, typeSet);
    }
    
    method.getParameters().forEach(param => {
      const paramType = param.getType().getText();
      extractTypes(paramType, typeSet);
    });
  });
  
  return Array.from(typeSet).join(', ');
}

后端实现

REST控制器

后端使用Spring MVC实现REST控制器:

// DreamController.java
package com.dreamplanner.controller;

import com.dreamplanner.entity.Dream;
import com.dreamplanner.service.DreamService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/dreams")
@RequiredArgsConstructor
public class DreamController {

    private final DreamService dreamService;
    
    @PostMapping
    public ResponseEntity<Dream> createDream(@RequestBody Dream dream) {
        Dream createdDream = dreamService.createDream(dream);
        return ResponseEntity.ok(createdDream);
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<Dream> getDreamById(@PathVariable Long id) {
        Dream dream = dreamService.getDreamById(id);
        return ResponseEntity.ok(dream);
    }
    
    @GetMapping("/user/{userId}")
    public ResponseEntity<List<Dream>> getDreamsByUserId(@PathVariable Long userId) {
        List<Dream> dreams = dreamService.getDreamsByUserId(userId);
        return ResponseEntity.ok(dreams);
    }
    
    @PutMapping("/{id}")
    public ResponseEntity<Dream> updateDream(@PathVariable Long id, @RequestBody Dream dream) {
        Dream updatedDream = dreamService.updateDream(id, dream);
        return ResponseEntity.ok(updatedDream);
    }
    
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteDream(@PathVariable Long id) {
        dreamService.deleteDream(id);
        return ResponseEntity.noContent().build();
    }
}

数据传输对象(DTO)

使用DTO保证前后端数据交换的一致性:

// DreamDTO.java
package com.dreamplanner.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DreamDTO {
    private Long id;
    private Long userId;
    private String title;
    private String description;
    private Integer category;
    private String targetDate;
    private Integer progress;
    private Integer priority;
    private Integer status;
    private String coverImage;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

全局异常处理

统一的异常处理机制确保前端收到一致的错误响应:

// GlobalExceptionHandler.java
package com.dreamplanner.exception;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleResourceNotFoundException(ResourceNotFoundException ex) {
        ErrorResponse errorResponse = new ErrorResponse(
            HttpStatus.NOT_FOUND.value(),
            ex.getMessage(),
            System.currentTimeMillis()
        );
        return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
    }
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ValidationErrorResponse> handleValidationExceptions(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        
        ValidationErrorResponse errorResponse = new ValidationErrorResponse(
            HttpStatus.BAD_REQUEST.value(),
            "验证失败",
            System.currentTimeMillis(),
            errors
        );
        
        return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGlobalException(Exception ex) {
        log.error("未处理异常", ex);
        
        ErrorResponse errorResponse = new ErrorResponse(
            HttpStatus.INTERNAL_SERVER_ERROR.value(),
            "服务器内部错误",
            System.currentTimeMillis()
        );
        
        return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

数据同步机制

请求拦截器

前端使用请求拦截器添加认证信息:

// AuthInterceptor.ets
import { PreferenceManager } from '../data/PreferenceManager';

/**
 * 添加认证令牌到请求头
 * @param headers 请求头对象
 */
export async function addAuthHeader(headers: Record<string, string>): Promise<void> {
  const prefManager = PreferenceManager.getInstance();
  const token = await prefManager.getString('auth_token', '');
  
  if (token) {
    headers['Authorization'] = `Bearer ${token}`;
  }
}

/**
 * 保存认证令牌
 * @param token JWT令牌
 */
export async function saveAuthToken(token: string): Promise<void> {
  const prefManager = PreferenceManager.getInstance();
  await prefManager.putString('auth_token', token);
}

数据翻译层

前后端数据结构转换工具:

// DataTranslator.ets
import { Dream, Task, Post, Comment } from '../model/CommonTypes';

/**
 * 将服务器响应数据转换为前端模型
 * @param serverDream 服务器返回的梦想数据
 * @returns 前端梦想模型
 */
export function translateDreamFromServer(serverDream: any): Dream {
  return {
    id: serverDream.id,
    userId: serverDream.userId,
    title: serverDream.title,
    description: serverDream.description,
    category: serverDream.category,
    targetDate: serverDream.targetDate,
    progress: serverDream.progress,
    priority: serverDream.priority,
    status: serverDream.status,
    coverImage: serverDream.coverImage,
    createdAt: serverDream.createdAt,
    updatedAt: serverDream.updatedAt
  };
}

/**
 * 将前端梦想模型转换为服务器请求数据
 * @param dream 前端梦想模型
 * @returns 服务器请求数据
 */
export function translateDreamToServer(dream: Dream): any {
  return {
    id: dream.id,
    userId: dream.userId,
    title: dream.title,
    description: dream.description,
    category: dream.category,
    targetDate: dream.targetDate,
    progress: dream.progress,
    priority: dream.priority,
    status: dream.status,
    coverImage: dream.coverImage
  };
}

安全策略

JWT认证

前后端使用JWT实现无状态认证:

// JwtTokenProvider.java
package com.dreamplanner.security;

import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.util.Base64;
import java.util.Date;

@Slf4j
@Component
public class JwtTokenProvider {

    @Value("${app.jwt.secret}")
    private String secretKey;

    @Value("${app.jwt.expiration}")
    private long validityInMilliseconds;

    private final UserDetailsService userDetailsService;

    public JwtTokenProvider(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @PostConstruct
    protected void init() {
        secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
    }

    public String createToken(String username) {
        Claims claims = Jwts.claims().setSubject(username);
        
        Date now = new Date();
        Date validity = new Date(now.getTime() + validityInMilliseconds);

        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(now)
                .setExpiration(validity)
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
    }

    public Authentication getAuthentication(String token) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(getUsername(token));
        return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
    }

    public String getUsername(String token) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
    }

    public String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }

    public boolean validateToken(String token) {
        try {
            Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
            return !claims.getBody().getExpiration().before(new Date());
        } catch (JwtException | IllegalArgumentException e) {
            log.error("无效的JWT令牌", e);
            return false;
        }
    }
}

HTTPS配置

确保数据传输安全:

// SecurityConfig.java
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http
        .requiresChannel(channel -> channel
            .anyRequest().requiresSecure())
        .csrf(csrf -> csrf.disable())
        .sessionManagement(session -> session
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .authorizeRequests(authorize -> authorize
            .antMatchers("/api/auth/**").permitAll()
            .anyRequest().authenticated())
        .exceptionHandling(handling -> handling
            .authenticationEntryPoint(jwtAuthenticationEntryPoint))
        .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
        .build();
}

性能优化

请求缓存

前端实现请求缓存减少重复网络请求:

// RequestCache.ets
import { MemoryCache } from '../utils/MemoryCache';

const REQUEST_CACHE = new MemoryCache<any>(60); // 60秒缓存

/**
 * 缓存包装器,如果缓存中存在数据则返回缓存数据,否则执行请求并缓存结果
 * @param cacheKey 缓存键
 * @param requestFn 请求函数
 * @param ttlSeconds 缓存有效期(秒)
 * @returns 请求结果
 */
export async function cachedRequest<T>(
  cacheKey: string, 
  requestFn: () => Promise<T>,
  ttlSeconds: number = 60
): Promise<T> {
  // 尝试从缓存获取
  const cachedData = REQUEST_CACHE.get(cacheKey);
  
  if (cachedData !== undefined) {
    console.info(`从缓存获取数据: ${cacheKey}`);
    return cachedData as T;
  }
  
  // 执行请求
  const result = await requestFn();
  
  // 缓存结果
  REQUEST_CACHE.set(cacheKey, result, ttlSeconds);
  
  return result;
}

/**
 * 清除指定键的缓存
 * @param cacheKey 缓存键
 */
export function invalidateCache(cacheKey: string): void {
  REQUEST_CACHE.remove(cacheKey);
}

/**
 * 清除所有缓存
 */
export function clearAllCache(): void {
  REQUEST_CACHE.clear();
}

分页与懒加载

列表数据分页加载优化:

// PaginatedList.ets
@Component
export struct PaginatedList<T> {
  @State private items: T[] = [];
  @State private currentPage: number = 0;
  @State private pageSize: number = 10;
  @State private loading: boolean = false;
  @State private hasMore: boolean = true;
  
  private loadPage: (page: number, size: number) => Promise<PageResponse<T>>;
  private renderItem: (item: T) => void;
  
  aboutToAppear() {
    this.loadInitialData();
  }
  
  async loadInitialData() {
    this.loading = true;
    try {
      const response = await this.loadPage(this.currentPage, this.pageSize);
      this.items = response.content;
      this.hasMore = !response.last;
    } catch (error) {
      console.error(`加载数据失败: ${error instanceof Error ? error.message : String(error)}`);
    } finally {
      this.loading = false;
    }
  }
  
  async loadMoreData() {
    if (this.loading || !this.hasMore) {
      return;
    }
    
    this.loading = true;
    try {
      this.currentPage++;
      const response = await this.loadPage(this.currentPage, this.pageSize);
      this.items = [...this.items, ...response.content];
      this.hasMore = !response.last;
    } catch (error) {
      console.error(`加载更多数据失败: ${error instanceof Error ? error.message : String(error)}`);
      this.currentPage--; // 恢复页码
    } finally {
      this.loading = false;
    }
  }
  
  build() {
    List() {
      ForEach(this.items, (item: T) => {
        ListItem() {
          this.renderItem(item)
        }
      })
      
      if (this.loading) {
        ListItem() {
          Row() {
            LoadingProgress()
              .width(24)
              .height(24)
            Text('加载中...')
              .fontSize(14)
              .margin({ left: 8 })
          }
          .height(60)
          .width('100%')
          .justifyContent(FlexAlign.Center)
        }
      }
    }
    .onReachEnd(() => {
      if (this.hasMore && !this.loading) {
        this.loadMoreData();
      }
    })
  }
}

版本控制与兼容性

API版本策略

通过URL路径管理API版本:

@RestController
@RequestMapping("/api/v1/dreams") // v1版本
public class DreamControllerV1 {
    // V1版本API实现
}

@RestController
@RequestMapping("/api/v2/dreams") // v2版本
public class DreamControllerV2 {
    // V2版本API实现,保持向后兼容
}

版本兼容策略

前端API服务工厂根据应用版本创建对应的API服务:

// ApiServiceFactory.ets
import { AppConfig } from '../config/AppConfig';
import * as ApiV1 from './api/v1/ApiService';
import * as ApiV2 from './api/v2/ApiService';

/**
 * 获取当前应用使用的API服务
 */
export function getApiService() {
  const apiVersion = AppConfig.API_VERSION;
  
  switch (apiVersion) {
    case 'v2':
      return ApiV2;
    case 'v1':
    default:
      return ApiV1;
  }
}

部署与发布

CI/CD流程

CI/CD流水线

梦想生活规划师项目的CI/CD流程包括:

  1. 代码提交触发自动构建
  2. 单元测试与集成测试
  3. 前后端代码质量检查
  4. 自动打包构建
  5. 自动部署到测试环境
  6. 人工质量验收
  7. 发布到生产环境

环境配置

通过配置文件区分不同环境:

// AppConfig.ets
export const AppConfig = {
  // 开发环境配置
  development: {
    BASE_API_URL: 'https://dev-api.dreamplanner.com/api',
    API_VERSION: 'v2',
    LOG_LEVEL: 'DEBUG'
  },
  
  // 测试环境配置
  testing: {
    BASE_API_URL: 'https://test-api.dreamplanner.com/api',
    API_VERSION: 'v2',
    LOG_LEVEL: 'INFO'
  },
  
  // 生产环境配置
  production: {
    BASE_API_URL: 'https://api.dreamplanner.com/api',
    API_VERSION: 'v2',
    LOG_LEVEL: 'ERROR'
  }
}[process.env.NODE_ENV || 'development'];

结论

梦想生活规划师应用的客户端服务端集成架构采用了现代化的设计原则和技术选型,实现了高效、可靠的数据交互。通过分层设计、统一的数据交换格式和完善的错误处理,确保了前后端协作的流畅性和一致性。该架构不仅满足了当前应用需求,还具有良好的可扩展性,能够支持未来功能的持续演进和优化。