点击蓝字 关注我们
作者简介
冯地木
去哪儿网前端技术总监
08年开始从事前端工作,曾就职于人人网、新浪微博、爱奇艺等,12年加入去哪儿网,主导多次前端技术架构升级,推进Node前后端分离,React,RN等技术架构在业务上的落地应用,擅长各种前端性能优化和工程化实践,目前负责推进 Qunar 的多端统一化升级方案。
孟晨
2019年加入去哪儿网,擅长多种前后端语言,主要负责Node工程化,规范化工作,现阶段工作重心为多端统一框架的开发与推广。
王佳峰
2019年加入去哪儿网,主要负责公共产品业务向RN技术栈迁移以及主流程RN工程向三端工程迁移。主要涉及React-Native、Touch、小程序业务开发和多端融合框架等基础架构的研发。
1 前言
qrn-remax-unir 是由去哪儿网前端技术团队实现的一套将 RN 适配到小程序端的跨端组件,通过该组件库可快速方便的将 RN 源代码直接运行到小程序端。方案参考了 react-native-web 的适配方案,使用 remax 框架来实现适配组件库并达到适配多小程序的目的。和 react-native-web一样,它对 RN 源代码侵入度低,并且调试和替换组件相当方便。
方案来自于社区,我们只是合理的应用用来解决我们遇到的业务问题,最终开发了这套组件库,也希望开源出来跟大家分享和共同探讨多端开发之路。
下面我们简单介绍下 RN 到小程序的适配方案和适配组件库的点滴。
2 多端方案回顾
qrn-remax-unir 组件库是整个去哪儿网跨端方案的一个部分,通过整套方案可实现将 RN 代码直接运行到 H5 和小程序,达到跨 IOS,ADR,H5,小程序的目标。整体多端方案的思路是:
-
使用 react-native-web 将 RN 代码适配到 H5 上运行,这也是 RN 官方推荐和集成的方式。
-
在 remax 组件的基础上实现一套 RN 的组件库,借用 remax 来适配到多端(qrn-remax-unir)。
-
借助 webpack 的特性来实现针对不同平台的打包。
3 RN 相关多端方案
3.1 对 alita 的调研
在最初的调研中我们注意到 alita 可以实现将 ReactNative 转换到微信小程序运行,也对方案进行了详细的调研:
-
alita 会在编译阶段将 JSX 语法按照代码块编译成微信 wxml 的碎片,然后用 templete 包裹;
-
内部实现了一个 mini-react 来对接适配 react 和小程序的生命周期。
详细原理可参考:
https://zhuanlan.zhihu.com/p/69333351
https://www.jqhtml.com/44555.html
于是跑了些 demo 对方案进行使用觉得还是跟我们的期望有些差距:
-
虽然期望是对 RN 代码无入侵,但还是会对 RN 源码有限制的,部分特性不能用,动画之类的要求用 alita 提供的自定义动画库;
-
内部使用了自定义的 mini-react 来对接 react 和小程序的生命周期,考虑后期对 react 新特性的适配进度风险;
-
专注于 RN 到微信小程序的转换,对其他小程序不支持。
基于以上原因我们最终没有直接使用该方案,调研过程感觉他们对 jsx 模板的处理思路还是很新颖的,是很好的借鉴。
3.2 remax 简单原理
remax 可实现在小程序中用真正的 react 进行开发。remax 将自己实现成了一个 react 的渲染器,收集宿主 UI 变更指令生成页面 VNode 树,然后在小程序渲染层渲染这个树。
详细原理可参考:https://juejin.cn/post/6844904131157557262
由于基于运行时适配的方案,有以下优缺点:
-
适配在 react 虚拟 dom 之上,对 react 的技术栈无任何限制,redux,mobx,hooks 等都可自由使用;
-
模板的动态处理在渲染的时候可能效率低下,需要针对性解决;
-
由于没有编译处理,运行时都是平台源代码,查问题更方便;
-
基于 react 组件,可以很好的对小程序原生组件进行支持;
-
并没有提供对各个小程序的跨端组件,只对部分基础组件进行了封装和适配。
4 我们的适配方案
remax 虽然支持 remax 的语法,但是他是基于自定义组件提供的语法 DSL,提供的组件和 RN 也没有任何关系。但他们对 react 语法适配到小程序的方案还是很大胆实用的,也启发我们可以按照类似的适配方案来实现 RN 组件到小程序的适配,这样就可以将 RN 运行到小程序了。
4.1 ReactNative 到 remax 组件
调研 remax 的适配方案,虽然方案原理说起来很简单,但是真正要做这么一套东西评估了下还是非常耗时耗力的。
重新评估下来,发现 remax 其实已经可以做到 react 到小程序的适配,且提供了基础的几个跨端组件可供使用。我们直接用 remax 组件来实现 RN 的组件则可以快速的适配到小程序端,虽然可能带来部分性能损耗。
看个将 RN 的 Text 组件适配到 remax 组件的例子:
4.2 ReactNative API 的适配
对与 API 的实现要更为简单直接一些,这里请允许我将 react-native 的组件分为两类:
其一是纯函数的 api,简单理解就是你调用了一个由框架提供的函数,得到了你想要的数据写入或返回,比如 Dimensions,这一类普通 api,qrn-remax-unir 的处理方案就是能够通过封装调用小程序提供的 api 并存在两个平台磨平参数差异可能的即可完美适配。对无法通过调用小程序 api 来实现同样效果的或小程序场景逻辑上就不存在的,qrn-remax-unir 往往通过空实现并对开发者加以告知。
其二是调用一个有框架提供的方法,在页面上进行呈现,我们姑且叫它 api 组件(react-native 中是 api、但在小程序平台更像是组件),例如:Alert、ToastAndroid、ActionSheetIOS 等,这一类 api 的实现方案是构造展示的模板组件,暴露出方法能够间接修改组件的状态,例如 alert 就是通过获取调用方法传入的内容进行填充,当获取用户操作后修改为隐藏不显示的状态并回调组件的回调方法。
4.3 复杂组件和 API 的适配
复杂组件和第三方组件其实都是对基础元素组件的组合使用,我们从最底层的基础组件(View Text 等)完成适配,这样大部分复杂组件都可以直接适配到小程序端。如下图只要 qrn-remax-unir 的基础组件适配得足够完整就可以保证对上层 RN 业务和组件的完整适配。
5 开发过程
虽然开发思路大体清晰了,但在开发过程中还是遇到了一些比较棘手的问题。
5.1 样式处理
对于将 RN 的样式转换成小程序端能用的样式,我们一开始确定了两个方向:
-
编译时提取并转换
-
运行时动态生成
编译时提取并转换
这个方式其实是我们一开始经过讨论觉得可行的方案,具体方式就是通过编写 Webpack 以及 Babel 插件,实现在编译阶段对样式相关对 RN 代码进行抽取并生成最终的 WXSS 文件。
这个方案的好处就是:
静态编译出的文件可以更好的进行管理,对于样式转化出的问题可以直观的进行修改,相当于有一个可见的原生产物去进行调试,对于开发者显得更友好,更直观。
实现的效果理想中是这样的。
对于源代码:
const styles = StyleSheet.create({
container:{
width:100,
height:100
}
})
<View style={styles.container}>
<Text>Hello World</Text>
</View>
处理后生成的 styles.wxss 文件中:
.container-test {
width:100px,
height:100px
}
然而在尝试开发好了相关 babel loader 以及 webpack 插件以后,面对真实的复杂项目时,却发现代码的书写复杂情况很难穷举解决。
因为 RN 代码的样式并不总是规矩的写成:
const styles = StyleSheet.create ({
key:value
})
在项目中由于一些样式的值,例如屏幕宽高等,需要在运行时获取,于是会有一些不规则的赋值形式:
const height = DeviceInfo.isAndroid?100:120;
const styles = StyleSheet.create ({
height
})
当出现这种动态获取运行时值的情况时,我们的这个策略就无法处理了,于是这个方案只能暂且作罢。
运行时动态生成
这种方案的基本实现方式其实是借鉴了 React-native-web 的做法,也许 Twitter 也尝试过编译时去生成 CSS 文件转换到 web 的方案,最后未果,才使用的运行时提取 RN 样式成 class 并赋值到 DOM 元素上的方案。当然,通过分析了 react-native-web 的源码库,我们做了一定程度的改动以适配到微信小程序的情况。具体做法如下:
以最基础组件 View 为例:
import { View , Text } from 'react-native';
```
<View style={{flex:1}}>
<Text>Hello World</Text>
</View>
```
进行完转换的 className 以及 style,会随同一些默认补全样式,传递到 Remax 的 View 组件中对于一个普调的 View 组件,我们按照 RN 正常书写习惯,对 style 赋值。我们适配层做的核心处理,就是将这些 styles 解析,并且转换和处理 Class 以及数组格式的样式,生成 classList 以及 style 变量。
const convertStyle = (styles) => {
const classList = [];
const style = {};
const convert = (value) => {
// 对于Class,直接增加进classlist
if (typeof value === 'string') {
classList.push(value)
} else {
// 对于数组,递归处理每一个元素
if (Array.isArray(value)) {
value.forEach(item => {
convert(item)
})
} else {
// 对于一般的属性,直接转换成行内样式
value && Object.assign(style, inline(value))
}
}
}
convert(styles)
return {classList, style}
}
进行完转换的 className 以及 style,会随同一些默认补全样式,传递到 Remax 的 View 组件中。
// 默认样式
supportedProps.className = "view-default " + classList.join(" ");
supportedProps.style = style;
supportedProps.id = this.props.id;
return (<RemaxView {...supportedProps}/>)
至此在运行时的 Dom 渲染,就会得到相应的 class 名以及行内 style 的值。
所以,对于编译时遇到的,那种 DeviceInfo 这类赋值,也会在运行时得到最终的值并添加到组件的 style 属性里,从而解决了编译时无法控制的事情。
样式小结与思考
对于样式的处理,有很多细节需要注意,例如对于不同端的单位转换问题,对于不同端一些默认样式的补全的设置等。此处重在方案的选择过程介绍,并未列出具体样式差异,还需要更多的人来贡献力量,持续投入,跟进 react-native 的版本以及 remax 的版本。而且对于 StyleSheet 的细节处理,感兴趣的同学也可以去查看 react-native-web 的源码,还是值得对跨端开发感兴趣的同学深入研究学习的。关于方案,我们也在思考是否可以更进一步提升,比如将编译和运行时结合,从而减少行内样式,目前这样直接写入行内样式还是会导致我们的 dom 元素上属性值过长,不可读,不利于开发调试。另一方面,这种过长的属性也使小程序端的性能表现受到了一定的影响。
5.2 对动画的处理
qrn-remax-unir 在实现动画方案上,采取了尽量不引入新的写法,与 react-native 原本写法尽量保持一致的思路,对 RN 代码做到零入侵。
这里准备了一个 demo,代码中的动画写法和 react-native 官方文档的写法是一致的,先看一下效果。
在定义 react-native 的单一动画是这样写的:
Animated.timing(
this.state.opacity,
{
toValue:1, // 1
duration:3000, // 2
easing:Easing.ease // 3
}
).start(); // 5
qrn-remax-unir 将上面的代码转换成类似下面的能够在小程序运行的代码,1-5的动画关键信息通过框架层处理成一一对应,过渡时间就是 react-native 的动画时间即 duration,opacity 就是 react-native 的 Animated 组件的动画样式 key 即 opacity、linear 就是 react-native 中的赋值 API 即 Easing(有一些复杂曲线难以适配会在小程序端做相应的降级处理,比如使用 linear),初始值和目标值直接传递过去就可以了再利用插值器将原始单位正则解析出来放如动画属性值中,产物就是这样的 transition: 3000ms opacity cubic-bezier(0.42, 0, 1, 1);
组合动画、平行动画和顺序延迟动画都是由 Animated.timing 组成动画序列,由 react-native 的 Animated 本身的处理机制处理多个动画触发和销毁。
6 组件库的使用
组件库地址:https://github.com/qunarcorp/qrn-remax-unir
组件 demo 地址:https://github.com/qunarcorp/qrn-remax-unir-demo
组件文档地址:https://qunarcorp.github.io/qrn-remax-unir/#/./
想要了解具体如何使用我们的组件库,可以下载我们的示例工程进行开发学习:传送门(https://github.com/qunarcorp/qrn-remax-unir-demo/tree/master)
这个工程内集成了 RN,web,微信小程序三端的编译开发能力,并且启动十分简单,你可以基于这个项目进行跨端项目的改造。此项目开发时效果如下图(自左至右:RN->Web→微信小程序):
项目启动过程很简单,进入到项目根目录下:
1、安装依赖
yarn
2、运行 dev 环境
yarn dev //RN,web环境
yarn dev:wx //微信小程序,需要用小程序IDE打开./dist/wechat目录进行调试开发
此时修改代码就可以实时热更新每一端的代码,是不是很方便!赶快开启你的跨端开发之旅吧!
如果你想了解具体搭建过程,也可以参考我们 github 网站上的介绍:传送门(https://qunarcorp.github.io/qrn-remax-unir/#/./),尝试自己去动手改造出这样一个跨端项目。
END
文章评论