Authors:  Gao CongPerry Poon
Illustrators:  Shena Bian

April 20, 2019

重新编写,又一次,我们又一次重新编写了移动端应用和移动端网站。要重新编写是一个风险很大的决定,但是其必要性以及它所带来的收益是我们无法拒绝的。这篇文章会分享我们为什么这么做,我们是怎么做的,以及这次重写后为我们带来了什么收获。如果你也正在经受和我们一样的遭遇,希望这个分享会带来帮助。

欢迎在 https://org.modao.cc/downloads 下载应用及访问 https://org.modao.cc/mobile 体验我们全新的移动端应用、网站

动机

团队的处境是,我们有使用 Swift 开发的 iOS 客户端,基于 React Native 的 Android 客户端,以及应对国内庞大的微信生态的移动端网站。而墨刀的技术团队保持着精简的人员结构,团队成员不足10人,同时我们的技术栈也主要分布在 JS 和 Ruby,没有全职移动客户端经历的成员。维护这三个相互独立的应用对我们来说是一件成本极高的事情。

墨刀在18年开始了 To B 的更进一步转变-墨刀企业版的开发。新的版本中由于全新的产品架构,应用自然而然地也要经历大规模的改变。同时,在新版中,我们开始更近一步地使用了我们自己的设计系统,而新的设计语言涵盖了用户在各平台的使用场景,这是一套体验、界面一致的设计规范。

因此,重新开发,统一墨刀的移动端应用,移动端网站成为了一件自然而然的想法。于是我们开始了对这一想法的实践。

找到一个可行的方案

为了满足我们同时支持的三个平台愿景,一番搜索后这几个框架进入了我们的视野:

如雷贯耳的 React Native 和如日中天的 Flutter 就不再介绍了,我们重点介绍下 ReactXP。ReactXP 是微软的 Skype 团队研发的框架,它的官方介绍其似乎是一种究极的跨平台解决方案,在兼容 iOS、Android 和 web 的同时甚至还兼容了 Windows 桌面应用开发。相较于 React Native 配合 React Native for Web 的那种需要在构建过程做些定制化才能实现的写一次,部署到各个平台的方式,ReactXP 则更为彻底,开发者几乎不需要做什么的配置就达到目的。这是一个很吸引人的点。而 Flutter 对 web 的支持目前未实现。所以在跨平台这一特性上,ReactXP 引起了我们的注意。

为了刚进一步地了解 ReactXP,我们通读了 ReactXP 的源码。ReactXP 的源码其实不多,而且文件结构清晰,很容易了解。我们发现 ReactXP 在移动端其实只是一个抽象层,使用 TypeScript 开发。它在 web 上使用 React,Native 上使用 React Native,自己实现一套类似于 React Native 风格的 API。而功能上基本实现了 React Native 的各种组件及 API。构建方案在 Native 端用的就是 React Native 自带的打包工具 Metro,而在 web 端是 webpack。在最终的产出时,在 native 平台则会打包出 .app 和 .apk;在 web 平台则是前端开发最熟悉的 SPA。同时,ReactXP 采用插件的形式,官方实现了一些跨平台的组件以满足更进阶的使用需求。而前面介绍到,墨刀团队在 React Native 上已经做过一个完整功能的原生应用,需求已被证明可以被满足,该踩的坑我们都已经填过,因此采用这个方案我们也能更有自信。

于是,我们便使用 ReactXP 开始对整个应用的重新编写。

过程比较流畅,但并非一帆风顺

因为有过之前的 React Native 使用经验,同时应用的主要功能没有发生变化,所以开发起来相对容易一些,但是我们也遇到了大大小小的问题,在这里分享一些令我们印象较深的以及我们的解决办法。

第三方的模块大多不是跨平台的

这个是我们最先遇到的问题。在引用了 react-native-wechat 来完成微信登录需求时,在 web 端首先就遇到了构建失败的问题,这也是跨平台开发中会经常碰到的问题,因为大多数的模块设计之初也没有考虑到这种跨平台的使用场景。

前面提到过 ReactXP 在 web 端使用的是 webpack 作为构建工具,于是我们想起来了 webpack 的 alias 特性。简而言之就是给模块一个别名,比如

# webpack.config.js
const WEB_MODULE_ALIAS_PATH = path.join(__dirname, '..', '..', 'web', 'alias')

module.exports = {
  resolve: {
    alias: {
      'react-native-wechat$': path.resolve(WEB_MODULE_ALIAS_PATH, 'react-native-wechat.ts'),
    },
  },
}

有了这样的配置后,webpack 就会在引用 react-native-wechat 的地方转而引用 /web/alias/react-native-wechat.ts。这样就允许我们在这个单独的文件中为 web 端 patch 掉 react-native-wechat

而有些时候采用这种方式向着一方 patch 接口是件很难实现的事。比如为了达到更好的用户体验和开发效率,我们为 native 选用了 React Navigation 而在 web 上使用 React Router。但是他们的 API 几乎没有一样的地方,于是我们采用了分文件的写法,如 router.tsx 和 router.web.tsx,同样采用 alias 的方法。二者都暴露出一个接口一致的组件,这样就能够满足跨平台的要求。

ReactXP 有对应用很多限制

在列表组件中,ReactXP 为了达到跨平台的目的而禁用了 RefreshControl 这个基本组件的使用,但这个是不能满足我们的下拉更新的需求。通过阅读源码我们发现 ReactXP 在调用 React Native 的 FlatList 组件时屏蔽掉了 RefreshControl 这个 Props,于是我们直接在 native 平台使用 React Native 的 FlatList 模块。

这类问题也发生在了样式中。在使用百分比形式的长度时,比如 width: '80%',TS 的类型检查就会报出不支持的类型错误。搜索后发现这是 ReactXP 因为性能原因而有意为之。但在实现一些设计时百分比是一个绕不开的话题,如果运行时手动用 js 计算又显得有些繁琐。同样阅读源码我们发现这种禁用只是在类型中,实际 ReactXP 在调用底层框架时依旧是将百分比的值传了下去。因此我们便能够自信地在不可避免的地方采用百分比的值还原设计。

React Native 的样式在一些地方不符合 CSS 的标准

在带阴影的圆角矩形的实现上,我们发现在 overflow: 'hidden' 的时候 box-shadow 在 native 平台是不生效的。其实这个问题和 ReactXP 无关,而是 React Native 遵循了 iOS 的渲染方式才出现了这个与 CSS 规范不相符的表现。但在编写跨平台的组件时,这个问题就变得非常明显。其实解决办法也很简单,就是再加一层 wrapper,在它身上应用 shadow 和设置 overflow 为 visible,而在自身节点控制圆角。

IconWrapper: RX.Styles.createViewStyle({
  shadowOffset: {
    width: 0,
    height: 3,
  },
  shadowRadius: 8,
  shadowColor: 'rgb(0, 0, 0)',
  shadowOpacity: 0.1,
  overflow: 'visible',
}),
Icon: RX.Styles.createViewStyle({
  elevation: 5,
  borderRadius: 9,
}),

带来的收获

虽然经历了种种的坑,但一个个填平后,ReactXP 为我们指引的这条路还是挺宽敞明亮的。我们共计两位开发,其中一名是之前没有移动开发经验的同事,在经历满打满算的5个月后,使用一套代码开发出了横跨3个平台的移动端应用并顺利发布。同时也保持着良好的用户体验。而这其中的在解决技术难题的时间其实只占了不到 30% 的时间。重要的是我们今后只用维护这一份代码,这是我们梦寐以求的。


跨平台应用虽然问题多多,但是并不是一条走不通的路。我们之所以能够成功使用 ReactXP 来完成我们的目标,很大原因是因为 ReactXP 并没有重造轮子,而是站在 React 和 React Native 这个两个巨人的肩上,使得我们在遇到问题时很容易地在适当的地方找到解决办法,而不是去花更多的时间去学习一个全新的体系。这也帮助我们降低了重新编写而带来的风险。

如果你也对跨平台抱有希望,那么尝试一下 ReactXP 吧,或许它也能让你从维护多平台的困境中解脱出来。

嘿,这儿是墨刀技术团队的官方网站,你会在这里发现墨刀背后的技术故事。

在 GitHub 上关注我们

