上篇我們了解了
PEG.js
的基礎使用,忘記的童鞋建議複習一下,對于本文的食用效果會更佳哦!
光說不練,等于白學。是以本文來實作一個編譯器(瞎搞、玩具、歡樂)。
需求
我們知道
Vue
的
template
不支援中文标簽名,比如下面這段代碼:
<下拉框 選中值="番茄" :資料="{
"list":[
{
"名稱": "🍅",
"id": "番茄"
},
{
"名稱": "🍌",
"id": "香蕉"
}
],
"total": 2
}">
<子元件></子元件>
</下拉框>
複制
使用 astexplorer[1] 生成的結果:
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAjM2EzLcd3LcJzLcJzdllmVldWYtl2Pn5GcuYGNkNDOhJGNwYWZhhjM3ITMjRGNkJDOmJzMyEDZyQDOvwVM3gjM2ATMtUGall3LcVmdhNXLwRHdo9CXt92YucWbpRWdvx2Yx5yazF2Lc9CX6MHc0RHaiojIsJye.png)
可以看到,vue-template-compiler[2] 将
<下拉框>
元件識别成了文本。我們的需求來了:要将上述代碼編譯成跟其他元件一樣的 AST 。先看下正确被編譯的元件 AST:
我們重點關注
type
、
tag
、
children
和
attrs
這四個屬性,其他字段都是一些附加資訊。因為本文重點是編譯邏輯,其他字段都可以基于
PEG.js
的
action
去添加,是以不會詳細講解。
下面我們就來實作上圖中的
zh-template-compiler
。
分析
基于上述需求,可以分析得到我們需要識别的詞法跟文法:
- 正确識别元件的父子關系;在
的模闆編譯中,通過正則和棧去維護開始标簽和結束标簽的關系,沒有接觸過的童鞋可以前往模闆編譯 了解。vue2
則可以直接通過規則去比對;PEG.js
- 元件的屬性比對;能夠将模闆中的
識别成props
中的ast
和name
的形式,并且能夠區分靜态屬性和動态屬性(value
);對于複雜類型的v-bind
(比如對象),期望能夠表現得更好,而不是僅僅當作字元串處理;value
- 元件名和屬性名隻能包含中文;
測試用例
我們習慣用單測去了解架構的最小最細粒度功能,梳理場景也一樣可以用這個方法。
針對上述分析的第一個需求,我們可以寫出以下用例(😉 自閉合元件的邏輯沒有處理哦,感興趣的童鞋可以
fork
項目[3]去練練手):
const { parse } = require('../src/zh-template-compiler')
describe('zh-template-compiler', () => {
test('不帶屬性的元件', () => {
const template = `<元件></元件>`
const ast = parse(template)
expect(ast.attrs.length).toBe(0)
expect(ast.children.length).toBe(0)
expect(ast.type).toBe(1)
expect(ast.tag).toBe('元件')
})
test('包含子元件', () => {
const template = `<元件><子元件></子元件></元件>`
const ast = parse(template)
expect(ast).toMatchSnapshot()
})
test('包含多個子元件', () => {
const template = `<元件><第一個子元件></第一個子元件><第二個子元件></第二個子元件></元件>`
const ast = parse(template)
expect(ast).toMatchSnapshot()
})
test('包含多層子元件', () => {
const template = `<元件><子元件><孫子元件></孫子元件></子元件></元件>`
const ast = parse(template)
expect(ast).toMatchSnapshot()
})
})
複制
第二個需求,識别中文的 props 并區分靜态和動态:
describe('zh-template-compiler', () => {
// ...接上述代碼
test('帶靜态屬性的元件', () => {
const template = `<元件 屬性="值"></元件>`
const ast = parse(template)
const attr = ast.attrs
expect(attr.length).toBe(1)
expect(attr[0]).toEqual({
isBind: false,
name: "屬性",
value: "值"
})
})
test('帶動态屬性的元件', () => {
const template = `<元件 :屬性="值"></元件>`
const ast = parse(template)
const attr = ast.attrs
expect(attr.length).toBe(1)
expect(attr[0]).toEqual({
isBind: true,
name: "屬性",
value: "值"
})
})
test('複雜的屬性值', () => {
const template = `
<下拉框 選中值="番茄" :資料="{
"list":[
{
"名稱": "🍅",
"id": "番茄"
},
{
"名稱": "🍌",
"id": "香蕉"
}
],
"total": 2
}">
<子元件></子元件>
</下拉框>`
const ast = parse(template)
expect(ast).toMatchSnapshot()
})
test('帶靜态+動态屬性的元件', () => {
const template = `<元件 靜态屬性="靜态屬性的值" :動态屬性="動态屬性的值"></元件>`
const ast = parse(template)
const attrs = ast.attrs
expect(attrs.length).toBe(2)
expect(attrs).toEqual([{
isBind: false,
name: "靜态屬性",
value: "靜态屬性的值"
}, {
isBind: true,
name: "動态屬性",
value: "動态屬性的值"
}])
})
})
複制
最後元件名和屬性名隻能包含中文的用例比較簡單:
describe('zh-template-compiler', () => {
// ...接上述代碼
test('元件名稱隻能包含漢字', () => {
const template = `<元件1></元件1>`
try {
parse(template)
} catch (e) {
console.log(e)
expect(e.message).toBe('Expected ":", ">", or [一-龥] but "1" found.')
}
})
test('屬性名稱隻能包含漢字', () => {
const template = `<元件 屬性1="值1"></元件>`
try {
parse(template)
} catch (e) {
console.log(e)
expect(e.message).toBe('Expected \"=\" or [一-龥] but \"1\" found.')
}
})
})
複制
編碼
開始先寫入口規則:
Program
= program:Tag {
return program;
}
複制
還記得前文提到
--allowed-start-rules
的配置,如果沒有配置預設就從第一條規則開始執行。緊接着就是核心的規則定義:
// 一個完整的模闆定義
// ws 即空白符,開始标簽前随便你輸入幾個空白字元
// StartTag,開始标簽的比對
// children: (Tag*) 很關鍵,很關鍵,很關鍵!!!反複比對 Tag 規則。
// EndTag,結束标簽的比對
// 最後的 action 即處理函數很關鍵,拿到比對資訊你可以做任何的判斷、格式化
// 比如這裡的 start 和 end 标簽的 tag 不一緻即元件名不一緻,那必須報錯。vue2中是通過棧去維護的這個關系,可以看到 PEG.js 的處理更加簡潔。
Tag
= ws
start:StartTag
children: (Tag*)
end:EndTag
ws
{
if (start.tag !== end.tag) {
throw Error('開始标簽和結束标簽不一緻')
}
return {
...start,
children
}
}
// 開始标簽和屬性
// component:$zh 元件名隻能是中文,zh = [\u4e00-\u9fa5]+ 比對一個以上的漢字,有個細節,zh 前面有一個 $,這裡拿到的 component 是一個比對的中文字元串,如果不加這個 $,那拿到的是一個比對數組。忘記這個文法的童鞋可以回到上篇再回顧哦
// attrsList: (...)* 比對任意個 attr,并存入 attrList
// attrs:Attrs 比對單個元件屬性
// 最後 action 處理傳回了一個對象,這裡的 type = 1 跟 vue2 中的 VNode 保持一緻,表示的是元件類型
StartTag
= "<"
ws
component:$zh
attrList: (
ws
attrs:Attrs
ws
{
if (attrs.name) {
return attrs
}
}
)*
">" {
return {
type: 1,
tag: component,
attrs: attrList
}
}
// 結束标簽
EndTag
= "</"
component:$zh
">"
{
return {
tag: component
}
}
// 比對中文字元
zh = [\u4e00-\u9fa5]+
// 比對元件屬性
// isBind:name_separator ? 比對到 : 就傳回對應的串,然後傳回 null
// attrName:$zh+ 屬性名稱是一個中文字元串
// quotation_mark 引号
// attrValue:( $zh / JSON_text ) 屬性值可以是一個中文,或者是一個 JSON 文本,JSON_text 是利用了上篇文章中那個定義哦,想了解的可以回去上文檢視注釋。
// 全部比對完成之後傳回比對對象
Attrs
= isBind:name_separator ?
attrName:$zh+
"="
quotation_mark
attrValue:(
$zh / JSON_text
)
quotation_mark {
if (attrName) {
let hasVbind = isBind ? true : false
return {
isBind: hasVbind,
name: attrName,
value: attrValue
}
}
}
複制
核心的規則定義就如上述代碼所示,難點在解析子元件那裡,通過利用
rule
遞歸(類似函數遞歸)的思路去解決的話就變得
so easy
。
驗證
最後,将上述規則生成編譯器:
npx pegjs -o zh-template-compiler.js src/zh-template-compiler.pegjs
複制
文章開頭的 🌰 生成的 AST 結果如下:
{
"type": 1,
"tag": "下拉框",
"attrs": [
{
"isBind": false,
"name": "選中值",
"value": "番茄"
},
{
"isBind": true,
"name": "資料",
"value": {
"list": [
{
"名稱": "🍅",
"id": "番茄"
},
{
"名稱": "🍌",
"id": "香蕉"
}
],
"total": 2
}
}
],
"children": [
{
"type": 1,
"tag": "子元件",
"attrs": [],
"children": []
}
]
}
複制
執行測試用例結果如下圖所示:
最簡單的一個中文模闆編譯器就完成了。通過這個練習,相信你對
PEG.js
的基礎掌握得更加熟練,也能夠利用它去解決日常開發中的一些問題。讀完本文,想繼續細化該編譯器的童鞋可以
fork
zh-template-compiler[4] 接着玩哦~
下篇文章将會基于 AST 結果去生成頁面上真實的下拉框,如果是你,你會怎麼做?
參考資料
[1]
astexplorer: https://astexplorer.net/
[2]
vue-template-compiler: https://www.npmjs.com/package/vue-template-compiler
[3]
項目: https://github.com/Jouryjc/zh-template-compiler
[4]
zh-template-compiler: https://github.com/Jouryjc/zh-template-compiler