REST API即使通过网络进行调用的API接口,与具体的编程语言无关。现在常见的是通过标准的HTTP GET/POST请求,从服务器获取响应的资源或服务,服务器返回调用的结果内容,一般为xml格式或者json格式的数据(现在使用json的更多)。

在开发App的时候,一般原型设计好(如使用just in mind之类的工具)之后,我们会设计出与服务器交互的接口文档。一般情况下,App的开发进度(尤其原型)要快于服务器的开发进度。在App静态原型开发完到服务器实现所有的交互接口这段期间内,我们当然不能闲着。这时,我们可以本地模拟一个HTTP服务器,从而可以继续App的”动态化”开发。

由于对javascript比较熟悉,简单看了一下node js之后,就使用它来开发本地的HTTP服务器并提供各种交互的接口。这里记录一下是怎么一步一步实现的。

一步一步实现HTTP服务器

示例起见,杜撰了3个接口(无论多少个,原理都一样),如下:

焦点图
/sample_app/focus_pic

文章列表
/sample_app/article_list

文章详情
/sample_app/article_detail

我们可以将服务器划分为以下几个模块:

  1. 入口 - app.js,总体管理服务器。一般是启动服务器
  2. server模块 - server.js,负责服务器的配置与请求的转发。如服务器监听的端口,请求日志的记录,请求转发至具体的处理函数等
  3. router模块 - router.js,顾名思义,负责请求的路由功能。例如,我们在这里可以将不同的请求地址转发至不同的函数处理。
  4. request handlers模块 - request_handler.js,针对每个请求进行处理的函数都定义在这个模块里边。
  5. response template模块,由于我们只是快速模拟提供Web API服务的HTTP服务器,所以真正返回的内容写在模板里边即可。

其实3个接口的路由(将不同的请求转到对应的处理函数)按照道理应该在router模块中进行处理,但是因为针对每个接口的处理逻辑都是相同的,只是返回的内容不同,我就把路由逻辑转到request handlers模块中去了。具体如何写,可以根据实际的情况进行调整,这里边只是提供一个思路而已。

单看文字还是比较晦涩的,我们来看一下具体的代码:
server.js

/**
 * Created by FIMH on 2016/05/05.
 */
var http = require('http');
var url = require('url');

function start(route, handle) {
    function onRequest(request, response) {
        // 获取请求路径
        var parsedUrl = url.parse(request.url);
        var pathname = parsedUrl.pathname;

        console.log('Request for ' + pathname + ' received.');

        route(handle, parsedUrl, request, response);
    }

    http.createServer(onRequest).listen(9999);
    console.log('Server has started.');
}

exports.start = start;

可以看到,主要配置了HTTP服务器监听的端口 - 9999,以及打印了一条log信息,然后将请求转至router模块进行处理

router.js

/**
 * Created by FIMH on 2016/05/05.
 */
// 针对不同的请求,做出不同的相应
function route(handler, parsedUrl, request, response) {
    var pathname = parsedUrl.pathname;
    console.log('About to route a request for ' + pathname);

    // 禁止访问favicon.ico
    if (!pathname.indexOf('/favicon.ico')) {
        return;
    }

    // 这里不用检查请求路径是否正确,将路由放到handle对应的函数中去
    handler(parsedUrl, request, response);
}

exports.route = route;

这里我们主要拦截了对favicon.ico文件的访问,关于这个文件是什么,大家可以自行搜索。
前面也提到了,由于这个sample里边,每个接口的处理逻辑都是相同的,只是返回的内容不同,我就把路由逻辑转到request handlers模块中去了。

真正的处理逻辑都在下面这个模块中
requests_handlers.js

/**
 * 请求处理入口。
 */
function handleRequests(parsedUrl, request, response) {
    // 解码并解析querystring
    //var queryStringUtil = require('querystring');
    //var queryString = parsedUrl.query;
    //var queryStringResultObject = queryStringUtil.parse(queryString);

    var pathname = parsedUrl.pathname;

    // 在这里进行处理
}

exports.handleRequests = handleRequests;

这里,我只贴了一个请求处理的入口函数。
在这个地方我重构了一次,最初的处理逻辑大致如下:

var templateName;
    var innerHtml;
    if (pathname == '/sample_app/focus_pic') {
       templateName = 'focus_pic';
    } else if (pathname == '/sample_app/article_list') {
        templateName = 'article_list';
    } else if (pathname == '/sample_app/article_detail') {
        templateName = 'article_detail';
        innerHtml = 'article';
    }

    if (templateName) {
        handleValidRequest(request, response, templateName, innerHtml);
    } else {
        handleErrorOutput(request, response, 400, 'Invalid request url!');
    }

因为这里只是3个请求,看着还不明显,如果比较多了,那么if…else写起来就太烦了,这时候我想起来好多js语言的项目(如cocos 2d-js,egret)都会使用json文件作为项目的配置文件,依次来简化代码并提高灵活性。

这时,我们可以定义一个项目的配置文件,我这里取名叫appProperties.json,内容如下

{
  "route": {
    "/sample_app/focus_pic": {
      "template": "focus_pic"
    },
    "/sample_app/article_list": {
      "template": "article_list"
    },
    "/sample_app/article_detail": {
      "template": "article_detail",
      "inner_html": "article"
    }
  }
}

然后我们修改前面提到的模块 - requests_handlers.js
先定义一个全局变量var routeObj;
然后在函数handleRequests里这样处理:

// 解析route配置信息
    if (!routeObj) {
        var fs = require('fs');
        var propertiesPath = './appProperties.json';
        var propertiesData = fs.readFileSync(propertiesPath, 'utf-8');

        routeObj = JSON.parse(propertiesData);
    }

    var templateObj = routeObj['route'][pathname];
    if (templateObj) {
        handleValidRequest(request, response, templateObj['template'], templateObj['inner_html']);
    } else {
        handleErrorOutput(request, response, 400, 'Invalid request url!');
    }

重构之后,无论接口有多少个,处理的代码依然是这几行;而重构前的方法,每添加一个接口,都需要增加一个 else if
拿算法复杂度的概念来比对,重构前就是O(n),而重构后则为O(1)。

关于response template模块的处理,这里就不贴代码了。主要是使用node js同步或者异步读取模板文件,还有对json对象的序列化,编辑与反序列化。大家有兴趣的话可以看整个sample的源代码,见文章底部。

总结与源代码

如果使用传统的方式,你需要安装一个http服务器 - 如apache,还有一个语言处理模块 - 如php。
而使用了node js之后,你只需要安装一个node运行时,剩下的http服务器,请求解析,处理,返回等全部使用js来进行编写即可,而且书写的代码量也很小。