无感刷新 token

在这里插入图片描述

文章目录

    • 背景
    • 基本思路
    • 需解决的问题
      • 请求进入死循环
      • 标记刷新 token 请求避免请求拦截覆盖 refresh token
      • 并发刷新 token
    • 完整代码
    • 注意:拦截器注册顺序
    • 另一种方案:事件驱动刷新

前景提要:

  • ts 简易封装 axios,统一 API

  • 实现在 config 中配置开关拦截器

  • axios 实现请求 loading 效果

背景

无感刷新 token 一般指的是使用 refresh token 无感刷新 access token。

基本思路

设置全局请求拦截器,从 localstorage 或其他地方获取 token 放在请求头中携带。在响应拦截器中,判断响应结果中是否有 token,有就存下来放在 localstorage 或其他地方。

一句话总结就是本地有就带,响应有就存。

实现自动刷新,就是在响应拦截之前的基础上再加一个判断,如果 access token 过期了,就携带 refresh token 去请求认证中心的接口。拿到新的 access token 后,再次对业务接口发起请求。

  • 注意别忘了要让新业务请求携带最新的 access token。

在axios拦截器中重新发起请求,就是拿到业务请求的 config,用axios实例发起请求。所以也可以说,一个请求的本质就是它的config配置对象。

src\api\http\token.ts

import { AxiosResponse, InternalAxiosRequestConfig } from "axios";
import httpRequest from "..";
import { refreshAccessToken } from "../modules/refreshToken";

export const ACCESS_TOKEN_KEY = "access_token";
export const REFRESH_TOKEN_KEY = "refresh_token";
export const UNAUTHORIZED_STATUS_CODE = 401;

// token 工具函数

/**
 * 获取token
 * @param key token的key
 * @returns {string}accessToken
 */
export function getToken(key: string) {
    return localStorage.getItem(key);
}

/**
 * 存储token到本地
 * @param key token的key
 * @param token token的值
 */
export function setToken(key: string, token: string) {
    localStorage.setItem(key, token);
}

// 拦截器

/**
 * 请求拦截器:只要本地有access token,所有请求的请求头就携带上它。(对于没有采用单点登录方案的系统,access token就是普通 token)
 * @param {InternalAxiosRequestConfig}config
 * @returns {InternalAxiosRequestConfig}config
 */
export function setAccessTokenRequestInterceptor(config: InternalAxiosRequestConfig) {
    if (config.headers) config.headers.authorization = `Bearer ${getToken("accessToken")}`;
    return config;
}

/**
 * 响应拦截器:只要服务器响应了 token,就保存下来到本地,无论是 access token 还是 refresh token。
 * 当接口因权限拒绝或者本地计算 access token 过期,则就去拿 refresh token 无感刷新 access token。
 * @param {AxiosResponse}res
 * @returns {AxiosResponse}res
 */
export async function getTokenResponseInterceptor(res: AxiosResponse) {
    // 假设服务器将token放在响应头中返回

    // 保存授权 token,也就是 access token 或者普通的 token
    if (res.headers.authorization) {
        const token = res.headers.authorization.repalce("Bearer ", "");
        setToken(ACCESS_TOKEN_KEY, token);
    }

    // 保存 refresh token
    if (res.headers.refreshtoken) {
        const refreshToken = res.headers.refreshtoken.repalce("Bearer ", "");
        setToken(REFRESH_TOKEN_KEY, refreshToken);
    }

    // 请求业务接口没有权限,说明 access token 过期,需要刷新 token
    if (res.data.code === UNAUTHORIZED_STATUS_CODE) {
        // 请求服务器获取最新access token,当前拦截器递归保存token
        const isRefreshSuccess = await refreshAccessToken();
        if (isRefreshSuccess) {
            // 刷新 access token 成功,装配新 access token 后拿到 axios 实例重新发起请求
            res.config.headers.Authorization = `Bearer ${getToken(ACCESS_TOKEN_KEY)}`;
            const response = await httpRequest.getInstance().request(res.config);
            return response;
        } else {
            // 刷新失败,refresh token 过期,跳转登录页面重新登录
           	window.location.hash = "/login";
            // window.location.href = "/login";
        }
    }

    return res;
}

src\api\modules\refreshToken.ts

import httpRequest from "..";
import { REFRESH_TOKEN_KEY, UNAUTHORIZED_STATUS_CODE, getToken } from "../http/token";

