node-webot / wechat

微信公共平台消息接口服务中间件
Other
5.11k stars 1.18k forks source link

对于API设计的一些想法 #181

Closed skinnyworm closed 9 years ago

skinnyworm commented 9 years ago

刚开始使用node, 可能有些建议不尽正确,请多包涵。

我觉得这个项目的设计还是有些可以改进的地方,其实分解开来看,在设计上可以更加模块化。

// 回复位置消息
responder.when({MsgType: 'location'}, function(message, reply){
  reply.text('You are at lat: ' + message.Location_X);
});

// 完整写法
responder.when(
   function(message){
     return message.MsgType === 'location';
 },
 function(message, reply){
    reply({
      msgType: 'text',
      content: 'You are at lat: ' + message.Location_X
    });
});
最终回复,如果无任何符合条件的回复,就使用最终回复。默认的最终回复仅回复空,通过Api可以修改。
responder
.when({MsgType: 'location'}, function(message, reply){
  reply.text('You are at lat: ' + message.Location_X);
}).
finally(function(message, reply){
  reply.text('Received your message");
});
消息处理机制,不管回复是什么,可以配置一个消息处理函数,比如将消息加入队列或存到数据库中。
responder
.when({MsgType: 'location'}, function(message, reply){
  reply.text('You are at lat: ' + message.Location_X);
}).
finally(function(message, reply){
  reply.text('Received your message");
}).
always(function(message, cb){
  // 'this' is the running context, could be an instance of a Model, messages is a hasMany relation
  this.messages.create({data: message}, cb); 
});

在以上的示例中,表示

var _ = require('lodash');
var ContentBuilder = require('./contentBuilder');
var async = require('async');

function Responder(){
  if (!(this instanceof Responder)) {
    return new Responder();
  }
  this.conditionHandlers = [];
  this.alwaysProcessor = null;
  // reply empty by default
  this.finalHandler = function(message, reply){reply();};
  return this;
}

/**
 * Create a handler which can response to a message that match the
 * condition.
 */
Responder.prototype.when = function(condition, handler){
  this.conditionHandlers.push({condition: condition, handler: handler});
  return this;
};

Responder.prototype.finally = function(handler){
  this.finalHandler = handler;
  return this;
};

Responder.prototype.always = function(processor){
  this.alwaysProcessor = processor;
  return this;
};

Responder.prototype.response = function(context, message, cb){
  var always = this.alwaysProcessor;
  var matched = _.find(this.conditionHandlers, function(v){return v.condition(message);}) || {handler: this.finalHandler};

  async.parallel({
    content: function(callback){
      matched.handler.bind(context)(message, ContentBuilder(message, callback));
    },
    always: function(callback){
      always ? always.bind(context)(message, callback) : callback(null,{});
    }
  }, cb);
};

module.exports = Responder;
skinnyworm commented 9 years ago

@JacksonTian 有没有可能将wechat-api, wechat, co-wechat重构成一个项目? 我可以提供loopback部分的集成。

JacksonTian commented 9 years ago
  1. 其实这样的API在现有的API上封装一下就能完成,不要太容易。
  2. wechat-api, wechat曾经是一个项目,被我拆分了。做且做好一件事情。co-wechat部分嘛,koa现在的用户还比较少。并且也不太适合你上面的想法。
JacksonTian commented 9 years ago

其实我内部也设计了一些类似语法糖的东东(权且叫API糖好了)。类似:

var Event = require('wechat').Event;
var events = new Event();
events.add('pic_weixin', function (message, req, res, next) {
   // 弹出微信相册发图器的事件推送
});
var handle = Event.dispatch(events);
app.use('/wechat', wechat(config).event(handle).middlewarify());
skinnyworm commented 9 years ago

@JacksonTian 好的,明白了,只是觉得现在几个项目的代码质量可能有待提高,提出的一些意见。不过如果您有时间仔细了解一下,可能会发现没那么容易。相同的功能,实现代码总量可以只有目前这个项目的1/3,且适用于不同应用场景, 比如API server, Web app, Mbass

我提出合并的初衷可能没有表达清楚,很大一部分在于code reuse, 而不是code copy。比如在一个Loopback的model api中同时完成api和responder功能,用户可以选择仅使用api或者是responder

var WechatService = require('../../index')

module.exports = function(Wechat){
  WechatService.bindApi(Wechat);

  WechatService.bindResponder(Wechat, function(responder){
    responder
    .always(function(message, cb){
      this.messages.create({content: message}, cb);
    })
    .finally(function(message, reply){
      reply({
        msgType: 'text',
        content: "Received :)"
      });
    });
  });
};
JacksonTian commented 9 years ago

拆分成小模块也就是为了基于module的级别进行code reuse呀。

JacksonTian commented 9 years ago

不是太熟悉loopback,但是如果是connect或者express的话,其实也能写出很简洁的逻辑代码的。不过我基本上把这部分交给用户自己去管理复杂度了。

skinnyworm commented 9 years ago

好的,理解了,以下是Loopback的api explorer, 用户可以在服务应用中直接使用model或绑定到restful api上

strongloop api explorer

JacksonTian commented 9 years ago

以群发消息为例,在现有API下,用loopback的话 你是怎么写的?

skinnyworm commented 9 years ago

在node应用中


// 单个公众号, promisify
wechat.broadcast({type:'mpvideo', data: videoMsg}).promise.then(function(result){// result from 微信});

// 多个公众号, callback
Wechat.findById(appid, function(err, wechat){
  if(err){return cb(err);}
  wechat.broadcast({type:'mpvideo', data: videoMsg}, cb);
});
// 以上代码可以用async.seq优化

绑定到Restful api

群发 POST http://server/wechats/{appid}/broadcast/ json: {"type": "mpvideo", "data": videoData}

查询群发消息 GET http://server/wechats/{appid}/broadcast/{messageId}/status

JacksonTian commented 9 years ago

这个地方是用的客户端模式:

api.massSendText(content, receivers, callback);

和直接调用api并没有多大的差别吧。

skinnyworm commented 9 years ago

对的,在api层面上,我仅仅封装了wechat-api,加上了对remoting的支持,重新定义了restful语意。不过我对api的实现还是有些意见的。

其实如果不封装wechat-api,我们可以在不需要写任何JS方法(除了accessToken部分)用remote datasource定义接口, 以下是对百度地图geocoder的定义


"baiduMap": {
    "name": "baiduMap",
    "connector": "rest",
    "debug": "true",
    "operations": [
      {
        "template": {
          "method": "GET",
          "url": "http://api.map.baidu.com/geocoder/v2/",
          "headers": {
            "accepts": "application/json",
            "content-type": "application/json"
          },
          "query": {
            "output": "json",
            "ak": "XiZd42Z2Iv3eIoeGL1xxxxxxxxx",
            "address": "{address}"
          }
        },
        "functions": {
          "geocode": [
            "address"
          ]
        }
      }
    ]
  }

定义好了,我们就可以直接使用

Baidu.geocode({address:'上海市淮海西路1080号'}, cb);
skinnyworm commented 9 years ago

@JacksonTian 不好意思,我主要是对回复(respond)这部分有些想法而已, API部分虽然啰嗦,但接口用法还是一致的。

JacksonTian commented 9 years ago

对wechat-api部分我觉得我这方面暂时不需要改什么。如果觉得不够promise,不够语义,另外封装即可。

对wechat这里,我原始提供的借口确实还有点原始。对业务逻辑的把握取决用户的驾驭能力,很有可能会写出比较罗嗦的代码出来。这部分欢迎一些新想法。

skinnyworm commented 9 years ago

以下是我对wechat-api的group部分接口的封装,其中apiMethod属性就是wechat-api中的方法。但是我修改了部分api方法,特别是文件上传这部分,由于使用file不适合绑定到restful后上传,所以改用了stream可以直接pipe到微信服务器上。

{
  "findGroups": {
    "apiMethod": "getGroups",
    "description": "获取分组列表",
    "accepts": [],
    "returns": {
      "arg": "groups",
      "type": "object",
      "root":true
    },
    "http":{
      "path": "/groups",
      "verb": "get"
    }
  },

  "createGroup":{
    "apiMethod": "createGroup",
    "description": "创建分组",
    "accepts": [{
      "arg": "name",
      "type": "string",
      "required": true,
      "http":{
        "source":"form"
      }
    }],
    "returns": {
      "arg": "group",
      "type": "object",
      "root":true
    },
    "http":{
      "path": "/groups",
      "verb": "post"
    }
  },

  "updateGroup":{
    "apiMethod": "updateGroup",
    "description": "更新分组名字",
    "accepts": [{
      "arg": "groupid",
      "type": "string",
      "required": true
    },{
      "arg": "name",
      "type": "string",
      "required": true,
      "http":{
        "source":"form"
      }
    }],
    "returns": {
      "arg": "result",
      "type": "object",
      "root":true
    },
    "http":{
      "path": "/groups/:groupid",
      "verb": "put"
    }
  },

  "deleteGroup":{
    "apiMethod": "removeGroup",
    "description": "删除分组",
    "accepts": [{
      "arg": "groupid",
      "type": "number",
      "required": true
    }],
    "returns": {
      "arg": "result",
      "type": "object",
      "root":true
    },
    "http":{
      "path": "/groups/:groupid",
      "verb": "delete"
    }
  }
}
JacksonTian commented 9 years ago

封装得挺好的。有点往config的方向去了。

calidion commented 9 years ago

@skinnyworm 比较赞同你的观点。

我也对这个项目的代码也不是很满意,原本准备fork的,但是现在是重新写了。 项目地址: https://github.com/JSSDKCN/node-weixin-api

目标就是你说的将所有的api集成到一个库里面。 由于现在API实现还不够完善,所以如果有兴趣可以一起增加自己需要的,一起改进代码。 基础功能应该是比较完善的,所以直接拿过来用就可以了。 具体实现参考原有的代码。

对于一些接收性质的http服务器API来说,只要参照expressjs标准去实现就可以了。 不必跟任何具体的服务相关联上。 欢迎提交代码。 :)