express-validator 7.0.0 使用介绍

官方文档

介绍

概述

Express-validator 是一个基于 Express.js 的验证器中间件,它封装了 validator.js 提供的广泛验证器和清理器集合。

Express-validator 允许您以多种方式组合它们,以便验证和清理您的 Express 请求。它还提供了工具来确定请求是否有效,以及根据您的验证器匹配了哪些数据等等。

支持

此版本的express-validator要求您的应用程序在Node.js 14+上运行。

它也被验证为与express.js 4.x兼容。

请注意,尽管名称如此,express-validator可能适用于非express.js的库。主要要求是您使用的HTTP服务器库类似于express.js对其HTTP请求对象进行建模,并包含以下属性:

  • req.body:HTTP 请求的主体。可以是任何值,但是对象、数组和其他 JavaScript 原语效果更好。
  • req.cookies:Cookie header被解析为从cookie名称到其值的对象。
  • req.headers:随HTTP请求一起发送的 headers。
  • req.params:一个从名称到值的对象。
    在express.js中,它是从请求路径解析出来的,并与路由定义路径相匹配,但它实际上可以是来自HTTP请求的任何有意义的东西。
  • req.query:HTTP 请求路径中? 后的部分,解析为从查询参数名称到值的对象。

一个与express-validator一起开箱即用的示例库是 Restify

安装

npm install express-validator

指南

开始使用

学习某件事的最佳方法之一就是通过例子!所以让我们卷起袖子,开始编码吧。

设置

首先需要一个运行中的express.js应用程序。让我们实现一个向某人打招呼的应用程序;为此,使用您喜欢的语言创建一个新文件,并添加以下代码:

index.js 复制代码
const express = require('express');
const app = express();

app.use(express.json());
app.get('/hello', (req, res) => {
  res.send(`Hello, ${req.query.person}!`);
});

app.listen(3000);

现在,在终端上执行 node index.js 来运行此文件。

HTTP 服务器应该正在运行,您可以打开 http://localhost:3000/hello?person=John 向 John 打招呼!

您可以使用 node-dev 来运行您的应用程序( nodemon index.js )。这些工具会在文件发生更改时自动重新启动应用程序,因此您不必自己动手!

添加验证器

虽然这个应用程序现在正在工作中,但它也存在一些问题。最明显的是,当人的名字没有设置时,你不想说嘿。

例如,访问 http://localhost:3000/hello 将打印 "Hello, undefined!"。

这就是express-validator派上用场的地方:让我们添加一个验证器,检查person查询字符串不能为空,并直观地命名为notEmpty验证器

index.js 复制代码
const express = require('express');
const { query } = require('express-validator');
const app = express();

app.use(express.json());
app.get('/hello', query('person').notEmpty(), (req, res) => {
  res.send(`Hello, ${req.query.person}!`);
});

app.listen(3000);

现在,重新启动你的应用程序,再次前往 http://localhost:3000/hello 。嗯,它仍然打印"Hello,undefined!"......为什么?

处理验证错误

express-validator验证器不会自动向用户报告验证错误。

原因很简单:当你添加更多的验证器或更多的字段时,你想如何收集错误?你想要一个所有错误的列表,每个字段只有一个,还是只有一个整体...?

因此,下一个明显的步骤是再次更改上述代码,这次使用validationResult函数验证验证结果

index.js 复制代码
const express = require('express');
const { query, validationResult } = require('express-validator');
const app = express();

app.use(express.json());
app.get('/hello', query('person').notEmpty(), (req, res) => {
  const result = validationResult(req);
  if (result.isEmpty()) {
    return res.send(`Hello, ${req.query.person}!`);
  }

  res.send({ errors: result.array() });
});

app.listen(3000);

现在,如果你再次访问 http://localhost:3000/hello 你会看到以下 JSON 内容

json 复制代码
{
  "errors": [
    {
      "location": "query",
      "msg": "Invalid value",
      "path": "person",
      "type": "field"
    }
  ]
}

现在,这告诉我们的是:

  • 此请求中只有一个错误;
  • 错误出现在字段中("type":"field");
  • 这个字段称为 person;
  • 它位于查询字符串中("location": "query");
  • 给出的错误消息是"无效值"。

这是一个比较好的场景,但它仍然可以改进。让我们继续。

清洗输入

虽然用户不能再发送空的 person 名称,但他们仍然可以在你的页面中注入 HTML!这被称为跨站脚本(XSS)漏洞。

让我们看看这是如何工作的。访问 http://localhost:3000/hello?person=John,你应该看到 "Hello, John!"。

虽然这个例子没问题,但攻击者可能会将 person 查询字符串更改为一个 <script> 标签,该标签加载其自己的 JavaScript,这可能是有害的。

在这种情况下,使用 express-validator 缓解问题的一种方法是使用清洗器,特别是 escape,它将特殊 HTML 字符与其他字符进行转换,可以以文本形式表示。

index.js 复制代码
const express = require('express');
const { query, validationResult } = require('express-validator');
const app = express();

app.use(express.json());
app.get('/hello', query('person').notEmpty().escape(), (req, res) => {
  const result = validationResult(req);
  if (result.isEmpty()) {
    return res.send(`Hello, ${req.query.person}!`);
  }

  res.send({ errors: result.array() });
});

app.listen(3000);

现在,如果你重启服务器并刷新页面,你会看到Hello,<b>John</b>。我们的示例页面不再容易受到XSS攻击!

访问经过验证的数据

这个应用程序非常简单,但随着它的增长,键入req.body. name1、req.body. name2等可能会变得非常重复。

为了帮助完成这项工作,您可以使用matchedData(),它会自动收集所有经过express-validator验证和/或净化的数据

ini 复制代码
const express = require('express');
const { query, matchedData, validationResult } = require('express-validator');
const app = express();

app.use(express.json());
app.get('/hello', query('person').notEmpty().escape(), (req, res) => {
  const result = validationResult(req);
  if (result.isEmpty()) {
    const data = matchedData(req);
    return res.send(`Hello, ${data.person}!`);
  }

  res.send({ errors: result.array() });
});

app.listen(3000);

验证链

验证链是express-validator中的主要概念之一,因此了解它是有用的,这样你就可以有效地使用它。

但不用担心:如果你已经阅读了入门指南,那么你已经在不知不觉中使用了验证链!

什么是验证链?

验证链由body()、param()、query()等函数创建。

它们之所以有这个名字,是因为它们用验证(或净化)来包装字段的值,并且它的每个方法都返回自身。

这种模式通常被称为方法链,因此被称为验证链。

验证链不仅具有许多用于定义验证和清理的有用方法,而且它们也是中间件函数,这意味着它们可以被传递给任何express.js路由处理程序。

这是验证链通常如何使用以及如何读取它的一个示例

less 复制代码
app.post(
  '/newsletter',
  // For the `email` field in `req.body`...
  body('email')
    // 标记字段为可选
    .optional()
    // 当它存在时,修剪它的值,然后将其验证为电子邮件地址
    .trim()
    .isEmail(),
  maybeSubscribeToNewsletter,
);

特征

验证链有三种方法:验证器、净化器和修改器。

验证器确定请求字段的值是否有效。这意味着检查字段的格式是否符合您的预期。例如,如果您正在构建一个注册表单,您的要求可能是用户名必须是电子邮件地址,密码必须至少有8个字符长。

如果该值无效,则使用某些错误消息记录该字段的错误。然后可以在路由处理程序的稍后阶段检索此验证错误并将其返回给用户。

sanitizers 转换字段值。它们可用于从值中删除噪声,将值转换为正确的 JavaScript 类型,甚至可能提供一些基本的防御威胁的防线。

Sanitizer 会将更新的字段值持久化到请求中,以便其他 express-validator 函数、您自己的路由处理程序代码甚至其他中间件可以使用它。

修饰符定义了验证链在运行时的行为。这可能包括添加关于验证链何时运行的条件,甚至包括验证器应具有的错误消息。

您可以访问ValidationChain API查看方法列表。

标准验证器/消毒器

验证链暴露的大部分功能实际上来自validator.js,这是一个专门从事字符串验证的JavaScript库。

这包括validator.js的所有验证器和消毒器,从常用的isEmail、isLength和trim到更专业的isISBN、isMultibyte和stripLow!

在express-validator中,这些被称为标准验证器和标准消毒器。

由于validator.js仅适用于字符串,express-validator将始终首先将具有标准验证器/消毒器的字段转换为字符串。 非字符串值根据以下表格进行转换:

  • 日期对象将使用toISOString()方法的返回值;
  • null、undefined和NaN被转换为空字符串;
  • 实现了自定义toString()方法的对象将使用该方法的返回值;
  • 其他对象将使用默认的toString()方法;
  • 所有其他值都按原样转换为字符串(例如布尔值或数字)。

