基于redis的小程序登录实现方法流程分析


Posted in Javascript onMay 25, 2020

这张图是小程序的登录流程解析:

基于redis的小程序登录实现方法流程分析

小程序登陆授权流程:

在小程序端调用wx.login()获取code,由于我是做后端开发的这边不做赘述,直接贴上代码了.有兴趣的直接去官方文档看下,链接放这里: wx.login()

wx.login({
 success (res) {
 if (res.code) {
 //发起网络请求
 wx.request({
 url: 'https://test.com/onLogin',
 data: {
 code: res.code
 }
 })
 } else {
 console.log('登录失败!' + res.errMsg)
 }
 }
})

小程序前端登录后会获取code,调用自己的开发者服务接口,调用个get请求:

GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code

需要得四个参数:
appid:小程序appid
secret: 小程序密钥
js_code: 刚才获取的code
grant_type:‘authorization_code' //这个是固定的

如果不出意外的话,微信接口服务器会返回四个参数:

基于redis的小程序登录实现方法流程分析

详情可以看下官方文档:jscode2session

下面附上我的代码:

@AuthIgnore
 @RequestMapping("/login")
 @ResponseBody
 public ResponseBean openId(@RequestParam(value = "code", required = true) String code,
  @RequestParam(value = "avatarUrl") String avatarUrl,
  @RequestParam(value = "city") String city,
  @RequestParam(value = "country") String country,
  @RequestParam(value = "gender") String gender,
  @RequestParam(value = "language") String language,
  @RequestParam(value = "nickName") String nickName,
  @RequestParam(value = "province") String province,
  HttpServletRequest request) { // 小程序端获取的CODE
 ResponseBean responseBean = new ResponseBean();
 try {
 boolean check = (StringUtils.isEmpty(code)) ? true : false;
 if (check) {
 responseBean = new ResponseBean(false, UnicomResponseEnums.NO_CODE);
 return responseBean;
 }
 //将获取的用户数据存入数据库;
 Map<String, Object> msgs = new HashMap<>();
 msgs.put("appid", appId);
 msgs.put("secret", secret);
 msgs.put("js_code", code);
 msgs.put("grant_type", "authorization_code");
 // java的网络请求,返回字符串
 String data = HttpUtils.get(msgs, Constants.JSCODE2SESSION);
 logger.info("======> " + data);
 String openId = JSONObject.parseObject(data).getString("openid");
 String session_key = JSONObject.parseObject(data).getString("session_key");
 String unionid = JSONObject.parseObject(data).getString("unionid");
 String errcode = JSONObject.parseObject(data).getString("errcode");
 String errmsg = JSONObject.parseObject(data).getString("errmsg");

 JSONObject json = new JSONObject();
 int userId = -1;

 if (!StringUtils.isBlank(openId)) {
 Users user = userService.selectUserByOpenId(openId);
 if (user == null) {
  //新建一个用户信息
  Users newUser = new Users();
  newUser.setOpenid(openId);
  newUser.setArea(city);
  newUser.setSex(Integer.parseInt(gender));
  newUser.setNickName(nickName);
  newUser.setCreateTime(new Date());
  newUser.setStatus(0);//初始
  if (!StringUtils.isBlank(unionid)) {
  newUser.setUnionid(unionid);
  }
  userService.instert(newUser);
  userId = newUser.getId();
 } else {
  userId = user.getId();
 }
 json.put("userId", userId);
 }
 if (!StringUtils.isBlank(session_key) && !StringUtils.isBlank(openId)) {
 //这段可不用redis存,直接返回session_key也可以
 String userAgent = request.getHeader("user-agent");
 String sessionid = tokenService.generateToken(userAgent, session_key);
 //将session_key存入redis
 redisUtil.setex(sessionid, session_key + "###" + userId, Constants.SESSION_KEY_EX);
 json.put("token", sessionid);

 responseBean = new ResponseBean(true, json);
 } else {
 responseBean = new ResponseBean<>(false, null, errmsg);
 }
 return responseBean;
 } catch (Exception e) {
 e.printStackTrace();
 responseBean = new ResponseBean(false, UnicomResponseEnums.JSCODE2SESSION_ERRO);
 return responseBean;
 }
 }

