前言
上一节笼统介绍了 Vue 进行模板编译的过程,但其实其背后的复杂度绝对是超呼想象,要当其为一个庞大的工程来处理。众所周知,归根结底,Vue 项目也是一 html5 页面,要对其进行模板编译,也就时相当于去把这个页面所有的内容都给爬下来对其中的标签、属性等等进行一步步取值处理。下面,就一步步来欣赏其中的精彩:
首先,我们先来欣赏一下 html-parser
以前已经有比较多的大牛做过这方便的工具并开源了出来。而 Vue 中使用的 html-parser(https://github.com/vuejs/vue/blob/dev/src/compiler/parser/html-parser.js)是源于经典的 Simple Html Parser(http://erik.eae.net/simplehtmlparser/simplehtmlparser.js)
先来对其用到的一些正则进行简单的理解吧
1、属性值的匹配
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
属性值的匹配:1)以0个或者多个空格开始。2)[分组]然后至少一个不能包含空格、双引号、单引号、左尖括号、右尖括号、斜杠和等于号的字符。3)0或者一组([非捕获分组]0或者多个空格、等于号、0或者多个空格、[非捕获分组]双引号、[分组]0或者多个非双引号的字符、至少一个双引号;[分组]或者单引号、0或者多个非单引号的字符、至少一个单引号;[分组]或者然后至少一个不能包含空格、双引号、单引号、左尖括号、右尖括号、等于号和反单引号)。
()表示捕获分组,()会把每个分组里的匹配的值保存起来,使用 $n(n 是一个数字,表示第 n 个捕获组的内容)
(?:)表示非捕获分组,和捕获分组唯一的区别在于,非捕获分组匹配的值不会保存起来
具体可以匹配到类似下面三种情况如下:
'@click="tipShow=!tipShow""dfdfdfdfdfdf"""'.match(attribute)
// => ["@click="tipShow=!tipShow""", "@click", "=", "tipShow=!tipShow", undefined, undefined, index: 0, input: "@click="tipShow=!tipShow""dfdfdfdfdfdf"""", groups: undefined]
" @click = 'tipShow=!tipShow''dfdfdfdfdfdf'''".match(attribute)
// => [" @click = 'tipShow=!tipShow''", "@click", "=", undefined, "tipShow=!tipShow", undefined, index: 0, input: " @click = 'tipShow=!tipShow''dfdfdfdfdfdf'''", groups: undefined]
'@click=tipShow=!tipShowdfdfdfdfdfdf"""""'.match(attribute)
// => ["@click=tipShow", "@click", "=", undefined, undefined, "tipShow", index: 0, input: "@click=tipShow=!tipShowdfdfdfdfdfdf"""""", groups: undefined]
2、动态属性值(如包今 @、:、v- 等等 Vue 专有的)的匹配
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
动态属性值的匹配:1)以 0 个或者多个空格开始。2)[分组][非捕获分组]以 v-、加上至少一个单词字符或者 -、加上 :;或者以 @;或者以 : 或者以 # 开始。接着是 [、然后是一个或者多个非等号字符、接着是],然后是至少一个不能包含空格、双引号、单引号、左尖括号、右尖括号、等于号和斜杠的字符。3)[非捕获分组]以 0 个或者多个空格开始,接着是 =,接着是 0 个或者多个空格,然后[非捕获分组]双引号、[分组] 0 或者多个非双引号的字符、至少一个双引号;[分组]或者单引号、0 或者多个非单引号的字符、至少一个单引号;[分组]或者然后至少一个不能包含空格、双引号、单引号、左尖括号、右尖括号、等于号和反单引号)。
具体可以匹配到类似下面的情况如下:
' v-click:[kk] = "tipShow=!tipShow"dfdfdfdf'.match(dynamicArgAttribute)
// => [" v-click:[kk] = "tipShow=!tipShow"", "v-click:[kk]", "=", "tipShow=!tipShow", undefined, undefined, index: 0, input: " v-click:[kk] = "tipShow=!tipShow"dfdfdfdf", groups: undefined]
' @[kk] = "tipShow=!tipShow"dfdfdfdf'.match(dynamicArgAttribute)
// => [" @[kk] = "tipShow=!tipShow"", "@[kk]", "=", "tipShow=!tipShow", undefined, undefined, index: 0, input: " @[kk] = "tipShow=!tipShow"dfdfdfdf", groups: undefined]
' :[kk] = "tipShow=!tipShow"dfdfdfdf'.match(dynamicArgAttribute)
// => [" :[kk] = "tipShow=!tipShow"", ":[kk]", "=", "tipShow=!tipShow", undefined, undefined, index: 0, input: " :[kk] = "tipShow=!tipShow"dfdfdfdf", groups: undefined]
' #[dfdfdfdfdfkk] = "tipShow=!tipShow"dfdfdfdf'.match(dynamicArgAttribute)
// => [" #[dfdfdfdfdfkk] = "tipShow=!tipShow"", "#[dfdfdfdfdfkk]", "=", "tipShow=!tipShow", undefined, undefined, index: 0, input: " #[dfdfdfdfdfkk] = "tipShow=!tipShow"dfdfdfdf", groups: undefined]
3、其它正则
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
// #7298: escape - to avoid being pased as HTML comment when inlined in page
const comment = /^<!\--/
const conditionalComment = /^<!\[/
首先,ncname 是一个必须以字母开头然后跟着一个或者多个下横线、点、数字、字母或者其他可支持的字符。
startTagOpen 是开标签的识别,它是由 < 开头,然后跟着[分组][零个或一个非捕获分组]的 ncname 和接着的冒号,然后是 ncname
具体可以匹配到类似下面的情况如下:
'<html>dfdfdfd</html>'.match(startTagOpen)
// => ["<html", "html", index: 0, input: "<html>dfdfdfd</html>", groups: undefined]
'<my-component v-bind="onTap">dfdfdfd</my-component>'.match(startTagOpen)
// => ["<my-component", "my-component", index: 0, input: "<my-component v-bind="onTap">dfdfdfd</my-component>", groups: undefined]
'<hi:hello:com v-bind="onTap">dfdfdfd</hi:hello:com>'.match(startTagOpen)
// => ["<hi:hello", "hi:hello", index: 0, input: "<hi:hello:com v-bind="onTap">dfdfdfd</hi:hello:com>", groups: undefined]
startTagClose就是对开始标签的结束进行匹配,以0个或者多个空格开始然后是 0 或者 1 个[分组]反斜杠 / 接着是右尖括号 >
具体可以匹配到类似下面的情况如下:
' />fdfdfdfdfd'.match(startTagClose)
// => [" />", "/", index: 0, input: " />fdfdfdfdfd", groups: undefined]
' >fdfdfdfdfd'.match(startTagClose)
[" >", "", index: 0, input: " >fdfdfdfdfd", groups: undefined]
endTag 就是匹配整个结束标签,能左尖括号 < 开始跟着是斜杠 /,然后是跟上面 startTagOpen 的 qnameCapture 一样,然后是 0 或者多个非右尖括号 >然后最后是结束符号右尖括号 >
具体可以匹配到类似下面的情况如下:
'</kjdkfjdfkdj>dfdfd'.match(endTag)
// => ["</kjdkfjdfkdj>", "kjdkfjdfkdj", index: 0, input: "</kjdkfjdfkdj>dfdfd", groups: undefined]
'</kjdkf:jdfkdj>dfdfd'.match(endTag)
// => ["</kjdkf:jdfkdj>", "kjdkf:jdfkdj", index: 0, input: "</kjdkf:jdfkdj>dfdfd", groups: undefined]
doctype 就比较简单,直接匹配 html5 页面开始的 doctype 声明标签
comment 也是非常简单,直接就匹配注释部分
conditionalComment 就是条件注释部分,这经常会出现在要对 ie 浏览器进行版本渲染的情况。
总结
通过这些正则,我们大概知道接下来准备要进行什么操作了,就是要把整个 html 源码由头到尾吃掉,然后把所有 tag,text,样式,JS 等内容全部爬下来,在整个爬的过程以回调(start,end, chars,comment共 4 个回调函数并带参数)的方式作为工具给 index.js 中的 parse
函数使用。下一节就来详细介绍一下 parseHTML
函数。