跨平台技术实践案例: 用 reactxp 重写墨刀的移动端的更多相关文章

  1. 《SaltStack技术入门与实践》—— 实践案例 <中小型Web架构>3 Memcached配置管理

    实践案例 <中小型Web架构>3 Memcached配置管理 本章节参考<SaltStack技术入门与实践>,感谢该书作者: 刘继伟.沈灿.赵舜东 Memcached介绍 Me ...

  2. 子弹短信光鲜的背后:网易云信首席架构师分享亿级IM平台的技术实践

    本文原文内容来自InfoQ的技术分享,本次有修订.勘误和加工,感谢原作者的分享. 1.前言 自从2018年8月20日子弹短信在锤子发布会露面之后(详见<老罗最新发布了“子弹短信”这款IM,主打熟 ...

  3. 王晶:华为云OCR文字识别服务技术实践、底层框架及应用场景 | AI ProCon 2019

    演讲嘉宾 | 王晶(华为云人工智能高级算法工程师王晶) 出品 | AI科技大本营(ID:rgznai100) 近期,由 CSDN 主办的 2019 中国AI 开发者大会(AI ProCon 2019) ...

  4. ASP.NET跨平台最佳实践

    前言 八年的坚持敌不过领导的固执,最终还是不得不阔别已经成为我第二语言的C#,转战Java阵营.有过短暂的失落和迷茫,但技术转型真的没有想象中那么难.回头审视,其实单从语言本身来看,C#确实比Java ...

  5. 让互联网更快:新一代QUIC协议在腾讯的技术实践分享

    本文来自腾讯资深研发工程师罗成在InfoQ的技术分享. 1.前言 如果:你的 App,在不需要任何修改的情况下就能提升 15% 以上的访问速度,特别是弱网络的时候能够提升 20% 以上的访问速度. 如 ...

  6. 让AI简单且强大:深度学习引擎OneFlow技术实践

    本文内容节选自由msup主办的第七届TOP100summit,北京一流科技有限公司首席科学家袁进辉(老师木)分享的<让AI简单且强大:深度学习引擎OneFlow背后的技术实践>实录. 北京 ...

  7. 腾讯技术分享:GIF动图技术详解及手机QQ动态表情压缩技术实践

    本文来自腾讯前端开发工程师“ wendygogogo”的技术分享,作者自评:“在Web前端摸爬滚打的码农一枚,对技术充满热情的菜鸟,致力为手Q的建设添砖加瓦.” 1.GIF格式的历史 GIF ( Gr ...

  8. IPv6技术详解:基本概念、应用现状、技术实践(下篇)

    本文来自微信技术架构部的原创技术分享. 1.前言 在上篇<IPv6技术详解:基本概念.应用现状.技术实践(上篇)>,我们讲解了IPV6的基本概念. 本篇将继续从以下方面展开对IPV6的讲解 ...

  9. IPv6技术详解:基本概念、应用现状、技术实践(上篇)

    本文来自微信技术架构部的原创技术分享. 1.前言 普及IPV6喊了多少年了,连苹果的APP上架App Store也早已强制IPV6的支持,然并卵,因为历史遗留问题,即使在IPV4地址如果饥荒的情况下, ...

随机推荐

  1. 集腋成裘-09-ECharts -HelloECharts-02

    上一篇我们讲了可视化数据分析的重要性以及ECharts的一些的特性,这一篇我们继续学习一下ECharts的简单Demo 一:5 分钟上手 ECharts <!DOCTYPE html> & ...

  2. burp基本使用

    记录一个burp最基础的小白使用过程: 以firefox为例设置一下代理,代理到127.0.0.1:8090 设置Burp的相关: 1.为Burp添加一个代理ip和端口:如 127.0.0.1:809 ...

  3. spring boot vuejs

    项目介绍 Spring Boot 2.0 + VueJs 2.0 开发项目示例 GitHub: https://github.com/fqybzhangji/spring-boot-vue 码云: h ...

  4. Codeforces Round #524 (Div. 2) F

    题解: 首先这个东西因为强制在线区间查询 所以外面得套线段树了 然后考虑几条线段怎么判定 我们只需要按照右端点排序,然后查询的时候查找最右节点的前缀最大值就可以了 然后怎么合并子区间信息呢 (刚开始我 ...

  5. SQL Server 中执行Shell脚本计算本地文件的内容大小

    SQL Server 数据库中除了能执行基本的SQL语句外,也可以执行Shell脚本.默认安装后,SQL中的Shell脚本的功能是关闭的,需要手动打开, 执行以下脚本即可打开该功能. -- 允许配置高 ...

  6. python运算符——比较运算符

    比较运算符的运算结果会得到一个bool类型,也就是逻辑判定,要么是真True,要不就是False 大于“>”  小于“<”  不说了,看看不等于,用“!=”表示.大于等于“>=”和小 ...

  7. Python3-Cookbook总结 - 第一章:数据结构和算法

    第一章:数据结构和算法 Python 提供了大量的内置数据结构,包括列表,集合以及字典.大多数情况下使用这些数据结构是很简单的. 但是,我们也会经常碰到到诸如查询,排序和过滤等等这些普遍存在的问题. ...

  8. 【redis】-- springboot集成redis及使用

    springboot自动配置的redis并不是特别好用,所以需要我们使用原生的jedis, 1.添加依赖 2.在application文件中配置 # Redis服务器地址 redis.host= # ...

  9. 微信小程序做radio,可以拖动进度条

    很简单的一个音乐播放器 data:{ src: 'http://ws.stream.qqmusic.qq.com/M500001VfvsJ21xFqb.mp3?guid=ffffffff82def4a ...

  10. django中models field详解

    本文参考自:django官方文档models/field 在model中添加字段的格式一般为:  field_name = field_type(**field_options) 一  field o ...