使用REST风格完成MVC前后端分离,行为驱动开发

作者: 生命科学  发布:2019-10-01

图片 1

一个具有REST风格项目的基本特征:

图片 2Designer News.png

这一次我们将要讨论的是移动开发中比较重要的一环--网络请求的封装.鉴于个人经验有限,本文将在一定程度上参考 基于AFNetworking2.0和ReactiveCocoa2.1的iOS REST Client,来以LeanCloud的Rest Api来练手.前两节的示例,我们都是使用自定义的PHP接口来作为测试服务器,但是真实的服务器接口是涉及到许多细节的,比如一个基本的权限控制机制,用户登录登出等.为了能更真实快速的开始网络请求类的重构,本节选取一个国内较为常用的后端开发平台LeanCloud. 本文将实现一个拥有真实数据的博客App的Demo,数据源取自博客主站:ios122.com.

  1. 具有统一响应结构
  2. 前后台数据流转机制(HTTP消息与Java对象的互相转化机制)
  3. 统一的异常处理机制
  4. 参数验证机制
  5. Cors跨域请求机制
  6. 鉴权机制

前段时间在design+code购买了一个学习iOS设计和编码在线课程,使用Sketch设计App,然后使用Swift语言实现Designer News客户端。作者Meng To已经开源到Github:MengTo/DesignerNewsApp · GitHub。虽然实现整个Designer News客户端基本功能,但是采用臃肿MVC(Model-View-Controller)架构,不易于代码的测试和复用,于是使用ReactiveCocoa实现MVVM(Model-View-View Model)架构,加上一个用Objective-C实现的BDD测试框架Kiwi来单元测试,就可以行为驱动开发iOS App。

完整代码示例下载: github

一:统一响应结构

使用REST框架实现前后端分离架构,我们需要首先确定返回的JSON响应结构是统一的,也就是说,每个REST请求将返回相同结构的JSON响应结构。不妨定义一个相对通用的JSON响应结构,其中包含两部分:元数据与返回值,其中,元数据表示操作是否成功与返回值消息等,返回值对应服务端方法所返回的数据。该JSON响应结构如下: 

{
     "success": true,
     "message": "ok",
     "data": ...
}

 

为了在框架中映射以上JSON响应结构,我们需要编写一个Response类与其对应:

public class Result implements Serializable {
    /**
     * Comment for <code>serialVersionUID</code>
     */
    private static final long serialVersionUID = -1776835663544058134L;

    /**
     * 处理结果
     */
    private boolean success = true;
    /**
     * 返回信息
     */
    private String message = "";
    /**
     * error code
     */
    private String code = "0";

    public Object data;

    public boolean isSuccess() {
        return success;
    }

    public void setSuccess(boolean success) {
        this.success = success;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        if (!code.equals("0")) {
            success = false;
        }
        this.code = code;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

}

 

 

ReactiveCocoa是一个用Objective-C编写,具有函数式和响应式特性的编程框架。大多数的开发者他们解决问题的思考方式都是如何完成任务,通常的做法就是编写很多指令,然后修改重要数据结构的状态,这种编程范式叫做命令式编程(Imperative Programming)。与命令式编程不同的是函数式编程(Functional Programming),思考问题的方式是完成什么任务,怎样描述这个任务。关于对函数式编程入门概念的理解,可以参考酷壳《函数式编程》这篇文章,深入浅出对函数式编程的思考方式、特性和技术通过一些示例来讲解。

将WP导出的XML数据转换成JSON文件,导入LeanCloud.

首先,你是肯定要先去它们官网注册一个账号,然后添加一个应用.这是我是添加了应用iOS122.然后新建一个名为Post的Class,字段信息如下:

图片 3

iOS122是一个wordpress搭建的博客站点,导出的文章为xml格式,需要处理成 LeanCloud 需要的JSON格式才能导入,主站文章不多,几十篇,一个一个手动输,也是可以的.我将试着写一小段代码,来自动解析wp导出的文件,并根据需要生成对应的 JSON 文件.感兴趣的,可以自己试着弄下!

  • 这是原始的从wp中导出的主站的所有文章: .
  • 这是通过iOS代码解析处理后,生成的可直接导入进LeanCloud的JSON文件.
  • 这是XML转JSON核心代码,完整代码见文首github链接,XML解析用了一个第三方库Ono:
/* 要实现的逻辑很简单: 
 1.读取XML文件;
 2.解析为JSON,并显示;
 3.将JSON输出为json文件.*/

/* 1.读取并解析XML. */
NSMutableArray * jsonArray = [NSMutableArray arrayWithCapacity: 42];

NSString *XMLFilePath = [[NSBundle mainBundle] pathForResource:@"Post" ofType:@"xml"];
ONOXMLDocument *document = [ONOXMLDocument XMLDocumentWithData:[NSData dataWithContentsOfFile:XMLFilePath] error: NULL];

NSString *XPath = @"//channel/item";

[document enumerateElementsWithXPath:XPath usingBlock:^(ONOXMLElement *element, __unused NSUInteger idx, __unused BOOL *stop) {
    ONOXMLElement * titleElement = [element firstChildWithTag:@"title"];
    ONOXMLElement * descElement = [element firstChildWithTag: @"encoded" inNamespace: @"excerpt"];
    ONOXMLElement * contentElement = [element firstChildWithTag: @"encoded" inNamespace:@"content"];

    NSDictionary * jsonDict = @{
                                @"title": [titleElement stringValue],
                                @"desc": [descElement stringValue],
                                @"body": [contentElement stringValue]};

    [jsonArray addObject: jsonDict];
}];

/* 2.显示JSON字符串. */
NSData * jsonData = [NSJSONSerialization dataWithJSONObject:jsonArray
                                                   options:NSJSONWritingPrettyPrinted
                                                     error:NULL];

NSString * jsonString = [[NSString alloc] initWithData:jsonData
                                             encoding:NSUTF8StringEncoding];

self.textView.text = jsonString;

/*3.存储到文件中.
 真机下,暂无法找到Documents目录下的东西,可以通过模拟器运行此段代码,并通过finder-->前往文件夹,输入此处jsonPath对应的文件路径来获取 Post.json 文件.
 */
NSArray *paths=NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString * path=[paths objectAtIndex:0];
NSString * jsonPath=[path stringByAppendingPathComponent:@"Post.json"];

[jsonData writeToFile: jsonPath atomically:YES];
  • 导入后,LeanCloud控制台显示是这样的:

图片 4

二:前后台数据流转 

java对象与json的转换问题

只需在Controller的方法参数中使用@RequestBody注解定义需要转化的参数即可;类似地,若需要对Controller的方法返回值进行转化,则需要在该返回值上使用@ResponseBody注解来定义。

或者使用:@RestController注解来取代以上的@Controller注解,这样我们就可以省略返回值前面的@ResponseBody注解了。

需要注意的是,该特性在Spring 4.0中才引入。

除了使用注解来定义消息转化行为以外,我们还需要添加Jackson或者fastjsoon包进行支持。

