Mini Program(二)组件二

1、scroll-view

scroll-view 组件是为了特定的滚动场景设计的。与 movable-view、movable-area、cover-view 等组件一样都是为了开发者实现特定场景下的业务功能而设计开发的。

scroll-view 的滚动属性主要实现了两套功能:左右或上下滚动、下拉更新。

滚动锚定

与滚动有关的属性有以下几个:

  1. scroll-x:横向滚动,默认为 false。需要设置一个宽度,子元素的高度合大于设定宽度。如果启用了 scroll-x 却出现不滚动的现象,可以尝试给滚动容器添加两个样式:white-space: nowrap; display: inline-block;
  2. scroll-y:纵向滚动,默认为 false。需要设置一个高度,子元素的高度合大于设定高度。
  3. scroll-top:指内部的滚动实体的上边高于容器盒子顶部边缘多少距离,默认为 0。向上滚动,值增加。
  4. scroll-left:滚动实体的左边距离父容器盒子的左边界的距离。
  5. scroll-into-view:用于滚动到某个元素,值必须是一个 scroll-view 子组件的 id。当滚动的时候,小程序以子组件的上、左边界为测量依据。
  6. scroll-anchoring:控制滚动位置不随内容变化而抖动,默认为 false。目前小程序只支持 iOS 手机,Android 手机上需要开发者自己处理,可以添加 overflow-anchor: auto。
  7. upper-threshold:用于控制 scrolltoupper 事件何时派发,默认为 50px。纵向滚动时,当 scroll-top 小于 upper-threshold 时,scroll-view 组件派发 scrolltoupper 事件。横向滚动是以 scroll-left 作为比对值。
  8. lower-threshold:用于控制 scrolltolower 事件何时派发,默认为 50px。纵向滚动时,当 scroll-top 小于 lower-threshold 时,scroll-view 组件派发 scrolltolower 事件。横向滚动是以 scroll-left 作为比对值。

如果同时开启横向、纵向两个方向的滚动,当通过 scroll-into-view 滚动时,滚动行为变化有两种:如果不加 scroll-with-animation 属性,也就是不开启动画,可以同时在 x、y 两个方向上瞬时移动到目标位置;如果开启动画,同一时间只能在一个方向上滚动,有时在 x 轴滚动,有时在 y 轴滚动。所以 scroll-x 和 scroll-y 最好不要同时开启。示例代码如下:

index.wxml

<view class="page-section">
	<view class="page-section-spacing">
		<scroll-view enable-flex scroll-into-view="{{scrollIntoViewId}}" bindscroll="onScroll" scroll-y scroll-x scroll-with-animation="{{false}}" style="width: 100%;height:300rpx;">
			<view id="childview{{item}}0" wx:for="{{[1, 2, 3, 4, 5, 6, 7, 8, 9,10]}}" class="scroll-row">{{item}}
				<view wx:for="{{[0, 1, 2, 3, 4, 5,6,7,8,9]}}" class="scroll-item" id="childview{{item}}{{item2}}" wx:for-item="item2">{{item}}:{{item2}}</view>
			</view>
		</scroll-view>
	</view>
	<view class="btn-area">
		<button type="primary" bindtap="scrollToView1">滚动到子组件2</button>
	</view>
</view>

index.js

let viewId = 5
Page({
    scrollToView1(){
        viewId += 2
        this.setData({
            scrollIntoViewId:'childview'+viewId
        })
        console.log(this.data.scrollIntoViewId)
    },onScroll(e){
        console.log(e.detail.scrollTop, e.detail.scrollLeft, e.detail.scrollHeight,e.detail.scrollWidth)
    },
})

下拉更新

下拉更新的属性主要有以下几个:

  1. refresher-enabled:用于控制是否开启自定义下拉刷新,默认为 false。
  2. refresher-threshold:触发下拉更新的临界值,默认为 45。向下拉,松手又回去了,列表没有更新,这是没有达到阈值。
  3. refresher-triggered:在更新后取消下拉更新的状态。默认为 false。当组件处于下拉更新的状态后,值变为 true,此时程序要去做一些异步耗时的事件,待处理完成后,值变为 false,下拉更新的状态恢复。
  4. bindrefresherpulling:手指按住往下拉的过程中派发,自定义的动画效果要在这个事件里面处理。
  5. bindrefresherrefresh:状态恢复后,设置 refresher-triggered 为 false,动画完成后派发的事件。
  6. bindrefresherrestore:自定义下拉刷新被复位。
  7. bindrefresherabort:下拉行为被打断时派发的事件。

最佳实践为以下步骤:

  1. 启用 scroll-anchoring 属性时,同时添加一个 overflow-anchor:auto 的样式,用于解决 Android 机型不兼容的问题。
  2. scroll-y 和 scroll-x 尽可能只设置一个;如果启用了 scroll-x 却出现不滚动的现象,可以尝试给滚动容器添加两个样式 white-space: nowrap; display: inline-block;
  3. 使用 enable-flex 开启 scroll-view 的 flex 布局。相当于添加了一个 display: flex; 样式,如果是我们自己添加,是加在了外围的容器上,只有通过这个属性才能加到内部真正的容器上。
  4. 使用 refresher-enabled 启用下拉动画的自定义;下拉容器的 slot 属性要标记为 refresher。
  5. 下拉动画组件的背景色用 #f8f8f8,前景色(包括图标与文本)用 #888888。
  6. 尽量不要在 JavaScript 里的 scroll 事件的句柄函数中直接更新视图,执行频繁更新视图的代码,可以把这种代码放在 WXS 模块中,在大列表视图中尤其要这么做。

