JS验证框架 ajv 入门教程

在现代Web开发中,数据验证是确保应用稳定性的关键环节。Ajv(Another JSON Schema Validator)作为高性能的JSON Schema验证工具,已成为全栈开发的首选方案之一。

ajv官网:ajv.js.org/

什么是Ajv?

Ajv是一个符合JSON Schema标准的验证器,具有以下核心优势:

  • 超高性能:每秒可验证数万条数据
  • 标准兼容:支持JSON Schema Draft 6/7/2019-09等标准
  • 扩展性强:支持自定义关键字和格式
  • 零依赖:保持轻量级体积
  • 全栈通用:完美适配Node.js和浏览器环境

基础使用示例

javascript 复制代码
import Ajv from "ajv";

// 创建实例
const ajv = new Ajv();

// 定义schema
const userSchema = {
  type: "object",
  properties: {
    id: { type: "number" },
    name: { type: "string", minLength: 3 },
    email: { type: "string", format: "email" }
  },
  required: ["id", "name"],
  additionalProperties: false
};

// 编译schema
const validateUser = ajv.compile(userSchema);

// 验证数据
const valid = validateUser({
  id: 1,
  name: "Alice",
  email: "alice@example.com"
});

if (!valid) console.log(validateUser.errors);

与常见框架集成

1. Express/Koa中间件

javascript 复制代码
// ajv-middleware.js
export const validate = (schema) => {
  const validateFn = ajv.compile(schema);
  return (req, res, next) => {
    if (!validateFn(req.body)) {
      return res.status(400).json({
        errors: validateFn.errors.map(e => `${e.instancePath} ${e.message}`)
      });
    }
    next();
  };
};

// 在路由中使用
app.post("/users", validate(userSchema), (req, res) => {
  // 安全使用已验证的req.body
});

2. React/Vue前端验证

javascript 复制代码
// 前端验证函数
export const validateFormData = (schema, data) => {
  const ajv = new Ajv();
  const validate = ajv.compile(schema);
  const valid = validate(data);
  return valid ? null : validate.errors;
};

// React组件中使用
function UserForm() {
  const [errors, setErrors] = useState(null);

  const handleSubmit = (data) => {
    const validationErrors = validateFormData(userSchema, data);
    if (validationErrors) {
      setErrors(validationErrors);
      return;
    }
    // 提交数据...
  };
}

复杂场景示例

1. 条件验证

javascript 复制代码
/**
 * 支付方式条件验证:
 * - 当支付方式为信用卡时,必须提供有效的卡号
 * - 当支付方式为PayPal时,必须提供有效的邮箱
 */
const paymentSchema = {
  type: "object",
  properties: {
    paymentMethod: { 
      enum: ["credit", "paypal"] // 限定支付方式选项
    }
  },
  required: ["paymentMethod"], // 支付方式必填
  if: {
    properties: { 
      paymentMethod: { const: "credit" } // 条件:支付方式为信用卡
    }
  },
  then: {
    required: ["cardNumber"], // 需要信用卡号字段
    properties: {
      cardNumber: { 
        type: "string", 
        pattern: "\\d{16}", // 必须为16位数字
        errorMessage: "信用卡号必须为16位数字"
      }
    }
  },
  else: {
    required: ["paypalEmail"], // 需要PayPal邮箱字段
    properties: {
      paypalEmail: { 
        format: "email", // 必须符合邮箱格式
        errorMessage: "请输入有效的PayPal邮箱"
      }
    }
  }
};

2. 自定义关键字

javascript 复制代码
import Ajv from "ajv";
const ajv = new Ajv();

/**
 * 注册自定义关键字:范围验证
 * @param schema 包含min/max范围定义的对象
 * @param data 待验证的数据值
 */
ajv.addKeyword({
  keyword: "range",
  type: "number", // 限定只处理数字类型
  validate: function(schema, data) {
    // 验证数据是否在指定范围内
    return data >= schema.min && data <= schema.max;
  },
  errors: true, // 允许返回错误信息
  metaSchema: { // 定义关键字本身的schema
    type: "object",
    properties: {
      min: { type: "number" },
      max: { type: "number" }
    },
    required: ["min", "max"]
  }
});

// 使用自定义关键字
const ageSchema = {
  type: "object",
  properties: {
    age: { 
      type: "number",
      range: { min: 18, max: 100 }, // 应用自定义验证
      errorMessage: "年龄必须在18-100岁之间"
    }
  }
};

3. 组合与继承(allOf + $ref)

javascript 复制代码
/**
 * 基础用户Schema(可复用)
 */
const baseUserSchema = {
  $id: "https://example.com/schemas/userBase",
  type: "object",
  properties: {
    id: { type: "number" },
    name: { type: "string", minLength: 2 }
  },
  required: ["id", "name"]
};

/**
 * 管理员Schema(继承基础用户并扩展)
 * 使用allOf实现组合继承
 */
const adminSchema = {
  $id: "https://example.com/schemas/admin",
  allOf: [
    { $ref: "userBase" }, // 引用基础schema
    {
      properties: {
        permissions: { 
          type: "array", 
          items: { type: "string", enum: ["read", "write", "delete"] }
        },
        securityLevel: { type: "number", minimum: 1 }
      },
      required: ["permissions"]
    }
  ]
};

// 注册schemas并编译
ajv.addSchema(baseUserSchema);
ajv.addSchema(adminSchema);
const validateAdmin = ajv.getSchema("https://example.com/schemas/admin");

4. 属性多类型(anyOf处理对象A/B)

javascript 复制代码
/**
 * 内容区块Schema:
 * 可以是文本区块或图片区块
 */
const contentBlockSchema = {
  type: "object",
  properties: {
    type: { enum: ["text", "image"] },
    content: { 
      // 根据type字段决定content结构
      anyOf: [
        {
          // 当type=text时的结构
          if: { properties: { type: { const: "text" } } },
          then: {
            type: "object",
            properties: {
              text: { type: "string" },
              format: { enum: ["markdown", "plaintext"] }
            },
            required: ["text"]
          }
        },
        {
          // 当type=image时的结构
          if: { properties: { type: { const: "image" } } },
          then: {
            type: "object",
            properties: {
              url: { type: "string", format: "uri" },
              altText: { type: "string" },
              width: { type: "number" },
              height: { type: "number" }
            },
            required: ["url"]
          }
        }
      ]
    }
  },
  required: ["type", "content"]
};

5. 泛型实现(JSON Schema模式)

javascript 复制代码
/**
 * 创建分页响应泛型Schema
 * @param {object} itemSchema - 数据项的Schema定义
 * @returns 分页响应Schema
 */
function createPaginationSchema(itemSchema) {
  return {
    type: "object",
    properties: {
      total: { type: "number" },
      limit: { type: "number" },
      offset: { type: "number" },
      data: {
        type: "array",
        items: itemSchema // 动态插入项目Schema
      }
    },
    required: ["total", "data"]
  };
}

// 用户分页响应示例
const userPaginationSchema = createPaginationSchema({
  $ref: "https://example.com/schemas/userBase"
});

// 产品分页响应示例
const productSchema = {
  $id: "https://example.com/schemas/product",
  type: "object",
  properties: {
    id: { type: "string" },
    name: { type: "string" },
    price: { type: "number", minimum: 0 }
  }
};
const productPaginationSchema = createPaginationSchema({
  $ref: "https://example.com/schemas/product"
});

6. 深度TS集成(泛型+类型守卫)

typescript 复制代码
import { JSONSchemaType, DefinedError } from "ajv";

/**
 * 创建类型安全的验证器
 * @param schema JSON Schema定义
 * @returns 验证函数和类型守卫
 */
function createValidator<T>(schema: JSONSchemaType<T>) {
  const validate = ajv.compile(schema);
  
  // 类型守卫函数
  const isType = (data: unknown): data is T => validate(data);
  
  // 验证函数
  const assertType = (data: unknown): T => {
    if (!validate(data)) {
      const errors = validate.errors as DefinedError[];
      throw new Error(ajv.errorsText(errors));
    }
    return data as T;
  };
  
  return { validate, isType, assertType };
}

// 用户类型定义
interface User {
  id: number;
  name: string;
  email?: string;
}

// 创建用户验证器
const userValidator = createValidator<User>({
  type: "object",
  properties: {
    id: { type: "number" },
    name: { type: "string", minLength: 2 },
    email: { 
      type: "string", 
      format: "email", 
      nullable: true 
    }
  },
  required: ["id", "name"],
  additionalProperties: false
});

// 使用示例
const data: unknown = JSON.parse(request.body);

// 方法1:类型守卫
if (userValidator.isType(data)) {
  console.log(data.name); // 安全访问
}

// 方法2:验证并转换
try {
  const user = userValidator.assertType(data);
  console.log(user.email); // 安全访问
} catch (err) {
  // 处理验证错误
}

7. 多文件组合验证($ref高级用法)

json 复制代码
// schemas/address.json
{
  "$id": "https://example.com/schemas/address",
  "type": "object",
  "properties": {
    "street": { "type": "string" },
    "city": { "type": "string" },
    "zipCode": { "type": "string" }
  },
  "required": ["street", "city"]
}

// schemas/user.json
{
  "$id": "https://example.com/schemas/user",
  "type": "object",
  "properties": {
    "id": { "type": "number" },
    "name": { "type": "string" },
    "address": { 
      "$ref": "https://example.com/schemas/address" 
    },
    "contacts": {
      "type": "array",
      "items": { 
        "$ref": "https://example.com/schemas/contact" 
      }
    }
  }
}

// schemas/contact.json
{
  "$id": "https://example.com/schemas/contact",
  "oneOf": [
    { "$ref": "#/definitions/emailContact" },
    { "$ref": "#/definitions/phoneContact" }
  ],
  "definitions": {
    "emailContact": {
      "type": "object",
      "properties": {
        "type": { "const": "email" },
        "value": { "type": "string", "format": "email" }
      },
      "required": ["type", "value"]
    },
    "phoneContact": {
      "type": "object",
      "properties": {
        "type": { "const": "phone" },
        "value": { "type": "string", "pattern": "^\\+?[0-9]{7,15}$" }
      },
      "required": ["type", "value"]
    }
  }
}

// 加载所有schemas
ajv.addSchema(require('./schemas/address.json'));
ajv.addSchema(require('./schemas/contact.json'));
ajv.addSchema(require('./schemas/user.json'));

// 验证完整用户对象
const validateUser = ajv.getSchema("https://example.com/schemas/user");

TypeScript深度集成

1. 从Schema生成类型

typescript 复制代码
import { JSONSchemaType } from "ajv";

interface User {
  id: number;
  name: string;
  email?: string;
}

const userSchema: JSONSchemaType<User> = {
  type: "object",
  properties: {
    id: { type: "number" },
    name: { type: "string", minLength: 3 },
    email: { type: "string", format: "email", nullable: true }
  },
  required: ["id", "name"],
  additionalProperties: false
};

// 自动类型检查
const validateUser = ajv.compile(userSchema);

2. 类型安全的验证函数

typescript 复制代码
function validate<T>(schema: JSONSchemaType<T>, data: any): T {
  const validate = ajv.compile(schema);
  if (!validate(data)) {
    throw new Error(ajv.errorsText(validate.errors));
  }
  return data as T; // 安全类型转换
}

// 使用示例
const user = validate<User>(userSchema, request.body);
console.log(user.name); // 类型安全访问

实现前后端通用验证

共享策略

  1. Schema共享目录

    vbnet 复制代码
    project/
    ├── shared/
    │   ├── schemas/
    │   │   ├── user.ts
    │   │   └── product.ts
    ├── client/
    └── server/
  2. 通用验证模块

typescript 复制代码
// shared/validation.ts
import Ajv from "ajv";
import { UserSchema } from "./schemas/user";

export const ajv = new Ajv();

export function validateData<T>(schema: object, data: any): T {
  const validate = ajv.compile(schema);
  if (!validate(data)) {
    throw new ValidationError(validate.errors);
  }
  return data as T;
}

