Loading [MathJax]/jax/input/TeX/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >手摸手实现一个编译器(中)

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

作者头像
码农小余
发布于 2022-06-16 08:42:30
发布于 2022-06-16 08:42:30
60400
代码可运行
举报
文章被收录于专栏:码农小余码农小余
运行总次数:0
代码可运行

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

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

需求

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<下拉框 选中值="番茄" :数据="{
    "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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
Program
 = program:Tag {
  return program;
 }

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 一个完整的模板定义
// 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
代码运行次数:0
运行
AI代码解释
复制
npx pegjs -o zh-template-compiler.js src/zh-template-compiler.pegjs

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
{
   "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 删除。

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
震惊!这家伙居然用中文写 vue 组件
大家好,我是码农小余。作为手摸手实现编译器的终(下)篇,调皮地改了一个标题。回顾前两篇内容:
码农小余
2022/06/16
5140
震惊!这家伙居然用中文写 vue 组件
手摸手实现一个编译器(上)
PEG.js 是一个简单的 JavaScript 解析器生成器,可以生成具有出色错误报告的快速解析器。您可以使用它来处理复杂的数据或计算机语言,并轻松构建转换器、解释器、编译器和其他工具。
码农小余
2022/06/16
8640
手摸手实现一个编译器(上)
深度学习Vue源码-模板编译原理
上一篇咱们主要介绍了 Vue 数据的响应式原理 对于中高级前端来说 响应式原理基本是面试 Vue 必考的源码基础类 如果不是很清楚的话基本就被 pass 了 那么今天咱们手写的模板编译原理也是 Vue 面试比较频繁的一个点 而且复杂程度是高于响应式原理的 里面主要涉及到 ast 以及大量正则匹配 大家学习完可以看着思维导图一起手写一遍加深印象哈
yyzzabc123
2022/10/03
4110
[咖聊] “模板编译”真经
冲一杯美式 ☕️ ,读编译真经,岂不快哉? 本文的 🍪 (表示 例子,☕️ 和 🍪 更配哦!全文都会围绕这个 DEMO 做解析。⚠️ 因不能直接跳转到外链,注意有标注的地方,文末有对应的地址哦,跳转着看更容易理解哦!): <div id="app"> <!-- 这是一个注释节点 --> <Child name="yjc" :age="12" v-if="isShow"></Child> <input type="text" v-model="inputValue" /> <d
码农小余
2022/06/16
1.1K0
[咖聊] “模板编译”真经
【Vue原理】Compile - 源码版 之 属性解析
哈哈哈,今天终于到了属性解析的部分了,之前已经讲过了 parse 流程,标签解析,最后就只剩下 属性解析了 (´・ᴗ・`)
神仙朱
2019/08/02
1K0
【Vue原理】Compile - 源码版 之 属性解析
Vue 的生命周期之间到底做了什么事清?(源码详解,带你从头梳理组件化流程)
相信大家对 Vue 有哪些生命周期早就已经烂熟于心,但是对于这些生命周期的前后分别做了哪些事情,可能还有些不熟悉。
ssh_晨曦时梦见兮
2024/01/26
4930
Vue(v2.6.11)万行源码生啃,就硬刚!
众所周知,以下代码就是 vue 的一种直接上手方式。通过 cdn 可以在线打开 vue.js。一个文件,一万行源码,是万千开发者赖以生存的利器,它究竟做了什么?让人品味。
掘金安东尼
2024/01/27
4510
Vue(v2.6.11)万行源码生啃,就硬刚!
手摸手打造类码上掘金在线IDE(六)——沙箱编译(二)
我们上回书说道沙箱编译的vue编译部分,很多jym以为我会就此金盆洗手, 等着东家发完盒饭踏实回家搬砖。
用户7413032
2022/12/02
8040
手摸手打造类码上掘金在线IDE(六)——沙箱编译(二)
Vue 的生命周期之间到底做了什么事清?(源码详解,带你从头梳理组件化流程)
相信大家对 Vue 有哪些生命周期早就已经烂熟于心,但是对于这些生命周期的前后分别做了哪些事情,可能还有些不熟悉。
ssh_晨曦时梦见兮
2020/04/11
1K0
Vue.js 2 深入理解
含了父作用域中不作为 prop 被识别(且获取)的特性绑定(class 和 style 除外)
Cellinlab
2023/05/17
1.2K0
Vue.js 2 深入理解
# Vue 模板编译原理解析
在 Vue 开发过程中,我们通常使用.vue文件进行开发,然后上线时打包成一个js最后在页面中加载然后渲染 DOM。
九旬
2023/10/18
4270
# Vue 模板编译原理解析
Vue模板是怎样编译的
这一章我们开始讲模板解析编译:总结来说就是通过compile函数把tamplate解析成render Function形式的字符串compiler/index.js
yyds2026
2022/10/19
1.1K0
Vue2.0模板编译原理
写过 Vue 的同学肯定体验过, .vue 这种单文件组件有多么方便。但是我们也知道,Vue 底层是通过虚拟 DOM 来进行渲染的,那么 .vue 文件的模板到底是怎么转换成虚拟 DOM 的呢?这一块对我来说一直是个黑盒,之前也没有深入研究过,今天打算一探究竟。
若川
2020/11/10
1.2K0
Vue2.0模板编译原理
前端工程化在WMS 6.0中的实践
Tech 导读 在对大型前端项目进行国际化改造时,经常会遇到过工作量大、干扰项多以及容易遗漏等问题。而针对这些大量的重复的工作,自动化工具往往能提升很大的工作效率。本文将带读者了解node cli开发的基础知识,并对如何开发一个国际化校验工具来解决这些问题展开教学。 01  背景 在今年的敏捷团队建设中,我通过Suite执行器实现了一键自动化单元测试。Juint除了Suite执行器还有哪些执行器呢?由此我的Runner探索之旅开始了! 仓储中台的愿景是,以用户为根本,通过发现、定义、设计、交付可被多BP
京东技术
2022/08/25
1.1K0
前端工程化在WMS 6.0中的实践
深入理解 Vue 模板渲染:Vue 模板反编译
熟悉 vue 的同学应该都知道,vue 单文件模板中一般含有三个部分,template,script,style。
WecTeam
2020/09/01
7.1K1
深入理解 Vue 模板渲染:Vue 模板反编译
【Vuejs】1094- 你真的了解vue模版编译么?
本文的初衷是想让更多的同学知道并了解vue模版编译,所以文中主要以阶段流程为主,不会涉及过多的底层代码逻辑,请耐心观看。
pingan8787
2021/10/08
1K0
【Vuejs】1094- 你真的了解vue模版编译么?
常考vue面试题(附答案)
当一个Vue实例创建时,Vue会遍历data中的属性,用 Object.defineProperty(vue3.0使用proxy )将它们转为 getter/setter,并且在内部追踪相关依赖,在属性被访问和修改时通知变化。 每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用时,会通知watcher重新计算,从而致使它关联的组件得以更新。
bb_xiaxia1998
2022/12/14
7320
点击页面元素,这个Vite插件帮我打开了Vue组件
前言 大家好,我是webfansplz.这两天肝了个Vite插件,本文主要跟大家分享一下它的功能和实现思路.如果你觉得它对你有帮助,请给一个star支持作者 💗. 介绍 vite-plugin-vue-inspector的功能是点击页面元素,自动打开本地IDE并跳转到对应的Vue组件.类似于Vue DevTools的 Open component in editor功能. 用法 vite-plugin-vue-inspector支持Vue2 & Vue3,并且只需要进行简单的配置就可以使用. Vue2 //
null仔
2022/04/19
1.2K0
点击页面元素,这个Vite插件帮我打开了Vue组件
vue-loader&vue-template-compiler详解
在 vue 工程中,安装依赖时,需要 vue 和 vue-template-compiler 版本必须保持一致,否则会报错。
奋飛
2020/05/28
2.4K0
一份vue面试知识点梳理清单
指令本质上是装饰器,是 vue 对 HTML 元素的扩展,给 HTML 元素增加自定义功能。vue 编译 DOM 时,会找到指令对象,执行指令的相关方法。
bb_xiaxia1998
2022/11/15
8660
相关推荐
震惊!这家伙居然用中文写 vue 组件
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验