一文带你了解二维码扫码的全部用途

一文带你了解二维码扫码的全部用途

扫描二维码是如今非常常见的一个功能。

用户可以通过扫描二维码,来实现登录、网页跳转、信息获取、授权等功能。

下面这个文章来详细的说一些跟二维码扫码有关的实例。

二维码扫码登录(本地代码实现)

扫码登录,我们首先来梳理一下具体的逻辑。

  1. 移动端发起扫码请求,服务端生成二维码,缓存二维码ID及初始状态,并将二维码以Base64格式返回给前端用于展示。
  2. PC端页面定时轮询服务端,检查二维码状态。
  3. 用户使用移动端扫码后,调用扫码接口,将移动端Token与二维码ID一并提交至服务端。服务端校验后生成临时Token,并将二维码状态更新为"已扫码"。
  4. PC端继续轮询,检测二维码状态是否变为"已扫码"。
  5. 用户在移动端确认登录,将临时Token与二维码ID提交至服务端进行确认。服务端校验临时Token无误后,将二维码状态更新为"已确认",并生成正式的有效期Token。
  6. PC端检测到二维码状态为"已确认" ,获取有效Token后完成登录流程。

我们来看流程图

之后我们直接来设计。

首先来设计数据库

sql 复制代码
CREATE TABLE u_qrlogin (
    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '自增主键',
    uuid VARCHAR(64) NOT NULL UNIQUE COMMENT '唯一标识',
    device VARCHAR(64) COMMENT '设备号',
    token TEXT COMMENT 'JWT令牌',
    status VARCHAR(32) COMMENT '扫码状态',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
);

之后就是对应的dto

swift 复制代码
package com.xiaou.qrcodelogin.dto;
​
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
​
import java.time.LocalDateTime;
​
/**
 * 对应数据库表:u_qrlogin
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LoginInfoDTO {
​
    /**
     * 自增主键
     */
    @TableId
    private Long id;
​
    /**
     * 唯一标识
     */
    private String uuid;
​
    /**
     * 设备号
     */
    private String device;
​
    /**
     * JWT令牌
     */
    private String token;
​
    /**
     * 扫码状态
     */
    private String status;
​
    /**
     * 创建时间
     */
    private LocalDateTime createTime;
​
    /**
     * 更新时间
     */
    private LocalDateTime updateTime;
}
​

之后是我们要向前端返回的vo

arduino 复制代码
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
​
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ResponseVO {
    
    /**
     * 唯一标识
     */
    private String uuid;
    /**
     * 登录二维码
     */
    private String qrcode;
    
    /**
     * jwt令牌
     */
    private String token;
    
    /**
     * 扫码状态
     */
    private String status;
}

之后我们需要一个枚举类来记录二维码的状态

scss 复制代码
@Getter
public enum LoginStatusEnum {
    UNSCANNED("未扫描"),
    SCANNED("已扫描"),
    CONFIRMED("已确认");
    
    private String desc;
    
    
    LoginStatusEnum(String desc) {
        this.desc = desc;
    }
}

之后需要一些工具类的辅助。

这里用到了jjwt以及google.zxing来进行二维码的辅助生成。

java 复制代码
package com.xiaou.qrcodelogin.utils;
​
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
​
import java.nio.charset.StandardCharsets;
import java.util.Date;
​
public class JwtUtil {
​
    private static final String SECRET_KEY = "9dad5e7e-bcb7-438f-b39e-ad8c67814915";
​
    public static  String generateAuthToken(String uuid) {
        // 生成JWT或其他形式令牌
        return Jwts.builder()
            .setSubject(uuid)
            .setExpiration(new Date(System.currentTimeMillis() + 3600000)) // 1小时过期
            .signWith(SignatureAlgorithm.HS256, SECRET_KEY.getBytes(StandardCharsets.UTF_8))
            .compact();
    }
}
​
java 复制代码
package com.xiaou.qrcodelogin.utils;
​
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
​
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;
import java.util.Hashtable;
​
public class QRCodeUtil {
​
    public static String generateQRCode(String content, int width, int height)
        throws WriterException, IOException {
​
        Hashtable<EncodeHintType, Object> hints = new Hashtable<>();
        hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
​
        BitMatrix matrix = new MultiFormatWriter().encode(
            content, BarcodeFormat.QR_CODE, width, height, hints);
​
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        MatrixToImageWriter.writeToStream(matrix, "PNG", outputStream);
        return Base64.getEncoder().encodeToString(outputStream.toByteArray());
​
    }
}
​

