JDreact 像一位安静的女子独立窗前,明眸皓齿的样子让你不敢贸然向前,直到慢慢熟悉之后才会发现,原来她真是上得了厅堂,下得了厨房,写得了代码,查得出异常,既能支持安卓,又可兼容苹果,直到最后我们发现,她居然还可以转成 web 端代码!
然而就像你心爱的姑娘一样,岂能让你这么容易追到手?今天咱们就来谈谈怎么追女孩!呸,不对!怎么把 JDreact 顺利的转成 H5 代码!
你准备好了吗?
相信你手头已经有一套千锤百炼的 JDreact 代码了!啥?你还没有?点击查看文章
《与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 : ‘xxxx’, //标题名称
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 文件:
将这里的
$
!
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端代码
}
但是问题是,JDreact 中使用的这种形式的 css 没有办法根据平台去做兼容处理,
const
styles = StyleSheet . create ({
wraper :{
flex : 1 ,
display : 'flex' ,
justifyContent : 'center' ,
alignItems : 'center' ,
},
});
所以,如果转 H5 后要修改样式,在
jsbundles 文件夹下的 web.js 后缀文件中
引入外部 css 文件,举个例子,首先在 Index.js 文件中给要设定样式的元素定义 calssName:
style ={ styles . box } className = "ouot-box" >
我是文本内容
对应的 css 样式文件 outstyle.css:
. out - box {
color :# fff ;
font - size : 16px ;
}
最后在
web.js 后缀文件中
引入 css 文件:
import './JDReactYouka/outstyle.css' ;
4.require提升问题
如果你的项目中使用了 web 端没有的功能组件,比如说为了调用手机通讯录而调使用了
var
Subscribable
=
require
(
'Subscribable '
)
,你会发现在启动服务后会报错:
这是因为 web 端无法调用
Subscribable
文件,那么我们首先想到的是使用平台判断,如果是 web 端就不要引入
Subscribable
文件:
if ( Platform . OS !==
'web' ){
var Subscribable = require ( 'Subscribable ' )
}
但是即使这样处理仍然是报错:
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 属性 。我们先来看个例子(
为了方便理解示例,已经把其他不必要的属性去掉
):
render
(){
return (
< View >
< TextInput
ref = "telInput"
/>
< JDTouchable onPress ={()=>{ this . _getFocus ()}}>
< JDText >点击我 input 获取光标 JDText >
JDTouchable >
View >
)
},
_getFocus (){
this . refs . telInput . focus ();
},
上面的例子中,
可以理解为
HTML
中的
标签,
是点击事件组件。因此,该示例是点击下面的按钮,让上面的输入框主动获得光标,
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 中获取到了
元素,怪异的事情又发生了,点击下面的按钮之后,输入框中主动获取的光标闪烁一下就不见了。动态图感受一下:
可以看到点击按钮之后
获取到光标了,但是随即又消失了,说明触发了其他的事件导致光标移出了输入框,而我们做的动作只是点击了按钮而已,所以问题出现在点击事件上。再来看看使用的是
组件,在转成 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.不听话的数字输入框
正如上面介绍的
组件,该组件提供 onChangeText 事件来监听输入框内容的变化,通过获取到输入的 text 值改变 state 值,再赋值给
组件的 value 值,来达到更新
的内容,其逻辑如下图所示:
日常项目中只要涉及到向输入框中输入数字,经常会规定输入的数字满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. ‘多选一’组件的边框线也不见了
是 JDreact 中的一个多选一的组件,类似于HTML中的
type
=
"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 三端的影响,只是经历得多了,才能快速的定位问题甚至一开始就会避免弯路。而本篇文章正是旨在抛砖引玉,由于作者水平有限,希望各路大神多多指教!