一种让小程序支持JSX语法的新思路

React社区一直在探寻使用React语法开发小程序的方式,其中比较著名的项目有Taronanachi。而使用React语法开发小程序的难点主要就是在JSX语法上,JSX本质上是JS,相比于小程序静态模版来讲太灵活。本文所说的新思路就是在处理JSX语法上的新思路,这是一种更加动态的处理思路,相比于现有方案,基本上不会限制任何JSX的写法,让你以真正的React方式处理小程序,但愿这个新思路能够给任何有志于用React开发小程序的人带来启发。javascript

现有思路的局限

在介绍新的思路以前,咱们先来看下Taro(最新版1.3)nanachi是怎么在小程序端处理JSX语法的。简单来讲,主要是经过在编译阶段JSX转化为等效的小程序wxml来把React代码运行在小程序端的。html

举个例子,好比React逻辑表达式:java

xx && <Text>Hello</Text>
复制代码

将会被转化为等效的小程序wx:if指令:git

<Text wx:if="{{xx}}">Hello</Text>
复制代码

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

编译阶段有什么问题和局限呢?咱们如下面的例子说明:小程序

class App extends React.Component {
    render () {
        const a = <Text>Hello</Text>
        const b = a

        return (
            <View> {b} </View>
        )
    }
}
复制代码

首先咱们声明 const a = <Text>Hello</Text>,而后把a赋值给了b,咱们看下最新版本Taro 1.3的转换,以下图:bash

这个例子不是特别复杂,却报错了。babel

要想理解上面的代码为何报错,咱们首先要理解编译阶段。本质上来讲在编译阶段,代码其实就是‘字符串’,而编译阶段处理方案,就须要从这个‘字符串’中分析出必要的信息(经过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对象,形如

// 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,因此<Text>Hello</Text>这个JSX片断被特殊处理了,a再也不是一个普通js对象,这里咱们看到a变量甚至丢失了,这里暴露了一个很严重的问题:代码语义被破坏了,也就是说因为编译时方案对JSX的特殊处理,真正运行在小程序上的代码语义并非你的预期。这个是比较头疼。

新的思路

正由于编译时方案,有如上的限制,在使用的时候经常让你有“我仍是在写React吗?”这种感受。

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

第一步:给每一个独立的JSX片断打上惟一标识uuid,假定咱们有以下代码:

const a = <Text uuid="000001">Hello</Text>

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文件

<template name="000001">
	<Text>Hello</Text>
</template>

<template name="000002">
	<View uuid="000002">
		<Image/>
		<Text/>
	</View>
</template>


<!--占位template-->
<template is="{{uiDes.name}}" data="{{...uiDes}}"/>
复制代码

注意这里每个templatename标识和 JSX片断的惟一标识uuid是同样的。最后,须要在结尾生成一个占位模版:<template is="{{uiDes.name}}" data="{{...uiDes}}"/>

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

第五步:把第四步生成的uiDes,传递给小程序环境,小程序把uiDes 设置给占位模版<template 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 (
	      React.createElement(View, {uuid: "000002"} , b);
	    )
	  }
}
复制代码

同时生成wxml文件:

<template name="000001">
	<Text>Hello</Text>
</template>

<template name="000002">
	<View>
		<template is="{{child0001.name}}" data="{{...child0001}}"/>
	</View>
</template>

<!--占位template-->
<template is="{{uiDes.name}}" data="{{...uiDes}}"/>
复制代码

使用咱们定制以后render执行ReactDOM.render(<App/>, parent)。在render的递归过程当中,除了会执行常规的建立组件实例,执行生命周期以外,还会额外的收集执行过程当中组件的uuid标识,最终生成 uiDes 对象

const uiDes = {
	name: "000002",
	
    child0001: {
   	    name: 000001,
   	    ...
   }
   
   ...
}
复制代码

小程序获取到这个uiDes,设置给占位模版<template is="{{uiDes.name}}" data="{{...uiDes}}"/>。 最终渲染出小程序视图。

在这整个过程当中,你的全部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。这和编译时方案的静态识别有着本质的区别。

结语

"Talk is cheap. Show me your code!" 这仅仅是一个思路?仍是已经有落地完整的实现呢?

是有完整的实现的,alita项目在处理JSX语法的时候,采用的就是这个思路,这也是alita基本不限制写法却能够转化整个React Native项目的缘由,另外alita在这个思路上作了不少优化。若是对这个思路的具体实现有兴趣,能够去研读一下alita源码,它彻底是开源的github.com/areslabs/al…

固然,你也能够基于这个思路,构造出本身的React小程序开发方案

相关文章
相关标签/搜索