之后来具体实现。

首先来看二维码的生成接口

scss 复制代码
/**
     * 生成二维码
     */
    @GetMapping("/generate")
    public ResponseEntity<ResponseVO> generateQRCode() throws Exception {
        String uuid = UUID.randomUUID().toString();
        String base64QR = QRCodeUtil.generateQRCode(uuid, 200, 200);
​
        LocalDateTime now = LocalDateTime.now();
        LoginInfoDTO loginInfoDTO = LoginInfoDTO.builder()
            .uuid(uuid)
            .status(LoginStatusEnum.UNSCANNED.name())
            .createTime(now)
            .updateTime(now)
            .build();
​
        cache(loginInfoDTO);
​
        ResponseVO vo = ResponseVO.builder()
            .uuid(uuid)
            .qrcode("data:image/png;base64," + base64QR)
            .build();
​
        log.info("✅ 生成二维码成功 uuid: {}", uuid);
        return ResponseEntity.ok(vo);
    }

相当于我们构建了一个LoginInfoDTO的对象,放到了redis里面。之后进行了一个生成。返回的就是一个二维码。

typescript 复制代码
private void cache(LoginInfoDTO loginInfoDTO) {
    stringRedisTemplate.opsForValue().set(
        cacheKey(loginInfoDTO.getUuid()),
        JSONObject.toJSONString(loginInfoDTO),
        2,
        TimeUnit.MINUTES
    );
}

之后我们根据这个二维码,要有以下的一些接口。

首先是检测状态的接口:

scss 复制代码
/**
 * 检查扫码状态
 */
@GetMapping("/check/{uuid}")
public ResponseEntity<?> checkStatus(@PathVariable String uuid) {
    LoginInfoDTO loginInfoDTO = getCache(uuid);
    if (loginInfoDTO == null) {
        return ResponseEntity.status(410).body("二维码已过期");
    }
​
    String token = "";
    if (LoginStatusEnum.CONFIRMED.name().equals(loginInfoDTO.getStatus())) {
        token = JwtUtil.generateAuthToken(uuid);
        loginInfoDTO.setToken(token);
        loginInfoDTO.setUpdateTime(LocalDateTime.now());
        cache(loginInfoDTO);
    }
​
    ResponseVO vo = ResponseVO.builder()
        .token(token)
        .status(loginInfoDTO.getStatus())
        .build();
​
    log.info("🧐 校验二维码状态 uuid: {}, 状态: {}", uuid, loginInfoDTO.getStatus());
    return ResponseEntity.ok(vo);
}

我们从redis里面获得之前创建的loginInfoDTO。这里是如果状态是已经确认的我们就设置他的token以及UpdateTime

typescript 复制代码
private LoginInfoDTO getCache(String uuid) {
    String s = stringRedisTemplate.opsForValue().get(cacheKey(uuid));
    return s == null ? null : JSONObject.parseObject(s, LoginInfoDTO.class);
}

之后是手机端扫码的接口

less 复制代码
/**
 * 扫码(手机端)
 */
@PostMapping("/scan/{uuid}")
public ResponseEntity<?> scanQrCode(@PathVariable String uuid) {
    LoginInfoDTO loginInfoDTO = getCache(uuid);
    if (loginInfoDTO == null) {
        return ResponseEntity.status(410).body("二维码已过期");
    }
​
    loginInfoDTO.setStatus(LoginStatusEnum.SCANNED.name());
    loginInfoDTO.setUpdateTime(LocalDateTime.now());
    cache(loginInfoDTO);
​
    log.info("📱 扫码成功 uuid: {}", uuid);
    return ResponseEntity.ok().build();
}

就是当我们调用这个接口的时候,给他一个已扫描的信息。

同理登录也是这样

less 复制代码
/**
 * 确认登录(手机端)
 */
@PostMapping("/confirm/{uuid}")
public ResponseEntity<?> confirm(@PathVariable String uuid) {
    LoginInfoDTO loginInfoDTO = getCache(uuid);
    if (loginInfoDTO == null) {
        return ResponseEntity.status(410).body("二维码已过期");
    }
​
    loginInfoDTO.setStatus(LoginStatusEnum.CONFIRMED.name());
    loginInfoDTO.setUpdateTime(LocalDateTime.now());
    cache(loginInfoDTO);
​
    log.info("🔒 确认登录成功 uuid: {}", uuid);
    return ResponseEntity.ok().build();
}

在经过扫描和确认登录后。我们在去检测checkStatus她的状态的时候,就已经是登录成功的状态了。

至此我们的流程就结束了。下面来看具体的演示。

首先是生成二维码的接口

我们首先不扫码的时候,检查一下状态。

之后我们来进行一个扫码模拟操作

继续检查可以看到是已经扫码的状态

之后我们确认登录

状态就是登录的状态,并且有token了

这就是一个简单的模拟。

至此最简单的一个二维码扫码登录就完成了。我们可以根据实际开发来对这些接口进行一个更详细的补充

二维码扫码登录(第三方登录实现微信)

当然,如今的项目,大多数的项目已经不会自己去开发一个扫码的功能,而是去借助第三方来实现。最常见的有微信扫一扫登录,qq扫一扫登录等。

下面我讲用代码来演示如何去对接第三方的登录。

为什么要用第三方登录?首先,例如微信,qq这种应用,是每一个用户基本上都有的功能。并且通过第三方登录可以直接去获取一些信息,方便用户进行一个快速的注册。

对接微信公众号流程

  1. 在微信公众平台填写服务器接口配置

    • 配置服务器地址(URL)、Token、消息加解密密钥(EncodingAESKey)等信息。
  2. 服务器提供两个接口

    • GET接口:用于微信服务器进行首次验证(验签)。
    • POST接口:用于接收用户发送的消息或事件推送。
  3. 微信服务器调用 GET 接口进行验签操作

    • 验证是否是合法的服务器地址,根据 signature, timestamp, nonce 进行校验。
  4. 微信服务器调用 POST 接口进行消息推送/事件回调

    • 用户操作触发事件(如关注、扫码等)后,微信服务器将消息以POST方式发送给你的服务器。
  5. 通过调用微信接口获取二维码

    • 例如使用 生成带参数二维码接口。
  6. 获取二维码需要提供 ticket 凭证

    • 微信返回二维码 ticket,可根据该 ticket 获取二维码图片。
  7. 获取 ticket 凭证前需先获取 accessToken

    • accessToken 是调用所有微信开放接口的全局唯一凭证,有效期通常为 2 小时。
  8. 用户使用微信扫码

    • 用户扫码后,微信将扫码事件通过 POST 接口推送给你的服务器,可进行后续业务处理。

下面我们来分步来讲

首先先看官方的一个流程图

之后我们来实操。

首先是原理讲解的实操。

第一我们要

注册应用:在微信开放平台注册应用,获取AppIDAppSecret

配置回调域名:设置授权后的回调地址(redirect_uri),需通过微信审核

第二:

前面已经提到过获取微信二维码需要先获取到 accessToken 全局唯一接口调用凭据,再使用accessToken才能获取到 ticket,最后使用 ticket 获取二维码扫码登录

所以需要获取 accessToken 全局唯一接口调用凭据

https请求方式: GET api.weixin.qq.com/cgi-bin/tok...

开始开发 / 获取 Access token (qq.com)

该接口会返回 accessToken,并返回 accessToken 的有效期

第三:

获取 ticket 凭证

微信获取 ticket 以及使用 ticket 获取二维码文档

账号管理 / 生成带参数的二维码 (qq.com)