解析:

这边我的登录获取的session_key出于安全性考虑没有直接在前端传输,而是存到了redis里面给到前端session_key的token传输,
而且session_key的销毁时间是20分钟,时间内可以重复获取用户数据.
如果只是简单使用或者对安全性要求不严的话可以直接传session_key到前端保存.

session_key的作用:

校验用户信息(wx.getUserInfo(OBJECT)返回的signature);
解密(wx.getUserInfo(OBJECT)返回的encryptedData);

按照官方的说法,wx.checksession是用来检查 wx.login(OBJECT) 的时效性,判断登录是否过期;
疑惑的是(openid,unionid )都是用户唯一标识,不会因为wx.login(OBJECT)的过期而改变,所以要是没有使用wx.getUserInfo(OBJECT)获得的用户信息,确实没必要使用wx.checksession()来检查wx.login(OBJECT) 是否过期;
如果使用了wx.getUserInfo(OBJECT)获得的用户信息,还是有必要使用wx.checksession()来检查wx.login(OBJECT) 是否过期的,因为用户有可能修改了头像、昵称、城市,省份等信息,可以通过检查wx.login(OBJECT) 是否过期来更新着些信息;

小程序的登录状态维护本质就是维护session_key的时效性

这边附上我的HttpUtils工具代码,如果只要用get的话可以复制部分:

如果是直

/**
 * HttpUtils工具类
 *
 * @author
 */
