犯了一个低级错误, 应该是
server.send(buf, **0**,buf.length, 8000, '230.185.192.108');
没有足够的数据
(゚∀゚ )
暂时没有任何数据
(゚∀゚ )
暂时没有任何数据
黄土地的汉子 赞了回答 · 2019-12-20
犯了一个低级错误, 应该是
server.send(buf, **0**,buf.length, 8000, '230.185.192.108');
犯了一个低级错误, 应该是 {代码...}
关注 2 回答 2
黄土地的汉子 赞了文章 · 2019-09-27
最近遇到个需求:前端登录后,后端返回token
和token有效时间
,当token过期时要求用旧token去获取新的token,前端需要做到无痛刷新token
,即请求刷新token时要做到用户无感知。
当用户发起一个请求时,判断token是否已过期,若已过期则先调refreshToken
接口,拿到新的token后再继续执行之前的请求。
这个问题的难点在于:当同时发起多个请求,而刷新token的接口还没返回,此时其他请求该如何处理?接下来会循序渐进地分享一下整个过程。
由于后端返回了token的有效时间,可以有两种方法:
在请求发起前拦截每个请求,判断token的有效时间是否已经过期,若已过期,则将请求挂起,先刷新token后再继续请求。
不在请求前拦截,而是拦截返回后的数据。先发起请求,接口返回过期后,先刷新token,再进行一次重试。
方法一
PS:token有效时间建议是时间段,类似缓存的MaxAge,而不要是绝对时间。当服务器和本地时间不一致时,绝对时间会有问题。
方法二
综上,方法一和二优缺点是互补的,方法一有校验失败的风险(本地时间被篡改时,当然一般没有用户闲的蛋疼去改本地时间的啦),方法二更简单粗暴,等知道服务器已经过期了再重试一次,只是会耗多一个请求。
在这里博主选择了 方法二。
这里会使用axios来实现,方法一是请求前拦截,所以会使用axios.interceptors.request.use()
这个方法;
而方法二是请求后拦截,所以会使用axios.interceptors.response.use()
方法。
首先说明一下,项目中的token是存在localStorage
中的。request.js
基本骨架:
import axios from 'axios'
// 从localStorage中获取token
function getLocalToken () {
const token = window.localStorage.getItem('token')
return token
}
// 给实例添加一个setToken方法,用于登录后将最新token动态添加到header,同时将token保存在localStorage中
instance.setToken = (token) => {
instance.defaults.headers['X-Token'] = token
window.localStorage.setItem('token', token)
}
// 创建一个axios实例
const instance = axios.create({
baseURL: '/api',
timeout: 300000,
headers: {
'Content-Type': 'application/json',
'X-Token': getLocalToken() // headers塞token
}
})
// 拦截返回的数据
instance.interceptors.response.use(response => {
// 接下来会在这里进行token过期的逻辑处理
return response
}, error => {
return Promise.reject(error)
})
export default instance
这个是项目中一般的axios实例的封装,创建实例时,将本地已有的token放进header,然后export出去供调用。接下来就是如何拦截返回的数据啦。
后端接口一般会有一个约定好的数据结构,如:
{code: 1234, message: 'token过期', data: {}}
如我这里,后端约定当code === 1234
时表示token过期了,此时就要求刷新token。
instance.interceptors.response.use(response => {
const { code } = response.data
if (code === 1234) {
// 说明token过期了,刷新token
return refreshToken().then(res => {
// 刷新token成功,将最新的token更新到header中,同时保存在localStorage中
const { token } = res.data
instance.setToken(token)
// 获取当前失败的请求
const config = response.config
// 重置一下配置
config.headers['X-Token'] = token
config.baseURL = '' // url已经带上了/api,避免出现/api/api的情况
// 重试当前请求并返回promise
return instance(config)
}).catch(res => {
console.error('refreshtoken error =>', res)
//刷新token失败,神仙也救不了了,跳转到首页重新登录吧
window.location.href = '/'
})
}
return response
}, error => {
return Promise.reject(error)
})
function refreshToken () {
// instance是当前request.js中已创建的axios实例
return instance.post('/refreshtoken').then(res => res.data)
}
这里需要额外注意的是,response.config
就是原请求的配置,但这个是已经处理过了的,config.url
已经带上了baseUrl
,因此重试时需要去掉,同时token也是旧的,需要刷新下。
以上就基本做到了无痛刷新token,当token正常时,正常返回,当token已过期,则axios内部进行一次刷新token和重试。对调用者来说,axios内部的刷新token是一个黑盒,是无感知的,因此需求已经做到了。
上面的代码还是存在一些问题的,没有考虑到多次请求的问题,因此需要进一步优化。
如果refreshToken接口还没返回,此时再有一个过期的请求进来,上面的代码就会再一次执行refreshToken,这就会导致多次执行刷新token的接口,因此需要防止这个问题。我们可以在request.js
中用一个flag
来标记当前是否正在刷新token的状态,如果正在刷新则不再调用刷新token的接口。
// 是否正在刷新的标记
let isRefreshing = false
instance.interceptors.response.use(response => {
const { code } = response.data
if (code === 1234) {
if (!isRefreshing) {
isRefreshing = true
return refreshToken().then(res => {
const { token } = res.data
instance.setToken(token)
const config = response.config
config.headers['X-Token'] = token
config.baseURL = ''
return instance(config)
}).catch(res => {
console.error('refreshtoken error =>', res)
window.location.href = '/'
}).finally(() => {
isRefreshing = false
})
}
}
return response
}, error => {
return Promise.reject(error)
})
这样子就可以避免在刷新token时再进入方法了。但是这种做法是相当于把其他失败的接口给舍弃了,假如同时发起两个请求,且几乎同时返回,第一个请求肯定是进入了refreshToken后再重试,而第二个请求则被丢弃了,仍是返回失败,所以接下来还得解决其他接口的重试问题。
两个接口几乎同时发起和返回,第一个接口会进入刷新token后重试的流程,而第二个接口需要先存起来,然后等刷新token后再重试。同样,如果同时发起三个请求,此时需要缓存后两个接口,等刷新token后再重试。由于接口都是异步的,处理起来会有点麻烦。
当第二个过期的请求进来,token正在刷新,我们先将这个请求存到一个数组队列中,想办法让这个请求处于等待中,一直等到刷新token后再逐个重试清空请求队列。
那么如何做到让这个请求处于等待中呢?为了解决这个问题,我们得借助Promise
。将请求存进队列中后,同时返回一个Promise
,让这个Promise
一直处于Pending
状态(即不调用resolve),此时这个请求就会一直等啊等,只要我们不执行resolve,这个请求就会一直在等待。当刷新请求的接口返回来后,我们再调用resolve,逐个重试。最终代码:
// 是否正在刷新的标记
let isRefreshing = false
// 重试队列,每一项将是一个待执行的函数形式
let requests = []
instance.interceptors.response.use(response => {
const { code } = response.data
if (code === 1234) {
const config = response.config
if (!isRefreshing) {
isRefreshing = true
return refreshToken().then(res => {
const { token } = res.data
instance.setToken(token)
config.headers['X-Token'] = token
config.baseURL = ''
// 已经刷新了token,将所有队列中的请求进行重试
requests.forEach(cb => cb(token))
// 重试完了别忘了清空这个队列(掘金评论区同学指点)
requests = []
return instance(config)
}).catch(res => {
console.error('refreshtoken error =>', res)
window.location.href = '/'
}).finally(() => {
isRefreshing = false
})
} else {
// 正在刷新token,返回一个未执行resolve的promise
return new Promise((resolve) => {
// 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
requests.push((token) => {
config.baseURL = ''
config.headers['X-Token'] = token
resolve(instance(config))
})
})
}
}
return response
}, error => {
return Promise.reject(error)
})
这里可能比较难理解的是requests
这个队列中保存的是一个函数,这是为了让resolve不执行,先存起来,等刷新token后更方便调用这个函数使得resolve执行。至此,问题应该都解决了。
import axios from 'axios'
// 从localStorage中获取token
function getLocalToken () {
const token = window.localStorage.getItem('token')
return token
}
// 给实例添加一个setToken方法,用于登录后将最新token动态添加到header,同时将token保存在localStorage中
instance.setToken = (token) => {
instance.defaults.headers['X-Token'] = token
window.localStorage.setItem('token', token)
}
function refreshToken () {
// instance是当前request.js中已创建的axios实例
return instance.post('/refreshtoken').then(res => res.data)
}
// 创建一个axios实例
const instance = axios.create({
baseURL: '/api',
timeout: 300000,
headers: {
'Content-Type': 'application/json',
'X-Token': getLocalToken() // headers塞token
}
})
// 是否正在刷新的标记
let isRefreshing = false
// 重试队列,每一项将是一个待执行的函数形式
let requests = []
instance.interceptors.response.use(response => {
const { code } = response.data
if (code === 1234) {
const config = response.config
if (!isRefreshing) {
isRefreshing = true
return refreshToken().then(res => {
const { token } = res.data
instance.setToken(token)
config.headers['X-Token'] = token
config.baseURL = ''
// 已经刷新了token,将所有队列中的请求进行重试
requests.forEach(cb => cb(token))
requests = []
return instance(config)
}).catch(res => {
console.error('refreshtoken error =>', res)
window.location.href = '/'
}).finally(() => {
isRefreshing = false
})
} else {
// 正在刷新token,将返回一个未执行resolve的promise
return new Promise((resolve) => {
// 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
requests.push((token) => {
config.baseURL = ''
config.headers['X-Token'] = token
resolve(instance(config))
})
})
}
}
return response
}, error => {
return Promise.reject(error)
})
export default instance
希望对大家有帮助。感谢看到最后,感谢点赞^_^。
针对方法一的实现,请大家阅读: axios如何利用promise无痛刷新token(二)
查看原文最近遇到个需求:前端登录后,后端返回token和token有效时间,当token过期时要求用旧token去获取新的token,前端需要做到无痛刷新token,即请求刷新token时要做到用户无感知。
赞 166 收藏 116 评论 45
黄土地的汉子 回答了问题 · 2019-09-23
可以看看这篇文章
https://segmentfault.com/a/11...
可以看看这篇文章[链接]
关注 7 回答 6
黄土地的汉子 赞了回答 · 2019-06-27
不过期也就是说"过期时间为永久",只是过期策略的一个特例,所以需要有这个配置,至于选择什么样的策略,针对不同项目有不同的选择
不过期也就是说"过期时间为永久",只是过期策略的一个特例,所以需要有这个配置,至于选择什么样的策略,针对不同项目有不同的选择
关注 6 回答 6
黄土地的汉子 回答了问题 · 2018-10-26
ES6的class不支持mixins属性。可以考虑用 React高阶组件 代替。
React ES6 处理 mixin
https://blog.csdn.net/u012720...
ES6的class不支持mixins属性。可以考虑用 React高阶组件 代替。 React ES6 处理 mixin[链接]
关注 2 回答 1
黄土地的汉子 赞了回答 · 2018-08-19
既然是单线程异步,那么只能对IO密集型的业务能显著提高并发,对CPU密集型业务没有太大优势。
既然是单线程异步,那么只能对IO密集型的业务能显著提高并发,对CPU密集型业务没有太大优势。
关注 29 回答 7
黄土地的汉子 赞了回答 · 2018-08-19
对于node可以从process.nextTick()入手看
对于node可以从process.nextTick()入手看
关注 29 回答 7
黄土地的汉子 回答了问题 · 2018-08-03
楼主看这里,还需要改个配置项
https://segmentfault.com/q/10...
楼主看这里,还需要改个配置项[链接]
关注 4 回答 4
黄土地的汉子 赞了文章 · 2018-08-03
关于推送的集成请参考这篇文章,关于统计的集成请参考这篇文章,本篇文章将引导你集成分享功能。
在集成插件之前,需要在各大开放平台上成功注册应用,并通过审核(支持 3 个可选的主流平台)。支持的平台如下:
第一步:安装
npm install jshare-react-native --save
npm install jcore-react-native --save
react-native link
第二步:配置
如果 link 失败,则需要进行手动配置 settings.gradle
和 build.gradle
部分
2.1 配置settings.gradle
your project/settings.gradle
include ':app', ':jshare-react-native', ':jcore-react-native'
project(':jshare-react-native').projectDir = new File(rootProject.projectDir, '../node_modules/jshare-react-native/android')
project(':jcore-react-native').projectDir = new File(rootProject.projectDir, '../node_modules/jcore-react-native/android')
2.2 配置build.gradle
your project/app/build.gradle
android {
...
defaultConfig {
applicationId "your application id"
...
manifestPlaceholders = [
JSHARE_PKGNAME: "cn.jiguang.share.demo",
JPUSH_APPKEY: "your app key", //在此替换你的APPKey
JPUSH_CHANNEL: "developer-default", //应用渠道号, 默认即可
TENCENT_APPID: "your tencent app id"
]
}
...
signingConfigs {
debug {
storeFile file("jshare.jks") //你的签名文件路径
storePassword "sdkteam" //你的文件保存密码
keyAlias "jshare" //你的别名
keyPassword "sdkteam" //你的签名密码
}
release {
storeFile file("jshare.jks") //你的签名文件路径
storePassword "sdkteam" //你的文件保存密码
keyAlias "jshare" //你的别名
keyPassword "sdkteam" //你的签名密码
}
}
buildTypes {
release {
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
signingConfig signingConfigs.debug
}
debug {
signingConfig signingConfigs.debug
}
}
...
dependencies {
compile project(':jshare-react-native')
compile project(':jcore-react-native')
}
}
2.3 配置 AndroidManifest.xml
your app/AndroidManifest.xml
...
<application>
...
<!-- Required. For publish channel feature -->
<!-- JPUSH_CHANNEL 是为了方便开发者统计APK分发渠道。-->
<!-- 例如: -->
<!-- 发到 Google Play 的APK可以设置为 google-play; -->
<!-- 发到其他市场的 APK 可以设置为 xxx-market。 -->
<!-- 目前这个渠道统计功能的报表还未开放。-->
<meta-data android:name="JPUSH_CHANNEL" android:value="${JPUSH_CHANNEL}"/>
<meta-data android:name="JPUSH_APPKEY" android:value="${JPUSH_APPKEY}"/>
<meta-data android:name="JSHARE_PKGNAME" android:value="${applicationId}" />
<meta-data android:name="TENCENT_APPID" android:value="${TENCENT_APPID}" />
<!-- Optional 微信分享回调,wxapi必须在包名路径下,否则回调不成功-->
<activity
android:name=".wxapi.WXEntryActivity"
android:theme="@android:style/Theme.NoTitleBar"
android:exported="true" />
</application>
2.4 添加 WXEntryActivity
需要在你的 app 下新建一个 wxapi 的包名,然后创建 WXEntryActivity。
your app/src/.../wxapi/WXEntryActivity.java
package yourPackageName.wxapi;
import cn.jiguang.share.wechat.WeChatHandleActivity;
public class WXEntryActivity extends WeChatHandleActivity {
}
2.5 添加 JGShareSDK.xml
在你的 assets 目录下添加 JGShareSDK.xml,并且将新浪微博,QQ,微信的 AppId 和 AppSecret 替换成自己的。
<?xml version="1.0" encoding="utf-8"?>
<DevInfor>
<!-- 如果不需要支持某平台,可缺省该平台的配置-->
<!-- 各个平台的KEY仅供DEMO演示,开发者要集成发布需要改成自己的KEY-->
<SinaWeibo
AppKey="374535501"
AppSecret="baccd12c166f1df96736b51ffbf600a2"
RedirectUrl="https://www.jiguang.cn"/>
<QQ
AppId="1106011004"
AppKey="YIbPvONmBQBZUGaN"/>
<Wechat
AppId="wxc40e16f3ba6ebabc"
AppSecret="dcad950cd0633a27e353477c4ec12e7a"/>
</DevInfor>
做完了以上步骤,sync 一下项目,如果成功了,接下来就可以开始使用了。
目录结构
2.6 加入 JSharePackage
your app/src/../MainApplication.java
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new JSharePackage(SHUTDOWN_TOAST, SHUTDOWN_LOG)
);
}
初始化:还是在 MainAppliation 中加入一下代码初始化 JShareInterface:
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, false);
// 在 Init 之前调用,设置为 true,则会打印 debug 级别日志,否则只会打印 warning 级别以上的日志
// JShareInterface.setDebugMode(true);
JShareInterface.init(this);
}
第三步:使用
接下来就可以在 JS 中引入 JShareModule 调用它的接口:
your component.js
...
import JShareModule from 'jshare-react-native';
Android 接口
getPlatformList(cb)
/**
* 获取SDK所有能用的平台名称,如要使用某个平台,必须在JGShareSDK.xml中配置。
* Android only
* @param {Function} callback 返回值 list 是一个数组
*/
usage:
JShareModule.getPlatformList((list) => {
console.log(list);
});
share(message, successCallback, failCallback)
/**
* 分享
* @param {object} message = {
*
* platformString 必填,用于分享置不同的平台 //可以是 'wechat_session' / 'wechat_timeLine' / 'wechat_favourite' / 'qq' / 'qzone' / 'sina_weibo' / 'sina_weibo_contact'
* type 必填
*
* {
* type: 'text'
* platform: platformString // 分享到指定平台
* text: String
* imagePath: // 选填,新浪微博本地图片地址,其他平台没有这个字段(iOS 不支持这个字段)
* }
*
* {
* type: 'image'
* platform: platformString // 分享到指定平台
* imagePath: String // 本地图片路径 imagePath, imageUrl imageArray 必须三选一
* text: String // 选填
* imageUrl: String // 网络图片地址,必须以 http 或 https 开头,imagePath, imageUrl imageArray 必须三选一 (iOS 不支持这个字段)
* imageArray: [String] // (选填: 分享到 Qzone 才提供这个字段) 如果需要分享多张图片需要这个参数,数组中问题图片路径 imagePath, imageUrl imageArray 必须三选一
* }
*
* {
* type: 'video'
* platform: platformString // 分享到指定平台
* title: String // 选填
* url: String // 视频跳转页面 url
* text: String // 选填
* imagePath: String // 选填,缩略图,本地图片路径
*
* videoUrl: String // QQ 空间本地视频 (iOS 不支持这个字段)
* }
*
* {
* type: 'audio'
* platform: platformString // 分享到指定平台
* musicUrl: String //必填 点击直接播放的 url
* url: String //选填,点击跳转的 url
* imagePath: String //选填,缩略图,本地图片路径,imagePath,imageUrl 必须二选一
* imageUrl: String // 选填,网络图片路径,imagePath, imageUrl 必须二选一
* title: String // 选填
* text: String // 选填
* }
*
* {
* type: 'file'
* platform: platformString // 分享到指定平台
* path: String // 必填,文件路径
* fileExt: String // 必填,文件类型后缀
* tile: String
* }
*
* {
* type: 'emoticon'
* platform: platformString // 分享到指定平台
* imagePath: String // 必填,本地图片路径
* }
*
* {
* type: 'app' // wechat_favourite 不支持
* platform: platformString // 分享到指定平台
* url: String // 点击跳转 url
* extInfo: String // 选填 第三方应用自定义数据
* path: String // 选填 对应 app 数据文件
* title: String // 选填
* text: String // 选填
* }
*
* {
* type: 'link'
* platform: platformString // 分享到指定平台
* url: String // 必填,网页 url
* imagePath: String // 选填,本地图片路径 imagePath,imageUrl 必须二选一
* imageUrl: String // 选填,网络图片地址 imagePath imageUrl 必须二选一 (iOS 不支持)
* title: String // 选填
* text: String // 选填
* }
*
* {
* type: 'undefined'
* platform: platformString // 分享到指定平台
* }
*
* @param {Function} success = function (state) {} ##
* state = {state: String} state = 'success' / 'fail' / 'cancel' / 'unknow'
*
* @param {Function} fail = function (error) {} ##
* error = {code: number, descript: String}
*/
usage:
var message = {
platform: "wechat_session",
type: "image",
text: "JShare test text",
imagePath: "/storage/emulated/0/DCIM/Camera/xx.jpg"
};
JShareModule.share(message, (map) => {
console.log("share succeed, map: " + map);
}, (map) => {
console.log("share failed, map: " + map);
})
getSocialUserInfo(param, successCallback, failCallback)
/**
* 获取社交平台用户信息
* @param {Object} param = {
* platform: String //可以是 'wechat_session' / 'wechat_timeLine' / 'wechat_favourite' / 'qq' / 'qzone' / 'sina_weibo' / 'sina_weibo_contact'
* }
* @param {Function} success function (userInfo) {}
* userInfo = {
* name: String
* iconUrl: String // 社交平台头像链接
* gender: String // 'female' / 'male'
* response: Object // 社交平台上的原始数据
* }
*
* @param {Function} fail = function (error) {} ##
* error = {code: number, descript: String}
*/
usage:
var param = {
platform: "wechat_session"
};
JShareModule.getSocialUserInfo(param, (map) => {
console.log(map);
}, (errorCode) => {
console.log("errorCode: " + errorCode);
});
isPlatformAuth(param, callback)
/**
* 判断某平台是否支持授权
*
* @param {Object} param = {
* platform: String //可以是 'wechat_session' / 'wechat_timeLine' / 'wechat_favourite' / 'qq' / 'qzone' / 'sina_weibo' / 'sina_weibo_contact'
* }
* @param {Function} callback = (Boolean) => {}
*/
usage:
var param = {
platform: "wechat_session"
};
JShareModule.isPlatformAuth(param, (result) => {
console.log(param.platform + "is Auth: " + result);
});
isClientValid(param, callback)
/**
* 判断该平台的分享是否有效
* Android only
* @param {Object} param = {
* platform: String //可以是 'wechat_session' / 'wechat_timeLine' / 'wechat_favourite' / 'qq' / 'qzone' / 'sina_weibo' / 'sina_weibo_contact'
* }
* @param {Function} callback = (Boolean) => {}
*/
usage:
var param = {
platform: "wechat_session"
};
JShareModule.isClientValid(param, (result) => {
console.log(param.platform + "is valid: " + result);
});
authorize(param, successCallback, failCallback)
/**
* 授权接口
* @param {Object} param = {
* platform: String //可以是 'wechat_session' / 'wechat_timeLine' / 'wechat_favourite' / 'qq' / 'qzone' / 'sina_weibo' / 'sina_weibo_contact'
* }
* @param {Function} success
* @param {Function} fail
*/
usage:
var param = {
platform: "wechat_session"
};
JShareModule.authorize(param, (map) => {
console.log("Authorize succeed " + map);
}, (errorCode) => {
console.log("Authorize failed, errorCode : " + errorCode);
});
isAuthorize(param, callback)
/**
* 判断是否授权接口
* @param {Object} param = {
* platform: String //可以是 'wechat_session' / 'wechat_timeLine' / 'wechat_favourite' / 'qq' / 'qzone' / 'sina_weibo' / 'sina_weibo_contact'
* }
* @param {Function} callback = (Boolean) => {}
*/
usage:
var param = {
platform: "wechat_session"
};
JShareModule.isAuthorize(param, (result) => {
console.log("param is Authorize: " + result);
});
cancelAuthWithPlatform(param, callback)
/**
* 删除用户授权本地数据
*
* @param {Object} param = {
* platform: String //可以是 'wechat_session' /
* 'wechat_timeLine' /
* 'wechat_favourite' /
* 'qq' /
* 'qzone' /
* 'sina_weibo' /
* 'sina_weibo_contact'
* }
* @param {Function} callback = (Int) => {}
* @code 返回码,0 表示成功删除
*/
usage:
var param = {
platform: "wechat_session"
};
JShareModule.cancelAuthWithPlatform(param, (code) => {
if (code === 0) {
console.log("remove authorize succeed");
} else {
console.log("remove authorize failed, errorCode: " + code);
}
});
查看原文关于推送的集成请参考这篇文章,关于统计的集成请参考这篇文章,本篇文章将引导你集成分享功能。 在集成插件之前,需要在各大开放平台上成功注册应用,并通过审核(支持 3 个可选的主流平台)。支持的平台如下: 微信开放平台 QQ 开放平台 微博开放平台 第一步:安装...
赞 3 收藏 4 评论 5
查看全部 个人动态 →
(゚∀゚ )
暂时没有
(゚∀゚ )
暂时没有
注册于 2018-07-16
个人主页被 159 人浏览
推荐关注