IOriens / ioriens.github.io

https://Junjie.xyz
12 stars 2 forks source link

wxml 转译器实现过程分析 #5

Open IOriens opened 6 years ago

IOriens commented 6 years ago

目标

目标是实现wcc工具的大部分功能。该工具会遍历输入的文件列表,读取相应的文件,分别解析其中的组件(小程序中每一个标签即为一个组件),最后将它们组合起来输出一个JS文件。这个JS文件由三部分构成:首部定义了许多用于生成或组合虚拟节点的函数,中部存放所有组件的属性值,尾部将各个组件以函数组合的形式描述出来。

输入

<button bindtap="addNumberToFront"> Add to the front </button>

输出

======== 顶部定义的函数 ======== 
function _createTagNode(tag) { // 根据组件名创建虚拟节点
  return {
    tag: tag
    attr: {},
    children: [],
    n: []
  }
}
function _pushChild(a, b) { // 父组件接收子组件
  b && a.children.push(b)
}

======== 中部数据收集 ======== 
var z = []
function Z(ops) {
  z.push(ops)
}
Z([3, 'addNumberToFront'])
Z([3, ' Add to the front '])

======== 底部组件声明 ======== 
var A = _createTagNode('button')  // 生成button组件
_attachAttribute(A, 'bindtap', 0) // 绑定属性 !!!!!! -> 0代表z数组中的第一个元素
var B = _createTextNode(1) // 生成子组件 !!!!!! -> 1代表z数组中的第一个元素
_pushChild(A, B) // 父组件接收子组件
_pushChild(rootNode, A) // 根组件接收父组件

本项目主要完成的工作是该JS文件后两部分的生成工作。

基础概念

转译器在现代前端开发过程中是一个必不可少的工具,如babel、uglifyjs以及postcss。这些工具的作用是将一门高级语言转换成同种或其它种类的高级语言,以拓展原有代码的功能、优化原有代码的性能。与传统的编译器或解释器不同,它并不需要生成与平台绑定的二进制代码。

大部分转译器主要由以下三个部分构成:

解释器

解析器会进行词法分析和语义分析。首先,词法分析负责将输入的代码文本拆分成一个个单词或算符(统称Token),如12 * (3 + 4)^2会被拆分为12, *, (, 3,+, 4, ), ^, 2。然后,语义分析器将这些Token组装成一颗语法树(AST == Abstract Syntax Tree),如下图:

image

代码转换器

代码转换器负责将原有的ast转换成我们期望的ast,以上例,如果我们想要把自然语言的^符号换成Python的**,我们可以直接修改原有的ast:

image

当目标代码和原有代码差别较大时,可以直接遍历原有ast生成一颗新的ast。

代码生成器

通过递归遍历AST,生成目标代码。以上图较为简单的ast为例,可以将其转换成多种目标语言:

Python

12 * (3 + 4) ** 2

Racket

#lang racket

; 提前声明**函数 
(define **
  (lambda (a b)
    (expt a b)))

; 遍历AST生成的代码
(* 12
   (** (+ 3
          4) 
       2))

当然,以上只是简述,各部分具体的实现都有很多值得研究的地方。

整体转译流程

  1. 根据输入的列表,读取所有文件
  2. 调用VUE的HTML Parser,解析输入的标签及属性,生成一颗DOM树
  3. 在解析组件的标签时,对其上包含的属性值进行解析(边Parse边Transform)
  4. 根据已有的AST生成JS文件(Generate)

顶部部分:直接复制

顶部定义的代码提供给尾部的代码使用,这部分代码直接复制进目标文件即可:

export function genTemplate(slot) {
  return `
   // 顶部定义的各种函数
   function _a () {}
   function _b () {}
   function _c () {}

   // 中部和尾部代码
   ${slot}
  `
}

中间部分:wxml属性的处理

该部分的作用是收集所有文件内所有组件上的所有属性的值, 尾部代码将会使用这些属性。引用时首先找出所需属性在数组中所出现的位置,然后将其作为参数传递给节点生产函数。

小程序支持在组件属性中绑定动态值,如下面代码中view组件的hidden属性值是动态生成的,这个值包裹在双花括号中:

<view hidden="{{flag ? true : false}}"> Hidden </view>

双花括号中的代码为JS语言,VUE内置的HTML Parser不能处理该语言,所以此处需要引入一个JS Parser来解析花括号中的属性,本项目中使用的解析器为:babylon

从文章开头的示例,你可以看到,组件的属性值会被解析成数组的形式,这是我们的目标语言,而输入的语言则为JS。首先我们使用JS解析器将JS语言转换为相应的AST:

=== 输入 JS===
3 + 4

=== 输出 AST===
{
    "type": "ExpressionStatement",
    "expression": {
        "type": "BinaryExpression",
        "left": {
            "type": "Literal",
            "value": 3,
        },
        "operator": "+",
        "right": {
            "type": "Literal",
            "value": 4,
        }
    }
}

=== 目标 数组形式 ===
[[2, "+"], [1, 3], [1, 4]]

从目标语言可以推测出,BinaryExpression 可以解析为如下形式:

`[[2 "${operator}"], ${left}, ${right}]`

Literal 则解析为如下形式:

`[[1], ${value}]`

使用JS代码实现的Code Generator如下(点我在线运行):

function walk (node) {
  switch (node.type) {
    case 'BinaryExpression':
      return `[[2, "${node.operator}"], ${walk(node.left)}, ${walk(node.right)}]`
      break
    case 'Literal':
      return `[1, ${node.value}]`
      break
  }
}

const code = walk(ast.expression)

末尾部分:wxml标签的处理

尾部标签的处理与上面类似,先使用HTML Parser将HTML标签转换为一颗AST,再遍历AST并生成JS代码(点我在线运行

=== 输入 HTML===
<button> Add to the front </button>

=== 输出 AST===
const ast = {
    "type": "tag",
    "name": "button",
    "children": [
      {
        "data": " Add to the front ",
        "type": "text"
      }
    ]
}

=== 目标 JS代码 ===
var A = _createTag("button")
var B = _createTextNode(" Add to the front ")
_pushChild(A ,B)
_pushChild(root ,A)
lixuefengabc commented 6 years ago

楼主有尝试实现这个功能吗?

IOriens commented 6 years ago

@lixuefengabc Hera项目目前使用的就是我们用 JS 实现的 wxml 编译器, 只不过没有开源。wxss 编译器开源了。

IOriens commented 5 years ago

开源了,没有整理过,里面一堆垃圾代码。。 https://github.com/IOriens/wxml-transpiler

AngusFu commented 4 years ago

这里一版基础实现 https://github.com/AngusFu/wxml2vue