临时二维码请求说明:http请求方式: POST URL: api.weixin.qq.com/cgi-bin/qrc... POST数据格式:json POST数据例子:{"expire_seconds": 604800, "action_name": "QR_SCENE", "action_info": {"scene": {"scene_id": 123}}} 或者也可以使用以下POST数据创建字符串形式的二维码参数:{"expire_seconds": 604800, "action_name": "QR_STR_SCENE", "action_info": {"scene": {"scene_str": "test"}}}

永久二维码请求说明:http请求方式: POST URL: api.weixin.qq.com/cgi-bin/qrc... POST数据格式:json POST数据例子:{"action_name": "QR_LIMIT_SCENE", "action_info": {"scene": {"scene_id": 123}}} 或者也可以使用以下POST数据创建字符串形式的二维码参数: {"action_name": "QR_LIMIT_STR_SCENE", "action_info": {"scene": {"scene_str": "test"}}}

前面我们已经获取了 accessToken 唯一接口调用凭据,现在即可使用accessToken根据不同的场景(例如需要获取临时二维码或者永久二维码的不同请求)获取得到可以获取二维码的ticket

第四就是去请求二维码了

获取微信二维码

HTTP GET请求(请使用https协议)mp.weixin.qq.com/cgi-bin/sho...

成功扫码后微信会调用前面配置的回调接口 ,回调接口会传递给服务器openid参数,openid唯一标识一个用户,在回调接口中可以将ticket和openid进行绑定缓存,后续可通过ticket获取openid来判断用户是否登录

vue实现完整版

之后我们来看一种vue来实现的完整版。

首先引入插件

javascript 复制代码
// 安装
npm install vue-wxlogin --save-dev
// js引入
import wxlogin from 'vue-wxlogin'
components: { wxlogin }

使用插件

ini 复制代码
<wxlogin
        :appid="appid"
        :scope="'snsapi_login'"  // 网页固定的
        :theme="'black'"
        :redirect_uri="redirect_uri"
        :href='bast64css'
 
        >
 </wxlogin>
 
//   data
<wxlogin
        :appid="appid"
        :scope="'snsapi_login'"  // 网页固定的
        :theme="'black'"
        :redirect_uri="redirect_uri"
        :href='bast64css'
 
        >
 </wxlogin>
 
//   data
bast64css: 'data:text/css;base64,LmltcG93ZXJCb3ggLnFyY29kZSB7d2lkdGg6IDIwMHB4O2hlaWdodDoyMDBweH0NCi5pbXBvd2VyQm94IC5pbmZvIHt3aWR0aDogMjAwcHh9DQouc3RhdHVzX2ljb24ge2Rpc3BsYXk6IG5vbmV9DQouaW1wb3dlckJveCAuc3RhdHVzIHt0ZXh0LWFsaWduOiBjZW50ZXI7fQ0KLmltcG93ZXJCb3ggLnRpdGxlIHtkaXNwbGF5OiBub25lfQ0KaWZyYW1lIHtoZWlnaHQ6IDMyMnB4O30NCg==',
                appid: 'xxx', // 后端提供
                redirect_uri: 'xxx', // 后端提供

结果:这样微信二维码就会显示在自己写的网页上

扫描后,页面的url会给一个带code的地址 ,去获取code

下面是一些参数的说明

ini 复制代码
if (window.location.href.indexOf('code') >= 0) {
                let code = window.location.href.split('?')[1];
                code = code.substring(5, code.indexOf('&'));
                this.wechatcode = code
                this.wechatLogin()
            }

这个是用插件,如果不用插件我们可以

xml 复制代码
<!--微信授权登录按钮-->
<img src="@/assets/images/weixin.png" /><a style="line-height: 60px;height: 60px; margin: 0 5px;" type="text" @click="handLoginByWx">微信扫码登录</a>

配置登录相关参数,跳转微信登录二维码授权页面

