专栏名称: 前端外刊评论
最新、最前沿的前端资讯,最有深入、最干前端相关的技术译文。
目录
相关文章推荐
商务河北  ·  经开区“美•强•优”三重奏 ·  10 小时前  
奇舞精选  ·  从 DeepSeek 看25年前端的一个小趋势 ·  昨天  
奇舞精选  ·  从 DeepSeek 看25年前端的一个小趋势 ·  昨天  
前端早读课  ·  【第3452期】React 开发中使用开闭原则 ·  昨天  
前端早读课  ·  【第3451期】前端 TypeError ... ·  2 天前  
51好读  ›  专栏  ›  前端外刊评论

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

前端外刊评论  · 公众号  · 前端  · 2019-08-09 08:53

正文

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

现有思路的局限

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

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

  1. xx && <Text>HelloText>

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

  1. wx:if="{{xx}}">Hello

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

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

  1. class App extends React.Component {

  2. render () {

  3. const a = <Text>HelloText>

  4. const b = a


  5. return (

  6. <View>

  7. {b}

  8. View>

  9. )

  10. }

  11. }

首先我们声明 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 对象,形如

  1. // ReactElement对象

  2. {

  3. tag: Text,

  4. props: null,

  5. children: 'Hello'

  6. ...

  7. }

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

  1. class App extends React.Component {

  2. render () {

  3. const a = {

  4. tag: Text,

  5. props: null,

  6. children: 'Hello'

  7. ...

  8. }

  9. const b = a


  10. return {

  11. tag: View,

  12. props: null,

  13. children: b

  14. ...

  15. }

  16. }

  17. }

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

新的思路

正因为编译时方案,有如上的限制,在使用的时候常常让你有“我还是在写 React 吗?”这种感觉。

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

第一步:给每个独立的 JSX 片段打上唯一标识 uuid ,假定我们有如下代码:

  1. const a = <Text uuid="000001">HelloText>


  2. const y = <View uuid="000002">

  3. <Image/>

  4. <Text/>

  5. View>

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

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

  1. const a = React.createElement(Text, {

  2. uuid: "000001"

  3. }, "Hello");

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

  1. Hello


  2. uuid="000002">



注意这里每一个 template name 标识和 JSX 片段的唯一标识 uuid 是一样的。最后,需要在结尾生成一个占位模版:

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

第五步:把第四步生成的 uiDes ,传递给小程序环境,小程序把 uiDes 设置给占位模版 ,渲染出最终的视图。

我们以上面的 App 组件的例子来说明整个过程,首先 js 代码会被转义为:

  1. class App extends React.Component {

  2. render () {

  3. const a = React.createElement(Text, {uuid: "000001"}, "Hello");

  4. const b = a


  5. return (







请到「今天看啥」查看全文