從 Vue parseHTML 來學習正規表示式

Yutoti_三石發表於2021-07-05

寫作本文的起源在於,在分析 Vue 原始碼中 parseHTML 方法時,發現網上對其中正則的解析文章較少,找到的幾篇文章也有些語焉不詳。於是靜下心逐個表示式分析了其中的正則,以備檢視。

常見正則規則可參見附錄 1,Vue parseHTML 正則所用規則均可在其中找到定義。

Vue parseHTML 中所用的所有正則如下:

const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i
const comment = /^<!\--/
const conditionalComment = /^<!\[/

接下來一個個通過拆解表示式,來分析上述正則規則。

attribute

const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/

分析其結構:

  1. ^\s* 匹配 0 至多個以空白字元開頭的字串空白字元的部分

  2. 捕獲組:([^\s"'<>\/=]+) 匹配並捕獲 1 至多次除 空白字元 " ' < > / = 以外的所有字元

  3. 非捕獲組:(?:\s(=)\s(?:"(["]*)"+|'([']*)'+|([^\s"'=<>`]+)))?

  • \s* 匹配 0 至多個空白字元
  • 捕獲組:(=) 匹配並捕獲 =
  • \s* 匹配 0 至多個空白字元
  • 非捕獲組:(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>\]+))`
    • "([^"]*)"+
      • " 匹配 "
      • ([^"]*) 匹配並捕獲 0 至多個除 " 外的字元
      • "+ 匹配 1 至多次 "
    • '([^']*)'+
      • ' 匹配 '
      • ([^']*) 匹配並捕獲 0至多個除 ' 外的字元
      • '+ 匹配 1 至多次 '
    • ([^\s"'=<>`]+) 匹配並捕獲 1 至多次除 空白字元 " ' = < > ` 外的字元
  • ? 匹配 3 中非捕獲組 0 次或 1 次

小結

attribute 表示式匹配的是:

  1. 以 0 至多個空白字元開頭;

  2. 緊接著 1 至多個除 空白字元 " ' < > / = 以外的字元;

  3. 緊接著 0 至多個空白字元;

  4. 緊接著 =

  5. 緊接著 0 至多個空白字元;

  6. 緊接著匹配 0 次或 1 次:

    (1) " + 0 至多個除 " 外的字元 + "

    (2) 或 ' + 0 至多個除 ' 外的字元 + '

    (3) 或 1 至多次除 空白字元 " ' = < > ` 外的字元

例如:

<div id="mydiv" class="myClass" style="color: #ff0000" >

在 Vue 的 parseHTML 時,就能將 id="mydiv"class="myClass"style="color: #ff0000"提取出來。

dynamicArgAttribute

const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/

分析其結構:

  1. ^\s* 匹配以 0 至多個空白字元開頭

  2. 捕獲組:((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)

  • 非捕獲組:(?:v-[\w-]+:|@|:|#) 匹配:

    (1)v- + 1 次或多次包括下劃線在內的任意單詞字元 + :

    (2)或 @

    (3)或 :

    (4)或 #

  • \[[^=]+\] 匹配 以 [ + 1 次或多次除 = 外的所有字元 + ]

  • [^\s"'<>\/=]* 匹配 0 次或多次除 空白字元"'<>/= 以外的字元

  1. 非捕獲組:(?:\s(=)\s(?:"(["]*)"+|'([']*)'+|([^\s"'=<>`]+)))?

已在 attribute 章節分析過。

小結

dynamicArgAttribute 用於匹配:

  1. 以 0 至多個空白字元開頭

  2. 緊接著:

    (1)v- + 1 次或多次包括下劃線在內的任意單詞字元 + :

    (2)或 @

    (3)或 :

    (4)或 #

  3. 緊接著以 [ + 1 次或多次除 = 外的所有字元 + ]

  4. 匹配 0 次或多次除 空白字元"'<>/= 以外的字元

  5. 緊接著 0 至多個空白字元;

  6. 緊接著 =

  7. 緊接著 0 至多個空白字元;

  8. 緊接著匹配 0 次或 1 次:

    (1) " + 0 至多個除 " 外的字元 + "

    (2) 或 ' + 0 至多個除 ' 外的字元 + '

    (3) 或 1 至多次除 空白字元 " ' = < > ` 外的字元

例如:

<a v-bind:[attributeName]="url"> ... </a>
<a v-on:[eventName]="doSomething"> ... </a>

在 Vue 的 parseHTML 時,就能將 v-bind:[attributeName]="url" 這種動態引數提取出來。

ncname

const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`

首先看 unicodeRegExp

const unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/

定義了一系列合法字元,通過 Unicode 字符集範圍匹配。

unicodeRegExp.source 用於拿到正規表示式 unicodeRegExp 的字串。

ncname 即是一系列合法字元的集合。

qnameCapture

const qnameCapture = `((?:${ncname}\\:)?${ncname})`

表示匹配 xxx:xxxxxx 模式的字元。

startTagOpen

const startTagOpen = new RegExp(`^<${qnameCapture}`)

startTagOpen 可匹配標籤開始部分,即:<xxx:xxx<xxx 的模式。

<xxx:xxx 代表的是帶名稱空間的 html 標籤,文可參見這裡,這種型別的標籤主要作用是可以指定標籤的名稱空間,避免衝突。Vue 對這類的標籤也做了解析。

如:<div <math:div

startTagClose

const startTagClose = /^\s*(\/?)>/

^\s*(\/?)> 匹配以 0 至多個空白字元開頭,接 0 或 1 個 / ,緊接 > 的字串。

如: />

endTag

const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)

匹配以 </ 開頭,後接合法字元,後接 0 至多個除 > 以外的任何字元,最後接 >

如:</div>

doctype

const doctype = /^<!DOCTYPE [^>]+>/i

匹配以 <!DOCTYPE 開頭,後接 1 至多次除 > 外的所有字元,後接 >。注意該匹配模式不區分大小寫。

如:<!DOCTYPE html>

comment

const comment = /^<!\--/

匹配以 <!-- 開頭的字串。

如:<!--STATUS OK-->

conditionalComment

const conditionalComment = /^<!\[/

匹配以 <![ 開頭的字串。條件註釋主要用於做瀏覽器相容等,文可參見這裡

總結

本文以 Vue 原始碼中 parseHTML 方法為例,分析了其中定義的正規表示式,將常見的正則規則做了梳理,同時可以備查 parseHTML 方法的正則規則,方便後續繼續分析該方法。

附錄 1 常見正則規則

正則裡的特殊字元

* ? + . [ ] ( ) { } | ^ $,共 13 個。
  1. * 表示匹配前面字元(或括號括起來的表示式,或方括號括起來的字符集)0 次或 多 次;
  2. ?(1)? 表示匹配前面字元(或括號括起來的表示式,或方括號括起來的字符集)0 次或 1 次;(2)? 緊跟在其它任何一個限制符(如 * + 等)後時,匹配模式為非貪婪,儘可能少地匹配;
  3. + 表示匹配前面字元(或括號括起來的表示式,或方括號括起來的字符集)1 次或 多 次;
  4. . 表示匹配除換行符以外的任意字元 1 次;
  5. [] 字符集,表示匹配方括號內字元中的其中一個,其中的特殊字元會被當做普通字元處理;
  6. () 捕獲組,表示匹配其中的子表示式;
  7. {n,m} 匹配前面表示式最少 n 次,最多 m 次
  8. | 或,常用在捕獲組中;
  9. ^ 匹配字串的開始位置
  10. $ 匹配字串的結束位置

表示式 () 中常見寫法

  1. (pattern) 匹配 pattern 並獲取該匹配;
  2. (?:pattern) 匹配 pattern 但不獲取該匹配;
  3. pattern1(?=pattern2) 匹配 pattern1 後面跟 pattern2 的字串,不獲取該匹配
  4. pattern1(?!pattern2) 匹配 pattern1 後面不跟 pattern2 的字串,不獲取該匹配
  5. (?<=pattern2)pattern1 匹配 pattern1 前面為 pattern2 的字串,不獲取該匹配
  6. (?<!pattern2)pattern1 匹配 pattern1 前面不為 pattern2 的字串,不獲取該匹配

字符集 [] 常見寫法

  1. [x|y] 匹配 x 或 y,可為字串
  2. [xyz] 匹配 x 或 y 或 z 的字元
  3. [^xyz] 匹配 非 x 或 y 或 z 的字元
  4. [a-z] 匹配 a 到 z 的任意小寫字元

常見特殊字元

  1. \b 匹配單詞邊界
  2. \d 匹配一個數字字元
  3. \n 匹配一個換行符
  4. \r 匹配一個回車符
  5. \t 匹配製表符
  6. \s 匹配任何空白字元
  7. \w 匹配包括下劃線的任何單詞字元

相關文章