- 接口数据请求,解决跨域问题
#调用方法
import { http } from '@/utils/index.js'http.post('/student_chat/Chat/getChatList', {chat_type: ''}).then(res => {console.log(res,'res');}).catch(err => {console.log(err,'err');})http.get('', {}).then(res => {console.log(res,'res');}).catch(err => {console.log(err,'err');})
#调用结果
{"data": {"code": 1,"msg": "success","data": {"list": [{"chat_no": "kf_group","chat_type": "kf_group","user_no": "","chat_weight": 0,"msg_time": 0,"msg_unread_num": 0,"chat_target": {"target_title": "客服消息","target_icon": "http://cdn.zsdx.cn/student-app/images/message/sys_kf_user.png"},"chat_user": [],"latest_msg": {"msg_id": 0,"msg_type": "","msg_content": ""}}, {"chat_no": "chat_no_user_recommend","chat_type": "user_recommend","user_no": "user_no_user_recommend","chat_weight": 0,"msg_time": 1583801141,"msg_unread_num": 0,"chat_target": {"target_title": "同校同学推荐","target_icon": "http://cdn.zsdx.cn/student-app/images/message/user_recommend.png"},"chat_user": [],"latest_msg": {"msg_id": 0,"msg_type": "","msg_content": ""}}, {"chat_no": "fc_c7061c8d390cf","user_no": "st_c8142529456da","chat_type": "social","chat_weight": 0,"msg_time": 1585124777,"msg_unread_num": 0,"latest_msg": {"msg_id": 11371,"msg_type": "emoji","msg_content": "{\"url\":\"http://pic.wxhand.com/dev/phaadmin_image/04d98272df70e4017eb981f9cbeaf054.png\",\"title\":\"奇怪脸\",\"preview_url\":\"\",\"w\":154,\"h\":154}"},"chat_user": {"student_no": "st_c8142529456da","true_name": "你可愿与我再并肩","head_img": "http://pic.wxhand.com/dev/student_image/9e3f6cea939ce916e18b449d9549e49f!Thumbwidth320","sex": 1,"is_system": 0,"candle_step": 2,"candle_status": 0,"stuagent_level": 1},"chat_shop": [],"chat_company": []}, {"chat_no": "fc_a905236cd4dcf","user_no": "st_87ca94738e53c","chat_type": "social","chat_weight": 0,"msg_time": 1583801141,"msg_unread_num": 0,"latest_msg": {"msg_id": 9865,"msg_type": "card_image","msg_content": "{\"image_url\":\"http:\\/\\/cdn.zsdx.cn\\/student-app\\/images\\/message\\/msg_exam_result.png\",\"app_action\":{\"action\":\"app_page\",\"target\":\"forum_exam_result\",\"param\":{\"submit_id\":\"490\"}}}"},"chat_user": {"student_no": "st_87ca94738e53c","true_name": "掌大客服","head_img": "http://cdn.zsdx.cn/student-app/images/message/sys_kf.png","sex": 1,"is_system": 1,"candle_step": 0,"candle_status": 0,"stuagent_level": 0},"chat_shop": [],"chat_company": []}]}},"statusCode": 200,"header": {"Access-Control-Allow-Origin": "*","Content-Type": "application/json; charset=utf-8","X-Powered-By": "PHP/7.1.4","Access-Control-Allow-Methods": "POST, GET, OPTIONS","Access-Control-Allow-Credentials": "true","Access-Control-Max-Age": "86400","Date": "Wed, 25 Mar 2020 12:17:47 GMT","Access-Control-Allow-Headers": "zsdx-session-ticket, zsdx-app-type, zsdx-app-uuid, zsdx-version, zsdx-device-info, content-type","Set-Cookie": "SERVERID=d3438ece8edc13d4d09e5c8a2df783d4|1585138667|1585138188;Path=/","Transfer-Encoding": "Identity","Connection": "keep-alive"},"errMsg": "request:ok","config": {"url": "/student_chat/Chat/getChatList","data": {"chat_type": "social"},"method": "POST","baseUrl": "http://student-app-api.zsdx.cn/api","dataType": "json","params": {},"header": {"content-type": "application/json","zsdx-app-type": 1,"zsdx-app-uuid": "","zsdx-device-info": "...","zsdx-session-ticket": "...","zsdx-version": "..."},"custom": {},"sslVerify": true}}
#index.js
import Request from './request'const http = new Request()http.setConfig((config) => { /* 设置全局配置 */config.baseUrl = 'http://student-app-api.zsdx.cn/api' /* 根域名配置 */config.header = {...config.header,//请求头配置处理,配置接口需要的参数'zsdx-app-type': '','zsdx-app-uuid': '','zsdx-device-info': '','zsdx-session-ticket': '','zsdx-version': ''//请求头配置处理,配置接口需要的参数}return config})/*** 自定义验证器,如果返回true 则进入响应拦截器的响应成功函数(resolve),否则进入响应拦截器的响应错误函数(reject)* @param { Number } statusCode - 请求响应体statusCode(只读)* @return { Boolean } 如果为true,则 resolve, 否则 reject*/http.validateStatus = (statusCode) => {return statusCode === 200}http.interceptor.request((config, cancel) => { /* 请求之前拦截器 */config.header = {...config.header}/*if (!token) { // 如果token不存在,调用cancel 会取消本次请求,但是该函数的catch() 仍会执行cancel('token 不存在') // 接收一个参数,会传给catch((err) => {}) err.errMsg === 'token 不存在'}*/return config})http.interceptor.response((response) => { /* 请求之后拦截器 */// if (response.data.code !== 200) { // 服务端返回的状态码不等于200,则reject()// return Promise.reject(response)// }// if (response.config.custom.verification) { // 演示自定义参数的作用// return response.data// }if(response.data.code == '1001') { // 异常处理//...} else {return response}}, (response) => { // 请求错误做点什么return response})export {http}
#request.js
export default class Request {config = {baseUrl: '',header: {'content-type': 'application/json'},method: 'GET',dataType: 'json',// #ifndef MP-ALIPAY || APP-PLUSresponseType: 'text',// #endifcustom: {},// #ifdef MP-ALIPAYtimeout: 30000,// #endif// #ifdef APP-PLUSsslVerify: true// #endif}static posUrl (url) { /* 判断url是否为绝对路径 */return /(http|https):\/\/([\w.]+\/?)\S*/.test(url)}static mergeUrl (url, baseUrl, params) {let mergeUrl = Request.posUrl(url) ? url : `${baseUrl}${url}`if (Object.keys(params).length !== 0) {const paramsH = Request.addQueryString(params)mergeUrl += mergeUrl.includes('?') ? `&${paramsH}` : `?${paramsH}`}return mergeUrl}static addQueryString (params) {let paramsData = ''Object.keys(params).forEach(function (key) {paramsData += key + '=' + encodeURIComponent(params[key]) + '&'})return paramsData.substring(0, paramsData.length - 1)}/*** @property {Function} request 请求拦截器* @property {Function} response 响应拦截器* @type {{request: Request.interceptor.request, response: Request.interceptor.response}}*/interceptor = {/*** @param {Request~requestCallback} cb - 请求之前拦截,接收一个函数(config, cancel)=> {return config}。第一个参数为全局config,第二个参数为函数,调用则取消本次请求。*/request: (cb) => {if (cb) {this.requestBeforeFun = cb}},/*** @param {Request~responseCallback} cb 响应拦截器,对响应数据做点什么* @param {Request~responseErrCallback} ecb 响应拦截器,对响应错误做点什么*/response: (cb, ecb) => {if (cb) {this.requestComFun = cb}if (ecb) {this.requestComFail = ecb}}}requestBeforeFun (config) {return config}requestComFun (response) {return response}requestComFail (response) {return response}/*** 自定义验证器,如果返回true 则进入响应拦截器的响应成功函数(resolve),否则进入响应拦截器的响应错误函数(reject)* @param { Number } statusCode - 请求响应体statusCode(只读)* @return { Boolean } 如果为true,则 resolve, 否则 reject*/validateStatus (statusCode) {return statusCode === 200}/*** @Function* @param {Request~setConfigCallback} f - 设置全局默认配置*/setConfig (f) {this.config = f(this.config)}/*** @Function* @param {Object} options - 请求配置项* @prop {String} options.url - 请求路径* @prop {Object} options.data - 请求参数* @prop {Object} [options.responseType = config.responseType] [text|arraybuffer] - 响应的数据类型* @prop {Object} [options.dataType = config.dataType] - 如果设为 json,会尝试对返回的数据做一次 JSON.parse* @prop {Object} [options.header = config.header] - 请求header* @prop {Object} [options.method = config.method] - 请求方法* @returns {Promise<unknown>}*/async request (options = {}) {options.baseUrl = this.config.baseUrloptions.dataType = options.dataType || this.config.dataType// #ifndef MP-ALIPAY || APP-PLUSoptions.responseType = options.responseType || this.config.responseType// #endif// #ifdef MP-ALIPAYoptions.timeout = options.timeout || this.config.timeout// #endifoptions.url = options.url || ''options.data = options.data || {}options.params = options.params || {}options.header = options.header || this.config.headeroptions.method = options.method || this.config.methodoptions.custom = { ...this.config.custom, ...(options.custom || {}) }// #ifdef APP-PLUSoptions.sslVerify = options.sslVerify === undefined ? this.config.sslVerify : options.sslVerify// #endifoptions.getTask = options.getTask || this.config.getTaskreturn new Promise((resolve, reject) => {let next = trueconst cancel = (t = 'handle cancel', config = options) => {const err = {errMsg: t,config: config}reject(err)next = false}const handleRe = { ...this.requestBeforeFun(options, cancel) }const _config = { ...handleRe }if (!next) returnconst requestTask = uni.request({url: Request.mergeUrl(_config.url, _config.baseUrl, _config.params),data: _config.data,header: _config.header,method: _config.method,// #ifdef MP-ALIPAYtimeout: _config.timeout,// #endifdataType: _config.dataType,// #ifndef MP-ALIPAY || APP-PLUSresponseType: _config.responseType,// #endif// #ifdef APP-PLUSsslVerify: _config.sslVerify,// #endifcomplete: (response) => {response.config = handleReif (this.validateStatus(response.statusCode)) { // 成功response = this.requestComFun(response)resolve(response)} else {response = this.requestComFail(response)reject(response)}}})if (handleRe.getTask) {handleRe.getTask(requestTask, handleRe)}})}get (url, options = {}) {return this.request({url,method: 'GET',...options})}post (url, data, options = {}) {return this.request({url,data,method: 'POST',...options})}// #ifndef MP-ALIPAYput (url, data, options = {}) {return this.request({url,data,method: 'PUT',...options})}// #endif// #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-BAIDUdelete (url, data, options = {}) {return this.request({url,data,method: 'DELETE',...options})}// #endif// #ifdef APP-PLUS || H5 || MP-WEIXINconnect (url, data, options = {}) {return this.request({url,data,method: 'CONNECT',...options})}// #endif// #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-BAIDUhead (url, data, options = {}) {return this.request({url,data,method: 'HEAD',...options})}// #endif// #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-BAIDUoptions (url, data, options = {}) {return this.request({url,data,method: 'OPTIONS',...options})}// #endif// #ifdef APP-PLUS || H5 || MP-WEIXINtrace (url, data, options = {}) {return this.request({url,data,method: 'TRACE',...options})}// #endifupload (url, {// #ifdef APP-PLUSfiles,// #endif// #ifdef MP-ALIPAYfileType,// #endiffilePath,name,header,formData = {},custom = {},params = {},getTask}) {return new Promise((resolve, reject) => {let next = trueconst globalHeader = { ...this.config.header }delete globalHeader['content-type']delete globalHeader['Content-Type']const pubConfig = {baseUrl: this.config.baseUrl,url,// #ifdef MP-ALIPAYfileType,// #endiffilePath,method: 'UPLOAD',name,header: header || globalHeader,formData,params,custom: { ...this.config.custom, ...custom },getTask: getTask || this.config.getTask}// #ifdef APP-PLUSif (files) {pubConfig.files = files}// #endifconst cancel = (t = 'handle cancel', config = pubConfig) => {const err = {errMsg: t,config: config}reject(err)next = false}const handleRe = { ...this.requestBeforeFun(pubConfig, cancel) }const _config = {url: Request.mergeUrl(handleRe.url, handleRe.baseUrl, handleRe.params),// #ifdef MP-ALIPAYfileType: handleRe.fileType,// #endiffilePath: handleRe.filePath,name: handleRe.name,header: handleRe.header,formData: handleRe.formData,complete: (response) => {response.config = handleReif (typeof response.data === 'string') {response.data = JSON.parse(response.data)}if (this.validateStatus(response.statusCode)) { // 成功response = this.requestComFun(response)resolve(response)} else {response = this.requestComFail(response)reject(response)}}}// #ifdef APP-PLUSif (handleRe.files) {_config.files = handleRe.files}// #endifif (!next) returnconst requestTask = uni.uploadFile(_config)if (handleRe.getTask) {handleRe.getTask(requestTask, handleRe)}})}download (url, options = {}) {return new Promise((resolve, reject) => {let next = trueconst pubConfig = {baseUrl: this.config.baseUrl,url,method: 'DOWNLOAD',header: options.header || this.config.header,params: options.params || {},custom: { ...this.config.custom, ...(options.custom || {}) },getTask: options.getTask || this.config.getTask}const cancel = (t = 'handle cancel', config = pubConfig) => {const err = {errMsg: t,config: config}reject(err)next = false}const handleRe = { ...this.requestBeforeFun(pubConfig, cancel) }if (!next) returnconst requestTask = uni.downloadFile({url: Request.mergeUrl(handleRe.url, handleRe.baseUrl, handleRe.params),header: handleRe.header,complete: (response) => {response.config = handleReif (this.validateStatus(response.statusCode)) { // 成功response = this.requestComFun(response)resolve(response)} else {response = this.requestComFail(response)reject(response)}}})if (handleRe.getTask) {handleRe.getTask(requestTask, handleRe)}})}}/*** setConfig回调* @return {Object} - 返回操作后的config* @callback Request~setConfigCallback* @param {Object} config - 全局默认config*//*** 请求拦截器回调* @return {Object} - 返回操作后的config* @callback Request~requestCallback* @param {Object} config - 全局config* @param {Function} [cancel] - 取消请求钩子,调用会取消本次请求*//*** 响应拦截器回调* @return {Object} - 返回操作后的response* @callback Request~responseCallback* @param {Object} response - 请求结果 response*//*** 响应错误拦截器回调* @return {Object} - 返回操作后的response* @callback Request~responseErrCallback* @param {Object} response - 请求结果 response*/
#文档说明
- 基于 Promise 对象实现更简单的 request 使用方式,支持请求和响应拦截- 支持全局挂载- 支持多个全局配置实例- 支持自定义验证器- 支持文件上传/下载(如不使用可以删除class里upload,download 方法)- 把 http-request 文件夹放到项目 utils/ 目录下**Example**---创建实例``` javascriptconst http = new Request();
执行GET请求
http.get('/user/login', {params: {userName: 'name', password: '123456'}}).then(res => {}).catch(err => {})// 局部修改配置,局部配置优先级高于全局配置http.get('/user/login', {params: {userName: 'name', password: '123456'}, /* 会加在url上 */header: {}, /* 会覆盖全局header */dataType: 'json',// 注:如果局部custom与全局custom有同名属性,则后面的属性会覆盖前面的属性,相当于Object.assign(全局,局部)custom: {auth: true}, // 可以加一些自定义参数,在拦截器等地方使用。比如这里我加了一个auth,可在拦截器里拿到,如果true就传token// #ifndef MP-ALIPAY || APP-PLUSresponseType: 'text',// #endif// #ifdef MP-ALIPAYtimeout: 30000, // 仅支付宝小程序支持// #endif// #ifdef APP-PLUSsslVerify: true, // 验证 ssl 证书 仅5+App安卓端支持(HBuilderX 2.3.3+)// #endif// 返回当前请求的task, options。请勿在此处修改options。非必填getTask: (task, options) => {// setTimeout(() => {// task.abort()// }, 500)}}).then(res => {}).catch(err => {})
执行POST请求
http.post('/user/login', {userName: 'name', password: '123456'} ).then(res => {}).catch(err => {})// 局部修改配置,局部配置优先级高于全局配置http.post('/user/login', {userName: 'name', password: '123456'}, {params: {}, /* 会加在url上 */header: {}, /* 会覆盖全局header */dataType: 'json',// 注:如果局部custom与全局custom有同名属性,则后面的属性会覆盖前面的属性,相当于Object.assign(全局,局部)custom: {auth: true}, // 可以加一些自定义参数,在拦截器等地方使用。比如这里我加了一个auth,可在拦截器里拿到,如果true就传token// #ifndef MP-ALIPAY || APP-PLUSresponseType: 'text',// #endif// #ifdef MP-ALIPAYtimeout: 30000, // 仅支付宝小程序支持// #endif// #ifdef APP-PLUSsslVerify: true, // 验证 ssl 证书 仅5+App安卓端支持(HBuilderX 2.3.3+)// #endif// 返回当前请求的task, options。请勿在此处修改options。非必填getTask: (task, options) => {// setTimeout(() => {// task.abort()// }, 500)}}).then(res => {}).catch(err => {})
执行upload请求
http.upload('api/upload/img', {params: {}, /* 会加在url上 */files: [], // 仅5+App支持fileType: 'image/video/audio', // 仅支付宝小程序,且必填。filePath: '', // 要上传文件资源的路径。// 注:如果局部custom与全局custom有同名属性,则后面的属性会覆盖前面的属性,相当于Object.assign(全局,局部)custom: {auth: true}, // 可以加一些自定义参数,在拦截器等地方使用。比如这里我加了一个auth,可在拦截器里拿到,如果true就传tokenname: 'file', // 文件对应的 key , 开发者在服务器端通过这个 key 可以获取到文件二进制内容header: {},formData: {}, // HTTP 请求中其他额外的 form data// 返回当前请求的task, options。请勿在此处修改options。非必填getTask: (task, options) => {// task.onProgressUpdate((res) => {// console.log('上传进度' + res.progress);// console.log('已经上传的数据长度' + res.totalBytesSent);// console.log('预期需要上传的数据总长度' + res.totalBytesExpectedToSend);//// // 测试条件,取消上传任务。// if (res.progress > 50) {// uploadTask.abort();// }// });}}).then(res => {// 返回的res.data 已经进行JSON.parse}).catch(err => {})
luch-request API
http.request({method: 'POST', // 请求方法必须大写url: '/user/12345',data: {firstName: 'Fred',lastName: 'Flintstone'},params: { // 会拼接到url上token: '1111'},// 注:如果局部custom与全局custom有同名属性,则后面的属性会覆盖前面的属性,相当于Object.assign(全局,局部)custom: {}, // 自定义参数// 返回当前请求的task, options。请勿在此处修改options。非必填getTask: (task, options) => {// setTimeout(() => {// task.abort()// }, 500)}})// 具体参数说明:[uni.uploadFile](https://uniapp.dcloud.io/api/request/network-file)http.upload('api/upload/img', {params: {}, /* 会加在url上 */files: [], // 仅5+App支持fileType: 'image/video/audio', // 仅支付宝小程序,且必填。filePath: '', // 要上传文件资源的路径。name: 'file', // 文件对应的 key , 开发者在服务器端通过这个 key 可以获取到文件二进制内容header: {}, // 如填写,会覆盖全局header,custom: {}, // 自定义参数formData: {}, // HTTP 请求中其他额外的 form data// 返回当前请求的task, options。请勿在此处修改options。非必填getTask: (task, options) => {// setTimeout(() => {// task.abort()// }, 500)}}).then(res => {// 返回的res.data 已经进行JSON.parse}).catch(err => {})// 具体参数说明:[uni.downloadFile](https://uniapp.dcloud.io/api/request/network-file?id=downloadfile)http.download('api/download', {params: {}, /* 会加在url上 */header: {}, // 如填写,会覆盖全局header,custom: {}, // 自定义参数// 返回当前请求的task, options。非必填getTask: (task, options) => {// setTimeout(() => {// task.abort()// }, 500)}}).then(res => {}).catch(err => {})
请求方法别名 / 实例方法
http.request(config)http.get(url[, config])http.upload(url[, config])http.delete(url[, data[, config]])http.head(url[, data[, config]])http.post(url[, data[, config]])http.put(url[, data[, config]])http.connect(url[, data[, config]])http.options(url[, data[, config]])http.trace(url[, data[, config]])
全局请求配置
{baseUrl: '',header: {'content-type': 'application/json'},method: 'GET',dataType: 'json',// #ifndef MP-ALIPAY || APP-PLUSresponseType: 'text',// #endif// 注:如果局部custom与全局custom有同名属性,则后面的属性会覆盖前面的属性,相当于Object.assign(全局,局部)custom: {}, // 全局自定义参数默认值// #ifdef MP-ALIPAYtimeout: 30000,// #endif// #ifdef APP-PLUSsslVerify: true,// #endif// 局部优先级高于全局,返回当前请求的task,options。请勿在此处修改options。非必填// getTask: (task, options) => {// 相当于设置了请求超时时间500ms// setTimeout(() => {// task.abort()// }, 500)// }}
全局配置修改setConfig
/*** @description 修改全局默认配置* @param {Function}*/http.setConfig((config) => { /* config 为默认全局配置*/config.baseUrl = 'http://www.bbb.cn'; /* 根域名 */config.header = {a: 1,b: 2}return config})
自定义验证器validateStatus
/*** 自定义验证器,如果返回true 则进入响应拦截器的响应成功函数(resolve),否则进入响应拦截器的响应错误函数(reject)* @param { Number } statusCode - 请求响应体statusCode(只读)* @return { Boolean } 如果为true,则 resolve, 否则 reject*/http.validateStatus = (statusCode) => { // 默认return statusCode === 200}// 举个栗子http.validateStatus = (statusCode) => {return statusCode && statusCode >= 200 && statusCode < 300}
拦截器
在请求之前拦截
/*** @param { Function} cancel - 取消请求,调用cancel 会取消本次请求,但是该函数的catch() 仍会执行; 不会进入响应拦截器** @param {String} text ['handle cancel'| any] - catch((err) => {}) err.errMsg === 'handle cancel'。非必传,默认'handle cancel'* @cancel {Object} config - catch((err) => {}) err.config === config; 非必传,默认为request拦截器修改之前的config* function cancel(text, config) {}*/http.interceptor.request((config, cancel) => { /* cancel 为函数,如果调用会取消本次请求。需要注意:调用cancel,本次请求的catch仍会执行。必须return config */config.header = {...config.header,a: 1}// if (config.custom.auth) {// config.header.token = 'token'// }/**if (!token) { // 如果token不存在,调用cancel 会取消本次请求,不会进入响应拦截器,但是该函数的catch() 仍会执行cancel('token 不存在', config) // 把修改后的config传入,之后响应就可以拿到修改后的config。 如果调用了cancel但是不传修改后的config,则catch((err) => {}) err.config 为request拦截器修改之前的config}**/return config})
在请求之后拦截
http.interceptor.response((response) => { /* 对响应成功做点什么 (statusCode === 200),必须return response*/// if (response.data.code !== 200) { // 服务端返回的状态码不等于200,则reject()// return Promise.reject(response)// }// if (response.config.custom.verification) { // 演示自定义参数的作用// return response.data// }console.log(response)return response}, (response) => { /* 对响应错误做点什么 (statusCode !== 200),必须return response*/console.log(response)return response})
常见问题
- 为什么会请求两次?
- 总有些小白问这些很那啥的问题,有两种可能,一种是‘post三次握手’,还有一种可能是
本地访问接口时跨域请求,所以浏览器会先发一个option 去预测能否成功,然后再发一个真正的请求(自己观察请求头,Request Method,百度简单请求)。
- 总有些小白问这些很那啥的问题,有两种可能,一种是‘post三次握手’,还有一种可能是
- 如何跨域?
- 问的人不少,可以先百度了解一下。如何跨域
- post 怎么传不了数组的参数啊?
- uni-request
可以点击看一下uni-request 的api 文档,data支持的文件类型只有Object/String/ArrayBuffer这个真跟我没啥关系 0.0
- uni-request
- ‘Content-Type’ 为什么要小写?
- hbuilderX 更新至‘2.3.0.20190919’ 后,uni.request post请求,如果 ‘Content-Type’ 大写,就会在后面自动拼接‘ application/json’,请求头变成
Content-Type: application/json;charset=UTF-8 application/json,导致后端无法解析类型,Status Code 415,post 请求失败。但是小写就不会出现这个问题。至于为什么我也没有深究,我现在也不清楚这是他们的bug,还是以后就这样规范了。我能做的只有立马兼容,至于后边uni官方会不会继续变动也不清楚。
- hbuilderX 更新至‘2.3.0.20190919’ 后,uni.request post请求,如果 ‘Content-Type’ 大写,就会在后面自动拼接‘ application/json’,请求头变成
```
