import es6 from '../helpers/plugins/es6-promise' /** * 创建资源对象的工厂函数 * * @param {string} url 一个含有参数的URL模板 * @param {Object} paramDefaults URL参数的默认值 * @param {Object} actions 资源方法的默认值 * @param {Object} options 资源方法后缀字符串的默认值 * * @example * * ```js // 例如以下为后台提供的接口文档 // GET /api/users:获取所有用户资源 // GET /api/users/ID:获取某个指定用户的信息 // POST /api/users:新建一个用户 // PUT /api/users/ID:更新某个指定用户的信息 // DELETE /api/users/ID:删除某个指定用户 // 创建资源实例对象,接收四个参数url, paramDefaults, actions, options const user = new Resource('/api/users/:id', {id:'@id'}, { list: { method: 'GET', header: { Authorization: 'Authorization', }, }, }, { stripTrailingSlashes: true, suffix: 'Async', }) // 获取所有用户资源 user.listAsync() .then(res => console.log(res)) .catch(err => console.log(err)) // 获取ID=123用户的信息 user.getAsync({ id: 123 }) .then(res => console.log(res)) .catch(err => console.log(err)) // 新建一个用户 user.saveAsync({ name: '微信小程序' }) .then(res => console.log(res)) .catch(err => console.log(err)) // 更新ID=123用户的信息 user.updateAsync({ id: 123 },{ name: '微信小程序联盟' }) .then(res => console.log(res)) .catch(err => console.log(err)) // 删除ID=123用户的信息 user.deleteAsync({ id: 123 }) .then(res => console.log(res)) .catch(err => console.log(err)) // 返回的实例对象包含六个默认方法,getAsync、saveAsync、queryAsync、removeAsync、deleteAsync与一个自定义方法listAsync // // user.getAsync({id: 123}) 向/api/users/123发起一个GET请求,params作为填充url中变量,一般用来请求某个指定资源 // user.queryAsync(params) 同getAsync()方法使用类似,一般用来请求多个资源 // user.saveAsync(params, payload) 发起一个POST请,payload作为请求体,一般用来新建一个资源 // user.updateAsync(params, payload) 发起一个PUT请,payload作为请求体,一般用来更新某个指定资源 // user.deleteAsync(params, payload) 发起一个DELETE请求,payload作为请求体,一般用来移除某个指定资源 // user.removeAsync(params, payload) 同deleteAsync()方法使用类似,一般用来移除多个资源 // // user.listAsync({}) 向/api/users发送一个GET请求 * ``` */ class Resource { constructor(url = '', paramDefaults = {}, actions = {}, options = {}) { Object.assign(this, { url, paramDefaults, actions, options, }) this.__init() } /** * __init */ __init() { this.__initTools() this.__initDefaults() this.__initResource() } /** * Utility functions. */ __initTools() { this.__tools = { isArray(value) { return Array.isArray(value) }, isFunction(value) { return typeof value === 'function' }, isDefined(value) { return typeof value !== 'undefined' }, isObject(value) { return value !== null && typeof value === 'object' }, type(obj) { const toString = Object.prototype.toString if ( obj == null ) { return obj + '' } return typeof obj === 'object' || typeof obj === 'function' ? toString.call(obj) || 'object' : typeof obj }, clone(obj) { if (typeof obj !== 'object' || !obj) { return obj } let copy = {} for (let attr in obj) { if (obj.hasOwnProperty(attr)) { copy[attr] = obj[attr] } } return copy }, each(obj, iterator) { let i, key if (obj && typeof obj.length === 'number') { for (i = 0; i < obj.length; i++) { iterator.call(obj[i], obj[i], i) } } else if (this.isObject(obj)) { for (key in obj) { if (obj.hasOwnProperty(key)) { iterator.call(obj[key], obj[key], key) } } } return obj }, isPlainObject(obj) { let getProto = Object.getPrototypeOf let class2type = {} let toString = class2type.toString let hasOwn = class2type.hasOwnProperty let fnToString = hasOwn.toString let ObjectFunctionString = fnToString.call(Object) let proto, Ctor if (!obj || this.type(obj) !== '[object Object]') { return false } proto = getProto( obj ) if ( !proto ) { return true } Ctor = hasOwn.call(proto, 'constructor') && proto.constructor return typeof Ctor === 'function' && fnToString.call(Ctor) === ObjectFunctionString }, extend() { let src, copyIsArray, copy, name, options, clone, target = arguments[0] || {}, i = 1, length = arguments.length, deep = false; if (typeof target === 'boolean') { deep = target target = arguments[ i ] || {} i++ } if (typeof target !== 'object' && !this.isFunction(target)) { target = {} } if (i === length) { target = this i-- } for (; i < length; i++) { if ( (options = arguments[ i ]) != null ) { for (name in options) { src = target[name] copy = options[name] if (target === copy) { continue } if (deep && copy && (this.isPlainObject(copy) || (copyIsArray = isArray(copy)))) { if (copyIsArray) { copyIsArray = false clone = src && isArray(src) ? src : [] } else { clone = src && this.isPlainObject(src) ? src : {} } target[name] = this.extend(deep, clone, copy) } else if (copy !== undefined) { target[name] = copy } } } } return target }, encodeUriSegment(val) { return this.encodeUriQuery(val, true). replace(/%26/gi, '&'). replace(/%3D/gi, '='). replace(/%2B/gi, '+') }, encodeUriQuery(val, pctEncodeSpaces) { return encodeURIComponent(val). replace(/%40/gi, '@'). replace(/%3A/gi, ':'). replace(/%24/g, '$'). replace(/%2C/gi, ','). replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')) }, } } /** * __initDefaults */ __initDefaults() { this.defaults = { // 拦截器 interceptors: [{ request: (request) => { return request }, requestError: (requestError) => { return requestError }, response: (response) => { return response }, responseError: (responseError) => { return responseError }, }], // URL是否以‘/‘结尾 stripTrailingSlashes: true, // 方法名后缀字符串 suffix: 'Async', // 默认方法 actions: { 'get': { method: 'GET' }, 'save': { method: 'POST' }, 'update': { method: 'PUT' }, 'query': { method: 'GET' }, 'remove': { method: 'DELETE' }, 'delete': { method: 'DELETE' }, } } } /** * __initRoute */ __initRoute(template, defaults) { const that = this const PROTOCOL_AND_DOMAIN_REGEX = /^https?:\/\/[^\/]*/ function Route(template, defaults) { this.template = template this.defaults = that.__tools.extend({}, that.defaults, defaults) this.urlParams = {} } Route.prototype = { setUrlParams: function(config, params, actionUrl) { let self = this, url = actionUrl || self.template, val, encodedVal, protocolAndDomain = '' let urlParams = self.urlParams = {} that.__tools.each(url.split(/\W/), (param, key) => { if (param === 'hasOwnProperty') { throw `hasOwnProperty is not a valid parameter name.` } if (!(new RegExp('^\\d+$').test(param)) && param && (new RegExp('(^|[^\\\\]):' + param + '(\\W|$)').test(url))) { urlParams[param] = { isQueryParamValue: (new RegExp('\\?.*=:' + param + '(?:\\W|$)')).test(url) } } }) url = url.replace(/\\:/g, ':') url = url.replace(PROTOCOL_AND_DOMAIN_REGEX, function(match) { protocolAndDomain = match return '' }) params = params || {} that.__tools.each(self.urlParams, (paramInfo, urlParam) => { val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam] if (that.__tools.isDefined(val) && val !== null) { if (paramInfo.isQueryParamValue) { encodedVal = that.__tools.encodeUriQuery(val, true) } else { encodedVal = that.__tools.encodeUriSegment(val) } url = url.replace(new RegExp(':' + urlParam + '(\\W|$)', 'g'), function(match, p1) { return encodedVal + p1 }) } else { url = url.replace(new RegExp('(/?):' + urlParam + '(\\W|$)', 'g'), function(match, leadingSlashes, tail) { if (tail.charAt(0) === '/') { return tail } else { return leadingSlashes + tail } }) } }) // strip trailing slashes and set the url (unless this behavior is specifically disabled) if (self.defaults.stripTrailingSlashes) { url = url.replace(/\/+$/, '') || '/' } // then replace collapse `/.` if found in the last URL path segment before the query // E.g. `http://url.com/id./format?q=x` becomes `http://url.com/id.format?q=x` url = url.replace(/\/\.(?=\w+($|\?))/, '.') // replace escaped `/\.` with `/.` config.url = protocolAndDomain + url.replace(/\/\\\./, '/.') // set params - delegate param encoding to $http that.__tools.each(params, (value, key) => { if (!self.urlParams[key]) { config.data = config.data || {} config.data[key] = value } }) } } return new Route(template, defaults) } /** * __initResource */ __initResource() { const route = this.__initRoute(this.url, this.options) const actions = this.__tools.extend({}, this.defaults.actions, this.actions) for(let name in actions) { this[name + route.defaults.suffix] = (...args) => { const httpConfig = this.__setHttpConfig(route, actions[name], ...args) return this.__defaultRequest(httpConfig) } } } /** * 设置httpConfig */ __setHttpConfig(route, action, ...args) { const MEMBER_NAME_REGEX = /^(\.[a-zA-Z_$@][0-9a-zA-Z_$@]*)+$/ // 判断是否为有效的路径 const isValidDottedPath = (path) => { return (path != null && path !== '' && path !== 'hasOwnProperty' && MEMBER_NAME_REGEX.test('.' + path)) } // 查找路径 const lookupDottedPath = (obj, path) => { if (!isValidDottedPath(path)) { throw `badmember, Dotted member path ${path} is invalid.` } let keys = path.split('.') for (let i = 0, ii = keys.length; i < ii && this.__tools.isDefined(obj); i++) { let key = keys[i] obj = (obj !== null) ? obj[key] : undefined } return obj } // 提取参数 const extractParams = (data, actionParams) => { let ids = {} actionParams = this.__tools.extend({}, this.paramDefaults, actionParams) for(let key in actionParams) { let value = actionParams[key] if (this.__tools.isFunction(value)) { value = value(data) } ids[key] = value && value.charAt && value.charAt(0) === '@' ? lookupDottedPath(data, value.substr(1)) : value } return ids } let params = {}, data = {}, httpConfig = {}, hasBody = /^(POST|PUT|PATCH)$/i.test(action.method) // 判断参数个数 switch (args.length) { case 2: params = args[0] data = args[1] break case 1: if (hasBody) data = args[0] else params = args[0] break case 0: break default: throw `Expected up to 2 arguments [params, data, success, error], got ${args.length} arguments` } // 设置httpConfig for(let key in action) { switch (key) { default: httpConfig[key] = this.__tools.clone(action[key]) break case 'params': break } } // 判断是否为(POST|PUT|PATCH)请求,设置请求data if (hasBody) { httpConfig.data = data } // 解析URL route.setUrlParams(httpConfig, this.__tools.extend({}, extractParams(data, action.params || {}), params), action.url) return httpConfig } /** * 以uni.request作为底层方法 * @param {Object} httpConfig 请求参数配置 */ __defaultRequest(httpConfig) { // 注入拦截器 const chainInterceptors = (promise, interceptors) => { for (let i = 0, ii = interceptors.length; i < ii;) { let thenFn = interceptors[i++] let rejectFn = interceptors[i++] promise = promise.then(thenFn, rejectFn) } return promise } let requestInterceptors = [] let responseInterceptors = [] let reversedInterceptors = this.defaults.interceptors let promise = this.__resolve(httpConfig) // 缓存拦截器 reversedInterceptors.forEach((n, i) => { if (n.request || n.requestError) { requestInterceptors.push(n.request, n.requestError) } if (n.response || n.responseError) { responseInterceptors.unshift(n.response, n.responseError) } }) // 注入请求拦截器 promise = chainInterceptors(promise, requestInterceptors) // 发起HTTPS请求 promise = promise.then(this.__http) // 注入响应拦截器 promise = chainInterceptors(promise, responseInterceptors) // 接口调用成功,res = {data: '开发者服务器返回的内容'} promise = promise.then(res => res.data, err => err) return promise } /** * __http - uni.request */ __http(obj) { return new es6.Promise((resolve, reject) => { obj.success = (res) => resolve(res) obj.fail = (res) => reject(res) uni.request(obj) }) } /** * __resolve */ __resolve(res) { return new es6.Promise((resolve, reject) => { resolve(res) }) } /** * __reject */ __reject(res) { return new es6.Promise((resolve, reject) => { reject(res) }) } /** * setDefaults */ setDefaults(newDefaults) { this.__tools.extend(this.defaults, newDefaults) } } export default Resource