前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >手摸手实现一个编译器(中)

手摸手实现一个编译器(中)

作者头像
码农小余
发布2022-06-16 16:42:30
5760
发布2022-06-16 16:42:30
举报
文章被收录于专栏:码农小余

上篇我们了解了 PEG.js 的基础使用,忘记的童鞋建议复习一下,对于本文的食用效果会更佳哦!

光说不练,等于白学。所以本文来实现一个编译器(瞎搞、玩具、欢乐)。

需求

我们知道 Vuetemplate 不支持中文标签名,比如下面这段代码:

代码语言:javascript
复制
<下拉框 选中值="番茄" :数据="{
    "list":[
        {
          "名称": "🍅",
          "id": "番茄"
        },
        {
          "名称": "🍌",
          "id": "香蕉"
        }
    ],
    "total": 2
}">
  <子组件></子组件>
</下拉框>

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

可以看到,vue-template-compiler[2]<下拉框> 组件识别成了文本。我们的需求来了:要将上述代码编译成跟其他组件一样的 AST 。先看下正确被编译的组件 AST:

我们重点关注 typetagchildrenattrs 这四个属性,其他字段都是一些附加信息。因为本文重点是编译逻辑,其他字段都可以基于 PEG.jsaction 去添加,所以不会详细讲解。

下面我们就来实现上图中的 zh-template-compiler

分析

基于上述需求,可以分析得到我们需要识别的词法跟语法:

  • 正确识别组件的父子关系;在 vue2 的模板编译中,通过正则和栈去维护开始标签和结束标签的关系,没有接触过的童鞋可以前往模板编译 了解。PEG.js 则可以直接通过规则去匹配;
  • 组件的属性匹配;能够将模板中的 props 识别成 ast 中的 namevalue 的形式,并且能够区分静态属性和动态属性(v-bind);对于复杂类型的 value(比如对象),期望能够表现得更好,而不是仅仅当作字符串处理;
  • 组件名和属性名只能包含中文;

测试用例

我们习惯用单测去了解框架的最小最细粒度功能,梳理场景也一样可以用这个方法。

针对上述分析的第一个需求,我们可以写出以下用例(😉 自闭合组件的逻辑没有处理哦,感兴趣的童鞋可以 fork 项目[3]去练练手):

代码语言:javascript
复制
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 并区分静态和动态:

代码语言:javascript
复制
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: "动态属性的值"
    }])
  })
})

最后组件名和属性名只能包含中文的用例比较简单:

代码语言:javascript
复制
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.')
    }
  })
})

编码

开始先写入口规则:

代码语言:javascript
复制
Program
 = program:Tag {
  return program;
 }

还记得前文提到 --allowed-start-rules 的配置,如果没有配置默认就从第一条规则开始执行。紧接着就是核心的规则定义:

代码语言:javascript
复制
// 一个完整的模板定义
// 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

验证

最后,将上述规则生成编译器:

代码语言:javascript
复制
npx pegjs -o zh-template-compiler.js src/zh-template-compiler.pegjs

文章开头的 🌰 生成的 AST 结果如下:

代码语言:javascript
复制
{
   "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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-12-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码农小余 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 需求
  • 分析
  • 测试用例
  • 编码
  • 验证
  • 参考资料
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档