背景
RELAY一直致力于解决设计项目全周期云端管理诉求。因此除了Web端的上传设计稿、查看标注的核心流程外,移动端随时随地的浏览也是我们一直关注的场景。在过去我们提供了H5的预览方式,但是只能对每个项目单独扫码,更多是考虑到设计验收场景下可以在实际环境中预览移动端稿件。这次RELAY小程序是内容管理延伸到移动端的尝试,提供所有设计项目的常规浏览。在体验上则希望依然可以做到轻而快,除了常规的按业务线和项目查看设计稿件之外,我们单独增加了最近浏览与搜索项目两个Web端没有的功能:最近打开会同步大家在移动端最近查看的项目,搜索则可以搜索全部业务线中的项目——都是考虑到业务线-项目的层级限制带来的效率成本,打破了业务线界限的浏览方式。可以在web端项目内生成预览二维码,通过微信扫描打开小程序预览;同时也在京M中可以打开H5端的页面,暂时开放稿件列表和稿件详情的预览页面。
基本搭建
首先,Taro需要使用 npm 或者 yarn 全局安装 @tarojs/cli
,然后项目初始化,他会默认开始安装项目所需要的依赖。
(1) npm install -g @tarojs/cli
(2) taro init myApp
项目实践
小程序总体分为,登录,业务线列表,最近浏览列表,稿件列表包含查看稿件,用户可以很轻松找到自己最近使用项目,锁定稿件位置并查看。H5端页面在京me中打开PC端relay项目稿件列表、稿件详情和预览链接,转化为H5页面。项目中分为两大块来给大家介绍,分别是小程序的京东登录和稿件列表页操作。
项目展示
登录
小程序采用的京东插件登录,目前仅京东员工可以登录。微信小程序里面开放插件的开发和调用,需要走流程申请插件的使用,可以在小程序后台第三方设置里添加京东登录插件,方便为小程序提供登录的基础服务。
首先进入小程序会有一个落地页面,点击按钮使用京东账号登录后会跳转到京东插件登录页面。
这里需要注意的是,登录成功后是需要知道跳转到某个页面,默认进来是首页,如果其他页面上重新登录,需要跳转到当前页面。所以这里,需要携带的参数是在登录成功后跳转的页面路径。
当京东登录插件登录后返回跳转到登录校验页面,因为在这个页面里还有我们自己的登录逻辑,需要本地存储两个标识,用这两个标识和后端交互换取用户信息。
来看下校验页面的主要登录逻辑:
-
首先会先判断本地是否有存两个标识的storage,如果存在,就请求后端接口
/api/regist
返回用户信息,然后全局存储用户的信息方便页面使用。 -
如果没有这两个字段,京东插件的登录状态,如果已经登录,并且有京东插件的Storage信息:那就带着这个key请求我们自己的接口。调用login方法,请求
/user/login
接口,这个接口会返回携带两个['Set-Cookie']字段,用定义好的getCookie(通过正则匹配值new RegExp("(^|,|, )" + name + "=([^;]*)(;|$)")
)方法拿到两个标识的值。 -
假如都没有的话或者是首次进入,就去落地页面。
我们可以看下完整的流程图:
到这里小程序登录就搭建完成了,还需要在项目中加一些状态判断。接下来看下稿件列表页面,在这个页面可以直接查看稿件,支持手势缩放查看。
稿件列表
首先看到这个页面,分析一下结构: 头部是显示当前分组的名称和当前分组的稿件数量。下面是所有分组的稿件,并且分组带有标题。点击头部分组展开当前项目的所有的分组名称,点击分组列表会跳到指定分组上,同样的,列表在滚动的时候,头部会根据滚动的位置来改变展示的分组名称。点击图片是可以查看大图,并支持缩放查看。
稿件列表页分为四个组件 当前detail文件下存放稿件列表页的内容,index相当于初始化组件,主要用来请求数据和loading 的判断:
- list 组件可以把主体结构搭建起来,并保留头部列表逻辑。
- scroll 组件存放的是页面主体内容,分组的标题和卡片的内容。
- unPage 组件存放的是卡片的内容,稿件标题和图片。
- 我们在做的过程中,用到过的Taro的组件包括:ScrollView,previewImage,Image。
首先我们先来介绍第一个功能,点击头部分组选项,页面滚动到指定位置,按照我们正常的逻辑,首先在循环数据的时候,每个分组分配一个id,然后点击头部分组的时候,取到对应的id,去匹配列表的分组id,取offsetTop 值,然后滚动到指定位置。
📣 注意一点:在Taro中元素绑定id,不能是直接数字开头,因为数据id 是数字开头的,所以我们在绑定的时候拼接一个字符串开头,"ID"。
在不断的尝试下,发现在Taro中是不可以直接使用document去获取元素的,但是在H5环境下是可以的,所以这里做的兼容处理,在h5端直接获取元素的offsetTop值是可以的,在小程序端可以用提供的apiTaro.createSelectorQuery().selectAll('元素').boundingClientRect
,可以取到元素距离顶部的距离,但是这是随着页面滚动而改变的,这样的话是有问题的,页面滚动后,我们在切换分组的时候其实是想知道offsetTop固定值,所以,我们暂且在进入页面的时候,把分组的值存起来,用domOffsetTop存起来:
Taro.createSelectorQuery().selectAll('.group_item').boundingClientRect(function(rects){
rects.forEach(function(rect){
groupId[rect.id] = rect.top
_this.setState({
domOffsetTop:groupId
});
})
}).exec()
这样的话还是会有一定的风险,如果网络出现卡顿或者图片加载慢一下,会导致获取的值不准确,即使加上延时。然后发现可以借助ScrollView组件的scrollIntoView
属性,可以直接把这个属性值设置成选择的分组id。然后handleChangeGroup方法就是点击分组的方法,这样就可以在小程序和H5端做好了定位功能。
接下来我们看下页面滚动功能,也是这次功能出问题的点,首先来看一段代码:
onScroll = (event=>{
contentItems.map(dom => {
if (满足条件) {
锁定分组
}
})
})
ScrollView的onScroll的方法里,循环页面中所有分组,利用getBoundingClientRect拿到某个元素相对于视窗的位置集合。集合中有top, right, bottom, left等属性。我们判断top值和bottom值,如果头部小于70并且底部是大于0的,就是当前到达了这个分组,然后找到和当前id匹配的那条数据来做更新。这样做在H5端是没有问题的,但是,在微信小程序端我们只能用提供api来实现:
Taro.createSelectorQuery().selectAll('.group_item').boundingClientRect(function(rects){
rects.forEach(function(rect){
if (满足条件) {
找到和当前id匹配的那条数据来做更新
}
})
}).exec()
然后在小程序真机调试下发现,页面在滚动的时候非常卡顿,并且伴有闪烁,像下面这样:
开始分析页面,难道是卡片比较多吗? 并不然,卡片比较少的时候也会卡顿,因为这里比较特殊,没有做上滑加载,毕竟点击头部迅速到达指定的位置,所以开始找问题吧。
分三个点来做优化:
🔔 第一点 :既然用到onScroll事件,频繁触发某个事件,会想到防抖和节流,这两个都是为了解决频繁触发某个事件的情况造成的性能消耗。
-
防抖就是在触发后的一段时间内执行一次,例如:在进行搜索的时候,当用户停止输入后调用方法,节约请求资源。
-
节流就是在频繁触发某个事件的情况下,每隔一段时间请求一次,类似打游戏的时候长按某个按键,动作是有规律的在间隔时间触发一次。
我们就用防抖的方式改造一下,把方法放在util文件下,并抛出debounce方法: onScroll = debounce((event)=>{})
🔔 第二点 :其实第一个优化点就有效的降低了触发的事件次数,紧接着还有一个问题就是,在互动分组满足条件的时候,分组头部到达顶部时,其实这个判断是总会有一个分组会满足,所以也会不停地触发,那么我们定义一个state值scrollHeadID,记录当前是哪个分组的id,初始化进入页面默认保存第一个分组的id,所以滑动分组的时候进入符合条件内,还需要加一层判断:
rect.id.slice(2) === item.id && rect.id != _this.state.scrollHeadID
也就是头部和底部距离满足,假如判断找到当前那条item数据,并且当前的id不是保存的id,如果是的话不需要更改,只有不一样的时候才改变。
🔔 第三点 : 到现在为止还不够,因为最重要的一点来了,如果页面稿件多了的话,还是一样会卡顿,所以再进一步寻找切入点来。在react中state作为一个重要的部分,固然自动渲染给我们省了很多的麻烦,然而并不是我们想让他不渲染就很容易做到的,一些时候,state的重新渲染机制导致了不必要的渲染,所以研究一下用来一些特定的情况下也是很有必要的。
所以我在看子组件render 的时候发现,滑动的时候如果满足了会有setState
来改变状态值,但是这样的话,假如稿件很多的话,当父组件改变state 的时候会导致子组件重新渲染,数据多必然会造成很多的资源重新加载浪费,因为稿件此时是固定的。
于是看到官方提供的语法,React.PureComponent
,一起来看下是怎么使用的,两种使用方式,一个是class类的使用方式,另一种是函数式:
class Demo extends PureComponent{
render() {
console.log(“是否重复渲染?”)
return (
{ this.props.cont }
)
}
}
import React { memo } from 'react'
const memoDemo = memo(props => {
return <div>...</div>
})
📣 注意:React.PureComponent 要依靠 class类的形式才能使用。而 React.memo() 可以和 functional component 一起使用。React.PureComponent和React.memo()默认仅用作对象的浅层比较。还有另外一种方式,那就是shouldComponentUpdate
函数 , 他是重渲染时render()函数调用前被调用的函数,它接受两个参数:nextProps和nextState,分别表示下一个props和下一个state的值。并且,当函数返回false时候,阻止接下来的render()函数的调用,阻止组件重渲染,而返回true时,组件照常重新渲染。感觉这种方式更加直接和灵活,所以采用shouldComponentUpdate函数的方式来修改下,在scroll组件中我们会传入一个domId,可以用这个id做判断,到最后一层在unPage组件中先判断出如果传过来说的卡片数据相同就返回false,阻止渲染:
scroll组件:
shouldComponentUpdate(nextProps) {
if (this.state.imgUpdate) return true
if(this.props.domId != 'fasle' && !this.state.isRender){
if(nextProps.domId == this.props.domId){
return false
}
}
return true
}
unPage组件:
shouldComponentUpdate(nextProps) {
if(nextProps.page == this.props.page){
return false
}
return true
}
到现在,我们经过了三层的优化,看看效果:
可以看出进入稿件页面滑动很顺滑,并且查看大图也是很顺畅的。
同时,在pc端的项目列表右侧面板中,可以直接扫描二维码进入小程序里面当前的项目列表。
有时候会有这种场景,在使用RELAY平台的时候,我可能在pc端浏览的时候,需要复制稿件列表或者稿件详情给我的同事来一起看下项目的稿件,如果用手机在咚咚内直接复制链接,对方打开的其实是pc端的页面,所以就需要生成H5端的页面,目前暂时先转化稿件列表和稿件详情页面。 在pc端main.js中加入判断,在全局路由钩子函数中判断是移动端并且是京M环境中再打开H5页面:
router.beforeResolve( async (to, from, next) => {
var reg = RegExp(/\/preview/)
var userAgent = navigator.userAgent.toLowerCase()
if(userAgent && /(iphone|android).*jdme/.test(userAgent)){
判断跳转指定页面
}
})
查看大图
在小程序端,我们可以使用自带的api语法 Taro.previewImage
,这个函数接受一个对象,current表示当前显示图片的http链接,urls是数组形式,传入当前可预览的一组图片,也就是当前列表页面的所有稿件的连链接地址。所以我们在unPage组件中给Image增加点击事件,在list组件中拿到数据就已经把稿件列表的图片存到list的数组中,所以可以直接用了。handlePreviewImage:
handlePreviewImage = (imageUrl)=>{
const { list } = this.props
Taro.previewImage({
current:imageUrl, // 当前显示图片的http链接
urls: list, // 需要预览的图片http链接列表
showmenu:false
})
}
可以看到点击图片可以查看大图,并且可以切换稿件查看,那是不是在H5端以同样适配呢?很遗憾,并不能完全适配,在H5页面中只能切换图片,不能支持手势缩放,所以这时候我们就需要判断设备,当H5端页面的时候采用另外一个解决方案。这里用到了一个内置环境变量process.env.TARO_ENV
。用于判断当前编译类型,目前有 weapp 、 swan 、 alipay 、 h5 、 rn 、 tt 、qq 、 quickapp 8个取值,可以通过这个变量来书写对应一些不同环境下的代码,在编译时会将不属于当前编译类型的代码去掉,只保留当前编译类型下的代码,例如想在微信小程序和 H5 端分别引用不同资源。
那在H5端是可以引入第三方插件的,所以我一次采用的是WxImageViewer
,虽然需要的功能都有,但是效果是有问题的,在滑动图片的时候图片会飞出屏幕。这肯定不行的。还是继续探索......经过三个插件的适用,react-photo-view
效果是最好的。
可以使用npm或者yarn来安装:
npm||yarn add react-photo-view
在项目中引入插件,同时引入css样式表,基本用法就是像下面这样嵌套的方式:
import { PhotoProvider, PhotoConsumer } from 'react-photo-view';
import 'react-photo-view/dist/index.css';
function ImageView() {
return (
<PhotoProvider>
{photoImages.map((item, index) => (
<PhotoConsumer key={index} src={item} intro={item}>
<img src={item} alt="" />
</PhotoConsumer>
))}
</PhotoProvider>
);
}
看起来有些麻烦,官方还提供了另外一种使用插件方式PhotoSlider
,它继承自 PhotoProvider,手动控制 react-photo-view 的展现与隐藏,也就是说现有的unPage结构可以不用改动,新增加PhotoSlider组件来做H5页面的点击查看大图。
return (
<View className='img-border'>
<View key={page.id} className='img-border'>
<Image className='img' mode='widthFix' onClick={(e)=>{this.handlePreviewImage(page.image,page.id,e)}} src={src} onError={this.imgError.bind(this, page)} />
</View>
</View>
<PhotoSlider
onIndexChange={this.setPhotoIndex.bind(this)}
images={imgArr.map(item => ({ src: item }))}
visible={isOpen}
onClose={() => this.onClose(false)}
index={photoIndex}
photoClosable={true}
/>
</View>
)
images: 是稿件的图片列表数组。 visible:是否展示。 index: 是当前图片的索引值。 onIndexChange:索引改变回调。
这里要注意的一点,不同于小程序端 Taro.previewImage
,在切换图片的时候需要在索引改变回调函数中重新设置当前的图片的索引值。
setPhotoIndex = (e) => {
this.setState({photoIndex: e})
}
我们来看此时在H5端页面中的展示效果吧:
总结
有了小程序和移动端H5的加入,相信会给很多用户带来极大的方便,当你在公交车、地铁、或者会议上,在手机上就可以预览最近的项目,快速预览稿件,让您无缝连接。其实在这次的小程序开发中,同样也是遇到了不少的问题和一些坑,也是一步一步的去优化和解决问题。同时需要总结和规划开发步骤,遇到困难一步一步的去梳理,总会有些收获。如果大家对RELAY平台有好的建议,欢迎讨论,谢谢大家。
欢迎大家查看RELAY协作设计平台 (relay.jd.com) 也希望大家多提出宝贵的意见,期待更进一步的交流,一起共同进步。
另呈上RELAY小程序二维码: