JavaScript 错误处理与测试 Errors and Testing 实战指南

在 JavaScript 开发中,错误处理与测试是确保代码质量和系统稳定性的关键环节。无论是前端页面交互,还是后端服务逻辑,错误处理能够帮助我们优雅地应对各种异常情况,而测试则能够提前发现潜在问题,避免它们在生产环境中引发灾难。

然而,许多开发者在实际开发中常常忽视这两个方面,导致代码中存在大量未处理的错误,或者测试用例不够全面,最终引发各种难以排查的问题。本教程将系统地介绍 JavaScript 中的错误处理与测试技术,从基础的错误捕获到高级的异步错误处理,再到代码单元测试和覆盖率追踪,帮助你构建更加健壮、可靠的 JavaScript 应用。

无论你是初学者,还是有一定经验的开发者,本教程都适合你。我们将通过丰富的示例和实战技巧,带你深入理解 JavaScript 错误处理与测试的核心概念,并掌握实用的方法和工具。让我们一起踏上这段提升代码质量的旅程吧!

1. 捕获并处理错误

1.1 try-catch语句基本用法

在JavaScript中,try-catch语句是捕获和处理同步错误的基本方式。try块中包含可能抛出错误的代码,而catch块则用于捕获并处理这些错误。例如:

try {
    // 可能会抛出错误的代码
    let result = someFunction();
} catch (error) {
    // 处理错误
    console.error("发生错误:", error.message);
}

这种方式可以有效避免程序因错误而崩溃,同时提供更友好的错误处理逻辑。

1.2 finally块的作用

finally块可以与try-catch语句一起使用,无论是否发生错误,finally块中的代码都会执行。这在清理资源或执行必要的收尾工作时非常有用。例如:

try {
    let data = fetchData();
} catch (error) {
    console.error("获取数据时发生错误:", error.message);
} finally {
    // 无论是否发生错误,都会执行
    cleanUp();
}

finally块确保了即使在错误发生时,资源也能被正确释放,增强了代码的健壮性。

2. 捕获不同类型的错误

2.1 常见错误类型及其特点

JavaScript 中有多种常见的错误类型,每种错误类型都有其特定的触发场景和特点。了解这些错误类型有助于我们更精准地捕获和处理错误。

  • SyntaxError(语法错误):这是由于代码中存在语法问题而引发的错误,例如缺少括号、引号不匹配或关键字使用错误。这类错误通常在代码执行前就能被检测到,因为它们会阻止代码的正常解析。

  • TypeError(类型错误):当操作的数据类型不符合预期时会抛出此错误。例如,尝试对一个字符串调用 .length 属性以外的方法,或者对一个未定义的变量进行操作。

  • ReferenceError(引用错误):当尝试访问一个未声明的变量时会抛出此错误。这通常是因为拼写错误或变量未正确声明导致的。

  • RangeError(范围错误):当数值超出有效范围时会抛出此错误。例如,数组的索引超出范围或数字过大导致溢出。

  • URIError(URI错误):与 URI 相关的操作(如 encodeURIdecodeURI)中,如果传入的参数无效,会抛出此错误。

  • EvalError(eval错误):与 eval() 函数相关的错误。虽然现代 JavaScript 中较少使用 eval(),但仍然保留了这个错误类型。

2.2 针对不同错误类型进行捕获

在实际开发中,我们可以通过 instanceof 操作符或错误对象的 name 属性来判断错误类型,并根据不同的错误类型执行不同的处理逻辑。例如:

try {
    // 可能会抛出错误的代码
    let result = someFunction();
} catch (error) {
    if (error instanceof TypeError) {
        console.error("类型错误:", error.message);
    } else if (error instanceof ReferenceError) {
        console.error("引用错误:", error.message);
    } else if (error instanceof RangeError) {
        console.error("范围错误:", error.message);
    } else {
        console.error("未知错误:", error.message);
    }
}

通过这种方式,我们可以更精确地处理不同类型的错误,提供更有针对性的错误信息和解决方案。

3. 捕获异步错误

3.1 Promise中的错误捕获

在JavaScript中,异步编程是常见的开发模式,而Promise是处理异步操作的重要工具。Promise的错误捕获主要通过.catch()方法或then()方法的第二个参数来实现。

  • 使用.catch()方法捕获错误.catch()方法是专门用于捕获Promise链中的错误。它会捕获从Promise链的起点到.catch()方法之间的任何错误。例如:

  • fetch('https://api.example.com/data')
        .then(response => {
            if (!response.ok) {
                throw new Error('网络响应错误');
            }
            return response.json();
        })
        .then(data => {
            console.log('数据:', data);
        })
        .catch(error => {
            console.error('捕获到错误:', error.message);
        });

    在这个例子中,如果fetch请求失败或在then()链中抛出错误,.catch()方法会捕获到这些错误并处理。

  • 使用then()方法的第二个参数捕获错误then()方法的第二个参数也可以用于捕获错误,但它只能捕获当前Promise的错误,而不能捕获链中后续Promise的错误。例如:

  • fetch('https://api.example.com/data')
        .then(response => {
            if (!response.ok) {
                throw new Error('网络响应错误');
            }
            return response.json();
        }, error => {
            console.error('捕获到错误:', error.message);
        })
        .then(data => {
            console.log('数据:', data);
        });

    在这个例子中,then()的第二个参数会捕获fetch请求失败时的错误,但不会捕获后续then()链中的错误。

  • 全局错误捕获:除了在Promise链中捕获错误,还可以通过监听unhandledrejection事件来捕获未处理的Promise错误。这在调试和监控全局错误时非常有用。例如:

  • window.addEventListener('unhandledrejection', event => {
        console.error('未处理的Promise错误:', event.reason);
    });

3.2 async/await中的错误捕获

async/await是现代JavaScript中处理异步操作的另一种方式,它使得异步代码的编写更加简洁和易于理解。在async/await中,错误捕获主要通过try-catch语句来实现。

  • 基本用法:在async函数中,可以使用try-catch语句来捕获异步操作中抛出的错误。例如:

  • async function fetchData() {
        try {
            let response = await fetch('https://api.example.com/data');
            if (!response.ok) {
                throw new Error('网络响应错误');
            }
            let data = await response.json();
            console.log('数据:', data);
        } catch (error) {
            console.error('捕获到错误:', error.message);
        }
    }
    fetchData();

    在这个例子中,如果fetch请求失败或在await表达式中抛出错误,catch块会捕获到这些错误并处理。

  • 嵌套的try-catch:在复杂的异步操作中,可能需要嵌套使用try-catch语句来捕获不同层次的错误。例如:

  • async function fetchData() {
        try {
            let response = await fetch('https://api.example.com/data');
            if (!response.ok) {
                throw new Error('网络响应错误');
            }
            try {
                let data = await response.json();
                console.log('数据:', data);
            } catch (innerError) {
                console.error('解析数据时发生错误:', innerError.message);
            }
        } catch (outerError) {
            console.error('获取数据时发生错误:', outerError.message);
        }
    }
    fetchData();

    在这个例子中,外层的try-catch用于捕获网络请求相关的错误,而内层的try-catch用于捕获数据解析相关的错误。

  • 结合Promise.all捕获错误:当需要同时处理多个异步操作时,可以使用Promise.all。在Promise.all中,如果任何一个Promise失败,整个Promise.all会立即失败并抛出错误。例如:

  • async function fetchMultipleData() {
        try {
            let [data1, data2] = await Promise.all([
                fetch('https://api.example.com/data1').then(response => response.json()),
                fetch('https://api.example.com/data2').then(response => response.json())
            ]);
            console.log('数据1:', data1);
            console.log('数据2:', data2);
        } catch (error) {
            console.error('捕获到错误:', error.message);
        }
    }
    fetchMultipleData();

    在这个例子中,如果任何一个fetch请求失败,Promise.all会立即抛出错误,catch块会捕获到这个错误并处理。

4. 检测未处理的错误

4.1 window.onerror事件监听

window.onerror 事件监听器是 JavaScript 中用于捕获全局同步错误的重要机制。它可以在代码中未被 try-catch 捕获的错误发生时触发,从而提供一个最后的错误处理机会。

  • 基本用法window.onerror 事件监听器可以接收多个参数,包括错误消息、脚本 URI、行号、列号和错误对象。这些信息对于调试和记录错误非常有用。例如:

  • window.onerror = function (message, source, lineno, colno, error) {
        console.error('捕获到全局错误:', message);
        console.error('错误位置:', source, lineno, colno);
        console.error('错误对象:', error);
        // 可以在这里将错误信息发送到服务器进行记录
        return true; // 返回 true 表示错误已被处理,不会显示默认的错误提示
    };
  • 应用场景:在生产环境中,window.onerror 可以用于捕获未处理的同步错误,并将错误信息发送到后端服务器进行记录和分析。这对于监控应用程序的运行状态和及时发现潜在问题非常有帮助。

  • 局限性window.onerror 无法捕获异步错误(如 Promise 中的错误)。此外,它也无法捕获某些浏览器内部错误(如某些浏览器扩展引发的错误)。

4.2 unhandledrejection事件监听

unhandledrejection 事件监听器是用于捕获未处理的 Promise 错误的机制。当一个 Promise 抛出错误且没有通过 .catch()try-catch 捕获时,unhandledrejection 事件会被触发。

  • 基本用法unhandledrejection 事件监听器可以接收一个 event 参数,其中包含错误信息。例如:

  • window.addEventListener('unhandledrejection', event => {
        console.error('捕获到未处理的 Promise 错误:', event.reason);
        // 可以在这里将错误信息发送到服务器进行记录
        event.preventDefault(); // 防止默认的控制台错误提示
    });
  • 应用场景:在现代 JavaScript 应用程序中,异步操作非常常见,因此 unhandledrejection 事件监听器对于监控和记录未处理的异步错误非常重要。它可以确保即使在复杂的异步代码中,也不会遗漏任何潜在的错误。

  • window.onerror 的结合使用:虽然 unhandledrejection 专门用于捕获 Promise 错误,但它并不能捕获同步错误。因此,通常建议同时使用 window.onerrorunhandledrejection,以实现对全局错误的全面监控。

5. 抛出标准错误

5.1 使用 throw 语句抛出标准错误

在 JavaScript 中,throw 语句用于显式地抛出一个错误。你可以抛出一个标准的错误对象,也可以抛出任何其他类型的值(如字符串、数字等)。然而,抛出标准错误对象是更常见的做法,因为它提供了更丰富的错误信息,便于调试和处理。

标准错误对象包括以下几种类型:

  • Error:通用错误类型,适用于大多数情况。

  • TypeError:当操作的数据类型不符合预期时抛出。

  • ReferenceError:当尝试访问未声明的变量时抛出。

  • RangeError:当数值超出有效范围时抛出。

  • SyntaxError:当代码中存在语法问题时抛出。

  • URIError:与 URI 相关的操作中,如果传入的参数无效时抛出。

以下是一个使用 throw 抛出标准错误的示例:

function divide(a, b) {
    if (b === 0) {
        throw new RangeError("除数不能为零");
    }
    return a / b;
}

try {
    let result = divide(10, 0);
} catch (error) {
    console.error("捕获到错误:", error.name, error.message);
}

在这个例子中,当 b 为零时,divide 函数会抛出一个 RangeError,并提供具体的错误信息。try-catch 语句捕获了这个错误,并打印了错误名称和消息。

5.2 抛出标准错误的应用场景

抛出标准错误在实际开发中非常有用,以下是一些常见的应用场景:

1. 输入验证

在函数中,当输入参数不符合预期时,抛出标准错误可以提供更清晰的错误信息,帮助调用者快速定位问题。例如:

function getUserById(id) {
    if (typeof id !== "number") {
        throw new TypeError("用户ID必须是一个数字");
    }
    // 假设这里有一个获取用户信息的逻辑
    return { id, name: "John Doe" };
}

try {
    let user = getUserById("abc");
} catch (error) {
    console.error("捕获到错误:", error.name, error.message);
}

在这个例子中,如果传入的 id 不是数字,函数会抛出一个 TypeError,并提示调用者正确的输入类型。

2. 异常处理

在处理可能出现异常的操作时,抛出标准错误可以更好地控制程序流程。例如,在文件操作或网络请求中,如果发生错误,可以抛出一个标准错误对象,以便在 catch 块中进行统一处理。例如:

function fetchData(url) {
    if (!url) {
        throw new ReferenceError("URL不能为空");
    }
    // 假设这里有一个网络请求的逻辑
    return fetch(url).then(response => {
        if (!response.ok) {
            throw new Error("网络响应错误");
        }
        return response.json();
    });
}

