React
社区一直在探寻使用
React
语法开发小程序的方式,其中比较著名的项目有
Taro
,
nanachi
。而使用
React
语法开发小程序的难点主要就是在
JSX
语法上,
JSX
本质上是
JS
,相比于小程序静态模版来说太灵活。本文所说的新思路就是在处理
JSX
语法上的新思路,这是一种更加动态的处理思路,相比于现有方案,基本上不会限制任何
JSX
的写法,让你以真正的React方式处理小程序,希望这个新思路可以给任何有志于用
React
开发小程序的人带来启发。
现有思路的局限
在介绍新的思路之前,我们先来看下
Taro
(最新版
1.3
)
,
nanachi
是怎么在小程序端处理
JSX
语法的。简单来说,主要是通过在编译阶段把
JSX
转化为等效的小程序
wxml
来把
React
代码运行在小程序端的。
举个例子,比如
React
逻辑表达式:
xx && <Text>HelloText>
将会被转化为等效的小程序wx:if指令:
wx:if="{{xx}}">Hello
这种方式把对
JSX
的处理,主要放在了编译阶段,他依赖于编译阶段的信息收集,以上面为例,它必须识别出逻辑表达式,然后做对应的
wx
:
if
转换处理。
那编译阶段有什么问题和局限呢?我们以下面的例子说明:
class App extends React.Component {
render () {
const a = <Text>HelloText>
const b = a
return (
<View>
{b}
View>
)
}
}
首先我们声明
const
a
=
<
Text
>
Hello
Text
>
,然后把
a
赋值给了
b
,我们看下最新版本
Taro
1.3
的转换,如下图:
这个例子不是特别复杂,却报错了。
要想理解上面的代码为什么报错,我们首先要理解编译阶段。本质上来说在编译阶段,代码其实就是‘字符串’,而编译阶段处理方案,就需要从这个‘字符串’中分析出必要的信息(通过
AST
,正则等方式)然后做对应的等效转换处理。
而对于上面的例子,需要做什么等效处理呢?需要我们在编译阶段分析出
b
是
JSX
片段:
b
=
a
=
<
Text
>
Hello
Text
>
,然后把
{b}
中的
{
b
}
等效替换为
Hello
。然而在编译阶段要想确定
b
的值是很困难的,有人说可以往前追溯来确定b的值,也不是不可以,但是考虑一下 由于
b
=
a
,那么就先要确定
a
的值,这个
a
的值怎么确定呢?需要在
b
可以访问到的作用域链中确定
a
,然而
a
可能又是由其他变量赋值而来,循环往复,期间一旦出现不是简单赋值的情况,比如函数调用,三元判断等运行时信息,追溯就宣告失败,要是
a
本身就是挂在全局对象上的变量,追溯就更加无从谈起。
所以在编译阶段 是无法简单确定
b
的值的。
我们再仔细看下上图的报错信息:
a
is
not
defined
。
为什么说
a
未定义呢?这是涉及到另外一个问题,我们知道
Hello
,其实等效于
React
.
createElement
(
Text
,
null
,
'Hello'
)
,而
React
.
createElement
方法的返回值就是一个普通
JS
对象,形如
// ReactElement对象
{
tag: Text,
props: null,
children: 'Hello'
...
}
所以上面那一段代码在
JS
环境真正运行的时候,大概等效如下:
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
,所以
Hello
这个
JSX
片段被特殊处理了,
a
不再是一个普通
js
对象,这里我们看到
a
变量甚至丢失了,这里暴露了一个很严重的问题:代码语义被破坏了,也就是说由于编译时方案对
JSX
的特殊处理,真正运行在小程序上的代码语义并不是你的预期。这个是比较头疼。
新的思路
正因为编译时方案,有如上的限制,在使用的时候常常让你有“我还是在写
React
吗?”这种感觉。
下面我们介绍一种全新的处理思路,这种思路在小程序运行期间和真正的
React
几无区别,不会改变任何代码语义,
JSX
表达式只会被处理为
React
.
createElement
方法调用,实际运行的时候就是普通
js
对象,最终通过其他方式渲染出小程序视图。下面我们仔细说明一下这个思路的具体内容。
第一步:给每个独立的
JSX
片段打上唯一标识
uuid
,假定我们有如下代码:
const a = <Text uuid="000001">HelloText>
const y = <View uuid="000002">
<Image/>
<Text/>
View>
我们给
a
片段,
y
片段 添加了
uuid
属性
第二步:把
React
代码通过
babel
转义为小程序可以识别的代码,例如
JSX
片段用等效的
React
.
createElement
替换等
const
a = React.createElement(Text, {
uuid: "000001"
}, "Hello");
第三步:提取每个独立的
JSX
片段,用小程序
template
包裹,生成
wxml
文件
name="000001">
Hello
name="000002">
uuid="000002">
is
="{{uiDes.name}}" data="{{...uiDes}}"/>
注意这里每一个
template
的
name
标识和
JSX
片段的唯一标识
uuid
是一样的。最后,需要在结尾生成一个占位模版:
is
=
"{{uiDes.name}}"
data
=
"{{...uiDes}}"
/>
。
第四步:修改
ReactDOM
.
render
的递归(
React
16.x
之后,不在是递归的方式)过程,递归执行阶段,聚合
JSX
片段的
uuid
属性,生成并返回
uiDes
数据结构。
第五步:把第四步生成的
uiDes
,传递给小程序环境,小程序把
uiDes
设置给占位模版
is
=
"{{uiDes.name}}"
data
=
"{{...uiDes}}"
/>
,渲染出最终的视图。
我们以上面的
App
组件的例子来说明整个过程,首先
js
代码会被转义为:
class App extends React.Component {
render () {
const a = React.createElement(Text, {uuid: "000001"}, "Hello");
const b = a
return (