天天看點

手摸手實作一個編譯器(中)

上篇我們了解了

PEG.js

的基礎使用,忘記的童鞋建議複習一下,對于本文的食用效果會更佳哦!

光說不練,等于白學。是以本文來實作一個編譯器(瞎搞、玩具、歡樂)。

需求

我們知道

Vue

template

不支援中文标簽名,比如下面這段代碼:

<下拉框 選中值="番茄" :資料="{
    "list":[
        {
          "名稱": "🍅",
          "id": "番茄"
        },
        {
          "名稱": "🍌",
          "id": "香蕉"
        }
    ],
    "total": 2
}">
  <子元件></子元件>
</下拉框>
           

複制

使用 astexplorer[1] 生成的結果:

手摸手實作一個編譯器(中)

可以看到,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