// 自定义错误类型
export class ValidationError extends Error {
  constructor(public errors: any[]) {
    super("Validation failed");
  }
}
  1. 构建配置
  • 使用Webpack/Vite配置别名:@shared/*

  • 配置TS Paths:

    json 复制代码
    {
      "compilerOptions": {
        "paths": {
          "@shared/*": ["../shared/*"]
        }
      }
    }

测试策略

在数据验证层实施全面的测试策略至关重要。以下是针对Ajv验证框架的完整测试方案:

单元测试:验证核心逻辑

使用Jest进行Schema验证的单元测试:

typescript 复制代码
// __tests__/ajvValidation.test.ts
import Ajv from 'ajv';
import { userSchema } from '../shared/schemas/user';

const ajv = new Ajv();
const validateUser = ajv.compile(userSchema);

describe('用户Schema验证', () => {
  test('接受有效用户数据', () => {
    const validUser = {
      id: 1,
      name: 'Alice',
      email: 'alice@example.com'
    };
    expect(validateUser(validUser)).toBe(true);
  });

  test('拒绝缺少姓名的用户', () => {
    const invalidUser = {
      id: 2,
      email: 'bob@example.com'
    };
    expect(validateUser(invalidUser)).toBe(false);
    expect(validateUser.errors).toContainEqual(
      expect.objectContaining({
        keyword: 'required',
        params: { missingProperty: 'name' }
      })
    );
  });

  test('拒绝无效邮箱格式', () => {
    const invalidEmailUser = {
      id: 3,
      name: 'Charlie',
      email: 'invalid-email'
    };
    expect(validateUser(invalidEmailUser)).toBe(false);
    expect(validateUser.errors).toContainEqual(
      expect.objectContaining({
        keyword: 'format',
        instancePath: '/email'
      })
    );
  });

  test('验证自定义关键字 - 年龄范围', () => {
    const ageSchema = {
      type: 'object',
      properties: {
        age: { type: 'number', range: { min: 18, max: 100 } }
      }
    };
    
    ajv.addKeyword({
      keyword: 'range',
      validate: (schema, data) => data >= schema.min && data <= schema.max
    });
    
    const validateAge = ajv.compile(ageSchema);
    
    // 有效案例
    expect(validateAge({ age: 25 })).toBe(true);
    
    // 无效案例
    expect(validateAge({ age: 16 })).toBe(false);
    expect(validateAge({ age: 120 })).toBe(false);
  });
});

集成测试:中间件与框架集成

测试Express中间件的验证行为:

typescript 复制代码
// __tests__/middleware.integration.test.ts
import request from 'supertest';
import express from 'express';
import { validate } from '../server/middleware/ajvMiddleware';
import { userSchema } from '../shared/schemas/user';

const app = express();
app.use(express.json());
app.post('/users', validate(userSchema), (req, res) => {
  res.status(201).json({ success: true });
});

describe('AJV中间件集成测试', () => {
  test('有效请求应返回201', async () => {
    const response = await request(app)
      .post('/users')
      .send({
        id: 1,
        name: 'Test User'
      });
    
    expect(response.status).toBe(201);
    expect(response.body).toEqual({ success: true });
  });

  test('无效请求应返回400和错误详情', async () => {
    const response = await request(app)
      .post('/users')
      .send({ id: 'invalid-id' }); // 错误的ID类型
    
    expect(response.status).toBe(400);
    expect(response.body).toEqual({
      error: 'Validation failed',
      details: expect.arrayContaining([
        expect.stringContaining('必须是 number 类型')
      ])
    });
  });

  test('应正确处理嵌套对象验证', async () => {
    const addressSchema = {
      type: 'object',
      properties: {
        street: { type: 'string' },
        city: { type: 'string' }
      },
      required: ['street', 'city']
    };
    
    app.post('/address', validate(addressSchema), (req, res) => {
      res.status(200).json(req.body);
    });
    
    const response = await request(app)
      .post('/address')
      .send({ street: '123 Main St' }); // 缺少city
    
    expect(response.status).toBe(400);
    expect(response.body.details).toContainEqual(
      expect.stringContaining('必须拥有属性 city')
    );
  });
});

端到端测试:全栈验证流程

使用Cypress测试前端到后端的完整验证流程:

javascript 复制代码
// cypress/e2e/userRegistration.cy.js
describe('用户注册流程', () => {
  beforeEach(() => {
    cy.intercept('POST', '/api/users', (req) => {
      // 模拟后端验证
      if (!req.body.name || req.body.name.length < 2) {
        req.reply({
          statusCode: 400,
          body: { error: '姓名至少需要2个字符' }
        });
      } else {
        req.reply({
          statusCode: 201,
          body: { id: 123, ...req.body }
        });
      }
    }).as('registerRequest');
  });

  it('应显示前端验证错误', () => {
    cy.visit('/register');
    
    // 尝试提交空表单
    cy.get('form').submit();
    
    // 验证前端错误显示
    cy.get('[data-testid="name-error"]')
      .should('be.visible')
      .and('contain', '姓名不能为空');
  });

  it('应处理后端验证错误', () => {
    cy.visit('/register');
    
    // 输入无效数据
    cy.get('input[name="name"]').type('A'); // 过短的姓名
    cy.get('input[name="email"]').type('test@example.com');
    cy.get('form').submit();
    
    // 等待并验证API响应
    cy.wait('@registerRequest');
    cy.get('[data-testid="form-error"]')
      .should('be.visible')
      .and('contain', '姓名至少需要2个字符');
  });

  it('应成功完成注册', () => {
    cy.visit('/register');
    
    // 输入有效数据
    cy.get('input[name="name"]').type('Valid User');
    cy.get('input[name="email"]').type('valid@example.com');
    cy.get('form').submit();
    
    // 验证成功状态
    cy.wait('@registerRequest');
    cy.url().should('include', '/welcome');
    cy.get('[data-testid="welcome-message"]')
      .should('contain', 'Valid User');
  });
});

Schema一致性测试

确保前后端Schema定义一致:

typescript 复制代码
// __tests__/schemaConsistency.test.ts
import fs from 'fs';
import path from 'path';
import { JSONSchemaType } from 'ajv';

// 从前后端导入相同的Schema
const frontendSchema = require('../../client/src/schemas/user.ts');
const backendSchema = require('../../server/src/schemas/user.ts');

describe('Schema一致性测试', () => {
  test('用户Schema在前后端应完全一致', () => {
    // 标准化Schema对象
    const normalizeSchema = (schema: JSONSchemaType<any>) => {
      const clone = JSON.parse(JSON.stringify(schema));
      delete clone.$schema; // 移除可能存在的元数据
      return JSON.stringify(clone, null, 2);
    };
    
    const frontendStr = normalizeSchema(frontendSchema);
    const backendStr = normalizeSchema(backendSchema);
    
    expect(frontendStr).toEqual(backendStr);
  });

  test('所有共享Schema应通过一致性检查', () => {
    const sharedSchemaDir = path.join(__dirname, '../../shared/schemas');
    
    fs.readdirSync(sharedSchemaDir).forEach(file => {
      if (file.endsWith('.ts')) {
        const schemaPath = path.join(sharedSchemaDir, file);
        const schema = require(schemaPath);
        
        // 验证Schema基本结构
        expect(schema).toHaveProperty('type');
        expect(schema).toHaveProperty('properties');
        
        // 验证ID格式
        if (schema.$id) {
          expect(schema.$id).toMatch(/^https?:\/\//);
        }
      }
    });
  });
});

性能与压力测试

typescript 复制代码
// __tests__/performance.test.ts
import Ajv from 'ajv';
import { userSchema } from '../shared/schemas/user';

describe('Ajv性能测试', () => {
  const ajv = new Ajv();
  const validate = ajv.compile(userSchema);
  
  // 生成测试数据
  const generateTestUsers = (count: number) => {
    return Array.from({ length: count }, (_, i) => ({
      id: i + 1,
      name: `User ${i}`,
      email: `user${i}@example.com`
    }));
  };
  
  test('应高效验证1000个用户', () => {
    const users = generateTestUsers(1000);
    
    const start = performance.now();
    users.forEach(user => {
      const valid = validate(user);
      if (!valid) throw new Error(`验证失败: ${JSON.stringify(validate.errors)}`);
    });
    const duration = performance.now() - start;
    
    console.log(`验证1000个用户耗时: ${duration.toFixed(2)}ms`);
    expect(duration).toBeLessThan(100); // 100毫秒内完成
  });
  
  test('复杂Schema应保持高性能', () => {
    // 创建复杂嵌套Schema
    const complexSchema = {
      type: 'array',
      items: {
        type: 'object',
        properties: {
          id: { type: 'number' },
          profile: {
            type: 'object',
            properties: {
              name: { type: 'string' },
              contacts: {
                type: 'array',
                items: {
                  type: 'object',
                  properties: {
                    type: { enum: ['email', 'phone'] },
                    value: { type: 'string' }
                  }
                }
              }
            }
          }
        }
      }
    };
    
    const validateComplex = ajv.compile(complexSchema);
    const complexData = generateTestUsers(500).map(user => ({
      ...user,
      profile: {
        name: user.name,
        contacts: [
          { type: 'email', value: user.email },
          { type: 'phone', value: `+1${user.id.toString().padStart(10, '0')}` }
        ]
      }
    }));
    
    const start = performance.now();
    const valid = validateComplex(complexData);
    const duration = performance.now() - start;
    
    expect(valid).toBe(true);
    console.log(`验证500个复杂对象耗时: ${duration.toFixed(2)}ms`);
    expect(duration).toBeLessThan(50); // 50毫秒内完成
  });
});

错误处理与本地化测试

typescript 复制代码
// __tests__/errorHandling.test.ts
import Ajv from 'ajv';
import localize from 'ajv-i18n';

describe('Ajv错误处理', () => {
  const ajv = new Ajv({ allErrors: true });
  
  test('应正确收集所有错误', () => {
    const schema = {
      type: 'object',
      properties: {
        id: { type: 'number' },
        name: { type: 'string', minLength: 2 },
        email: { type: 'string', format: 'email' }
      },
      required: ['id', 'name', 'email']
    };
    
    const validate = ajv.compile(schema);
    const invalidData = {
      name: 'A', // 太短
      email: 'invalid' // 无效邮箱
    };
    
    validate(invalidData);
    
    expect(validate.errors).toHaveLength(3);
    expect(validate.errors).toEqual(
      expect.arrayContaining([
        expect.objectContaining({ keyword: 'required' }),
        expect.objectContaining({ keyword: 'minLength' }),
        expect.objectContaining({ keyword: 'format' })
      ])
    );
  });
  
  test('应支持错误消息本地化', () => {
    const schema = {
      properties: {
        age: { type: 'number', minimum: 18 }
      }
    };
    
    const validate = ajv.compile(schema);
    const invalidData = { age: 16 };
    validate(invalidData);
    
    // 应用中文本地化
    localize.zh(validate.errors);
    
    expect(validate.errors[0].message).toBe('应当大于等于 18');
    
    // 应用德语本地化
    localize.de(validate.errors);
    
    expect(validate.errors[0].message).toBe(
      'muss größer oder gleich 18 sein'
    );
  });
  
  test('自定义错误消息应覆盖默认消息', () => {
    ajv.addKeyword({
      keyword: 'errorMessage',
      macro: () => ({}),
      metaSchema: { type: 'string' }
    });
    
    const schema = {
      properties: {
        email: {
          type: 'string',
          format: 'email',
          errorMessage: '请输入有效的邮箱地址'
        }
      }
    };
    
    const validate = ajv.compile(schema);
    validate({ email: 'invalid' });
    
    expect(validate.errors[0].message).toBe(
      '请输入有效的邮箱地址'
    );
  });
});

性能优化技巧

  1. 预编译Schema

    javascript 复制代码
    // 服务端启动时预编译
    const schemas = loadSchemas();
    const compiledSchemas = new Map();
    schemas.forEach(schema => {
      compiledSchemas.set(schema.$id, ajv.compile(schema));
    });
  2. 缓存验证函数

    javascript 复制代码
    const validationCache = new Map();
    
    function getValidator(schema) {
      const key = JSON.stringify(schema);
      if (!validationCache.has(key)) {
        validationCache.set(key, ajv.compile(schema));
      }
      return validationCache.get(key);
    }
  3. 关闭冗长错误(生产环境):

    javascript 复制代码
    const ajv = new Ajv({ allErrors: false, verbose: false });

总结

Ajv作为JSON Schema验证的标杆工具,通过其:

  1. 强大的标准支持
  2. 优异的性能表现
  3. 灵活的可扩展性
  4. 全栈通用能力

结合TypeScript使用时,Ajv能提供端到端的类型安全验证体验。通过共享验证逻辑,团队可以显著减少重复代码,提高数据一致性,构建更健壮的应用系统。

最佳实践建议

  • 在复杂业务场景中使用$ref管理大型Schema
  • 利用Ajv的异步验证处理数据库检查
  • 结合OpenAPI实现全栈类型安全
  • 定期更新Ajv版本获取性能改进

通过合理应用Ajv,开发者可以构建出具备工业级强度的数据验证层,为应用稳定性提供坚实保障。

相关推荐
寅时码6 分钟前
我开源了一款 Canvas “瑞士军刀”,十几种“特效与工具”开箱即用
前端·开源·canvas
CF14年老兵8 分钟前
🚀 React 面试 20 题精选:基础 + 实战 + 代码解析
前端·react.js·redux
CF14年老兵9 分钟前
2025 年每个开发人员都应该知道的 6 个 VS Code AI 工具
前端·后端·trae
十五_在努力12 分钟前
参透 JavaScript —— 彻底理解 new 操作符及手写实现
前端·javascript
拾光拾趣录28 分钟前
🔥99%人答不全的安全链!第5问必翻车?💥
前端·面试
IH_LZH32 分钟前
kotlin小记(1)
android·java·前端·kotlin
lwlcode40 分钟前
前端大数据渲染性能优化 - 分时函数的封装
前端·javascript
Java技术小馆41 分钟前
MCP是怎么和大模型交互
前端·面试·架构
玲小珑1 小时前
Next.js 教程系列(二十二)代码分割与打包优化
前端·next.js