public class HttpUtils {
 /**
 * 请求方式:post
 */
 public static String POST = "post";
 /**
 * 编码格式:utf-8
 */
 private static final String CHARSET_UTF_8 = "UTF-8";
 /**
 * 报文头部json
 */
 private static final String APPLICATION_JSON = "application/json";
 /**
 * 请求超时时间
 */
 private static final int CONNECT_TIMEOUT = 60 * 1000;
 /**
 * 传输超时时间
 */
 private static final int SO_TIMEOUT = 60 * 1000;
 /**
 * 日志
 */
 private static final Logger logger = LoggerFactory.getLogger(HttpUtils.class);
 /**
 * @param protocol
 * @param url
 * @param paraMap
 * @return
 * @throws Exception
 */
 public static String doPost(String protocol, String url,
  Map<String, Object> paraMap) throws Exception {
 CloseableHttpClient httpClient = null;
 CloseableHttpResponse resp = null;
 String rtnValue = null;
 try {
 if (protocol.equals("http")) {
 httpClient = HttpClients.createDefault();
 } else {
 // 获取https安全客户端
 httpClient = HttpUtils.getHttpsClient();
 }
 HttpPost httpPost = new HttpPost(url);
 List<NameValuePair> list = msgs2valuePairs(paraMap);
// List<NameValuePair> list = new ArrayList<NameValuePair>();
// if (null != paraMap &¶Map.size() > 0) {
// for (Entry<String, Object> entry : paraMap.entrySet()) {
//  list.add(new BasicNameValuePair(entry.getKey(), entry
//  .getValue().toString()));
// }
// }
 RequestConfig requestConfig = RequestConfig.custom()
  .setSocketTimeout(SO_TIMEOUT)
  .setConnectTimeout(CONNECT_TIMEOUT).build();// 设置请求和传输超时时间
 httpPost.setConfig(requestConfig);
 httpPost.setEntity(new UrlEncodedFormEntity(list, CHARSET_UTF_8));
 resp = httpClient.execute(httpPost);
 rtnValue = EntityUtils.toString(resp.getEntity(), CHARSET_UTF_8);
 } catch (Exception e) {
 logger.error(e.getMessage());
 throw e;
 } finally {
 if (null != resp) {
 resp.close();
 }
 if (null != httpClient) {
 httpClient.close();
 }
 }
 return rtnValue;
 }
 /**
 * 获取https,单向验证
 *
 * @return
 * @throws Exception
 */
 public static CloseableHttpClient getHttpsClient() throws Exception {
 try {
 TrustManager[] trustManagers = new TrustManager[]{new X509TrustManager() {
 public void checkClientTrusted(
  X509Certificate[] paramArrayOfX509Certificate,
  String paramString) throws CertificateException {
 }
 public void checkServerTrusted(
  X509Certificate[] paramArrayOfX509Certificate,
  String paramString) throws CertificateException {
 }
 public X509Certificate[] getAcceptedIssuers() {
  return null;
 }
 }};
 SSLContext sslContext = SSLContext
  .getInstance(SSLConnectionSocketFactory.TLS);
 sslContext.init(new KeyManager[0], trustManagers,
  new SecureRandom());
 SSLContext.setDefault(sslContext);
 sslContext.init(null, trustManagers, null);
 SSLConnectionSocketFactory connectionSocketFactory = new SSLConnectionSocketFactory(
  sslContext,
  SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
 HttpClientBuilder clientBuilder = HttpClients.custom()
  .setSSLSocketFactory(connectionSocketFactory);
 clientBuilder.setRedirectStrategy(new LaxRedirectStrategy());
 CloseableHttpClient httpClient = clientBuilder.build();
 return httpClient;
 } catch (Exception e) {
 throw new Exception("http client 远程连接失败", e);
 }
 }
 /**
 * post请求
 *
 * @param msgs
 * @param url
 * @return
 * @throws ClientProtocolException
 * @throws UnknownHostException
 * @throws IOException
 */
 public static String post(Map<String, Object> msgs, String url)
 throws ClientProtocolException, UnknownHostException, IOException {
 CloseableHttpClient httpClient = HttpClients.createDefault();
 try {
 HttpPost request = new HttpPost(url);
 List<NameValuePair> valuePairs = msgs2valuePairs(msgs);
// List<NameValuePair> valuePairs = new ArrayList<NameValuePair>();
// if (null != msgs) {
// for (Entry<String, Object> entry : msgs.entrySet()) {
//  if (entry.getValue() != null) {
//  valuePairs.add(new BasicNameValuePair(entry.getKey(),
//  entry.getValue().toString()));
//  }
// }
// }
 request.setEntity(new UrlEncodedFormEntity(valuePairs, CHARSET_UTF_8));
 CloseableHttpResponse resp = httpClient.execute(request);
 return EntityUtils.toString(resp.getEntity(), CHARSET_UTF_8);
 } finally {
 httpClient.close();
 }
 }
 /**
 * post请求
 *
 * @param msgs
 * @param url
 * @return
 * @throws ClientProtocolException
 * @throws UnknownHostException
 * @throws IOException
 */
 public static byte[] postGetByte(Map<String, Object> msgs, String url)
 throws ClientProtocolException, UnknownHostException, IOException {
 CloseableHttpClient httpClient = HttpClients.createDefault();
 InputStream inputStream = null;
 byte[] data = null;
 try {
 HttpPost request = new HttpPost(url);
 List<NameValuePair> valuePairs = msgs2valuePairs(msgs);
// List<NameValuePair> valuePairs = new ArrayList<NameValuePair>();
// if (null != msgs) {
// for (Entry<String, Object> entry : msgs.entrySet()) {
//  if (entry.getValue() != null) {
//  valuePairs.add(new BasicNameValuePair(entry.getKey(),
//  entry.getValue().toString()));
//  }
// }
// }
 request.setEntity(new UrlEncodedFormEntity(valuePairs, CHARSET_UTF_8));
 CloseableHttpResponse response = httpClient.execute(request);
 try {
 // 获取相应实体
 HttpEntity entity = response.getEntity();
 if (entity != null) {
  inputStream = entity.getContent();
  data = readInputStream(inputStream);
 }
 return data;
 } catch (Exception e) {
 e.printStackTrace();
 } finally {
 httpClient.close();
 return null;
 }
 } finally {
 httpClient.close();
 }
 }
 /** 将流 保存为数据数组
 * @param inStream
 * @return
 * @throws Exception
 */
 public static byte[] readInputStream(InputStream inStream) throws Exception {
 ByteArrayOutputStream outStream = new ByteArrayOutputStream();
 // 创建一个Buffer字符串
 byte[] buffer = new byte[1024];
 // 每次读取的字符串长度,如果为-1,代表全部读取完毕
 int len = 0;
 // 使用一个输入流从buffer里把数据读取出来
 while ((len = inStream.read(buffer)) != -1) {
 // 用输出流往buffer里写入数据,中间参数代表从哪个位置开始读,len代表读取的长度
 outStream.write(buffer, 0, len);
 }
 // 关闭输入流
 inStream.close();
 // 把outStream里的数据写入内存
 return outStream.toByteArray();
 }
 /**
 * get请求
 *
 * @param msgs
 * @param url
 * @return
 * @throws ClientProtocolException
 * @throws UnknownHostException
 * @throws IOException
 */
 public static String get(Map<String, Object> msgs, String url)
 throws ClientProtocolException, UnknownHostException, IOException {
 CloseableHttpClient httpClient = HttpClients.createDefault();
 try {
 List<NameValuePair> valuePairs = msgs2valuePairs(msgs);
// List<NameValuePair> valuePairs = new ArrayList<NameValuePair>();
// if (null != msgs) {
// for (Entry<String, Object> entry : msgs.entrySet()) {
//  if (entry.getValue() != null) {
//  valuePairs.add(new BasicNameValuePair(entry.getKey(),
//  entry.getValue().toString()));
//  }
// }
// }
 // EntityUtils.toString(new UrlEncodedFormEntity(valuePairs),
 // CHARSET);
 url = url + "?" + URLEncodedUtils.format(valuePairs, CHARSET_UTF_8);
 HttpGet request = new HttpGet(url);
 CloseableHttpResponse resp = httpClient.execute(request);
 return EntityUtils.toString(resp.getEntity(), CHARSET_UTF_8);
 } finally {
 httpClient.close();
 }
 }
 public static <T> T post(Map<String, Object> msgs, String url,
  Class<T> clazz) throws ClientProtocolException,
 UnknownHostException, IOException {
 String json = HttpUtils.post(msgs, url);
 T t = JSON.parseObject(json, clazz);
 return t;
 }
 public static <T> T get(Map<String, Object> msgs, String url, Class<T> clazz)
 throws ClientProtocolException, UnknownHostException, IOException {
 String json = HttpUtils.get(msgs, url);
 T t = JSON.parseObject(json, clazz);
 return t;
 }
 public static String postWithJson(Map<String, Object> msgs, String url)
 throws ClientProtocolException, IOException {
 CloseableHttpClient httpClient = HttpClients.createDefault();
 try {
 String jsonParam = JSON.toJSONString(msgs);
 HttpPost post = new HttpPost(url);
 post.setHeader("Content-Type", APPLICATION_JSON);
 post.setEntity(new StringEntity(jsonParam, CHARSET_UTF_8));
 CloseableHttpResponse response = httpClient.execute(post);
 return new String(EntityUtils.toString(response.getEntity()).getBytes("iso8859-1"), CHARSET_UTF_8);
 } finally {
 httpClient.close();
 }
 }
 public static <T> T postWithJson(Map<String, Object> msgs, String url, Class<T> clazz) throws ClientProtocolException,
 UnknownHostException, IOException {
 String json = HttpUtils.postWithJson(msgs, url);
 T t = JSON.parseObject(json, clazz);
 return t;
 }
 public static List<NameValuePair> msgs2valuePairs(Map<String, Object> msgs) {
 List<NameValuePair> valuePairs = new ArrayList<NameValuePair>();
 if (null != msgs) {
 for (Entry<String, Object> entry : msgs.entrySet()) {
 if (entry.getValue() != null) {
  valuePairs.add(new BasicNameValuePair(entry.getKey(),
  entry.getValue().toString()));
 }
 }
 }
 return valuePairs;
 }
}

接传session_key到前端的,下面的可以不用看了.
如果是redis存的sesssion_key的token的话,这边附上登陆的时候的token转换为session_key.

自定义拦截器注解:

import java.lang.annotation.*;
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuthIgnore {
}

拦截器部分代码:

@Override
 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
 AuthIgnore annotation;
 if(handler instanceof HandlerMethod) {
 annotation = ((HandlerMethod) handler).getMethodAnnotation(AuthIgnore.class);
 }else{
 return true;
 }
 //如果有@AuthIgnore注解,则不验证token
 if(annotation != null){
 return true;
 }
// //获取微信access_token;
// if(!redisUtil.exists(Constants.ACCESS_TOKEN)){
// Map<String, Object> msgs = new HashMap<>();
// msgs.put("appid",appId);
// msgs.put("secret",secret);
// msgs.put("grant_type","client_credential");
// String data = HttpUtils.get(msgs,Constants.GETACCESSTOKEN); // java的网络请求,返回字符串
// String errcode= JSONObject.parseObject(data).getString("errcode");
// String errmsg= JSONObject.parseObject(data).getString("errmsg");
// if(StringUtils.isBlank(errcode)){
// //存储access_token
// String access_token= JSONObject.parseObject(data).getString("access_token");
// long expires_in=Long.parseLong(JSONObject.parseObject(data).getString("expires_in"));
// redisUtil.setex("ACCESS_TOKEN",access_token, expires_in);
// }else{
// //异常返回数据拦截,返回json数据
// response.setCharacterEncoding("UTF-8");
// response.setContentType("application/json; charset=utf-8");
// PrintWriter out = response.getWriter();
// ResponseBean<Object> responseBean=new ResponseBean<>(false,null, errmsg);
// out = response.getWriter();
// out.append(JSON.toJSON(responseBean).toString());
// return false;
// }
// }
 //获取用户凭证
 String token = request.getHeader(Constants.USER_TOKEN);
// if(StringUtils.isBlank(token)){
// token = request.getParameter(Constants.USER_TOKEN);
// }
// if(StringUtils.isBlank(token)){
// Object obj = request.getAttribute(Constants.USER_TOKEN);
// if(null!=obj){
// token=obj.toString();
// }
// }
// //token凭证为空
// if(StringUtils.isBlank(token)){
// //token不存在拦截,返回json数据
// response.setCharacterEncoding("UTF-8");
// response.setContentType("application/json; charset=utf-8");
// PrintWriter out = response.getWriter();
// try{
// ResponseBean<Object> responseBean=new ResponseBean<>(false,null, UnicomResponseEnums.TOKEN_EMPTY);
// out = response.getWriter();
// out.append(JSON.toJSON(responseBean).toString());
// return false;
// }
// catch (Exception e) {
// e.printStackTrace();
// response.sendError(500);
// return false;
// }
// }
 if(token==null||!redisUtil.exists(token)){
 //用户未登录,返回json数据
 response.setCharacterEncoding("UTF-8");
 response.setContentType("application/json; charset=utf-8");
 PrintWriter out = response.getWriter();
 try{
 ResponseBean<Object> responseBean=new ResponseBean<>(false,null, UnicomResponseEnums.DIS_LOGIN);
 out = response.getWriter();
 out.append(JSON.toJSON(responseBean).toString());
 return false;
 }
 catch (Exception e) {
 e.printStackTrace();
 response.sendError(500);
 return false;
 }
 }
 tokenManager.refreshUserToken(token);
 return true;
 }
}

