├── .gitignore ├── README.md ├── index.js ├── lib ├── interfacemanager.js ├── modelproxy-client.js ├── modelproxy.js ├── modelproxy.js.bak ├── proxyfactory.js └── proxyfactory.js_redis ├── package.json └── tests ├── README.md ├── interfaceRules ├── Cart.getMyCart.rule.json ├── Search.getNav.rule.json ├── Search.list.rule.json └── Search.suggest.rule.json ├── interface_test.json ├── interfacemanager.test.js ├── mockserver.js ├── modelproxy.test.js └── proxyfactory.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | tests/coverage.html 2 | 3 | node_modules/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | > 如果觉得不错的话,请star一下吧 😊 4 | 5 | ##### 使用技术: 前端架构express + node + xtemplate + mockjs + modelproxy-copy,服务端架构nodejs + express 6 | 7 | ##### 项目说明: 此项目是本人空余时间搭建的。希望大家提供宝贵的意见和建议,谢谢。 8 | 9 | ##### JS/React/Vue/Angular前端群: 599399742 10 | 11 | ##### 邮 箱: sosout@139.com 12 | 13 | ##### 个人网站: http://www.sosout.com/ 14 | 15 | ##### 个人博客: http://blog.sosout.com/ 16 | 17 | ##### 个人简书: http://www.jianshu.com/users/23b9a23b8849/latest_articles 18 | 19 | 该模块基于modelproxy改写,大部分代码保持不变,加上modelproxy在网上很难下载,所以我特此分享下,供大家使用,今后有什么新功能我也会在此基础迭代。 20 | --- 21 | 淘系的技术大背景下,必须依赖Java提供稳定的后端接口服务。在这样环境下,Node Server在实际应用中的一个主要作用即是代理(Proxy)功能。由于淘宝业务复杂,后端接口方式多种多样(MTop, Modulet, HSF...)。然而在使用Node开发web应用时,我们希望有一种统一方式访问这些代理资源的基础框架,为开发者屏蔽接口访问差异,同时提供友好简洁的数据接口使用方式。于是就有了 midway-modelproxy 这个构件。使用midway-modelproxy,可以提供如下好处: 22 | 23 | 1. 不同的开发者对于接口访问代码编写方式统一,含义清晰,降低维护难度。 24 | 2. 框架内部采用工厂+单例模式,实现接口一次配置多次复用。并且开发者可以随意定制组装自己的业务Model(依赖注入)。 25 | 3. 可以非常方便地实现线上,日常,预发环境的切换。 26 | 4. 内置[river-mock](http://gitlab.alibaba-inc.com/river/mock/tree/master)和[mockjs](http://mockjs.com)等mock引擎,提供mock数据非常方便。 27 | 5. 使用接口配置文件,对接口的依赖描述做统一的管理,避免散落在各个代码之中。 28 | 6. 支持浏览器端共享Model,浏览器端可以使用它做前端数据渲染。整个代理过程对浏览器透明。 29 | 7. 接口配置文件本身是结构化的描述文档,可以使用[river](http://gitlab.alibaba-inc.com/river/spec/tree/master)工具集合,自动生成文档。也可使用它做相关自动化接口测试,使整个开发过程形成一个闭环。 30 | 31 | ### ModelProxy工作原理图及相关开发过程图览 32 | --- 33 |  34 | 35 | # 使用前必读 36 | --- 37 | 使用ModelProxy之前,您需要在工程根目录下创建名为interface.json的配置文件。该文件定义了工程项目中所有需要使用到的接口集合(详细配置说明见后文)。定义之后,您可以在代码中按照需要引入不同的接口,创建与业务相关的Model对象。接口的定义和model其实是多对多的关系。也即一个接口可以被多个model使用,一个model可以使用多个接口。具体情况由创建model的方式来决定。下面用例中会从易到难交您如何创建这些model。 38 | 39 | # 快速开始 40 | --- 41 | 42 | ### 用例一 接口文件配置->引入接口配置文件->创建并使用model 43 | * 第一步 配置接口文件命名为:interface_sample.json,并将其放在工程根目录下。 44 | 注意:整个项目有且只有一个接口配置文件,其interfaces字段下定义了多个接口。在本例中,仅仅配置了一个主搜接口。 45 | 46 | ```json 47 | { 48 | "title": "pad淘宝项目数据接口集合定义", 49 | "version": "1.0.0", 50 | "engine": "mockjs", 51 | "rulebase": "./interfaceRules/", 52 | "status": "online", 53 | "interfaces": [ { 54 | "name": "主搜索接口", 55 | "id": "Search.getItems", 56 | "urls": { 57 | "online": "http://s.m.taobao.com/client/search.do" 58 | } 59 | } ] 60 | } 61 | ``` 62 | 63 | * 第二步 在代码中引入ModelProxy模块,并且初始化引入接口配置文件(在实际项目中,引入初始化文件动作应伴随工程项目启动时完成,有且只有一次) 64 | 65 | ```js 66 | // 引入模块 67 | var ModelProxy = require( 'modelproxy' ); 68 | 69 | // 初始化引入接口配置文件 (注意:初始化工作有且只有一次) 70 | ModelProxy.init( './interface_sample.json' ); 71 | ``` 72 | 73 | * 第三步 使用ModelProxy 74 | 75 | ```js 76 | // 创建model 77 | var searchModel = new ModelProxy( { 78 | searchItems: 'Search.getItems' // 自定义方法名: 配置文件中的定义的接口ID 79 | } ); 80 | // 或者这样创建: var searchModel = new ModelProxy( 'Search.getItems' ); 此时getItems 会作为方法名 81 | 82 | // 使用model, 注意: 调用方法所需要的参数即为实际接口所需要的参数。 83 | searchModel.searchItems( { keyword: 'iphone6' } ) 84 | // !注意 必须调用 done 方法指定回调函数,来取得上面异步调用searchItems获得的数据! 85 | .done( function( data ) { 86 | console.log( data ); 87 | } ) 88 | .error( function( err ) { 89 | console.log( err ); 90 | } ); 91 | ``` 92 | 93 | ### 用例二 model多接口配置及合并请求 94 | * 配置 95 | 96 | ```json 97 | { // 头部配置省略... 98 | "interfaces": [ { 99 | "name": "主搜索搜索接口", 100 | "id": "Search.list", 101 | "urls": { 102 | "online": "http://s.m.taobao.com/search.do" 103 | } 104 | }, { 105 | "name": "热词推荐接口", 106 | "id": "Search.suggest", 107 | "urls": { 108 | "online": "http://suggest.taobao.com/sug" 109 | } 110 | }, { 111 | "name": "导航获取接口", 112 | "id": "Search.getNav", 113 | "urls": { 114 | "online": "http://s.m.taobao.com/client/search.do" 115 | } 116 | } ] 117 | } 118 | ``` 119 | 120 | * 代码 121 | 122 | ```js 123 | // 更多创建方式,请参考后文API 124 | var model = new ModelProxy( 'Search.*' ); 125 | 126 | // 调用自动生成的不同方法 127 | model.list( { keyword: 'iphone6' } ) 128 | .done( function( data ) { 129 | console.log( data ); 130 | } ); 131 | 132 | model.suggest( { q: '女' } ) 133 | .done( function( data ) { 134 | console.log( data ); 135 | } ) 136 | .error( function( err ) { 137 | console.log( err ); 138 | } ); 139 | 140 | // 合并请求 141 | model.suggest( { q: '女' } ) 142 | .list( { keyword: 'iphone6' } ) 143 | .getNav( { key: '流行服装' } ) 144 | .done( function( data1, data2, data3 ) { 145 | // 参数顺序与方法调用顺序一致 146 | console.log( data1, data2, data3 ); 147 | } ); 148 | ``` 149 | 150 | ### 用例三 Model混合配置及依赖调用 151 | 152 | * 配置 153 | 154 | ```json 155 | { // 头部配置省略... 156 | "interfaces": [ { 157 | "name": "用户信息查询接口", 158 | "id": "Session.getUser", 159 | "urls": { 160 | "online": "http://taobao.com/getUser.do" 161 | } 162 | }, { 163 | "name": "订单获取接口", 164 | "id": "Order.getOrder", 165 | "urls": { 166 | "online": "http://taobao.com/getOrder" 167 | } 168 | } ] 169 | } 170 | ``` 171 | 172 | * 代码 173 | 174 | ``` js 175 | var model = new ModelProxy( { 176 | getUser: 'Session.getUser', 177 | getMyOrderList: 'Order.getOrder' 178 | } ); 179 | // 先获得用户id,然后再根据id号获得订单列表 180 | model.getUser( { sid: 'fdkaldjfgsakls0322yf8' } ) 181 | .done( function( data ) { 182 | var uid = data.uid; 183 | this.getMyOrderList( { id: uid } ) 184 | .done( function( data ) { 185 | console.log( data ); 186 | } ); 187 | } ); 188 | ``` 189 | 190 | ### 用例四 配置mock代理 191 | * 第一步 在相关接口配置段落中启用mock 192 | 193 | ```json 194 | { 195 | "title": "pad淘宝数据接口定义", 196 | "version": "1.0.0", 197 | "engine": "mockjs", <-- 指定mock引擎 198 | "rulebase": "./interfaceRules/", <-- 指定存放相关mock规则文件的目录 199 | "status": "online", 200 | "interfaces": [ { 201 | "name": "主搜索接口", 202 | "id": "Search.getItems", 203 | "ruleFile": "Search.getItems.rule.json", <-- 指定数据mock规则文件名,如果不配置,则将默认设置为 id + '.rule.json' 204 | "urls": { 205 | "online": "http://s.m.taobao.com/client/search.do", 206 | "prep": "http://s.m.taobao.com/client/search.do", 207 | "daily": "http://daily.taobao.net/client/search.do" 208 | }, 209 | status: 'mock' <-- 启用mock状态,覆盖全局status 210 | } ] 211 | } 212 | ``` 213 | 214 | * 第二步 添加接口对应的规则文件到ruleBase(./interfaceRules/)指定的文件夹。mock数据规则请参考 [http://mockjs.com]。 215 | 启动程序后,ModelProxy即返回相关mock数据。 216 | 217 | 218 | ### 用例五 使用ModelProxy拦截请求 219 | 220 | ```js 221 | var app = require( 'connect' )(); 222 | var ModelProxy = require( 'modelproxy' ); 223 | ModelProxy.init( './interface_sample.json' ); 224 | 225 | // 指定需要拦截的路径 226 | app.use( '/model', ModelProxy.Interceptor ); 227 | 228 | // 此时可直接通过浏览器访问 /model/[interfaceid] 调用相关接口(如果该接口定义中配置了 intercepted = false, 则无法访问) 229 | ``` 230 | 231 | ### 用例六 在浏览器端使用ModelProxy 232 | * 第一步 按照用例二配置接口文件 233 | 234 | * 第二步 按照用例五 启用拦截功能 235 | 236 | * 第三步 在浏览器端使用ModelProxy 237 | 238 | ```html 239 | 240 | 241 | ``` 242 | 243 | ```html 244 | 267 | ``` 268 | 269 | ### 用例七 代理带cookie的请求并且回写cookie (注:请求是否需要带cookie或者回写取决于接口提供者) 270 | 271 | * 关键代码(app 由express创建) 272 | 273 | ```js 274 | app.get( '/getMycart', function( req, res ) { 275 | var cookie = req.headers.cookie; 276 | var cart = ModelProxy.create( 'Cart.*' ); 277 | cart.getMyCart() 278 | // 在调用done之前带上cookie 279 | .withCookie( cookie ) 280 | // done 回调函数中最后一个参数总是需要回写的cookie,不需要回写时可以忽略 281 | .done( function( data , setCookies ) { 282 | // 回写cookie 283 | res.setHeader( 'Set-Cookie', setCookies ); 284 | res.send( data ); 285 | }, function( err ) { 286 | res.send( 500, err ); 287 | } ); 288 | } ); 289 | 290 | ``` 291 | 292 | # 配置文件详解 293 | --- 294 | 295 | ``` js 296 | { 297 | "title": "pad淘宝项目数据接口集合定义", // [必填][string] 接口文档标题 298 | "version": "1.0.0", // [必填][string] 版本号 299 | "engine": "river-mock", // [选填][string] mock 引擎,取值可以是river-mock 和mockjs。不需要mock数据时可以不配置 300 | "rulebase": "./interfaceRules/", // [选填][string] mock规则文件夹路径。不需要mock数据时可以不配置。 301 | // 默认会设置为与本配置文件同级别的文件夹下名位 interfaceRules的文件夹 302 | "status": "online", // [必填][string] 全局代理状态,取值只能是 interface.urls中出现过的键值或者mock 303 | "interfaces": [ { 304 | "name": "获取购物车信息", // [选填][string] 接口名称 生成文档有用 305 | "desc": "接口负责人", // [选填][string] 接口描述 生成文档有用 306 | "version": "0.0.1", // [选填][string] 接口版本号 发送请求时会带上版本号字段 307 | "id": "cart.getCart", // [必填][string] 接口ID,必须由英文单词+点号组成 308 | "urls": { // [如果ruleFile不存在, 则必须有一个地址存在][object] 可供切换的url集合 309 | "online": "http://url1", // 线上地址 310 | "prep": "http://url2", // 预发地址 311 | "daily": "http://url3", // 日常地址 312 | }, 313 | "ruleFile": "cart.getCart.rule.json",// [选填][string] 对应的数据规则文件,当Proxy Mock状态开启时回返回mock数据, 314 | // 不配置时默认为id + ".rule.json"。 315 | "isRuleStatic": true, // [选填][boolean] 数据规则文件是否为静态,即在开启mock状态时,程序会将ruleFile 316 | // 按照静态文件读取, 而非解析该规则文件生成数据,默认为false 317 | "status": "online", // [选填][string] 当前代理状态,可以是urls中的某个键值(online, prep, daily)或者mock 318 | // 或mockerr。如果不填,则代理状态依照全局设置的代理状态;如果设置为mock,则返回ruleFile中定义 319 | // response 内容;如果设置为mockerr,则返回ruleFile中定义的responseError内容。 320 | "method": "post", // [选填][string] 请求方式,取值post|get 默认get 321 | "dataType": "json", // [选填][string] 返回的数据格式, 取值 json|text, 默认为json 322 | "isCookieNeeded": true, // [选填][boolean] 是否需要传递cookie 默认false 323 | "encoding": "utf8" // [选填][string] 代理的数据源编码类型。取值可以是常用编码类型'utf8', 'gbk', 324 | // 'gb2312' 或者 'raw' 如果设置为raw则直接返回2进制buffer,默认为utf8。 325 | // 注意,不论数据源原来为何种编码,代理之后皆以utf8编码输出。 326 | "timeout": 5000, // [选填][number] 延时设置,默认10000 327 | "intercepted": true // [选填][boolean] 是否拦截请求,默认为true 328 | // format // 未完待续 329 | // filter... // 未完待续 330 | }, { 331 | ... 332 | } ], 333 | combo: { 334 | // 未完待续 335 | } 336 | } 337 | ``` 338 | 339 | # API 340 | --- 341 | ### ModelProxy 对象创建方式 342 | 343 | * 直接new 344 | 345 | ```js 346 | var model = new ModelProxy( profile ); 347 | 348 | ``` 349 | 350 | * 工厂创建 351 | 352 | ```js 353 | var model = ModelProxy.create( profile ); 354 | ``` 355 | 356 | ### 创建ModelProxy对象时指定的 profile 相关形式 357 | * 接口ID 生成的对象会取ID最后'.'号后面的单词作为方法名 358 | 359 | ```js 360 | ModelProxy.create( 'Search.getItem' ); 361 | ``` 362 | 363 | * 键值JSON对象 自定义方法名: 接口ID 364 | 365 | ```js 366 | ModelProxy.create( { 367 | getName: 'Session.getUserName', 368 | getMyCarts: 'Cart.getCarts' 369 | } ); 370 | ``` 371 | 372 | * 数组形式 取最后 . 号后面的单词作为方法名 373 | 下例中生成的方法调用名依次为: Cart_getItem, getItem, suggest, getName 374 | 375 | ```js 376 | ModelProxy.create( [ 'Cart.getItem', 'Search.getItem', 'Search.suggest', 'Session.User.getName' ] ); 377 | 378 | ``` 379 | 380 | * 前缀形式 (推荐使用) 381 | 382 | ```js 383 | ModelProxy.create( 'Search.*' ); 384 | ``` 385 | 386 | ### ModelProxy对象方法 387 | 388 | * .method( params ) 389 | method为创建model时动态生成,参数 params{Object}, 为请求接口所需要的参数键值对。 390 | 391 | * .done( callback, errCallback ) 392 | 接口调用完成函数,callback函数的参数与done之前调用的方法请求结果保持一致.最后一个参数为请求回写的cookie。callback函数中的 this 指向ModelProxy对象本身,方便做进一步调用。errCallback 即出错回调函数(可能会被调用多次)。 393 | 394 | * .withCookie( cookies ) 395 | 如果接口需要提供cookie才能返回数据,则调用此方法来设置请求的cookie{String} (如何使用请查看用例七) 396 | 397 | * .error( errCallback ) 398 | 指定全局调用出错处理函数, errCallback 的参数为Error对象。 399 | 400 | 401 | # Mock 功能相关说明 402 | --- 403 | ### rule.json 文件 404 | 当mock状态开启时,mock引擎会读取与接口定义相对应的rule.json规则文件,生成相应的数据。该文件应该位于interface.json配置文件中 405 | ruleBase字段所指定的文件夹中。 (建议该文件夹与interface配置文件同级) 406 | 407 | 408 | ### rule.json 文件样式 409 | 410 | ```js 411 | { 412 | "request": { // 请求参数列表 413 | "参数名1": "规则一", // 具体规则取决于采用何种引擎 414 | "参数名2": "规则二", 415 | ... 416 | }, 417 | "response": 响应内容规则, // 响应内容规则取决于采用何种引擎 418 | "responseError": 响应失败规则 // 响应内容规则取决于采用何种引擎 419 | } 420 | 421 | ``` 422 | 423 | ## [Test Coverage] 424 | --- 425 | 426 | **Overview: `96%` coverage `272` SLOC** 427 | 428 | [modelproxy.js](lib/modelproxy.js) : `98%` coverage `57` SLOC 429 | 430 | [interfacemanager.js](lib/interfacemanager.js): `98%` coverage `76` SLOC 431 | 432 | [proxyfactory](lib/proxyfactory.js) : `93%` coverage `139` SLOC 433 | 434 | ======= 435 | >>>>>>> 8ce533e0358055a823c470a608996e654b00682e 436 | 437 | 438 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require( './lib/modelproxy' ); -------------------------------------------------------------------------------- /lib/interfacemanager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * InterfaceManager 3 | * This Class is provided to parse the interface configuration file so that 4 | * the Proxy class can easily access the structure of the configuration. 5 | * @author ShanFan 6 | * @created 24-3-2014 7 | **/ 8 | 9 | var fs = require( 'fs' ); 10 | 11 | /** 12 | * InterfaceManager 13 | * @param {String|Object} path The file path of inteface configuration or the interface object 14 | */ 15 | function InterfaceManager( path ) { 16 | this._path = path; 17 | 18 | // {Object} Interface Mapping, The key is interface id and 19 | // the value is a json profile for this interface. 20 | this._interfaceMap = {}; 21 | 22 | // {Object} A interface Mapping for client, the key is interface id and 23 | // the value is a json profile for this interface. 24 | this._clientInterfaces = {}; 25 | 26 | // {String} The path of rulebase where the interface rules is stored. This value will be override 27 | // if user specified the path of rulebase in interface.json. 28 | this._rulebase = typeof path === 'string' ? path.replace( /\/[^\/]*$/, '/interfaceRules' ) : ''; 29 | 30 | typeof path === 'string' 31 | ? this._loadProfilesFromPath( path ) 32 | : this._loadProfiles( path ); 33 | } 34 | 35 | // InterfaceManager prototype 36 | InterfaceManager.prototype = { 37 | 38 | // @throws errors 39 | _loadProfilesFromPath: function( path ) { 40 | console.info( 'Loading interface profiles.\nPath = ', path ); 41 | 42 | try { 43 | var profiles = fs.readFileSync( path, { encoding: 'utf8' } ); 44 | } catch ( e ) { 45 | throw new Error( 'Fail to load interface profiles.' + e ); 46 | } 47 | try { 48 | profiles = JSON.parse( profiles ); 49 | } catch( e ) { 50 | throw new Error( 'Interface profiles has syntax error:' + e ); 51 | } 52 | this._loadProfiles( profiles ); 53 | }, 54 | 55 | _loadProfiles: function( profiles ) { 56 | if ( !profiles ) return; 57 | console.info( 'Title:', profiles.title, 'Version:', profiles.version ); 58 | 59 | this._rulebase = profiles.rulebase 60 | ? ( profiles.rulebase || './' ).replace(/\/$/, '') 61 | : this._rulebase; 62 | 63 | // {String} The mock engine name. 64 | this._engine = profiles.engine || 'mockjs'; 65 | 66 | if ( profiles.status === undefined ) { 67 | throw new Error( 'There is no status specified in interface configuration!' ); 68 | } 69 | 70 | // {String} The interface status in using. 71 | this._status = profiles.status; 72 | 73 | var interfaces = profiles.interfaces || []; 74 | for ( var i = interfaces.length - 1; i >= 0; i-- ) { 75 | this._addProfile( interfaces[i] ) 76 | && console.info( 'Interface[' + interfaces[i].id + '] is loaded.' ); 77 | } 78 | }, 79 | getProfile: function( interfaceId ) { 80 | return this._interfaceMap[ interfaceId ]; 81 | }, 82 | getClientInterfaces: function() { 83 | return this._clientInterfaces; 84 | }, 85 | // @throws errors 86 | getRule: function( interfaceId ) { 87 | if ( !interfaceId || !this._interfaceMap[ interfaceId ] ) { 88 | throw new Error( 'The interface profile ' + interfaceId + " is not found." ); 89 | } 90 | path = this._interfaceMap[ interfaceId ].ruleFile; 91 | if ( !fs.existsSync( path ) ) { 92 | throw new Error( 'The rule file is not existed.\npath = ' + path ); 93 | } 94 | try { 95 | var rulefile = fs.readFileSync( path, { encoding: 'utf8' } ); 96 | } catch ( e ) { 97 | throw new Error( 'Fail to read rulefile of path ' + path ); 98 | } 99 | try { 100 | return JSON.parse( rulefile ); 101 | } catch( e ) { 102 | throw new Error( 'Rule file has syntax error. ' + e + '\npath=' + path ); 103 | } 104 | }, 105 | getEngine: function() { 106 | return this._engine; 107 | }, 108 | getStatus: function( name ) { 109 | return this._status; 110 | }, 111 | // @return Array 112 | getInterfaceIdsByPrefix: function( pattern ) { 113 | if ( !pattern ) return []; 114 | var ids = [], map = this._interfaceMap, len = pattern.length; 115 | for ( var id in map ) { 116 | if ( id.slice( 0, len ) == pattern ) { 117 | ids.push( id ); 118 | } 119 | } 120 | return ids; 121 | }, 122 | 123 | isProfileExisted: function( interfaceId ) { 124 | return !!this._interfaceMap[ interfaceId ]; 125 | }, 126 | _addProfile: function( prof ) { 127 | if ( !prof || !prof.id ) { 128 | console.error( "Can not add interface profile without id!" ); 129 | return false; 130 | } 131 | if ( !/^((\w+\.)*\w+)$/.test( prof.id ) ) { 132 | console.error( "Invalid id: " + prof.id ); 133 | return false; 134 | } 135 | if ( this.isProfileExisted( prof.id ) ) { 136 | console.error( "Can not repeat to add interface [" + prof.id 137 | + "]! Please check your interface configuration file!" ); 138 | return false; 139 | } 140 | 141 | prof.ruleFile = this._rulebase + '/' 142 | + ( prof.ruleFile || ( prof.id + ".rule.json" ) ); 143 | 144 | if ( !this._isUrlsValid( prof.urls ) 145 | && !fs.existsSync( prof.ruleFile ) ) { 146 | console.error( 'Profile is deprecated:\n', 147 | prof, '\nNo urls is configured and No ruleFile is available' ); 148 | return false; 149 | } 150 | if (!( prof.status in prof.urls || prof.status === 'mock' 151 | || prof.status === 'mockerr')) { 152 | prof.status = this._status; 153 | } 154 | 155 | prof.method = { POST: 'POST', GET:'GET' } 156 | [ (prof.method || 'GET').toUpperCase() ]; 157 | prof.dataType = { json: 'json', text: 'text', jsonp: 'jsonp' } 158 | [ (prof.dataType || 'json').toLowerCase() ]; 159 | prof.isRuleStatic = !!prof.isRuleStatic || false; 160 | prof.isCookieNeeded = !!prof.isCookieNeeded || false; 161 | prof.signed = !!prof.signed || false; 162 | prof.timeout = prof.timeout || 10000; 163 | 164 | // prof.format 165 | // prof.filter = ... 166 | this._interfaceMap[ prof.id ] = prof; 167 | 168 | this._clientInterfaces[ prof.id ] = { 169 | id: prof.id, 170 | method: prof.method, 171 | dataType: prof.dataType 172 | }; 173 | 174 | return true; 175 | }, 176 | _isUrlsValid: function( urls ) { 177 | if ( !urls ) return false; 178 | for ( var i in urls ) { 179 | return true; 180 | } 181 | return false; 182 | } 183 | }; 184 | 185 | module.exports = InterfaceManager; -------------------------------------------------------------------------------- /lib/modelproxy-client.js: -------------------------------------------------------------------------------- 1 | KISSY.add( 'modelproxy', function ( S, IO ) { 2 | function Proxy( options ) { 3 | this._opt = options; 4 | } 5 | Proxy.prototype = { 6 | request: function( params, callback, errCallback ) { 7 | IO( { 8 | url: Proxy.base + '/' + this._opt.id, 9 | data: params, 10 | type: this._opt.method, 11 | dataType: this._opt.dataType, 12 | success: callback, 13 | error: errCallback 14 | } ); 15 | }, 16 | getOptions: function() { 17 | return this._opt; 18 | } 19 | }; 20 | 21 | Proxy.objects = {}; 22 | 23 | Proxy.create = function( id ) { 24 | if ( this.objects[ id ] ) { 25 | return this.objects[ id ]; 26 | } 27 | var options = this._interfaces[ id ]; 28 | if ( !options ) { 29 | throw new Error( 'No such interface id defined: ' 30 | + id + ', please check your interface configuration file' ); 31 | } 32 | return this.objects[ id ] = new this( options ); 33 | }, 34 | 35 | Proxy.configBase = function( base ) { 36 | if ( this.base ) return; 37 | this.base = ( base || '' ).replace( /\/$/, '' ); 38 | var self = this; 39 | // load interfaces definition. 40 | IO( { 41 | url: this.base + '/$interfaces', 42 | async: false, 43 | type: 'get', 44 | dataType: 'json', 45 | success: function( interfaces ) { 46 | self.config( interfaces ); 47 | }, 48 | error: function( err ) { 49 | throw err; 50 | } 51 | } ); 52 | }; 53 | 54 | Proxy.config = function( interfaces ) { 55 | this._interfaces = interfaces; 56 | }; 57 | 58 | Proxy.getInterfaceIdsByPrefix = function( pattern ) { 59 | if ( !pattern ) return []; 60 | var ids = [], map = this._interfaces, len = pattern.length; 61 | for ( var id in map ) { 62 | if ( id.slice( 0, len ) == pattern ) { 63 | ids.push( id ); 64 | } 65 | } 66 | return ids; 67 | }; 68 | 69 | function ModelProxy( profile ) { 70 | if ( !profile ) return; 71 | 72 | if ( typeof profile === 'string' ) { 73 | if ( /^(\w+\.)+\*$/.test( profile ) ) { 74 | profile = Proxy 75 | .getInterfaceIdsByPrefix( profile.replace( /\*$/, '' ) ); 76 | 77 | } else { 78 | profile = [ profile ]; 79 | } 80 | } 81 | if ( profile instanceof Array ) { 82 | var prof = {}, methodName; 83 | for ( var i = profile.length - 1; i >= 0; i-- ) { 84 | methodName = profile[ i ]; 85 | methodName = methodName 86 | .substring( methodName.lastIndexOf( '.' ) + 1 ); 87 | if ( !prof[ methodName ] ) { 88 | prof[ methodName ] = profile[ i ]; 89 | 90 | } else { 91 | methodName = profile[ i ].replace( /\./g, '_' ); 92 | prof[ methodName ] = profile[ i ]; 93 | } 94 | } 95 | profile = prof; 96 | } 97 | 98 | for ( var method in profile ) { 99 | this[ method ] = ( function( methodName, interfaceId ) { 100 | var proxy = Proxy.create( interfaceId ); 101 | return function( params ) { 102 | params = params || {}; 103 | if ( !this._queue ) { 104 | this._queue = []; 105 | } 106 | this._queue.push( { 107 | params: params, 108 | proxy: proxy 109 | } ); 110 | return this; 111 | }; 112 | } )( method, profile[ method ] ); 113 | } 114 | } 115 | 116 | ModelProxy.prototype = { 117 | done: function( f, ef ) { 118 | if ( typeof f !== 'function' ) return; 119 | 120 | if ( !this._queue ) { 121 | f.apply( this ); 122 | return; 123 | } 124 | this._sendRequestsParallel( this._queue, f, ef ); 125 | 126 | this._queue = null; 127 | return this; 128 | }, 129 | _sendRequestsParallel: function( queue, callback, errCallback ) { 130 | var args = [], self = this; 131 | 132 | var cnt = queue.length; 133 | 134 | for ( var i = 0; i < queue.length; i++ ) { 135 | ( function( reqObj, k ) { 136 | reqObj.proxy.request( reqObj.params, function( data ) { 137 | args[ k ] = data; 138 | --cnt || callback.apply( self, args ); 139 | }, function( err ) { 140 | errCallback = errCallback || self._errCallback; 141 | if ( typeof errCallback === 'function' ) { 142 | errCallback( err ); 143 | 144 | } else { 145 | console.error( 'Error occured when sending request =' 146 | , reqObj.proxy.getOptions(), '\nCaused by:\n', err ); 147 | } 148 | } ); 149 | } )( queue[i], i ); 150 | } 151 | }, 152 | error: function( f ) { 153 | this._errCallback = f; 154 | } 155 | }; 156 | 157 | ModelProxy.create = function( profile ) { 158 | return new this( profile ); 159 | }; 160 | 161 | ModelProxy.configBase = function( path ) { 162 | Proxy.configBase( path ); 163 | }; 164 | 165 | return ModelProxy; 166 | 167 | }, { requires: ['io'] } ); -------------------------------------------------------------------------------- /lib/modelproxy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ModelProxy 3 | * As named, this class is provided to model the proxy. 4 | * @author ShanFan 5 | * @created 24-3-2014 6 | **/ 7 | 8 | // Dependencies 9 | var InterfaceManager = require( './interfacemanager' ) 10 | , ProxyFactory = require( './proxyfactory' ); 11 | 12 | /** 13 | * ModelProxy Constructor 14 | * @param {Object|Array|String} profile. This profile describes what the model looks 15 | * like. eg: 16 | * profile = { 17 | * getItems: 'Search.getItems', 18 | * getCart: 'Cart.getCart' 19 | * } 20 | * profile = ['Search.getItems', 'Cart.getCart'] 21 | * profile = 'Search.getItems' 22 | * profile = 'Search.*' 23 | */ 24 | function ModelProxy( profile ) { 25 | if ( !profile ) return; 26 | var that = this; 27 | if ( typeof profile === 'string' ) { 28 | 29 | // Get ids via prefix pattern like 'packageName.*' 30 | if ( /^(\w+\.)+\*$/.test( profile ) ) { 31 | profile = ProxyFactory 32 | .getInterfaceIdsByPrefix( profile.replace( /\*$/, '' ) ); 33 | 34 | } else { 35 | profile = [ profile ]; 36 | } 37 | } 38 | if ( profile instanceof Array ) { 39 | var prof = {}, methodName; 40 | for ( var i = profile.length - 1; i >= 0; i-- ) { 41 | methodName = profile[ i ]; 42 | methodName = methodName 43 | .substring( methodName.lastIndexOf( '.' ) + 1 ); 44 | if ( !prof[ methodName ] ) { 45 | prof[ methodName ] = profile[ i ]; 46 | 47 | // The method name is duplicated, so the full interface id is set 48 | // as the method name. 49 | } else { 50 | methodName = profile[ i ].replace( /\./g, '_' ); 51 | prof[ methodName ] = profile[ i ]; 52 | } 53 | } 54 | profile = prof; 55 | } 56 | that.profile = profile; //wmm 57 | // Construct the model following the profile 58 | for ( var method in profile ) { 59 | this[ method ] = ( function( methodName, interfaceId ) { 60 | var proxy = ProxyFactory.create( interfaceId ); 61 | return function( params, expire ) { 62 | params = params || {}; 63 | 64 | if ( !that._queue ) { 65 | that._queue = []; 66 | } 67 | // Push this method call into request queue. Once the done method 68 | // is called, all requests in this queue will be sent. 69 | that._queue.push( { 70 | params: params, 71 | proxy: proxy, 72 | expire: expire || 0 73 | } ); 74 | return this; 75 | }; 76 | } )( method, profile[ method ] ); 77 | // this._addMethod( method, profile[ method ] ); 78 | } 79 | } 80 | 81 | ModelProxy.prototype = { 82 | done: function( f, ef ) { 83 | if ( typeof f !== 'function' ) return; 84 | 85 | // No request pushed in _queue, so callback directly and return. 86 | if ( !this._queue ) { 87 | f.apply( this ); 88 | return; 89 | } 90 | // Send requests parallel 91 | this._sendRequestsParallel( this._queue, f, ef ); 92 | 93 | // Clear queue 94 | this._queue = null; 95 | return this; 96 | }, 97 | // 针对动态调用方法,做个封装 98 | invoke:function(map,key){ 99 | var intfs = (map[key] && map[key].interfaces) || []; 100 | for (var i = 0; i < intfs.length; i++) { 101 | var intf = intfs[i]; 102 | if(!intf.name){ 103 | continue; 104 | }else{ 105 | var proxy = ProxyFactory.create(this.profile[intf.name]); 106 | var params = intf.params || []; 107 | var expire = intf.expire || 0; 108 | if ( !this._queue ) { 109 | this._queue = []; 110 | } 111 | this._queue.push( { 112 | params: params, 113 | proxy: proxy, 114 | expire: expire 115 | }); 116 | } 117 | } 118 | 119 | return this; 120 | }, 121 | 122 | withCookie: function( cookie ) { 123 | this._cookies = cookie; 124 | return this; 125 | }, 126 | 127 | _sendRequestsParallel: function( queue, callback, errCallback ) { 128 | 129 | // The final data array 130 | var args = [], setcookies = [], self = this; 131 | 132 | // Count the number of callback; 133 | var cnt = queue.length; 134 | 135 | // Send each request 136 | for ( var i = 0; i < queue.length; i++ ) { 137 | ( function( reqObj, k, cookie ) { 138 | //console.log("reqObj:"+JSON.stringify(reqObj)); 139 | 140 | reqObj.proxy.request( reqObj, function( data, setcookie ) { 141 | // console.log(113,reqObj.params); 142 | // fill data for callback 143 | args[ k ] = data; 144 | // concat setcookie for cookie rewriting 145 | setcookies = setcookies.concat( setcookie ); 146 | args.push( setcookies ); 147 | 148 | // push the set-cookies as the last parameter for the callback function. 149 | --cnt || callback.apply( self, args.push( setcookies ) && args ); 150 | 151 | }, function( err ) { 152 | errCallback = errCallback || self._errCallback; 153 | if ( typeof errCallback === 'function' ) { 154 | errCallback( err ); 155 | 156 | } else { 157 | console.error( 'Error occured when sending request =' 158 | , reqObj.params, '\nCaused by:\n', err ); 159 | } 160 | }, cookie ); // request with cookie. 161 | 162 | } )( queue[i], i, self._cookies ); 163 | } 164 | // clear cookie of this request. 165 | self._cookies = undefined; 166 | }, 167 | error: function( f ) { 168 | this._errCallback = f; 169 | } 170 | }; 171 | 172 | /** 173 | * ModelProxy.init 174 | * @param {String} path The path refers to the interface configuration file. 175 | */ 176 | ModelProxy.init = function( path ) { 177 | ProxyFactory.use( new InterfaceManager( path ) ); 178 | }; 179 | 180 | 181 | ModelProxy.create = function( profile ) { 182 | return new this( profile ); 183 | }; 184 | 185 | ModelProxy.Interceptor = function( req, res ) { 186 | // todo: need to handle the case that the request url is multiple 187 | // interfaces combined which configured in interface.json. 188 | ProxyFactory.Interceptor( req, res ); 189 | }; 190 | 191 | module.exports = ModelProxy; 192 | -------------------------------------------------------------------------------- /lib/modelproxy.js.bak: -------------------------------------------------------------------------------- 1 | /** 2 | * ModelProxy 3 | * As named, this class is provided to model the proxy. 4 | * @author ShanFan 5 | * @created 24-3-2014 6 | **/ 7 | 8 | // Dependencies 9 | var InterfaceManager = require( './interfacemanager' ) 10 | , ProxyFactory = require( './proxyfactory' ); 11 | 12 | /** 13 | * ModelProxy Constructor 14 | * @param {Object|Array|String} profile. This profile describes what the model looks 15 | * like. eg: 16 | * profile = { 17 | * getItems: 'Search.getItems', 18 | * getCart: 'Cart.getCart' 19 | * } 20 | * profile = ['Search.getItems', 'Cart.getCart'] 21 | * profile = 'Search.getItems' 22 | * profile = 'Search.*' 23 | */ 24 | function ModelProxy( profile ) { 25 | if ( !profile ) return; 26 | var that = this; 27 | if ( typeof profile === 'string' ) { 28 | 29 | // Get ids via prefix pattern like 'packageName.*' 30 | if ( /^(\w+\.)+\*$/.test( profile ) ) { 31 | profile = ProxyFactory 32 | .getInterfaceIdsByPrefix( profile.replace( /\*$/, '' ) ); 33 | 34 | } else { 35 | profile = [ profile ]; 36 | } 37 | } 38 | if ( profile instanceof Array ) { 39 | var prof = {}, methodName; 40 | for ( var i = profile.length - 1; i >= 0; i-- ) { 41 | methodName = profile[ i ]; 42 | methodName = methodName 43 | .substring( methodName.lastIndexOf( '.' ) + 1 ); 44 | if ( !prof[ methodName ] ) { 45 | prof[ methodName ] = profile[ i ]; 46 | 47 | // The method name is duplicated, so the full interface id is set 48 | // as the method name. 49 | } else { 50 | methodName = profile[ i ].replace( /\./g, '_' ); 51 | prof[ methodName ] = profile[ i ]; 52 | } 53 | } 54 | profile = prof; 55 | } 56 | that.profile = profile; //wmm 57 | // Construct the model following the profile 58 | for ( var method in profile ) { 59 | this[ method ] = ( function( methodName, interfaceId ) { 60 | var proxy = ProxyFactory.create( interfaceId ); 61 | return function( params ) { 62 | params = params || {}; 63 | 64 | if ( !that._queue ) { 65 | that._queue = []; 66 | } 67 | // Push this method call into request queue. Once the done method 68 | // is called, all requests in this queue will be sent. 69 | that._queue.push( { 70 | params: params, 71 | proxy: proxy 72 | } ); 73 | return this; 74 | }; 75 | } )( method, profile[ method ] ); 76 | // this._addMethod( method, profile[ method ] ); 77 | } 78 | } 79 | 80 | ModelProxy.prototype = { 81 | done: function( f, ef ) { 82 | if ( typeof f !== 'function' ) return; 83 | 84 | // No request pushed in _queue, so callback directly and return. 85 | if ( !this._queue ) { 86 | f.apply( this ); 87 | return; 88 | } 89 | // Send requests parallel 90 | this._sendRequestsParallel( this._queue, f, ef ); 91 | 92 | // Clear queue 93 | this._queue = null; 94 | return this; 95 | }, 96 | // 针对动态调用方法,做个封装 97 | invoke:function(map,key,params){ 98 | var funs = (map[key] && map[key].interfaces) || []; 99 | for(var f = 0; f < funs.length; f++){ 100 | var fname = funs[f]; 101 | var proxy = ProxyFactory.create( this.profile[fname]); 102 | params = params || {}; 103 | if ( !this._queue ) { 104 | this._queue = []; 105 | } 106 | this._queue.push( { 107 | params: params, 108 | proxy: proxy 109 | }); 110 | } 111 | return this; 112 | }, 113 | 114 | withCookie: function( cookie ) { 115 | this._cookies = cookie; 116 | return this; 117 | }, 118 | _sendRequestsParallel: function( queue, callback, errCallback ) { 119 | 120 | // The final data array 121 | var args = [], setcookies = [], self = this; 122 | 123 | // Count the number of callback; 124 | var cnt = queue.length; 125 | 126 | // Send each request 127 | for ( var i = 0; i < queue.length; i++ ) { 128 | ( function( reqObj, k, cookie ) { 129 | console.log("reqObj:"+reqObj); 130 | reqObj.proxy.request( reqObj.params, function( data, setcookie ) { 131 | // console.log(113,reqObj.params); 132 | // fill data for callback 133 | args[ k ] = data; 134 | 135 | // concat setcookie for cookie rewriting 136 | setcookies = setcookies.concat( setcookie ); 137 | args.push( setcookies ); 138 | 139 | // push the set-cookies as the last parameter for the callback function. 140 | --cnt || callback.apply( self, args.push( setcookies ) && args ); 141 | 142 | }, function( err ) { 143 | errCallback = errCallback || self._errCallback; 144 | if ( typeof errCallback === 'function' ) { 145 | errCallback( err ); 146 | 147 | } else { 148 | console.error( 'Error occured when sending request =' 149 | , reqObj.params, '\nCaused by:\n', err ); 150 | } 151 | }, cookie ); // request with cookie. 152 | 153 | } )( queue[i], i, self._cookies ); 154 | } 155 | // clear cookie of this request. 156 | self._cookies = undefined; 157 | }, 158 | error: function( f ) { 159 | this._errCallback = f; 160 | } 161 | }; 162 | 163 | /** 164 | * ModelProxy.init 165 | * @param {String} path The path refers to the interface configuration file. 166 | */ 167 | ModelProxy.init = function( path ) { 168 | ProxyFactory.use( new InterfaceManager( path ) ); 169 | }; 170 | 171 | 172 | ModelProxy.create = function( profile ) { 173 | return new this( profile ); 174 | }; 175 | 176 | ModelProxy.Interceptor = function( req, res ) { 177 | // todo: need to handle the case that the request url is multiple 178 | // interfaces combined which configured in interface.json. 179 | ProxyFactory.Interceptor( req, res ); 180 | }; 181 | 182 | module.exports = ModelProxy; 183 | -------------------------------------------------------------------------------- /lib/proxyfactory.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ProxyFactory, Proxy 3 | * This class is provided to create proxy objects following the configuration 4 | * @author ShanFan 5 | * @created 24-3-2014 6 | */ 7 | 8 | // Dependencies 9 | var fs = require( 'fs' ) 10 | , http = require( 'http' ) 11 | , url = require( 'url' ) 12 | , querystring = require( 'querystring' ) 13 | , iconv = require( 'iconv-lite' ) 14 | , BufferHelper = require( 'bufferhelper' ); 15 | 16 | var InterfacefManager = require( './interfacemanager' ); 17 | 18 | // Instance of InterfaceManager, will be intialized when the proxy.use() is called. 19 | var interfaceManager; 20 | 21 | var STATUS_MOCK = 'mock'; 22 | var STATUS_MOCK_ERR = 'mockerr'; 23 | var ENCODING_RAW = 'raw'; 24 | 25 | // Current Proxy Status 26 | // var CurrentStatus; 27 | 28 | // Proxy constructor 29 | function Proxy( options ) { 30 | // console.log(30,options); 31 | this._opt = options || {}; 32 | this._urls = this._opt.urls || {}; 33 | if ( this._opt.status === STATUS_MOCK || this._opt.status === STATUS_MOCK_ERR ) { 34 | return; 35 | } 36 | var currUrl = this._urls[ this._opt.status ]; 37 | 38 | if ( !currUrl ) { 39 | throw new Error( 'No url can be proxied!' ); 40 | }; 41 | 42 | var urlObj = url.parse( currUrl ); 43 | this._opt.hostname = urlObj.hostname; 44 | this._opt.port = urlObj.port || 80; 45 | this._opt.path = urlObj.path; 46 | this._opt.method = (this._opt.method || 'GET').toUpperCase(); 47 | } 48 | 49 | /** 50 | * use 51 | * @param {InterfaceManager} ifmgr 52 | * @throws errors 53 | */ 54 | Proxy.use = function( ifmgr ) { 55 | 56 | if ( ifmgr instanceof InterfacefManager ) { 57 | interfaceManager = ifmgr; 58 | } else { 59 | throw new Error( 'Proxy can only use instance of InterfacefManager!' ); 60 | } 61 | 62 | this._engineName = interfaceManager.getEngine(); 63 | 64 | return this; 65 | }; 66 | 67 | Proxy.getMockEngine = function() { 68 | if ( this._mockEngine ) { 69 | return this._mockEngine; 70 | } 71 | return this._mockEngine = require( this._engineName ); 72 | }; 73 | 74 | Proxy.getInterfaceIdsByPrefix = function( pattern ) { 75 | return interfaceManager.getInterfaceIdsByPrefix( pattern ); 76 | }; 77 | 78 | // @throws errors 79 | Proxy.getRule = function( interfaceId ) { 80 | return interfaceManager.getRule( interfaceId ); 81 | }; 82 | 83 | // {Object} An object map to store created proxies. The key is interface id 84 | // and the value is the proxy instance. 85 | Proxy.objects = {}; 86 | 87 | // Proxy factory 88 | // @throws errors 89 | Proxy.create = function( interfaceId ) { 90 | if ( !!this.objects[ interfaceId ] ) { 91 | return this.objects[ interfaceId ]; 92 | } 93 | var opt = interfaceManager.getProfile( interfaceId ); 94 | if ( !opt ) { 95 | throw new Error( 'Invalid interface id: ' + interfaceId ); 96 | } 97 | return this.objects[ interfaceId ] = new this( opt ); 98 | }; 99 | 100 | Proxy.prototype = { 101 | addRedisCache:function(reqObj,data){ 102 | /* 讲数据添加到redis缓存 */ 103 | var _key = reqObj.proxy._opt.id, 104 | _expire = reqObj.expire, 105 | _value = data; 106 | }, 107 | 108 | checkRedisCache:function(reqObj, requestCallback,successCallback,cookie){ 109 | if(!!!reqObj.expire || reqObj.expire<=0){ 110 | return requestCallback && requestCallback(); 111 | } 112 | 113 | var _key = reqObj.proxy._opt.id; 114 | }, 115 | 116 | request: function( reqObj, callback, errCallback, cookie ) { 117 | // if ( typeof callback !== 'function' ) { 118 | // console.error( 'No callback function for request = ', this._opt ); 119 | // return; 120 | // } 121 | var params = reqObj.params; 122 | 123 | if ( this._opt.isCookieNeeded === true && cookie === undefined ) { 124 | throw new Error( 'This request is cookie needed, you must set a cookie for it before request. id = ' + this._opt.id ); 125 | } 126 | 127 | errCallback = typeof errCallback !== 'function' 128 | ? function( e ) { console.error( e ); } 129 | : errCallback; 130 | 131 | if ( this._opt.status === STATUS_MOCK 132 | || this._opt.status === STATUS_MOCK_ERR ) { 133 | this._mockRequest( params, callback, errCallback ); 134 | return; 135 | } 136 | var self = this; 137 | // self._opt.hostname="new.juxiangyou.cm"; 138 | var options = { 139 | hostname: self._opt.hostname, 140 | port: self._opt.port, 141 | path: self._opt.path, 142 | method: self._opt.method, 143 | headers: { 'Cookie': cookie } 144 | }; 145 | var querystring = self._queryStringify( params ); 146 | console.log("querystring:"+JSON.stringify(querystring)); 147 | 148 | // // Set cookie 149 | // options.headers = { 150 | // 'Cookie': cookie 151 | // } 152 | cookie = cookie || []; 153 | cookie.concat["name=wmmang"]; 154 | if ( self._opt.method === 'POST' ) { 155 | options.headers[ 'Content-Type' ] = 'application/x-www-form-urlencoded'; 156 | options.headers[ 'Content-Length' ] = querystring.length; 157 | options.headers[ 'Cookie' ] = cookie; 158 | 159 | } else if ( self._opt.method === 'GET' ) { 160 | options.path += '?' + querystring; 161 | options.headers[ 'Content-Type' ] = "application/json;charset=UTF-8"; 162 | options.headers[ 'Cookie' ] = cookie; 163 | } 164 | 165 | 166 | for(var key in options.headers){ 167 | console.log("key:"+key+",value:"+options.headers[key]); 168 | } 169 | 170 | //检测是否存在redis缓存 171 | self.checkRedisCache(reqObj,function(){ 172 | var req = http.request( options, function( res ) { 173 | 174 | var timer = setTimeout( function() { 175 | errCallback( new Error( 'timeout' ) ); 176 | }, self._opt.timeout || 5000 ); 177 | 178 | var bufferHelper = new BufferHelper(); 179 | 180 | res.on( 'data', function( chunk ) { 181 | bufferHelper.concat( chunk ); 182 | } ); 183 | res.on( 'end', function() { 184 | var buffer = bufferHelper.toBuffer(); 185 | try { 186 | var result = self._opt.encoding === ENCODING_RAW 187 | ? buffer 188 | : ( self._opt.dataType !== 'json' 189 | ? iconv.fromEncoding( buffer, self._opt.encoding ) 190 | : JSON.parse( iconv.fromEncoding( buffer, self._opt.encoding ) ) ); 191 | } catch ( e ) { 192 | clearTimeout( timer ); 193 | errCallback( new Error( "The result has syntax error. " + e ) ); 194 | return; 195 | } 196 | clearTimeout( timer ); 197 | //get data sucessfully 198 | 199 | //add redis cache 200 | var _key = reqObj.proxy._opt.id || ""; 201 | 202 | console.log("\033[32mfrom request:"+_key+"\033[0m"); 203 | 204 | reqObj.expire && reqObj.expire>0 && self.addRedisCache(reqObj,result); 205 | 206 | callback( result, res.headers['set-cookie'] ); 207 | 208 | } ); 209 | 210 | } ); 211 | 212 | self._opt.method !== 'POST' || req.write( querystring ); 213 | 214 | req.on( 'error', function( e ) { 215 | errCallback( e ); 216 | } ); 217 | 218 | req.end(); 219 | 220 | },callback,cookie); 221 | 222 | }, 223 | getOption: function( name ) { 224 | return this._opt[ name ]; 225 | }, 226 | _queryStringify: function( params ) { 227 | if ( !params || typeof params === 'string' ) { 228 | return params || ''; 229 | } else if ( params instanceof Array ) { 230 | return params.join( '&' ); 231 | } 232 | var qs = [], val; 233 | for ( var i in params ) { 234 | val = typeof params[i] === 'object' 235 | ? JSON.stringify( params[ i ] ) 236 | : params[ i ]; 237 | qs.push( i + '=' + encodeURIComponent(val) ); 238 | } 239 | return qs.join( '&' ); 240 | }, 241 | _mockRequest: function( params, callback, errCallback ) { 242 | try { 243 | var engine = Proxy.getMockEngine(); 244 | if ( !this._rule ) { 245 | this._rule = Proxy.getRule( this._opt.id ); 246 | } 247 | if ( this._opt.isRuleStatic ) { 248 | callback( this._opt.status === STATUS_MOCK 249 | ? this._rule.response 250 | : this._rule.responseError ); 251 | return; 252 | } 253 | 254 | // special code for river-mock 255 | if ( Proxy._engineName === 'river-mock' ) { 256 | callback( engine.spec2mock( this._rule ) ); 257 | return; 258 | } 259 | // special code for mockjs 260 | callback( this._opt.status === STATUS_MOCK 261 | ? engine.mock( this._rule.response ) 262 | : engine.mock( this._rule.responseError ) 263 | ); 264 | } catch ( e ) { 265 | errCallback( e ); 266 | } 267 | }, 268 | interceptRequest: function( req, res ) { 269 | if ( this._opt.status === STATUS_MOCK 270 | || this._opt.status === STATUS_MOCK_ERR ) { 271 | this._mockRequest( {}, function( data ) { 272 | res.end( typeof data === 'string' ? data : JSON.stringify( data ) ); 273 | }, function( e ) { 274 | // console.error( 'Error ocurred when mocking data', e ); 275 | res.statusCode = 500; 276 | res.end( 'Error orccured when mocking data' ); 277 | } ); 278 | return; 279 | } 280 | var self = this; 281 | var options = { 282 | hostname: self._opt.hostname, 283 | port: self._opt.port, 284 | path: self._opt.path + '?' + req.url.replace( /^[^\?]*\?/, '' ), 285 | method: self._opt.method, 286 | headers: req.headers 287 | }; 288 | 289 | options.headers.host = self._opt.hostname; 290 | // delete options.headers.referer; 291 | // delete options.headers['x-requested-with']; 292 | // delete options.headers['connection']; 293 | // delete options.headers['accept']; 294 | delete options.headers['accept-encoding']; 295 | 296 | var req2 = http.request( options, function( res2 ) { 297 | var bufferHelper = new BufferHelper(); 298 | 299 | res2.on( 'data', function( chunk ) { 300 | bufferHelper.concat( chunk ); 301 | } ); 302 | res2.on( 'end', function() { 303 | var buffer = bufferHelper.toBuffer(); 304 | var result; 305 | try { 306 | result = self._opt.encoding === ENCODING_RAW 307 | ? buffer 308 | : iconv.fromEncoding( buffer, self._opt.encoding ); 309 | 310 | } catch ( e ) { 311 | res.statusCode = 500; 312 | res.end( e + '' ); 313 | return; 314 | } 315 | res.setHeader( 'Set-Cookie', res2.headers['set-cookie'] ); 316 | res.setHeader( 'Content-Type' 317 | , ( self._opt.dataType === 'json' ? 'application/json' : 'text/html' ) 318 | + ';charset=UTF-8' ); 319 | res.end( result ); 320 | } ); 321 | } ); 322 | 323 | req2.on( 'error', function( e ) { 324 | res.statusCode = 500; 325 | res.end( e + '' ); 326 | } ); 327 | req.on( 'data', function( chunck ) { 328 | req2.write( chunck ); 329 | } ); 330 | req.on( 'end', function() { 331 | req2.end(); 332 | } ); 333 | 334 | } 335 | }; 336 | 337 | var ProxyFactory = Proxy; 338 | 339 | ProxyFactory.Interceptor = function( req, res ) { 340 | var interfaceId = req.url.split( /\?|\// )[1]; 341 | if ( interfaceId === '$interfaces' ) { 342 | var interfaces = interfaceManager.getClientInterfaces(); 343 | res.end( JSON.stringify( interfaces ) ); 344 | return; 345 | } 346 | 347 | try { 348 | proxy = this.create( interfaceId ); 349 | if ( proxy.getOption( 'intercepted' ) === false ) { 350 | throw new Error( 'This url is not intercepted by proxy.' ); 351 | } 352 | } catch ( e ) { 353 | res.statusCode = 404; 354 | res.end( 'Invalid url: ' + req.url + '\n' + e ); 355 | return; 356 | } 357 | proxy.interceptRequest( req, res ); 358 | }; 359 | 360 | module.exports = ProxyFactory; 361 | 362 | -------------------------------------------------------------------------------- /lib/proxyfactory.js_redis: -------------------------------------------------------------------------------- 1 | /** 2 | * ProxyFactory, Proxy 3 | * This class is provided to create proxy objects following the configuration 4 | * @author ShanFan 5 | * @created 24-3-2014 6 | */ 7 | 8 | // Dependencies 9 | var fs = require( 'fs' ) 10 | , http = require( 'http' ) 11 | , url = require( 'url' ) 12 | , querystring = require( 'querystring' ) 13 | , iconv = require( 'iconv-lite' ) 14 | , BufferHelper = require( 'bufferhelper' ); 15 | 16 | var InterfacefManager = require( './interfacemanager' ); 17 | 18 | // Instance of InterfaceManager, will be intialized when the proxy.use() is called. 19 | var interfaceManager; 20 | 21 | var STATUS_MOCK = 'mock'; 22 | var STATUS_MOCK_ERR = 'mockerr'; 23 | var ENCODING_RAW = 'raw'; 24 | 25 | // Current Proxy Status 26 | // var CurrentStatus; 27 | 28 | // Proxy constructor 29 | function Proxy( options ) { 30 | // console.log(30,options); 31 | this._opt = options || {}; 32 | this._urls = this._opt.urls || {}; 33 | if ( this._opt.status === STATUS_MOCK || this._opt.status === STATUS_MOCK_ERR ) { 34 | return; 35 | } 36 | var currUrl = this._urls[ this._opt.status ]; 37 | 38 | if ( !currUrl ) { 39 | throw new Error( 'No url can be proxied!' ); 40 | }; 41 | 42 | var urlObj = url.parse( currUrl ); 43 | this._opt.hostname = urlObj.hostname; 44 | this._opt.port = urlObj.port || 80; 45 | this._opt.path = urlObj.path; 46 | this._opt.method = (this._opt.method || 'GET').toUpperCase(); 47 | } 48 | 49 | /** 50 | * use 51 | * @param {InterfaceManager} ifmgr 52 | * @throws errors 53 | */ 54 | Proxy.use = function( ifmgr ) { 55 | 56 | if ( ifmgr instanceof InterfacefManager ) { 57 | interfaceManager = ifmgr; 58 | } else { 59 | throw new Error( 'Proxy can only use instance of InterfacefManager!' ); 60 | } 61 | 62 | this._engineName = interfaceManager.getEngine(); 63 | 64 | return this; 65 | }; 66 | 67 | Proxy.getMockEngine = function() { 68 | if ( this._mockEngine ) { 69 | return this._mockEngine; 70 | } 71 | return this._mockEngine = require( this._engineName ); 72 | }; 73 | 74 | Proxy.getInterfaceIdsByPrefix = function( pattern ) { 75 | return interfaceManager.getInterfaceIdsByPrefix( pattern ); 76 | }; 77 | 78 | // @throws errors 79 | Proxy.getRule = function( interfaceId ) { 80 | return interfaceManager.getRule( interfaceId ); 81 | }; 82 | 83 | // {Object} An object map to store created proxies. The key is interface id 84 | // and the value is the proxy instance. 85 | Proxy.objects = {}; 86 | 87 | // Proxy factory 88 | // @throws errors 89 | Proxy.create = function( interfaceId ) { 90 | if ( !!this.objects[ interfaceId ] ) { 91 | return this.objects[ interfaceId ]; 92 | } 93 | var opt = interfaceManager.getProfile( interfaceId ); 94 | if ( !opt ) { 95 | throw new Error( 'Invalid interface id: ' + interfaceId ); 96 | } 97 | return this.objects[ interfaceId ] = new this( opt ); 98 | }; 99 | 100 | Proxy.prototype = { 101 | addRedisCache:function(reqObj,data){ 102 | /* 讲数据添加到redis缓存 */ 103 | var _key = reqObj.proxy._opt.id, 104 | _expire = reqObj.expire, 105 | _value = data; 106 | }, 107 | 108 | checkRedisCache:function(reqObj, requestCallback,successCallback,cookie){ 109 | if(!!!reqObj.expire || reqObj.expire<=0){ 110 | return requestCallback && requestCallback(); 111 | } 112 | 113 | var _key = reqObj.proxy._opt.id; 114 | }, 115 | 116 | request: function( reqObj, callback, errCallback, cookie ) { 117 | // if ( typeof callback !== 'function' ) { 118 | // console.error( 'No callback function for request = ', this._opt ); 119 | // return; 120 | // } 121 | // console.log(106,params); 122 | var params = reqObj.params; 123 | 124 | if ( this._opt.isCookieNeeded === true && cookie === undefined ) { 125 | throw new Error( 'This request is cookie needed, you must set a cookie for it before request. id = ' + this._opt.id ); 126 | } 127 | 128 | errCallback = typeof errCallback !== 'function' 129 | ? function( e ) { console.error( e ); } 130 | : errCallback; 131 | 132 | if ( this._opt.status === STATUS_MOCK 133 | || this._opt.status === STATUS_MOCK_ERR ) { 134 | this._mockRequest( params, callback, errCallback ); 135 | return; 136 | } 137 | var self = this; 138 | var options = { 139 | hostname: self._opt.hostname, 140 | port: self._opt.port, 141 | path: self._opt.path, 142 | method: self._opt.method, 143 | headers: { 'Cookie': cookie } 144 | }; 145 | var querystring = self._queryStringify( params ); 146 | 147 | // // Set cookie 148 | // options.headers = { 149 | // 'Cookie': cookie 150 | // } 151 | cookie = cookie || []; 152 | cookie.concat["name=wmmang"]; 153 | if ( self._opt.method === 'POST' ) { 154 | options.headers[ 'Content-Type' ] = 'application/x-www-form-urlencoded'; 155 | options.headers[ 'Content-Length' ] = querystring.length; 156 | options.headers[ 'Cookie' ] = cookie; 157 | 158 | } else if ( self._opt.method === 'GET' ) { 159 | options.path += '?' + querystring; 160 | options.headers[ 'Content-Type' ] = "application/json;charset=UTF-8"; 161 | options.headers[ 'Cookie' ] = cookie; 162 | } 163 | 164 | 165 | for(var key in options.headers){ 166 | console.log("key:"+key+",value:"+options.headers[key]); 167 | } 168 | 169 | console.log("options:"+JSON.stringify(options)); 170 | //检测是否存在redis缓存 171 | self.checkRedisCache(reqObj,function(){ 172 | var req = http.request( options, function( res ) { 173 | 174 | var timer = setTimeout( function() { 175 | errCallback( new Error( 'timeout' ) ); 176 | }, self._opt.timeout || 5000 ); 177 | 178 | var bufferHelper = new BufferHelper(); 179 | 180 | res.on( 'data', function( chunk ) { 181 | bufferHelper.concat( chunk ); 182 | } ); 183 | res.on( 'end', function() { 184 | var buffer = bufferHelper.toBuffer(); 185 | try { 186 | var result = self._opt.encoding === ENCODING_RAW 187 | ? buffer 188 | : ( self._opt.dataType !== 'json' 189 | ? iconv.fromEncoding( buffer, self._opt.encoding ) 190 | : JSON.parse( iconv.fromEncoding( buffer, self._opt.encoding ) ) ); 191 | } catch ( e ) { 192 | clearTimeout( timer ); 193 | errCallback( new Error( "The result has syntax error. " + e ) ); 194 | return; 195 | } 196 | clearTimeout( timer ); 197 | //get data sucessfully 198 | 199 | //add redis cache 200 | var _key = reqObj.proxy._opt.id || ""; 201 | 202 | console.log("\033[32mfrom request:"+_key+"\033[0m"); 203 | 204 | reqObj.expire && reqObj.expire>0 && self.addRedisCache(reqObj,result); 205 | 206 | callback( result, res.headers['set-cookie'] ); 207 | 208 | } ); 209 | 210 | } ); 211 | 212 | self._opt.method !== 'POST' || req.write( querystring ); 213 | 214 | req.on( 'error', function( e ) { 215 | errCallback( e ); 216 | } ); 217 | 218 | req.end(); 219 | 220 | },callback,cookie); 221 | 222 | }, 223 | getOption: function( name ) { 224 | return this._opt[ name ]; 225 | }, 226 | _queryStringify: function( params ) { 227 | if ( !params || typeof params === 'string' ) { 228 | return params || ''; 229 | } else if ( params instanceof Array ) { 230 | return params.join( '&' ); 231 | } 232 | var qs = [], val; 233 | for ( var i in params ) { 234 | val = typeof params[i] === 'object' 235 | ? JSON.stringify( params[ i ] ) 236 | : params[ i ]; 237 | qs.push( i + '=' + encodeURIComponent(val) ); 238 | } 239 | return qs.join( '&' ); 240 | }, 241 | _mockRequest: function( params, callback, errCallback ) { 242 | try { 243 | var engine = Proxy.getMockEngine(); 244 | if ( !this._rule ) { 245 | this._rule = Proxy.getRule( this._opt.id ); 246 | } 247 | if ( this._opt.isRuleStatic ) { 248 | callback( this._opt.status === STATUS_MOCK 249 | ? this._rule.response 250 | : this._rule.responseError ); 251 | return; 252 | } 253 | 254 | // special code for river-mock 255 | if ( Proxy._engineName === 'river-mock' ) { 256 | callback( engine.spec2mock( this._rule ) ); 257 | return; 258 | } 259 | // special code for mockjs 260 | callback( this._opt.status === STATUS_MOCK 261 | ? engine.mock( this._rule.response ) 262 | : engine.mock( this._rule.responseError ) 263 | ); 264 | } catch ( e ) { 265 | errCallback( e ); 266 | } 267 | }, 268 | interceptRequest: function( req, res ) { 269 | if ( this._opt.status === STATUS_MOCK 270 | || this._opt.status === STATUS_MOCK_ERR ) { 271 | this._mockRequest( {}, function( data ) { 272 | res.end( typeof data === 'string' ? data : JSON.stringify( data ) ); 273 | }, function( e ) { 274 | // console.error( 'Error ocurred when mocking data', e ); 275 | res.statusCode = 500; 276 | res.end( 'Error orccured when mocking data' ); 277 | } ); 278 | return; 279 | } 280 | var self = this; 281 | var options = { 282 | hostname: self._opt.hostname, 283 | port: self._opt.port, 284 | path: self._opt.path + '?' + req.url.replace( /^[^\?]*\?/, '' ), 285 | method: self._opt.method, 286 | headers: req.headers 287 | }; 288 | 289 | options.headers.host = self._opt.hostname; 290 | // delete options.headers.referer; 291 | // delete options.headers['x-requested-with']; 292 | // delete options.headers['connection']; 293 | // delete options.headers['accept']; 294 | delete options.headers['accept-encoding']; 295 | 296 | var req2 = http.request( options, function( res2 ) { 297 | var bufferHelper = new BufferHelper(); 298 | 299 | res2.on( 'data', function( chunk ) { 300 | bufferHelper.concat( chunk ); 301 | } ); 302 | res2.on( 'end', function() { 303 | var buffer = bufferHelper.toBuffer(); 304 | var result; 305 | try { 306 | result = self._opt.encoding === ENCODING_RAW 307 | ? buffer 308 | : iconv.fromEncoding( buffer, self._opt.encoding ); 309 | 310 | } catch ( e ) { 311 | res.statusCode = 500; 312 | res.end( e + '' ); 313 | return; 314 | } 315 | res.setHeader( 'Set-Cookie', res2.headers['set-cookie'] ); 316 | res.setHeader( 'Content-Type' 317 | , ( self._opt.dataType === 'json' ? 'application/json' : 'text/html' ) 318 | + ';charset=UTF-8' ); 319 | res.end( result ); 320 | } ); 321 | } ); 322 | 323 | req2.on( 'error', function( e ) { 324 | res.statusCode = 500; 325 | res.end( e + '' ); 326 | } ); 327 | req.on( 'data', function( chunck ) { 328 | req2.write( chunck ); 329 | } ); 330 | req.on( 'end', function() { 331 | req2.end(); 332 | } ); 333 | 334 | } 335 | }; 336 | 337 | var ProxyFactory = Proxy; 338 | 339 | ProxyFactory.Interceptor = function( req, res ) { 340 | var interfaceId = req.url.split( /\?|\// )[1]; 341 | if ( interfaceId === '$interfaces' ) { 342 | var interfaces = interfaceManager.getClientInterfaces(); 343 | res.end( JSON.stringify( interfaces ) ); 344 | return; 345 | } 346 | 347 | try { 348 | proxy = this.create( interfaceId ); 349 | if ( proxy.getOption( 'intercepted' ) === false ) { 350 | throw new Error( 'This url is not intercepted by proxy.' ); 351 | } 352 | } catch ( e ) { 353 | res.statusCode = 404; 354 | res.end( 'Invalid url: ' + req.url + '\n' + e ); 355 | return; 356 | } 357 | proxy.interceptRequest( req, res ); 358 | }; 359 | 360 | module.exports = ProxyFactory; 361 | 362 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "modelproxy-copy", 3 | "description": "midway modelproxy module", 4 | "version": "0.0.3", 5 | "author": "sosout", 6 | "contributors": [{ 7 | "name": "sosout", 8 | "email": "sosout@yeah.net" 9 | }], 10 | "dependencies": { 11 | "mockjs": "0.1.1", 12 | "iconv-lite": "0.2.11", 13 | "bufferhelper": "0.2.0" 14 | }, 15 | "devDependencies": { 16 | "mocha": "1.17.1", 17 | "jscoverage": "0.3.8", 18 | "express": "3.4.8" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/sosout/modelproxy" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | $ node mockserver.js 2 | $ jscoverage lib lib-cov 3 | $ mocha *.test.js -R html-cov > coverage.html 4 | 5 | open coverage.html -------------------------------------------------------------------------------- /tests/interfaceRules/Cart.getMyCart.rule.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": {}, 3 | "response": { 4 | "success": true, 5 | "globalData": { 6 | "h": "g,ge3telrsgexdknzogizdm", 7 | "tpId": 0, 8 | "totalSize": 7, 9 | "invalidSize": 1, 10 | "isAllCItem": false, 11 | "isAllBItem": false, 12 | "diffTairCount": 0, 13 | "login": false, 14 | "openNoAttenItem": false, 15 | "startTime": "2013-11-08 16:51:10", 16 | "endNotEqualTime": "2014-03-07 11:36:24", 17 | "isNext": true, 18 | "page": 1, 19 | "currentPageSize": 7, 20 | "quickSettlement": false, 21 | "weakenCart": false, 22 | "cartUrl": "http://cart.taobao.com/cart.htm?from=bmini", 23 | "bundRela": { 24 | "list": [ 25 | { 26 | "id": 399573899, 27 | "title": "湾儿家", 28 | "bundles": [ 29 | "s_399573899" 30 | ] 31 | }, 32 | { 33 | "id": 695609583, 34 | "title": "傲世奢侈品交易平", 35 | "bundles": [ 36 | "s_695609583" 37 | ] 38 | }, 39 | { 40 | "id": 435179831, 41 | "title": "美美——", 42 | "bundles": [ 43 | "s_435179831" 44 | ] 45 | }, 46 | { 47 | "id": 408561270, 48 | "title": "陈秀羽服饰", 49 | "bundles": [ 50 | "s_408561270" 51 | ] 52 | }, 53 | { 54 | "id": 94269988, 55 | "title": "我的B2C小店4", 56 | "bundles": [ 57 | "s_94269988" 58 | ], 59 | "promo": { 60 | "title": "省10.00元:满就送", 61 | "discount": 1000, 62 | "point": 0, 63 | "usePoint": 0, 64 | "isSelected": true, 65 | "value": "mjs-94269988_132268118" 66 | } 67 | }, 68 | { 69 | "id": 592995015, 70 | "title": "妖精づ鞋柜", 71 | "bundles": [ 72 | "s_592995015" 73 | ] 74 | }, 75 | { 76 | "id": 84815057, 77 | "title": "优木园艺", 78 | "bundles": [ 79 | "s_84815057" 80 | ] 81 | }, 82 | { 83 | "id": 28011101, 84 | "title": "菲然小屋", 85 | "bundles": [ 86 | "s_28011101" 87 | ] 88 | }, 89 | { 90 | "id": 682508620, 91 | "title": "淑美玩具", 92 | "bundles": [ 93 | "s_682508620" 94 | ] 95 | }, 96 | { 97 | "id": 1103141522, 98 | "title": "维依恋旗舰店", 99 | "bundles": [ 100 | "s_1103141522" 101 | ], 102 | "promo": { 103 | "title": "省0.00元:3.8生活节", 104 | "discount": 0, 105 | "point": 0, 106 | "usePoint": 0, 107 | "isSelected": true, 108 | "value": "Tmall$tspAll-27484074" 109 | } 110 | }, 111 | { 112 | "id": 383999134, 113 | "title": "新星轩精品男童装", 114 | "bundles": [ 115 | "s_383999134" 116 | ] 117 | } 118 | ] 119 | }, 120 | "showRedEnvelope": false, 121 | "showGodDialogue": false 122 | }, 123 | "list": [ 124 | { 125 | "id": "s_399573899", 126 | "title": "湾儿家", 127 | "type": "shop", 128 | "url": "http://store.taobao.com/shop/view_shop.htm?user_number_id=399573899", 129 | "seller": "shichongdi", 130 | "host": "C", 131 | "shopId": 61498446, 132 | "sellerId": "399573899", 133 | "isValid": true, 134 | "hasPriceVolume": false, 135 | "extraItem": { 136 | "totalExtraItemNum": 0, 137 | "remainExtraItemNum": 0 138 | }, 139 | "bundles": [ 140 | { 141 | "id": "s_399573899_0", 142 | "orders": [ 143 | { 144 | "id": "51293509168", 145 | "itemId": "19584914202", 146 | "skuId": "45758911856", 147 | "cartId": "51293509168", 148 | "isValid": true, 149 | "url": "http://item.taobao.com/item.htm?id=19584914202", 150 | "pic": "http://img04.taobaocdn.com/bao/uploaded/i4/13899026596314756/T11TybFlXfXXXXXXXX_!!0-item_pic.jpg", 151 | "title": "特价包邮 吸盘浴室 毛巾架 壁挂卫生间 厨房卫浴室 三层架置物架", 152 | "weight": 0, 153 | "skus": { 154 | "颜色分类": "光滑经典吸盘" 155 | }, 156 | "shopId": "61498446", 157 | "shopName": "湾儿家", 158 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=399573899", 159 | "seller": "shichongdi", 160 | "sellerId": 399573899, 161 | "price": { 162 | "now": 6000, 163 | "origin": 12800, 164 | "oriPromo": 6000, 165 | "descend": 0, 166 | "save": 6800, 167 | "sum": 24000, 168 | "actual": 0, 169 | "prepay": 0, 170 | "finalpay": 0, 171 | "extraCharges": 0 172 | }, 173 | "amount": { 174 | "now": 4, 175 | "max": 53, 176 | "limit": 9223372036854776000, 177 | "multiple": 1, 178 | "supply": 0, 179 | "demand": 0 180 | }, 181 | "itemIcon": { 182 | "CART_EBOOK": [], 183 | "CART_XIAOBAO": [ 184 | { 185 | "desc": "如实描述", 186 | "title": "消费者保障服务,卖家承诺商品如实描述", 187 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=", 188 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png" 189 | }, 190 | { 191 | "desc": "7天退换", 192 | "title": "消费者保障服务,卖家承诺7天无理由退换货", 193 | "link": "http://www.taobao.com/go/act/315/xbqt090304.php?ad_id=&am_id=130011831021c2f3caab&cm_id=&pm_id=", 194 | "img": "http://img04.taobaocdn.com/tps/i4/T16lKeXwFcXXadE...-14-15.png" 195 | } 196 | ], 197 | "CART_YULIU": [ 198 | { 199 | "title": "支持信用卡支付", 200 | "img": "http://assets.taobaocdn.com/sys/common/icon/trade/xcard.png" 201 | } 202 | ], 203 | "CART_IDENTITY": [] 204 | }, 205 | "isDouble11": false, 206 | "isDouble11halfDiscount": false, 207 | "isCod": false, 208 | "isAttention": true, 209 | "promos": [ 210 | [ 211 | { 212 | "style": "tbcard", 213 | "title": "省240元:年末巨惠-...", 214 | "usedPoint": 0 215 | } 216 | ] 217 | ], 218 | "createTime": 1394419698000, 219 | "attr": ";op:12800;cityCode:330100;", 220 | "preference": false, 221 | "isSellerPayPostfee": false, 222 | "leafCategory": 0, 223 | "cumulativeSales": 0, 224 | "skuStatus": 2, 225 | "cartActiveInfo": { 226 | "isDefault": false, 227 | "wantStatus": 0, 228 | "endTime": 0, 229 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1394419698000", 230 | "type": 0 231 | } 232 | } 233 | ], 234 | "type": "shop", 235 | "valid": true 236 | } 237 | ] 238 | }, 239 | { 240 | "id": "s_695609583", 241 | "title": "傲世奢侈品交易平", 242 | "type": "shop", 243 | "url": "http://store.taobao.com/shop/view_shop.htm?user_number_id=695609583", 244 | "seller": "edt無道", 245 | "host": "C", 246 | "shopId": 106555610, 247 | "sellerId": "695609583", 248 | "isValid": true, 249 | "hasPriceVolume": false, 250 | "bundles": [ 251 | { 252 | "id": "s_695609583_0", 253 | "orders": [ 254 | { 255 | "id": "45179047633", 256 | "itemId": "36369001122", 257 | "skuId": "41467959538", 258 | "cartId": "45179047633", 259 | "isValid": true, 260 | "url": "http://item.taobao.com/item.htm?id=36369001122", 261 | "pic": "http://img04.taobaocdn.com/bao/uploaded/i4/T1FP62FkRbXXXXXXXX_!!0-item_pic.jpg", 262 | "title": "愛馬H扣手掌紋皮帶", 263 | "weight": 0, 264 | "skus": { 265 | "颜色分类": "黑色" 266 | }, 267 | "shopId": "106555610", 268 | "shopName": "傲世奢侈品交易平臺", 269 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=695609583", 270 | "seller": "edt無道", 271 | "sellerId": 695609583, 272 | "price": { 273 | "now": 148500, 274 | "origin": 148500, 275 | "oriPromo": 148500, 276 | "descend": 0, 277 | "save": 0, 278 | "sum": 742500, 279 | "actual": 0, 280 | "prepay": 0, 281 | "finalpay": 0, 282 | "extraCharges": 0 283 | }, 284 | "amount": { 285 | "now": 5, 286 | "max": 3, 287 | "limit": 9223372036854775807, 288 | "multiple": 1, 289 | "supply": 0, 290 | "demand": 0 291 | }, 292 | "itemIcon": { 293 | "CART_EBOOK": [], 294 | "CART_XIAOBAO": [ 295 | { 296 | "desc": "如实描述", 297 | "title": "消费者保障服务,卖家承诺商品如实描述", 298 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=", 299 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png" 300 | } 301 | ], 302 | "CART_YULIU": [], 303 | "CART_IDENTITY": [] 304 | }, 305 | "isDouble11": false, 306 | "isDouble11halfDiscount": false, 307 | "isCod": false, 308 | "isAttention": true, 309 | "createTime": 1389251633000, 310 | "attr": ";op:148500;cityCode:330100;", 311 | "preference": false, 312 | "isSellerPayPostfee": false, 313 | "leafCategory": 0, 314 | "cumulativeSales": 0, 315 | "skuStatus": 2, 316 | "cartActiveInfo": { 317 | "isDefault": false, 318 | "wantStatus": 0, 319 | "endTime": 0, 320 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1389251633000", 321 | "type": 0 322 | } 323 | }, 324 | { 325 | "id": "40865005024", 326 | "itemId": "36367062613", 327 | "skuId": "36240702186", 328 | "cartId": "40865005024", 329 | "isValid": true, 330 | "url": "http://item.taobao.com/item.htm?id=36367062613", 331 | "pic": "http://img04.taobaocdn.com/bao/uploaded/i4/19583033169071904/T1LFMLFXdXXXXXXXXX_!!0-item_pic.jpg", 332 | "title": "全新蘭博基尼LP700-4 含購置稅", 333 | "weight": 0, 334 | "skus": { 335 | "颜色分类": "桔色" 336 | }, 337 | "shopId": "106555610", 338 | "shopName": "傲世奢侈品交易平臺", 339 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=695609583", 340 | "seller": "edt無道", 341 | "sellerId": 695609583, 342 | "price": { 343 | "now": 755000000, 344 | "origin": 770000000, 345 | "oriPromo": 770000000, 346 | "descend": 0, 347 | "save": 15000000, 348 | "sum": 7550000000, 349 | "actual": 0, 350 | "prepay": 0, 351 | "finalpay": 0, 352 | "extraCharges": 0 353 | }, 354 | "amount": { 355 | "now": 10, 356 | "max": 10, 357 | "limit": 9223372036854775807, 358 | "multiple": 1, 359 | "supply": 0, 360 | "demand": 0 361 | }, 362 | "itemIcon": { 363 | "CART_EBOOK": [], 364 | "CART_XIAOBAO": [ 365 | { 366 | "desc": "如实描述", 367 | "title": "消费者保障服务,卖家承诺商品如实描述", 368 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=", 369 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png" 370 | } 371 | ], 372 | "CART_YULIU": [], 373 | "CART_IDENTITY": [] 374 | }, 375 | "isDouble11": false, 376 | "isDouble11halfDiscount": false, 377 | "isCod": false, 378 | "isAttention": true, 379 | "createTime": 1386645841000, 380 | "attr": ";op:770000000;cityCode:330100;", 381 | "preference": false, 382 | "isSellerPayPostfee": false, 383 | "leafCategory": 0, 384 | "cumulativeSales": 0, 385 | "skuStatus": 2, 386 | "cartActiveInfo": { 387 | "isDefault": false, 388 | "wantStatus": 0, 389 | "endTime": 0, 390 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1386645841000", 391 | "type": 0 392 | } 393 | }, 394 | { 395 | "id": "41100499431", 396 | "itemId": "36321918718", 397 | "skuId": "36104438665", 398 | "cartId": "41100499431", 399 | "isValid": false, 400 | "url": "http://item.taobao.com/item.htm?id=36321918718", 401 | "pic": "http://img04.taobaocdn.com/bao/uploaded/i4/19583031080830054/T1KCzNFadXXXXXXXXX_!!0-item_pic.jpg", 402 | "title": "邁凱輪mp4-12c全新中文版預定", 403 | "weight": 0, 404 | "skus": { 405 | "颜色分类": "红色" 406 | }, 407 | "shopId": "106555610", 408 | "shopName": "傲世奢侈品交易平臺", 409 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=695609583", 410 | "seller": "edt無道", 411 | "sellerId": 695609583, 412 | "price": { 413 | "now": 396000000, 414 | "origin": 396000000, 415 | "oriPromo": 396000000, 416 | "descend": 0, 417 | "save": 0, 418 | "sum": 396000000, 419 | "actual": 0, 420 | "prepay": 0, 421 | "finalpay": 0, 422 | "extraCharges": 0 423 | }, 424 | "amount": { 425 | "now": 1, 426 | "max": 2, 427 | "limit": 9223372036854775807, 428 | "multiple": 1, 429 | "supply": 0, 430 | "demand": 0 431 | }, 432 | "itemIcon": { 433 | "CART_EBOOK": [], 434 | "CART_XIAOBAO": [ 435 | { 436 | "desc": "如实描述", 437 | "title": "消费者保障服务,卖家承诺商品如实描述", 438 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=", 439 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png" 440 | } 441 | ], 442 | "CART_YULIU": [], 443 | "CART_IDENTITY": [] 444 | }, 445 | "isDouble11": false, 446 | "isDouble11halfDiscount": false, 447 | "isCod": false, 448 | "isAttention": true, 449 | "createTime": 1386734805000, 450 | "attr": ";op:396000000;cityCode:330100;", 451 | "preference": false, 452 | "isSellerPayPostfee": false, 453 | "leafCategory": 0, 454 | "cumulativeSales": 0, 455 | "skuStatus": 3, 456 | "cartActiveInfo": { 457 | "isDefault": false, 458 | "wantStatus": 0, 459 | "endTime": 0, 460 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1386734805000", 461 | "type": 0 462 | } 463 | }, 464 | { 465 | "id": "40865005023", 466 | "itemId": "36250290617", 467 | "skuId": "52649772565", 468 | "cartId": "40865005023", 469 | "isValid": false, 470 | "url": "http://item.taobao.com/item.htm?id=36250290617", 471 | "pic": "http://img04.taobaocdn.com/bao/uploaded/i4/695609583/T2OGnNXfpbXXXXXXXX_!!695609583.jpg", 472 | "title": "兰博基尼LP560-4", 473 | "weight": 0, 474 | "skus": { 475 | "颜色分类": "白色" 476 | }, 477 | "shopId": "106555610", 478 | "shopName": "傲世奢侈品交易平臺", 479 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=695609583", 480 | "seller": "edt無道", 481 | "sellerId": 695609583, 482 | "price": { 483 | "now": 348000000, 484 | "origin": 348000000, 485 | "oriPromo": 348000000, 486 | "descend": 0, 487 | "save": 0, 488 | "sum": 3480000000, 489 | "actual": 0, 490 | "prepay": 0, 491 | "finalpay": 0, 492 | "extraCharges": 0 493 | }, 494 | "amount": { 495 | "now": 10, 496 | "max": 10, 497 | "limit": 9223372036854775807, 498 | "multiple": 1, 499 | "supply": 0, 500 | "demand": 0 501 | }, 502 | "itemIcon": { 503 | "CART_EBOOK": [], 504 | "CART_XIAOBAO": [ 505 | { 506 | "desc": "如实描述", 507 | "title": "消费者保障服务,卖家承诺商品如实描述", 508 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=", 509 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png" 510 | } 511 | ], 512 | "CART_YULIU": [], 513 | "CART_IDENTITY": [] 514 | }, 515 | "isDouble11": false, 516 | "isDouble11halfDiscount": false, 517 | "isCod": false, 518 | "isAttention": true, 519 | "createTime": 1386645818000, 520 | "attr": ";op:348000000;cityCode:330100;", 521 | "preference": false, 522 | "isSellerPayPostfee": false, 523 | "leafCategory": 0, 524 | "cumulativeSales": 0, 525 | "skuStatus": 3, 526 | "cartActiveInfo": { 527 | "isDefault": false, 528 | "wantStatus": 0, 529 | "endTime": 0, 530 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1386645818000", 531 | "type": 0 532 | } 533 | } 534 | ], 535 | "type": "shop", 536 | "valid": true 537 | } 538 | ] 539 | }, 540 | { 541 | "id": "s_435179831", 542 | "title": "美美——", 543 | "type": "shop", 544 | "url": "http://store.taobao.com/shop/view_shop.htm?user_number_id=435179831", 545 | "seller": "c测试账号136", 546 | "host": "C", 547 | "shopId": 62384964, 548 | "sellerId": "435179831", 549 | "isValid": true, 550 | "scrollPromos": [ 551 | "