arccode

arccode 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑

积累技术,努力工作,坚持学习,认真生活。

个人动态

arccode 发布了文章 · 2016-05-03

微信支付SDK-两行代码解决支付

背景

让使用微信支付的朋友最快速度接入微信支付.

核心

两行代码解决微信支付提供的各种服务, 开箱即用, 可扩展性超强(只需根据服务的上下行协议定义协议类后, 放入工厂即可获取调用结果).

架构图

项目源代码

目前支持的服务及调用示例

所有服务在单元测试类(WXPayClientTest.java)中均已测试通过, 下行参数response.isSuccess == true表示服务调用成功.

扫码支付

文档详见: https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_1

String nonceStr = SDKUtils.genRandomStringByLength(32);
UnifiedOrderRequest request = new UnifiedOrderRequest("wuspace-899",SDKUtils.genOutTradeNo(),1, "192.168.1.1", asyncNotifyUrl, "NATIVE", nonceStr);
UnifiedOrderResponse response = wxPayClient.execute(request);
Assert.assertNotNull(response);
LOG.info(JSON.toJSONString(response));

公众号支付

文档详见: https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_1

String nonceStr = SDKUtils.genRandomStringByLength(32);
UnifiedOrderRequest request = new UnifiedOrderRequest("wuspace-899",SDKUtils.genOutTradeNo(),
                1, "192.168.1.1", asyncNotifyUrl, "JSAPI", nonceStr);
request.setOpenId("oKVmeuHht8J0Ni58CSNe474AHA3E");
UnifiedOrderResponse response = wxPayClient.execute(request);
Assert.assertNotNull(response);
LOG.info(JSON.toJSONString(response));

APP支付

文档详见: https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_1

String nonceStr = SDKUtils.genRandomStringByLength(32);
UnifiedOrderRequest request = new UnifiedOrderRequest("wuspace-899",SDKUtils.genOutTradeNo(),
                1, "192.168.1.1", asyncNotifyUrl, "APP", nonceStr);
UnifiedOrderResponse response = wxPayClient.execute(request);
Assert.assertNotNull(response);
LOG.info(JSON.toJSONString(response));

商家支付

文档详见: https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_2

String nonceStr = SDKUtils.genRandomStringByLength(32);
String customerOpenId = "oKVmeuHht8J0Ni58CSNe474AHA3E";
MchPayRequest mchPayRequest = new MchPayRequest(SDKUtils.genOutTradeNo(),
                customerOpenId, "NO_CHECK", 100, "xxxx年xx月结算", "192.168.1.1", nonceStr);
MchPayResponse response = wxPayVIPClient.execute(mchPayRequest);
Assert.assertNotNull(response);
LOG.info(JSON.toJSONString(response));

退款

文档详见: https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_4&index=6

String nonceStr = SDKUtils.genRandomStringByLength(32);
RefundRequest request = new RefundRequest("T15121416014891124211768",
                SDKUtils.genOutRefundNo(), 1, 1, "112102020", nonceStr);
RefundResponse response = wxPayVIPClient.execute(request);
Assert.assertNotNull(response);
LOG.info(JSON.toJSONString(response));

支付异步通知解析

文档详见: https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_7

String notifyTxt = "<xml>\n" +
                "  <appid><![CDATA[wx2421b1c4370eccdcd]]></appid>\n" +
                "  <attach><![CDATA[支付测试]]></attach>\n" +
                "  <bank_type><![CDATA[CFT]]></bank_type>\n" +
                "  <fee_type><![CDATA[CNY]]></fee_type>\n" +
                "  <is_subscribe><![CDATA[Y]]></is_subscribe>\n" +
                "  <mch_id><![CDATA[10000100]]></mch_id>\n" +
                "  <nonce_str><![CDATA[5d2b6c2a8db53831f7eda20af46e531c]]></nonce_str>\n" +
                "  <openid><![CDATA[oUpF8uMEb4qRXf22hE3X68TekukE]]></openid>\n" +
                "  <out_trade_no><![CDATA[1409811653]]></out_trade_no>\n" +
                "  <result_code><![CDATA[SUCCESS]]></result_code>\n" +
                "  <return_code><![CDATA[SUCCESS]]></return_code>\n" +
                "  <sign><![CDATA[B552ED6B279343CB493C5DD0D78AB241]]></sign>\n" +
                "  <sub_mch_id><![CDATA[10000100]]></sub_mch_id>\n" +
                "  <time_end><![CDATA[20140903131540]]></time_end>\n" +
                "  <total_fee>1</total_fee>\n" +
                "  <trade_type><![CDATA[JSAPI]]></trade_type>\n" +
                "  <transaction_id><![CDATA[1004400740201409030005092168]]></transaction_id>\n" +
                "</xml>";
PayNotifyResponse response = wxPayClient.parseNotify(notifyTxt, PayNotifyResponse.class);
Assert.assertNotNull(response);
LOG.info(JSON.toJSONString(response));

刷卡支付

文档详见: https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=9_10&index=1

