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


Posted in Javascript onJune 16, 2019

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

现有思路的局限

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

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

xx && <Text>Hello</Text>

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

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

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

那编译阶段有什么问题和局限呢?我们以下面的例子说明:

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的转换,如下图:

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

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

要想理解上面的代码为什么报错,我们首先要理解编译阶段。本质上来说在编译阶段,代码其实就是‘字符串',而编译阶段处理方案,就需要从这个‘字符串'中分析出必要的信息(通过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

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

为什么说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}}"/>。 最终渲染出小程序视图。

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

在这整个过程中,你的所有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源码,它完全是开源的https://github.com/areslabs/alita。

当然,你也可以基于这个思路,构造出自己的React小程序开发方案。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JavaScript中yield实用简洁实现方式
Jun 12 Javascript
javascript获取当前日期时间及其它操作函数
Jan 11 Javascript
EasyUI中的tree用法介绍
Nov 01 Javascript
javascript写的简单的计算器,内容很多,方法实用,推荐
Dec 29 Javascript
开发中可能会用到的jQuery小技巧
Mar 07 Javascript
jquery.ajax之beforeSend方法使用介绍
Dec 08 Javascript
JavaScript检查弹出窗口是否被阻拦的方法技巧
Mar 13 Javascript
javascript等号运算符使用详解
Apr 16 Javascript
js结合正则实现国内手机号段校验
Jun 19 Javascript
vue-resource拦截器设置头信息的实例
Oct 27 Javascript
快速解决vue-cli不能初始化webpack模板的问题
Mar 20 Javascript
js利用拖放实现添加删除
Aug 27 Javascript
JavaScript 处理树数据结构的方法示例
Jun 16 #Javascript
JavaScript中的ES6 Proxy的具体使用
Jun 16 #Javascript
简谈创建React Component的几种方式
Jun 15 #Javascript
JS中的一些常用的函数式编程术语
Jun 15 #Javascript
JavaScript模块管理的简单实现方式详解
Jun 15 #Javascript
JavaScript工具库之Lodash详解
Jun 15 #Javascript
jQuery创建折叠式菜单
Jun 15 #jQuery
You might like
php中判断一个字符串包含另一个字符串的方法
2007/03/19 PHP
PHP var_dump遍历对象属性的函数与应用代码
2010/06/04 PHP
php目录拷贝实现方法
2015/07/10 PHP
弹出模态框modal的实现方法及实例
2017/09/19 PHP
PHP实现抽奖功能实例代码
2020/06/30 PHP
JavaScript之编码规范 推荐
2012/05/23 Javascript
从jquery的过滤器.filter()方法想到的
2013/09/29 Javascript
js中判断用户输入的值是否为空的简单实例
2013/12/23 Javascript
JS获取单击按钮单元格所在行的信息
2014/06/17 Javascript
以JavaScript来实现WordPress中的二级导航菜单的方法
2015/12/14 Javascript
详解XMLHttpRequest(二)响应属性、二进制数据、监测上传下载进度
2016/09/14 Javascript
利用Js的console对象,在控制台打印调式信息测试Js的实现
2016/11/26 Javascript
AngularJS获取json数据的方法详解
2017/05/27 Javascript
Easyui使用Dialog行内按钮布局的实例
2017/07/27 Javascript
js下拉菜单生成器dropMenu使用方法详解
2017/08/01 Javascript
JavaScript模块管理的简单实现方式详解
2019/06/15 Javascript
express框架中使用jwt实现验证的方法
2019/08/25 Javascript
vue使用@scroll监听滚动事件时,@scroll无效问题的解决方法详解
2019/10/15 Javascript
vue不操作dom实现图片轮播的示例代码
2019/12/18 Javascript
简洁的十分钟Python入门教程
2015/04/03 Python
分析并输出Python代码依赖的库的实现代码
2015/08/09 Python
python win32 简单操作方法
2017/05/25 Python
Collatz 序列、逗号代码、字符图网格实例
2017/06/22 Python
Python利用PyExecJS库执行JS函数的案例分析
2019/12/18 Python
python获取依赖包和安装依赖包教程
2020/02/13 Python
Python 炫技操作之合并字典的七种方法
2020/04/10 Python
使用html2canvas实现浏览器截图的示例代码
2018/01/26 HTML / CSS
Web前端页面跳转并取到值
2017/04/24 HTML / CSS
缓解脚、腿和背部疼痛:Z-CoiL鞋
2019/03/12 全球购物
一些关于MySql加速和优化的面试题
2014/01/30 面试题
经销商订货会主持词
2014/03/27 职场文书
书香家庭事迹材料
2014/05/09 职场文书
回门宴新娘答谢词
2015/09/29 职场文书
golang gopm get -g -v 无法获取第三方库的解决方案
2021/05/05 Golang
Nginx进程调度问题详解
2021/09/25 Servers
微信小程序 WeUI扩展组件库的入门教程
2022/04/21 Javascript