写在前面

本文为 Vue3+Vite 项目实战系列教程文章第五篇,系列文章建议从头观看作用更佳,咱们可重视 Vue3 实战系列 防走失!点个赞再看有助于全文完好阅读!

此系列文章首要运用到的首要技能站栈为 Vue3+Vite,那既然是 Vue3,状态库咱们运用的是 Pinia 而不是 Vuex,在写法上也必定是以 CompositionAPI 为主而不是 OptionsAPI,组件库方面咱们运用的是 ArcoDesign (赶紧丢掉 ElementUI 吧!)。

  • 这是一份保姆级Vue3+Vite实战教程 – 介绍 Vue3+Vite 项目的搭建以及项目上的一些装备。
  • 保姆级Vue3+Vite项目实战多布局(上) – 介绍多布局完成思路以及默许布局搭建。
  • 保姆级Vue3+Vite项目实战多布局(下) – 介绍边栏布局完成以及动态布局切换。
  • 保姆级Vue3+Vite项目实战是非办法切换 – 项目中的是非(浅色/暗色)办法切换

总算到了写正派项目代码了,那之前也说过,榜首个小功用咱们写正则校验东西,由于往常写正则老是找在线的校验东西测验,所以就想着自己写一个,今后每次写的正则都弥补到在线东西里,渐渐的积累的多了,也能少写一点点(其实仍是由于懒)。

项目 GitHub 地址

项目在线预览

假如咱们不想从头来过能够直接下载截止到上文内容的代码, toolsdog tag v0.0.3-dev !

代码拉下来之后,npm install || pnpm install 下载依靠,然后 npm run serve || pnpm serve 启动,假如悉数没问题的话,当时项目运转起来是这样的:

保姆级Vue3+Vite项目实战正则在线校验东西

功用构思

写之前咱们先来说一说大约的一个构思。由于我现在暂时算是写完了,所以能够先给咱们放一个写完的图看下功用,稍候给咱们介绍,如下:

保姆级Vue3+Vite项目实战正则在线校验东西

大约便是上面这么个姿态,其实也没什么特别杂乱的当地。

总共也就 5 排内容,咱们一排一排介绍。

先说榜首排内容吧,榜首排最左边那个图标是是否敞开预设匹配,由于咱们要在内部预设一些正则,敞开预设匹配之后,在正则输入框输入对应正则中文名就能够模糊匹配咱们预设的正则,这个是懒癌患者必备。

中心输入框便是正则输入框,能够输入正则字符串,当然敞开了主动匹配预设之后也能够输入中文匹配预设。

输入框右边漏斗图标是修饰符筛选项,便是挑选正则匹配的一些修饰符(g、i、m、s)。

再往右小眼睛图标是可视化预览,敞开之后咱们输入正则就会主动烘托图中第二排那个图形化正则匹配过程,只有可视化正则过程敞开而且正则表达式无误才会有榜首排最右边倒数第二个即图片下载按钮,用来下载正则可视化 png 图。

最右边按钮则是仿制正则目标按钮。

第三排输入框是待匹配字符串,输入一些字符串用来做正则匹配检测的。

第四排便是展现匹配到的字符串,匹配到多个用不同的色彩去展现。

第五排便是计算总共匹配到了那些字符串。

功用大约便是这样。

正则校验页面

开端写代码了,先做下准备工作,之前咱们已经在 src/views 文件夹下创立了正则匹配页面 RegularPage.vue,现在咱们修正一下,把这个页面修正为 RegularPage/index.vue,由于后边咱们要写个正则可视化预览的组件,所以这儿先把正则校验页面改成文件夹,那这儿改了,路由页面也需求改一下,修正 src/router/menuRouter.js 文件如下:

import IconMaterialSymbolsCodeBlocksOutline from '~icons/material-symbols/code-blocks-outline'
export const menuRouter = [
  {
    path: 'devtools',
    name: 'DevTools',
    meta: {
      title: '开发东西',
      icon: markRaw(IconMaterialSymbolsCodeBlocksOutline)
    },
    redirect: { name: 'RegularPage' },
    children: [
      {
        path: 'regular',
        name: 'RegularPage',
        meta: {
          title: '正则在线校验'
        },
				// 修正如下
        component: () => import('@/views/RegularPage/index.vue')
        // component: () => import('@/views/RegularPage.vue')
      }
    ]
  }
]
// ...

OK,再次改写页面没问题的话就能够继续了!

准备正则预设

之前咱们也说了,要写一些预设的正则让用户能够直接搜正则功用名匹配,那写之前,咱们先来准备一下这些预设的正则,在 src/utils 文件夹下新建 regexp.js 文件,咱们把一些常用正则写进去,如下:

export const isNumber = /^[0-9]*$/g
isNumber.name = '匹配数字'
export const isNonnegativeInteger = /^d+$/g
isNonnegativeInteger.name = '匹配非负整数(正整数 + 0)'
export const isPositiveInteger = /^[0-9]*[1-9][0-9]*$/g
isPositiveInteger.name = '匹配正整数'
export const isNegativeInteger = /^-[0-9]*[1-9][0-9]*$/g
isNegativeInteger.name = '匹配负整数'
export const isInteger = /^-?d+$/g
isInteger.name = '匹配整数'
export const isNonpositiveInteger = /^((-d+)|(0+))$/g
isNonpositiveInteger.name = '匹配非正整数(负整数 + 0)'
export const isNonnegativeFloat = /^d+(.d+)?$/g
isNonnegativeFloat.name = '匹配非负浮点数(正浮点数 + 0)'
export const isPositiveFloat =
  /^((0.d*[1-9]d*)|([1-9]d*.d*)|([1-9]d*))$/g
isPositiveFloat.name = '匹配正浮点数'
export const isNegativeFloat =
  /^-((0.d*[1-9]d*)|([1-9]d*.d*)|([1-9]d*))$/g
isNegativeFloat.name = '匹配负浮点数'
export const isFloat = /^(-?d+)(.d+)?$/g
isFloat.name = '匹配浮点数'
export const isNonpositiveFloat = /^((-d+(.d+)?)|(0+(.0+)?))$/g
isNonpositiveFloat.name = '匹配非正浮点数(负浮点数 + 0)'
export const isEnglish = /^[A-Za-z]+$/g
isEnglish.name = '匹配由26个英文字母组成的字符串'
export const isLowercaseEnglish = /^[a-z]+$/g
isLowercaseEnglish.name = '匹配由26个英文字母的小写组成的字符串'
export const isUppercaseEnglish = /^[A-Z]+$/g
isUppercaseEnglish.name = '匹配由26个英文字母的大写组成的字符串'
export const isEnglishAndNumber = /^[A-Za-z0-9]+$/g
isEnglishAndNumber.name = '匹配由数字和26个英文字母组成的字符串'
export const isEnglishAndNumberAndUnderline = /^w+$/g
isEnglishAndNumberAndUnderline.name =
  '匹配由数字、26个英文字母或许下划线组成的字符串'
export const isChinese = /^[u4e00-u9fa5]{0,}$/g
isChinese.name = '匹配中文'
export const isPhone = /^1[3456789]d{9}$/g
isPhone.name = '匹配手机号'
export const isIdCard = /(^d{15}$)|(^d{18}$)|(^d{17}(d|X|x)$)/g
isIdCard.name = '匹配身份证(一代&二代)'
export const isFirstGenerationIDCard = /(^d{15}$)/g
isFirstGenerationIDCard.name = '匹配一代身份证'
export const isSecondGenerationIDCard = /(^d{18}$)|(^d{17}(d|X|x)$)/g
isSecondGenerationIDCard.name = '匹配二代身份证'
export const isUrl = /^((https|http|ftp|rtsp|mms)?://)[^s]+/g
isUrl.name = '匹配URL'
export const isIP = /^((25[0-5]|2[0-4]d|[01]?dd?)($|(?!.$).)){4}$/g
isIP.name = '匹配IP'
export const isDate = /^(d{4})-(d{2})-(d{2})$/g
isDate.name = '匹配日期'
export const isTime = /^([01]d|2[0-3])(:[0-5]d){1,2}$/g
isTime.name = '匹配时刻'
export const isDateTime =
  /^(d{4})-(d{2})-(d{2})s([01]d|2[0-3])(:[0-5]d){1,2}$/g
isDateTime.name = '匹配日期时刻'
export const isColor = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/g
isColor.name = '匹配色彩'
export const isQQ = /^[1-9][0-9]{4,9}$/g
isQQ.name = '匹配QQ'
export const isWeChat = /^[a-zA-Z]([-_a-zA-Z0-9]{5,19})+$/g
isWeChat.name = '匹配微信'
export const isPostalCode = /[1-9]d{5}(?!d)/g
isPostalCode.name = '匹配邮编'
export const isMacAddress = /^([A-Fa-f0-9]{2}:){5}[A-Fa-f0-9]{2}$/g
isMacAddress.name = '匹配MAC地址'
export const isIPV4 = /^((25[0-5]|2[0-4]d|[01]?dd?)($|(?!.$).)){4}$/g
isIPV4.name = '匹配IPV4地址'
export const isIPV6 = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/g
isIPV6.name = '匹配IPV6地址'
export const isBase64 = /[^A-Za-z0-9+/=]/g
isBase64.name = '匹配Base64'
export const isPasswordStrong = /^(?=.*[a-z])(?=.*[A-Z])(?=.*d)[^]{8,16}$/g
isPasswordStrong.name = '匹配强密码'
export const isPasswordMedium = /^(?=.*[a-z])(?=.*[A-Z])(?=.*d)[^]{6,16}$/g
isPasswordMedium.name = '匹配中等密码'
export const isPasswordSimple = /^[a-zA-Z0-9_-]{6,16}$/g
isPasswordSimple.name = '匹配简单密码'
export const isCarNumber = /^[u4e00-u9fa5]{1}[A-Z]{1}[A-Z_0-9]{5}$/g
isCarNumber.name = '匹配车牌号(简单)'
export const isLicensePlateNumber =
  /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领 A-Z]{1}[A-HJ-NP-Z]{1}[A-Z0-9]{4}[A-Z0-9挂学警港澳]{1}$/g