目前公司未使用, 待续......

扩展

该SDK设计了一个服务工厂, 该工厂中包含HTTP执行器/返回数据解析方式(json/xml)/入参数据格式(json/xml)构造等, 开发人员需要增加服务仅需要根据服务协议文档编写上下行协议, 并在协议中指明API接口和返回数据类型, 再将上行协议放入工厂中执行即可; 可参考已完成的服务协议进行扩展编写.

本系列文章

查看原文

赞 4 收藏 35 评论 3

arccode 赞了文章 · 2015-05-18

盘点JavaScript里好用的原生API ꒰・◡・๑꒱

这段时间翻了一番JavaScript的api,发现不少好的轮子,省去造的麻烦了。

直接进入正题

解析字符串对象

我们都知道,JavaScript对象可以序列化为JSON,JSON也可以解析成对象,但是问题是如果出现了一个既不是JSON也不是对象的"东西",转成哪一方都不方便,那么eval就可以派上用场

var obj = "{a:1,b:2}";   // 看起来像对象的字符串
eval("("+ obj +")")      // {a: 1, b: 2}

因为 eval 可以执行字符串表达式,我们希望将 obj 这个字符串对象 执行成真正的对象,那么就需要用eval。但是为了避免eval 将带 {} 的 obj 当语句来执行,我们就在obj的外面套了对 (),让其被解析成表达式。

& (按位与)

判断一个数是否为2的n次幂,可以将其与自身减一相与

var number = 4
(number & number -1) === 0 // true

^ (按位异或)

不用第三个变量,就可以交换两个变量的值
var a = 4,b = 3
a = a ^ b  //    7
b = a ^ b  //    4
a = a ^ b  //    3

格式化Date

想得到format后的时间?现在不用再get年月日时分秒了,三步搞定

var temp = new Date();
var regex = /\//g;
(temp.toLocaleDateString() + ' ' + temp.toLocaleTimeString().slice(2)).replace(regex,'-');

// "2015-5-7 9:04:10"

想将format后的时间转换为时间对象?直接用Date的构造函数

new Date("2015-5-7 9:04:10");

// Thu May 07 2015 09:04:10 GMT+0800 (CST)
经测试发现火狐没法对format后的时间字符串使用Date.parse(),故这个方法在火狐上不好使

想将一个标准的时间对象转换为unix时间戳?valueOf搞定之

(new Date).valueOf();

// 1431004132641

许多朋友还提醒了这样可以快速得到时间戳

+new Date
// 1431004132641

一元加

一元加可以快速将字符串的数字转换为数学数字,即
var number = "23" 
typeof number  // string
typeof +number // number

可以将时间对象转为时间戳
new Date  //  Tue May 12 2015 22:21:33 GMT+0800 (CST)
+new Date //  1431440459887

转义URI

需要将url当做参数在路由中传递,现在转义之

var url = encodeURIComponent('http://segmentfault.com/questions/newest')

// "http%3A%2F%2Fsegmentfault.com%2Fquestions%2Fnewest"

再反转义

decodeURIComponent(url)
// "http://segmentfault.com/questions/newest"

Number

希望保留小数点后的几位小数,不用再做字符串截取了,toFixed拿走
number.toFixed()     // "12346"
number.toFixed(3)    // "12345.679"
number.toFixed(6)    // "12345.678900"

参数范围为0~20,不写默认0

类型检测

typeof是使用最频繁的类型检测手段

typeof 3        // "number"
typeof "333"    // "string"
typeof false    // "boolean"

对于基本(简单)数据类型还是挺好的,但是一旦到了引用数据类型的时候,就不那么好使了

typeof new Date()   // "object"
typeof []           // "object"
typeof {}           // "object"
typeof null         // "object"      

前三个还能忍,null居然也返回object,你是在逗我吗!!!(ps:其实这是JavaScript的bug 人艰不拆 ꒰・◡・๑꒱ )

这时,我们会使用instanceof

toString instanceof Function
// true
(new Date) instanceof Date
// true
[] instanceof Object
// true
[] instanceof Array
// true

其实我们可以发现,[] 和 Object得到了true,虽然我们知道,[]也是对象,但是我们希望一个能更准确的判断类型的方法,现在它来了

使用Object.prototype.toString()来判断,为了让每一个对象都能通过检测,我们需要使用Function.prototype.call或者Function.prototype.apply的形式来调用

var toString = Object.prototype.toString;

toString.call(new Date)    // "[object Date]"
toString.call(new Array)   // "[object Array]"
toString.call(new Object)  // "[object Object]"
toString.call(new Number)  // "[object Number]"
toString.call(new String)  // "[object String]"
toString.call(new Boolean) // "[object Boolean]"

要注意的是:toString方法极有可能被重写,所以需要使用的时候,
可以直接使用Object.prototype.toString()方法

实现继承

看一个官方给的例子
//Shape - superclass
function Shape() {
  this.x = 0;
  this.y = 0;
}

Shape.prototype.move = function(x, y) {
    this.x += x;
    this.y += y;
    console.info("Shape moved.");
};

// Rectangle - subclass
function Rectangle() {
  Shape.call(this); //call super constructor.
}

Rectangle.prototype = Object.create(Shape.prototype);

var rect = new Rectangle();

rect instanceof Rectangle //true.
rect instanceof Shape //true.

rect.move(1, 1); //Outputs, "Shape moved."

通过call来获取初始化的属性和方法,通过Object.create来获取原型对象上的属性和方法

迭代

ES5出了挺多的迭代函数,如map,filter,some,every,reduce等,这里有传送门,可以看到挺多的例子

http://segmentfault.com/a/1190000002687651

Array

具体的api这里介绍的很详细。

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Glob...

这里就提几句:
pop,push,reverse,shift,sort,splice,unshift会改变原数组

join,concat,indexOf,lastIndexOf,slice,toString不会改变原数组

map,filter,some,every,reduce,forEach这些迭代方法不会改变原数组

几个注意点:

1 shift,pop会返回那个被删除的元素
2 splice 会返回被删除元素组成的数组,或者为空数组
3 push 会返回新数组长度
4 some 在有true的时候停止
5 every 在有false的时候停止
6 上述的迭代方法可以在最后追加一个参数thisArg,它是执行 callback 时的 this 值。
查看原文

赞 21 收藏 166 评论 34

arccode 赞了回答 · 2015-04-20

java web开发,报表工具的选择

使用过POI, dxhtmlgrid

关注 15 回答 10

arccode 发布了文章 · 2015-04-20

前后端完全分离之 API 设计

背景

API 就是开发者使用的界面。我的目标不仅是能用,而且好用,跨平台(PC, Android, IOS, etc...)使用。本文将详细介绍 API 的设计及异常处理,并将异常信息进行封装友好地反馈给前端.

上篇文章 前后端完全分离初探 只是讲了些宽泛的概念,接下来的文章将直接上干货,干货的源码会挂在 GitHub 上。

前后端完全分离后,前端和后端如何交互?答:通过双方协商好的API。

接下来我分享我自己设计的 API 接口,欢迎各位朋友指教。

API 设计理念

  1. 将涉及的实体抽象成资源,即按 id 访问资源,在 url 上做文章,以后再也不用为 url 起名字而苦恼了;

  2. 使用 HTTP 动词对资源进行 CRUD(增删改查):

    get -> 查, post -> 增, put -> 改, delete -> 删
  3. URL 命名规则,对于资源无法使用一个单数名词表示的情况,我使用中横线 - 连接

    • 资源采用名词命名,e.g:产品 -> product

    • 新增资源,e.g:新增产品 url -> /product, verb -> POST

    • 修改资源,e.g:修改产品 url -> /products/{id}, verb -> PUT

    • 资源详情,e.g:指定产品详情 url -> /products/{id}, verb -> GET

    • 删除资源,e.g:删除产品 url -> /products/{id}, verb -> DELETE

    • 资源列表,e.g:产品列表 url -> /products, verb -> GET

    • 资源关联关系,e.g:收藏产品 url -> /products/{id}/star, verb -> PUT

    • 资源关联关系,e.g:删除收藏产品 url -> /products/{id}/star, verb -> DELETE

目前我 API 的设计只涉及这两点,至于第三点 HATEOAS(Hypermedia As The Engine Of Application State) 那就由读者自己去选择了,

<!--more-->

项目地址

本文中只涉及了设计的理念,具体的实现请下载源码 rest-api,项目内写了比较详细的注释。

项目实战

实战将从业务场景出发,详细介绍如何使用 HTTP verb 对资源进行操作(状态转移),使用 JSON 返回结果(资源表述),并定义 JSON 的基础结构。

JSON 结构

requestParams:

{
}

responseBody:

{
  "meta": {
  },
  "data": {
  }
}

meta 中封装操作成功或失败的消息,data 中封装返回的具体数据。

当新建商品或更新产品时,相关属性封装在 JSON 中,通过 POST 或 PUT 发送。

{
  "name": "Apple Watch SPORT",
  "description": "Sport 系列的表壳材料为轻巧的银色及深空灰色阳极氧化铝金属,强化 Ion-X 玻璃材质为显示屏提供保护。搭配高性能 Fluoroelastomer 表带,共有 5 款缤纷色彩。"
}

当用户对商品进行操作后,将得到响应结果。GET, POST, PUT 操作成功,返回如下结果

{
  "meta": {
    "code": 201,
    "message": "创建成功"
  },
  "data": {
    "id": "5308e9c2-a4ce-4dca-9373-cc1ffe63d5f9",
    "name": "Apple Watch SPORT",
    "description": "Sport 系列的表壳材料为轻巧的银色及深空灰色阳极氧化铝金属,强化 Ion-X 玻璃材质为显示屏提供保护。搭配高性能 Fluoroelastomer 表带,共有 5 款缤纷色彩。"
  }
}

DELETE 操作成功,返回如下结果

{
  "meta": {
    "code": 204,
    "message": "删除成功"
  }
}

业务场景一

电商网站的管理员对商品进行新增、编辑、删除、浏览的操作,暂时不考虑认证授权,只关注对商品的操作。

为了以后便于做分布式,所有资源 id(表主键)均采用 uuid。

新增商品
  1. url: /api/product

  2. method: POST

  3. requestParams:

    {
      "name": "Apple Watch SPORT",
      "description": "Sport 系列的表壳材料为轻巧的银色及深空灰色阳极氧化铝金属,强化 Ion-X 玻璃材质为显示屏提供保护。搭配高性能 Fluoroelastomer 表带,共有 5 款缤纷色彩。"
    }
  4. responseBody

    {
      "meta": {
        "code": 201,
        "message": "创建成功"
      },
      "data": {
        "id": "5308e9c2-a4ce-4dca-9373-cc1ffe63d5f9",
        "name": "Apple Watch SPORT",
        "description": "Sport 系列的表壳材料为轻巧的银色及深空灰色阳极氧化铝金属,强化 Ion-X 玻璃材质为显示屏提供保护。搭配高性能 Fluoroelastomer 表带,共有 5 款缤纷色彩。"
      }
    }
编辑商品
  1. url: /api/products/{id}

  2. method: PUT

  3. requestParams:

    {
      "name": "iPhone 6",
      "description": "此次苹果发布会发布了iPhone 6与iPhone 6 Plus,搭载iOS 8,尺寸分别是4.7和5.5英寸。外观设计不再棱角分明,表层玻璃边有一个弧度向下延伸,与阳极氧化铝金属机身边框衔接。机身背部采用三段式设计。机身更薄,续航能力更强。"
    }
  4. responseBody

    {
      "meta": {
        "code": 200,
        "message": "修改成功"
      },
      "data": {
        "id": "5308e9c2-a4ce-4dca-9373-cc1ffe63d5f9",
        "name": "iPhone 6",
        "description": "此次苹果发布会发布了iPhone 6与iPhone 6 Plus,搭载iOS 8,尺寸分别是4.7和5.5英寸。外观设计不再棱角分明,表层玻璃边有一个弧度向下延伸,与阳极氧化铝金属机身边框衔接。机身背部采用三段式设计。机身更薄,续航能力更强。"
      }
    }
删除商品
  1. url: /api/products/{id}

  2. method: DELETE

  3. responseBody

    {
      "meta": {
        "code": 204,
        "message": "删除成功"
      },
      "data": {}
    }
获取商品详情
  1. url: /api/products/{id}

  2. method: GET

  3. responseBody:

    //删除前
    
    {
      "meta": {
        "code": 200,
        "message": "查询成功"
      },
      "data": {
        "id": "5308e9c2-a4ce-4dca-9373-cc1ffe63d5f9",
        "name": "Apple Watch SPORT",
        "description": "Sport 系列的表壳材料为轻巧的银色及深空灰色阳极氧化铝金属,强化 Ion-X 玻璃材质为显示屏提供保护。搭配高性能 Fluoroelastomer 表带,共有 5 款缤纷色彩。"
      }
    }
    
    //删除后
    {
      "meta": {
        "code": 404,
        "message": "指定产品不存在"
      }
    }
获取商品列表(未分页)
  1. url: /api/products

  2. method: GET

  3. responseBody

    {
      "meta": {
        "code": 200,
        "message": "获取全部商品成功"
      },
      "data": [
        {
          "id": "5308e9c2-a4ce-4dca-9373-cc1ffe63d5f9",
          "name": "Apple Watch SPORT",
          "description": "Sport 系列的表壳材料为轻巧的银色及深空灰色阳极氧化铝金属,强化 Ion-X 玻璃材质为显示屏提供保护。搭配高性能 Fluoroelastomer 表带,共有 5 款缤纷色彩。"
        },
        {
          "id": "9db1992a-c342-4ff0-a2a4-aeb3dbfd93f6",
          "name": "Apple Watch SPORT",
          "description": "Sport 系列的表壳材料为轻巧的银色及深空灰色阳极氧化铝金属,强化 Ion-X 玻璃材质为显示屏提供保护。搭配高性能 Fluoroelastomer 表带,共有 5 款缤纷色彩。"
        },
        {
          "id": "4481619b-45c5-4729-9539-f93bb01f10d8",
          "name": "Apple Watch SPORT",
          "description": "Sport 系列的表壳材料为轻巧的银色及深空灰色阳极氧化铝金属,强化 Ion-X 玻璃材质为显示屏提供保护。搭配高性能 Fluoroelastomer 表带,共有 5 款缤纷色彩。"
        }
      ]
    }

业务场景二

业务场景一中只涉及了单个资源的操作,但实际场景中还有些关联操作;如用户去电商网站浏览商品,并收藏了一些商品,之后又取消收藏了部分商品。

暂时不考虑用户认证授权,以后加了token后,用户信息可以从中获取。

收藏商品
  1. url: /api/products/{id}/star

  2. method: PUT

  3. responseBody

    {
      "meta": {
        "code": 200,
        "message": "收藏商品[5308e9c2-a4ce-4dca-9373-cc1ffe63d5f9]成功"
      },
      "data": [
        {
          "id": "5308e9c2-a4ce-4dca-9373-cc1ffe63d5f9",
          "name": "iPhone 6",
          "description": "此次苹果发布会发布了iPhone 6与iPhone 6 Plus,搭载iOS 8,尺寸分别是4.7和5.5英寸。外观设计不再棱角分明,表层玻璃边有一个弧度向下延伸,与阳极氧化铝金属机身边框衔接。机身背部采用三段式设计。机身更薄,续航能力更强。"
        }
      ]
    }
取消收藏商品
  1. url: /api/products/{id}/star

  2. method: DELETE

  3. responseBody

    {
      "meta": {
        "code": 200,
        "message": "删除收藏商品[5308e9c2-a4ce-4dca-9373-cc1ffe63d5f9]成功"
      },
      "data": []
    }

自定义异常和异常处理

所有自定义异常继承 RuntimeException,在业务层抛出,统一在 Controller 层进行处理。

异常分为全局异常和局部异常,例如 http method unsupported (405),unauthorized (401),accessDenied (403),not found (404) 等属于全局异常。针对对独立业务的一些异常属于局部异常,例如产品编辑出错。

异常在 Controller 中进行处理,并封装成 json 返回给前端,封装后的数据如下,相关实现见源码

{
  "meta": {
    "code": 404,
    "message": "指定产品不存在"
  }
}
{
  "meta": {
    "code": 405,
    "message": "Request method 'POST' not supported"
  }
}

项目运行截图部分

本系列文章

  • 前后端完全分离初探

  • 前后端完全分离之API设计

  • 前后端完全分离之安全认证与授权-上

  • 前后端完全分离之安全认证与授权-下

  • 前后端完全分离之前端模块化开发

  • 前后端完全分离之前端路由系统

  • 前后端完全分离之后端面向服务的模块化开发

原文出自 前后端完全分离之API设计,欢迎转载,转载请注明出处。

查看原文

赞 26 收藏 204 评论 12

arccode 赞了文章 · 2015-04-10

一种SPA(单页面应用)架构

未经允许,请勿转载。本文同时也发布在我的博客


(如果对SPA概念不清楚的同学可以先自行了解相关概念)

平时喜欢做点小页面来玩玩,并且一直采用单页面应用(Single Page Application)的方式来进行开发。这种开发方式是在之前一年做的一个创业项目的经验和思考,一直想写篇博客来总结一下。

个人认为单页面应用的优势相当明显:

  1. 前后端职责分离,架构清晰:前端进行交互逻辑,后端负责数据处理。
  2. 前后端单独开发、单独测试。
  3. 良好的交互体验,前端进行的是局部渲染。避免了不必要的跳转和重复渲染。

当然,SPA也有它自身的缺点,例如不利于搜索引擎优化等等,这些问题也有其相应的解决方案。

下面要介绍的这种方式可以说是一种模式或者工作流,和前端使用什么框架无关,也和后端使用什么语言、数据库无关。不能说是The Best Practice,我相信经过更多人的讨论和思考会有A Better Practice。:)

概览

下图展示了这种模式的整个前后端及各自的主要组成:

overview

看起来有点复杂,接下来会仔细地对上面每一个部分进行解释。看完本文,就应该能理解上图中的各部件之间的交互流程。

前端架构

把上图的前端部分单独抽出来进行研究:

simple-front

前端中大致分为四种类型的模块:

  1. components:前端UI组件
  2. services:前端数据缓存和操作层
  3. databus:封装一系列Ajax操作,和后端进行数据交互的部件
  4. common/utils:以上组件的共用部件,可复用的函数、数据等

components

component指的是页面上的一个可复用UI交互单元,例如一个博客的评论功能:

comment

我们可以把博客评论做为一个组件,这个组件有自己的结构(html),外观(css),交互逻辑(js),所以我们可以单独做一个叫comment的component,由以下文件组成:

  1. comment.html
  2. comment.css
  3. comment.js

(每个component可以想象成一个工程,甚至可以有自己的README、测试等)

components tree

一个component可以依赖另外一个component,这时候它们是父子关系;component之间也可以互相组合,它们就是兄弟关系。最后的结果就类似DOM tree,component可以组成components tree。

例如,现在要给这个博客添加两个功能:

  1. 显示评论回复。
  2. 鼠标放到评论或者回复的用户头像上可以显示用户名片。

comments-replies-poputbox

我们构建两个组件,reply和user-info-card。因为每个comment都要有自己的回复列表,所以comment组件是依赖于reply组件的,comment和reply组件是嵌套关系。

而user-info-card可以出现在comment或者reply当中,并且为了以后让user-info-card复用性更强,它应该不属于任何一个组件,它和其他组件是组合关系。所以我们就得到一个简单的componenets tree:

components-tree

components之间的通信

怎么可以做到鼠标放到评论和回复的用户头像上显示名片呢?这其实牵涉到组件之间是如何进行通信的问题。

最佳的方式就是使用事件机制,所有组件之间可以通过一个叫eventbus通用组件进行信息的交互。所以,要做到上述功能:

  1. user-info-card可以在eventbus监听一个user-info-card:show的事件。
  2. 而当鼠标放到comment和reply组件的头像上的时候,组件可以使用eventbus触发user-info-card:show事件。

user-info-card:

var eventbus = require("eventbus")
eventbus.on("user-info-card:show", function(user) {
    // 显示用户名片
})

comment or reply:

var eventbus = require("eventbus")
$avatar.on("mouseover", function(event) {
    eventbus.emit("user-info-card:show", userData)
})

components之间用事件进行通信的优势在于:

  1. 组件之间没有强的依赖,组件之间被解耦。
  2. 组件之间可以单独开发、单独测试。数据和事件都可以简单的进行伪造进行测试(mocking)。

总结:component之间有嵌套和组合的关系,构成components tree;component之间通过事件进行信息、数据的交换。

services

component的渲染和显示依赖于数据(model)。例如上面的评论,就会有一个评论列表的model。

comments: [
    {user:.., content:.., createTime: ..}, 
    {user:.., content:.., createTime: ..}, 
    {user:.., content:.., createTime: ..}
]

每个评论的component会对应一个comment(comments数组中的对象)进行渲染,渲染完以后就会正确地显示在页面上。

因为可能在其他component中也会需要用到这些数据,所以comment component不会自己直接保存这些comment model。这些model都会保存在service当中,而component会从service拿取数据。components和services之间是多对多的关系:一个component可能会从不同的services中拿取数据,而一个service可能为多个components提供数据。

services除了用于缓存数据以外,还提供一系列对数据的一些操作接口。可以提供给components进行操作。这样的好处在于保持了数据的一直性,假如你使用的是MVVM框架进行component的开发,对数据的操作还可以直接对多个视图产生数据绑定,当services中的数据变化了,多个components的视图也会相应地得到更新。

总结:services是对前端数据(也就是model)的缓存和操作。

databus

而services中缓存的数据是从哪里来的呢?当然也许想到的第一个方案是在services中直接发送Ajax请求去服务器中拉去数据。而这里建议不直接这样做,而是把各种和后端的API进行交互的接口封装到一个叫databus的模块当中,这里的databus相当于是“对后端数据进行原子操作的集合”。

如上面的comment service需要从后端进行拉取数据,它会这样做:

var databus = require("databus")
var comments = null
databus.getAllComments(function(cmts) { // 调用databus方法进行数据拉取
    comments = cmts
})

而databus中则封装了一层Ajax

databus.getAllCommetns = function(callback) {
    utils.ajax({
        url: "/comments",
        method: "GET",
        success: callback
    })
}

这样做是因为,不同的services之间可能会用到同样的接口对后端进行操作,把操作封装起来可以提高接口的复用性。注意,如果databus中的某些操作不涉及到servcies的数据,这操作也可以被components所调用(例如退出、登录等)。

总结:databus封装了提供给services和component和后端API进行交互的接口。

common/utils

这两个模块都可以被其他组件所依赖。

common,故名思议,组件之间的共用数据和一些程序参数可以缓存在这里。

utils,封装了一些可复用的函数,例如ajax等。

eventbus

所有组件(特别是components之间)的通过事件机制进行数据、消息通信的接口。可以简单地使用EventEmitter这个库来实现。

后端架构

传统的网页页面一般都是由后端进行页面的渲染,而在我们的架构当中,后端只渲染一个页面,其后,后端只是相当于一个Web Service,前端使用Ajax调用其接口进行数据的调取和操作,使用数据进行页面的渲染。

这样的好处就是,后端不仅仅能处理Web端的页面的请求,而且处理提供移动端、桌面端的请求或者作为第三方开放接口来使用。大大提高后端处理请求的灵活性。

后端对比起前端的架构来说会简单很多,但是这只是其中一种模式,对于不同复杂程度的应用可能会做相应的调整。后端大概分为三层:

server

  1. CGI:设置不同的路由规则,接受前端来的请求,处理数据,返回结果。
  2. business:这一层封装了对数据库的一些操作,business可以被CGI所调用。
  3. database:数据库,进行数据的持久化。

例如上面的comments的例子,CGI可以接收到前端发送的请求:

var commentsBusiness = require("./businesses/comments")
app.get("/comments", function(req, res) {
    // 此处调用comments的business数据库操作
    commentsBusiness.getAllComments(function(comments) {
        // 返回数据结果
        res.json(comments)
    })
})

后端的API可以采用更规范的RESTful API的方式,而RESTful不在本文的讨论范围内。有兴趣的可以参考Best Practices for Designing a Pragmatic RESTful API

前后端的架构都基本清晰了,我们来看看文章开头的图:

overview