数组的每个项都根据这些规则单独进行验证/消毒。 例如,当req.body.ids为[5, '33', 'abc', 'def']时,一个验证链body('ids').isNumber()会找到两个错误。

链接顺序

在验证链上调用方法的顺序通常很重要。 它们几乎总是按照指定的顺序运行,因此,通过阅读验证链的定义,从第一个链接方法到最后一个链接方法,你就可以知道验证链会做什么。

以以下代码段为例

scss 复制代码
// 校验 search_query 是否为空,然后删除头尾空白字符
query('search_query').notEmpty().trim();

在这种情况下,如果用户传递的search_query值只由空格组成,它不会是空值,因此验证通过。但是,由于存在.trim()消毒器,空格将被删除,字段将变为空值,因此您实际上得到了一个假阳性。

现在,将其与下面的代码段进行比较:

scss 复制代码
// 删除头尾空白字符,然后验证它是否为空
query('search_query').trim().notEmpty();

这个链将更明智地删除空白,然后验证值是否为空。

此规则的一个例外是 .optional():它可以放置在链中的任何位置,并且它将以相同的方式将链标记为可选。

重用验证链

验证链是可变的。

这意味着,对一个对象调用方法会导致原始链对象被更新,就像对它的任何引用一样。

如果你想重用相同的链,最好从函数中返回它们

less 复制代码
const createEmailChain = () => body('email').isEmail();
app.post('/login', createEmailChain(), handleLoginRoute);
app.post('/signup', createEmailChain().custom(checkEmailNotInUse), handleSignupRoute);

注意下面这种危险的使用情况

下面显示了电子邮件未使用验证不仅在注册页面上运行,而且在登录页面上也运行:

ini 复制代码
const baseEmailChain = body('email').isEmail();
app.post('/login', baseEmailChain, handleLoginRoute);
app.post('/signup', baseEmailChain.custom(checkEmailNotInUse), handleSignupRoute);

字段选择

在 Express Validator 中,字段是经过验证或清理的任何值。

它可以是简单的值,如字符串或数字,也可以是更复杂的数组或对象值。

几乎每个函数或值都以某种方式引用express-validator中的字段。因此,在选择字段进行验证以及访问验证错误或已验证数据时,理解字段路径语法非常重要。

语法

字段的路径始终是一个字符串,类似于您使用纯JavaScript引用它的方式。

  • 每个单词样式的字符序列都是一个段。段就像 JavaScript 对象的属性。
  • 通过使用 . 分隔两个部分,可以选择对象下嵌套的字段。
  • 数组索引可以通过将它们括在方括号中来选择
  • 带有特殊字符(如.)的段可以通过括在方括号和双引号中来选择
    例如,假设req.body如下所示
json 复制代码
{
  "name": "John McExpress",
  "addresses": {
    "work": {
      "country": "express-validator land"
    }
  },
  "siblings": [{ "name": "Maria von Validator" }],
  "websites": {
    "www.example.com": { "dns": "1.2.3.4" }
  }
}

| Path | Selected value |
|----------------------------------------------------------------------------------------------------------------------|---------------------------------------|---|
| name | "John McExpress" | |
| address.work.country | "express-validator land" |
| siblings | [{ "name": "Maria von Validator" }] |
| siblings[0] | { "name": "Maria von Validator" } |
| siblings[0].name | "Maria von Validator" |
| siblings.name | undefined |
| websites["www.example.com"] | { "dns": "1.2.3.4" } |
| websites.www.example.com | undefined |

整个body选择

有时请求的主体不是对象或数组,但你仍然想选择它进行验证/净化。

这可以通过省略字段路径或使用空字符串来实现。两者产生相同的结果

less 复制代码
app.post(
  '/recover-password',
  // These are equivalent.
  body().isEmail(),
  body('').isEmail(),
  (req, res) => {
    // Handle request
  },
);

也可以选择整个req.cookies、req.params等,尽管它可能不如req.body有用或常见。

高级功能

有时,您希望对数组中的所有项目或对象的所有键应用相同的规则。这就是使用 *(也称为通配符)的原因。

通配符可用于代替任何段,这将正确选择数组的所有索引或它所在对象的键。

每个匹配的字段都作为不同的实例返回;也就是说,它独立于其他字段进行验证或净化。

如果放置通配符的数组或对象为空,则不会验证任何内容。

让我们假设更新用户个人资料的端点接受他们的地址和兄弟姐妹

json 复制代码
{
  "addresses": {
    "home": { "number": 35 },
    "work": { "number": 501 }
  },
  "siblings": [{ "name": "Maria von Validator" }, { "name": "Checky McCheckFace" }]
}

为了验证地址号都是整数,并且设置了兄弟姐妹的名称,您可以设置以下验证链

less 复制代码
app.post(
  '/update-user',
  body('addresses.*.number').isInt(),
  body('siblings.*.name').notEmpty(),
  (req, res) => {
    // Handle request
  },
);

通配符星号

Globstars将通配符扩展到无限深度。

当您有未知级别的嵌套字段,并且想以相同的方式验证/清理所有字段时,可以使用它们。

例如,假设您的端点处理公司组织图的更新。

该结构是递归的,因此大致看起来像这样

4 复制代码
{
  "name": "Team name",
  "teams": [{ "name": "Subteam name", "teams": [] }]
}

在这种情况下,一个团队在另一个团队内部,而另一个团队又在另一个团队内部,以此类推。

您可以使用 globstar (**) 来定位任何字段,无论它在请求中有多深。

以下示例检查是否设置了所有名为 name 的字段,包括位于 req.body 根部的字段

less 复制代码
app.put('/update-chart', body('**.name').notEmpty(), (req, res) => {
  // Handle request
});

自定义验证器

如果你正在构建的应用程序不是非常简单,那么迟早你会需要内置于express-validator之外的验证器、清理器和错误消息。

这就是为什么它有多种定制方式,其中一些在本页介绍。

自定义验证器和消毒器

验证器无法满足您的一个典型需求,您可能会遇到这种情况,即在用户注册时验证电子邮件地址是否在使用中。

通过实现自定义验证器,可以在express-validator中实现这一点。

自定义验证器是简单的函数,它们接收字段值和有关它的某些信息,并且必须返回一个值,该值将确定字段是否有效。

自定义 sanitizer 类似,除了它们会转换字段的值。

实现自定义验证器

自定义验证器必须返回一个真值来表示字段有效,或返回一个假值来表示字段无效。

自定义验证器可以是异步的,在这种情况下,它可以返回一个承诺。返回的 promise 会等待,并且必须解决才能使字段有效。如果它被拒绝,该字段将被视为无效。

如果自定义验证器抛出,也被视为无效。

例如,为了检查电子邮件是否没有被使用:

javascript 复制代码
app.post(
  '/create-user',
  body('email').custom(async value => {
    const user = await UserCollection.findUserByEmail(value);
    if (user) {
      throw new Error('E-mail already in use');
    }
  }),
  (req, res) => {
    // Handle the request
  },
);

或者,您也可以验证密码是否与重复密码匹配

javascript 复制代码
app.post(
  '/create-user',
  body('password').isLength({ min: 5 }),
  body('passwordConfirmation').custom((value, { req }) => {
    return value === req.body.password;
  }),
  (req, res) => {
    // Handle request
  },
);
实现自定义消毒器

自定义消毒剂没有太多规则。无论它们返回什么值,都是字段将获取的值。

自定义 sanitizer 也可以是异步的,因此如果它们返回一个 promise,则将等待该 promise,并在字段上设置已解析的值。

例如,假设你想将ID从字符串转换为MongoDB ObjectId格式

javascript 复制代码
import { param } from 'express-validator';
import { ObjectId } from 'mongodb';

app.post(
  '/user/:id',
  param('id').customSanitizer(value => ObjectId(value)),
  (req, res) => {
    // req.params.id is an ObjectId now
  },
);

如果你没有从自定义消毒剂中返回,你的字段将变得未定义!

错误消息

每当字段值无效时,都会记录一条错误消息。

默认错误消息是"无效值",这根本不能描述错误是什么,因此您可能需要对其进行自定义。有几种方法可以做到这一点

验证器级消息

验证器级消息仅在字段未通过特定验证器时适用。这可以通过使用 .withMessage() 方法来完成:

css 复制代码
body('email').isEmail().withMessage('Not a valid e-mail address');
自定义验证器级消息

