JDreact 像一位安静的女子独立窗前,明眸皓齿的样子让你不敢贸然向前,直到慢慢熟悉之后才会发现,原来她真是上得了厅堂,下得了厨房,写得了代码,查得出异常,既能支持安卓,又可兼容苹果,直到最后我们发现,她居然还可以转成 Web 端代码! 然而就像你心爱的姑娘一样,岂能让你这么容易追到手?今天咱们就来谈谈怎么追女孩!呸,不对!怎么把 JDreact 顺利的转成 H5 代码!
相信你手头已经有一套千锤百炼的 JDreact 代码了!啥?你还没有?快去看看我之前写的文章 《与JDReact的第一次亲密接触——加油卡项目总结》 ,呀!别走啊!即使不想看也没关系!你可以把 JDreact 想象成 React 代码,毕竟两者的语法还是有些类似的,如果也不知道 React ,那也没关系,留意一下呗,万一以后用到了呢!
我们先来看看 JDreact 转化成 H5 代码的主要步骤:
其中的注意事项,我们缓缓道来:
执行完第 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 是需要根据业务代码进行配置的,其他参数可以使用默认值。
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,记得本地开发完之后再把这里还原回去。 好了,至此终于可以调通后台的接口了!
接下来我们看外部 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' ;
如果你的项目中为了调用手机通讯录而调用了 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
, 就是见证奇迹诞生的时刻了!项目终于跑起来了!然而到此就大功告成了吗?不可能啊!女孩在对你有感觉也要矜持一下的,何况咱们的项目呢?那么接下来会遇到什么问题呢?
在 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();
根据上面的兼容处理,在 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(); },
经过上面的阻止默认事件,输入框的光标就不会不翼而飞了。效果如下图所示:
正如上面介绍的 <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); } }
这两种方法孰好孰坏没有定论,要看实际项目中代码逻辑的处理了,经过上述方法后,效果如下所示:
<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 中页面都是显示正常的)
在 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 对复杂数据中的各种符号进行转义。
根据上面提供的方法,页面之间可以进行数据传输了,但是发现传输的数据全部暴露在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 端的两个存储方式。
既然需要使用 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 三端的影响,只是经历得多了,才能快速的定位问题甚至一开始就会避免弯路。而本篇文章正是旨在抛砖引玉,由于作者水平有限,希望各路大神多多指教!
更多内容请关注我们团队的公众账号“全栈探索”。定期会有好文推送,满满的干货。