看着图来,我们总结一下整个前后端的交互流程:

  1. 前端向服务端请求第一个页面,后端渲染返回。
  2. 前端加载各个component,components从services拿数据,services通过databus发送Ajax请求向后端取数据。
  3. 后端的CGI接收到前端databus发送过来的请求,处理数据,调用business操作数据库,返回结果。
  4. 前端接收到后端返回的结果,把数据缓存到service,component拿到数据进行前端组件的渲染、显示。

工作流

一个好的工作流可以让开发事半功倍。上面的这种单页面应用也有其相应的一种开发工作流,当然这种工作流也适合非单页面应用:

  1. 进行产品功能、原型设计。
  2. 后端数据库设计。
  3. 根据产品确定前后端的API(or RESTful API),以文档方式纪录。
  4. 前后端就可以针对API文档同时进行开发。
  5. 前后端最后进行连接测试。

前后端分离开发。建议都可以采用TDD(测试驱动开发)的方式来单独测试、单独开发(关于Web APP测试这一块可以单独进行讨论研究),提高产品的可靠性、稳定性。

(完)

查看原文

赞 25 收藏 217 评论 25

arccode 赞了回答 · 2015-04-10

前后端分离用什么通讯?

expresskoa 各种框架很多。
后端架api服务器,架设的过程中要在服务器端设置开启 CORS 跨域资源共享,否则前后端只能架设在同一个服务器上的同一个端口上,没有办法做到真正的分离。
分离之后,用户登陆或者认证可以不使用cookie,选择使用token,每次请求api时放在请求头部。jsonwebtoken目前是较好的选择。
前端使用Reactangular之类的框架,全部使用静态文件,数据使用json请求api服务器加载。文件可以放在另外的服务器上,可以用nginx或者其他的静态文件服务器。有扩展需求时,将文件放到CDN上,速度飞快。

另外,这种架构下,由于前后端分离,后端只做api服务器,想要今后写个android或ios app的客户端,就不用完全重写整个项目了。。

关注 11 回答 4

arccode 发布了文章 · 2015-04-09

前后端完全分离初探

核心思路

后端负责业务逻辑处理, 前端负责展示逻辑的处理.

背景

  • 2013年3月,首次接触了underscore.js这个区区45k大小的js库, 在使用其提供的简单模板完成表格异步分页后,从此我开始慢慢践行前后端完全分离的架构.

  • 2013年7月,我接触到了REST这种重用HTTP应用协议的架构, 更坚定了我践行前后端分离的决心.

  • 2014年3月,国内刮起了nodeJS的风暴, 此时我采用国外的一个开源项目MEAN开发了一套完整的CMS系统, 此次开发学习到了API接口如何标准化, API具体的设计参考了github API, Instagram API.

  • 2015年1月, 公司开发新产品, 需要新开发一套支持平台及数据下发平台, 在该产品中我主要参与API的设计, 并主导开发支撑平台, 下面我就介绍下这套支撑平台.

工具

工欲善其事, 必先利其器, 先介绍下自己使用的工具

IntelliJ IDEA

强大的Java集成开发工具, 没有之一.

Webstorm

强大的HTML,CSS,Javascript集成开发工具, 没有之一.

Sublime

强大的文本编辑工具, 目前我主要用于临时文件查看, 临时json数据格式化查看, 集成了plugin后, 它的功能会吓到人.

Navicat

小巧强大的关系数据库建模工具, 相当易用.

Cornerstone

强大的版本控制客户端.

Mou

一款简洁强大的MarkDown可视化编辑工具.

Zoc6

一款强大的Linux SSH客户端工具.

Transmit

一款强大的Linux SFTP客户端可视化工具.

代理

云梯

强大的VPN, 连接稳定, 速度快, 价格实惠, 可做全局代理, 包括terminal.

项目

目的

  • 提高使用者的工作效率
  • 提高开发人员的开发效率

前端类库

vue.js

一个用于创建web交互界面的库, 主要用于数据双向绑定.

superagent.js

一个非常方便的客户端请求代理模块,可方便的使用get,post,put,delete,head动词, 异步回调中封装了http状态码和业务数据等等.

dropzone.js

一个强大的文件上传库, 可获取文件mime, 文件大小等; 针对图片可生成缩略图, 获取图片宽度,高度.

backgrid.js

一个强大的表格组件.

director.js

一个强大的前端路由库, 通过#符号进行路径组织, 结合vuecomponent可进行单页的局部模块刷新.

后端类库

spring hateoas

spring 超文本驱动库, 可根据需求返回不同的httpStatus及links.

spring framework

对架构进行分层, 目前分三层

  • web接入层: 提供API接口,对前端传过来的数据进行校验, 校验未通过的话使用spring hateoas包装返回错误消息, 校验通过的话调用业务逻辑层
  • 业务逻辑层: 该层主要处理上层传进来的数据, 调用数据访问层进行数据持久化, 该层具有强大的扩展性;

    • 对于高并发写db的操作, 可集成消息队列库(如ActiveMQ)将消息发送至队列, 采用队列依次消费减轻db写负担.
    • 对于高并发频繁读db的操作, 可集成缓存库(如redis), 把读db的操作改为先读缓存, 未命中才去读db, 这样可大大减轻数据库读负担.
    • 在该层上还可以使用spring AOP对业务进行事务控制及日志记录.
  • 数据访问层: 该层只做一件事, 对数据库中的表记录进行增删改查, 不进行业务逻辑判断.

mybatis

一个半自动ORM(对象关系映射库)库, 简单灵活, 使用原生sql, 可完成较复杂的查询.

mybatis-generator-maven-plugin

mybatis插件, 可根据数据库中的表生成JavaBean,Dao接口,Dao的xml文件, 本人修改了部分源码, 可在JavaBean中自动添加注释, 详见另一篇博文MyBatis-Generator最佳实践.

spring security

完成用户的认证和授权.

thumbnailator(注: 需翻墙)

一个Java端的图片处理库, 可完成图片压缩, 裁剪, 水印等功能.

实战

如何提高使用者的工作效率?

  • 使用者看见就知道怎么使用
  • 为使用者自动完成部分可推导的表单填写
  • 拥有尽可能全的提示

如何提高开发人员的开发效率?

  • 技术选型时权衡考虑学习曲线与其提供的功能
  • 设计一套通用的可扩展的架构
  • 对常用操作及模块进行封装.

目前, 后端中将数据访问层和业务逻辑层中常用的方法封装成了泛型接口, 并使用抽象类来实现最基础的逻辑, 开发人员如果觉得指定方法无法满足需求, 可重写指定方法或使用新方法. 具体封装如下:


/** * GenericDao : 封装通用dao操作, 服务于GenericService * * @author http://arccode.net * @since 2014-12-03 */ public interface GenericDao<Model, PK> { /** * 插入 * * @param model */ int insertSelective(Model model); /** * 删除 * * @param id */ int deleteByPrimaryKey(PK id); /** * 更新 * * @param model */ int updateByPrimaryKeySelective(Model model); /** * 查询单条记录 * * @param id */ Model selectByPrimaryKey(PK id); /** * 分页查询 * @param page * @param model * @return */ List selectAndPage(Page<Model> page, Model model); }
/**
 * GenericService : 所有自定义Service的顶级接口,封装常用的增删查改操作
 *
 * Model : 代表数据库中的表映射的Java对象类型
 * PK :代表对象的主键类型
 *
 * @author http://arccode.net
 * @since 2014-12-03
 */
public interface GenericService<Model, PK> {

    /**
     * 分页获取集合, 带过滤功能
     * @param page 分页对象
     * @param model 分页条件, eg: model.name = "zhangsan", 对应sql语句为where name like %zhangsan%
     * @return
     */
    List getModels(Page<Model> page, Model model);

    /**
     * 新增
     * @param model
     * @return
     */
    Integer addModel(Model model);

    /**
     * 删除
     * @param id
     * @return
     */
    Integer removeModelById(PK id);

    /**
     * 修改
     * @param model
     * @return
     */
    Integer modifyModelById(Model model);

    /**
     * 根据主键id获取单条记录详情
     * @param id
     * @return
     */
    Model getModelById(PK id);

}

/**
 * GenericServiceSupport : 通用接口的实现类
 *
 * @author http://arccode.net
 * @since 2014-12-03
 */
public abstract class GenericServiceSupport<Model, PK>  implements GenericService<Model, PK>{

    /**
     * 定义成抽象方法,由子类实现,完成dao的注入
     * @return
     */
    public abstract GenericDao<Model, PK> getDao();

    @Override
    public List getModels(Page<Model> page, Model model) {
        return getDao().selectAndPage(page, model);
    }

    @Override
    public Integer addModel(Model model) {
        return getDao().insertSelective(model);
    }

    @Override
    public Integer removeModelById(PK id) {
        return getDao().deleteByPrimaryKey(id);
    }

    @Override
    public Integer modifyModelById(Model model) {
        return getDao().updateByPrimaryKeySelective(model);
    }

    @Override
    public Model getModelById(PK id) {
        return getDao().selectByPrimaryKey(id);
    }
}

熟练使用该模式, 开发一套业务逻辑的增删改查, 那速度是相当快.

针对前段也做了一些组件式的封装, 表格采用backgrid.js完成展示及分页操作, 只需要在html文件指定位置写入两个带id的html元素(一个用于展示表格, 一个用于分页), 之后copy写好的js模板, 在js中修改属性便可快速完成表格异步分页.

接下来将陆续发布实战干货.

本系列文章

  • 前后端完全分离初探
  • 前后端完全分离之API设计
  • 前后端完全分离之安全认证与授权-上
  • 前后端完全分离之安全认证与授权-下
  • 前后端完全分离之前端模块化开发
  • 前后端完全分离之前端路由系统
  • 前后端完全分离之后端面向服务的模块化开发

原文出自 前后端完全分离之API设计, 欢迎转载, 转载请注明出处.

查看原文

赞 20 收藏 190 评论 7