浅谈一种让小程序支持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 相关文章推荐
用prototype实现的简单小巧的多级联动菜单
Mar 24 Javascript
两种WEB下的模态对话框 (asp.net或js的分别实现)
Dec 02 Javascript
multiSteps 基于Jquery的多步骤滑动切换插件
Jul 22 Javascript
js 定时器setTimeout无法调用局部变量的解决办法
Nov 28 Javascript
浅谈javascript语法和定时函数
May 03 Javascript
javascript图片切换综合实例(循环切换、顺序切换)
Jan 13 Javascript
JQuery点击行tr实现checkBox选中的简单实例
May 26 Javascript
Bootstrap select实现下拉框多选效果
Dec 23 Javascript
Vue 动态设置路由参数的案例分析
Apr 24 Javascript
vue cli3 配置proxy代理无效的解决
Oct 30 Javascript
微信小程序换肤功能实现代码(思路详解)
Aug 25 Javascript
如何用threejs实现实时多边形折射
May 07 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的一个完整SMTP类(解决邮件服务器需要验证时的问题)
2006/10/09 PHP
基于PHP读取csv文件内容的详解
2013/06/18 PHP
php使用指定编码导出mysql数据到csv文件的方法
2015/03/31 PHP
thinkphp autoload 命名空间自定义 namespace
2015/07/17 PHP
在openSUSE42.1下编译安装PHP7 的方法
2015/12/24 PHP
php PDO判断连接是否可用的实现方法
2017/04/03 PHP
Yii2压缩PHP中模板代码的输出问题
2018/08/28 PHP
YII框架行为behaviors用法示例
2019/04/26 PHP
最好用的省市二级联动 原生js实现你值得拥有
2013/09/22 Javascript
jQuery对html元素取值与赋值的方法
2013/11/20 Javascript
快速解决jquery之get缓存问题的最简单方法介绍
2013/12/19 Javascript
jQuery判断div随滚动条滚动到一定位置后停止
2014/04/02 Javascript
jQuery ajax serialize() 方法使用示例
2014/11/02 Javascript
Javascript中获取对象的原型对象的方法小结
2015/02/25 Javascript
详解iframe与frame的区别
2016/01/13 Javascript
基于原生JS实现图片裁剪
2016/08/01 Javascript
Jquery组件easyUi实现表单验证示例
2016/08/23 Javascript
JS作用域闭包、预解释和this关键字综合实例解析
2016/12/16 Javascript
JavaScript中最常见的三个面试题解析
2017/03/04 Javascript
bootstrap的常用组件和栅格式布局详解
2017/05/02 Javascript
VueJs监听window.resize方法示例
2018/01/17 Javascript
AngularJS与后端php的数据交互方法
2018/08/13 Javascript
javascript中导出与导入实现模块化管理教程
2020/12/03 Javascript
three.js中多线程的使用及性能测试详解
2021/01/07 Javascript
Python通过命令开启http.server服务器的方法
2017/11/04 Python
Python标准库inspect的具体使用方法
2017/12/06 Python
Python使用matplotlib绘制正弦和余弦曲线的方法示例
2018/01/06 Python
纯css3实现宠物小鸡实例代码
2018/10/08 HTML / CSS
Perfume’s Club美国官网:西班牙第一家在线美容店
2020/06/10 全球购物
构造器Constructor是否可被override?
2013/08/06 面试题
面向对象编程的优势是什么
2015/12/17 面试题
专业实习自我鉴定
2013/10/29 职场文书
大学生的网络创业计划书
2013/12/26 职场文书
幼教简历自我评价
2014/01/28 职场文书
征用土地赔偿协议书
2014/09/26 职场文书
《世界多美呀》教学反思
2016/02/22 职场文书