const REFRESH_TOKEN_API = "/refreshToken";

/**
 * 获取 refresh token 的接口
 * 这个接口不同于业务接口,它携带的 token 是 refresh token 而不是 access token。
 * 当请求 access token 回来后,就会启动响应拦截将 access token 保存
 * 返回一个布尔值,用于判断是否刷新成功,因为 refresh token 也会过期,导致刷新失败。
 * @returns {boolean} isRefreshSuccess 刷新 access token 是否成功。
 */
export async function refreshAccessToken() {
    const res = await httpRequest.get({
        url: REFRESH_TOKEN_API,
        headers: {
            Authorization: `Bearer ${getToken(REFRESH_TOKEN_KEY)}`
        }
    });

    // 响应状态码不为 401,则表示刷新成功
    return res.code !== UNAUTHORIZED_STATUS_CODE;
}

需解决的问题

请求进入死循环

假如 refresh token 也过期了,那么携带 refresh token 去刷新 access token时就会被拒绝,refreshAccessToken 请求失败,状态码 401。这时因为是 401,响应拦截器中就又会以为是 access token 过期,又拿着 refresh token 去刷新。至此陷入死循环了。

核心就是当前是否启动无感刷新 access token 的判断条件,需要区分是 access token 过期导致的业务接口拒绝,还是 refresh token 过期导致的授权接口拒绝。

解决办法可以是服务器接口给出过期时间,或者前端自己解析 jwt,拿到过期时间。然后通过判断 access token 过期时间来选择是否要去刷新。

假如通过接口返回 401 来判断。这时可以引入一个变量做标志,表明当前请求是否是刷新 access token 的请求,还是业务请求。
src\api\modules\refreshToken.ts

export async function refreshAccessToken() {
    const res = await httpRequest.get({
        url: REFRESH_TOKEN_API,
        headers: {
            Authorization: `Bearer ${getToken(REFRESH_TOKEN_KEY)}`,
            _isRefreshAccessTokenRequest: true // 标记当前请求为刷新 token 请求
        }
    });
    return res.code !== UNAUTHORIZED_STATUS_CODE;
}

src\api\http\token.ts

// 请求业务接口没有权限,说明 access token 过期,需要刷新 token
if (res.data.code === UNAUTHORIZED_STATUS_CODE && !res.config.headers._isRefreshAccessTokenRequest) {
    // 请求服务器获取最新access token,当前拦截器递归保存token
    const isRefreshSuccess = await refreshAccessToken();
    if (isRefreshSuccess) {
        // 刷新 access token 成功,装配新 access token 后拿到 axios 实例重新发起请求
        res.config.headers.Authorization = `Bearer ${getToken(ACCESS_TOKEN_KEY)}`;
        const response = await httpRequest.getInstance().request(res.config);
        return response;
    } else {
        // 刷新失败,refresh token 过期,跳转登录页面重新登录
        window.location.hash = "/login";
        // window.location.href = "/login";
    }
}

标记刷新 token 请求避免请求拦截覆盖 refresh token

刷新 access token 的请求也是一个请求,它携带的是 refresh token。但是之前我们设置了全局的请求拦截器。又因为无论是 access token 还是 refresh token 都是放在请求头的 authorization 上携带,此时请求拦截设置的 access token 就会覆盖掉 refresh token,导致刷新接口拿不到 refresh token。
因此请求拦截器也要对刷新 token 的请求做额外的区分,过滤掉刷新请求。也可以通过请求头的标记实现。

export function setAccessTokenRequestInterceptor(config: InternalAxiosRequestConfig) {
    if (config.headers && !config.headers._isRefreshAccessTokenRequest) {
        config.headers.authorization = `Bearer ${getToken(ACCESS_TOKEN_KEY)}`;
    }
    return config;
}

并发刷新 token

如果当前有很多业务请求,然后 access token 刚好过期了。那这些业务请求的响应拦截器中都会拿着 refresh token 去请求刷新接口刷新 access token。前一个刷新请求还没拿到最新的 access token,后一个刷新请求又发出了,这就出现了并发刷新 token ,冗余发送请求的情况。

解决这个问题的核心,无非就是确定上一个刷新 token 的请求是否结束,它没结束后续的刷新请求就得等着。这种观测异步处理的状态,promise 干的就是这个。

定义一个全局的变量,用这个全局的变量保存刷新 token 的请求,也就是保存一个 promise。
变量初始是空的,因为没有刷新请求。当有刷新请求发起,就生成一个 promise 观测该请求,并将该 promise 保存在全局变量中。此时后续想要再次发起刷新请求,就直接返回这个“全局的请求”(promise)给它们,避免了发起冗余请求。并且这样当第一个刷新请求得到结果,后续所有请求就都拿到了结果,因为都是同一个 promise。
promise 有结果后,无论刷新成功与否,都代表了本轮并发刷新 token 请求的结束,需将全局变量重置为 null,以准备下一次并发刷新请求。

import httpRequest from "..";
import { REFRESH_TOKEN_KEY, UNAUTHORIZED_STATUS_CODE, getToken } from "../http/token";

const REFRESH_TOKEN_API = "/refreshtoken";

let promise: Promise<any> | null = null;

/**
 * 获取 refresh token 的接口
 * 这个接口不同于业务接口,它携带的 token 是 refresh token 而不是 access token。
 * 当请求 access token 回来后,就会启动响应拦截将 access token 保存
 * 返回一个 Promise 布尔值,用于判断是否刷新成功,因为 refresh token 也会过期,导致刷新失败。
 * @returns {Promise<boolean>} isRefreshSuccess 刷新 access token 是否成功。
 */
export function refreshAccessToken() {
    // 前面已经发送了刷新请求,promise 有值,此时后续请求全都返回最开始的 promise
    if (promise) {
        return promise;
    }
    promise = new Promise((resolve, rejects) => {
        httpRequest
            .get({
                url: REFRESH_TOKEN_API,
                headers: {
                    Authorization: `Bearer ${getToken(REFRESH_TOKEN_KEY)}`,
                    _isRefreshAccessTokenRequest: true
                }
            })
            .then(res => {
                resolve(res.code !== UNAUTHORIZED_STATUS_CODE);
            })
            .catch(() => rejects(false))
            .finally(() => {
                promise = null; // 本次并发刷新请求结束,重置变量为 null
            });
    });

    return promise;
}

完整代码

src\api\http\token.ts

import { AxiosResponse, InternalAxiosRequestConfig } from "axios";
import httpRequest from "..";
import { refreshAccessToken } from "../modules/refreshToken";

export const ACCESS_TOKEN_KEY = "access_token";
export const REFRESH_TOKEN_KEY = "refresh_token";
export const UNAUTHORIZED_STATUS_CODE = 401;

// token 工具函数

/**
 * 获取token
 * @param key token的key
 * @returns {string}accessToken
 */
export function getToken(key: string) {
    return localStorage.getItem(key);
}

/**
 * 存储token到本地
 * @param key token的key
 * @param token token的值
 */
export function setToken(key: string, token: string) {
    localStorage.setItem(key, token);
}

// 拦截器

/**
 * 请求拦截器:只要本地有access token,所有请求的请求头就携带上它。(对于没有采用单点登录方案的系统,access token就是普通 token)
 * @param {InternalAxiosRequestConfig}config
 * @returns {InternalAxiosRequestConfig}config
 */
export function setAccessTokenRequestInterceptor(config: InternalAxiosRequestConfig) {
    if (config.headers && !config.headers._isRefreshAccessTokenRequest) {
        config.headers.authorization = `Bearer ${getToken(ACCESS_TOKEN_KEY)}`;
    }
    return config;
}

/**
 * 响应拦截器:只要服务器响应了 token,就保存下来到本地,无论是 access token 还是 refresh token。
 * 当接口因权限拒绝或者本地计算 access token 过期,则就去拿 refresh token 无感刷新 access token。
 * @param {AxiosResponse}res
 * @returns {AxiosResponse}res
 */
export async function getTokenResponseInterceptor(res: AxiosResponse) {
    // 假设服务器将token放在响应体中返回

    // 保存授权 token,也就是 access token 或者普通的 token
    if (res.data.data?.accessToken) {
        const token = res.data.data.accessToken.replace("Bearer ", "");
        setToken(ACCESS_TOKEN_KEY, token);
    }

    // 保存 refresh token
    if (res.data.data?.refreshToken) {
        const refreshToken = res.data.data.refreshToken.replace("Bearer ", "");
        setToken(REFRESH_TOKEN_KEY, refreshToken);
    }

    // 请求业务接口没有权限,说明 access token 过期,需要刷新 token
    if (res.data.code === UNAUTHORIZED_STATUS_CODE && !res.config.headers._isRefreshAccessTokenRequest) {
        // 请求服务器获取最新access token,当前拦截器递归保存token
        const isRefreshSuccess = await refreshAccessToken();
        if (isRefreshSuccess) {
            // 刷新 access token 成功,装配新 access token 后拿到 axios 实例重新发起请求
            res.config.headers.Authorization = `Bearer ${getToken(ACCESS_TOKEN_KEY)}`;
            const response = await httpRequest.getInstance().request(res.config);
            return response;
        } else {
            // 刷新失败,refresh token 过期,跳转登录页面重新登录
            window.location.hash = "/login";
            // window.location.href = "/login";
        }
    }

    return res;
}

src\api\modules\refreshToken.ts

import httpRequest from "..";
import { REFRESH_TOKEN_KEY, UNAUTHORIZED_STATUS_CODE, getToken } from "../http/token";

const REFRESH_TOKEN_API = "/refreshtoken";

let promise: Promise<any> | null = null;

/**
 * 获取 refresh token 的接口
 * 这个接口不同于业务接口,它携带的 token 是 refresh token 而不是 access token。
 * 当请求 access token 回来后,就会启动响应拦截将 access token 保存
 * 返回一个 Promise 布尔值,用于判断是否刷新成功,因为 refresh token 也会过期,导致刷新失败。
 * @returns {Promise<boolean>} isRefreshSuccess 刷新 access token 是否成功。
 */
export function refreshAccessToken() {
    // 前面已经发送了刷新请求,promise 有值,此时后续请求全都返回最开始的 promise
    if (promise) {
        return promise;
    }
    promise = new Promise((resolve, rejects) => {
        httpRequest
            .get({
                url: REFRESH_TOKEN_API,
                headers: {
                    Authorization: `Bearer ${getToken(REFRESH_TOKEN_KEY)}`,
                    _isRefreshAccessTokenRequest: true
                }
            })
            .then(res => {
                resolve(res.code !== UNAUTHORIZED_STATUS_CODE);
            })
            .catch(() => rejects(false))
            .finally(() => {
                promise = null; // 本次并发刷新请求结束,重置变量为 null
            });
    });

    return promise;
}

测试代码

<template>
  <div>
    <h2>测试无感刷新 token</h2>
    <el-button type="primary" round @click="handleClickLogin">登录</el-button>
    <el-button type="primary" round @click="handleClickGetProtectData">请求受保护资源</el-button>
  </div>
</template>

<script setup lang="ts">
  import { login } from "@/api/modules/login";
  import { fetchUsersList } from "@/api/modules/user";

  const handleClickLogin = () => {
    login({ username: "admin" }).then(res => {
      console.log(res);
    });
  };

  const handleClickGetProtectData = async () => {
    const res = await fetchUsersList();
    console.log("res", res);
  };
</script>

<style scoped></style>

注意:拦截器注册顺序

注册无感刷新的响应拦截器要在防抖拦截器的后面。
如这样:

// debounceRequest
httpRequest.getInstance().interceptors.request.use(compareUrl);
httpRequest.getInstance().interceptors.response.use(filterFulfilledUrl);

// token
httpRequest.getInstance().interceptors.request.use(setAccessTokenRequestInterceptor);
httpRequest.getInstance().interceptors.response.use(getTokenResponseInterceptor);

请求防抖是通过比较请求 url 来实现的。在请求拦截器中保存当前请求的url到数组中,后续的请求都需要判断一下,当前请求的url是否已经在数组中。当响应拦截器启动,说明请求完毕,就从数组中清除此url。

此时问题就来了,假如 token 的响应拦截定义在防抖响应拦截器的前面。(axios响应拦截,越晚定义越晚执行)
当 access token 过期,token 响应拦截器中会去刷新 token,并对业务接口重新发起请求。注意,此时仍然处于上一次被拒绝请求的拦截器中。那后续的防抖响应拦截器肯定还没执行,也就是还没有清除数组中被拒绝请求的url。此时又重新发送了请求,防抖的请求拦截中就会发挥防抖功能抛出错误“请求频繁"。
因此防抖响应拦截器和无感刷新 token 的响应拦截器有注册顺序,token 拦截要后注册。

另一种方案:事件驱动刷新

这种方式以某个页面事件触发刷新,而不是在拦截器中判断所有请求的结果。

用户登陆返回accesToken,refreshToken 还有accesToken有效时间戳。
每次加载到home页面,直接判断accesToken是否过期,过期了直接用refreshToken请求刷新accesToken接口,返回新的accesToken,新的refreshToken,accesToken的有效时间戳。

accesToken的有效时间戳并不是accesToken真正失效时间,一般会比真的失效时间点会提前的。

至于其他request和这套机制完全独立的。只是每次请求带上accessToken罢了。不用考虑accesToken过期啥的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/115282.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

海康Visionmaster调试脚本:对脚本进行调试的方法

第一步&#xff0c;在脚本模块中使用导出工程功能&#xff0c;将模块中的代码导出 第二步&#xff0c;找到导出的工程&#xff0c;并打开 第三步&#xff0c;生成解决方案&#xff0c;设置断点&#xff0c;点击 VS 菜单调试中的附加到进程&#xff0c;选择 ShellModuleManage…

计算虚拟化1——CPU虚拟化

目录 vCPU的概念 vCPU和CPU的关系 CPU的Ring级别 CPU虚拟化技术 软件辅助全虚拟化 半虚拟化 硬件辅助虚拟化 计算资源的虚拟化可以分为CPU虚拟化、内存虚拟化、I/O虚拟化三个方面 CPU虚拟化&#xff1a;多个虚拟机共享CPU资源&#xff0c;对虚拟机中的敏感指令进行截获…

EntherNet IP通讯学习

# 哎 最近接触ENIP通讯&#xff0c;但是觉得这玩意真的挺复杂的&#xff0c;主要是资料太少了。 好像大家都在保密一样。 1、学习这个通讯一定是因为实际工作中有用到&#xff0c;所以这个时候你一定有一个PLC做了从站。 OK&#xff0c;那下面继续学习吧&#xff01; 首先先上…

数智赋能!麒麟信安参展全球智慧城市大会

10月31日至11月2日&#xff0c;为期三天的2023全球智慧城市大会长沙在湖南国际会展中心举办&#xff0c;大会已连续举办12届&#xff0c;是目前全球规模最大、专注于城市和社会智慧化发展及转型的主题展会。长沙市委常委、常务副市长彭华松宣布开幕&#xff0c;全球智慧城市大会…

TypeScript 第一站概念篇

前言 &#x1f52e; 好长一段时间没有写文章了&#xff0c;原因是经历了一次工作变动&#xff0c;加入了一个有一定规模的开发团队&#xff0c;前端算上我有四个人&#xff0c;很欣慰&#xff0c;体验一下团队配合的感觉&#xff0c;在我之上有一个组长&#xff0c;比我年长四…

Mozilla Firefox 119 现已可供下载

Mozilla Firefox 119 开源网络浏览器现在可以下载了&#xff0c;是时候先看看它的新功能和改进了。 Firefox 119 改进了 Firefox View 功能&#xff0c;现在可以提供更多内容&#xff0c;如最近关闭的标签页和浏览历史&#xff0c;你可以按日期或网站排序&#xff0c;还支持查…

学习笔记三十一:k8s安全管理:认证、授权、准入控制概述SA介绍

K8S安全实战篇之RBAC认证授权-v1 k8s安全管理&#xff1a;认证、授权、准入控制概述认证k8s客户端访问apiserver的几种认证方式客户端认证&#xff1a;BearertokenServiceaccountkubeconfig文件 授权Kubernetes的授权是基于插件形成的&#xff0c;其常用的授权插件有以下几种&a…

SpringBoot集成Dubbo

在SpringMVC中Dubbo的使用https://tiantian.blog.csdn.net/article/details/134194696?spm1001.2014.3001.5502 阿里巴巴提供了Dubbo集成SpringBoot开源项目。(这个.....) 地址GitHub https://github.com/apache/dubbo-spring-boot-project 查看入门教程 反正是pilipala一大…

【技术分享】RK356X Android 使用 libgpiod 测试gpio

前言 libgpiod 是用于与 Linux GPIO 字符设备交互的 C 库和工具库&#xff1b;此项目包含六种命令行工具&#xff08;gpiodetect、gpioinfo、gpioset、gpioget、gpiomon&#xff09;&#xff0c;使用这些工具可以在命令行设置和获取GPIO的状态信息&#xff1b;在程序开发中也可…

网易按照作者批量采集新闻资讯软件说明文档

