2019-08-10 | UNLOCK

让小程序支持jsx语法

一 现有思路的局限性

先看看 Taronanachi是怎么在小程序端处理 JSX语法的。简单来说,主要是通过在编译阶段把 JSX转化为等效的小程序 wxml来把 React代码运行在小程序端的

比如将

1
xx && <Text>hello</Text>

将会被转化为wx:if

1
<Text wx:if="{{xx}}">Hello</Text>

这种方式把对 JSX的处理,主要放在了编译阶段,他依赖于编译阶段的信息收集,以上面为例,它必须识别出逻辑表达式,然后做对应的 wx:if转换处理。

那编译阶段有什么问题和局限呢?

1
2
3
4
5
6
7
8
9
10
11
class App extends React.Component {
render(){
const a=<Text>hello</Text>
const b=a
return (
<View>
{b}
</View>
)
}
}

首先我们声明 consta=<Text>Hello</Text>,然后把 a赋值给了 b,在最新版本 Taro1.3的转换后报错了!!!

a is not defined

为什么呢?

想理解上面的代码为什么报错,我们首先要理解编译阶段。本质上来说在编译阶段,代码其实就是‘字符串’,而编译阶段处理方案,就需要从这个‘字符串’中分析出必要的信息(通过 AST,正则等方式)然后做对应的等效转换处理。

而对于上面的例子,需要做什么等效处理呢?需要我们在编译阶段分析出 bJSX片段:b=a=<Text>Hello</Text>,然后把 <View>{b}</View>中的 {b}等效替换为 <Text>Hello</Text>。然而在编译阶段要想确定 b的值是很困难的,有人说可以往前追溯来确定b的值,也不是不可以,但是考虑一下 由于 b=a,那么就先要确定 a的值,这个 a的值怎么确定呢?需要在 b可以访问到的作用域链中确定 a,然而 a可能又是由其他变量赋值而来,循环往复,期间一旦出现不是简单赋值的情况,比如函数调用,三元判断等运行时信息,追溯就宣告失败,要是 a本身就是挂在全局对象上的变量,追溯就更加无从谈起。

所以在编译阶段 是无法简单确定 b的值的。

我们再仔细看下报错信息:a is not defined

什么说 a未定义呢?这是涉及到另外一个问题,我们知道 <Text>Hello</Text>,其实等效于 React.createElement(Text,null,'Hello'),而 React.createElement方法的返回值就是一个普通 JS对象

1
2
3
4
5
6
{
tag:Text,
props:null,
children:'hello'
...
}

所以上面那一段代码在 JS环境真正运行的时候,大概等效如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class App extends React.Component {
render(){
const a={
tag:Text,
props:null,
children:'hello'
...
}
const b=a
return {
tag:View,
props:null,
children:b
...
}
}
}

但是,之前说编译阶段需要对 JSX做等效处理,需要把 JSX转换为 wxml,所以 <Text>Hello</Text>这个 JSX片段被特殊处理了, a不再是一个普通 js对象,这里我们看到 a变量甚至丢失了,这里暴露了一个很严重的问题:代码语义被破坏了,也就是说由于编译时方案对 JSX的特殊处理,真正运行在小程序上的代码语义并不是你的预期。

二 新的思路

下面我们介绍一种全新的处理思路,这种思路在小程序运行期间和真正的 React几无区别,不会改变任何代码语义, JSX表达式只会被处理为 React.createElement方法调用,实际运行的时候就是普通 js对象,最终通过其他方式渲染出小程序视图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
第一步:给每个独立的 `JSX`片段打上唯一标识 `uuid`,例如:

const a=<Text uuid='00001'></Text>
const y=<View uuid='00002'>
<Image></Image>
<Text></Text>
</View>

我们给 `a`片段, `y`片段 添加了 `uuid`属性

第二步:把 `React`代码通过 `babel`转义为小程序可以识别的代码,例如 `JSX`片段用等效的 `React.createElement`替换等

const a=React.createElement(Text,{
uuid:'00001'
},'hello')

第三步:提取每个独立的 `JSX`片段,用小程序 `template`包裹,生成 `wxml`文件

<template name='00001'>
<Text>hello</Text>
</template>
<template name='00002'>
<View uuid='00002'>
<Image></Image>
<Text></Text>
</View>
</template>
<template is="{{uiDes.name}}" data="{{...uiDes}}"></template>

注意这里每一个 `template` 的 `name`标识和 `JSX`片段的唯一标识 `uuid`是一样的。最后,需要在结尾生成一个占位模版:`<templateis="{{uiDes.name}}"data="{{...uiDes}}"/>`。

第四步:修改 `ReactDOM.render`的递归( `React16.x`之后,不在是递归的方式)过程,递归执行阶段,聚合 `JSX`片段的 `uuid`属性,生成并返回 `uiDes`数据结构。

第五步:把第四步生成的 `uiDes`,传递给小程序环境,小程序把 `uiDes` 设置给占位模版 `<templateis="{{uiDes.name}}"data="{{...uiDes}}"/>`,渲染出最终的视图。

const uiDes={
name:'00002',
child0001:{
name:00001,
...
}
...
}

在这整个过程中,你的所有 JS代码都是运行在 React过程中的,语义完全一致, JSX片段也不会被任何特殊处理,只是简单的 React.createElement调用,另外由于这里的 React过程只是纯 js运算,执行是非常迅速的,通常只有几ms。最终会输出一个 uiDes数据到小程序,小程序通过这个 uiDes渲染出视图。

现在我们在看之前的赋值 const b=a,就不会有任何问题了,因为 a 不过是普通对象。另外对于常见的编译时方案的限制,比如任意函数返回 JSX片段,动态生成 JSX片段, for循环使用 JSX片段等等,都可以完全解除了,因为 JSX片段只是 js对象,你可以做任何操作,最终 ReactDOM.render会搜集所有执行结果的片段的 uuid标识,生成 uiDes,而小程序会根据这个 uiDes数据结构渲染出最终视图。

可以看出这种新的思路和以前编译时方案还是有很大的区别的,对 JSX片段的处理是动态的,你可以在任何地方,任何函数出现任何 JSX片段, 最终执行结果会确定渲染哪一个片段,只有执行结果的片段的 uuid会被写入 uiDes。这和编译时方案的静态识别有着本质的区别。

jsx

评论加载中