try {
    let data = fetchData();
} catch (error) {
    console.error("捕获到错误:", error.name, error.message);
}

在这个例子中,如果 url 为空,函数会抛出一个 ReferenceError,提示调用者必须提供有效的 URL。如果网络请求失败,会抛出一个通用的 Error,并提供具体的错误信息。

3. 资源管理

在管理资源(如文件句柄、数据库连接等)时,抛出标准错误可以确保在发生错误时能够正确地清理资源。例如:

function openFile(filePath) {
    if (!filePath) {
        throw new ReferenceError("文件路径不能为空");
    }
    // 假设这里有一个打开文件的逻辑
    let fileHandle = open(filePath);
    return fileHandle;
}

try {
    let file = openFile();
} catch (error) {
    console.error("捕获到错误:", error.name, error.message);
    // 清理资源
    closeFile(file);
}

在这个例子中,如果 filePath 为空,函数会抛出一个 ReferenceError,提示调用者必须提供有效的文件路径。在 catch 块中,可以捕获这个错误并进行资源清理,确保不会出现资源泄漏。

通过这些应用场景,我们可以看到抛出标准错误不仅可以提供更清晰的错误信息,还可以帮助我们更好地控制程序流程,提高代码的健壮性和可维护性。

6. 抛出自定义错误

6.1 创建自定义错误类

在 JavaScript 中,除了使用标准的错误类型外,我们还可以创建自定义错误类,以更好地描述特定的错误场景。自定义错误类可以通过继承 Error 类来实现,这样可以保留标准错误对象的特性,同时添加自定义的属性和方法。

以下是一个创建自定义错误类的示例:

class MyCustomError extends Error {
    constructor(message, code) {
        super(message); // 调用父类的构造函数
        this.name = "MyCustomError"; // 设置错误名称
        this.code = code; // 添加自定义属性
    }
}

在这个例子中,MyCustomError 类继承了 Error 类,并添加了一个自定义属性 code。通过这种方式,我们可以为自定义错误提供更丰富的信息。

自定义错误类的创建可以用于以下场景:

1. 业务逻辑错误

在复杂的业务逻辑中,可能会遇到一些特定的错误情况,这些情况无法用标准错误类型准确描述。例如,在一个电子商务系统中,可能会遇到库存不足的情况,这时可以抛出自定义错误:

class StockError extends Error {
    constructor(productName, availableStock) {
        super(`库存不足:${productName} 只剩下 ${availableStock} 件`);
        this.name = "StockError";
        this.productName = productName;
        this.availableStock = availableStock;
    }
}

2. API 错误

在开发 API 时,可能会遇到一些特定的错误情况,如请求参数无效或用户权限不足。这些情况可以通过自定义错误类来描述:

class ApiError extends Error {
    constructor(message, statusCode) {
        super(message);
        this.name = "ApiError";
        this.statusCode = statusCode;
    }
}

通过创建自定义错误类,我们可以为不同的错误场景提供更清晰、更有针对性的错误信息,便于调试和处理。

6.2 抛出自定义错误的实践

创建了自定义错误类后,我们可以在代码中抛出自定义错误。以下是一些抛出自定义错误的实践示例:

1. 在函数中抛出自定义错误

在函数中,当遇到特定的错误情况时,可以抛出自定义错误。例如:

function checkStock(productName, availableStock, requiredStock) {
    if (availableStock < requiredStock) {
        throw new StockError(productName, availableStock);
    }
    console.log("库存检查通过");
}

try {
    checkStock("产品A", 5, 10);
} catch (error) {
    if (error instanceof StockError) {
        console.error("捕获到库存错误:", error.message);
    } else {
        console.error("捕获到未知错误:", error.message);
    }
}

在这个例子中,当库存不足时,checkStock 函数会抛出一个 StockError,并提供具体的错误信息。try-catch 块捕获了这个错误,并根据错误类型进行了处理。

2. 在 API 中抛出自定义错误

在开发 API 时,可以在控制器中抛出自定义错误。例如:

function handleRequest(req, res) {
    if (!req.body.userId) {
        throw new ApiError("用户ID不能为空", 400);
    }
    // 处理请求的逻辑
    res.send("请求成功");
}

try {
    handleRequest({ body: {} }, { send: console.log });
} catch (error) {
    if (error instanceof ApiError) {
        console.error("捕获到API错误:", error.message, "状态码:", error.statusCode);
    } else {
        console.error("捕获到未知错误:", error.message);
    }
}

在这个例子中,当请求中缺少 userId 时,handleRequest 函数会抛出一个 ApiError,并提供错误信息和状态码。try-catch 块捕获了这个错误,并根据错误类型进行了处理。

通过抛出自定义错误,我们可以更好地控制程序流程,提供更清晰的错误信息,便于调试和维护。

7. 编写代码单元测试

7.1 选择合适的测试框架

在 JavaScript 开发中,选择合适的测试框架是编写单元测试的第一步。目前,常用的 JavaScript 测试框架有 Jest、Mocha 和 Jasmine 等,它们各有特点,适用于不同的开发场景。

  • Jest:由 Facebook 开发,是目前最流行的 JavaScript 测试框架之一。它提供了丰富的功能,如自动 mock、快照测试、代码覆盖率报告等。Jest 的安装和使用非常简单,支持零配置运行测试。例如,使用 Jest 可以轻松编写和运行测试用例:

npm install --save-dev jest

然后在 package.json 中添加测试脚本:

"scripts": {
    "test": "jest"
}

运行测试时只需执行:

  • npm test
  • Mocha:是一个灵活且功能强大的测试框架,支持多种断言库(如 Chai、Should.js 等)。Mocha 提供了丰富的钩子函数(如 beforeafterbeforeEachafterEach),可以方便地进行测试前后的准备工作。例如:

npm install --save-dev mocha

使用 Mocha 时,通常需要结合断言库来编写测试用例。以 Chai 为例:

  • npm install --save-dev chai
  • Jasmine:是一个行为驱动开发(BDD)风格的测试框架,提供了简洁的语法和丰富的断言方法。Jasmine 适合用于编写易于理解和维护的测试用例,尤其适合团队开发。例如:

  • npm install --save-dev jasmine

选择测试框架时,需要根据项目的具体需求和团队的开发习惯来决定。对于大多数现代 JavaScript 项目,Jest 是一个非常不错的选择,因为它提供了强大的功能和良好的社区支持。

7.2 编写测试用例的基本方法

编写测试用例是单元测试的核心环节。一个好的测试用例应该能够准确地验证代码的功能是否符合预期,并且能够覆盖各种边界情况和异常情况。

1. 测试用例的结构

一个完整的测试用例通常包括以下几个部分:

  • 测试描述:清晰地描述测试的目的和预期结果。

  • 测试代码:编写具体的测试逻辑,调用被测试的函数或方法。

  • 断言:使用断言库验证测试结果是否符合预期。

以 Jest 为例,一个简单的测试用例可能如下所示:

// sum.js
function sum(a, b) {
    return a + b;
}
module.exports = sum;

// sum.test.js
const sum = require('./sum');

test('测试 sum 函数', () => {
    expect(sum(1, 2)).toBe(3);
    expect(sum(-1, 1)).toBe(0);
    expect(sum(0, 0)).toBe(0);
});

在这个例子中,test 函数用于描述测试的目的,expect 是 Jest 提供的断言方法,用于验证测试结果是否符合预期。

2. 测试用例的分类

根据测试的目的和范围,测试用例可以分为以下几类:

  • 功能测试:验证代码是否实现了预期的功能。例如,测试一个排序函数是否能够正确地对数组进行排序。

  • 边界测试:验证代码在边界条件下的行为。例如,测试一个函数在输入为空、输入为最大值或最小值时的行为。

  • 异常测试:验证代码在异常情况下的行为。例如,测试一个函数在输入为非法值时是否抛出正确的错误。

以一个简单的数组排序函数为例,可以编写以下测试用例:

// sort.js
function sortArray(arr) {
    if (!Array.isArray(arr)) {
        throw new TypeError('输入必须是一个数组');
    }
    return arr.sort((a, b) => a - b);
}
module.exports = sortArray;

// sort.test.js
const sortArray = require('./sort');

test('测试正常排序', () => {
    expect(sortArray([3, 1, 2])).toEqual([1, 2, 3]);
});