 在Spring配置文件中添加以下配置即可:

 <!-- 该配置会自动注册RequestMappingHandlerMapping与RequestMappingHandlerAdapter两个Bean,
    这是SpringMVC为@Controllers分发请求所必需的,并提供了数据绑定支持、@NumberFormatannotation支持、
    @DateTimeFormat支持、@Valid支持、读写XML的支持和读写JSON的支持等功能。 -->
    <mvc:annotation-driven />

 

ReactiveCocoa解决哪些问题?

  • 对象之间状态与状态的依赖过多问题借用ReactiveCocoa中一个例子来说明:用户在登录界面时,有一个用户名输入框和密码输入框,还有一个登录按钮。登录交互要求如下:
  1. 当用户名和密码符合验证格式,并且之前还没登录时,登录按钮才能点击。
  2. 当点击登录成功登录后,设置已登录状态。

传统的做法代码如下:

static void *ObservationContext = &ObservationContext;- viewDidLoad { [super viewDidLoad]; [LoginManager.sharedManager addObserver:self forKeyPath:@"loggingIn" options:NSKeyValueObservingOptionInitial context:&ObservationContext]; [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(loggedOut:) name:UserDidLogOutNotification object:LoginManager.sharedManager]; [self.usernameTextField addTarget:self action:@selector(updateLogInButton) forControlEvents:UIControlEventEditingChanged]; [self.passwordTextField addTarget:self action:@selector(updateLogInButton) forControlEvents:UIControlEventEditingChanged]; [self.logInButton addTarget:self action:@selector(logInPressed:) forControlEvents:UIControlEventTouchUpInside];}- dealloc { [LoginManager.sharedManager removeObserver:self forKeyPath:@"loggingIn" context:ObservationContext]; [NSNotificationCenter.defaultCenter removeObserver:self];}- updateLogInButton { BOOL textFieldsNonEmpty = self.usernameTextField.text.length > 0 && self.passwordTextField.text.length > 0; BOOL readyToLogIn = !LoginManager.sharedManager.isLoggingIn && !self.loggedIn; self.logInButton.enabled = textFieldsNonEmpty && readyToLogIn;}- logInPressed:(UIButton *)sender { [[LoginManager sharedManager] logInWithUsername:self.usernameTextField.text password:self.passwordTextField.text success:^{ self.loggedIn = YES; } failure:^(NSError *error) { [self presentError:error]; }];}- loggedOut:(NSNotification *)notification { self.loggedIn = NO;}- observeValueForKeyPath:(NSString *)keyPath ofObject:object change:(NSDictionary *)change context:context { if (context == ObservationContext) { [self updateLogInButton]; } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; }}

以上使用KVO、Notification、Target-Action等处理事件或消息的方式编写的代码分散到各个地方,变得杂乱和难以理解;但是使用RACSignal统一处理的话,代码更加简洁和易读。使用RAC后代码如下:

- viewDidLoad { [super viewDidLoad]; @weakify; RAC(self.logInButton, enabled) = [RACSignal combineLatest:@[ self.usernameTextField.rac_textSignal, self.passwordTextField.rac_textSignal, RACObserve(LoginManager.sharedManager, loggingIn), RACObserve(self, loggedIn) ] reduce:^(NSString *username, NSString *password, NSNumber *loggingIn, NSNumber *loggedIn) { return @(username.length > 0 && password.length > 0 && !loggingIn.boolValue && !loggedIn.boolValue); }]; [[self.logInButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(UIButton *sender) { @strongify; RACSignal *loginSignal = [LoginManager.sharedManager logInWithUsername:self.usernameTextField.text password:self.passwordTextField.text]; [loginSignal subscribeError:^(NSError *error) { @strongify; [self presentError:error]; } completed:^{ @strongify; self.loggedIn = YES; }]; }]; RAC(self, loggedIn) = [[NSNotificationCenter.defaultCenter rac_addObserverForName:UserDidLogOutNotification object:nil] mapReplace:@NO];}
  • 传统MVC架构中,由于Controller承担数据验证、映射数据模型到View和操作View层次结构等多个责任,导致Controller过于臃肿,不利于代码的复用和测试。在传统的MVC架构中,主要有Model, View和Controller三部分组成。Model主要是保存数据和处理业务逻辑,View将数据显示,而Controller调解关于Model和View之间的所有交互。当数据到达时,Model通过Key-Value Observation来通知View Controller, 然后View Controller更新View。当View与用户交互后,View Controller更新Model。

图片 5Typical MVC paradigm.png

正如你所见,View Controller隐式承担很多责任:数据验证、映射数据模型到View和操作View层次结构。MVVM将很多逻辑从View Controller移走到View-Model,等介绍完ReactiveCocoa后会介绍MVVM架构。还有一些关于如何减负View Controller好文章请参阅objc中国更轻量的View Controllers系列:

  • 更轻量的 View Controllers

  • 整洁的 Table View 代码

  • 测试 View Controllers

  • 使用Signal来代替KVO、Notification、Delegate和Target-Action等传递消息iOS开发中有多种消息传递方式,KVO、Notification、Delegate、Block和Target-Action,对于它们之间有什么差异以及如何选择请参考《消息传递机制》。但RAC提供RACSignal来统一消息传递机制,不再为如何选择何种传递消息方式而烦恼。

    RAC对常用UI控件事件进行封装成一个RACSignal对象,以便对发生的各种事件进行监听。KVO示例代码如下:

// When self.username changes, logs the new name to the console.//// RACObserve(self, username) creates a new RACSignal that sends the current// value of self.username, then the new value whenever it changes.// -subscribeNext: will execute the block whenever the signal sends a value.[RACObserve(self, username) subscribeNext:^(NSString *newName) { NSLog(@"%@", newName);}];

Target-Action示例代码如下:

// Logs a message whenever the button is pressed.//// RACCommand creates signals to represent UI actions. Each signal can// represent a button press, for example, and have additional work associated// with it.//// -rac_command is an addition to NSButton. The button will send itself on that// command whenever it's pressed.self.button.rac_command = [[RACCommand alloc] initWithSignalBlock:^ { NSLog(@"button was pressed!"); return [RACSignal empty];}];

Notification示例代码如下:

 // Respond to when email text start and end editing [[[NSNotificationCenter defaultCenter] rac_addObserverForName:UITextFieldTextDidBeginEditingNotification object:self.emailTextField] subscribeNext:^ { [self.emailImageView animate]; self.emailImageView.image = [UIImage imageNamed:@"icon-mail-active"]; self.emailTextField.background = [UIImage imageNamed:@"input-outline-active"]; }]; [[[NSNotificationCenter defaultCenter] rac_addObserverForName:UITextFieldTextDidEndEditingNotification object:self.emailTextField] subscribeNext:^ { self.emailTextField.background = [UIImage imageNamed:@"input-outline"]; self.emailImageView.image = [UIImage imageNamed:@"icon-mail"]; }];

除此之外,还可以使用AFNetworking访问服务器后对返回数据自创建一个RACSignal。示例代码如下:

 + (RACSubject*)storiesForSection:(NSString*)section page:(NSInteger)page{ RACSubject* signal = [RACSubject subject]; NSDictionary* parameters = @{ @"page" : [NSString stringWithFormat:@"%ld", page], @"client_id" : clientID }; [[AFHTTPSessionManager manager] GET:[DesignerNewsURL stroiesURLString] parameters:parameters success:^(NSURLSessionDataTask* task, id responseObject) { NSLog(@"url string = %@", task.currentRequest.URL); [signal sendNext:responseObject]; [signal sendCompleted]; } failure:^(NSURLSessionDataTask* task, NSError* error) { NSLog(@"url string = %@", task.currentRequest.URL); [signal sendError:error]; }]; return signal;}

有些朋友可以感觉有点奇怪,上面代码明明返回的是RACSubject,而不是RACSignal,其实RACSubject是RACSignal的子类,但是RACSubject写出代码更加简洁,所以采用RACSubject(官方不推荐使用)。等下将RAC核心类设计时,你就会了解它们之间的关系和如何选择。

模仿 "花瓣",重写 LeanCloud Rest Api的iOS REST Client.

接下来的文字,思路上将在很大程度上参考 @limboy的文章,但是会相对更加完整.另外,其实 LeanCloud 其实是有自己的iOS API的,但是是一个抽象的封装,和实际应用中使用的网络请求API有很大不同.两种方式的差别,有点类似于是使用 字典等基本类型存储数据,还是使用 自定义的Model来存储数据.两种方式,不过多置评,个人倾向于后一种,方便后续的代码重构.

// TODO:Models Group包含了所有跟服务端API对应的Model,比如HBPComment

三:处理异常行为

在Spring MVC中,我们可以使用AOP技术,编写一个全局的异常处理切面类,用它来统一处理所有的异常行为,在Spring 3.2中才开始提供。使用很简单,只需定义一个类,并通过@ControllerAdvice注解将其标注即可,同时需要使用@ResponseBody注解表示返回值可序列化为JSON字符串。代码如下:

/**        
 * Title: 全局异常处理切面    
 * Description: 利用 @ControllerAdvice + @ExceptionHandler 组合处理Controller层RuntimeException异常
 * @author rico       
 * @created 2017年7月4日 下午4:29:07    
 */      
@ControllerAdvice   // 控制器增强
@ResponseBody
public class ExceptionAspect {

    /** Log4j日志处理(@author: rico) */
    private static final Logger log = Logger.getLogger(ExceptionAspect.class);

    /**
     * 400 - Bad Request
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(HttpMessageNotReadableException.class)
    public Result handleHttpMessageNotReadableException(
            HttpMessageNotReadableException e) {
        log.error("could_not_read_json...", e);
        return new Response().failure("could_not_read_json");
    }

    /**
     * 400 - Bad Request
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler({MethodArgumentNotValidException.class})
    public Response handleValidationException(MethodArgumentNotValidException e) {
        log.error("parameter_validation_exception...", e);
        return new Result().failure("parameter_validation_exception");
    }

    /**
     * 405 - Method Not Allowed。HttpRequestMethodNotSupportedException
     * 是ServletException的子类,需要Servlet API支持
     */
    @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public Result handleHttpRequestMethodNotSupportedException(
            HttpRequestMethodNotSupportedException e) {
        log.error("request_method_not_supported...", e);
        return new Result().failure("request_method_not_supported");
    }

    /**
     * 415 - Unsupported Media Type。HttpMediaTypeNotSupportedException
     * 是ServletException的子类,需要Servlet API支持
     */
    @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
    @ExceptionHandler({ HttpMediaTypeNotSupportedException.class })
    public Result handleHttpMediaTypeNotSupportedException(Exception e) {
        log.error("content_type_not_supported...", e);
        return new Result().failure("content_type_not_supported");
    }

    /**
     * 500 - Internal Server Error
     */
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(TokenException.class)
    public Result handleTokenException(Exception e) {
        log.error("Token is invaild...", e);
        return new Result().failure("Token is invaild");
    }

    /**
     * 500 - Internal Server Error
     */
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(Exception.class)
    public Result handleException(Exception e) {
        log.error("Internal Server Error...", e);
        return new Result().failure("Internal Server Error");
    }
}

在ExceptionAdvice类中包含一系列的异常处理方法,每个方法都通过@ResponseStatus注解定义了响应状态码,此外还通过@ExceptionHandler注解指定了具体需要拦截的异常类。以上过程只是包含了一部分的异常情况,若需处理其它异常,可添加方法具体的方法。需要注意的是,在运行时从上往下依次调用每个异常处理方法,匹配当前异常类型是否与@ExceptionHandler注解所定义的异常相匹配,若匹配,则执行该方法,同时忽略后续所有的异常处理方法,最终会返回经JSON序列化后的Result对象。

 

ReactiveCocoa核心类设计

关于RAC核心类设计,官方文档有详细的解释:Framework Overview

基本结构

使用时,直接引用 YFAPI.h 即可,里面包含了所有的Class:

|- YFAPI.h
|- Classes
    |- YFAPIManager.h
    |- YFAPIManager.m
    |- Models
        |- YFPostModel.h
        |- YFPostModel.h
           ...

YFAPIManager包含了所有的跟服务端通信的方法,通过Category来区分:

//
//  YFAPIManager.h
//  iOS122
//
//  Created by 颜风 on 15/10/28.
//  Copyright © 2015年 iOS122. All rights reserved.
//

#import <Foundation/Foundation.h>
#import <AFNetworking.h>

@class RACSignal, YFUserModel;

@interface YFAPIManager : AFHTTPRequestOperationManager

@property (nonatomic, nonatomic) YFUserModel * user; //!< 当前登录的用户,可能为nil.


/**
 *  一个单例.
 *
 *  @return 共享的实例对象.
 */
+ (instancetype) sharedInstance;

@end

/**
 *  私有扩展,其他网路请求的基础.
 */
@interface YFAPIManager (Private)

/**
 *  内部统一使用这个方法来向服务端发送请求
 *
 *  @param method       请求方式.
 *  @param relativePath 相对路径.
 *  @param parameters   参数.
 *  @param resultClass  从服务端获取到JSON数据后,使用哪个Class来将JSON转换为OC的Model.
 *
 *  @return RACSignal 信号对象.
 */
- (RACSignal *)requestWithMethod:(NSString *)method relativePath:(NSString *)relativePath parameters:(NSDictionary *)parameters resultClass:(Class)resultClass;

@end


/**
 *  用户信息相关的操作.
 */
@interface YFAPIManager (User)


/**
 *  用户登录.
 *
 *  获取到用户数据后,会自动更新User属性,所以仅需要在必要的地方观察user属性即可.
 *
 *  @param username 用户名.
 *  @param password 用户密码.
 *
 *  @return RACSingal对象,sendNext的是此类的的单例实例.
 */
- (RACSignal *)signInUsingUsername:(NSString *)username passowrd:(NSString *)password;

/**
 *  登出.
 *
 *  登出,其实就是把 user 属性设为nil.
 *
 *  @return sendNext为此类的单例实例.
 */
- (RACSignal *) logout;

@end

/**
 *  文章相关操作.
 */
@interface YFAPIManager (Post)
//....

@end

Models Group包含了所有跟服务端API对应的Model,比如 YFPostModel:

//
//  YFPostModel.h
//  iOS122
//
//  Created by 颜风 on 15/10/28.
//  Copyright © 2015年 iOS122. All rights reserved.
//

#import <Foundation/Foundation.h>
#import <Mantle.h>

/**
 *  文章.
 */
@interface YFPostModel : MTLModel <MTLJSONSerializing>

@property (strong, nonatomic) NSString * postId; //!< 文章唯一标识.
@property (copy, nonatomic) NSString * title; //!< 文章标题.
@property (copy, nonatomic) NSString * desc; //!< 文章简介.
@property (copy, nonatomic) NSString * body; //!< 文章详情.

@end

//
//  YFPostModel.m
//  iOS122
//
//  Created by 颜风 on 15/10/28.
//  Copyright © 2015年 iOS122. All rights reserved.
//

#import "YFPostModel.h"

@implementation YFPostModel

/**
 *  用于指定模型属性与JSON数据字段的对应关系.
 *
 *  @return 模型属性与JSON数据字段的对应关系:以模型属性为键,JSON字段为值.
 */

+ (NSDictionary *)JSONKeyPathsByPropertyKey {
    NSDictionary * dictMap = @{
                               @"postId": @"objectId",
                               @"title": @"title",
                               @"desc": @"desc",
                               @"body": @"body"
                               };

    return dictMap;
}

@end

可以使用类似下面的语句,来将JSON转换为Model:

YFPostModel * model = [MTLJSONAdapter modelOfClass:[YFPostModel class] fromJSONDictionary:@{@"title": @"标题", @"desc": @"简介", @"body": @"内容", @"objectId": @"id"} error: NULL];

四:支持参数验证

我们回到上文所提到的示例,这里处理一个普通的PUT请求,代码如下:

@RestController
@RequestMapping("/users")
public class UserController {

    private UserService userService;

    /** Log4j日志处理(@author: rico) */
    private static final Logger log = Logger.getLogger(UserController.class);

    public UserService getUserService() {
        return userService;
    }

    @Resource(name = "userService")
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    @RequestMapping(value = "/user", method = RequestMethod.PUT, produces = "application/json", 
            consumes = "application/json")
    public User addUser(@RequestBody User user) {  // 将接收到的HTTP消息转化为Java对象
        userService.addUser(user);
        log.debug("添加用户 :" + user);
        return user;
    }

    ...
}

其中,User参数包含若干属性,通过以下类结构可见,它是一个传统的POJO:

public class User implements Serializable{

    private static final long serialVersionUID = 1L;

    private int id;
    private String uname;
    private String passwd;
    private String gentle;
    private String email;
    private String city;

    public User() {
        super();
    }

    // getter/setter

    // toString
}

如果业务上需要确保User对象的uname属性必填,如何实现呢?若将这类参数验证的代码写死在Controller中,势必会与正常的业务逻辑搅在一起,导致责任不够单一,违背于“单一责任原则”。建议将其参数验证行为从Controller中剥离出来,放到另外的类中,这里仅通过@Valid注解来定义uname参数,并通过Bean Validation的参考实现Hibernate Validator的@NotEmpty注解来定义User类中的uname属性,就像下面这样:

@RestController
@RequestMapping("/users")
public class UserController {

    private UserService userService;

    /** Log4j日志处理(@author: rico) */
    private static final Logger log = Logger.getLogger(UserController.class);

    public UserService getUserService() {
        return userService;
    }

    @Resource(name = "userService")
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    @RequestMapping(value = "/user", method = RequestMethod.PUT, produces = "application/json", 
            consumes = "application/json")
    public User addUser(@RequestBody  @Valid User user) {  // 将接收到的HTTP消息转化为Java对象
        userService.addUser(user);
        log.debug("添加用户 :" + user);
        return user;
    }

    ...
}


public class User implements Serializable{

    private static final long serialVersionUID = 1L;

    private int id;
    @NotEmpty
    private String uname;
    private String passwd;
    private String gentle;
    private String email;
    private String city;

    public User() {
        super();
    }

    // getter/setter

    // toString
}

这里的@Valid注解实际上是Validation Bean规范提供的注解,该规范已由Hibernate Validator框架实现,因此需要添加以下Maven依赖到pom.xml文件中:

<dependency>  
    <groupId>org.hibernate</groupId>  
    <artifactId>hibernate-validator</artifactId>  
    <version>${hibernate-validator.version}</version>  
</dependency> 

需要注意的是,Hibernate Validator与Hibernate没有任何依赖关系,唯一有联系的只是都属于JBoss公司的开源项目而已。然后,我们需要在Spring配置文件中开启该特性,需添加如下配置:

<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"/> 

最后,我们在全局异常处理类中添加对参数验证异常的处理方法,代码如下:

@ControllerAdvice  
@ResponseBody  
public class ExceptionAdvice {  

    /** 
     * 400 - Bad Request 
     */  
    @ResponseStatus(HttpStatus.BAD_REQUEST)  
    @ExceptionHandler(ValidationException.class)  
    public Result handleValidationException(ValidationException e) {  
        logger.error("参数验证失败", e);  
        return new Result().failure("validation_exception");  
    }  
}  

至此,REST框架已集成了Bean Validation特性,我们可以使用各种注解来完成所需的参数验证行为了。 
看似该框架可以在本地成功跑起来,整个架构包含两个应用,前端应用提供纯静态的HTML页面,后端应用发布REST API,前端需要通过AJAX调用后端发布的REST API,然而AJAX是不支持跨域访问的,也就是说,前后端两个应用必须在同一个域名下才能访问。

Sequence和Signal基本操作

了解完整个RAC核心类设计之后,要学会对Sequence和Signal基本操作,比如:用signal执行side effects,转换streams, 合并stream和合并signal。详情请查阅官方文档:Basic Operators

Archive / UnArchive / Copy

每一个Model都要支持Archive / UnArchive / Copy,也就是要实现和协议,这两个协议的内容其实就是对Object的Property做些处理,所以如果可以在基类里把这些事都统一处理,就会方便许多。考虑到设计的稳定性和后期的可扩展性,我们使用比较著名的第三方库--Mantle 来处理.你可以使用CocoaPods安装这个库,然后引入头文件 #import <Mantle.h> 到自定义的Model中即可.

pod 'Mantle' # JSON <==> Model

五:解决跨域问题

比如,前端应用为静态站点且部署在域下,后端应用发布REST API并部署在域下,如何使前端应用通过AJAX跨域访问后端应用呢?这需要使用到CORS技术来实现,这也是目前最好的解决方案了。

CORS全称为Cross Origin Resource Sharing(跨域资源共享),服务端只需添加相关响应头信息,即可实现客户端发出AJAX跨域请求。

CORS技术非常简单,易于实现,目前绝大多数浏览器均已支持该技术(IE8浏览器也支持了),服务端可通过任何编程语言来实现,只要能将CORS响应头写入response对象中即可。

下面我们继续扩展REST框架,通过CORS技术实现AJAX跨域访问。首先,我们需要编写一个Filter,用于过滤所有的HTTP请求,并将CORS响应头写入response对象中,代码如下:

/**        
 * Title: 跨域访问处理(跨域资源共享)    
 * Description: 解决前后端分离架构中的跨域问题
 * @author zhuzhen
 * @created 2018年04月17日 下午5:00:09    
 */      
public class CorsFilter implements Filter {

    /** Log4j日志处理(@author: rico) */
    private static final Logger log = Logger.getLogger(UserController.class);

    private String allowOrigin;
    private String allowMethods;
    private String allowCredentials;
    private String allowHeaders;
    private String exposeHeaders;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        allowOrigin = filterConfig.getInitParameter("allowOrigin");
        allowMethods = filterConfig.getInitParameter("allowMethods");
        allowCredentials = filterConfig.getInitParameter("allowCredentials");
        allowHeaders = filterConfig.getInitParameter("allowHeaders");
        exposeHeaders = filterConfig.getInitParameter("exposeHeaders");
    }


    /** 
     * @description 通过CORS技术实现AJAX跨域访问,只要将CORS响应头写入response对象中即可
     * @author zhuzhen
     * @created 2018年04月17日 下午5:02:38      
     * @param req
     * @param res
     * @param chain
     * @throws IOException
     * @throws ServletException     
     * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)     
     */  
    @Override
    public void doFilter(ServletRequest req, ServletResponse res,
            FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        String currentOrigin = request.getHeader("Origin");
        log.debug("currentOrigin : " + currentOrigin);
        if (StringUtil.isNotEmpty(allowOrigin)) {
            List<String> allowOriginList = Arrays
                    .asList(allowOrigin.split(","));
            log.debug("allowOriginList : " + allowOrigin);
            if (CollectionUtil.isNotEmpty(allowOriginList)) {
                if (allowOriginList.contains(currentOrigin)) {
                    response.setHeader("Access-Control-Allow-Origin",
                            currentOrigin);
                }
            }
        }
        if (StringUtil.isNotEmpty(allowMethods)) {
            response.setHeader("Access-Control-Allow-Methods", allowMethods);
        }
        if (StringUtil.isNotEmpty(allowCredentials)) {
            response.setHeader("Access-Control-Allow-Credentials",
                    allowCredentials);
        }
        if (StringUtil.isNotEmpty(allowHeaders)) {
            response.setHeader("Access-Control-Allow-Headers", allowHeaders);
        }
        if (StringUtil.isNotEmpty(exposeHeaders)) {
            response.setHeader("Access-Control-Expose-Headers", exposeHeaders);
        }
        chain.doFilter(req, res);
    }

    @Override
    public void destroy() {
    }
}

