Authors: Gao Cong, Perry 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 吧,或许它也能让你从维护多平台的困境中解脱出来。
嘿,这儿是墨刀技术团队的官方网站,你会在这里发现墨刀背后的技术故事。
原文地址:https://www.cnblogs.com/productcompass/p/10777103.html