渲染长列表

微信在 WeUI 扩展组件库中给出了一个长列表组件:recycle-view,用于渲染无限长的列表。

通过监听 scroll 事件,只渲染当前视图窗口内的 list 列表,看不见的地方用空白的占位符代替。

在使用 recycle-view 扩展组件的时候,batch 属性的值必须为 batchSetRecycleData。

在 JavaScript 代码中,调用 createRecycleContext 时传入的 dataKey 是 recycleList。这个名称必须与 WXML 中的 wx:for 指定的数据名称一致。如果一个页面中还使用了另外一个长列表,则需要再换一个名字。

使用方法如下

1、安装组件。安装完后在菜单栏的工具里,点击『构建 npm』。

npm install --save miniprogram-recycle-view

2、在页面的 json 配置文件中添加 recycle-view 和 recycle-item 自定义组件的配置。

{
  "usingComponents": {
    "recycle-view": "miniprogram-recycle-view/recycle-view",
    "recycle-item": "miniprogram-recycle-view/recycle-item"
  }
}

3、index.wxml

<view class="page-section">
    <recycle-view height="200" batch="{{batchSetRecycleData}}" id="recycleId" batch-key="batchSetRecycleData" style="background:white;">
        <recycle-item wx:for="{{recycleList}}" wx:key="index" class='item'>
            <view>
                {{item.id}}: {{item.name}}
            </view>
        </recycle-item>
    </recycle-view>
</view>

4、index.js

const createRecycleContext = require('miniprogram-recycle-view')
function rpx2px(rpx) {
    return (rpx / 750) * wx.getSystemInfoSync().windowWidth
}
Page({
    onReady: function () {
        var ctx = createRecycleContext({
            id: 'recycleId',
            dataKey: 'recycleList',
            page: this,
            itemSize: {
                width: rpx2px(650),
                height: rpx2px(100)
            }
        })
        let newList = []
        for (let i = 0; i < 20; i++) {
            newList.push({
                id: i,
                name: `标题${i + 1}`
            })
        }
        ctx.append(newList)

        const arr = []
        for (let i = 0; i < 20; i++) arr.push(i)
        this.setData({
            arr
        })

        setTimeout(() => {
            this.setData({
                triggered: true,
            })
        }, 1000)
        // 
        let activeTab = 0, page=1, res = {something:''}
        let tabsData = this.data.tabs[activeTab] || {list:[]}
        tabsData.page = page+1
        tabsData.list.push(res)
        let key = `tabs[${activeTab}]`
        this.setData({
            [key]: tabsData
        })
        console.log(this.data.tabs)
    },
})

实现购物类小程序分类选择物品的页面

主要实现两个功能:1、单击左侧菜单,右侧区域自动滚动到相应的位置;2、在右侧滚动的时候,左侧菜单自动同步选择并高亮显示。

第一个功能点通过 scroll-into-view 属性去实现。

<!-- 左侧菜单 -->
<scroll-view scroll-y class="nav">
    <view wx:for='{{list}}' wx:key='{{item.id}}' id='{{item.id}}' class='navList{{currentIndex==index?"active":""}}'
          bindtap="menuListOnClick" data-index='{{index}}'>{{item.name}}</view>

</scroll-view>
<!-- 右侧菜单 -->
<scroll-view scroll-y scroll-into-view='{{activeViewId}}' bindscroll='scrollFunc'>
    <view class="fishList" wx:for='{{content}}' id='{{item.id}}' wx:key='{{item.id}}'>
        <p>{{item.name}}</p>
    </view>
</scroll-view>

scroll-into-view 绑定了一个 activeViewId 变量,需要与左侧菜单中的 id 对应起来。在点击左侧每一个具体菜单的时候,bindtap 绑定的 JavaScript 函数中需要将 activeViewId 指定为当前点击的这个 item.id。

// 点击左侧菜单
menuListOnClick:function(e){
    let me= this;
    me.setData({
        activeViewId: e.target.id,
        currentIndex: e.target.dataset.index
    })
}

e 是事件对象,取到 target.id,赋值给 activeViewId,设置完后功能就实现了。


第二个功能点是在右侧滚动的时候,左侧菜单同时自动去选择并带高亮。

在右侧滚动的时候,将 bind 的 scroll 事件绑定到 scrollFunc 的 JavaScript 函数上面。

// 滚动时触发,计算当前滚动到的位置对应的菜单是哪个
scrollFunc:function(e){
    this.setData({
        scrollTop: e.detail.scrollTop
    })
    for(let i= 0; i< this.data.heightList.height; i++){
        let height1= this.data.heightList[i];
        let height2= this.data.heightList[i+ 1];
        if(!height2|| (e.detail.scrollTop>= height1 && e.detail.scrollTop < height2)){
            this.setData({
                currentIndex: 1
            })
            return;
        }
    }
    this.setData({
        currentIndex: 0
    })
}

vtabs 侧边栏分类的商品浏览组件

WeUI 组件库中有一个 vtabs 组件,是一个有侧边栏分类的商品浏览组件。

vtabs 是一个选项卡组件,在使用这个组件的时候,要把它和 vtabs-content 结合起来进行实现。其中 vtabs-content 是 vtabs 的一个子组件。

1、安装组件。

npm i @miniprogram-component-plus/vtabs --save

npm i @miniprogram-component-plus/vtabs-content --save

安装完后在菜单栏的工具里,构建 npm。

2、在页面的 json 配置文件中添加 recycle-view 和 recycle-item 自定义组件的配置。

{
  "usingComponents": {
    "mp-vtabs": "@miniprogram-component-plus/vtabs/index",
    "mp-vtabs-content": "@miniprogram-component-plus/vtabs/index"
  }
}

3、index.wxml

<mp-vtabs 
          vtabs="{{vtabs}}" 
          activeTab="{{activeTab}}" 
          bindtabclick="onTabCLick"
          bindchange="onChange"
          class="test"
          >
    <block wx:for="{{vtabs}}" wx:key="title" >
        <mp-vtabs-content tabIndex="{{index}}">
            <view class="vtabs-content-item">我是第{{index + 1}}项: {{item.title}}</view>
        </mp-vtabs-content>
    </block>
</mp-vtabs>

4、index.js

Page({
    data: {
        vtabs: [],
        activeTab: 0,
    },

    onLoad() {
        const titles = ['热搜推荐', '手机数码', '家用电器',
                        '生鲜果蔬', '酒水饮料', '生活美食', 
                        '美妆护肤', '个护清洁', '女装内衣', 
                        '男装内衣', '鞋靴箱包', '运动户外', 
                        '生活充值', '母婴童装', '玩具乐器', 
                        '家居建材', '计生情趣', '医药保健', 
                        '时尚钟表', '珠宝饰品', '礼品鲜花', 
                        '图书音像', '房产', '电脑办公']
        const vtabs = titles.map(item => ({title: item}))
        this.setData({vtabs})
    },

    onTabCLick(e) {
        const index = e.detail.index
        console.log('tabClick', index)
    },

    onChange(e) {
        const index = e.detail.index
        console.log('change', index)
    }

})

5、index.wxss

.vtabs-content-item {
    width: 100%;
    height: 300px;
    box-sizing: border-box;
    border-bottom: 1px solid #ccc;
    padding-bottom: 20px;
}

2、基于 picker-view 实现省市区三级联动的多项选择器

picker 本身有一个模式是 region,是省市区三级联动的。这个默认组件在某些特定场景下,不能满足我们的样式需求。另外不一定是省市区,还有像基于其它数据源的选择器,也可以自定义实现,样式也可以自如控制。

非自定义选择器

index.wxml

<view class="section">
    <view class="section__title">省市区选择器</view>
    <picker mode="region" bindchange="bindRegionChange" value="{{region}}" custom-item="{{customItem}}">
        <view class="picker">
            当前选择:{{region[0]}},{{region[1]}},{{region[2]}}
        </view>
    </picker>
</view>

自定义选择器

pick-view 基于子组件 picker-view-column 这种松耦合的架构实现的,本身没有数据源。它的所有数据都是在 picker-view-column 这个子组件里面,由开发者通过 wx:for 循环去绑定。

picker 是底部滑出的,picker-view 是页面嵌入的,为了实现底部滑出效果,可以把 picker-view 放在滑出的面板上。至于嵌入组件的蒙层的样式效果,可以通过 mask-style 或者 mask-class 控制,这两个属性是专门用于控制蒙层效果的。

region-picker-view 目前有两个问题。

一是最好不要在它的 change 事件里面去改变视图,因为在滑动的时候可能会涉及到连续的多次的滑动,在用户还没有选到目标值之前,可能会涉及到多次的 change 事件派发。最好是在用户选择结束之后,例如在 touchend 事件中,再去判断有没有变化。如果有变化,再去改变数据源。

二是通过测试发现关于 picker 组件当 mode 为 multiSelector 时,当手指滑动时,它的 columnchange 事件会有多次派发。picker 组件的 change 事件是在单击『确定』按钮之后才派发的,而对于 picker-view 组件,在滑动选择的过程中,change 事件是不派发的,change 事件只在选定之后派发了一次。在 picker-view 组件中,还有 pickerstart 和 pickend 事件,分别代表滚动选择的开始和结束,这就没必要从 touchend 事件中自己做状态的判断了。可以从 change 事件中先拿到 value,然后在 pickend 事件里,在选择结束的时候再去作这个逻辑的处理。

从以上两点看,picker-view 组件的涉及是优于 picker 组件的,picker 组件的功能,使用 picker-view 也是可以完全实现的;关于交互的操作,最好是写在 WXS 模块里面,而不是在 JavaScript 里面。减少视图层与逻辑层之间的通讯,可以显著提高界面的流畅性。

region-picker-view2 就是使用 WXS 脚本,将 region-picker-view 自定义组件改写。

index.wxml

<!-- 自定义选择器 -->
<view class="page__section">
    <view class="page__section-title">两个自定义实现选择器</view>
    <!-- js滚动选择器 -->
    <region-picker-view bindchange="onRegionChange"></region-picker-view>
    <!-- wxs滚动选择器 -->
    <region-picker-view2 bindchange="onRegionChange"></region-picker-view2>
</view>

index.json

{
    "usingComponents": {
        "region-picker-view": "/components/region-picker-view/index",
        "region-picker-view2": "/components/region-picker-view2/index"
    }
}

3、自定义竖向 slider 组件

一个 ComponentDescriptor 组件描述对象有以下几个方法:

  1. selectComponet:返回组件的 ComponentDescriptor 实例,与当前查看的对象类型一样。用于查找 WXML 页面中的组件,参数与 JQuery 中组件查询类似。可以是以 # 开头的组件 id,也可以是以 . 开头的样式类名称。
  2. selectAllComponents:返回组件的 ComponentDescriptor 对象数组,与第一个方法类似,不同地方在于返回的是一个数组。
  3. setStyle:设置内联样式,优先级比组件 WXML 里面定义的样式要高,但不能用它设置最高层页面的样式。
  4. addClass / removeClass / hasClass:设置组件的 class 样式,作用于 setStyle 类似,但这里使用的是类名称,并且设置的class 优先级比组件的 WXML 里面定义的 class 要高,同样不能设置最高层页面的 class 样式。
  5. getDataset:返回当前组件对象或页面对象的 dataset 对象。可以在组件上以 dataset-x 这样的形式定义组件的扩展样式,绑定逻辑层 JavaScript 中的数据,并将它们以这种方式传递到 WXS 脚本中。
  6. callMethod(funcName:string, args:object):调用当前组件对象或页面对象在逻辑层,也就是 AppService 里面定义的函数。其中 funcName 表示的是函数名称,args 表示函数的参数,是一个对象。
  7. requestAnimationFrame:用于实现动画,相当于一个与页面渲染同频的定时器。每渲染一帧,执行一次。
  8. getState:返回一个 object 对象。当有一个数据变量要存储起来,在各个 WXS 方法之间共享使用的时候,用这个方法比较方便。
  9. triggerEvent(eventName, detail):派发事件,和 JavaScript 中组件的 triggerEvent 一致。但由于 callMethod 只能一次传递一个参数,并不能通过 callMethod 调用这个方法,这是一个替代方法。

非自定义横向

index.wxml

<view class="section section_gap">
    <text class="section__title">设置最小 / 最大值,步进为 5</text>
    <view class="body-view">
        <slider bindchange="slider4change" bindchanging="onSliderChanging" min="50" max="200" show-value step="5" />
    </view>
</view>

index.js

var pageData = {}
for (var i = 1; i < 5; ++i) {
    (function (index) {
        pageData[`slider${index}change`] = function (e) {
            console.log(`slider${index}发生change事件,携带值为`, e.detail.value)
        }
    })(i);
}
Page(Object.assign({
    data:{},
    onSliderChanging(e){
        console.log(e.type, e.detail.value);

    },
}, pageData))

自定义纵向

自定义纵向 slider 采用 WXS 脚本编写。

竖向 slider 是以底部为起点的,滑动到底部为 min 值,滑动到顶部为 max 值,整个 slider 分成上、中、下三部分:灰色的竖条、白色的滑块、绿色的竖条。中间滑块 slider-middle 是不占用大小的,宽高都是 0,它的子组件 slider-block 是有大小的,并且它的 postion 样式是 relative。它是以相对定位的方式挂在中间位置的,也就是在 slider-middle 里。

index.wxml

<view class="section section_gap">
    <text class="section__title">自定义竖向slider</text>
    <view class="body-view">
        <view style="height: 400rpx;margin: 20px;display: flex;justify-content: space-around">

            <slider-vertical block-color="#ffffff" block-size="28" backgroundColor="#e9e9e9" activeColor="#1aad19" bindchange="slider1change" bindchanging="slider1changing" step="1" min="50" max="200" value="0" disabled="{{false}}" show-value="{{true}}"></slider-vertical>

            <slider-vertical block-color="#ffffff" block-size="28" backgroundColor="#e9e9e9" activeColor="#1aad19" bindchange="slider1change" bindchanging="slider1changing" step="5" min="50" max="200" value="115" disabled="{{false}}" show-value="{{false}}"></slider-vertical>
        </view>
    </view>
</view>

index.js

var pageData = {}
for (var i = 1; i < 5; ++i) {
    (function (index) {
        pageData[`slider${index}change`] = function (e) {
            console.log(`slider${index}发生change事件,携带值为`, e.detail.value)
        }
    })(i);
}
Page(Object.assign({
    data:{},
    slider1change: function (e) {
        console.log("change:",e)
    },
    slider1changing: function (e) {
        console.log("changing:",e)
    }
}, pageData))

index.json

{
    "usingComponents": {
        "slider-vertical": "/components/vertical-slider/index"
    },
    "navigationStyle": "custom",
    "navigationBarTitleText": "自定义导航标题"
}

