专栏名称: 京东设计中心JDC
专业,创造力,激情,设计。京东用户体验设计部门,致力于创造更美好的电子商务购物体验。
目录
相关文章推荐
51好读  ›  专栏  ›  京东设计中心JDC

JDC丨京东设计中心 - JDreact转H5:你需要做的兼容处理

京东设计中心JDC  · 公众号  ·  · 2018-04-17 12:28

正文

请到「今天看啥」查看全文


JDreact 像一位安静的女子独立窗前,明眸皓齿的样子让你不敢贸然向前,直到慢慢熟悉之后才会发现,原来她真是上得了厅堂,下得了厨房,写得了代码,查得出异常,既能支持安卓,又可兼容苹果,直到最后我们发现,她居然还可以转成 web 端代码! 然而就像你心爱的姑娘一样,岂能让你这么容易追到手?今天咱们就来谈谈怎么追女孩!呸,不对!怎么把 JDreact 顺利的转成 H5 代码!

你准备好了吗?

相信你手头已经有一套千锤百炼的 JDreact 代码了!啥?你还没有?点击查看文章 《与JDReact的第一次亲密接触——加油卡项目总结》 告诉你如果构建 JDreact 项目,呀!别走啊!即使不想看也没关系!你可以把 JDreact 想象成 React 代码,毕竟两者的语法还是有些类似的,如果也不知道 React,那也没关系,留意一下呗,万一以后用到了呢!

转化步骤

我们先来看看 JDreact 转化成 H5 代码的主要步骤:

其中的注意事项,我们缓缓道来:

1.修改 config.js 配置文件:

执行完第1、2步骤之后,在生成的依赖文件中,找到 web 文件夹下的 config.js 配置文件,根据下面注释修改

  1. module.exports = {

  2.  build: {                               //打包发布使用的下面的配置

  3.    entry: jsbundles/xxx.web.js,       //文件入口

  4.    publicPath: xxx/,                 //生成文件的公共路径

  5.    assetsRoot: build-web’,          //编译到哪个目录下面

  6.    src: jsbundles’,                //入口代码地址

  7.    template: {                     //生成的vm模板的配置

  8.      title: ‘xxxx’,            //标题名称

  9.      nofooter: true,             //不包含京东公共底部

  10.      noheader: true,            //不包含京东公共头部

  11.      downloadAppPlugIn: false, //关闭打开m页面是唤起想要原生页面的能力

  12.    },

  13.    includeJDShare: false,    //是否要包含京东WebView的分享能力

  14.  },

  15.  dev: {                    //平时开发使用的是下面的配置

  16.   ...

  17.  }

  18. }

其中,entry、publicPath 是需要根据业务代码进行配置的,其他参数可以使用默认值。

2.调用后台数据接口

JDreact 中访问后台接口的方法需要修改为 Jsonp 的方式。具体方法已经在修改 jsbundles 文件夹下的web.js后缀文件中给出,但是需要注意的是 Jsonp 中参数 appid 的获取。 首先,登录京东 API 开放平台 http://color.jd.com/,在“调用方”一栏,创建如下应用:

经过审批之后,会返回 appid,之后搜索到要调用的 API,提交调用申请,后端研发通过审批,我们就有调用后台接口的权限了。 然而你以为这样就可以请求到接口数据了吗?年轻人不要着急,不要忘了一般接口都要传递登录人的信息,可是在本地开发的时候没有办法拿到线上登录人的信息,这时需要修改 web 文件夹下的 index.tpl.vm 文件:

将这里的 $ ! pin 改为登录人的 pin( 记得本地开发完之后再把这里还原回去 )。

好了,至此终于可以调通后台的接口了!

3.引入外部css样式

接下来我们看外部 css 样式的引入,为什么要引入外部 css? 如果转换后的 web 端表现形式和移动端不一致,我们可以通过 Platform.OS 来区分平台兼容 js 和 html:

  1. if(Platform.OS == 'web'){

  2.  //web端代码

  3. }else if(Platform.OS == 'android'){

  4. //android端代码

  5. }else if(Platform.OS == 'ios'){

  6. //ios端代码

  7. }

但是问题是,JDreact 中使用的这种形式的 css 没有办法根据平台去做兼容处理,

  1. const styles = StyleSheet.create({

  2.  wraper:{

  3.      flex:1,

  4.      display:'flex',

  5.      justifyContent: 'center',

  6.      alignItems:'center',

  7.    },

  8. });

所以,如果转 H5 后要修改样式,在 jsbundles 文件夹下的 web.js 后缀文件中 引入外部 css 文件,举个例子,首先在  Index.js 文件中给要设定样式的元素定义 calssName:

  1. style={styles.box} className="ouot-box">

  2.     我是文本内容

对应的 css 样式文件 outstyle.css:

  1. .out-box{

  2.     color:#fff;

  3.     font-size: 16px;

  4. }

最后在 web.js 后缀文件中 引入 css 文件:

  1. import './JDReactYouka/outstyle.css';

4.require提升问题

如果你的项目中使用了 web 端没有的功能组件,比如说为了调用手机通讯录而调使用了 var Subscribable = require ( 'Subscribable ' ) ,你会发现在启动服务后会报错: 这是因为 web 端无法调用 Subscribable 文件,那么我们首先想到的是使用平台判断,如果是 web 端就不要引入 Subscribable 文件:

  1. if(Platform.OS !== 'web'){

  2.   var Subscribable = require('Subscribable ')

  3. }

但是即使这样处理仍然是报错: Module not found : Cant 't resolve ' Subscribable ' in... ,原来在转化为 H5 代码的过程中 require 会做提升处理,即使你使用了平台判断或者将 require 放在了后面,仍会在代码编译的时候提升处理,那么怎么办呢? 这时我们需要设置三个后缀不同的文件: xx . web . js xx . android . js xx . ios . js 。其中后缀为 web.js 的文件会在 web 端调用,其他两个对应不同平台调用,所以我们在 web.js 的文件中不再调用 Subscribable 这个文件,这样在转换后的 H5 代码中就不再报错了! 好了,经过上述步骤,执行 npm run web - start ,就是见证奇迹诞生的时刻了!项目终于跑起来了!然而到此就大功告成了吗?不可能啊!女孩在对你有感觉也要矜持一下的,何况咱们的项目呢?那么接下来会遇到什么问题呢?

绕不过的兼容问题

1.你熟悉的那个ref已经不在是那个ref了!

在 React 中,组件并不是真实的 DOM 节点,而是存在于内存之中的一种数据结构,叫做虚拟 DOM 。只有当它插入文档以后,才会变成真实的 DOM 。根据 React 的设计,所有的 DOM 变动,都先在虚拟 DOM 上发生,然后再将实际发生变动的部分,反映在真实DOM上,这种算法叫做 DOM diff ,它可以极大提高网页的性能表现。但是,有时需要从组件获取真实 DOM 的节点,这时就要用到 ref 属性 。我们先来看个例子( 为了方便理解示例,已经把其他不必要的属性去掉 ):

  1. render (){

  2.  return (

  3.    <View>

  4.      <TextInput

  5.        ref="telInput"

  6.      />

  7.      <JDTouchable onPress ={()=>{this._getFocus()}}>

  8.          <JDText>点击我input获取光标JDText>

  9.      JDTouchable>

  10.    View>

  11.  )

  12. },

  13. _getFocus(){

  14.  this.refs.telInput.focus();

  15. },

上面的例子中, 可以理解为 HTML 中的 标签, 是点击事件组件。因此,该示例是点击下面的按钮,让上面的输入框主动获得光标, this . refs . telInput 也正是获取到 input 输入框的常规用法,但是!在转成 H5 之后却报错了:

怎么回事? 于是打印出 console . log ( this . refs . telInput ) ,发现:

看到这里我们恍然大悟,转成 H5 之后,还需要再深入一层获取 Dom 元素,所以这里要做RN 和 H5 的兼容处理:

  1. Platform.OS == 'web' ? this.refs.telInput.refs.input.focus() : this.refs.telInput.focus();

2.输入框的光标不见了

根据上面的兼容处理,在 H5 中获取到了 元素,怪异的事情又发生了,点击下面的按钮之后,输入框中主动获取的光标闪烁一下就不见了。动态图感受一下:

可以看到点击按钮之后 获取到光标了,但是随即又消失了,说明触发了其他的事件导致光标移出了输入框,而我们做的动作只是点击了按钮而已,所以问题出现在点击事件上。再来看看使用的是 组件,在转成 H5 之后执行的是 touch 事件,之后还会再次触发 click 事件,从而导致在 touch 事件中刚刚获得光标的输入框,在click 事件时又失去了光标。好了!明白了问题出在哪里就好做了,解决方法是阻止冒泡事件:

  1. render(){

  2.  return (

  3.    <View>

  4.      <TextInput

  5.        ref="telInput"

  6.      />

  7.       <JDTouchable onPress ={(e)=>{this._getFocus(e)}}>

  8.          <JDText>点击我input获取光标JDText>

  9.      JDTouchable>

  10.    View>

  11.  )

  12. },

  13. _getFocus(){

  14.  event.preventDefault();

  15.  Platform.OS == 'web' ? this.refs.telInput.refs.input.focus()

  16.                                  : this.refs.telInput.focus();

  17. },

经过上面的阻止默认事件,输入框的光标就不会不翼而飞了。效果如下图所示:

3.不听话的数字输入框

正如上面介绍的 组件,该组件提供 onChangeText 事件来监听输入框内容的变化,通过获取到输入的 text 值改变 state 值,再赋值给 组件的 value 值,来达到更新 的内容,其逻辑如下图所示:

日常项目中只要涉及到向输入框中输入数字,经常会规定输入的数字满11位之后,光标自动离开输入框,且输入框内数字发生变化时需要对数字进行处理、校验等一系列的逻辑,再考虑到复制粘贴过来的文本都要在满足11位后离开输入框再进行上述校验,所以光标离开输入框后也需要进行 onChangeText 事件。但是转化为 H5 之后却发现光标总是提前一位数字离开输入框,让我们简化需求如下:

  • 输入框满5位数字后主动离开输入框;

  • 离开输入框时,再次执行onChangeText事件;

  1. blurEvent(){/*光标离开时需要把当前输入框中的值传给onChangeText执行的事件*/

  2.  this._changeInput(this.state.inputNum);

  3.  console.log('离开了输入框');

  4. }

从动态图中可以看出,输入框在满4位之后,在输入第5位数字时光标移出输入框,且输入框中只保留了4位数字,这是这么回事呢? 要知道 setState 可能是异步更新的,也就是说 onChangeText 事件中有可能 state 尚未更新成功,由于输入框中已经满了5位,所以光标离开输入框,而离开事件又再次执行了onChangeText 事件,所以导致这时传给 onChangeText 的参数仍是4位,简单来说就是光标在离开输入框的时候 state 值尚未更新成5位:

根据上述分析,我们需要有两个方法解决: 1)在改变 state 状态的回调函数中执行离开事件:

  1. _changeInput(text){

  2.  this.setState({

  3.      inputNum:text,

  4.  },()=>{

  5.      if(text.length>=5){

  6.          Platform.OS == 'web' ? this.refs.telInput.refs.input.blur() : this.refs.telInput.blur();

  7.      }

  8.  });

  9. }

2)给离开事件设置个短暂延时,给改变状态留下空余时间:

  1. _changeInput(text){

  2.  this.setState({

  3.      inputNum:text,

  4.  });

  5.  if(text.length>=5){

  6.      setTimeout(()=>{

  7.        Platform.OS == 'web' ? this.refs.telInput.refs.input. blur() : this.refs.telInput.blur();

  8.      },10);

  9.  }

  10. }

这两种方法孰好孰坏没有定论,要看实际项目中代码逻辑的处理了,经过上述方法后,效果如下所示:

4. ‘多选一’组件的边框线也不见了

是 JDreact 中的一个多选一的组件,类似于HTML中的 type = "radio" > ,但是功能和样式更加丰富,例如下面的单选面板就使用了该组件:

有6个面板默认灰色边框,点击选中,选中状态是红色边框。这里的问题是每个边框只有右边框和下边框,避免中间出现两个边框。于是样式代码如下所示(注意这里为了简洁,只保留了相关代码):

  1. const styles = StyleSheet.create({

  2.  defaultBox: {/*defaultBox为默认样式,右边框和下边框宽度为1px*/

  3.    borderColor: '#ccc',

  4.    borderRightWidth: JDDevice.getDpx(1),

  5.    borderBottomWidth: JDDevice.getDpx(1),

  6.  },

  7.  selectedBox:{/*selectedBox为选中样式,选中边框为1px*/

  8.    borderColor: '#F00',

  9.    borderWidth:JDDevice.getDpx(1),

  10.  }

  11. });

那么转成H5之后,样式出现了什么问题呢?

从图中可以看到,点击之后,默认的边框不见了,根据男人的第六感,我认为既然默认的边框是单个设置的,那么选中的边框是不是也要改成单个设置,于是改动了选中后的样式:

  1. /*selected为选中样式,选中边框为1px*/

  2. selected :{

  3.  borderColor: '#F00',

  4.  borderRightWidth:JDDevice.getDpx(1),

  5.  borderTopWidth:JDDevice.getDpx(1),

  6.  borderLeftWidth:JDDevice.getDpx(1),

  7.  borderBottomWidth:JDDevice.getDpx(1),

  8. }

于是页面变成了如下所示:

可以看到虽然边框不在消失,但、但、但是居然变粗了!真是感觉跑偏了,但是发现设置了边框宽度的下边框和右边框是没有变化的,所以我们再给上边框和左边框也加上宽度为0的设置:

  1. const styles = StyleSheet.create({

  2.  defaultBox: { /*defaultBox为默认样式,右边框和下边框宽度为1px*/

  3.    borderColor: '#ccc',

  4.    borderRightWidth: JDDevice.getDpx(1),

  5.    borderBottomWidth: JDDevice.getDpx(1),

  6.    borderLeftWidth:JDDevice.getDpx(0),

  7.    borderTopWidth:JDDevice.getDpx(0),

  8.  },

  9.  selectedBox:{/*selectedBox为选中样式,选中边框为1px*/

  10.    borderColor: '#F00',

  11.    borderRightWidth:JDDevice.getDpx(1 ),

  12.    borderTopWidth:JDDevice.getDpx(1),

  13.    borderLeftWidth:JDDevice.getDpx(1),

  14.    borderBottomWidth:JDDevice.getDpx(1),

  15.  }

  16. });

最终再看效果:

终于正常了!(以上代码在 JDreact 中页面都是显示正常的)

5.页面之间如何传递数据?

在 JDreact 中我们使用下面的方法传递数据:

  1. this.context.router.push(

  2.  { routeName: 'home',props:{tels:this.state. inputNum}}

  3. )

相应的在下一个页面通过 this . props . tels 接收传输的数据。 但是在转成 H5 之后就不能这样写了,分为传递简单数据和复杂数据,具体写法如下所示: 1) 页面之间传递简单的数据:

  1. this.context.router.push('indexList',

  2.  {

  3.    'tels': JSON.stringify(encodeURIComponent(this.state.inputNum))

  4.  }

  5. );

对应的接收数据:

  1. JSON.parse(decodeURIComponent(this.props.tels))

2) 页面之间传递复杂的数据:

  1. this.context.router.push('initPage',{'transData': JSON.stringify(this.state.transData)});

对应的接收数据:

  1. JSON.parse(decodeURIComponent(this.props.transData))

对比发现,在转成 H5 后传递复杂的数据时不需要再进行 encodeURIComponent 编码,避免了 encodeURIComponent 对复杂数据中的各种符号进行转义。

6.使用AsyncStorage进行数据存储

根据上面提供的方法,页面之间可以进行数据传输了,但是发现传输的数据全部暴露在URL 上:

传输的数据一目了然,这样就尴尬了,那怎么办呢? 正当思考良策之际,又一问题接踵而至,在 JDreact 中从 A 页面跳转到 B 页面,然后在返回A页面,这时 A 页面之前操作的状态还需要保留,在 JDreact 很好处理,使用路由的  push 方法从 A 页面跳转到 B 页面,再使用路由的 popToWithProps 方法即可从 B 页面返回 A 页面,且 A 页面保留原来的状态。但是在转成 H5 代码之后,再次返回 A 页面却刷新了页面,这将导致 A 页面的操作状态全部重置。 这可如何是好? 等待多时的 AsyncStorage 轻声咳嗽一声,大家让让,该老夫出场了! AsyncStorage 是一个简单的、异步的、持久化的 Key-Value 存储系统,它对于 App 来说是全局性的。转成H5 之后对应着 localStorage。它的使用方法也很简单,我们来看看它的使用方法:

  1. AsyncStorage.setItem(

  2.    'names','小明'

  3. );

对应的获取 AsyncStorage 存储 值:

  1. AsyncStorage.getItem('names',(err,result)=>{

  2.  if(result && result != ''){

  3.   let names = result;

  4.   alert('AsyncStorage='+names);

  5.  }

  6. });

再来看看上面提到的两个问题,都可以用 AsyncStorage 来解决,复杂敏感的数据就不要放在路由携带的参数上,而是使用 AsyncStorage 存储。类似的在 JDreact 中路由 push popToWithProps 的失效,也需要将当前页面的状态保存在 AsyncStorage 中,待返回当前页面的时候再重新渲染。注意的是,在JDreact中并不支持localStorage和sessionStorage,除非使用平台做兼容处理,也就是 Platform . OS !== 'web' 时再使用web 端的两个存储方式。

7.True和False怎么失效了?

既然需要使用 AsyncStorage 来保存当前页面的状态,却发现有个状态很不听话,简化例子如下图所示:

点击绿色按钮设置 AsyncStorage 的 showImg 为 true,点击红色按钮设置 AsyncStorage 的 showImg 为 false,达到的效果是在 showImg 为 true 时,显示下面圆形图片,如果showImg 等于 false 时,该图片消失。那么代码如下所示:

  1. //设置showImg为true,然后获取showImg,给state的showBoxFlag赋值为true

  2. _trueStore(){

  3.  AsyncStorage.setItem(

  4.      'showImg',true

  5.  );

  6.  AsyncStorage.getItem('showImg',(err,result)=>{

  7.    if(result && result != ''){

  8.       this.setState({

  9.          showBoxFlag : result

  10.       },()=>{

  11.          console.log(typeof(this.state.showBoxFlag));

  12.       });

  13.    }

  14.  });

  15. },

  16. //设置showImg为false,然后获取showImg,给state的showBoxFlag赋值为false

  17. _falseStore(){

  18.    AsyncStorage.setItem(

  19.      'showImg',false

  20.    );

  21.     AsyncStorage.getItem('showImg',(err,result)=>{

  22.      if(result && result != ''){

  23.         this.setState({

  24.            showBoxFlag : result

  25.         },()=>{

  26.          console.log(this.state.showBoxFlag);

  27.       });

  28.      }

  29.    });

  30. },

  31. _clearStore(){

  32.   AsyncStorage.clear();

  33. },

但是很奇怪的是,图片并没有根据按钮的切换来隐藏显示。我们打出 showFlag 的值和类型才发现,原来 showFlag 是 String 类型的 true/false。也就是说经过 AsyncStorage 存储 的值是 String 类型的值,而不是存储前的 Boolean 类型的 true/false。看到这里我们就知道如何做了,需要把 String 类型的 true/false 还原成 Boolean 类型:

  1. AsyncStorage.getItem('showImg',(err,result)=>{

  2.  if(result && result != ''){

  3.    this.setState({

  4.      showBoxFlag : result == 'true' ? true:false

  5.    });

  6.  }

  7. });

然后再看效果:

好了,根据上述代码的处理,可以根据点击设置的状态来调整图片的显示隐藏了!

总结

结束了吗?其实远远没有结束,由于 JDreact 更加接近原生性能,所以有些功能转成 H5 后无法支持,例如调用手机的通讯录等功能,这些情况需要再取舍了。相信在每一个转换  H5 的项目中都会遇到不同的情况,转换成 H5 之后更是面临着各种手机原生浏览器的兼容考验。每一次遇到问题,都要兼顾着 IOS、Android、H5 三端的影响,只是经历得多了,才能快速的定位问题甚至一开始就会避免弯路。而本篇文章正是旨在抛砖引玉,由于作者水平有限,希望各路大神多多指教!








请到「今天看啥」查看全文