随着今年的双十一落下帷幕,京喜(原京东拼购)也迎来了首捷。双十一前夕微信购物一级入口切换为京喜小程序,项目顺利经过近亿级的流量考验,在此与你们分享一点本身参与的工做。css
在接手项目前,京喜业务已在线上稳定运行较长时间。但通过一段时间迭代维护后,发现首页存在如下问题:html
综上所述,京喜迎来一次改版契机。前端
从前端角度来看,本次改版要实现如下目标:vue
京喜业务拥有很是丰富的产品形态,涵盖了 H五、微信小程序以及独立 APP 三种不一样的端,对支持多端的开发框架有着自然的需求。react
在技术选型上,咱们选择团队自研的 Taro 多端统一开发解决方案。android
Taro 是一套遵循 React 语法规范的多端开发解决方案。现现在市面上端的形态多种多样,Web、React-Native、微信小程序等各类端大行其道,当业务要求同时在不一样的端都要求有所表现的时候,针对不一样的端去编写多套代码的成本显然很是高,这时候只编写一套代码就可以适配到多端的能力就显得极为须要。webpack
使用 Taro,咱们能够只书写一套代码,再经过 Taro 的编译工具,将源代码分别编译出能够在不一样端(微信/百度/支付宝/字节跳动/QQ 小程序、快应用、H五、React-Native 等)运行的代码。git
选它有两个缘由,一来是 Taro 已经成熟,内部和外部都有大量实践,内部有京东 7FRESH、京东到家等,外部有淘票票、猫眼试用等多个案例,能够放心投入到业务开发;二来团队成员都拥有使用 Taro 来开发内部组件库的经验,对业务快速完成有保障。github
因为首页改版的开发排期并不充裕,所以充分地复用已有基础能力(好比像请求、上报、跳转等必不可少的公共类库),能大量减小咱们重复的工做量。话虽如此,但在三端统一开发过程当中,咱们仍遇到很多问题同时也带来解决方案,如下咱们一一阐述。web
咱们全部的页面都依赖现有业务的全局公共头尾及搜索栏等组件,这就不可避免的须要将 Taro 开发流程融入到现有开发和发布流程中去。同时公共组件都是经过 SSI 的方式引入和维护的,为了能在运行 npm run dev:h5
时预览到完整的页面效果,须要对 index.html
模版中的 SSI 语法进行解析,index.html
模版文件代码结构大体以下:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"> <title>京喜</title> <!--#include virtual="/sinclude/common/head_inc.shtml"--> </head> <body> <div id="m_common_header" style="display:none;"></div> <!--S 搜索框--> <div id="search_block" class="search_block"></div> <div id="smartboxBlock" style="display:none;"></div> <!--E 搜索框--> <div id="app" class="wx_wrap"></div> <!--#include virtual="/sinclude/common/foot.shtml"--> </body> </html>
能够看到模版中存在不少相似 <!--#include virtual="..." -->
格式的代码,这些就是经过 SSI 方式引入的 H5 公共组件,它的 virtual
属性指向的文件不存在于本地而是存在于服务器上的,因此咱们遇到的第一个问题就是在本地解析这些文件,确保能预览到完整的页面效果,否则开发调试起来就很是的低效。好在 Taro 有暴露出 webpack 的配置,咱们能够经过引入自定义加载器(这里就叫 ssi-loader
)来解析这些代码的路径,而后请求服务器上的文件内容并进行替换便可,要实现这个功能只需在项目的 config/dev.js
中加入以下代码便可:
module.exports = { h5: { webpackChain(chain, webpack) { chain.merge({ module: { rule: { ssiLoader: { test: /\.html/, use: [ { loader: 'html-loader' }, { loader: 'ssi-loader', options: { locations: { include: 'https://wqs.jd.com' } } } ] } } } }) } } }
这样就解决了本地开发调试难点,而后开开心心的进行页面开发。
当页面开发完成以后,接下来遇到的问题就是如何将前端资源部署到测试和生产环境。因为现有开发和发布流程都是基于内部已有的平台,咱们临时定制一套也不太现实,因此须要将它融入到 Taro 的流程中去,这里咱们引入了 gulp
来整合各类构建和发布等操做,只要构建出符合发布平台规范的目录便可利用它的静态资源构建、版本控制及服务器发布等能力,这样咱们就打通了整个开发和发布流程。
这套拼凑起来的流程还存在很多的问题,对于新接手的同窗有一点小繁琐,有着很多改善的空间,这也是接下来的重点工做方向。另外 Taro 的 H5 端以前是基于 SPA 模式,对于有着多页开发需求的项目来讲不太友好,当时反馈给 Taro 团队负责 H5 的同窗,很快获得了响应,目前 Taro 已支持 H5 多页开发模式,支持很是迅速。
因为开发完 H5 版以后,对应的业务逻辑就已经处理完了,接下来只须要处理小程序下的一些特殊逻辑(好比分享、前端测速上报等)便可,差别比较大的就是开发和发布流程。
这里讲一下如何在一个原生小程序项目中使用 Taro 进行开发,由于咱们的 Taro 项目跟已有的原生小程序项目是独立的两个项目,因此须要将 Taro 项目的小程序代码编译到已有的原生小程序项目目录下,第一步要作的就是调整 Taro 配置 config/index.js
,指定编译输出目录以及禁用 app 文件输出防止覆盖已有文件。
const config = { // 自定义输出根目录 outputRoot: process.argv[3] === 'weapp' ? '../.temp' : 'dist', // 不输出 app.js 和 app.json 文件 weapp: { appOutput: false } }
因为京喜之前是主购小程序的一个栏目,后面独立成了独立的小程序,可是核心购物流程仍是复用的主购小程序,因此这让状况变得更加复杂。这里仍是经过 gulp
来进行繁琐的目录文件处理,好比咱们的小程序页面和组件都须要继承主购小程序的 JDPage
和 JDComponent
基类,因此在进行文件复制以前须要进行代码替换,代码以下:
// WEAPP const basePath = `../.temp` const destPaths = [`${basePath}/pages/index/`, `${basePath}/pages/components/`] const destFiles = destPaths.map(item => `${item}**/*.js`) /* * 基类替换 */ function replaceBaseComponent (files) { return ( gulp .src(files || destFiles, { base: basePath }) .pipe( replace( /\b(Page|Component)(\(require\(['"](.*? "'"")\/npm\/)(.*)(createComponent.*)/, function(match, p1, p2, p3, p4, p5) { const type = (p5 || '').indexOf('true') != -1 || (p5 || '').indexOf('!0') != -1 ? 'Page' : 'Component' if (type == 'Page') p5 = p5.replace('))', '), true)') // 新:page.js基类要多传一个参数 const reservedParts = p2 + p4 + p5 // const type = p1 // const reservedParts = p2 const rootPath = p3 const clsName = type == 'Page' ? 'JDPage' : 'JDComponent' const baseFile = type == 'Page' ? 'page.taro.js' : 'component.js' console.log( `🌝 Replace with \`${clsName}\` successfully: ${this.file.path.replace( /.*?wxapp\//, 'wxapp/' )}` ) return `new (require("${rootPath}/bases/${baseFile}").${clsName})${reservedParts}` } ) ) .pipe(gulp.dest(basePath)) ) } // 基类替换 gulp.task('replace-base-component', () => replaceBaseComponent())
还有不少相似这样的骚操做,虽然比较麻烦,可是只须要处理一次,后续也不多改动。
对于 RN 开发,也是第一次将它落地到实际的业务项目中,因此大部分时候都是伴随着各类未知的坑不断前行,因此这里也友情提示一下,对于从未使用过的技术,仍是须要一些耐心的,遇到问题勤查勤问。
因为京喜 APP 是复用京东技术中台的基础框架和 JDReact 引擎,因此整个的开发和部署都是遵循 JDReact 已有的流程,画了一张大体的流程图以下:
JDReact 平台是在 Facebook ReactNative 开源框架基础上,进行了深度二次开发和功能扩展。不只打通了 Android/iOS/Web 三端平台,并且对京东移动端基础业务能力进行了 SDK 级别的封装,提供了统1、易于开发的 API。业务开发者能够经过 JDReact SDK 平台进行快速京东业务开发,而且不依赖发版就能无缝集成到客户端(android/iOS)或者转换成 Web 页面进行线上部署,真正实现了一次开发,快速部署三端。
因为京喜 APP 的 JDReact 模块都是独立的 git 仓库,因此须要调整咱们 Taro 项目配置 config/index.js
的编译输出路径以下:
rn: { outPath: '../jdreact-jsbundle-jdreactpingouindex' }
这样,当咱们运行 yarn run dev:rn
进行本地开发时,文件自动编译到了 JDReact 项目,接下来咱们就能够用模拟器或者真机来进行预览调试了。当咱们在进行本地开发调试的时候,最高效的方式仍是推荐用 Taro 官方提供的 taro-native-shell
原生 React Native 壳子来启动咱们的项目,详细的配置参照该项目的 README 进行配置便可。
因为 React Native 官方提供的 Remote Debugger 功能很是弱,推荐使用 React Native Debugger 来进行本地 RN 调试,提供了更为丰富的功能,基本接近 H5 和小程序的调试体验。
这样咱们就拥有了一个正常的开发调试环境,接下来就能够进行高效的开发了,因为咱们前面在 H5 和小程序版本阶段已经完成了绝大部分的业务逻辑开发,因此针对 RN 版本的主要工做集中在 iOS 和安卓不一样机型的样式和交互适配上。
在样式适配这块,不得不提下 Taro 针对咱们常见的场景提供了一些最佳实践,能够做为布局参考:
Image
组件处理flex
布局scalePx2dp
动态计算)在实际开发过程当中也遇到很多兼容性问题,这里整理出来以供你们参考:
<Text>
标签包起来,由于 RN 没有 textNode
的概念;margin
后会致使安卓下高度异常;Cannot read property 'x' of undefined
,Swiper 底层使用的 react-native-swiper 致使的问题,Disable Remote JS Debug 就不会出现。实现基线对齐:vertical-align: baseline
,用 <Text>
把须要基线对齐的组件包住便可。
<Text> <Text style={{ fontSize: 20 }}>abc</Text> <Text style={{ fontSize: 40 }}>123</Text> </Text>
line-height
,在安卓和 iOS 下表现不一致,并且即便设置为与 fontSize
相同也会致使裁剪;iOS 调试生产环境的 bundle,AppDelegate.m
中增长一行语句关闭 dev 便可:
[[RCTBundleURLProvider sharedSettings] setEnableDev:false]; // 找到这行,并在它的上面增长上面这行 jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
<Text>
与 <View>
支持的 style 属性不相同。
> [Text Style Props](https://facebook.github.io/react-native/docs/text-style-props "Text Style Props") & [View Style Props](https://facebook.github.io/
react-native/docs/view-style-props)
render 方法中不要返回空字符串
下面的代码在 android 下会报错(empty_string 内容为空字符串)
<View> {empty_string && <Text></Text>} </View>
由于 empty_string && <Text></Text>
的返回值是空字符串,RN 尝试把字符串添加到 View 的 children 时在安卓环境下会报错:
Error: Cannot add a child that doesn't have a YogaNode
border-radius
致使背景色异常,单独给某个角设置圆角时,没有设置圆角的边会出现一块与背景色颜色相同,但半透明的色块。
transform:tanslate()
藏起不想要的圆角style={{ backgroundColor: 'transparent' }}
,不能够用 scss 写,只有写在 JSX 上的才有效,Taro 编译时可能把透明背景色忽略了。多是 RN 布局引擎的问题,或单位转换以及浏览器渲染中的精度损失问题。能够调整页面结构来绕过。
或者简单粗暴一点,设置负 margin 值盖住缝隙。
要"完美"的编译出三端代码,首先要解决的是公共类库的适配问题,好在兄弟业务团队已经沉淀有完成度较高的三端公共类库,利用 Taro 提供的跨平台开发能力,抹平三端方法名和参数不统一的状况,便可很好的解决公共类库的适配问题,以下所示:
. ├── goto.h5.js ├── goto.rn.js ├── goto.weapp.js ├── request.h5.js ├── request.rn.js ├── request.weapp.js └── ...
以 request
公共组件为例,三端代码以下:
request.h5.js
import request from '@legos/request' export { request }
request.rn.js
import request from '@wqvue/jdreact-request' export { request }
request.weapp.js(因为小程序的公共组件没有发布至 npm,这里引用的本地项目源文件)
import { request } from '../../../common/request/request.js' export { request }
如遇到须要适配的方法参数不一致或者增长额外处理的状况,可进行再包装确保最终输出的接口一致,以下:
goto.rn.js
import jump from '@wqvue/jdreact-jump' function goto(url, params = {}, options = {}) { jump(url, options.des || 'm', options.source || 'JDPingou', params) } export default goto
文件引入的时候咱们正常使用就好,Taro 在编译的时候为咱们编译对应的平台的文件
import goto from './goto.js'
解决了公共类库适配以后,接下来就能够专一于业务代码开发了,一样业务代码在三端也可能存差别的状况,能够用 Taro 提供的环境变量来达到目的,示例代码以下:
if (process.env.TARO_ENV === 'h5') { this.speedReport(8) // [测速上报] 首屏渲染完成 } else if (process.env.TARO_ENV === 'weapp') { speed.mark(6).report() // [测速上报] 首屏渲染完成 } else if (process.env.TARO_ENV === 'rn') { speed.mark(7).report() // [测速上报] 首屏渲染完成 }
以上是 js 的代码处理方式,对于 css 文件及代码,一样也有相似的处理。
好比 RN 相对于 H5 和小程序的样式就存在比较大的差别,RN 支持的样式是 CSS 的子集,因此不少看起来很常见的样式是不支持的,能够经过如下方式进行差别化处理:
├── index.base.scss ├── index.rn.scss ├── index.scss
这里以 index.base.scss
做为三端都能兼容的公共样式(名字能够任取,不必定为 xxx.base.scss),index.rn.scss
则为 RN 端独特的样式,index.scss
则为 H5 和小程序独特的样式,由于 H5 和小程序样式基本上没有什么差别,这里合为一个文件处理。
Taro 也支持样式文件内的条件编译,语法以下:
/* #ifdef %PLATFORM% */ // 指定平台保留 /* #endif */ /* #ifndef %PLATFORM% */ // 指定平台剔除 /* #endif */
%PLATFORM%
的取值请参考 Taro 内置环境变量
如下为示例代码:
.selector { color: #fff; /* #ifndef RN */ box-shadow: 1px 1px 1px rgba(0, 0, 0, .1); /* #endif */ }
编译为 H5 和小程序的样式为:
.selector { color: #fff; box-shadow: 1px 1px 1px rgba(0, 0, 0, .1); }
RN 的样式为:
.selector { color: #fff; }
两种方式选其一便可,这样就能开开心心的编写业务代码了。
有些许遗憾的是产品经理对此次新版首页有着明确的上线优先级:先 H5 版,再微信小程序版,最后是 RN 版,这就为后续 RN 版本跟 H5 和 小程序版本分道扬镳埋下了伏笔,条件容许的话建议优先以 RN 版本为基准进行开发,以避免开发完成 H5 和小程序以后发现对结构和样式进行大的调整,由于 RN 对样式确实会弱一些。
电商性质的网站,会存在大量的素材或商品图片, 每每这些会对页面形成较大的性能影响。得益于京东图床服务,提供强大的图片定制功能,让咱们在图片优化方面省去大量工做。以引入商品图片 "https://img10.360buyimg.com/mobilecms/s355x355_jfs/t1/55430/24/116/143859/5cd27c99E71cc323f/0e8da8810fb49796.jpg!q70.dpg.webp"
为样本,咱们对图片应用作了部分优化:
为 Image 标签设置 lazyload 属性,这样能够在 H5 和小程序下得到懒加载功能。
起初京喜首页的首屏数据涉及的后端接口多达 20 余个,致使总体数据返回时间较长;为了此项痛点,咱们联合后端团队,独立开发首屏专用的聚合直出接口。一方面,将众多接口请求合并成一个,减小接口联动请求带来的性能损耗;另外一方面,将复杂的业务逻辑挪到后端处理,前端只负责视图渲染和交互便可,减小前端代码复杂度;经过此项优化,页面性能和体验获得极大改善。
因为京喜业务主要围绕下沉市场,其用户群体的网络环境会更加复杂,要保障页面的性能,减小网络延时是一项重要措施。
为了提高用户二次访问的加载性能,咱们决定采用缓存优先策略。即用户每次访问页面时所请求的主接口数据写入本地缓存,同时用户每次访问都优先加载缓存数据,造成一套规范的数据读取机制。经过优先读取本地缓存数据,可以让页面内容在极短期内完成渲染;另外,本地缓存数据亦可做为页面兜底数据,在用户网络超时或故障时使用,可避免页面空窗的情景出现。
首页紧接着首屏区域的是一个支持下滑加载的瀑布流长列表,每次滑到底部都会异步拉取 20 条数据,总计会拉取将近 500 条数据,这在 iOS 下交互体验还比较正常。可是在配置较低的安卓机型下,当滑动到 2 到 3 屏以后就开始出现严重卡顿,甚至会闪退。
针对这种场景也尝试过用 FlatList 和 SectionList 组件来优化,可是它们都要求规则等高的列表条目,因而不得不本身来实现不规则的瀑布流无限滚动加载。其核心思路是经过判断列表的条目是否在视窗内来决定图片是否渲染,要优化得更完全些得话,甚至能够移除条目内全部内容只保留容器,以达到减小内容节点以及内存占用,不过在快速进行滑动时比较容易出现一片白框,算是为了性能损失一些体验,总体上来讲是能够接受得。
因为 RN 下在获取元素坐标偏移等数据相对 H5 和小程序要麻烦获得,具体的实现细节能够查看抽离出来的简单实现Taro 高性能瀑布流组件(for RN)。
这篇文章从技术选型、开发实录再到性能优化三个维度对京喜首页改版作了简单总结。整个项目实践下来,证明 Taro 开发框架 已彻底具有投入大型商业项目的条件。虽在多端开发适配上耗费了一些时间,但仍比各端独立开发维护工做量要少。
在前端资源匮乏的今天,选择成熟的开发工具来控制成本、提高效率,已经是各团队的首要工做目标。 同时,京喜做为京东战略级业务,拥有千万级别的流量入口,咱们对页面的体验优化和性能改进远不止于此,但愿每一次微小的改动能为用户带来愉悦的感觉,始终为用户提供优质的产品体验。
欢迎关注凹凸实验室博客:aotu.io
或者关注凹凸实验室公众号(AOTULabs),不定时推送文章: