基于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 相关文章推荐
浅析hasOwnProperty方法的应用
Nov 20 Javascript
JQuery遍历json数组的3种方法
Nov 08 Javascript
谈谈jQuery之Deferred源码剖析
Dec 19 Javascript
在js代码拼接dom对象到页面上去的模板总结(必看)
Feb 14 Javascript
详解angularJS动态生成的页面中ng-click无效解决办法
Jun 19 Javascript
jquery基于layui实现二级联动下拉选择(省份城市选择)
Jun 20 jQuery
vue.js框架实现表单排序和分页效果
Aug 09 Javascript
微信小程序项目实践之九宫格实现及item跳转功能
Jul 19 Javascript
Bootstrap Table 双击、单击行获取该行及全表内容
Aug 31 Javascript
cnpm加速Angular项目创建的方法
Sep 07 Javascript
React 父子组件通信的实现方法
Dec 05 Javascript
element-ui中el-upload多文件一次性上传的实现
Dec 02 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
Syphon 使用方法
2021/03/03 冲泡冲煮
php实现快速排序的三种方法分享
2014/03/12 PHP
PHPWind9.0手动屏蔽验证码解决后台关闭验证码但是依然显示的问题
2016/08/12 PHP
jquery显示和隐藏div特效实例
2013/02/27 Javascript
JavaScript 垃圾回收机制分析
2013/10/10 Javascript
javascript从image转换为base64位编码的String
2014/07/29 Javascript
JavaScript设计模式之工厂模式和构造器模式
2015/02/11 Javascript
基于jquery实现鼠标滚轮驱动的图片切换效果
2015/10/26 Javascript
JavaScript省市区三级联动菜单效果
2016/09/21 Javascript
Bootstrap优化站点资源、响应式图片、传送带使用详解3
2016/10/14 Javascript
微信小程序 表单Form实例详解(附源码)
2016/12/22 Javascript
利用vscode编写vue的简单配置详解
2017/06/17 Javascript
jQuery实现拼图小游戏(实例讲解)
2017/07/24 jQuery
vue实现手机号码抽奖上下滚动动画示例
2017/10/18 Javascript
如何封装了一个vue移动端下拉加载下一页数据的组件
2019/01/06 Javascript
微信小程序获取地理位置及经纬度授权代码实例
2019/09/18 Javascript
微信小程序 this.triggerEvent()的具体使用
2019/12/10 Javascript
Python实现CET查分的方法
2015/03/10 Python
Python易忽视知识点小结
2015/05/25 Python
Python获取昨天、今天、明天开始、结束时间戳的方法
2018/06/01 Python
python 使用值来排序一个字典的方法
2018/11/16 Python
python字典的常用方法总结
2019/07/31 Python
PyTorch中topk函数的用法详解
2020/01/02 Python
Node.js 和 Python之间该选择哪个?
2020/08/05 Python
HTML5 Canvas的常用线条属性值总结
2016/03/17 HTML / CSS
巴西最大的珠宝连锁店:Vivara
2019/04/18 全球购物
武汉高蓝德国际.net机试
2016/06/24 面试题
介绍一下linux的文件系统
2015/10/06 面试题
自我鉴定模板
2013/10/29 职场文书
英文版销售经理个人求职信
2013/11/20 职场文书
优秀学生事迹材料
2014/02/08 职场文书
如何写求职信
2014/05/24 职场文书
关于成绩下滑的自我检讨书
2014/09/20 职场文书
医护人员继续教育学习心得体会
2016/01/19 职场文书
nginx刷新页面出现404解决方案(亲测有效)
2022/03/18 Servers
SQLServer RANK() 排名函数的使用
2022/03/23 SQL Server