ini 复制代码
// 跳转微信登录二维码授权页面
      handLoginByWx() {
        // 重定向地址重定到当前页面,在路径获取code
        const hrefUrl = window.location.href
        // 判断是否已存在code
        if (!this.code) {
        // 不存在,配置相关微信登录参数(主要是授权页面地址,appID,回调地址)
          window.location.href = `
              https://open.weixin.qq.com/connect/qrconnect
            ?appid=APPID
            &redirect_uri=${encodeURIComponent(hrefUrl)}
            &response_type=code
            &scope=snsapi_login
                    `
        }
      }

java实现完整版

这里我们用到了微信测试公众号,如果实际开发需要对自己的公众号进行配置审核。微信测试号:mp.weixin.qq.com/debug/cgi-b...

首先是代码架构

bash 复制代码
├── config
        │               │   └── JwtFilter.java   # JWT 身份认证拦截器
        │               ├── controller
        │               │   ├── WeixinServerController.java # 微信服务端调用接口
        │               │   └── WeixinUserController.java  # 浏览器调用接口
        │               ├── model
        │               │   ├── ApiResult.java     
        │               │   ├── ReceiveMessage.java  # 微信消息封装类
        │               │   └── WeixinQrCode.java # 微信二维码 Ticket 封装类
        │               ├── service
        │               │   ├── WeixinUserService.java # 微信调用处理类
        │               │   └── impl
        │               │       └── WeixinUserServiceImpl.java
        │               └── util
        │                   ├── AesUtils.java # AES 加密工具类
        │                   ├── ApiResultUtil.java
        │                   ├── HttpUtil.java # HTTP 工具类
        │                   ├── JwtUtil.java # JWT 工具类
        │                   ├── KeyUtils.java 
        │                   ├── WeixinApiUtil.java # 微信 API 工具类,如获取 AccessToken
        │                   ├── WeixinMsgUtil.java # 微信消息工具类
        │                   ├── WeixinQrCodeCacheUtil.java # 微信二维码Ticket缓存
        │                   └── XmlUtil.java 

WeixinServerControllerWeixinUserController 暴漏了三个 API。

/weixin/check :用于对接微信服务端,接收微信服务端的调用。

/user/qrcode: 用于获取二维码图片信息

/user/login/qrcode: 用于校验是否扫描成功,成功则返回身份认证后的 JWT 字符串。

验证签名实现

typescript 复制代码
// com.wdbyte.weixin.service.impl.WeixinUserServiceImpl.java
​
@Value("${weixin.token}")
private String token;
​
@Override
public void checkSignature(String signature, String timestamp, String nonce) {
    String[] arr = new String[] {token, timestamp, nonce};
    Arrays.sort(arr);
    StringBuilder content = new StringBuilder();
    for (String str : arr) {
        content.append(str);
    }
    String tmpStr = DigestUtils.sha1Hex(content.toString());
    if (tmpStr.equals(signature)) {
        log.info("check success");
        return;
    }
    log.error("check fail");
    throw new RuntimeException("check fail");
}

获取 Access Token

获取带有 Ticket 的公众号二维码之前,需要先获取公众号的 Access Token,这是调用微信公众号所有接口的前提。 Access Token 每日调用次数有限,应该进行缓存。

typescript 复制代码
// com.wdbyte.weixin.util.WeixinApiUtil.java
@Value("${weixin.appid}")
public String appId;
​
@Value("${weixin.appsecret}")
public String appSecret;
​
private static String ACCESS_TOKEN = null;
private static LocalDateTime ACCESS_TOKEN_EXPIRE_TIME = null;
​
/**
 * 获取 access token
 *
 * @return
 */
public synchronized String getAccessToken() {
    if (ACCESS_TOKEN != null && ACCESS_TOKEN_EXPIRE_TIME.isAfter(LocalDateTime.now())) {
        return ACCESS_TOKEN;
    }
    String api = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appId + "&secret="
        + appSecret;
    String result = HttpUtil.get(api);
    JSONObject jsonObject = JSON.parseObject(result);
    ACCESS_TOKEN = jsonObject.getString("access_token");
    ACCESS_TOKEN_EXPIRE_TIME = LocalDateTime.now().plusSeconds(jsonObject.getLong("expires_in") - 10);
    return ACCESS_TOKEN;
}