以上CorsFilter将从web.xml中读取相关Filter初始化参数,并将在处理HTTP请求时将这些参数写入对应的CORS响应头中,下面大致描述一下这些CORS响应头的意义:

  • Access-Control-Allow-Origin:允许访问的客户端域名,例如:

  • Access-Control-Allow-Methods:允许访问的方法名,多个方法名用逗号分割,例如:GET,POST,PUT,DELETE,OPTIONS;

  • Access-Control-Allow-Credentials:是否允许请求带有验证信息,若要获取客户端域下的cookie时,需要将其设置为true;

  • Access-Control-Allow-Headers:允许服务端访问的客户端请求头,多个请求头用逗号分割,例如:Content-Type;

  • Access-Control-Expose-Headers:允许客户端访问的服务端响应头,多个响应头用逗号分割。

需要注意的是,CORS规范中定义Access-Control-Allow-Origin只允许两种取值,要么为*,要么为具体的域名,也就是说,不支持同时配置多个域名。为了解决跨多个域的问题,需要在代码中做一些处理,这里将Filter初始化参数作为一个域名的集合(用逗号分隔),只需从当前请求中获取Origin请求头,就知道是从哪个域中发出的请求,若该请求在以上允许的域名集合中,则将其放入Access-Control-Allow-Origin响应头,这样跨多个域的问题就轻松解决了。以下是web.xml中配置CorsFilter的方法:

<!-- 通过CORS技术实现AJAX跨域访问 -->
    <filter>
        <filter-name>corsFilter</filter-name>
        <filter-class>cn.edu.tju.rico.filter.CorsFilter</filter-class>
        <init-param>
            <param-name>allowOrigin</param-name>
            <param-value>http://localhost:8020</param-value>
        </init-param>
        <init-param>
            <param-name>allowMethods</param-name>
            <param-value>GET,POST,PUT,DELETE,OPTIONS</param-value>
        </init-param>
        <init-param>
            <param-name>allowCredentials</param-name>
            <param-value>true</param-value>
        </init-param>
        <init-param>
            <param-name>allowHeaders</param-name>
            <param-value>Content-Type,X-Token</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>corsFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

完成以上过程即可实现AJAX跨域功能了,但似乎还存在另外一个问题,由于REST是无状态的,后端应用发布的REST API可在用户未登录的情况下被任意调用,这显然是不安全的,如何解决这个问题呢?我们需要为REST请求提供安全机制。

MVVM架构

图片 6MVVM high level.png

在MVVM架构中,通常都将view和view controller看做一个整体。相对于之前MVC架构中view controller执行很多在view和model之间数据映射和交互的工作,现在将它交给view model去做。至于选择哪种机制来更新view model或view是没有强制的,但通常我们都选择ReactiveCocoa。ReactiveCocoa会监听model的改变然后将这些改变映射到view model的属性中,并且可以执行一些业务逻辑。举个例子来说,有一个model包含一个dateAdded的属性,我想监听它的变化然后更新view model的dateAdded属性。但model的dateAdded属性的数据类型是NSDate,而view model的数据类型是NSString,所以在view model的init方法中进行数据绑定,但需要数据类型转换。示例代码如下:

RAC(self,dateAdded) = [RACObserve(self.model,dateAdded) map:^(NSDate*date){ return [[ViewModel dateFormatter] stringFromDate:date];}];

ViewModel调用dateFormatter进行数据转换,且方法dateFormatter可以复用到其他地方。然后view controller监听view model的dateAdded属性且绑定到label的text属性。

RAC(self.label,text) = RACObserve(self.viewModel,dateAdded);

现在我们抽象出日期转换到字符串的逻辑到view model,使得代码可以测试复用,并且帮view controller瘦身

Kiwi是一个iOS行为驱动开发(Behavior Driven Development)的库。相比于Xcode提供单元测试的XCTest是从测试的角度思考问题,而Kiwi是从行为的角度思考问题,测试用例都遵循三段式Given-When-Then的描述,清晰地表达测试用例是测试什么样的对象或数据结构,在基于什么上下文或情景,然后做出什么响应。

describe(@"Team", ^{ context(@"when newly created", ^{ it(@"has a name", ^{ id team = [Team team]; [[team.name should] equal:@"Black Hawks"]; }); it(@"has 11 players", ^{ id team = [Team team]; [[[team should] have:11] players]; }); });});

我们很容易根据上下文将其提取为Given..When..Then的三段式自然语言

Given a Team, when be newly created, it should have a name, it should have 11 player

用Xcode自带的XCTest测试框架写过测试代码的朋友可能体会到,以上代码更加易于阅读和理解。就算以后有新的开发者加入或修护代码时,不需要太大的成本去阅读和理解代码。具体如何使用Kiwi,请参考两篇文章:

  • TDD的iOS开发初步以及Kiwi使用入门
  • Kiwi 使用进阶 Mock, Stub, 参数捕获和异步测试

在编写Designer News客户端代码之前,首先通过UI来了解整个App的概况。设计Designer News UI的工具是Sketch,想获得Designer News UI,请点击下载Designer New UI。

图片 7Designer News Design.png如果将所有的页面都逐个说明如何编写,会比较耗时间,所以只拿登陆页面来说明我是如何行为驱动开发iOS,但我会将整个项目的代码上传到github。

由于这个项目简单并且只有一个人开发(多人开发的话,采用Storyboard不易于代码合并),加上Storyboard可以可视化的添加UI组件和Auto Layout的约束,并且可以同时预览多个不同分辨率iPhone的效果,极大地提高开发界面效率。

图片 8Login.png

登陆界面有Email输入框和密码输入框,当用户选中其他一个输入框时,左边对应的图标变成蓝色,同时会有pop动画表示用户准备要输入内容。当用户没有输入有效的Email或密码格式时,用户是不能点击登陆按钮,只有当用户输入有效的邮件和密码格式时,才能点击登陆按钮。

图片 9Login.gif

我们可以使用RAC通过监听Text Field的UITextFieldTextDidBeginEditingNotificationUITextFieldTextDidEndEditingNotification的通知来处理用户选中Email输入框和密码输入框时改变图标和显示的动画。