过滤器配置:

@Configuration
public class InterceptorConfig extends WebMvcConfigurerAdapter {
 @Override
 public void addInterceptors(InterceptorRegistry registry) {
 registry.addInterceptor(authorizationInterceptor())
 .addPathPatterns("/**")// 拦截所有请求,通过判断是否有 @AuthIgnore注解 决定是否需要登录
 .excludePathPatterns("/user/login");//排除登录
 }
 @Bean
 public AuthorizationInterceptor authorizationInterceptor() {
 return new AuthorizationInterceptor();
 }
}

token管理:

@Service
public class TokenManager {
 @Resource
 private RedisUtil redisUtil;
 //生成token(格式为token:设备-加密的用户名-时间-六位随机数)
 public String generateToken(String userAgentStr, String username) {
 StringBuilder token = new StringBuilder("token:");
 //设备
 UserAgent userAgent = UserAgent.parseUserAgentString(userAgentStr);
 if (userAgent.getOperatingSystem().isMobileDevice()) {
 token.append("MOBILE-");
 } else {
 token.append("PC-");
 }
 //加密的用户名
 token.append(MD5Utils.MD5Encode(username) + "-");
 //时间
 token.append(new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()) + "-");
 //六位随机字符串
 token.append(UUID.randomUUID().toString());
 System.out.println("token-->" + token.toString());
 return token.toString();
 }
 /**
 * 登录用户,创建token
 *
 * @param token
 */
 //把token存到redis中
 public void save(String token, Users user) {
 if (token.startsWith("token:PC")) {
 redisUtil.setex(token,JSON.toJSONString(user), Constants.TOKEN_EX);
 } else {
 redisUtil.setex(token,JSON.toJSONString(user), Constants.TOKEN_EX);
 }
 }
 /**
 * 刷新用户
 *
 * @param token
 */
 public void refreshUserToken(String token) {
 if (redisUtil.exists(token)) {
 String value=redisUtil.get(token);
 redisUtil.setex(token, value, Constants.TOKEN_EX);
 }
 }
 /**
 * 用户退出登陆
 *
 * @param token
 */
 public void loginOut(String token) {
 redisUtil.remove(token);
 }
 /**
 * 获取用户信息
 *
 * @param token
 * @return
 */
 public Users getUserInfoByToken(String token) {
 if (redisUtil.exists(token)) {
 String jsonString = redisUtil.get(token);
 Users user =JSONObject.parseObject(jsonString, Users.class);
 return user;
 }
 return null;
 }
}