test('测试空数组', () => {
    expect(sortArray([])).toEqual([]);
});

test('测试非法输入', () => {
    expect(() => sortArray('abc')).toThrow(TypeError);
});

在这个例子中,第一个测试用例验证了函数在正常输入下的行为,第二个测试用例验证了函数在空数组输入下的行为,第三个测试用例验证了函数在非法输入下的行为。

3. 测试覆盖率

测试覆盖率是指被测试代码中被执行的代码比例。高测试覆盖率意味着测试用例能够覆盖更多的代码逻辑,从而提高代码的可靠性和稳定性。常见的测试覆盖率指标包括:

  • 语句覆盖率:被测试的语句占总语句的比例。

  • 分支覆盖率:被测试的分支占总分支的比例。

  • 函数覆盖率:被测试的函数占总函数的比例。

Jest 提供了代码覆盖率报告功能,可以通过以下命令生成覆盖率报告:

npm test -- --coverage

生成的覆盖率报告会显示每个文件的语句覆盖率、分支覆盖率和函数覆盖率等信息。通过分析覆盖率报告,可以发现未被测试覆盖的代码,从而进一步完善测试用例。

编写高质量的测试用例是确保代码质量的重要手段。通过选择合适的测试框架和编写全面的测试用例,可以有效地提高代码的可靠性和可维护性。

8. 追踪测试代码覆盖率

8.1 使用工具追踪代码覆盖率

在 JavaScript 开发中,追踪代码覆盖率是确保测试质量的重要手段。常用的工具包括 Jest、Istanbul 等,它们能够自动分析代码的执行情况,并生成详细的覆盖率报告。

  • Jest:Jest 是一个强大的测试框架,内置了代码覆盖率工具。通过简单的命令,可以生成详细的覆盖率报告。例如,在项目中运行以下命令:

  • npm test -- --coverage

    Jest 会自动运行测试,并生成一个 coverage 文件夹,其中包含 HTML 格式的覆盖率报告。报告中详细展示了每个文件的语句覆盖率、分支覆盖率和函数覆盖率等信息。

  • Istanbul:Istanbul 是一个独立的代码覆盖率工具,支持多种 JavaScript 测试框架。使用 Istanbul 时,需要先安装相关工具,例如:

npm install --save-dev nyc

然后在 package.json 中配置测试命令:

  • "scripts": {
        "test": "nyc mocha"
    }

    运行测试时,nyc 会自动收集代码覆盖率数据,并生成报告。

这些工具不仅能够追踪代码覆盖率,还能提供详细的未覆盖代码片段,帮助开发者优化测试用例,确保代码的每个部分都经过充分测试。

8.2 分析代码覆盖率报告

代码覆盖率报告是评估测试质量的关键依据。通过分析覆盖率报告,可以发现测试中的不足之处,并针对性地改进测试用例。

  • 语句覆盖率:表示被测试的语句占总语句的比例。理想情况下,语句覆盖率应接近 100%。如果某些语句未被覆盖,可能是因为测试用例遗漏了某些执行路径。

  • 分支覆盖率:表示被测试的分支占总分支的比例。分支覆盖率重点关注条件语句(如 ifswitch)的执行情况。低分支覆盖率可能意味着某些条件分支未被测试到,需要补充测试用例。

  • 函数覆盖率:表示被测试的函数占总函数的比例。函数覆盖率反映了测试对函数的覆盖情况。未覆盖的函数可能是测试遗漏的潜在风险点。

在实际开发中,可以通过以下步骤分析代码覆盖率报告:

  1. 检查未覆盖的代码片段:重点关注未覆盖的语句、分支和函数,分析这些代码是否属于正常逻辑。如果这些代码是必要的,应补充相应的测试用例。

  2. 优化测试用例:根据覆盖率报告,优化现有的测试用例,确保测试能够覆盖更多的代码逻辑。

  3. 持续监控覆盖率:将代码覆盖率纳入持续集成(CI)流程中,每次提交代码时自动运行测试并生成覆盖率报告。通过持续监控,及时发现覆盖率的变化,确保代码质量。

通过合理使用代码覆盖率工具和深入分析覆盖率报告,可以显著提高测试的有效性和代码的可靠性,为软件的稳定运行提供有力保障。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

caifox菜狐狸

你的鼓励将是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值