基于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 相关文章推荐
js获取单选按钮的数据
Nov 27 Javascript
语义化 H1 标签
Jan 14 Javascript
JavaScript 申明函数的三种方法 每个函数就是一个对象(一)
Dec 04 Javascript
js静态方法与实例方法分析
Jul 04 Javascript
JS时间选择器 兼容IE6,7,8,9
Jun 26 Javascript
window.onload追加函数使用示例
Mar 03 Javascript
javascript实现俄罗斯方块游戏的思路和方法
Apr 27 Javascript
js中substr,substring,indexOf,lastIndexOf,split,replace的用法详解
Nov 09 Javascript
Node.js如何自动审核团队的代码
Jul 20 Javascript
Angular2实现自定义双向绑定属性
Mar 22 Javascript
Vue filter介绍及详细使用
Apr 04 Javascript
基于Express框架使用POST传递Form数据
Aug 10 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
CodeIgniter模板引擎使用实例
2014/07/15 PHP
深入讲解PHP的Yii框架中的属性(Property)
2016/03/18 PHP
PHP使用SWOOLE扩展实现定时同步 MySQL 数据
2017/04/09 PHP
PHP实现的支付宝支付功能示例
2019/03/26 PHP
微信公众平台开发教程⑤ 微信扫码支付模式介绍
2019/04/10 PHP
javascript 最常用的10个自定义函数[推荐]
2009/12/26 Javascript
yepnope.js 异步加载资源文件
2011/09/08 Javascript
解决IE6的PNG透明JS插件使用介绍
2013/04/17 Javascript
js图片卷帘门导航菜单特效代码分享
2015/09/10 Javascript
jQuery基于Ajax方式提交表单功能示例
2017/02/10 Javascript
js获取json中key所对应的value值的简单方法
2020/06/17 Javascript
jQuery扇形定时器插件pietimer使用方法详解
2017/07/18 jQuery
关于react-router的几种配置方式详解
2017/07/24 Javascript
原生js+canvas实现贪吃蛇效果
2020/08/02 Javascript
vue 通过base64实现图片下载功能
2020/12/19 Vue.js
[47:22]Mineski vs Winstrike 2018国际邀请赛小组赛BO2 第二场 8.16
2018/08/17 DOTA
Python魔术方法详解
2015/02/14 Python
Python中的默认参数详解
2015/06/24 Python
Python 专题五 列表基础知识(二维list排序、获取下标和处理txt文本实例)
2017/03/20 Python
Python快速排序算法实例分析
2017/11/29 Python
Python基于SMTP协议实现发送邮件功能详解
2018/08/14 Python
Python 炫技操作之合并字典的七种方法
2020/04/10 Python
python中id函数运行方式
2020/07/03 Python
使用Python制作一盏 3D 花灯喜迎元宵佳节
2021/02/26 Python
班主任工作年限证明
2014/01/12 职场文书
优秀毕业生自我鉴定
2014/01/19 职场文书
个人现实表现材料
2014/02/04 职场文书
向领导表决心的话
2014/03/11 职场文书
数学高效课堂实施方案
2014/03/29 职场文书
《荷花》教学反思
2014/04/16 职场文书
优秀学生党员先进事迹材料
2014/05/29 职场文书
县政府班子个人对照检查材料
2014/10/05 职场文书
2014年班主任工作总结
2014/11/08 职场文书
少先队辅导员事迹材料
2014/12/24 职场文书
2015年教师党员自我评价材料
2015/03/04 职场文书
践行三严三实心得体会(2016推荐篇)
2016/01/06 职场文书