4、自定义透明导航栏

小程序的导航组件有两个:functional-page-navigator 和 navigator。

前者是在插件中使用的,仅能跳转到插件的功能页。后者是小程序标准的导航组件,可以通过设置不同的跳转方式,实现不同的跳转功能。

open-type 一共有 6 个合法值:

  • navigate:代表页面跳转或者是小程序跳转。页面跳转时需要设置 url 属性;小程序跳转时徐要设置 target 属性为 miniProgram,同时提供一个 app-id,也就是要跳转的目标小程序的 app-id。如果跳转别的小程序的时候,还需要带上一些参数,要在 extra-data 属性中设置。
  • redirect:相当于接口 wx.redirectTo 的功能,使用这个类型的时候需要同时设置 url 属性。
  • switchTab:相当于接口 wx.switchTab 的功能,需要同时设置 url 属性。
  • reLaunch:相当于接口 wx.reLaunch 重启小程序的功能。
  • navigateBack:相当于接口 wx.navigateBack 的功能。使用的时候同时可以设置 delta 属性,表示回退的层数。
  • exit:退出小程序。

系统导航栏透明

在小程序页面的 json 配置文件中,设置 navigationStyle 为 custom,开启页面导航栏的自定义。在开启以后,系统状态栏也会透明。

index.wxml

<navigation-bar 
                ext-class="page-navigator-bar"
                active="{{active}}"
                loading="{{loading}}">
    <view class="left" slot="left">
        <icon bindtap="goBack" class="iconfont icon-back"></icon>
        <icon bindtap="goHome" class="iconfont icon-home"></icon>
    </view>
    <view slot="center">
        <view>自定义导航标题</view>
    </view>
</navigation-bar>
<view style="width:100%;height:400px;"></view>
<image class="top-banner" src="https://qiniu-image.qtshe.com/1557133211411_684.jpg" mode="widthFix" />
<view class="operate-wraper" style="background-color:#f2f2f2;--topBarHeight:{{topBarHeight}}px;">
</view>

index.js

Page({
    data: {
        loading: false,
        active: true
    },
    //点击back事件处理
    goBack: function () {
        wx.navigateBack();
        this.triggerEvent('back');
    },
    //返回首页
    goHome:function(){
        wx.reLaunch({
            url: '/pages/index/index'
        })
    },
    onPageScroll(res) {
        console.log(res);

        if (res.scrollTop > 400) {
            if (!this.data.active) {
                this.setData({
                    active: true
                })
            }
        } else {
            if (this.data.active) {
                this.setData({
                    active: false
                })
            }
        }
    }
})

index.wxss

@font-face {
    font-family: 'iconfont';  /* Project id 2503355 */
    src: url('//at.alicdn.com/t/font_2503355_3o3ks3b2xfn.woff2?t=1620132700001') format('woff2'),
        url('//at.alicdn.com/t/font_2503355_3o3ks3b2xfn.woff?t=1620132700001') format('woff'),
        url('//at.alicdn.com/t/font_2503355_3o3ks3b2xfn.ttf?t=1620132700001') format('truetype');
}
.iconfont{
    font-family: 'iconfont';
}
.icon-back::after{
    content: '\e67c';
    font-size: 22px;
}
.icon-home::after{
    content: '\e64e';
    font-size: 22px;
}
.left icon:last-child{
    padding-left: 20rpx;
}

.page-navigator-bar .navigator-normal .icon-back{
    color: white;
}
.page-navigator-bar .navigator-normal .icon-home{
    color: white;
}
.page-navigator-bar .navigator-active .icon-back{
    color: black;
}
.page-navigator-bar .navigator-active .icon-home{
    color: black;
}

index.json

{
  "usingComponents": {
    "navigation-bar":"/components/navigation-bar/index"
  },
  "navigationStyle": "custom"
}

返回按钮和主页按钮是在外面定义的,它们的切换是通过 ext-class 扩展样式属性实现的。设置这个属性,是为了在外面可以控制内部组件的样式。只要自定义组件使用插槽,基本上这个机制就是需要的。


5、实现图片懒加载

image 组件是最常使用的媒体组件之一,这个组件本身有一个 lazy-load 属性,该属性已经实现了图片的懒加载功能。下面是一些 image 组件的技术问题:

Webp 介绍:是 image 组件的布尔属性,开启这个属性,代表 url 可以设置 Webp 这种格式的图片。Webp 是一种同时提供了有损压缩与无损压缩,并且是可逆的图片压缩的这种文件格式。image 组件默认不解析这种图片格式。

Webp 优势:它具有更优的图像数据压缩算法,能带来更小的图片体积,并且拥有肉眼识别无差异的图像质量。同时提供了无损及有损的两种图片压缩模式,还提供 alpha 透明,以及动画的特性,对 JPEG 和 PNG 等这些图片格式的转化都有支持。Webp 既可以替代 JPEG、PNG 这些静态的图片,也可以替代 GIF 这种动态的图片。


show-menu-by-longpress 属性:是图片组件的一个布尔属性,开启这个属性,就是开启了长按图片,显示识别小程序码的一个菜单。这种识别仅是在 wx.previewImage 接口调用,开始预览图片的时候,才可以识别。


mina-lazy-image 主要实现原理是使用 wx.createIntersectionObserver 接口,创建 IntersectionObserver 实例,用这个实例去判断图片是否出现在用户的视图窗口中。如果出现了,再进行加载。

IntersectionObserver 主要用于推断某些组件节点是否可以被用户看见、有多大比例可以被用户看见。一共有下面四个方法:

  1. relativeTo(string selector, Object margins):该方法使用选择器,指定一个组件节点作为参照区域。可以是 id 选择器,也可以是类选择器。
  2. relativeToViewport(Object margins):该方法指定页面的视图显示区域作为交叉判断的参照区域。与第一个方法的区别在于它指定参考的对象是不一样的。
  3. observe(string targetSelector, callback):该方法用选择器指定目标节点,并且开始监听交叉状态的一个变化情况,变化情况会在 callback 回调函数中返回。
  4. disconnect():代表监听完成,要停止监听,回调函数不再触发。

npm install --save mina-lazy-image


index.wxml

<view class="page-head">
    <text class="page-head__title">slider</text>
    <text class="page-head__desc">滑块</text>
</view>

<view class="page-section">
    <text class="page-section__title">use image</text>
    <scroll-view class="cardbox">
        <button wx:if="{{item.live.play_urls}}" class="card" hover-class='none' wx:for="{{content}}" wx:key="*this" bindtap="gotoLive" data-url="{{item.live.play_urls.hdl.ORIGIN}}" data-ava="{{item.live.user_info.avatar}}" data-name="{{item.live.user_info.name}}" data-audience="{{item.live.audience_num}}" data-lid="{{item.live.id}}" data-cacheprepic="{{item.live.pic}}" data-prepic="{{item.live.pic_320}}" data-share_desc="{{item.live.share_info.wechat_contact.cn.text}}" style="position: relative;">
            <view class="image_card">
                <image class="showpic" mode="aspectFill" src="{{item.live.pic_320}}" lazy-load="{{true}}" />
                <view class="cover" />
                <text class="audience">{{item.live.audience_num}}观众</text>
            </view>
            <view class="user_card" catchtap="gotoHome" data-uid="{{item.live.user_info.id}}">
                <view class="avabox">
                    <image src="{{item.live.user_info.avatar}}" lazy-load="{{true}}" class="ava" data-uid="{{item.live.user_info.id}}" />
                    <image class="vip" wx:if="{{item.live.vip}}" lazy-load="{{true}}" src="http://img08.oneniceapp.com/upload/resource/9e7ca7ece11143b49fc952cfb2520e43.png" />
                </view>
                <text class="user_name">{{item.live.user_info.name}}</text>
            </view>
        </button>

        <button wx:if="{{item.live.playback_urls}}" class="card" open-type='getUserInfo' bindtap="gotoPlayback" wx:for="{{content}}" data-url="{{item.live.playback_urls.hls.ORIGIN}}" wx:key="*this" >
            <view class="image_card">
                <image className="showpic" mode="aspectFill" src="{{item.live.pic_320}}" lazy-load="{{true}}" />
                <view class="cover" />
                <text class="audience">{{item.live.audience_num}}观众</text>
                <image class="back" lazy-load="{{true}}" src="http://img08.oneniceapp.com/upload/resource/002bdceaa732f300e33ab8b2cb84dd17.png" />
            </view>
            <view class="user_card">
                <view class="avabox">
                    <image src="{{item.live.user_info.avatar}}" class="ava" lazy-load="{{true}}" />
                    <image class="vip" wx:if="{{item.live.vip}}" lazy-load="{{true}}" src="http://img08.oneniceapp.com/upload/resource/9e7ca7ece11143b49fc952cfb2520e43.png" />
                </view>
                <text class="user_name">{{item.live.user_info.name}}</text>
            </view>
        </button>
    </scroll-view>
</view>

<view class="page-section">
    <text class="page-section__title">use mina-lazy-image</text>
    <scroll-view class="cardbox">
        <button wx:if="{{item.live.play_urls}}" class="card" hover-class='none' wx:for="{{content}}" wx:key="*this" bindtap="gotoLive" data-url="{{item.live.play_urls.hdl.ORIGIN}}" data-ava="{{item.live.user_info.avatar}}" data-name="{{item.live.user_info.name}}" data-audience="{{item.live.audience_num}}" data-lid="{{item.live.id}}" data-cacheprepic="{{item.live.pic}}" data-prepic="{{item.live.pic_320}}" data-share_desc="{{item.live.share_info.wechat_contact.cn.text}}" style="position: relative;">
            <view class="image_card">
                <mina-lazy-image mode="aspectFill" src="{{item.live.pic_320}}" />
                <view class="cover" />
                <text class="audience">{{item.live.audience_num}}观众</text>
            </view>
            <view class="user_card" catchtap="gotoHome" data-uid="{{item.live.user_info.id}}">
                <view class="avabox">
                    <mina-lazy-image src="{{item.live.user_info.avatar}}"  data-uid="{{item.live.user_info.id}}" />
                    <image class="vip" wx:if="{{item.live.vip}}" lazy-load="{{true}}" src="http://img08.oneniceapp.com/upload/resource/9e7ca7ece11143b49fc952cfb2520e43.png" />
                </view>
                <text class="user_name">{{item.live.user_info.name}}</text>
            </view>
        </button>

        <button
                wx:if="{{item.live.playback_urls}}"
                class="card"
                open-type='getUserInfo'
                bindtap="gotoPlayback"
                wx:for="{{content}}"
                data-url="{{item.live.playback_urls.hls.ORIGIN}}"
                wx:key="*this"
                >
            <view class="image_card">
                <mina-lazy-image mode="aspectFill" src="{{item.live.pic_320}}" />
                <view class="cover" />
                <text class="audience">{{item.live.audience_num}}观众</text>
                <image class="back" lazy-load="{{true}}" src="http://img08.oneniceapp.com/upload/resource/002bdceaa732f300e33ab8b2cb84dd17.png" />
            </view>
            <view class="user_card">
                <view class="avabox">
                    <mina-lazy-image src="{{item.live.user_info.avatar}}"  />
                    <image class="vip" wx:if="{{item.live.vip}}" lazy-load="{{true}}" src="http://img08.oneniceapp.com/upload/resource/9e7ca7ece11143b49fc952cfb2520e43.png" />
                </view>
                <text class="user_name">{{item.live.user_info.name}}</text>
            </view>
        </button>
    </scroll-view>
</view>

<view class="page-section">
    <text class="page-section__title">设置step</text>
    <image bindtap="previewImage" data-url="http://t.cn/A622upBw" show-menu-by-longpress src="http://t.cn/A622upBw" mode="widthFit"></image>
</view>

index.js

const app = getApp()
Page({
    /**
   * 页面的初始数据
   */
    data: {

    },

    onLoad: function () {
        wx.request({
            url: 'https://wxapi.kkgoo.cn/live/discover?type=hot',
            method:'POST',
            success:(res) => {
                this.setDis(res);
            }
        })
    },
    setDis(r) {
        let newData = r.data.data;
        this.data.nextKey = newData.nextkey ? newData.nextkey : this.data.nextKey;
        this.setData({
            content: newData.discover ? newData.discover : this.data.content,
            banneritem: newData.cards ? newData.cards.slice(0, newData.cards.length - 1) : this.data.banneritem
        })
    },
    previewImage(e){
        console.log(e);
        let url = e.currentTarget.dataset.url
        wx.previewImage({
            current:url,
            urls: [url],
        })
    }
})

index.wxss

/* miniprogram/pages/2.14/index.wxss */
.lazy-image{

}


/* 用户列表相关样式 */
.main{
    font-size:0;
    width:100%;
    height: 100%;
    font-family: 'PingFangSC-Semibold';
}
.title{
    text-align:center;
    font-size: 0;
}
.u_title{
    display: inline-block;
    width:100%;
    font-size: 24rpx;
    line-height: 24rpx;
    margin:20rpx 0;
    font-weight: bold;
}
.d_title{
    display: inline-block;
    width:100%;
    line-height: 22rpx;
    font-size: 22rpx;
}
.cardbox{
    width: 100%;
    font-size: 0;
    box-sizing: border-box;
    padding: 0 32rpx;
    /*margin-top:60rpx;*/
    display: inline-block;
}
button::after{
    border: none
}
button{
    width: auto !important;
    padding-left: 0 !important;
    padding-right: 0 !important;
    background-color: #fff;
}
.card{
    display: inline-block;
    float:left;
    /* margin-top:60rpx; */
}
.card .image_card{
    width: 268rpx;
    height: 268rpx;
    border-radius: 8rpx;
    position: relative;
}
.cover{
    position: absolute;
    /* width: 327rpx;
    height: 327rpx; */
    top: 0;
    left: 0;
    background-color: rgba(0,0,0,0.3);
    z-index: 99;
    border-radius: 8rpx;
}
.card .image_card .audience{
    font-size: 22rpx;
    color:#fff;
    position: absolute;
    left:16rpx;
    top:16rpx;
    z-index:999; 
    font-weight: bold;
}
.card .image_card .back{
    position: absolute;
    right:16rpx;
    top:16rpx;
    width: 56rpx;
    height: 32rpx;
    z-index: 999;
}
.card .user_card{
    margin-top: 20rpx;
    margin-bottom: 20rpx;
    float:left;
    margin-right: 15rpx;
}
.card .user_card .avabox{
    width:48rpx;
    height: 48rpx;
    margin-right: 15rpx;
    position: relative;
    display: inline-block;
    vertical-align: middle;
}
.card .user_card .avabox .ava{
    width: 100%;
    height: 100%;
    border-radius: 8rpx;
    vertical-align: top
}
.card .user_card .avabox .vip{
    position: absolute;
    width: 32rpx;
    height: 32rpx;
    bottom:-5rpx;
    right:-5rpx;
    border-radius: 50%;
    background: red;
}
.card .user_name {
    width: auto;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    display: inline-block;
    font-size: 24rpx;
    text-align: start;
    display: inline-block;
    vertical-align: middle;
    font-weight: bold;
}
.card:nth-child(odd){
    margin-right:32rpx;
}
.showpic{
    width: 100%;
    height: 100%;
    border-radius: 8rpx;
    overflow: hidden;
}
.scroll-end{
    float: left;
    height: 50rpx;
    width: 100%;
    color: #999;
    line-height: 50rpx;
    font-size: 28rpx;
    text-align: center;
}

index.json

{
    "usingComponents": {
        "mina-lazy-image": "mina-lazy-image/index"
    }
}

问题一:图片链接正常访问,image 组件加载不出图片

image 组件拉取图片的本质是使用 wx.downloadFile 接口加载图片的资源,当加载以后,把加载的图像再绘制出来。很多时候由于图片的格式不规范,例如线上的 SSL 证书有问题,或者是文件的描述信息,例如 content-type、length 等信息不标准不完整,还有可能是由于这个服务器发生了 302 跳转等等原因导致图片拉取不成功,看到的现象就是这个图片没有显示出来。有时候网络不好,加载超时,图片也不会显示。

对于网络不好的情况,可以用 image 组件的 binderror 事件属性处理,监听 error 事件。当监听到错误以后,重新给 src 属性赋值,一般通过这种方法可以解决。

问题二:小程序背景图实现全屏,并视频所有机型

因为机型不同,尺寸也不一样。image 组件有一个关于缩放的属性 mode,经常使用的值有三个:scaleToFill、aspectFit、aspectFill。

  • scaleToFill:不保持纵横比例缩放图片,使图片的宽高完全拉伸填充整个 image 元素。

  • aspectFit:保持纵横比例去缩放图片,使图片的长边可以完全显示出来,可以完整的将图片显示出来,不会对图片有任何的裁剪。

  • aspectFill:保持纵横比例缩放图片,并且只保证图片的短边可以完全显示出来。图片同时只能在一个方向水平或垂直方向是完整的,在另外一个方向上将会发生裁剪。

    由于 image 在加载图片的时候会有一些缺陷,所以实现背景图片适配所有机型这个问题,最好不要使用 mode 属性去实现,最好使用 WXSS 样式来实现。

.container{
    position: fixed;
    width: 100%;
    height: 100%;
    background-color: azure;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: -1;
}
.container::after{
    content: "";
    background: url(https://caroly.site/caroly_img/86009625_p0_1607479719466.png) no-repeat center center;
    background-size: cover;
    opacity: 0.5;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    position: absolute;
}

要用一个尽量覆盖所有屏幕比例的大小来做这个背景图片。一般情况下是新建一个 750*1334 这样一个大小的背景图片,并且把分辨率设置为 72,用这样的尺寸做背景图片。


问题三:如何剪切图片

通过 image-cropper 组件进行裁剪,它可以让图片通过拖拽的方式,选择范围可以任意裁剪。

实现原理:通过四角的一个控制点去控制选择的范围,四角的控制点是通过 view 渲染出来的,图片加载完成以后绘制到 canvas 画布上,选定裁剪范围,再通过 wx.canvasToTempFilePath 接口生成一个临时的图片,临时图片就是裁剪的结果。

index.wxml

<view class="page-section">
    <text class="page-section__title">图片裁剪</text>
    <image-cropper id="image-cropper" limit_move="{{true}}" disable_rotate="{{true}}" width="{{width}}" height="{{height}}" imgSrc="{{src}}" bindload="cropperload" bindimageload="loadimage" bindtapcut="clickcut"></image-cropper>
</view>

index.js

Page({
/**
 * 页面的初始数据
 */
    data: {
        src: '',
        width: 250, //宽度
        height: 250, //高度
    },
    startCuting() {
        //获取到image-cropper对象
        this.cropper = this.selectComponent("#image-cropper");
        //开始裁剪
        this.setData({
            src: "https://cdn.nlark.com/yuque/0/2020/jpeg/1252071/1590847767698-f511e86d-f183-4f75-a04d-1b99cd9f0bd7.jpeg",
        });
        wx.showLoading({
            title: '加载中'
        })
    },
/**
 * 生命周期函数--监听页面加载
 */
    onLoad: function (options) {
        this.startCuting()
    },
    cropperload(e) {
        console.log("cropper初始化完成");
    },
    loadimage(e) {
        console.log("图片加载完成", e.detail);
        wx.hideLoading();
        //重置图片角度、缩放、位置
        this.cropper.imgReset();
    },
    clickcut(e) {
        console.log(e.detail);
        console.log(e.detail.url)
        //点击裁剪框阅览图片
        wx.previewImage({
            current: e.detail.url, // 当前显示图片的http链接
            urls: [e.detail.url] // 需要预览的图片http链接列表
        })
    }
})

index.json

{
    "usingComponents": {
        "image-cropper": "../../../components/image-cropper/index"
    }
}

更新时间:2021-05-04 23:52:57

本文由 caroly 创作,如果您觉得本文不错,请随意赞赏
采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载 / 出处外,均为本站原创或翻译,转载前请务必署名
原文链接:https://caroly.fun/archives/miniprogram二组件二
最后更新:2021-05-04 23:52:57

评论

Your browser is out of date!

Update your browser to view this website correctly. Update my browser now

×