#pragma mark - Text Field notification- textFieldStartEndEditing{ // Respond to when email text start and end editing [[[NSNotificationCenter defaultCenter] rac_addObserverForName:UITextFieldTextDidBeginEditingNotification object:self.emailTextField] subscribeNext:^ { [self.emailImageView animate]; self.emailImageView.image = [UIImage imageNamed:@"icon-mail-active"]; self.emailTextField.background = [UIImage imageNamed:@"input-outline-active"]; }]; [[[NSNotificationCenter defaultCenter] rac_addObserverForName:UITextFieldTextDidEndEditingNotification object:self.emailTextField] subscribeNext:^ { self.emailTextField.background = [UIImage imageNamed:@"input-outline"]; self.emailImageView.image = [UIImage imageNamed:@"icon-mail"]; }]; // Respond to when password text start and end editing [[[NSNotificationCenter defaultCenter] rac_addObserverForName:UITextFieldTextDidBeginEditingNotification object:self.passwordTextField] subscribeNext:^ { [self.passwordImageView animate]; self.passwordTextField.background = [UIImage imageNamed:@"input-outline-active"]; self.passwordImageView.image = [UIImage imageNamed:@"icon-password-active"]; }]; [[[NSNotificationCenter defaultCenter] rac_addObserverForName:UITextFieldTextDidEndEditingNotification object:self.passwordTextField] subscribeNext:^ { self.passwordTextField.background = [UIImage imageNamed:@"input-outline"]; self.passwordImageView.image = [UIImage imageNamed:@"icon-password"]; }];}

当点击登陆按钮后,客户端向服务端发送验证请求,服务端验证完账户和密码后,用户便可以成功登陆。所以,接下来要了解RESTful API的基本概念和Designer News提供的RESTful API。

用户的登录与登出

先来说说登录,由于使用RAC,在构造API时,就不需要传入Block了,随之而来的一个问题就是需要在注释中说明sendNext时会发送什么内容.LeanCloud用户登录接口会返回完整的用户信息:

+ (RACSignal *)signInUsingUsername:(NSString *)username passowrd:(NSString *)password
{
    NSDictionary *parameters = @{
                                 @"username": username,
                                 @"password": password,
                                 };

    YFAPIManager *manager = [self sharedInstance];

    // 需要配对使用@weakify 与 @strongify 宏,以防止block内的可能的循环引用问题.
    @weakify(manager);

    return [[[[manager rac_GET:@"login" parameters:parameters]
               // reduceEach的作用是传入多个参数,返回单个参数,是基于`map`的一种实现
               reduceEach:^id(NSDictionary *response, AFHTTPRequestOperation *operation){
                   @strongify(manager);

                   YFUserModel * user = [MTLJSONAdapter modelOfClass:[YFUserModel class] fromJSONDictionary: response error: NULL];

                   manager.user = user;

                   return manager;
               }]
              // 避免side effect,有点类似于 "懒加载".
              replayLazily]
            setNameWithFormat:@"+signInUsingUsername:%@ password:%@", username, password];
}

用户的登出就简单了,直接设置user为nil就行了:

+ (RACSignal *)logout
{
    YFAPIManager * manager = [YFAPIManager sharedInstance];
    @weakify(manager);

    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        @strongify(manager);

        manager.user = nil;


        [subscriber sendNext: manager];

        [subscriber sendCompleted];

        return nil;
    }];
}

六:提供安全机制

解决REST安全调用问题,可以做得很复杂,也可以做得特简单,可按照以下过程提供REST安全机制:

  (1). 当用户登录成功后,在服务端生成一个token,并将其放入内存中(可放入JVM或Redis中),同时将该token返回到客户端;

  (2). 在客户端中将返回的token写入cookie中,并且每次请求时都将token随请求头一起发送到服务端;

  (3). 提供一个AOP切面,用于拦截所有的Controller方法,在切面中判断token的有效性;

  (4). 当登出时,只需清理掉cookie中的token即可,服务端token可设置过期时间,使其自行移除。

首先,我们需要定义一个用于管理token的接口,包括创建token与检查token有效性的功能。代码如下:

/**        
 * Title: REST 鉴权   
 * Description: 登录用户的身份鉴权
 * @author zhuzhen
 * @created 2018年04月17日 下午4:41:43    
 */      
public interface TokenManager {

    String createToken(String username);  

    boolean checkToken(String token); 

    void deleteToken(String token);
}

然后,我们可提供一个简单的TokenManager实现类,将token存储到JVM内存中。代码如下:

/**        
 * Title: TokenManager的默认实现    
 * Description: 管理 Token
 * @author rico       
 * @created 2017年7月4日 下午4:41:32    
 */      
public class DefaultTokenManager implements TokenManager {

    /** 将token存储到JVM内存(ConcurrentHashMap)中   (@author: rico) */      
    private static Map<String, String> tokenMap = new ConcurrentHashMap<String, String>();

    /** 
     * @description 利用UUID创建Token(用户登录时,创建Token)
     * @author rico       
     * @created 2017年7月4日 下午4:46:46      
     * @param username
     * @return     
     * @see cn.edu.tju.rico.authorization.TokenManager#createToken(java.lang.String)     
     */  
    public String createToken(String username) {
        String token = CodecUtil.createUUID();
        tokenMap.put(token, username);
        return token;
    }


    /** 
     * @description Token验证(用户登录验证)
     * @author rico       
     * @created 2017年7月4日 下午4:46:50      
     * @param token
     * @return     
     * @see cn.edu.tju.rico.authorization.TokenManager#checkToken(java.lang.String)     
     */  
    public boolean checkToken(String token) {
        return !StringUtil.isEmpty(token) && tokenMap.containsKey(token);
    }


    /** 
     * @description Token删除(用户登出时,删除Token)
     * @author rico       
     * @created 2017年7月4日 下午4:46:54      
     * @param token     
     * @see cn.edu.tju.rico.authorization.TokenManager#deleteToken(java.lang.String)     
     */  
    @Override
    public void deleteToken(String token) {
        // TODO Auto-generated method stub
        tokenMap.remove(token);
    }
}

需要注意的是,如果需要做到分布式集群,建议基于Redis提供一个实现类,将token存储到Redis中,并利用Redis与生俱来的特性,做到token的分布式一致性。 
然后,我们可以基于Spring AOP写一个切面类,用于拦截Controller类的方法,并从请求头中获取token,最后对token有效性进行判断。代码如下:

/**
 * Title:安全检查切面(是否登录检查) 
 * Description: 通过验证Token维持登录状态
 * 
 * @author rico
 * @created 2017年7月4日 下午4:32:34
 */
@Component
@Aspect
public class SecurityAspect {

    /** Log4j日志处理(@author: rico) */
    private static final Logger log = Logger.getLogger(SecurityAspect.class);

    private TokenManager tokenManager;

    @Resource(name = "tokenManager")
    public void setTokenManager(TokenManager tokenManager) {
        this.tokenManager = tokenManager;
    }

    @Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
    public Object execute(ProceedingJoinPoint pjp) throws Throwable {
        // 从切点上获取目标方法
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        log.debug("methodSignature : " + methodSignature);
        Method method = methodSignature.getMethod();
        log.debug("Method : " + method.getName() + " : "
                + method.isAnnotationPresent(IgnoreSecurity.class));
        // 若目标方法忽略了安全性检查,则直接调用目标方法
        if (method.isAnnotationPresent(IgnoreSecurity.class)) {
            return pjp.proceed();
        }

        // 从 request header 中获取当前 token
        String token = WebContextUtil.getRequest().getHeader(
                Constants.DEFAULT_TOKEN_NAME);
        // 检查 token 有效性
        if (!tokenManager.checkToken(token)) {
            String message = String.format("token [%s] is invalid", token);
            log.debug("message : " + message);
            throw new TokenException(message);
        }
        // 调用目标方法
        return pjp.proceed();
    }
}

若要使SecurityAspect生效,则需要在SpringMVC配置文件中添加如下Spring 配置:

 <!-- 启用注解扫描,并定义组件查找规则 ,mvc层只负责扫描@Controller、@ControllerAdvice -->
    <!-- base-package 如果多个,用“,”分隔 -->
    <context:component-scan base-package="cn.edu.tju.rico"
        use-default-filters="false">
        <!-- 扫描 @Controller -->
        <context:include-filter type="annotation"
            expression="org.springframework.stereotype.Controller" />
        <!-- 控制器增强,使一个Contoller成为全局的异常处理类,类中用@ExceptionHandler方法注解的方法可以处理所有Controller发生的异常 -->
        <context:include-filter type="annotation"
            expression="org.springframework.web.bind.annotation.ControllerAdvice" />
    </context:component-scan>

    <!-- 支持Controller的AOP代理 -->
    <aop:aspectj-autoproxy />

最后,别忘了在web.xml中添加允许的X-Token响应头,配置如下:

<init-param>  
    <param-name>allowHeaders</param-name>  
    <param-value>Content-Type,X-Token</param-value>  
</init-param>  

 

RESTful API基本概念和设计

REST全称是Representational State Transfer,翻译过来就是表现层状态转化。要想真正理解它的含义,从几个关键字入手:Resource, Representation, State Transfer

  • ##### Resource

资源就是网络上的实体,它可以是文字、图片、声音、视频或一种服务。但网络有这么多资源,该如何标识它们呢?你可以用URL来唯一标识和定位它们。只要获得资源对应的URL,你就可以访问它们。

  • ##### Representation

资源是一种信息实体,它有多种表示方式。比如,文本可以用.txt格式表示,也可以用xml、json或html格式表示。

  • ##### State Transfer

客户端访问服务端,服务端处理完后返回客户端,在这个过程中,一般都会引起数据状态的改变或转换。客户端操作服务端,都是通过HTTP协议,而在这个HTTP协议中,有几个动词:GET, POST, DELETEUPDATE

  • GET表示获取资源
  • POST表示新增资源
  • DELETE表示删除资源
  • UPDATE表示更新资源

理解RESTful核心概念后,我们来简单了解RESTful API设计以便可以看懂Designer News提供API。就拿Designer News获取Stories对应URL的一个例子来说明:客户端请求GET https://api-news.layervault.com/api/v1/stories?client_id=91a5fed537b58c60f36be1sdf71ed1320e9e4af2bda4366f7dn3d79e63835278

服务端返回结果

{ "stories": [ { "id": 46826, "title": "A Year of DuckDuckGo", "comment": "", "comment_html": null, "comment_count": 4, "vote_count": 17, "created_at": "2015-03-28T14:05:38Z", "pinned_at": null, "url": "https://news.layervault.com/click/stories/46826", "site_url": "https://api-news.layervault.com/stories/46826-a-year-of-duckduckgo", "user_id": 3334, "user_display_name": "Thomas W.", "user_portrait_url": "https://designer-news.s3.amazonaws.com/rendered_portraits/3334/original/portrait-2014-09-16_13_25_43__0000-333420140916-9599-7pse94.png?AWSAccessKeyId=AKIAI4OKHYH7JRMFZMUA&Expires=1459149709&Signature=%2FqqLAgqpOet6fckn4TD7vnJQbGw%3D", "hostname": "designwithtom.com", "user_url": "http://news.layervault.com/u/3334/thomas-wood", "badge": null, "user_job": "Online Designer at IDG UK", "sponsored": false, "comments": [ { "id": 142530, "body": "Had no idea it had those customization settings — finally making the switch.", "body_html": "<p>Had no idea it had those customization settings — finally making the switch.</p>\n", "created_at": "2015-03-28T18:41:37Z", "depth": 0, "vote_count": 0, "url": "https://api-news.layervault.com/comments/142530", "user_url": "http://news.layervault.com/u/3826/matt-soria", "user_id": 3826, "user_display_name": "Matt S.", "user_portrait_url": "https://designer-news.s3.amazonaws.com/rendered_portraits/3826/original/portrait-2014-04-12_11_08_21__0000-382620140412-5896-1udai4f.png?AWSAccessKeyId=AKIAI4OKHYH7JRMFZMUA&Expires=1459125745&Signature=%2BDdWMtto3Q10dd677sUOjfvQO3g%3D", "user_job": "Web Dood @ mattsoria.com", "comments": [] },
  • 协议用户与API通信采用HTTPs协议
  • 域名(domain name)应该尽可能部署到专用域名下https://api-news.layervault.com/,但有时会进一步扩展为https://api-news.layervault.com/api
  • 版本应该将API版本号v1放入URL
  • 路径路径https://api-news.layervault.com/api/v1/stories表示API具体网址,代表网络一种资源,所以不能有动词,只有使用名词来表示。
  • HTTP动词动词GET,表示从服务端获取Stories资源
  • 过滤信息(Filtering)?client_id=91a5fed537b58c60f36be1sdf71ed1320e9e4af2bda4366f7dn3d79e63835278指定client_id的Stories资源
  • 状态码(Status Codes)服务器向客户端返回表示成功或失败的状态码,状态码列表请参考Status Code Definitions
  • 错误处理(Error handling)服务端处理用户请求失败后,一般都返回error字段来表示错误信息
{ error: "Invalid client id"}

本文由金沙澳门官网发布于生命科学,转载请注明出处:使用REST风格完成MVC前后端分离,行为驱动开发

关键词: