来这里找志同道合的小伙伴!
严康
京东商城前台产品研发部资深前端工程师,负责JDReact框架前端及小程序转换引擎开发
臧国东
京东商城前台产品研发部前端工程师,负责JDReact框架前端及小程序转换引擎开发
概述
JDReact是京东商城前台产品研发部推出的多端融合开发框架。经过不断的技术完善,目前已经在手机京东客户端累计接入100+业务,稳定支撑千万级DAU,并对外支持15+个独立APP,拥有完善的API和功能强大的开发IDE工具。
本文重点介绍了JDReact提供的小程序双向转换工具的原理及用法,通过此工具可以把已经开发的微信小程序低成本转换成JDReact应用,也支持把现有JDReact业务低成本转换成微信小程序应用,完全实现了JDReact和微信小程序生态的打通。
背景
此项目的最初灵感来源于我们团队今年5月份参加京东第六届黑客马拉松大赛并获得冠军的项目“微信小程序一键转换工具” 。
因为我们在进行项目开发时,常常会遇到以下情况:
项目之初,为了更好的利用微信的流量,更加方便的推广,我们会先直接发布一个小程序,等到后来我们的用户越来越多,应用也变得越来越复杂。这个时候想要把这个应用独立出来,把客户掌握在自己手里,进一步定制应用。此时,没有其他办法,我们只能叫上Android,IOS开发人员,叫上之前的产品经理,之前的测试把之前小程序的功能再重新在原生上实现一遍。
也可能,我们现在已经有了独立App,现在由于种种原因(流量的需求,运营的需要),需要发布一个小程序的版本。怎么办呢?同样我们只能叫上小程序开发人员,之前的产品经理,之前的测试在复制一个小程序的版本。
或者,我们现在即将开始一个新的项目,这个项目既有独立App也有小程序版本(或者可见的未来会有两个版本)。 那么我们是不是需要保持原生团队, 小程序团队,从而进行两个版本的开发呢?
如果我们可以把JDReact的应用转化为小程序,把小程序转化为JDReact应用,那么我们就可以低成本的把原来的JDReact项目/小程序项目移植到另一端了。而且新开始的项目我们就可以根据人员配置只开发一个JDReact的版本或者小程序的版本,等未来需要的时候,直接转化为对应的另一端。
由于只需要维护一端的版本,就可以大大的降低软件工程师的工作,同时产品,测试的工作量也会相应的减轻很多。另外, 我们希望转化之后的代码具有良好的可读性, 方便再次开发与修改。
效果演示
我们先用一个实际的例子来展示下转化工具的效果, 我们利用JDReact转换工具将 “值得买京东优选”的微信小程序转化为对应的JDReact版本并运行在手机京东客户端中;
首先看一下微信小程序版“值得买京东优选”
转化引擎将会遍历寻找小程序源代码目录中的wxml,进行转化期间会合并其对应的wxss, json, js文件。
转化完成之后,启动生成的JDReact原代码目录,运行模拟器查看效果。
以下是运行中手机京东客户端中的JDReact版“值得买京东优选”
原理介绍
不管是React应用还是小程序应用都可以表达为:ui = f(data)。并且他们提供很相似的数据更新方式,小程序是setData(newData, cb)
, React是 setState(newState,cb)
,这两个基本条件是我们转化引擎的前提,基于此前提,转化工作理论上是可行的。
f在React里面可以简单的理解为JSX,在小程序里面可以理解为wxml。wxml是小程序提供的“静态”的书写ui的方式灵活性比较低。JSX是react提供的方式,很灵活,里面可以嵌入任何表达式,本质上就是JS。如果我们可以把JSX代码翻译为等效的wxml代码,把wxml代码翻译为等效的JSX代码,那么我们就有能力实现两种应用的转化。
显然,我们的引擎必须能够“读懂”代码,为了实现这个目标,首先我们将代码转化为AST格式,然后根据相应规则不断的修改AST结构,最后生成新的代码。通过babel-parse(把源代码解析为AST),babel-traverse(遍历操作AST),babel-generator(生成新代码)来实现对源代码的操作。这个相应规则源自于两端的等效写法。 比如说: wx:for 和 Array.map的对应, wx:if和逻辑表达式的对应。
然而,并不是所有的规则都这么显而易见
比如JSX
getView() {
return <View/>
}
...
<View>
{this.getView()}
</View>
对于这种情况我们会不断遍历JSX表达式,如果发现是函数调用,将会用“返回值替换”, 也就是会用getView的返回值来替换对应JSX表达式,替换的时候需要处理好数据绑定。
再比如下面的JSX
render() {
const a = this.state
const b = this.f(a)
return (
<View>
{this.h(b) && <View/>}
<View>
)
}
wxml的变量绑定“{{}}”是不能出现函数调用(wxs除外)的,这种情况,我们将会使用“表达式前置”, 也就是相应表达式的值提前放置在小程序的data中,转化之后的wxml可能如下:
Page({
data: {
a: ,
var1: h(f(a))
}
...
})
/// wxml
<view>
<view wx:if="{{var1}}" />
</view>
遍历AST的时候,需要不断的判断JSX表达式
是否需要前置。最后转化之后的data会保护很多这样的var。
由于JSX的足够灵活,在进行JSX转向wxml,我们将会有很多类似的转化规则。
那是不是 wxml转JSX就一帆风顺呢? 也不是, 首先一个问题。babel-parse并不识别wxml代码格式。对于wxml,我们需要预处理wxml, 使其可以被parse识别。另外wxml的很多奇怪表现也是我们转化的时候需要兼容的。
比如:
Page({
data: {} // 空对象
})
/// wxml
<view wx:if="{{a.b.c.d}}">hi</view>
小程序的data里面并没有a属性,更别说b属性,c属性。但是这个在小程序里面是表现正常的,而且很常见。我们不希望转化之后的程序在这种情况下报错,我们对这种表达式进行了容错,react-native(预计0.56版本)支持optional-chaining之后,我们也会跟进用optional-chaining来改造这种情况。
wxml到JSX的转化,我们已经基本完成。JSX到wxml的转化已经覆盖所有常见的写法。 随着越来越多的规则被添加进来,我们转化引擎能够覆盖的情况将会越来越多。
wxml与JSX的双向转化成功,是转化引擎的第一步。
但是转化引擎应用于实际项目还有一段距离,因为不管是小程序项目还是JDReact项目都不可能只有View, Text组件, 即使我们把users && <FlatList/>
转化为小程序 <FlatList wx:if="{{users}}"/>
也是没有作用的,小程序根本就不认识FlatList。 要想让小程序认识FlatList,我们需要在小程序端实现一个小程序版的FlatList,好在发展到今天,小程序的自定义组件已经很完善。
意味着我们需要对齐两端组件,需要在小程序端实现一套JDReact的组件库,包括FlatList, SectionList,JDImage,JDSwiper等,同时实现组件的对应属性。 在React Native端,我们也必不可少的需要实现一套这样的小程序组件,包括 form,radio, radio-groupd等。实际上出于对齐属性的考虑,包括view/View, text/Text这些基本组件,也是通过在另外一端实现对应组件这种方式实现的。
对齐小程序组件库:
对齐React Native 和 JDReact组件库:
data驱动视图, 生命周期和事件提供了对data修改的时机。小程序的组件提供了与React相似的生命周期。
小程序自定义组件生命周期:
React的生命周期:
对于两端意义相同的生命周期,比如ready和componentDidMount,会在遍历AST的时候进行修改,对于那些React存在,小程序不存在的生命周期我们会在小程序调用setData前后进行模拟。
另外,小程序的Page具有和组件不一样的生命周期,其中有些比如 onShow,onHide需要和导航器配合实现。
小程序的事件系统源自于web,而RN是自己有一套独立的手势系统,这两种有一定差异。 明显的,小程序的每一个组件都可以响应事件,而RN的组件一般只是Touchable** 系列的组件响应事件。
对于这种情况,我们会检测每一个小程序组件,一旦发现组件响应了事件,就给对应的RN组件加上手势系统, 另外一个比较大的差异,RN的事件是不冒泡的。 除了这些差异, 两边的事件基本是可以对应上的,比如bindtap对应onPress。处理方式和生命周期大同小异。
如果说React Native转化为小程序难点是要处理JSX的灵活,那么小程序项目转化为React Native的坑就是样式了。小程序的wxss源自于css,基本上是css的全集。而React Native采用Yoga作为样式布局系统,Yoga是基于C实现的一套Flexbox布局系统。
所以,在进行小程序样式转化时,原有的小程序wxss代码必须进行适配才可以接入到RN项目中,产生效果,适配过程主要需要解决下面几个问题。
1. RN不支持CSS选择器
在React Native中为一个元素指定某种样式,只可采用如下方式:
<View style={styles.a}/>
const styles=StyleSheet.creatSheet({
a:{
color:'red'
}
})
在React Native中,只可以通过为某元素明确style来赋予样式,在小程序以及web中,样式赋予则非常的灵活,作为一个简单的例子,
<div id='test' class='a'/>
此时只需要在对应的css文件中写入
div.a{
color:red
}
在这个例子中,我们用到了css提供的元素选择器(div),类选择器(.a)。css提供了数十种选择器,功能十分强大。然而RN中却没有支持任何一种选择器,因此在进行小程序样式转化前,首先要考虑如何适配小程序的css的选择器功能。
css提供了数十种选择器,且各类选择器间的组合非常灵活,而究其根本,其最基本元素仅有五种:
其余类似于后代选择器之类则可以看作连接符,例如对于
div .a{
color=red
}
因此大多数的CSS组合可以看作 [基本元素,连接符,基本元素…] 的形式,考虑到这一点后,我们进一步研究发现,其实所有基本类型选择器都可以由某个标签的标签名,以及prop属性来获取,而所有连接符关系,都可以通过元素在小程序wxml文件中的文档结构来进行计算匹配,我们通过抽象语法树的方式解析wxml文件,为每个元素注入了它自身在文档结构中的信息,来进行选择器的计算适配,目前已经提供了近10种最常用的选择器类型,且功能在不断的完善与扩展。
2. CSS写法的不一致
RN与小程序对于CSS中的写法差异较大。选择器方面,小程序CSS中选择器名可以为相对随意的字符串,例如’test-a¥b’也是有效的选择器名,而在RN中,这并不是一个有效的变量命名,因此我们在RN中,我们将所有的选择器名定位字符串类型,例如上述选择器名将转为
"test-a¥b":{}
因此可以完整适配小程序中任意的命名方式。
另一方面,在属性上存在写法不一致的情形。例如,小程序中写为border-width,而适配到RN中,则需要转化为borderWidth,不仅如此,对于一些简写的属性,例如小程序CSS中的
div {margin 10px 0;}
有着明确的语意,然而在RN中,无法解析这样的语法,我们也对此进行了转化,例如对于上述情形,我们在RN中解析并转化为了
"div":{
marginTop : 10px;
marginBottom : 10px;
marginLeft : 0;
marginRight : 0;
}
在RN中与小程序还有众多写法不一致的情形,对此我们尽最大可能提供了支持,并给出了规范。
3. 在RN与CSS中存在属性默认值的不同
RN与小程序CSS存在很多属性默认值的不同,这就导致了,即使选择器适配功能完好,同样的CSS代码,在小程序上表现正常,RN上则显示不正确。
比如,RN中采用flex布局,其flex方向默认为列布局,而在小程序CSS则默认为行布局。又如,RN中的flexShrink默认值为0,小程序CSS中则为1,这会导致页面展示的不正常。
对此,我们提供了适配方案,首先我们会对小程序开发者提出一些基本要求,例如必须采用flex布局方式。另一方面,我们会对于每个RN中与小程序CSS中默认值存在差异的情况进行修正,尽可能让小程序开发者不改变自己的CSS写法。对于上述两种情形,我们都会提供出具体的规范。
我们仔细研究了小程序CSS与RN中CSS的不同,并在最大程度上适配了小程序CSS的写法,让用户可以自由使用小程序CSS的各项功能,这一切都是为了让开发者获得更好的开发体验。
另外,为了提供更好的服务,我们制定了具体的规范,确保小程序开发者在现有规范下开发完成后,转化前与转化后页面展示完全一致。
css转化流程总结如下:
有的时候, 知道我们“做不到什么”更加重要。
由于AST只是静态分析代码,许多“运行时”才能够得到的信息是得不到的,比如:
getView() {
let a = null
...
...
return a
}
<View>
{this.getView()}
</View>
这种情况,我们根本不知道a
到底是什么, “返回值替换” 就会出问题。
又比如:
import React, { Comonent } from 'react'
const ForFun = Component
class X extends ForFun {
}
这里的ForFun会直接导致我们判断React组件失败,代码需要自律!
React中的高价组件暂时不支持转换,并且我们目前只支持React Native官方组件和JDReact通过的组件。
两边系统的差异和限制,在小程序端,比如小程序的包大小要在2M以内, 那么当JDReact转化过来的小程序打完包也必须在2M以内, 比如小程序的tab页个数,路由深度也是有限制的,
另外,前文提到的,在小程序向React应用转化的时候,对小程序本身所使用的样式是有限制的。
在RN端,小程序的scroll-view是可以上下左右滚动的,而RN的只可以一个方向, 事件处理的差异等等。
对于所有的这些限制和约束,我们后续会给出一份完整的清单,同时也会给出相应的替换方案。
合作共赢
目前我们已经初步实现了反向转换引擎,正向转换引擎也正在加速开发中。非常欢迎有兴趣的个人或团队和我们交流合作,合作试用可以在公众号下方留下您的联系方式!!!
---------------END----------------
后续的内容同样精彩
长按关注“IT实战联盟”哦
注意:本文归作者所有,未经作者允许,不得转载