如果自定义验证器抛出异常,则抛出的值将用作其错误消息。

vbnet 复制代码
body('email')
  .isEmail()
  .custom(async value => {
    const existingUser = await Users.findByEmail(value);
    if (existingUser) {
      // Will use the below as the error message
      throw new Error('A user already exists with this e-mail address');
    }
  });

使用 .withMessage() 指定消息将优先于来自自定义验证器的抛出值。

字段级消息

当您创建验证链时,会设置字段级消息。当验证器不覆盖其错误消息时,它会被用作回退消息。

arduino 复制代码
body('json_string', 'Invalid json_string')
  // No message specified for isJSON, so use the default "Invalid json_string"
  .isJSON()
  .isLength({ max: 100 })
  // Overrides the default message when `isLength` fails
  .withMessage('Max length is 100 bytes');

ExpressValidator 类

·重用某些自定义设置的一个有用方法是使用 ExpressValidator 类。

它包含可以直接从express-validator导入的所有功能:body、matchedData、oneOf、validationResult等,但具有在实例化时指定的自定义功能。

例如,自定义验证器、净化器或错误格式化器。

javascript 复制代码
import { ExpressValidator } from 'express-validator';

const { body, validationResult } = new ExpressValidator(
  {
    isPostID: async value => {
      // Verify if the value matches the post ID format
    },
  },
  {
    muteOffensiveWords: value => {
      // Replace offensive words with ***
    },
  },
);

app.post(
  '/forum/:post/comment',
  param('post').isPostID(),
  body('comment').muteOffensiveWords(),
  (req, res) => {
    const result = validationResult(req);
    // Handle new post validation result
  },
);

手动运行验证

express-validator 赞成使用 express 中间件带来的声明式方法。这意味着,当简单传入 express 路由处理程序时,大多数 API 的外观和工作效果会更好。

但是,你可以在自己的中间件/路由处理程序中控制这些验证的运行。

这在表达式验证器函数中是可能的,该函数返回一个实现了ContextRunner的对象,ContextRunner是由所有ValidationChain、checkExact()、checkSchema()和一个Of实现的接口。

查看下面的示例,了解此方法如何为您提供帮助

示例:创建自己的验证运行器

javascript 复制代码
const express = require('express');
const { validationResult } = require('express-validator');
// can be reused by many routes

// sequential processing, stops running validations chain if the previous one fails.
const validate = validations => {
  return async (req, res, next) => {
    for (let validation of validations) {
      const result = await validation.run(req);
      if (result.errors.length) break;
    }

    const errors = validationResult(req);
    if (errors.isEmpty()) {
      return next();
    }

    res.status(400).json({ errors: errors.array() });
  };
};

app.post('/signup', validate([
  body('email').isEmail(),
  body('password').isLength({ min: 6 })
]), async (req, res, next) => {
  // request is guaranteed to not have any validation errors.
  const user = await User.create({ ... });
});

示例:使用条件进行验证

less 复制代码
import { body, matchedData } from 'express-validator';
app.post(
  '/update-settings',
  body('email').isEmail(),
  body('password').optional().isLength({ min: 6 }),
  async (req, res, next) => {
    // if a password has been provided, then a confirmation must also be provided.
    const { password } = matchedData(req);
    if (password) {
      await body('passwordConfirmation')
        .equals(password)
        .withMessage('passwords do not match')
        .run(req);
    }

    // Check the validation errors, and update the user's settings.
  },
);

这只是您如何使用手动运行验证的一个示例。您应该更喜欢使用 .if() 来创建条件验证链。

模式验证

什么是模式?

模式是一种基于对象的定义请求验证或清理的方法。它们提供的功能与常规验证链完全相同------事实上,在幕后,express-validator处理所有验证链!

如果你不喜欢使用JavaScript函数指定验证的想法,而是更喜欢一种声明性的方法,那么模式验证可能是你正确的表达式验证工具。

指定模式

模式是您传递给checkSchema()函数的普通JavaScript对象,您可以在其中指定要验证哪些字段作为键,以及字段的模式作为值。

反过来,字段模式包含验证器、净化器和任何修改内部验证链行为的选项。

一个基本示例如下所示

php 复制代码
checkSchema({
  username: {
    errorMessage: 'Invalid username',
    isEmail: true,
  },
  password: {
    isLength: {
      options: { min: 8 },
      errorMessage: 'Password should be at least 8 chars',
    },
  },
});

查看完整文档以了解所有选项。

使用通配符*和globstar**

使用模式时,还可以使用高级字段选择功能。

需要注意的是,在 JavaScript 中,不能直接使用 * 字符作为对象键,因此必须将其括在引号中才能使用

php 复制代码
checkSchema({
  'addresses.*.street': {
    notEmpty: true,
  },
  'addresses.*.number': {
    isInt: true,
  },
});

使用案例

地址栏参数校验

scss 复制代码
const express = require('express');
const { query, matchedData, validationResult } = require('express-validator');
const app = express();

app.use(express.json());
// escape 用于将特殊符号转为字符实体,防止 xss 攻击
app.get('/hello', query('person').notEmpty().escape(), (req, res) => {
  const result = validationResult(req); // 获取验证结果
  if (result.isEmpty()) {
    const data = matchedData(req); // data.person 就能取到 req.query.person
    return res.send(`Hello, ${data.person}!`);
  }

  res.send({ errors: result.array() });
});

app.listen(3000);

请求体 body 参数校验

scss 复制代码
const express = require('express');
const { body, validationResult } = require('express-validator');
const app = express();

app.post(
    '/update-user',
    body('addresses.*.number').isInt(),
    body('siblings.*.name').notEmpty(),
    (req, res) => {
        const result = validationResult(req);
        if (result.isEmpty()) {
            return res.send(`Hello, update succsss`);
        }

        res.send({ errors: result.array() });
    },
);

app.listen(3000);
// 因为校验会把数字转字符串,实际传进来的字符串形式的数字也成功

自定义校验

javascript 复制代码
app.post(
    '/create-user',
    body('pass').custom(async value => {
        console.log(value);

        if (value === '123456') {
            throw new Error('pass is simple');
        }
    }),
    (req, res) => {
        // Handle the request
        const result = validationResult(req);
        if (result.isEmpty()) {
            return res.send(`Hello, create succsss`);
        }

        res.send({ errors: result.array() });
    },
);

自定义杀毒器

javascript 复制代码
import { param } from 'express-validator';
import { ObjectId } from 'mongodb';

app.post(
  '/user/:id',
  param('id').customSanitizer(value => ObjectId(value)),
  (req, res) => {
    // req.params.id is an ObjectId now
  },
);

返回值就是经过处理的值

给验证添加提示信息

less 复制代码
app.post(
    '/create-user',
    body('email').isEmail().withMessage('Not a valid e-mail address'),
    (req, res) => {
        // Handle the request
        const result = validationResult(req);
        if (result.isEmpty()) {
            return res.send(`Hello, create succsss`);
        }

        res.send({ errors: result.array() });
    },
);

配置模式校验

php 复制代码
app.post(
    '/create-user',
    checkSchema({
        username: {
            errorMessage: '用户名为email',
            isEmail: true,
        },
        password: {
            isLength: {
                options: { min: 8 },
                errorMessage: '密码需要至少8位',
            },
        },
    }),
    (req, res) => {
        // Handle the request
        const result = validationResult(req);
        if (result.isEmpty()) {
            return res.send(`Hello, create succsss`);
        }

        res.send({ errors: result.array() });
    },
);
相关推荐
Leyla11 分钟前
【代码重构】好的重构与坏的重构
前端
影子落人间15 分钟前
已解决npm ERR! request to https://registry.npm.taobao.org/@vant%2farea-data failed
前端·npm·node.js
世俗ˊ39 分钟前
CSS入门笔记
前端·css·笔记
子非鱼92139 分钟前
【前端】ES6:Set与Map
前端·javascript·es6
6230_44 分钟前
git使用“保姆级”教程1——简介及配置项设置
前端·git·学习·html·web3·学习方法·改行学it
想退休的搬砖人1 小时前
vue选项式写法项目案例(购物车)
前端·javascript·vue.js
加勒比海涛1 小时前
HTML 揭秘:HTML 编码快速入门
前端·html
啥子花道1 小时前
Vue3.4 中 v-model 双向数据绑定新玩法详解
前端·javascript·vue.js
麒麟而非淇淋1 小时前
AJAX 入门 day3
前端·javascript·ajax
茶茶只知道学习1 小时前
通过鼠标移动来调整两个盒子的宽度(响应式)
前端·javascript·css