大家好&#xff0c;我是淘小白~ 今天给大家介绍的爬虫软件是网易按照作者采集的软件 1、软件语言&#xff1a; Python 2、使用到的工具 Python selenium库、谷歌浏览器、谷歌浏览器驱动 3、文件说明&#xff1a; 4、配置文件说明&#xff1a; 5、环境配置 安装Python&am…

【入门Flink】- 03Flink部署

集群角色 Flik提交作业和执行任务&#xff0c;需要几个关键组件&#xff1a; 客户端(Client)&#xff1a;代码由客户端获取并做转换&#xff0c;之后提交给JobManger JobManager&#xff1a;就是Fink集群里的“管事人”&#xff0c;对作业进行中央调度管理&#xff1b;而它获…

《面向对象软件工程》笔记——1-2章

“学习不仅是一种必要&#xff0c;而且是一种愉快的活动。” - 尼尔阿姆斯特朗 文章目录 第一章 面向对象软件工程的范畴历史方面经济方面维护方面现代软件维护观点交付后维护的重要性 需求、分析和设计方面团队开发方面没有计划&#xff0c;测试&#xff0c;文档阶段的原因面向…

Nginx简介,Nginx搭载负载均衡以及Nginx部署前端项目

目录 一. Nginx简介 Nginx的优点 二. Nginx搭载负载均衡 2.1 Nginx安装 2.1.1 安装依赖 2.1.2 解压nginx安装包 2.1.3 安装nginx 2.1.4 启动nginx服务 2.2 tomcat负载均衡 2.3 Nginx配置 三. Nginx前端部署 一. Nginx简介 NGINX&#xff08;读作&#xff1a;engi…

欧科云链研究院:如何降低Web3风险,提升虚拟资产创新的安全合规

在香港Web3.0行业&#xff0c;技术推动了虚拟资产投资市场的快速增长&#xff0c;但另一方面&#xff0c;JPEX诈骗案等行业风险事件也接连发生&#xff0c;为Web3行业发展提供了重要警示。在近期的香港立法会施政报告答问会上&#xff0c;行政长官李家超表示&#xff0c;与诈骗…

win10 下编译ffmpeg3.36.tar.gz

所需工具&#xff1a; win10 ffmpeg3.36.tar.gz。 或其他版本&#xff0c;下载地址&#xff1a;Index of /releases msys2。 下载地址&#xff1a;http://www.msys2.org。 Visual Studio 2017。 1. 安装MSYS MSYS2像是windows下的一个子系统&#xff0c;…

3.4_Linux-浏览文件系统

1.Linux 文件系统 如果你刚接触Linux系统&#xff0c;可能就很难弄清楚Linux如何引用文件和目录&#xff0c;对已经习惯Microsoft Windows操作系统方式的人来说更是如此。在继续探索Linux系统之前&#xff0c;先了解一下它的布局是有好处的。 你将注意到的第一个不同点是&…

MASK-RCNN tensorflow环境搭建

此教程默认你已经安装了Anaconda&#xff0c;且tensorflow 为cpu版本。为什么不用gpu版本&#xff0c;原因下面解释。 此教程默认你已经安装了Anaconda。 因为tensorflow2.1后的gpu版&#xff0c;不支持windows。并且只有高版本的tensorflow才对应我的CUDA12.2&#xff1b; 而…

从零开始的JSON库教程(一)

本文是学习github大佬miloyip而做的读书笔记&#xff0c;项目点此进入 目录 1、JSON是什么 2、搭建编译环境 3、头文件与API设计 4、JSON的语法子集 5、单元测试 6、宏的编写技巧 7、实现解析器 8、关于断言 1、JSON是什么 JSON&#xff08;JavaScript Object Notati…

SoftwareTest5 - 你就只知道功能测试吗 ?

你就只知道功能测试吗 ? 一 . 按照测试对象划分1.1 文档测试1.2 可靠性测试1.3 容错性测试1.4 安装卸载测试1.5 内存泄漏测试1.6 弱网测试 二 . 按是否查看代码划分2.1 黑盒测试2.2 白盒测试2.3 灰盒测试 三 . 按照开发阶段划分3.1 单元测试3.2 集成测试3.3 冒烟测试3.4 系统测…

用自己的数据集训练YOLO-NAS目标检测器

YOLO-NAS 是 Deci 开发的一种新的最先进的目标检测模型。 在本指南中&#xff0c;我们将讨论什么是 YOLO-NAS 以及如何在自定义数据集上训练 YOLO-NAS 模型。 在线工具推荐&#xff1a; Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 -…
最新文章