仿腾讯视频-微信小程序

2,541 阅读27分钟
  腾讯视频是一个让我们都喜爱的视频观看平台,用户群体也相当庞大。小编也非常喜欢使用腾讯视频播放软件,在娱乐的时间之中,也给本人来许多快乐。

前言

    在学习了小程序之后,为了巩固自身的学习知识和提高实战能力。小编也非常的喜欢写一个属于自己的小程序,而且也发现有些人写的视频类小程序不是很细节,所以小编选了‘腾讯视频’小程序,也开始走上一条“踩坑”的不归路。写下这边文章也是为了纪念自己的痛苦之路,同时也希望给学习小程序的你带来丁点帮助。

项目部分gif演示





1. 前期准备

2. tabBar设计

    在设计小程序的tabBar时,直接使用微信小程序官方提供给我们的tabBar。那如何使用微信小程序提供的tabBar来设计腾讯视频小程序的tabBar呢?

    a.首先,使用微信开发者工具(或者VScode)打开你新建的小程序项目,找到你的小程序中的app.json文件。在app.json文件中的pages项,新增如下配置:

                 
    

    b.接着,按(ctrl+s)进行保存。此时,微信开发者工具会帮你新建四个页面文件夹,你在pages文件夹打开即可看到这四个文件夹。

    c.然后,在pages同级目录下,新建images用来放置程序图片资源。紧接着我们去阿里巴巴矢量图标库搜索自己需要的tabBar对应的图标,把它们下载放置到imgages中去。

    d.开始配置tabBar,找到你的小程序中的app.json文件。在app.json文件中的tabBar项,新增如下配置:

"tabBar": {    "color": "#000000",    "selectedColor": "#FF4500",    "list": [      {        "pagePath": "pages/main/main",        "text": "首页",        "iconPath": "images/shouye.png",        "selectedIconPath": "images/shouye-action.png"      },      {        "pagePath": "pages/shortVideo/index",        "text": "短视频",        "iconPath": "images/duanshiping.png",        "selectedIconPath": "images/duanshiping-action.png"      },      {        "pagePath": "pages/brush/brush",        "text": "刷一刷",        "iconPath": "images/shuayishua.png",        "selectedIconPath": "images/shuayishua-action.png"      },      {        "pagePath": "pages/mine/mine",        "text": "我的",        "iconPath": "images/mine.png",        "selectedIconPath": "images/mine-action.png"      }    ]  }

   e.效果图如下:


               

3. 数据请求

    日常小程序开发过程中基本时通过微信小程序开发工具提供的wx.request来做数据请求,那么怎么可以让自己定义的数据库呢?我们这里采用云开发的微信提供的免费云数据库,做数据请求。在项目的cloudfunctions文件夹下新建几个自己需要的云函数请求响应的数据请求。
    以获取搜索建议为例:
    1. 云函数部分:

// 云函数入口文件const cloud = require('wx-server-sdk')const env = 'dhyuan'cloud.init()// 获取数据库句柄suggestVideoconst db = cloud.database({ env })// 云函数入口函数exports.main = async (event, context) => {  // cloud.getWXContext()直接拿到用户信息  console.log(event.key)  // 查询建议的 模糊查询  const _ = db.command  let suggestVideo = await db.collection('suggestVideo').where(_.or([{    keyword: db.RegExp({        regexp: '.*' + event.key,        options: 'i',      })    }  ])).get({    success: res => {      console.log(res)    },    fail: err => {      console.log(err)    }  })  let returnResult = [];  for (let i = 0; i < suggestVideo.data.length; i++) {    returnResult.push(suggestVideo.data[i])  }  return returnResult.sort((a,b) => a.createTime < b.createTime ? 1 : -1)}

     2. 搜索页中的 数据请求调用 与函数部分:

// 搜索建议  searchSuggest() {    const self = this;    //展示标题栏的loding    wx.showNavigationBarLoading();    //调用云函数    wx.cloud.callFunction({      name: 'search',      data:{ key: self.data.searchKey },      success(res){        // console.log(res);        self.setData({          showvideoresult: true,          searchsuggest: res.result        })      },      fail(err){        // console.log(err);        self.setData({          showvideoresult: false        })      },      complete(){        //让 loding消失        wx.hideNavigationBarLoading();      }    })      },

 4. 视频搜索

      在小程序开发中,搜索功能是一个比较难实现的功能,尤其涉及数据求以及动态化的实时搜索。下面小编进行一步一步搜索功能的解析


      搜索的样式设计

      以头部查询为例:(其他样式请见github: 传送门)

      在设计搜索栏的头部时,我们采用原生的样式渲染方式,这样大家也可以理解其实现的原理,所以就不采用UI框架了,当然如果你想使用UI小编也可以推荐你使用WeUI(微信原生视觉体验一致的基础样式库)。
     不多说废话啦,开始动手了。

                            

     1.  实现样式设计思路:使用view包裹search的icon和input,让包裹的view边框角变成圆角,在使用行内块级元素使其在一行显示,并使用vertical-align: middle;居中对齐

     2.  搜索头部基本结构

<!-- 搜索框 -->
  <view class="page__bd">
    <view class="searchsearch-bar">
      <view class="search-bar__box">
        <icon class="icon-search_in-box" type="search" size="14" color='#EDA8A3'></icon>
        <input focus='true' type="text" class="search-bar__input" placeholder="请输入片名、主演或导演" placeholder-style="font-size: 14px;color:#EDA8A3;padding-left:4px" value='{{inputValue}}' bindinput="getsearchKey" bindblur="routeSearchResPage" bindconfirm="searchover" />
        <!-- 点击×可以清空正在输入 -->
        <view class="icon-clear" wx:if="{{share}}">
          <icon type="clear" size="14" color='#EDA8A3' bindtap="clearInput"></icon>
        </view>
      </view>
      <view class="search-bar__cancel-btn {{isCancel?'':'hide'}}" bindtap="cancel">取消</view>
    </view>
  </view>

   3. 样式渲染

/* 搜索bar */
.page__bd {
  position: fixed;
  top:0;
  width:100%;
  background-color:#FF4500;
  z-index:1;
}
.search-bar {
  width:100%;
  display: flex;
  background-color:#FF4500;
  border: 1px  solid #DC4238;
}
.search-bar__box{
  vertical-align: middle;
  height: 65.52rpx;
  margin: 20rpx 10rpx 20rpx 25rpx;
  background-color:#DE655C;
  border-radius: 20rpx;
  width: 75%;
  padding-left: 30rpx;
  padding-right: 30rpx;
  display: inline-block;
  align-items: center;
}
.icon-search_in-box{
  width: 32.76rpx;
  height: 32.76rpx;
  vertical-align: middle;
  display: inline-block;
}
.icon-clear{
  width: 32.76rpx;
  height: 32px 0.76rpx;
  vertical-align: middle;
  display: inline-block;
  margin-left: 80rpx;
}
.search-bar__input{
  vertical-align: middle;
  display: inline-block;
}
.search-bar__cancel-btn {
  color:#ffffff;
  display: inline-block;
  font-size:32rpx;
}

     搜索功能部分

     1. 实现思路:a. 关键字搜索建议:绑定input输入框使其每输入一个值触发关键字搜索建议操作,并展示给用户观看,此时展示你的搜索建议view设置z-index;b. 关键字搜索结果:当你输完关键字回车时,触发搜索结果操作,云函数去查询云数据库并放回相关数据;c.取消:当你点击取消时,此时小程序会放回到首页;d.搜索历史:当你每次输完关键字点击回车时,使用wx.setStorageSync保存数据到本地,当回到搜索主页时,从本次内存取出你查询过的关键字即可。
  2. 实现关键字搜索建议

                       

    页面js求

searchResult() {    // console.log(this.data.searchKey)    const self = this;    //展示标题栏的loding    wx.showNavigationBarLoading();    //调用云函数    wx.cloud.callFunction({      name: 'searchResult',      data:{ key: self.data.searchKey },      success(res){        // console.log(res);        self.setData({          showvideoresult: false,          searchresult: res.result        })      },      fail(err){        // console.log(err);        self.setData({          showvideoresult: false        })      },      complete(){        //让 loding消失        wx.hideNavigationBarLoading();      }    })  }

    云函数:

// 云函数入口文件const cloud = require('wx-server-sdk')const env = 'dhyuan'cloud.init()// 获取数据库句柄suggestVideoconst db = cloud.database({ env })// 云函数入口函数exports.main = async (event, context) => {  // cloud.getWXContext()直接拿到用户信息  console.log(event.key)  // 查询建议的 模糊查询  const _ = db.command  let suggestVideo = await db.collection('suggestVideo').where(_.or([{    keyword: db.RegExp({        regexp: '.*' + event.key,        options: 'i',      })    }  ])).get({    success: res => {      console.log(res)    },    fail: err => {      console.log(err)    }  })  let returnResult = [];  for (let i = 0; i < suggestVideo.data.length; i++) {    returnResult.push(suggestVideo.data[i])  }  return returnResult.sort((a,b) => a.createTime < b.createTime ? 1 : -1)}

    3. 关键字搜索结果

                       

      js请求

// 搜索结果  searchResult() {    // console.log(this.data.searchKey)    const self = this;    //展示标题栏的loding    wx.showNavigationBarLoading();    //调用云函数    wx.cloud.callFunction({      name: 'searchResult',      data:{ key: self.data.searchKey },      success(res){        // console.log(res);        self.setData({          showvideoresult: false,          searchresult: res.result        })      },      fail(err){        // console.log(err);        self.setData({          showvideoresult: false        })      },      complete(){        //让 loding消失        wx.hideNavigationBarLoading();      }    })  }

    云函数

// 云函数入口文件const cloud = require('wx-server-sdk')const env = 'dhyuan'cloud.init()// 获取数据库句柄suggestVideoconst db = cloud.database({ env })// 云函数入口函数exports.main = async (event, context) => {  // cloud.getWXContext()直接拿到用户信息  console.log(event.key)  // 查询建议的 模糊查询  const _ = db.command  let serultVideo = await db.collection('searchResult').where(_.or([{    title: db.RegExp({        regexp: '.*' + event.key,        options: 'i',      })    },{      artists: db.RegExp({        regexp: '.*' + event.key,        options: 'i',      })    }  ])).get({    success: res => {      console.log(res)    },    fail: err => {      console.log(err)    }  })  let returnResult = [];  for (let i = 0; i < serultVideo.data.length; i++) {    returnResult.push(serultVideo.data[i])  }  return returnResult.sort((a,b) => a.createTime < b.createTime ? 1 : -1)}

     特别注意:

    搜索中有可能出现“抖动现象”,那么如何解决该现象呢?此时,你需要采用debounce来解决,防止用户多次输入抖动触发搜索,从而导致多次不必要的数据请求。

      具体解决如下:

//获取input文本并且实时搜索,动态隐藏组件  getsearchKey: function (e) {    // console.log(e.detail.value) //打印出输入框的值    let that = this;    if (e.detail.cursor != that.data.cursor) { //实时获取输入框的值      that.setData({        searchKey: e.detail.value      })    }    if (e.value != "") { //组件的显示与隐藏      that.setData({        showView: false,        share: true      })    } else {      that.setData({        showView: ""      })    }    if (e.detail.value != "") { //解决 如果输入框的值为空时,传值给搜索建议,会报错的bug      that.debounce(that.searchSuggest(), 300)    }  },  // 去除输入抖动  debounce (func, delay) {    let timer    let self = this    return function (...args) {      if (timer) {        clearTimeout(timer)      }      timer = setTimeout(() => {        func.apply(self, args)      }, delay)    }  },

    4. 取消
     js操作

//实现取消功能,停止搜索,返回首页  cancel: function () {    wx.switchTab({      url: '/pages/main/main'    })  },

      5. 搜索历史



      js操作

routeSearchResPage: function (e) {    // console.log(e.detail.value)    // 将数据存入本地    if (this.data.searchKey) {      let history = wx.getStorageSync("history") || [];      history.push(this.data.searchKey)      wx.setStorageSync("history", history);    }  },
//每次显示变动就去获取缓存,给history,并for出来。  onShow: function () {    this.setData({      history: wx.getStorageSync("history") || []    })  }

     wxml对应部分

<!-- 搜索历史 -->  <view class="{{showView?'hot_keys':'header_view_hide'}}">    <view class="title history">搜索历史</view>    <icon type='clear' bindtap="clearHistory" class="title record" color="#DE655C"></icon>    <view class='hot_key_box'>      <view wx:for="{{history}}" wx:key="{{index}}">        <view class='hot_keys_box_item' data-value='{{item}}' data-index="{{index}}" bindtap="fill_value">          {{item}}        </view>      </view>    </view>  </view>


5. 首页部分

    首页基本是结构的设计,以及轮播和菜单切换,主要时考验我们wxss的功底和js交互功底。

     1.样式结构设计

      结构设计基本没什么大的难度,小编就不多废话了,详情见github项目(传送门)。结果如下图:

                                                                                                

     2.滑动菜单切换&轮播

      a. 对于菜单的滑动切换,其实实现非常简单
      在实现之前,你需要了解的几个标签:swiper,swiper-item,scroll-view;滑块视图容器。swiper:其中只可放置swiper-item组件,否则会导致未定义的行为;scroll-view可滚动视图区域。使用竖向滚动时,需要给scroll-view一个固定高度,通过 WXSS 设置 height。组件属性的长度单位默认为px,2.4.0起支持传入单位(rpx/px)。
     b. 菜单滑动切换实现思路:给swiper绑定一个bindchange='swiperChange'事件,每当用户滑动页面时,触发'swiperChange'事件,并且在js中定义一个数据变量为curentIndex,通过监听if(e.detail.source == 'touch')其中的touch事件,从而让curentIndex用来记录每次滑动切换的swiper-item,并通过wx:if="{{curentIndex == 0}}来判断当前的swiper-item是否显示,从而达到滑动切换菜单的效果。并且,菜单栏的index也与curentIndex进行判断,从而让指定的菜单高亮显示。
   c. 实现过程
     1. wxml部分

 <!-- 视频菜单类型 -->  <scroll-view class="shouye-menu {{scrollTop > 40 ? 'menu-fixed' : ''}}" scroll-x="{{true}}" scroll-y="{{false}}">    <block  wx:for="{{moviesCategory}}" wx:key="{{item.id}}">      <text           class="{{curentIndex === index ? 'active' : ''}}"          data-id="{{item.id}}"          data-index="{{index}}"          bind:tap="switchTab"          >{{item.name}}</text>    </block>  </scroll-view><!-- 切换主体部分 -->
<view class="content" style="height:{{ch}}rpx;">  <swiper class="swiper" bindchange='swiperChange' current='{{curentIndex}}'>    <swiper-item>      <scroll-view scroll-y class="scroll" wx:if="{{curentIndex == 0}}">      </scroll-view>    </swiper-item>
  </swiper>
<view>

    2. js 部分

//改变swiper  swiperChange: function(e) {//切换    if(e.detail.source == 'touch') {      let curentIndex = e.detail.current;      this.setData({        curentIndex      })    }  },  switchTab(e){    this.setData({      curentIndex:e.currentTarget.dataset.index,      toView: e.currentTarget.dataset.id    })  }

    d. 你可能会踩的“坑”

    在你使用 swiper 和scroll-view时,会出现swiper-item中的内容超出可视范围时,无法上下滑动问题。这是你要第一时间想到“swiper高度自适应”这个关键词。小编在这提供几种解决方式。
   方案一:

    swiper高度固定,swiper-item默认绝对定位且宽高100%,每个swiper-item中内容由固定高度的child组成,然后根据child数量动态计算swiper高度,初始方案(由于rpx针对屏幕宽度进行自适应,child_height使用rpx方便child正方形情况下自适应):
swiper_height = child_height * child_num
    屏幕效果仅在宽度375的设备(ip6、ipⅩ)完美契合,其他设备都底部会出现多余空隙,并且在上拉加载过程中,随着内容增加,底部空隙也逐渐变大。

     方案二:

swiper_height = child_height * child_num * ( window_width / 750 )

然后并无变化,我们可以看到child_height在不同宽度屏幕下,显示的宽高尺寸是不一样的(px单位),那就尝试使用box在各个屏幕的实际高度进行计算swiper高度,box的高度可以单独在页面中增加一个固定标签,该标签样式和box宽高保持一致并且隐藏起来,然后在pageonload中通过wx.createSelectorQuery()获取标签实际高度baseItemHeightpx单位):

swiper_height = baseItemHeight * child_num

结果显示原本的ip6、ipⅩ没有问题,另外宽带小于375的ip5上也ok,但是在大于375的设备上还是出现空隙,比如ip的plus系列

     方案三:

  swiper底部有一个load标签显示“加载更多”,该标签紧贴box其后,通过wx.createSelectorQuery()来获取bottom,然而你会发现bottom是标签的heighttop的和。计算底部空隙(暂时忽略“加载更多”标签高度)space_height = swiper_height - load_top刚计算完可以看到在静止状态下,计算出space_height拿去修改swiper_height显示空隙刚好被清掉了,但是接着就发现在动过程中获取到的bottom是不固定的,也就是说数值可能不准确导致space_height计算错误,显示效果达不到要求

      小编采用的是方案一
      思路:给swiper一个外部view装载swiper,并给它设置style="height:{{ch}}rpx;",这里的ch为js中的数据变量方便动态修改view的高度。并且在js中的钩子函数中的onLoad函数中编写如下代码:

 onLoad: function (options) {    wx.getSystemInfo({      success: res => {        //转为rpx        let ch = (750 / res.screenWidth) * res.windowHeight - 180;        this.setData({          ch        })      },    })
}

式子中减 180 ,是小编自己调试的数据,具体减多少,可以根据具体情况而定。其实说白了,该方案的设计,基本是与better-scoll的滑动策略基本雷同。

6. 视频播放

     

1. 主体设计

    a. 主体结构设计

<!-- pages/videoDetail/index.wxml --><view class="detailContainer">    <!-- <view class="detail_movice">{{showModalStatus == true ? 'stopScroll' : ''}}        <video src="{{entitie.video}}" id="{{entitie.id}}" hidden="{{currentVid != entitie.id}}"></video>        <image src="{{entitie.image}}" bind:tap="play" data-vid="{{entitie.id}}" hidden="{{currentVid == entitie.id}}">            <view class="label">{{entitie.duration}}</view>        </image>    </view> -->    <view hidden="{{tvphide}}">        <txv-video vid="{{vid}}" class="{{detailOn ? '' : 'on'}}" width="{{width}}" height="{{height}}" playerid="txv0" autoplay="{{autoplay}}" controls="{{controls}}" title="{{title}}" defn="{{defn}}" vslideGesture="{{true}}" enablePlayGesture="{{true}}" playBtnPosition="center" bindstatechange="onStateChange" bindtimeupdate="onTimeUpdate" showProgress="{{showProgress1}}" show-progress="{{false}}" bindfullscreenchange="onFullScreenChange"></txv-video>    </view>    <!-- 介绍信息 -->    <view class="introduce">        <view class="top">            <text class="introduce-top-title">{{entitie.header}}</text>            <text class="introduce-top-text" bind:tap="showModal">简介</text>        </view>        <view class="center">            <text class="introduce-center-text">8.6分·VIP·视频·全36集·8.8亿</text>        </view>        <view class="bottom">            <image class="ping" src="../../images/ping.png" lazy-load="{{true}}" bind:tap="" />            <image class="like" src="../../images/like.png" lazy-load="{{true}}" bind:tap="" />            <image class="share" src="../../images/share.png" lazy-load="{{true}}" />            <button data-item='1' plain='true' open-type='share' class='share'></button>        </view>    </view>    <!-- 剧集 -->    <view class="episode">        <view class="top">            <text class="episode-top-title">剧集</text>            <text class="episode-top-text" bind:tap="showModal">每周一二三20点更新2集,会员多看6集</text>        </view>        <view class="center">            <block wx:for="{{episodes}}" wx:key="{{index}}">                <view class="gather" data-vid="{{item.vid}}" data-num="{{item.num}}" bind:tap="select">                    <view class="{{vid == item.vid ? 'episode-num' : '' }}" data-vid="{{item.vid}}" data-num="{{item.num}}">                        {{item.num}}                    </view>                </view>            </block>        </view>    </view>    <!-- 精彩片花 -->    <view class="clips">        <view class="clips-title">            <text class="clips-title-text">精彩片花</text>        </view>        <view class="mod_box mod_feeds_list">            <view class="mod_bd">                <view class="figure_box" wx:for="{{clips}}" wx:for-item="video" wx:for-index="index" wx:key="{{video.vid}}">                    <view class="mod_poster" data-vid="{{video.vid}}" data-index="{{index}}" bindtap="onPicClick">                        <image class="poster" src="{{video.img}}"></image>                        <!-- 标题、时间和播放按钮 -->                        <view>                            <image class="play_icon" src="https://puui.qpic.cn/vupload/0/20181023_1540283106706_mem4262nz4.png/0"></image>                            <view class="time">{{video.time}}</view>                            <view class="toptitle two_row">{{video.title}}</view>                        </view>                    </view>                </view>            </view>            <view wx:if="{{currVideo.vid}}" style="top:{{top+'px'}};" class="videoContainer">                <txv-video vid="{{currVideo.vid}}" playerid="tvp" autoplay="{{true}}" danmu-btn="{{true}}" width="{{'100%'}}" height="{{'194px'}}" bindcontentchange="onTvpContentChange" bindplay="onTvpPlay" bindended="onTvpEnd" bindpause="onTvpPause" bindstatechange="onTvpStateChanage" bindtimeupdate="onTvpTimeupdate" bindfullscreenchange="onFullScreenChange"></txv-video>                <view class='pinList'>                    <footer></footer>                </view>            </view>        </view>    </view>    <!-- 弹出框 -->    <view animation="{{animationData}}" class="commodity_attr_box" wx:if="{{showModalStatus}}">        <view class="commodity_hide">            <text class="title">{{entitie.header}}</text>            <view class="commodity_hide__on" bind:tap="hideModal"></view>        </view>        <view class="hight" catchtouchmove="stopScroll">            <scroll-view scroll-y class='hightDataView' style="height:{{ch}}rpx;">                <view class="top">                    <view class="top-text">每周一二三20点更新2集,会员多看6集</view>                    <view class="top-descrese">                        {{entitie.score}}分·VIP·{{entitie.type}}·全{{entitie.universe}}集·8.8亿                    </view>                </view>                <view class="center">                    <scroll-view class="star-list" scroll-x="{{true}}" scroll-y="{{false}}">                        <block wx:for="{{entitie.stars}}" wx:key="{{index}}">                            <view class="item">                                <image class="starImg" src="{{item.starImg}}" lazy-load="ture" />                                <view class="name">{{item.name}}</view>                            </view>                        </block>                    </scroll-view>                </view>                <view class="bottom">                    <view class="title">简介</view>                    <view class="text">{{entitie.original_description}}</view>                </view>            </scroll-view>        </view>    </view></view>

     b. js交互

// pages/videoDetail/index.jsconst entities = require('../../data/entities.js')const txvContext = requirePlugin("tencentvideo")const config = require('../../modules/config')const episodes = require('../../data/episodes.js')let currentVideo;Page({  /**   * 页面的初始数据   */  data: {    entitie: null,    id: null,    entities,    clips: null,    currentVid:null,    episodes: null,    tvphide: false,    vid: null,    title: "电视剧",    defn: "超清",    changingvid: '',    controls: !!config.get('controls'),    autoplay: !!config.get('autoplay'),    playState: '',    showProgress1: true,    width: "100%",    height: "auto",    showModalStatus: false,    car:{},    detailOn: true,    ch: 0,    currentIndex: 0,    top: 0,    currVideo:{}  },  play(event){    const target = event.target;    const currentVid = target.dataset.vid;    if(this.data.currentVid!=null){      currentVideo.pause();    }    if(currentVid){      currentVideo = wx.createVideoContext(`${currentVid}`);      this.txvContext.pause();      currentVideo.play();    }    this.setData({      currentVid    })  },  select(e){    const target = e.target;    const currentVid = target.dataset.vid;    const num = target.dataset.num;    console.log(currentVid, num);    this.setData({      vid: currentVid,      clips: this.data.episodes[num-1].clips    })    this.txvContext = txvContext.getTxvContext('txv0');    this.txvContext.play();  },  /**   * 生命周期函数--监听页面加载   */  onLoad: function (options) {    //动态设置 详情的高度  防止滑动失效    wx.getSystemInfo({      success: res => {        //转为rpx        let ch = (750 / res.screenWidth) * res.windowHeight -478;        this.setData({          ch        })      },    })    const id= options.id;    console.log('id', id);    let episode = episodes.find(function(item){      return item.id == id;    })    let entitie = entities.find(function(item){      return item.id == id;    })    this.setData({      entitie    })    //改变page里面的data    this.setData({      id: id,      episodes: episode.episodes,      vid: episode.episodes[0].vid,      clips: episode.episodes[0].clips    })    // console.log('vid', this.data.vid);    this.setData({      controls: !!config.get('controls'),      autoplay: !!config.get('autoplay')    })    this.txvContext = txvContext.getTxvContext('txv0');    this.txvContext.play();    this.videoContext = wx.createVideoContext('tvp');  },  onTvpPlay: function () {    // console.log('play')  },  onStateChange: function (e) {    this.setData({      playState: e.detail.newstate    })  },  onTvpContentChange: function () {  },  onTimeUpdate: function (e) {  },  requestFullScreen: function () {    this.txvContext.requestFullScreen();  },  onFullScreenChange: function () {    // console.log('onFullScreenChange!!!')  },  onTvpTimeupdate: function(){  },  onTvpPause: function () {  },  onTvpStateChanage: function () {  },  onPicClick(e) {    let dataset = e.currentTarget.dataset;    this.currIndex=dataset.index    this.setData({        "currVideo.vid":dataset.vid    })    // console.log(this.data.currVideo)    this.getTop()  },  getTop(){      let query = this.createSelectorQuery();      query.selectViewport().scrollOffset();      query          .selectAll(`.mod_poster`)          .boundingClientRect()          .exec(res => {            let originTop = 0;            this.setData({                top: originTop + this.currIndex * 224.5            })          });  },  /**   * 生命周期函数--监听页面初次渲染完成   */  onReady: function () {  },  /**   * 生命周期函数--监听页面显示   */  onShow: function () {      },  /**   * 生命周期函数--监听页面隐藏   */  onHide: function () {  },  /**   * 生命周期函数--监听页面卸载   */  onUnload: function () {  },  /**   * 页面相关事件处理函数--监听用户下拉动作   */  onPullDownRefresh: function () {  },  /**   * 页面上拉触底事件的处理函数   */  onReachBottom: function () {  },  /**   * 用户点击右上角分享   */  onShareAppMessage: function () {    console.log('share success!')  },  //显示对话框  showModal: function () {    // 显示遮罩层    var animation = wx.createAnimation({      duration: 200,      timingFunction: "linear",      delay: 0    })    this.animation = animation    animation.translateY(300).step()    this.setData({      animationData: animation.export(),      showModalStatus: true,      detailOn: false    })    setTimeout(function () {      animation.translateY(0).step()      this.setData({        animationData: animation.export()      })    }.bind(this), 200)  },  //隐藏对话框  hideModal: function () {    // 隐藏遮罩层    var animation = wx.createAnimation({      duration: 200,      timingFunction: "linear",      delay: 0    })    this.animation = animation;    animation.translateY(300).step();    this.setData({      animationData: animation.export(),    })    setTimeout(function () {      animation.translateY(0).step()      this.setData({        animationData: animation.export(),        showModalStatus: false,        detailOn: true      })    }.bind(this), 200)  },  // 默认阻止滚动  stopScroll() {    return false;  }})

    c. 你可能会遇到的‘坑’

      当你在设计简介的时候,你会发现自己设计的弹出框的内部滑动事件与 当前页的滑动事件一起触发了,那这是为啥呢?仔细想一下,你会发现是冒泡和捕获(详解参考该博文)在搞鬼,相信写过web项目的人对冒泡和捕获非常的熟悉。那么在小程序中也是有的,所以这里你就需要了解滑动穿透这个东西了。那么如何来解决这个问题呐?

     解决办法:在简介中需要滑动的view中 加上catchtouchmove="stopScroll",并且在js中定义stopScroll方法并放回false即可解决。具体如下:
    1. wxml:

<!-- 简介弹出框 -->    <view animation="{{animationData}}" class="commodity_attr_box" wx:if="{{showModalStatus}}">        <view class="commodity_hide">            <text class="title">{{entitie.header}}</text>            <view class="commodity_hide__on" bind:tap="hideModal"></view>        </view>        <view class="hight" catchtouchmove="stopScroll">            <scroll-view scroll-y class='hightDataView' style="height:{{ch}}rpx;">                <view class="top">                    <view class="top-text">每周一二三20点更新2集,会员多看6集</view>                    <view class="top-descrese">                        {{entitie.score}}分·VIP·{{entitie.type}}·全{{entitie.universe}}集·8.8亿                    </view>                </view>                <view class="center">                    <scroll-view class="star-list" scroll-x="{{true}}" scroll-y="{{false}}">                        <block wx:for="{{entitie.stars}}" wx:key="{{index}}">                            <view class="item">                                <image class="starImg" src="{{item.starImg}}" lazy-load="ture" />                                <view class="name">{{item.name}}</view>                            </view>                        </block>                    </scroll-view>                </view>                <view class="bottom">                    <view class="title">简介</view>                    <view class="text">{{entitie.original_description}}</view>                </view>            </scroll-view>        </view>    </view>

     2. js部分

// 默认阻止滚动  stopScroll() {    return false;  }

2.切换电视剧剧集

                

    a. 实现电视剧的剧集切换思路:拿到需要播放视频的vid,将vid替换掉当前的vid,然后执行播放操作。
    b.实现步骤:
     1. 在.json文件中,配置腾讯视频插件组件。如下:

{  "usingComponents": {    "txv-video": "plugin://tencentvideo/video"  }}

  2. 在wxml中使用,如下:

<view hidden="{{tvphide}}">        <txv-video 
          vid="{{vid}}" 
          class="{{detailOn ? '' : 'on'}}" 
          width="{{width}}" 
          height="{{height}}" 
          playerid="txv0" 
          autoplay="{{autoplay}}" 
          controls="{{controls}}" 
          title="{{title}}" 
          defn="{{defn}}" 
          vslideGesture="{{true}}" 
          enablePlayGesture="{{true}}" 
          playBtnPosition="center" 
          bindstatechange="onStateChange" 
          bindtimeupdate="onTimeUpdate" 
          showProgress="{{showProgress1}}" 
          show-progress="{{false}}" 
          bindfullscreenchange="onFullScreenChange"></txv-video></view>

    其中,在txv-video中的属性配置含义:

  • vid: 腾讯视频的vid,用于拿到该视频资源(必须)
  • playerid:playerid必须要全局唯一,可以设置为vid,否则导致视频播放错乱(必须)
  • autoplay:是否自动播放;true|false
  • controls: 是否显示控制栏(播放,暂停,全屏的按钮显示)
  • title:视频标题
  • defn:视频清晰度,默认auto,可选值:流畅,标清,高清,超清,蓝光,4K,杜比

其他属性见:腾讯视频插件官方文档
    3. js交互

select(e){    const target = e.target;    const currentVid = target.dataset.vid;    const num = target.dataset.num;    console.log(currentVid, num);    this.setData({      vid: currentVid,      clips: this.data.episodes[num-1].clips    })    this.txvContext = txvContext.getTxvContext('txv0');    this.txvContext.play();  }

    3. 简介实现

                           

    a. 简介部分主要是wxcss的渲染,没有什么逻辑,需要注意的时,点击下拉可以使简介下拉隐藏,并有下拉的过程出现。
    b. 主要代码如下:

    1. wxml部分:

<view animation="{{animationData}}" class="commodity_attr_box" wx:if="{{showModalStatus}}">        <view class="commodity_hide">            <text class="title">{{entitie.header}}</text>            <view class="commodity_hide__on" bind:tap="hideModal"></view>        </view>        <view class="hight" catchtouchmove="stopScroll">            <scroll-view scroll-y class='hightDataView' style="height:{{ch}}rpx;">                <view class="top">                    <view class="top-text">每周一二三20点更新2集,会员多看6集</view>                    <view class="top-descrese">                        {{entitie.score}}分·VIP·{{entitie.type}}·全{{entitie.universe}}集·8.8亿                    </view>                </view>                <view class="center">                    <scroll-view class="star-list" scroll-x="{{true}}" scroll-y="{{false}}">                        <block wx:for="{{entitie.stars}}" wx:key="{{index}}">                            <view class="item">                                <image class="starImg" src="{{item.starImg}}" lazy-load="ture" />                                <view class="name">{{item.name}}</view>                            </view>                        </block>                    </scroll-view>                </view>                <view class="bottom">                    <view class="title">简介</view>                    <view class="text">{{entitie.original_description}}</view>                </view>            </scroll-view>        </view>    </view>

    2. wxss部分:

.commodity_attr_box {  width: 100%;  height: 100%;  color: #fff;  overflow: hidden;  position: fixed;  bottom: 0;  top: 420rpx;  left: 0;  z-index: 998;  background-color: #1f1e1e;  padding-top: 20rpx;}.commodity_movableView{  width: 100%;  height: 2024rpx;}.commodity_hide{  position: relative;  height: 50rpx;}.commodity_hide .title{  margin-left: 30rpx;  font-size: 35rpx;  line-height: 35rpx;  font-weight: 40;}.commodity_hide .commodity_hide__on{  width: 50rpx;  height: 50rpx;  position: absolute;  display: inline-block;  right: 20rpx;}.commodity_hide .commodity_hide__on::after{  position: absolute;  top: 10rpx;  content: '';  color: #fff;  width: 20rpx;  height: 20rpx;  border-top: 4rpx solid #ece3e3;  border-right: 4rpx solid #ece3e3;  -webkit-transform: rotate(135deg);  transform: rotate(135deg);}.commodity_attr_box .hightDataView{  width: 100%;}.commodity_attr_box .hightDataView .top{  background-color:#1f1e1e;  color: #fff;  height: 140rpx;  box-sizing: border-box;  border-bottom: 4rpx solid #8b8989;}.commodity_attr_box .hightDataView .top .top-text{  font-size: 12px;  margin-top: 35rpx;  margin-left: 30rpx;  margin-right: 50rpx;  color: #C0C0C0;  line-height: 25px;}.commodity_attr_box .hightDataView .top .top-descrese{  margin-left: 30rpx;  font-size: 12px;  line-height: 25px;  color: #C0C0C0;}.commodity_attr_box .hightDataView .center{  border-bottom: 4rpx solid #8b8989;}.commodity_attr_box .hightDataView .center .star-list {  width: 100%;  margin-top: 30rpx;  margin-left: 20rpx;  margin-bottom: 50rpx;  white-space: nowrap;  box-sizing: border-box;}.commodity_attr_box .hightDataView .center .star-list .item{  text-align: center;  display: inline-block;  padding:4rpx;}.commodity_attr_box .hightDataView .center .star-list .item image{  width: 80rpx;  height: 80rpx;  border-radius: 50%;  margin: 10rpx;}.commodity_attr_box .hightDataView .center .star-list .item .name{  font-size: 10px;  font-weight: normal;}.commodity_attr_box .hightDataView .bottom{  width: 100%;}.commodity_attr_box .hightDataView .bottom .title{  margin-left: 30rpx;  font-size: 35rpx;  line-height: 35rpx;  font-weight: 40;  margin-top: 30rpx;}.commodity_attr_box .hightDataView .bottom .text{  font-size: 12px;  font-weight: normal;  text-indent: 34rpx;  margin-top: 20rpx;  color: #C0C0C0;  margin-left: 30rpx;}

    4. 片花部分

   在设计片花部分,最主要的是采用什么方式去解决,一次页面渲染加载多个视频问题,很多人直接用for循环放置,多个视频video标签;其实这是非常笨拙的办法;小编在这做了一个比较高级的办法,那就是:页面放置的都是view来存放该视频的vid,当点击相应图片时,触发一个onPicClick事件,此时拿到需要播放的vid,并通知页面我需要播放某个视频了,请给我一个video去播放视频;
   此外,你需要注意的是,你这个video出现的位置,必须是你点击的图标位置,这样就不会造成页面图片与视频位置不符的问题了。而且,采用这种办法,页可以减缓你的手机的cpu消耗,该办法算是非常高明的手法了。下面来看下怎么具体实现这种高明的手法吧。

   a. wxml部分

 <!-- 精彩片花 -->    <view class="clips">        <view class="clips-title">            <text class="clips-title-text">精彩片花</text>        </view>        <view class="mod_box mod_feeds_list">            <view class="mod_bd">                <view class="figure_box" wx:for="{{clips}}" wx:for-item="video" wx:for-index="index" wx:key="{{video.vid}}">                    <view class="mod_poster" data-vid="{{video.vid}}" data-index="{{index}}" bindtap="onPicClick">                        <image class="poster" src="{{video.img}}"></image>                        <!-- 标题、时间和播放按钮 -->                        <view>                            <image class="play_icon" src="https://puui.qpic.cn/vupload/0/20181023_1540283106706_mem4262nz4.png/0"></image>                            <view class="time">{{video.time}}</view>                            <view class="toptitle two_row">{{video.title}}</view>                        </view>                    </view>                </view>            </view>            <view wx:if="{{currVideo.vid}}" style="top:{{top+'px'}};" class="videoContainer">                <txv-video vid="{{currVideo.vid}}" playerid="tvp" autoplay="{{true}}" danmu-btn="{{true}}" width="{{'100%'}}" height="{{'194px'}}" bindcontentchange="onTvpContentChange" bindplay="onTvpPlay" bindended="onTvpEnd" bindpause="onTvpPause" bindstatechange="onTvpStateChanage" bindtimeupdate="onTvpTimeupdate" bindfullscreenchange="onFullScreenChange"></txv-video>                <view class='pinList'>                    <footer></footer>                </view>            </view>        </view>    </view>

    b.js交互部分

onPicClick(e) {    let dataset = e.currentTarget.dataset;    this.currIndex=dataset.index    this.setData({        "currVideo.vid":dataset.vid    })    // console.log(this.data.currVideo)    this.getTop()  },  getTop(){      let query = this.createSelectorQuery();      query.selectViewport().scrollOffset();      query          .selectAll(`.mod_poster`)          .boundingClientRect()          .exec(res => {            let originTop = 0;            this.setData({                top: originTop + this.currIndex * 224.5            })          });  }

 c. 特别注意:
    在getTop()方法中的逻辑,此处有些费解,为啥要去设置top值。其目的就是,为去矫正你点击某个图片之后,视频可以在相应位置出现,也就达到点击图片播放的效果。

7. 短视频

   该模块实现逻辑,基本与首页差不多,直接看源码即可

                   
     实现基本思路:使用swiper,scroll-view实现左右滑动菜单联动,播放视频思路与播放片花思路基本一致。

      1. json配置
      为啥要配置,因为我们这里使用了腾讯视频插件,以及自己定义的视频尾部的组件,该尾部用于视频分享操作,以及评论操作。配置如下:

{  "usingComponents": {    "txv-video": "plugin://tencentvideo/video",    "footer": "/components/footer/footer"  }}

     2. wxml部分

<!-- pages/shortVideo/index.wxml --><scroll-view class="short-menu {{scrollTop > 40 ? 'menu-fixed' : ''}}" scroll-x="{{true}}" scroll-y="{{false}}">  <block wx:for="{{shortCategory}}" wx:key="{{item.id}}">    <view class="name {{curentIndex === index ? 'active' : ''}}" data-id="{{item.id}}" data-index="{{index}}" bind:tap="switchTab">      {{item.name}}    </view>  </block></scroll-view><view class="content" style="height:{{ch}}rpx;">  <swiper class="swiper" bindchange='swiperChange' current='{{curentIndex}}'>    <block wx:for="{{videos}}" wx:key="{{index}}" wx:for-item="videoList">      <swiper-item class="{{index}}">        <scroll-view scroll-y class="scroll" wx:if="{{curentIndex == index}}">            <view class="mod_box mod_feeds_list">              <view class="mod_bd">                <view class="figure_box" wx:for="{{videoList.video}}" wx:for-item="video" wx:for-index="index" wx:key="{{video.vid}}">                  <view class="mod_poster" data-vid="{{video.vid}}" data-index="{{index}}" bindtap="onPicClick">                    <image class="poster" src="{{video.img}}"></image>                    <!-- 标题、时间和播放按钮 -->                    <view>                      <image class="play_icon" src="https://puui.qpic.cn/vupload/0/20181023_1540283106706_mem4262nz4.png/0"></image>                      <view class="time">{{video.time}}</view>                      <view class="toptitle two_row">{{video.title}}</view>                    </view>                  </view>                </view>              </view>              <view wx:if="{{currVideo.vid}}" style="top:{{top+'px'}};" class="videoContainer">                <txv-video                  vid="{{currVideo.vid}}"                  playerid="tvp"                  autoplay="{{true}}"                  danmu-btn="{{true}}"                  width="{{'100%'}}"                  height="{{'194px'}}"                  bindcontentchange="onTvpContentChange"                  bindplay="onTvpPlay"                  bindended="onTvpEnd"                  bindpause="onTvpPause"                  bindstatechange="onTvpStateChanage"                  bindtimeupdate="onTvpTimeupdate"                  bindfullscreenchange="onFullScreenChange">                </txv-video>                <view class='pinList'>                  <footer></footer>                </view>              </view>            </view>        </scroll-view>      </swiper-item>    </block>  </swiper></view>

     3. js部分

// pages/shortVideo/index.jsconst config = require('../../modules/config')const txvContext = requirePlugin("tencentvideo");const sysInfo =wx.getSystemInfoSync()const shortCategory = require('../../data/shortCategory.js')const videoUrl = require('../../data/videoUrl.js')Page({  /**   * 页面的初始数据   */  data: {    curentIndex: 0,    shortCategory: shortCategory,    videos: videoUrl,    ch: 0,    top: 0,    currVideo:{}  },  //改变swiper  swiperChange: function(e) {//切换    if(e.detail.source == 'touch') {      let curentIndex = e.detail.current;      this.setData({        curentIndex      })    }  },  switchTab(e){    this.setData({      curentIndex:e.currentTarget.dataset.index,      toView: e.currentTarget.dataset.id    })  },  onTvpTimeupdate: function(){  },  onTvpPlay: function () {  },  onTvpPause: function () {  },  onTvpContentChange: function () {  },  onTvpStateChanage: function () {  },  onPicClick(e) {    let dataset = e.currentTarget.dataset;    this.currIndex=dataset.index    this.setData({        "currVideo.vid":dataset.vid    })    console.log(this.data.currVideo)    this.getTop()  },  getTop(){      let query = this.createSelectorQuery();      query.selectViewport().scrollOffset();      query          .selectAll(`.mod_poster`)          .boundingClientRect()          .exec(res => {            console.log(res)            console.log(res[0].scrollTop, res[1][this.currIndex].top)            let originTop = res[0].scrollTop;            this.setData({                top: originTop + this.currIndex * 224.5            })          });  },  /**   * 生命周期函数--监听页面加载   */  onLoad: function (options) {    wx.getSystemInfo({      success: res => {        //转为rpx        let ch = (750 / res.screenWidth) * res.windowHeight - 80;        this.setData({          ch        })      },    })    this.videoContext = wx.createVideoContext('tvp');  },  /**   * 生命周期函数--监听页面初次渲染完成   */  onReady: function () {  },  /**   * 生命周期函数--监听页面显示   */  onShow: function () {  },  /**   * 生命周期函数--监听页面隐藏   */  onHide: function () {  },  /**   * 生命周期函数--监听页面卸载   */  onUnload: function () {  },  /**   * 页面相关事件处理函数--监听用户下拉动作   */  onPullDownRefresh: function () {  },  /**   * 页面上拉触底事件的处理函数   */  onReachBottom: function () {  },  /**   * 用户点击右上角分享   */  onShareAppMessage: function () {  },  // 默认阻止滚动  stopScroll() {    return false;  }})

 8. 我的

关于,我的部分实现基本内容是展示用户头像、姓名,显示是否开通了会员,观看历史,我的看单和设置功能,由于时间关系,小编只实现设置的部分功能

                 
       1.wxml部分

<!--miniprogram/pages/mine/mine.wxml--><view class="container">    <view class="header-image">      <image bindtap="bindViewTap" class="userinfo-avatar" src="{{userInfo.avatarUrl}}" mode="cover"></image>      <text class="userinfo-nickname">{{userInfo.nickName}}</text>    </view>    <view class="vip">        <image src="../../images/no_vip.png" mode="widthFix"></image>        <text class="h2">VIP暂未开通</text>        <text class="bottom" bindtap="lookBans">请点此去开通</text>    </view>    <view class="history" bindtap="lookBans">      <image class="icon" src="../../images/history.png" mode="widthFix"></image>      <text>观看历史</text><text class="fr"></text>    </view>    <view class="play-list history" bindtap="lookBans">      <image class="icon" src="../../images/view.png" mode="widthFix"></image>      <text>我的看单</text><text class="fr"></text>    </view>    <view class="history" catchtap='navigatItem' data-url='/pages/setting/setting' data-open='true'>      <image class="icon" src="../../images/settings.png" mode="widthFix"></image>      <text>设置</text><text class="fr"></text>    </view></view>

      2. js 部分

// miniprogram/pages/mine/mine.jsconst utils = require('../../utils/utils.js')//获取应用实例const app = getApp()Page({  /**   * 页面的初始数据   */  data: {    userInfo: {}  },  navigatItem(e) {    return utils.navigatItem(e)  },  getUserInfo: function(e) {    app.globalData.userInfo = e.detail.userInfo    this.setData({      userInfo: e.detail.userInfo    })  },    lookBans: function () {    const that = this;    wx.showModal({      content: '暂时未开发!',      showCancel: false,      confirmColor: '#FF4500',      success(res) {      }    })  },  /**   * 生命周期函数--监听页面加载   */  onLoad: function (options) {    if (app.globalData.userInfo) {      this.setData({        userInfo: app.globalData.userInfo      })    }else {      // 在没有 open-type=getUserInfo 版本的兼容处理      wx.getUserInfo({        success: res => {          app.globalData.userInfo = res.userInfo;          console.log(res.userInfo)          this.setData({            userInfo: res.userInfo          })        }      })    }      },  /**   * 生命周期函数--监听页面初次渲染完成   */  onReady: function () {  },  /**   * 生命周期函数--监听页面显示   */  onShow: function () {  },  /**   * 生命周期函数--监听页面隐藏   */  onHide: function () {  },  /**   * 生命周期函数--监听页面卸载   */  onUnload: function () {  },  /**   * 页面相关事件处理函数--监听用户下拉动作   */  onPullDownRefresh: function () {  },  /**   * 页面上拉触底事件的处理函数   */  onReachBottom: function () {  },  /**   * 用户点击右上角分享   */  onShareAppMessage: function () {  }})

    3. 你需要注意的地方

   在实现 设置功能部分时,这个小编在utils中写一个共有的 工具函数,用于页面跳转等操作。utils.js源码如下:

let navigatItem = (e) => {  const url = e.currentTarget.dataset.url || '/pages/main/main'  const open = e.currentTarget.dataset.open  const toUrl = () => {    wx.navigateTo({      url,    })  }  if (open) {    toUrl()  } else {    if (ifLogined()) {      toUrl()    } else {      wx.navigateTo({        url: '/pages/mine/mine'      })    }  }}module.exports = {  navigatItem}

项目完整源码:

github.com/hongdeyuan/…

9. 结语

      小编在写该项目时,踩了不少的坑,这里只写出了几个。虽然有些地方用框架的话会更方便,但是我觉得徒手写项目自己的能力才会得到进阶;最后,感谢大家来学习该文章,感谢你们的支持,欢迎各位来学习讨论。
      如果你喜欢这篇文章或者可以帮到你,不妨点个赞吧!