redis工具类:

@Component
public class RedisUtil {
 @Resource
 private RedisTemplate<String, String> redisTemplate;
 public void set(String key, String value) {
 ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
 valueOperations.set(key, value);
 }
 public void setex(String key, String value, long seconds) {
 ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
 valueOperations.set(key, value, seconds,TimeUnit.SECONDS);
 }
 public Boolean exists(String key) {
 return redisTemplate.hasKey(key);
 }
 public void remove(String key) {
 redisTemplate.delete(key);
 }
 public String get(String key) {
 ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
 return valueOperations.get(key);
 }
}

最后redis要实现序列化,序列化最终的目的是为了对象可以跨平台存储,和进行网络传输。而我们进行跨平台存储和网络传输的方式就是IO,而我们的IO支持的数据格式就是字节数组。本质上存储和网络传输 都需要经过 把一个对象状态保存成一种跨平台识别的字节格式,然后其他的平台才可以通过字节信息解析还原对象信息。

@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
public class RedisConfig {
 @Bean
 @ConditionalOnMissingBean(name = "redisTemplate")
 public RedisTemplate<Object, Object> redisTemplate(
 RedisConnectionFactory redisConnectionFactory) {
 RedisTemplate<Object, Object> template = new RedisTemplate<>();
 //使用fastjson序列化
 FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer(Object.class);
 // value值的序列化采用fastJsonRedisSerializer
 template.setValueSerializer(fastJsonRedisSerializer);
 template.setHashValueSerializer(fastJsonRedisSerializer);
 // key的序列化采用StringRedisSerializer
 template.setKeySerializer(new StringRedisSerializer());
 template.setHashKeySerializer(new StringRedisSerializer());
 template.setConnectionFactory(redisConnectionFactory);
 return template;
 }
 @Bean
 @ConditionalOnMissingBean(StringRedisTemplate.class)
 public StringRedisTemplate stringRedisTemplate(
 RedisConnectionFactory redisConnectionFactory) {
 StringRedisTemplate template = new StringRedisTemplate();
 template.setConnectionFactory(redisConnectionFactory);
 return template;
 }
}

作者:gigass
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

总结

到此这篇关于基于redis的小程序登录实现的文章就介绍到这了,更多相关redis小程序登录内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
防止动态加载JavaScript引起的内存泄漏问题
Oct 08 Javascript
jQuery中jqGrid分页实现代码
Nov 04 Javascript
用js来刷新当前页面保留参数的具体实现
Dec 23 Javascript
JavaScript父子窗体间的调用方法
Mar 31 Javascript
JavaScript中操作字符串之localeCompare()方法的使用
Jun 06 Javascript
JQuery中DOM事件冒泡实例分析
Jun 13 Javascript
深入理解Java线程编程中的阻塞队列容器
Dec 07 Javascript
React Native开发封装Toast与加载Loading组件示例
Sep 08 Javascript
「中高级前端面试」JavaScript手写代码无敌秘籍(推荐)
Apr 08 Javascript
小程序实现搜索框
Jun 19 Javascript
javascript设计模式 ? 备忘录模式原理与用法实例分析
Apr 21 Javascript
从零开始在vue-cli4配置自适应vw布局的实现
Jun 08 Javascript
JSONP解决JS跨域问题的实现
May 25 #Javascript
JS实现时间校验的代码
May 25 #Javascript
使用Typescript和ES模块发布Node模块的方法
May 25 #Javascript
js 动态校验开始结束时间的实现代码
May 25 #Javascript
使用 Opentype.js 生成字体子集的实例代码详解
May 25 #Javascript
Node.js API详解之 repl模块用法实例分析
May 25 #Javascript
微信小程序仿抖音视频之整屏上下切换功能的实现代码
May 24 #Javascript
You might like
目录,文件操作详谈―PHP
2006/11/25 PHP
PHP操作MongoDB GridFS 存储文件的详解
2013/06/20 PHP
PHP以指定字段为索引返回数据库所取的数据数组
2013/06/30 PHP
PHP获取时间排除周六、周日的两个方法
2014/06/30 PHP
Thinkphp中的curd应用实用要点
2015/01/04 PHP
php cli配置文件问题分析
2015/10/15 PHP
Laravel数据库读写分离配置的方法
2019/10/13 PHP
js使用for循环查询数组中是否存在某个值
2014/08/12 Javascript
Javascript中作用域的详细介绍
2016/10/06 Javascript
javascript实现右下角广告框效果
2017/02/01 Javascript
nodejs服务搭建教程 nodejs访问本地站点文件
2017/04/07 NodeJs
解决JS内存泄露之js对象和dom对象互相引用问题
2017/06/25 Javascript
AngularJS点击添加样式、点击变色设置的实例代码
2017/07/27 Javascript
node前端开发模板引擎Jade的入门
2018/05/11 Javascript
Vee-validate 父组件获取子组件表单校验结果的实例代码
2019/05/20 Javascript
ionic+html5+API实现双击返回键退出应用
2019/09/17 Javascript
js防抖函数和节流函数使用场景和实现区别示例分析
2020/04/11 Javascript
[37:02]OG vs INfamous 2019国际邀请赛小组赛 BO2 第二场 8.15
2019/08/17 DOTA
python编程实现归并排序
2017/04/14 Python
python 递归遍历文件夹,并打印满足条件的文件路径实例
2017/08/30 Python
python中利用队列asyncio.Queue进行通讯详解
2017/09/10 Python
Python通过OpenCV的findContours获取轮廓并切割实例
2018/01/05 Python
Python中使用threading.Event协调线程的运行详解
2020/05/02 Python
python爬虫用request库处理cookie的实例讲解
2021/02/20 Python
让IE6、IE7、IE8支持CSS3的脚本
2010/07/20 HTML / CSS
百丽国际旗下购物网站:优购
2017/02/28 全球购物
加拿大最大的箱包及旅游配件零售商:Bentley Leathers
2017/07/19 全球购物
澳大利亚首屈一指的鞋类品牌:Tony Bianco
2018/03/13 全球购物
英国豪华文具和皮具配件经典老品牌:Smythson(斯迈森)
2018/04/19 全球购物
nohup的用法
2014/08/10 面试题
五十岁生日宴会答谢词
2014/01/15 职场文书
普通党员个人剖析材料
2014/10/08 职场文书
实习单位指导教师评语
2014/12/30 职场文书
地道战观后感500字
2015/06/04 职场文书
gojs实现蚂蚁线动画效果
2022/02/18 Javascript
Win11 21h2可以升级22h2吗?看看你的电脑符不符合要求
2022/07/07 数码科技