β

JDreact转H5—你需要做的兼容处理

JDC | 京东设计中心 11 阅读

前言

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

你准备好了吗?

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

转化步骤

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

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

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

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

module.exports = {
 build: { //打包发布使用的下面的配置
  entry: ‘jsbundles/xxx.web.js, //文件入口
  publicPath: ‘xxx/, //生成文件的公共路径
  assetsRoot: ‘build-web’, //编译到哪个目录下面
  src: ‘jsbundles’, //入口代码地址
  template: { //生成的vm模板的配置
  title: ‘京东商城’, //标题名称
  nofooter: true, //不包含京东公共底部
  noheader: true, //不包含京东公共头部
  downloadAppPlugIn: false, //关闭打开m页面是唤起想要原生页面的能力
},
  includeJDShare: false, //是否要包含京东WebView的分享能力
},
 dev: { //平时开发使用的是下面的配置
   ...
 }
}

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

2.调用后台数据接口

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

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

<script type="text/javascript">
(function() {
window.GLOBAL_CONFIG = {
 pin : "$!pin",
 sid: "$!sid",
};
}())
</script>

将这里的 $!pin 改为登录人的 pin,记得本地开发完之后再把这里还原回去。 好了,至此终于可以调通后台的接口了!

3.引入外部css样式

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

if(Platform.OS == 'web'){
 //web端代码
}else if(Platform.OS == 'android'){
 //android端代码
}else if(Platform.OS == 'iOS'){
 //iOS端代码
}

但是问题是,下面这种形式的 CSS 没有办法根据平台去做兼容处理,

const styles = StyleSheet.create({
 wraper:{
  flex:1,
  display:'flex',
  justifyContent: 'center',
  alignItems:'center',
 },
});

这时需要在后缀为 web.js 的文件中引入外部 CSS,例如在 Index.js 文件中定义 calssName:

<View style={styles.box} className="ouot-box">
  <JDText>我是文本内容</JDText>
</View>

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

.out-box{
  color:#fff;
  font-size: 16px;
}

最后在以 web.js 为后缀的文件中引入 CSS 文件:

import './JDReactYouka/outstyle.css';

4.require提升问题

如果你的项目中为了调用手机通讯录而调用了 varSubscribable=require('Subscribable ') ,你会发现在启动服务后会报错:

这是因为 Web 端无法调用 Subscribable 文件,那么我们首先想到的是使用平台判断,如果是 Web 端就不在引入 Subscribable 文件:

if(Platform.OS !== 'web'){
  var Subscribable = require('Subscribable ')
}

但是即使这样处理仍然是报错: Modulenotfound: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  属性 。我们先来看个例子( 为了方便理解示例,已经把其他不必要的属性去掉 ):

render(){
return (
<View>
<TextInput
ref="telInput"
/>
<JDTouchable onPress ={()=>{this._getFocus()}}>
  <JDText>点击我input获取光标</JDText>
  </JDTouchable>
</View>
)
},
_getFocus(){
  this.refs.telInput.focus();
},

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

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

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

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

2.输入框的光标不见了

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

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

render(){
return (
<View>
<TextInput
ref="telInput"
/>
<JDTouchable onPress ={(e)=>{this._getFocus(e)}}>
  <JDText>点击我input获取光标</JDText>
</JDTouchable>
</View>
)
},
_getFocus(){
  event.preventDefault();
  Platform.OS == 'web' ? this.refs.telInput.refs.input.focus()
  : this.refs.telInput.focus();
},

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

3.不听话的数字输入框

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

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

blurEvent()  {/*光标离开时需要把当前输入框中的值传给onChangeText执行的事件*/
  this._changeInput(this.state.inputNum);
  console.log('离开了输入框');
}

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

根据上述分析,我们需要有两个方法解决:

1)在改变 state 状态的回调函数中执行离开事件:

_changeInput(text){
  this.setState({
  inputNum:text,
},()=>{
  if(text.length>=5){
  Platform.OS == 'web' ? this.refs.telInput.refs.input.blur() : this.refs.telInput.blur();
}
});
}

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

_changeInput(text){
  this.setState({
  inputNum:text,
});
if(text.length>=5){
  setTimeout(()=>{
  Platform.OS == 'web' ? this.refs.telInput.refs.input.blur() : this.refs.telInput.blur();
},10);
}
}

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

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

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

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

const styles = StyleSheet.create({
  defaultBox: {/*defaultBox为默认样式,右边框和下边框宽度为1px*/
  borderColor: '#ccc',
  borderRightWidth: JDDevice.getDpx(1),
  borderBottomWidth: JDDevice.getDpx(1),
},
  selectedBox:{/*selectedBox为选中样式,选中边框为1px*/
  borderColor: '#F00',
  borderWidth:JDDevice.getDpx(1),
}
});

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

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

/*selected为选中样式,选中边框为1px*/
selected:{
  borderColor: '#F00',
  borderRightWidth:JDDevice.getDpx(1),
  borderTopWidth:JDDevice.getDpx(1),
  borderLeftWidth:JDDevice.getDpx(1),
  borderBottomWidth:JDDevice.getDpx(1),
}

对应的页面变成了如下所示:

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

const styles = StyleSheet.create({
  defaultBox: {/*defaultBox为默认样式,右边框和下边框宽度为1px*/
  borderColor: '#ccc',
  borderRightWidth: JDDevice.getDpx(1),
  borderBottomWidth: JDDevice.getDpx(1),
  borderLeftWidth:JDDevice.getDpx(0),
  borderTopWidth:JDDevice.getDpx(0),
},
selectedBox:{/*selectedBox为选中样式,选中边框为1px*/
  borderColor: '#F00',
  borderRightWidth:JDDevice.getDpx(1),
  borderTopWidth:JDDevice.getDpx(1),
  borderLeftWidth:JDDevice.getDpx(1),
  borderBottomWidth:JDDevice.getDpx(1),
}
});

最终再看效果:

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

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

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

this.context.router.push(
  { routeName: 'home',props:{tels:this.state.inputNum}}
)

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

1) 页面之间传递简单的数据:

this.context.router.push('indexList',
 {
  'tels': JSON.stringify(encodeURIComponent(this.state.inputNum))
 }
);

对应的接收数据: JSON.parse(decodeURIComponent(this.props.tels))

2 )页面之间传递复杂的数据: this.context.router.push('initPage',{'transData': JSON.stringify(this.state.transData)});

对应的接收数据: 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 。它的使用方法也很简单,我们来看看它的使用方法:

AsyncStorage.setItem(
  'names','小明'
);

对应的获取AsyncStorage存储值:

AsyncStorage.getItem('names',(err,result)=>{
  if(result && result != ''){
  let names = result;
  alert('AsyncStorage='+names);
  }
});

再来看看上面提到的两个问题,都可以用 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时,该图片消失。那么代码如下所示:

//设置showImg为true,然后获取showImg,给state的showBoxFlag赋值为true
_trueStore(){
  AsyncStorage.setItem(
   'showImg',true
  );
 AsyncStorage.getItem('showImg',(err,result)=>{
  if(result && result != ''){
  this.setState({
  showBoxFlag : result
 },()=>{
  console.log(typeof(this.state.showBoxFlag));
 });
 }
 });
},
//设置showImg为false,然后获取showImg,给state的showBoxFlag赋值为false
_falseStore(){
AsyncStorage.setItem(
 'showImg',false
);
AsyncStorage.getItem('showImg',(err,result)=>{
  if(result && result != ''){
  this.setState({
  showBoxFlag : result
  },()=>{
  console.log(this.state.showBoxFlag);
  });
 }
 });
},
_clearStore(){
  AsyncStorage.clear();
},

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

AsyncStorage.getItem('showImg',(err,result)=>{
  if(result && result != ''){
  this.setState({
  showBoxFlag : result == 'true' ? true:false
  });
 }
});

然后再看效果:

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

总结

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

更多内容请关注我们团队的公众账号“全栈探索”。定期会有好文推送,满满的干货。

作者:JDC | 京东设计中心
京东设计中心
原文地址:JDreact转H5—你需要做的兼容处理, 感谢原作者分享。

发表评论