isLicensePlateNumber.name = '匹配车牌号(严厉)'
export const isStockCode =
  /^(s[hz]|S[HZ])(000[d]{3}|002[d]{3}|300[d]{3}|600[d]{3}|60[d]{4})$/g
isStockCode.name = '匹配股票代码'
export const isBankCard = /^([1-9]{1})(d{14}|d{18})$/g
isBankCard.name = '匹配银行卡'
export const is126Email = /((^([a-zA-Z]))(w){5,17})@126.com$/g
is126Email.name = '匹配126邮箱'
export const is163Email = /((^([a-zA-Z]))(w){5,17})@163.com$/g
is163Email.name = '匹配163邮箱'
export const isGmailEmail = /((^([a-zA-Z]))(w){5,17})@gmail.com$/g
isGmailEmail.name = '匹配Gmail邮箱'
export const isQQEmail = /((^([a-zA-Z]))(w){5,17})@qq.com$/g
isQQEmail.name = '匹配QQ邮箱'
export const isSinaEmail = /((^([a-zA-Z]))(w){5,17})@sina.com$/g
isSinaEmail.name = '匹配新浪邮箱'
export const isSohuEmail = /((^([a-zA-Z]))(w){5,17})@sohu.com$/g
isSohuEmail.name = '匹配搜狐邮箱'
export const isYahooEmail = /((^([a-zA-Z]))(w){5,17})@yahoo.com$/g
isYahooEmail.name = '匹配雅虎邮箱'
export const isOutlookEmail = /((^([a-zA-Z]))(w){5,17})@outlook.com$/g
isOutlookEmail.name = '匹配Outlook邮箱'
export const isHotmailEmail = /((^([a-zA-Z]))(w){5,17})@hotmail.com$/g
isHotmailEmail.name = '匹配Hotmail邮箱'
export const isEmail = /^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+(.[a-zA-Z0-9_-])+$/g
isEmail.name = '匹配邮箱'

如上,咱们字面量创立并导出了许多正则目标,而且给每个正则目标都加了名字留作后用!

PS: 网上随意摘抄了些,简单看了下,暂时没有验证对错,不保真哈,后边用着不对再改吧,咱们有什么弥补的或许发现有过错,能够直接去 GitHubPR 或许 issues 哈,能够一块完善下,方面咱们嘛!!

正则输入框

接下来咱们开端写代码,上面说过咱们能够将整个页面分为上中下三块,先来简单写个布局,修正 RegularPage/index.vue 正则校验页面,如下:

<script setup></script>
<template>
  <div class="max-w-1200px w-full p-20px box-border">
    <!-- 上 -->
    <div class="flex justify-start items-center"></div>
    <!-- 中 -->
    <div class="mt-20px bg-[var(--color-fill-2)]"></div>
    <!-- 下 -->
    <div class="w-full mt-20px"></div>
  </div>
</template>

OK,整活吧!咱们先看下代码,也便是正则输入框相关的代码,修正 RegularPage/index.vue 正则校验页面如下:

<script setup>
import * as regPresetsObj from '@/utils/regexp.js'
// 巨细
const size = ref('large')
// 正则表达式字符串
const regStr = ref('')
// 正则表达式修饰符
const regFlag = ref([])
// 正则表达式目标
const reg = ref(null)
watchEffect(() => {
  try {
    reg.value = regStr.value
      ? new RegExp(regStr.value, regFlag.value.join(''))
      : null
  } catch (err) {
    reg.value = null
  }
})
// 预设主动查找
const auto = ref(true)
// 正则预设列表
const regPresets = ref([])
// 正则预设匹配办法
const regPresetMatch = value => {
  if (value && auto.value) {
    regPresets.value = [...Object.values(regPresetsObj)]
      .filter(r => r.name.includes(value) || r.source.includes(value))
      .map(v => {
        return {
          label: v.name,
          regexp: v,
          value: v.source
        }
      })
  } else {
    regPresets.value = []
  }
}
// 正则预设选中办法
const regPresetSelect = r => {
  let selected = regPresets.value.find(v => v.value === r)
  if (selected) regFlag.value = selected.regexp.flags.split('')
}
</script>
<template>
  <div class="max-w-1200px w-full p-20px box-border">
    <!-- 上 -->
    <div class="flex justify-start items-center">
      <a-tooltip
        :content="`点击${auto ? '封闭' : '敞开'}预设正则主动匹配`"
        position="bottom"
      >
        <a-button :size="size" @click="auto = !auto">
          <template #icon>
            <icon-material-symbols-astrophotography-auto
              :class="auto ? 'text-[rgb(var(--arcoblue-6))]' : ''"
            />
          </template>
        </a-button>
      </a-tooltip>
      <a-auto-complete
        class="ml-10px"
        v-model="regStr"
        :data="regPresets"
        @search="regPresetMatch"
        @select="regPresetSelect"
        :placeholder="`请输入正则表达式${
          auto ? '或输入文字挑选主动匹配的预设正则' : ''
        }`"
        allow-clear
        :size="size"
      >
        <template #prepend>
          <a-tooltip
            :content="reg ? '正则表达式正确' : '正则表达式有误'"
            position="bottom"
            mini
          >
            <icon-jam-triangle-danger-f
              class="text-[rgb(var(--orange-6))]"
              v-if="!reg"
            />
            <icon-mdi-hand-okay class="text-[rgb(var(--green-6))]" v-else />
          </a-tooltip>
          <span class="ml-5px">/</span>
        </template>
        <template #append>
          <span> /{{ reg?.flags || regFlag?.join('') }}</span>
        </template>
      </a-auto-complete>
      <a-popover title="挑选修饰符" position="bl">
        <a-button :size="size" class="ml-10px">
          <template #icon>
            <icon-mdi-filter-multiple
              :class="regFlag.length > 0 ? 'text-[rgb(var(--arcoblue-6))]' : ''"
            />
          </template>
        </a-button>
        <template #content>
          <div class="min-w-200px">
            <a-checkbox-group
              :size="size"
              v-model="regFlag"
              direction="vertical"
            >
              <a-checkbox value="g"> -g:大局匹配 </a-checkbox>
              <a-checkbox value="i"> -i:疏忽巨细写</a-checkbox>
              <a-checkbox value="m"> -m:多行匹配</a-checkbox>
              <a-checkbox value="s"> -s:特别字符. 包括换行符</a-checkbox>
            </a-checkbox-group>
          </div>
        </template>
      </a-popover>
    </div>
    <!-- 中 -->
    <div class="mt-20px bg-[var(--color-fill-2)]"></div>
    <!-- 下 -->
    <div class="w-full mt-20px"></div>
  </div>
</template>

来简单介绍下上面代码内容:

这儿咱们仍是运用 ArcoDesign 组件,咱们创立了一个呼应式特点 size 用来操控页面上输入框等 ArcoDesign 组件巨细。

创立正则表达式字符串 regStr,留意这儿的正则字符串是不带双斜杠 // 的。

创立正则修饰符 regFlag,修饰符这儿咱们创立的是个呼应式数组,数组中每一项代表一个正则修饰符(修饰符是正则表达式的标记,用于指定额定的匹配策略,不理解的同学能够百度谷歌下)。

正则字符串和修饰符有了,那正则表达式也就有了,创立正则表达式目标 reg,咱们经过 Vue APIwatchEffect 办法来监听 regStrregFlag 改动,随之给正则目标 reg 赋值,创立的时候运用 try…catch 包裹,这样假如创立失败会走 catch,咱们在 catch 中把正则目标清空,别问为啥不必计算特点,由于正则目标 reg 后边用的许多,运用计算特点后边会很费事。

接着还创立了一个是否开起正则预设查找的特点 auto,默许为 true,代表能够输入中文模糊匹配预设的正则。

OK,再看模板,Template 模板中每个功用按钮咱们都运用 ArcoDesigna-tooltip 组件做了个悬浮气泡提示,下文所有功用按钮都有气泡提示,不在赘述。

在上中下三个模块的上模块中,咱们先写了一个预设匹配敞开按钮,经过 auto 特点值烘托不同图标色彩来区别是否敞开预设匹配,图标运用的是 iconify 图标库的 material-symbols:astrophotography-auto 图标(主动引进装备看前文,后边 iconify 图标库的图标我就不提了哈,看下图标组件名就知道是哪个图标了,假如有用到自定义的图标我再独自说),而且给该按钮写了点击事情,点击修正 auto 特点值,代码片段如下:

<a-tooltip
  :content="`点击${auto ? '封闭' : '敞开'}预设正则主动匹配`"
  position="bottom"
>
  <a-button :size="size" @click="auto = !auto">
    <template #icon>
      <icon-material-symbols-astrophotography-auto
        :class="auto ? 'text-[rgb(var(--arcoblue-6))]' : ''"
      />
    </template>
  </a-button>
</a-tooltip>

接着是输入框,由于输入框有预设匹配的需求,所以这儿咱们运用 ArcoDesigna-auto-complete 组件,组件 v-model 值即上面创立的正则字符串特点 regStr

在最初咱们导入了 regexp.js 文件中悉数的正则预设目标 regPresetsObj

a-auto-complete 组件的 data 特点即下拉列表数据,咱们填入预设数组 regPresets,默许是空的数组即没有匹配中项。组件的 search 办法即自定义的匹配办法,咱们写了 regPresetMatch 办法去匹配。regPresetMatch 办法中参数 value 即正则字符串 regStr 值,咱们在办法中校验 value 值存在而且预设匹配 auto 特点为 true 的情况下去匹配默许预设列表 regPresetsObj 中的数据匹配并回来一个匹配到的列表赋值给预设数组 regPresets,假如 value 值为空或许 auto 特点为 false 则重置预设数组 regPresets 。这样咱们每次写内容时预设列表中有匹配到就会以下拉的办法展现出来。

PS: 模糊匹配首要是用 ES6includesfilter 办法,新同学能够查查文档。

预设数组 regPresets 的格局是根据组件要求来的,即数组目标,数组中每个目标特点如下:

  • label 预设正则项中文名。
  • value 预设正则项 key ,咱们运用正则目标的 source 值来作为 key ,正则目标的 source 特点即正则字符串(不带双斜杠不带修饰符的那种)。
  • regexp 预设正则目标,这个是咱们自定义的特点,用来后边下拉选中时给修饰符赋值。

代码片段如下:

// 正则预设匹配办法
const regPresetMatch = value => {
  if (value && auto.value) {
    regPresets.value = [...Object.values(regPresetsObj)]
      .filter(r => r.name.includes(value) || r.source.includes(value))
      .map(v => {
        return {
          label: v.name,
          regexp: v,
          value: v.source
        }
      })
  } else {
    regPresets.value = []
  }
}

默许 a-auto-complete 组件选中时会将下拉列表选中项的 value 值赋值给组件 v-model 特点,匹配项的 value 值咱们之前设置的是正则字符串,即匹配到预设选中后 value 值就会赋值给 regStr 特点,由于咱们的预设正则中或许携带修饰符,所以,咱们还需求在组件 select 选中事情中给修饰符 regFlag 特点赋值,即 regPresetSelect 办法内容,如下:

// 正则预设选中办法
const regPresetSelect = r => {
  let selected = regPresets.value.find(v => v.value === r)
  if (selected) regFlag.value = selected.regexp.flags.split('')
}

PS: 正则目标中的 source 特点是正则字符串,而 flags 特点也是一个字符串,即该正则的修饰符,不晓得的同学能够操控台随意打印一个正则目标看下。

OK,咱们在 a-auto-complete 组件中还写了两个插槽,相同都是组件插槽哈,能够看看文档,前置插槽 prepend 和后置插槽 append,前置插槽中除了 / 之外,咱们还简单用 2 个图标来提示当时输入正则的对错,后置插槽中除了 / 之外,咱们还烘托了当时选中的修饰符 regFlag

代码片段如下:

<a-auto-complete
  class="ml-10px"
  v-model="regStr"
  :data="regPresets"
  @search="regPresetMatch"
  @select="regPresetSelect"
  :placeholder="`请输入正则表达式${
    auto ? '或输入文字挑选主动匹配的预设正则' : ''
  }`"
  allow-clear
  :size="size"
>
  <template #prepend>
    <a-tooltip
      :content="reg ? '正则表达式正确' : '正则表达式有误'"
      position="bottom"
      mini
    >
      <icon-jam-triangle-danger-f
        class="text-[rgb(var(--orange-6))]"
        v-if="!reg"
      />
      <icon-mdi-hand-okay class="text-[rgb(var(--green-6))]" v-else />
    </a-tooltip>
    <span class="ml-5px">/</span>
  </template>
  <template #append>
    <span> /{{ reg?.flags || regFlag?.join('') }}</span>
  </template>
</a-auto-complete>

OK,最终便是修饰符选项了,运用同一个图标按钮,以不同色彩区别是否选中有修饰符,运用 ArcoDesigna-popover 组件使鼠标悬浮到图标按钮时呈现气泡提示弹窗,弹窗内以多选框组 a-checkbox-group 组件烘托几个修饰符选项,v-model 值设置为修饰符特点 regFlag (数组)即可。

代码片段如下:

<a-popover title="挑选修饰符" position="bl">
  <a-button :size="size" class="ml-10px">
    <template #icon>
      <icon-mdi-filter-multiple
        :class="regFlag.length > 0 ? 'text-[rgb(var(--arcoblue-6))]' : ''"
      />
    </template>
  </a-button>
  <template #content>
    <div class="min-w-200px">
      <a-checkbox-group
        :size="size"
        v-model="regFlag"
        direction="vertical"
      >
        <a-checkbox value="g"> -g:大局匹配 </a-checkbox>
        <a-checkbox value="i"> -i:疏忽巨细写</a-checkbox>
        <a-checkbox value="m"> -m:多行匹配</a-checkbox>
        <a-checkbox value="s"> -s:特别字符. 包括换行符</a-checkbox>
      </a-checkbox-group>
    </div>
  </template>
</a-popover>

修饰符咱们写了四个,分别是:

  • -g 匹配大局。
  • -i 疏忽巨细写。
  • -m 匹配多行。
  • -s 特别字符.,包括换行符。

OK,简单解说了一下上面代码的意思,保存改写页面,当时作用如下:

保姆级Vue3+Vite项目实战正则在线校验东西

保姆级Vue3+Vite项目实战正则在线校验东西

正则目标有了,接下来咱们来写匹配成果!

正则匹配成果预览

想要匹配成果咱们要先声明一个待匹配的字符串特点 matchStr,然后运用 ArcoDesigna-textarea 组件写个多行文本即可,代码片段如下:

<script setup>
// 待匹配字符串
const matchStr = ref('')
</script>
<a-textarea
  v-model="matchStr"
  default-value=""
  placeholder="请输入要匹配的字符串"
  :auto-size="{
    minRows: 3,
    maxRows: 6
  }"
/>

接着咱们需求展现当时待匹配字符串根据正则目标匹配到的内容,这儿直接用计算特点来监听待匹配字符串改动然后运用 match 办法在待匹配字符串中匹配正则对应值即可。

PS: match 办法可在字符串内检索指定的值,或找到一个或多个正则表达式的匹配,不了解的同学仍是查下文档。

如下:

// 匹配成果数组 reg即上文写的正则目标 filter过滤空值,默许空数组
const matchingResults = computed(
  () => matchStr.value.match(reg.value)?.filter(v => v) || []
)

OK,现在咱们能够在 Template 模板中简单写一下匹配成果的 HTML 了,校验当匹配到的成果数组 matchingResults 长度大于 0,即展现总共多少项匹配,然后依次列出匹配的成果值。数组长度小于等于 0 则不展现。

<div
  v-if="matchingResults.length > 0"
  class="w-full min-h-100px bg-[var(--color-fill-2)] whitespace-pre-wrap break-words indent-0 leading-22px mt-20px px-12px py-4px box-border"
>
  <div>共 {{ matchingResults.length }} 处匹配:</div>
  <div v-for="(res, i) of matchingResults" :key="i">{{ res }}</div>
</div>

上面咱们只展现了匹配到的各个成果,咱们还需求在完好待匹配字符串中展现一下各个匹配成果的位置,即展现完好字符串,并把匹配成果以不同的色彩在原字符串中标识出来。

想一想,原始字符串咱们有,匹配到的成果咱们也有,所以根据原始字符串和匹配成果咱们写个格局化办法给匹配到的字符串加一下标签和款式然后回来烘托到页面就行了,代码片段如下:

<script>
// 待匹配字符串
const matchStr = ref('')
// 匹配成果数组 reg即上文写的正则目标 filter过滤空值,默许空数组
const matchingResults = computed(
  () => matchStr.value.match(reg.value)?.filter(v => v) || []
)
// 匹配成果格局化
const matchingFormat = computed(() => {
  let res = '',
    str = matchStr.value
  let n = 1
  matchingResults.value.forEach(v => {
    if (n > 4) n = 1
    let matchStr = str.substr(0, v.length + str.indexOf(v))
    str = str.substr(v.length + str.indexOf(v))
    res += matchStr.replace(
      v,
      `<span class="matching matching${n}">${v}</span>`
    )
    n++
  })
  return res + str
})
</script>
<div
  class="w-full min-h-100px bg-[var(--color-fill-2)] whitespace-pre-wrap break-words indent-0 leading-22px mt-20px px-12px py-4px box-border"
  v-html="matchingFormat || '无匹配成果'"
></div>
<style>
.matching {
  @apply px-0px rounded-4px box-border;
}
.matching1 {
  @apply bg-[rgb(var(--arcoblue-2))];
}
.matching2 {
  @apply bg-[rgb(var(--green-2))];
}
.matching3 {
  @apply bg-[rgb(var(--orange-2))];
}
.matching4 {
  @apply bg-[rgb(var(--red-2))];
}
</style>

OK,把上面这些内容组合一下,放到 RegularPage 页面中,现在正则校验页面的代码如下:

<script setup>
import * as regPresetsObj from '@/utils/regexp.js'
// 巨细
const size = ref('large')
// 正则表达式目标
const reg = ref(null)
// 正则表达式字符串
const regStr = ref('')
// 正则表达式修饰符
const regFlag = ref([])
watchEffect(() => {
  try {
    reg.value = regStr.value
      ? new RegExp(regStr.value, regFlag.value.join(''))
      : null
  } catch (err) {
    reg.value = null
  }
})
// 预设主动查找
const auto = ref(true)
// 正则预设列表
const regPresets = ref([])
// 正则预设匹配办法
const regPresetMatch = value => {
  if (value && auto.value) {
    regPresets.value = [...Object.values(regPresetsObj)]
      .filter(r => r.name.includes(value) || r.source.includes(value))
      .map(v => {
        return {
          label: v.name,
          regexp: v,
          value: v.source
        }
      })
  } else {
    regPresets.value = []
  }
}
// 正则预设选中办法
const regPresetSelect = r => {
  let selected = regPresets.value.find(v => v.value === r)
  if (selected) regFlag.value = selected.regexp.flags.split('')
}
// 待匹配字符串
const matchStr = ref('')
// 匹配成果
const matchingResults = computed(
  () => matchStr.value.match(reg.value)?.filter(v => v) || []
)
// 匹配成果格局化
const matchingFormat = computed(() => {
  let res = '',
    str = matchStr.value
  let n = 1
  matchingResults.value.forEach(v => {
    if (n > 4) n = 1
    let matchStr = str.substr(0, v.length + str.indexOf(v))
    str = str.substr(v.length + str.indexOf(v))
    res += matchStr.replace(
      v,
      `<span class="matching matching${n}">${v}</span>`
    )
    n++
  })
  return res + str
})
</script>
<template>
  <div class="max-w-1200px w-full p-20px box-border">
    <div class="flex justify-start items-center">
      <a-tooltip
        :content="`点击${auto ? '封闭' : '敞开'}预设正则主动匹配`"
        position="bottom"
      >
        <a-button :size="size" @click="auto = !auto">
          <template #icon>
            <icon-material-symbols-astrophotography-auto
              :class="auto ? 'text-[rgb(var(--arcoblue-6))]' : ''"
            />
          </template>
        </a-button>
      </a-tooltip>
      <a-auto-complete
        class="ml-10px"
        v-model="regStr"
        :data="regPresets"
        @search="regPresetMatch"
        @select="regPresetSelect"
        :placeholder="`请输入正则表达式${
          auto ? '或输入文字挑选主动匹配的预设正则' : ''
        }`"
        allow-clear
        :size="size"
      >
        <template #prepend>
          <a-tooltip
            :content="reg ? '正则表达式正确' : '正则表达式有误'"
            position="bottom"
            mini
          >
            <icon-jam-triangle-danger-f
              class="text-[rgb(var(--orange-6))]"
              v-if="!reg"
            />
            <icon-mdi-hand-okay class="text-[rgb(var(--green-6))]" v-else />
          </a-tooltip>
          <span class="ml-5px">/</span>
        </template>
        <template #append>
          <span> /{{ reg?.flags || regFlag?.join('') }}</span>
        </template>
      </a-auto-complete>
      <a-popover title="挑选修饰符" position="bl">
        <a-button :size="size" class="ml-10px">
          <template #icon>
            <icon-mdi-filter-multiple
              :class="regFlag.length > 0 ? 'text-[rgb(var(--arcoblue-6))]' : ''"
            />
          </template>
        </a-button>
        <template #content>
          <div class="min-w-200px">
            <a-checkbox-group
              :size="size"
              v-model="regFlag"
              direction="vertical"
            >
              <a-checkbox value="g"> -g:大局匹配 </a-checkbox>
              <a-checkbox value="i"> -i:疏忽巨细写</a-checkbox>
              <a-checkbox value="m"> -m:多行匹配</a-checkbox>
              <a-checkbox value="s"> -s:特别字符. 包括换行符</a-checkbox>
            </a-checkbox-group>
          </div>
        </template>
      </a-popover>
    </div>
    <!-- 中 -->
    <div class="mt-20px bg-[var(--color-fill-2)]"></div>
    <!-- 下 -->
    <div class="w-full mt-20px">
      <a-textarea
        v-model="matchStr"
        default-value=""
        placeholder="请输入要匹配的字符串"
        :auto-size="{
          minRows: 3,
          maxRows: 6
        }"
      />
      <div
        class="w-full min-h-100px bg-[var(--color-fill-2)] whitespace-pre-wrap break-words indent-0 leading-22px mt-20px px-12px py-4px box-border"
        v-html="matchingFormat || '无匹配成果'"
      ></div>
      <div
        v-if="matchingResults.length > 0"
        class="w-full min-h-100px bg-[var(--color-fill-2)] whitespace-pre-wrap break-words indent-0 leading-22px mt-20px px-12px py-4px box-border"
      >
        <div>共 {{ matchingResults.length }} 处匹配:</div>
        <div v-for="(res, i) of matchingResults" :key="i">{{ res }}</div>
      </div>
    </div>
  </div>
</template>
<style>
.matching {
  @apply px-0px rounded-4px box-border;
}
.matching1 {
  @apply bg-[rgb(var(--arcoblue-2))];
}
.matching2 {
  @apply bg-[rgb(var(--green-2))];
}
.matching3 {
  @apply bg-[rgb(var(--orange-2))];
}
.matching4 {
  @apply bg-[rgb(var(--red-2))];
}
</style>

保存改写页面,输入一个手机号正则,选中修饰符 m 即多行匹配,在待匹配输入框中输入一些正确或许过错的手机号,作用如下:

保姆级Vue3+Vite项目实战正则在线校验东西

正则可视化预览

接下来写下正则目标的可视化图表预览,首先咱们要写一个按钮来操控是否敞开可视化预览,声明一个是否敞开可视化预览特点 visualization (默许为 true 敞开即可),然后和上面敞开预设相同,写一下按钮(按钮图标仍是 iconify 图标库中随意找的图标哈,看代码就行)放在榜首行修饰符按钮右边即可,代码片段如下:

<script setup>
// 可视化
const visualization = ref(true)
</script>
<a-tooltip
  :content="`点击${visualization ? '封闭' : '敞开'}可视化解析`"
  position="bottom"
>
  <a-button
    :size="size"
    class="ml-10px"
    @click="visualization = !visualization"
  >
    <template #icon>
      <icon-ic-sharp-visibility
        class="text-[rgb(var(--arcoblue-6))]"
        v-if="visualization"
      />
      <icon-ic-sharp-visibility-off v-else />
    </template>
  </a-button>
</a-tooltip>

接下来咱们开端写可视化组件了,简单描绘流程便是先给正则字符串做 AST 解析,然后剖析正则的 AST 数据,最终烘托图表。

说起来很简单,可是真写的话仍是比较费事的,这儿就直接用三方包了,咱们运用 regulex 来做这件事。

regulex 是一个 JavaScript 正则表达式解析器和可视化东西,这个包也比较老了,好几年没更新了,由于咱们项目有是非办法切换,可是这个包是不支持修正色彩的,于是我 clone 了下来代码修正了烘托办法,让它支持了下是非办法。在克隆的时候由于主分支代码有点问题,所以我运用的是 legacy 分支代码做的修正,由于 legacy 分支代码构建时运用的是 require.js 输出的 AMD 规范代码,所以。。。先凑活用吧,由于它很久不更新了,后边有时刻咱们根据这个库,只运用它的正则 AST 解析以及规矩剖析两个中心办法,然后自己再写一个烘托办法构建发个 ESModule 包,到时候我弥补一篇文章,现在这个包没啥可说的,所以咱们一会儿只介绍下中心办法能运用就行了,由于真实是太老旧了。。。

PS: AMDESModule 以及 require.js 不了解能够看看另一篇文章 「前端工程四部曲」模块化的宿世此生(上)

后边可视化组件代码中咱们会运用到 regulex 库的三个中心办法,如下:

  • parse() – 做正则 AST 规矩解析,该办法接纳一个正则字符串,回来一个解析后的数据目标。
  • Raphael() – 这办法来源于 RaphaelJS ,它是一个根据 SVG 的绘图库,所以它是 regulex 依靠的三方包,首要便是用来制作正则目标的 SVG 图表的,它的用法咱们想要具体了解看文档吧。。。该办法回来一个 Raphael 实例,这儿咱们也就只需求创立个实例就行了。
  • visualize() – 有了 AST 和绘图实例,那中心的烘托逻辑便是这个办法了,首要作用便是根据解析后的正则目标规矩运用 Raphael 实例来制作图形。我这边改的也是只改了这个办法,其余都没动,而且也不影响原本的代码逻辑,此办法原本接纳三个参数,分别是解析后的正则数据目标(即 parse() 办法回来值)、正则修饰符字串和 Raphael 绘图实例,由于咱们加了是非办法,所以给这个烘托办法加了第四个参数即 mode 办法,接纳 dark、light 两个办法值,烘托出的图表是对应的两套色彩。当然,不传这个参数也能够的。

OK,放一个我这边本地修正后从头构建压缩好的 regulex 包,便是一个 AMD 规范的 JS 文件,地址在这儿 regulex.js,自行下载吧,要是你不需求办法切换时图表色彩也改动的话,你也能够直接克隆官方代码构建一个官方包,不过还得本地构建,太费事,直接用我这个改过的方便,下载个 JS 文件就行了!

咱们以 CDN 办法引进这个 JS 文件,当然,咱们没有 CDN,所以下载下来 regulex.js 文件后,直接把这个文件放在项目根目录下的静态资源文件中,即项目根目录下 public 文件夹下新建一个 static 文件夹,把 regulex.js 文件放在这个文件夹下即可,咱们项目代码默许构建时输出的静态资源文件就在 dist/static 文件夹下,这儿咱们直接在 public 文件夹下创立的文件构建时是不会被打包重写的,而是直接仿制文件到 dist 目录下,所以打包时,会直接把 public 文件夹下的文件 copy 到最终项目输出的 dist 目录下,public/static 文件夹下的文件也就会悉数 copydist/static 文件夹下。

文件放好之后,修正下进口 HTML 文件,引进该文件(留意这儿引进是需求肯定路径的),即根目录下的 index.html 文件如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <!-- 引进regulex包 -->
    <script src="/static/regulex.js"></script>
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

由于这是个 AMD 规范的文件,内置了 requireJS,运用的时候直接调用 require 办法引进模块即可,如下例:

let { parse, visualize, Raphael } = require('regulex')

准备工作就绪,能够开端写正则可视化组件了,其实很简单哈,先来看代码,在 src/views/RegularPage 文件夹下新建 components/RegularVisualization.vue 文件,组件代码如下:

<script setup>
// 留意需大局引进regulex.js
defineExpose({
  exportImage
})
const props = defineProps({
  // 正则表达式目标
  modelValue: {
    type: [Object, null],
    required: true
  },
  // 主题明暗办法
  mode: {
    type: String,
    required: true
  }
})
watch(
  [() => props.modelValue, () => props.mode],
  () => {
    nextTick(() => init(props.modelValue))
  },
  { immediate: true }
)
// 导出图片
function exportImage() {
  let ratio = window.devicePixelRatio || 1
  // 获取SVG画布元素
  let svg = window.graphCt.getElementsByTagName('svg')[0]
  // 获取SVG画布宽高
  let w = svg.clientWidth
  let h = svg.clientHeight
  // 创立Image目标
  let img = new Image()
  img.width = w
  img.height = h
  img.setAttribute('src', svgToBase64(svg))
  // 创立Canvas目标
  let canvas = document.createElement('canvas')
  canvas.width = w * ratio
  canvas.height = h * ratio
  let ctx = canvas.getContext('2d')
  ctx.setTransform(ratio, 0, 0, ratio, 0, 0)
  // Image目标onload事情
  img.onload = function () {
    // 给一个背景色彩,否则导出的图片是通明的
    let bgColor = props.mode === 'dark' ? '#2e2e31' : '#f2f3f5'
    ctx.fillStyle = bgColor
    // 制作Image目标到canvas画布
    ctx.fillRect(0, 0, canvas.width, canvas.height)
    ctx.drawImage(img, 0, 0)
    // canvas画布转 Base64 并导出 png
    canvasToPictureDownload(canvas, `regexp_visualization_${Date.now()}`)
  }
}
// canvas转图片下载
function canvasToPictureDownload(canvas, name) {
  let a = document.createElement('a')
  // 将画布内的信息导出为png图片数据 Base64
  a.href = canvas.toDataURL('image/png')
  // 设定下载称号
  a.download = name
  // 点击触发下载
  a.click()
}
// 将svg转换为base64
function svgToBase64(svg) {
  return 'data:image/svg+xml,' + encodeURIComponent(svg.outerHTML)
}
// 初始化办法
function init(reg) {
  if (!reg) return
  document.getElementById('graphCt').innerHTML = ''
  let { parse, visualize, Raphael } = require('regulex')
  // 创立 Raphael 实例
  let paper = Raphael('graphCt', 0, 0)
  // 清空画布
  paper.clear()
  try {
    // 烘托正则图表
    visualize(parse(reg.source), reg.flags, paper, props.mode)
  } catch (e) {
    if (e instanceof parse.RegexSyntaxError) {
      let msg = ['Error:' + e.message, '']
      if (typeof e.lastIndex === 'number') {
        msg.push(reg)
        msg.push(new Array(e.lastIndex).join('-') + '^')
      }
      console.error(msg.join('n'))
    } else {
      throw e
    }
  }
}
</script>
<template>
  <div
    v-if="props.modelValue"
    id="graphCt"
    class="w-full overflow-x-auto overflow-y-hidden"
  ></div>
</template>

简单介绍一下代码大约逻辑,该组件接纳两个参数,一个 modelValue,即 v-model 传入的是正则目标,另一是 mode 即办法字符串。

咱们运用 Vuewatch 办法监听了这两个参数,组件内部监听到参数改动时,会从头履行初始化办法。

该组件的中心便是初始化 init 办法,接下来看下这块的代码片段:

// 初始化办法
function init(reg) {
  if (!reg) return
  document.getElementById('graphCt').innerHTML = ''
  let { parse, visualize, Raphael } = require('regulex')
  // 创立 Raphael 实例
  let paper = Raphael('graphCt', 0, 0)
  // 清空画布
  paper.clear()
  try {
    // 烘托正则图表
    visualize(parse(reg.source), reg.flags, paper, props.mode)
  } catch (e) {
    if (e instanceof parse.RegexSyntaxError) {
      let msg = ['Error:' + e.message, '']
      if (typeof e.lastIndex === 'number') {
        msg.push(reg)
        msg.push(new Array(e.lastIndex).join('-') + '^')
      }
      console.error(msg.join('n'))
    } else {
      throw e
    }
  }
}

咱们在 Template 模板中创立了一个 div 元素,给它了个 IDgraphCt,这个元素便是将来 SVG 图表烘托的目标画布元素了。

上面初始化办法内咱们获取到画布元素,然后导出 regulex 包的三个中心办法。

先运用 Raphael 办法,传入画布 ID 和宽高创立 SVG 绘图库实例,这儿宽高传入 0 即可,由于还没有内容。紧接着调用绘图库实例中的 clear 办法,先清空一下画布,防止重复烘托。

然后运用 visualize 办法,榜首个参数传入的是由 regulex 库的 parse 办法创立的正则解析目标,第二个参数传入正则目标的修饰符字符串,第三个参数传入上面创立的绘图库实例,第四个参数即传入咱们的 mode 办法字符串就能够了。

每当正则目标或许办法字符串改动时就会从头履行 init 办法烘托画布。

上面代码 catch 代码块中咱们做了一些过错处理,不必在意。OK,一个正则可视化烘托组件中心就写好了,那由于咱们还要有一个可视化图表转图片下载的功用,所以还需求在组件内部写一下画布转图片下载办法露出出去。

中心代码片段如下:

// 导出图片
function exportImage() {
  let ratio = window.devicePixelRatio || 1
  // 获取SVG画布元素
  let svg = window.graphCt.getElementsByTagName('svg')[0]
  // 获取SVG画布宽高
  let w = svg.clientWidth
  let h = svg.clientHeight
  // 创立Image目标
  let img = new Image()
  img.width = w
  img.height = h
  img.setAttribute('src', svgToBase64(svg))
  // 创立Canvas目标
  let canvas = document.createElement('canvas')
  canvas.width = w * ratio
  canvas.height = h * ratio
  let ctx = canvas.getContext('2d')
  ctx.setTransform(ratio, 0, 0, ratio, 0, 0)
  // Image目标onload事情
  img.onload = function () {
    // 给一个背景色彩,否则导出的图片是通明的
    let bgColor = props.mode === 'dark' ? '#2e2e31' : '#f2f3f5'
    ctx.fillStyle = bgColor
    // 制作Image目标到canvas画布
    ctx.fillRect(0, 0, canvas.width, canvas.height)
    ctx.drawImage(img, 0, 0)
    // canvas画布转 Base64 并导出 png
    canvasToPictureDownload(canvas, `regexp_visualization_${Date.now()}`)
  }
}
// canvas转图片下载
function canvasToPictureDownload(canvas, name) {
  let a = document.createElement('a')
  // 将画布内的信息导出为png图片数据 Base64
  a.href = canvas.toDataURL('image/png')
  // 设定下载称号
  a.download = name
  // 点击触发下载
  a.click()
}
// 将svg转换为base64
function svgToBase64(svg) {
  return 'data:image/svg+xml,' + encodeURIComponent(svg.outerHTML)
}
defineExpose({
  exportImage
})

这块总共有三个办法,简单解说下,由于咱们烘托的图表是 SVG 图表,所以 exportImage 导出图片办法中咱们经过画布 ID 先直接获取到 SVG 元素,拿到 SVG 元素之后,咱们需求将 SVG 先转成 Base64 格局(即 svgToBase64 办法),拿到了 SVGBase64 数据,咱们直接 new Image 创立一个 Image 目标把这个 Base64 数据作为该目标 href 特点值写入,Image 目标宽高获取一下 SVG 画布,运用 SVG 画布的宽高即可。

接着咱们创立一个 Canvas 画布目标,在 Image 目标加载完成后,把 Image 目标制作到 Canvas 画布中,最终调用 canvasToPictureDownload 办法,该办法即把 Canvas 画布转成 png 格局的 Base64 数据,然后 JS 创立一个 a 标签写入 href 值,再给 a 标签目标写入 download 特点,其特点值为下载的默许图片名,这儿咱们给了一个时刻戳组成的字符串,最终手动触发下 click 点击事情触发下载即可。

最终咱们在可视化组件中露出出了图片下载办法,如下:

defineExpose({
  exportImage
})
// ...

PS: 由于组件咱们时运用 setup 办法写的,所以想要露出办法,有必要运用 defineExpose 办法手动露出。

还有之前文章也说过:由于咱们运用了 setup,那试想 setupVue 组件中的哪个时期才会调用?它是在组件调用时被调用,而且是在组件的 beforeCreate 生命周期之前履行,也便是想要拿到 setup 中的数据,那至少得等组件调用了才行,组件还没调用的时候,是肯定获取不了 setup 中特点或办法的。

这样的话咱们在父级组件中拿到组件实例目标后就能够调用其导出的 exportImage 办法了!!

OK,组件写完了,来在正则页面 RegularPage/index.vue 文件中运用一下。

由于这儿要用到办法特点,所以直接在文件最初,需求导入咱们上一篇文章中创立的 pinia system 模块中的当时办法目标,如下:

import { useSystemStore } from '@/stores/system'
const { currentMode } = storeToRefs(useSystemStore())

这儿有个问题哈,咱们这儿 visualize 办法的参数 mode 只支持传入字符串 dark 或许 light,可是咱们的当时办法目标中 name 特点在主动办法下会是 auto ,这就会存在问题,咱们需求获取到 auto 办法下到底是 dark 仍是 light

所以咱们需求凭借 VueUse 的另一个办法 useDark 经过和之前 useColorMode 办法相同的特点装备来获取当时办法,其实 useDark 办法内部也是靠 useColorMode 办法完成的,在 useDark 办法源码中,会运用 useColorMode 传入装备项生成一个呼应式的 mode 特点,而 useDark 办法回来的 isDark 特点,其实便是一个计算特点,其源码中心大至如下:

export function useDark(options = {}) {
  const mode = useColorMode({
    ...options,
    // ...
  })
  const isDark = computed({
    get() {
      return mode.value === 'dark'
    },
    set(v) {
      // ...
    },
  })
  return isDark
}

看,是不是很简单,跑题了,回到正题哈。咱们来写下当时办法获取的代码:

// 是否是漆黑办法
const isDark = useDark({
  selector: 'body',
  attribute: 'arco-theme',
  valueDark: 'dark',
  valueLight: 'light',
  initialValue: currentMode.value?.name,
  storageKey: null
})
// 当时色彩办法
const mode = computed(() => {
  if (currentMode.value?.name == 'auto') {
    return isDark.value ? 'dark' : 'light'
  }
  return currentMode.value?.name
})

OK,这样的话每当 pinia 中大局的当时办法目标改动,咱们这个页面的办法值也会跟着改动,而且当大局办法值为 auto 时,咱们这儿也能够知道它到底是 dark 仍是 light

拿到了当时办法数据,接下来咱们在 Template 模板就能够运用下组件了。

还记得开端时咱们把整个正则的页面分为上中下三个模块吗,中心那个模块便是放可视化图表的,咱们直接把可视化组件代码塞到中心模块的 div 中,匿名组件的组件名默许便是文件名哈,组件还要做校验,只有 visualization 特点为 true 时才加载可视化组件(reg 仍是咱们之前写的那个正则目标哈):

<div class="mt-20px bg-[var(--color-fill-2)]">
  <RegularVisualization
    ref="regVisualizationRef"
    v-if="visualization"
    v-model="reg"
    :mode="mode"
  />
</div>

趁热打铁,再来给下载可视化图片的按钮写一下,相同和上面其他按钮差不多,放在是否显现可视化预览按钮右边就行,不过这个下载图片的按钮也需求校验,只有当 reg 正则目标存在且是否显现可视化预览的 visualization 特点值为 true 时才显现,而且咱们给按钮加了一个点击事情 exportRegVisualizationToImg,即下载图片办法,代码片段如下:

<a-tooltip content="下载解析图片" position="bottom">
  <a-button
    :size="size"
    class="ml-10px"
    v-if="visualization && reg"
    @click="exportRegVisualizationToImg"
  >
    <template #icon>
      <icon-icon-park-outline-down-picture />
    </template>
  </a-button>
</a-tooltip>

OK,上面咱们写可视化组件时,在可视化组件标签中还写了个 ref 特点,值为 regVisualizationRef,那咱们能够经过 ref 特点来获取一下可视化组件实例,和 Vue2refs 类似,可是在 setup 中咱们需求创立一个和组件中 ref 特点值 regVisualizationRef 同名的 ref() 数据,默许是 null,当组件烘托之后此特点值即组件实例目标。

然后咱们补下下载按钮的点击事情内容,鄙人载按钮点击事情中调用可视化组件实例抛出的下载图片办法,如下:

// 可视化组件实例
const regVisualizationRef = ref(null)
// 导出正则可视化图片
const exportRegVisualizationToImg = () => {
  regVisualizationRef.value && regVisualizationRef.value?.exportImage()
}

可视化模块到此差不多就写完了,看下完好代码:

<script setup>
import * as regPresetsObj from '@/utils/regexp.js'
import { useSystemStore } from '@/stores/system'
const { currentMode } = storeToRefs(useSystemStore())
// 巨细
const size = ref('large')
// 正则表达式目标
const reg = ref(null)
// 正则表达式字符串
const regStr = ref('')
// 正则表达式修饰符
const regFlag = ref([])
watchEffect(() => {
  try {
    reg.value = regStr.value
      ? new RegExp(regStr.value, regFlag.value.join(''))
      : null
  } catch (err) {
    reg.value = null
  }
})
// 预设主动查找
const auto = ref(true)
// 正则预设列表
const regPresets = ref([])
// 正则预设匹配办法
const regPresetMatch = value => {
  if (value && auto.value) {
    regPresets.value = [...Object.values(regPresetsObj)]
      .filter(r => r.name.includes(value) || r.source.includes(value))
      .map(v => {
        return {
          label: v.name,
          regexp: v,
          value: v.source
        }
      })
  } else {
    regPresets.value = []
  }
}
// 正则预设选中办法
const regPresetSelect = r => {
  let selected = regPresets.value.find(v => v.value === r)
  if (selected) regFlag.value = selected.regexp.flags.split('')
}
// 待匹配字符串
const matchStr = ref('')
// 匹配成果
const matchingResults = computed(
  () => matchStr.value.match(reg.value)?.filter(v => v) || []
)
// 匹配成果格局化
const matchingFormat = computed(() => {
  let res = '',
    str = matchStr.value
  let n = 1
  matchingResults.value.forEach(v => {
    if (n > 4) n = 1
    let matchStr = str.substr(0, v.length + str.indexOf(v))
    str = str.substr(v.length + str.indexOf(v))
    res += matchStr.replace(
      v,
      `<span class="matching matching${n}">${v}</span>`
    )
    n++
  })
  return res + str
})
// 可视化
const visualization = ref(true)
// 可视化组件实例
const regVisualizationRef = ref(null)
// 是否是漆黑办法
const isDark = useDark({
  selector: 'body',
  attribute: 'arco-theme',
  valueDark: 'dark',
  valueLight: 'light',
  initialValue: currentMode.value?.name,
  storageKey: null
})
// 当时色彩办法
const mode = computed(() => {
  if (currentMode.value?.name == 'auto') {
    return isDark.value ? 'dark' : 'light'
  }
  return currentMode.value?.name
})
// 导出正则可视化图片
const exportRegVisualizationToImg = () => {
  regVisualizationRef.value && regVisualizationRef.value?.exportImage()
}
</script>
<template>
  <div class="max-w-1200px w-full p-20px box-border">
    <div class="flex justify-start items-center">
      <a-tooltip
        :content="`点击${auto ? '封闭' : '敞开'}预设正则主动匹配`"
        position="bottom"
      >
        <a-button :size="size" @click="auto = !auto">
          <template #icon>
            <icon-material-symbols-astrophotography-auto
              :class="auto ? 'text-[rgb(var(--arcoblue-6))]' : ''"
            />
          </template>
        </a-button>
      </a-tooltip>
      <a-auto-complete
        class="ml-10px"
        v-model="regStr"
        :data="regPresets"
        @search="regPresetMatch"
        @select="regPresetSelect"
        :placeholder="`请输入正则表达式${
          auto ? '或输入文字挑选主动匹配的预设正则' : ''
        }`"
        allow-clear
        :size="size"
      >
        <template #prepend>
          <a-tooltip
            :content="reg ? '正则表达式正确' : '正则表达式有误'"
            position="bottom"
            mini
          >
            <icon-jam-triangle-danger-f
              class="text-[rgb(var(--orange-6))]"
              v-if="!reg"
            />
            <icon-mdi-hand-okay class="text-[rgb(var(--green-6))]" v-else />
          </a-tooltip>
          <span class="ml-5px">/</span>
        </template>
        <template #append>
          <span> /{{ reg?.flags || regFlag?.join('') }}</span>
        </template>
      </a-auto-complete>
      <a-popover title="挑选修饰符" position="bl">
        <a-button :size="size" class="ml-10px">
          <template #icon>
            <icon-mdi-filter-multiple
              :class="regFlag.length > 0 ? 'text-[rgb(var(--arcoblue-6))]' : ''"
            />
          </template>
        </a-button>
        <template #content>
          <div class="min-w-200px">
            <a-checkbox-group
              :size="size"
              v-model="regFlag"
              direction="vertical"
            >
              <a-checkbox value="g"> -g:大局匹配 </a-checkbox>
              <a-checkbox value="i"> -i:疏忽巨细写</a-checkbox>
              <a-checkbox value="m"> -m:多行匹配</a-checkbox>
              <a-checkbox value="s"> -s:特别字符. 包括换行符</a-checkbox>
            </a-checkbox-group>
          </div>
        </template>
      </a-popover>
      <a-tooltip
        :content="`点击${visualization ? '封闭' : '敞开'}可视化解析`"
        position="bottom"
      >
        <a-button
          :size="size"
          class="ml-10px"
          @click="visualization = !visualization"
        >
          <template #icon>
            <icon-ic-sharp-visibility
              class="text-[rgb(var(--arcoblue-6))]"
              v-if="visualization"
            />
            <icon-ic-sharp-visibility-off v-else />
          </template>
        </a-button>
      </a-tooltip>
      <a-tooltip content="下载解析图片" position="bottom">
        <a-button
          :size="size"
          class="ml-10px"
          v-if="visualization && reg"
          @click="exportRegVisualizationToImg"
        >
          <template #icon>
            <icon-icon-park-outline-down-picture />
          </template>
        </a-button>
      </a-tooltip>
    </div>
    <div class="mt-20px bg-[var(--color-fill-2)]">
      <RegularVisualization
        ref="regVisualizationRef"
        v-if="visualization"
        v-model="reg"
        :mode="mode"
      />
    </div>
    <div class="w-full mt-20px">
      <a-textarea
        v-model="matchStr"
        default-value=""
        placeholder="请输入要匹配的字符串"
        :auto-size="{
          minRows: 3,
          maxRows: 6
        }"
      />
      <div
        class="w-full min-h-100px bg-[var(--color-fill-2)] whitespace-pre-wrap break-words indent-0 leading-22px mt-20px px-12px py-4px box-border"
        v-html="matchingFormat || '无匹配成果'"
      ></div>
      <div
        v-if="matchingResults.length > 0"
        class="w-full min-h-100px bg-[var(--color-fill-2)] whitespace-pre-wrap break-words indent-0 leading-22px mt-20px px-12px py-4px box-border"
      >
        <div>共 {{ matchingResults.length }} 处匹配:</div>
        <div v-for="(res, i) of matchingResults" :key="i">{{ res }}</div>
      </div>
    </div>
  </div>
</template>
<style>
.matching {
  @apply px-0px rounded-4px box-border;
}
.matching1 {
  @apply bg-[rgb(var(--arcoblue-2))];
}
.matching2 {
  @apply bg-[rgb(var(--green-2))];
}
.matching3 {
  @apply bg-[rgb(var(--orange-2))];
}
.matching4 {
  @apply bg-[rgb(var(--red-2))];
}
</style>

保存改写页面,看下作用。

dark 办法下:

保姆级Vue3+Vite项目实战正则在线校验东西

light 办法下:

保姆级Vue3+Vite项目实战正则在线校验东西

正则仿制

最终来一个仿制的小功用,便是最初咱们说的,正则存在时,榜首行最右边要显现一个 copy 按钮,用来把当时正则仿制到剪切板。

代码很简单,仍是在 RegularPage/index.vue 正则页面,script setup 中新增 JS

const { copy, text, isSupported } = useClipboard({ source: reg })
// Copy正则到剪贴板
const regCopy = async () => {
  if (!isSupported.value) return AMessage.info('不支持仿制')
  await copy()
  AMessage.success('仿制成功:' + text.value)
}

接着和之前的按钮相同,在 Template 模板的上模块最右侧加个仿制图标按钮,在 reg 存在时显现,新增模板内容如下:

<a-tooltip content="点击 copy 正则表达式" position="bottom">
  <a-button :size="size" class="ml-10px" v-if="reg" @click="regCopy">
    <template #icon>
      <icon-icon-park-solid-copy />
    </template>
  </a-button>
</a-tooltip>

仍是凭借了 VueUseuseClipboard 办法哈,这个办法比较简单,所以咱们看看就行,或许去刷下文档,仿制到剪切板有兼容性问题以及浏览器策略问题( 生产环境下 http 不行仿制,得 https 才行,开发环境没事)。

咱们运用该办法回来的 isSupported 特点判断下当时页面是否支持仿制到剪切板。

回来的 copy 办法就如办法名用来做仿制。

回来的 text 特点是仿制成功后剪切板的值。

先看下仿制的作用:

保姆级Vue3+Vite项目实战正则在线校验东西

到此正则校验页面功用就先告一段落了,完好代码鄙人一末节看吧

完好代码

<script setup>
import * as regPresetsObj from '@/utils/regexp.js'
import { useSystemStore } from '@/stores/system'
const { currentMode } = storeToRefs(useSystemStore())
// 巨细
const size = ref('large')
// 正则表达式目标
const reg = ref(null)
// 正则表达式字符串
const regStr = ref('')
// 正则表达式修饰符
const regFlag = ref([])
watchEffect(() => {
  try {
    reg.value = regStr.value
      ? new RegExp(regStr.value, regFlag.value.join(''))
      : null
  } catch (err) {
    reg.value = null
  }
})
// 预设主动查找
const auto = ref(true)
// 正则预设列表
const regPresets = ref([])
// 正则预设匹配办法
const regPresetMatch = value => {
  if (value && auto.value) {
    regPresets.value = [...Object.values(regPresetsObj)]
      .filter(r => r.name.includes(value) || r.source.includes(value))
      .map(v => {
        return {
          label: v.name,
          regexp: v,
          value: v.source
        }
      })
  } else {
    regPresets.value = []
  }
}
// 正则预设选中办法
const regPresetSelect = r => {
  let selected = regPresets.value.find(v => v.value === r)
  if (selected) regFlag.value = selected.regexp.flags.split('')
}
// 待匹配字符串
const matchStr = ref('')
// 匹配成果
const matchingResults = computed(
  () => matchStr.value.match(reg.value)?.filter(v => v) || []
)
// 匹配成果格局化
const matchingFormat = computed(() => {
  let res = '',
    str = matchStr.value
  let n = 1
  matchingResults.value.forEach(v => {
    if (n > 4) n = 1
    let matchStr = str.substr(0, v.length + str.indexOf(v))
    str = str.substr(v.length + str.indexOf(v))
    res += matchStr.replace(
      v,
      `<span class="matching matching${n}">${v}</span>`
    )
    n++
  })
  return res + str
})
// 可视化
const visualization = ref(true)
// 可视化组件实例
const regVisualizationRef = ref(null)
// 是否是漆黑办法
const isDark = useDark({
  selector: 'body',
  attribute: 'arco-theme',
  valueDark: 'dark',
  valueLight: 'light',
  initialValue: currentMode.value?.name,
  storageKey: null
})
// 当时色彩办法
const mode = computed(() => {
  if (currentMode.value?.name == 'auto') {
    return isDark.value ? 'dark' : 'light'
  }
  return currentMode.value?.name
})
// 导出正则可视化图片
const exportRegVisualizationToImg = () => {
  regVisualizationRef.value && regVisualizationRef.value?.exportImage()
}
const { copy, text, isSupported } = useClipboard({ source: reg })
// Copy正则到剪贴板
const regCopy = async () => {
  if (!isSupported.value) return AMessage.info('不支持仿制')
  await copy()
  AMessage.success('仿制成功:' + text.value)
}
</script>
<template>
  <div class="max-w-1200px w-full p-20px box-border">
    <div class="flex justify-start items-center">
      <a-tooltip
        :content="`点击${auto ? '封闭' : '敞开'}预设正则主动匹配`"
        position="bottom"
      >
        <a-button :size="size" @click="auto = !auto">
          <template #icon>
            <icon-material-symbols-astrophotography-auto
              :class="auto ? 'text-[rgb(var(--arcoblue-6))]' : ''"
            />
          </template>
        </a-button>
      </a-tooltip>
      <a-auto-complete
        class="ml-10px"
        v-model="regStr"
        :data="regPresets"
        @search="regPresetMatch"
        @select="regPresetSelect"
        :placeholder="`请输入正则表达式${
          auto ? '或输入文字挑选主动匹配的预设正则' : ''
        }`"
        allow-clear
        :size="size"
      >
        <template #prepend>
          <a-tooltip
            :content="reg ? '正则表达式正确' : '正则表达式有误'"
            position="bottom"
            mini
          >
            <icon-jam-triangle-danger-f
              class="text-[rgb(var(--orange-6))]"
              v-if="!reg"
            />
            <icon-mdi-hand-okay class="text-[rgb(var(--green-6))]" v-else />
          </a-tooltip>
          <span class="ml-5px">/</span>
        </template>
        <template #append>
          <span> /{{ reg?.flags || regFlag?.join('') }}</span>
        </template>
      </a-auto-complete>
      <a-popover title="挑选修饰符" position="bl">
        <a-button :size="size" class="ml-10px">
          <template #icon>
            <icon-mdi-filter-multiple
              :class="regFlag.length > 0 ? 'text-[rgb(var(--arcoblue-6))]' : ''"
            />
          </template>
        </a-button>
        <template #content>
          <div class="min-w-200px">
            <a-checkbox-group
              :size="size"
              v-model="regFlag"
              direction="vertical"
            >
              <a-checkbox value="g"> -g:大局匹配 </a-checkbox>
              <a-checkbox value="i"> -i:疏忽巨细写</a-checkbox>
              <a-checkbox value="m"> -m:多行匹配</a-checkbox>
              <a-checkbox value="s"> -s:特别字符. 包括换行符</a-checkbox>
            </a-checkbox-group>
          </div>
        </template>
      </a-popover>
      <a-tooltip
        :content="`点击${visualization ? '封闭' : '敞开'}可视化解析`"
        position="bottom"
      >
        <a-button
          :size="size"
          class="ml-10px"
          @click="visualization = !visualization"
        >
          <template #icon>
            <icon-ic-sharp-visibility
              class="text-[rgb(var(--arcoblue-6))]"
              v-if="visualization"
            />
            <icon-ic-sharp-visibility-off v-else />
          </template>
        </a-button>
      </a-tooltip>
      <a-tooltip content="下载解析图片" position="bottom">
        <a-button
          :size="size"
          class="ml-10px"
          v-if="visualization && reg"
          @click="exportRegVisualizationToImg"
        >
          <template #icon>
            <icon-icon-park-outline-down-picture />
          </template>
        </a-button>
      </a-tooltip>
      <a-tooltip content="点击 copy 正则表达式" position="bottom">
        <a-button :size="size" class="ml-10px" v-if="reg" @click="regCopy">
          <template #icon>
            <icon-icon-park-solid-copy />
          </template>
        </a-button>
      </a-tooltip>
    </div>
    <div class="mt-20px bg-[var(--color-fill-2)]">
      <RegularVisualization
        ref="regVisualizationRef"
        v-if="visualization"
        v-model="reg"
        :mode="mode"
      />
    </div>
    <div class="w-full mt-20px">
      <a-textarea
        v-model="matchStr"
        default-value=""
        placeholder="请输入要匹配的字符串"
        :auto-size="{
          minRows: 3,
          maxRows: 6
        }"
      />
      <div
        class="w-full min-h-100px bg-[var(--color-fill-2)] whitespace-pre-wrap break-words indent-0 leading-22px mt-20px px-12px py-4px box-border"
        v-html="matchingFormat || '无匹配成果'"
      ></div>
      <div
        v-if="matchingResults.length > 0"
        class="w-full min-h-100px bg-[var(--color-fill-2)] whitespace-pre-wrap break-words indent-0 leading-22px mt-20px px-12px py-4px box-border"
      >
        <div>共 {{ matchingResults.length }} 处匹配:</div>
        <div v-for="(res, i) of matchingResults" :key="i">{{ res }}</div>
      </div>
    </div>
  </div>
</template>
<style>
.matching {
  @apply px-0px rounded-4px box-border;
}
.matching1 {
  @apply bg-[rgb(var(--arcoblue-2))];
}
.matching2 {
  @apply bg-[rgb(var(--green-2))];
}
.matching3 {
  @apply bg-[rgb(var(--orange-2))];
}
.matching4 {
  @apply bg-[rgb(var(--red-2))];
}
</style>

假如咱们跟着写了的话,不知道咱们有没有亲身体验到 ComponsitionAPI 的一个众所周知的小优点,上面 JS 代码中我把不同的小功用模块以空白行分开了,每个小模块用到的特点值、办法都在一块,不需求像 OptionsAPI 那样上下来回跳转去改东西。当然也或许没啥感觉,究竟这个页面功用很少,有点简单,没事儿,后边还有时机。

其实到现在为止咱们用了许多 VueUse 的办法,真实是由于它很香啊,现在咱们还没有写过 hooks,当咱们后边写写 hooks 之后,你再回头看看 VueUse 库的一些办法,就会体会到 ComponsitionAPIhooks 的精华了。没事的话能够扒拉一下 VueUse 库,没用过看看文档了解下,用过的挑其间一些 hooks 的源码学习学习,会有许多收获,VueUse 中每个 hooks 的源码并不杂乱也不多,适合入门学习。

增加路由跳转动画

咱们现在路由跳转太生硬了,所以咱们运用 Vue 内置的 transition 组件来做个过渡动画。

官方文档:transition 组件

没用过的同学赶紧刷下文档,咱们这儿直接写了,修正可切换布局组件 DefaultLayoutSidebarLayout,找到 router-view 标签,修正如下:

<!-- 修正前 -->
<router-view v-slot="{ Component }">
  <component :is="Component" />
</router-view>
<!-- 修正后 -->
<router-view v-slot="{ Component }">
  <transition name="fade-x">
    <component :is="Component" />
  </transition>
</router-view>

留意,两个布局中都需求改。

transition 组件 name 特点咱们设置成 fade-x,接下来写下进入离开的过渡款式,加个平移淡入就能够了。

这个过渡款式在两个组件中都能够用到,而且后边说不定哪里也能用到,咱们给它写到公共款式中。

src/assets 文件夹下新增 css/index.css 文件,暂时先把公共款式写在这个文件里,如下:

/* 路由过度动画 */
.fade-x-enter-active {
  transition: all 0.3s ease-out;
}
.fade-x-leave-active {
  transition: all 0;
}
.fade-x-enter-from {
  transform: translateX(20px);
  opacity: 0;
}
.fade-x-leave-from {
  opacity: 0;
}

main.js 进口文件中引进下公共款式:

// 公共款式
import '@/assets/css/index.css'
// ...

OK,保存改写页面,跳转下路由就能够看到切换路由时有个圆润的平移淡入过渡动画了。

部部部布置下

由于总算写到了一个功用页面,所以能够布置上去预览了!

没有布置教程,由于现在这便是一个静态页面,所以我这边直接运用 docker nginx 镜像布置了一个 Web 服务,其实啥都没有,直接 build 构建下,然后把构建好的静态资源扔上去就能够了,咱们能够访问看看啥的,服务器不太行,略微有点慢,主动化布置的教程得后边项目陆续变得杂乱了再写。

哦,还没买域名,暂时就先用个二级域名吧!!!

预览地址:toolsdog.isboyjc.com

嗯。。。也还没来得及搞证书,So,不是 https 网站,所以碍于浏览器安全策略现在是不支持咱们上面写的仿制到剪切板的功用,回头我有空搞下证书就好了!

写在最终

嗯,就这样!

截止本文的代码已经打了 Tag 发布,可下载检查:

toolsdog tag v0.0.4-dev

项目 GitHub 地址

谢阅,如有过错请评论纠正,有什么疑问或许不理解的当地都能够私信咨询我,由于不经常写实战文章,也为了不同程度同学都能够看下去,文章或许略微有些烦琐,见谅,再次欢迎重视专栏 Vue3实战系列 ,点赞重视不迷路,回见 !

本文为稀土技能社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!