生成登录二维码

使用 Access Token 获取二维码 Ticket 用来换取二维码图片。

arduino 复制代码
// com.wdbyte.weixin.util.WeixinApiUtil.java
private static String QR_CODE_URL_PREFIX = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=";
​
/**
 * 二维码 Ticket 过期时间
 */
private static int QR_CODE_TICKET_TIMEOUT = 10 * 60;
​
​
/**
 * 获取二维码 Ticket
 *
 * https://developers.weixin.qq.com/doc/offiaccount/Account_Management/Generating_a_Parametric_QR_Code.html
 *
 * @return
 */
public WeixinQrCode getQrCode() {
    String api = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=" + getAccessToken();
    String jsonBody = String.format("{\n"
        + "  "expire_seconds": %d,\n"
        + "  "action_name": "QR_STR_SCENE",\n"
        + "  "action_info": {\n"
        + "    "scene": {\n"
        + "      "scene_str": "%s"\n"
        + "    }\n"
        + "  }\n"
        + "}", QR_CODE_TICKET_TIMEOUT, KeyUtils.uuid32());
    String result = HttpUtil.post(api, jsonBody);
    log.info("get qr code params:{}", jsonBody);
    log.info("get qr code result:{}", result);
    WeixinQrCode weixinQrCode = JSON.parseObject(result, WeixinQrCode.class);
    weixinQrCode.setQrCodeUrl(QR_CODE_URL_PREFIX + URI.create(weixinQrCode.getTicket()).toASCIIString());
    return weixinQrCode;
}
class WeixinQrCode {
    private String ticket;
    private Long expireSeconds;
    private String url;
    private String qrCodeUrl;
}
​

其中 Ticket 就是二维码凭证,用户扫码后微信会把此 Ticket 回调给网站服务端。可以在下面的链接后面拼上 Ticket 换取二维码图片。

完整的源码在xiaou61/xiaou-easyproject-backend: 后端通用接口代码片段汇总 (github.com)

二维码活码实现

二维码还有一个功能,比如说,你想要二维码里面的东西是可以后台控制的。

二维码"活码"的核心思路是:二维码的内容不直接指向最终内容,而是指向我们自建服务器的一个中间跳转地址(一般是带唯一ID的短链接),然后通过后台动态控制跳转到什么内容。

我们可以用 Spring Boot 来构建这个"活码"系统比如文字、图片、网页链接等。

这个过于简单了所以我只给一个数据库。

sql 复制代码
CREATE TABLE dynamic_qr (
    id VARCHAR(50) PRIMARY KEY,       -- 唯一短码(二维码内容中包含的部分)
    type VARCHAR(20),                 -- 类型:text、url、html、image
    content TEXT,                     -- 存储实际内容或地址
    create_time DATETIME,
    update_time DATETIME
);
​

就是一个简单的crud。

或者说通过一些别的东西来实现。

例如草料二维码的活码功能等。

相关推荐
DreamBoat_Onism几秒前
JVM 概述
java·jvm·后端
努力的搬砖人.1 分钟前
Spring Boot整合Kafka的详细步骤
spring boot·后端·kafka
独爱竹子的功夫熊猫5 分钟前
从复杂到优雅:用建造者和责任链重塑代码架构
java·后端·设计模式
movee22 分钟前
高性能编程开发(一):HugePage
linux·后端·性能优化
谦行31 分钟前
前端视角 Java Web 入门手册 5.6:真实世界 Web 开发——Redis
java·spring boot·后端
杯莫停丶39 分钟前
Web Worker在uniapp鸿蒙APP中的深度应用
前端·uni-app
小小小小宇41 分钟前
重新探讨React Diff算法
前端
风象南1 小时前
SpringBoot中4种接口幂等性实现策略
java·spring boot·后端
excel1 小时前
webpack 模块图 第 五 节
前端
好_快1 小时前
Lodash源码阅读-baseIndexOfWith
前端·javascript·源码阅读