@skyroc/utils

Regex

项目常用校验正则常量:用户名、手机号、密码(强 / 弱两档)、邮箱、验证码、URL

概述

import {
  REG_USER_NAME,
  REG_PHONE,
  REG_PWD,
  REG_PWD_STRONG,
  REG_EMAIL,
  REG_CODE_SIX,
  REG_CODE_FOUR,
  REG_URL
} from '@skyroc/utils';

reg 模块导出 8 个预定义正则常量,覆盖表单验证中最常见的校验场景。所有正则均为非全局模式(无 g 标志),可安全地在 test() 中复用而不存在状态问题。


正则速查

常量规则摘要典型合法值典型非法值
REG_USER_NAME4-16 位:中文 / 英文 / 数字 / _ / -alice_01张三ab(太短)、alice@(含非法字符)
REG_PHONE中国大陆 11 位手机号1381234567812345678901(非法号段)
REG_PWD6-18 位:字母 / 数字 / _弱策略pass_123abc(太短)、pass word(含空格)
REG_PWD_STRONG8-32 位非空白,须同时含大小写字母与数字Passw0rd!password1(无大写)、Passw0r(太短)
REG_EMAIL标准邮箱格式user@example.comuser@@example.com
REG_CODE_SIX恰好 6 位纯数字12345612345(5 位)、1234567(7 位)
REG_CODE_FOUR恰好 4 位纯数字123412312345
REG_URLHTTP(S) / 协议相对 URL,协议可省https://example.com/path?q=1//cdn.a.com/x.jsftp://…example(无点)

详细说明

REG_USER_NAME — 用户名

/^[\u4E00-\u9FA5a-zA-Z0-9_-]{4,16}$/
  • 字符集:中文(CJK 基本汉字)+ 英文大小写 + 数字 + 下划线 + 短横线
  • 长度限制:4 ~ 16 个字符
  • 全字符串匹配(^$
REG_USER_NAME.test('alice'); // true
REG_USER_NAME.test('张三_01'); // true
REG_USER_NAME.test('ab'); // false(太短)
REG_USER_NAME.test('alice@gmail'); // false(@ 非法)
REG_USER_NAME.test('a'.repeat(17)); // false(太长)

REG_PHONE — 中国大陆手机号

/^1[3-9]\d{9}$/
  • 首位必须为 1,第二位 3-9,共 11 位
  • 后 9 位为任意数字
REG_PHONE.test('13812345678'); // true
REG_PHONE.test('19912345678'); // true
REG_PHONE.test('12345678901'); // false(第二位是 2)
REG_PHONE.test('1381234567'); // false(只有 10 位)
REG_PHONE.test('+8613812345678'); // false(含国际区号)

这里不枚举号段白名单。运营商放新号段时,枚举式正则会立刻失效误杀真实用户; 放宽到 1[3-9] 的漏判成本远低于误杀。真正的号码有效性交给短信验证码。

如需校验带国际区号的手机号,需使用专门的国际号码校验库(如 libphonenumber-js)。


REG_PWD — 密码

/^\w{6,18}$/
  • 字符集:\w = [a-zA-Z0-9_](字母 + 数字 + 下划线)
  • 长度限制:6 ~ 18 个字符
  • 不允许空格、特殊符号(如 @!#
REG_PWD.test('pass_123'); // true
REG_PWD.test('UPPER_case_01'); // true
REG_PWD.test('abc'); // false(太短)
REG_PWD.test('pass word'); // false(含空格)
REG_PWD.test('p@ssword'); // false(含 @)

这条规则禁止符号,是一个偏弱的口令策略。仅在需要兼容存量账号体系时使用,新项目请用下面的 REG_PWD_STRONG


REG_PWD_STRONG — 密码(强策略)

/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)\S{8,32}$/
  • 长度限制:8 ~ 32 个字符
  • 必须同时包含:至少一个小写字母、一个大写字母、一个数字
  • 允许符号,只禁止空白字符(\S
REG_PWD_STRONG.test('Passw0rd'); // true
REG_PWD_STRONG.test('P@ssw0rd!'); // true(符号是允许的)
REG_PWD_STRONG.test('password1'); // false(缺大写)
REG_PWD_STRONG.test('PASSWORD1'); // false(缺小写)
REG_PWD_STRONG.test('Password'); // false(缺数字)
REG_PWD_STRONG.test('Passw0r'); // false(太短)
REG_PWD_STRONG.test('Passw 0rd'); // false(含空格)

前三个 (?=...) 是零宽断言,只做「存在性检查」不消费字符,真正决定长度和字符集的是末尾的 \S{8,32}


REG_EMAIL — 邮箱

/^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/
  • 本地部分:\w+([-+.]\w+)*,支持 +-. 分隔
  • 域名部分:\w+([-.]\w+)*\.\w+([-.]\w+)*,支持子域名和多级后缀
  • 校验范围覆盖 99% 的常见邮箱格式
REG_EMAIL.test('user@example.com'); // true
REG_EMAIL.test('user+filter@gmail.com'); // true
REG_EMAIL.test('user.name@sub.domain.io'); // true
REG_EMAIL.test('user@'); // false
REG_EMAIL.test('@example.com'); // false
REG_EMAIL.test('user@example'); // false(无顶级域名)

REG_CODE_SIX — 六位数字验证码

/^\d{6}$/

精确匹配 6 位连续数字,不多不少。

REG_CODE_SIX.test('123456'); // true
REG_CODE_SIX.test('12345'); // false(5 位)
REG_CODE_SIX.test('1234567'); // false(7 位)
REG_CODE_SIX.test('12345a'); // false(含字母)

REG_CODE_FOUR — 四位数字验证码

/^\d{4}$/

精确匹配 4 位连续数字,常用于支付密码、短信验证码等场景。

REG_CODE_FOUR.test('1234'); // true
REG_CODE_FOUR.test('0000'); // true
REG_CODE_FOUR.test('123'); // false(3 位)
REG_CODE_FOUR.test('12345'); // false(5 位)

REG_URL — HTTP/HTTPS URL

/^(https?:\/\/|\/\/)?([\w-]+\.)+[a-zA-Z]{2,}(:\d{1,5})?(\/[^\s?#]*)?(\?[^\s#]*)?(#\S*)?$/
  • 协议可选:http://https://,或协议相对的 //
  • 主机必须至少含一个点,顶级域为 2 位以上字母
  • 端口、路径、查询参数、锚点均可选
REG_URL.test('https://example.com'); // true
REG_URL.test('http://example.com/path?q=1#hash'); // true
REG_URL.test('https://example.com:8080'); // true
REG_URL.test('//cdn.example.com/a.js'); // true(协议相对)
REG_URL.test('www.example.com'); // true
REG_URL.test('example.com'); // true(有点即可,协议可省)
REG_URL.test('ftp://example.com'); // false(非 http/https)
REG_URL.test('not a url'); // false

这条正则各段字符集互不重叠、量词不嵌套,失配时不会产生指数级回溯。 早期版本把 \w 同时放进相邻两个字符类里,是典型的 ReDoS 形状——校验一条恶意长串就能把主线程挂住。


在表单中使用

原生表单

function validatePhone(value: string): string | null {
  if (!REG_PHONE.test(value)) return '请输入正确的手机号';
  return null;
}

Ant Design Form

<Form.Item
  name="phone"
  rules={[
    { required: true, message: '请输入手机号' },
    { pattern: REG_PHONE, message: '手机号格式不正确' }
  ]}
>
  <Input placeholder="请输入手机号" />
</Form.Item>

React Hook Form

<input
  {...register('email', {
    required: '请输入邮箱',
    pattern: {
      value: REG_EMAIL,
      message: '邮箱格式不正确',
    },
  })}
/>

Zod schema

import { z } from 'zod';
import { REG_PHONE, REG_EMAIL, REG_PWD } from '@skyroc/utils';

const loginSchema = z.object({
  phone: z.string().regex(REG_PHONE, '手机号格式不正确'),
  password: z.string().regex(REG_PWD, '密码为 6-18 位字母、数字或下划线')
});

const registerSchema = z.object({
  email: z.string().regex(REG_EMAIL, '邮箱格式不正确'),
  username: z.string().regex(REG_USER_NAME, '用户名为 4-16 位,支持中文、英文、数字、_、-'),
  verifyCode: z.string().regex(REG_CODE_SIX, '请输入 6 位验证码')
});

关于 g 标志

这些正则不含 g(全局)标志,可以安全地在多次 test() 调用中复用,不会因 lastIndex 状态导致奇怪的行为:

// ✅ 安全,无 g 标志时 lastIndex 始终为 0
REG_PHONE.test('13812345678'); // true
REG_PHONE.test('13812345678'); // true(仍然正确)

// ⚠️ 如果有 g 标志(本模块不存在此问题,仅作示意)
const reg = /^\d+$/g;
reg.test('123'); // true
reg.test('123'); // false(lastIndex 被修改,第二次从 lastIndex 开始匹配)

Last updated on