小程序技巧

好久没有写新的博客了。最近在联系小程序。所以记录一下小程序的坑与技巧。由于用 mpvue 和原生都撸过小程序,所以记录一些都会面临的情况。

页面相关

最外层占用整个屏幕

1
2
3
4
5
6
7
.container {
position: fixed;
height: 100%;
width: 100%;
display:flex;
flex-direction:column;
}

scroll-view 占满整个屏幕

此方法在 6s 等机型上存在不能滑动的问题,必须要手动设置 scroll-view 的高度才可以解决。所以需要在 scroll-view 外再包裹一个 view,然后通过 wx.createSelectorQuery() 拿到 view 的高度,再设置给 scroll-view。注意拿到的高度是 px 不是 rpx

scroll-view 如果设置 flex: 1 可以让 scroll-view 撑满屏幕。但是你会发现,scroll-view 无法滚动了。我们可以再设置一个样式:

1
2
3
4
.scroll-view {
flex: 1;
overflow: hidden;
}

设置超出部分隐藏。这样控件就自动拥有 scroll-view 的滚动功能了。

wxs 使用的坑

虽然我很抗拒 wxs,但是由于小程序不支持 computed,由于代码的美观,少在 data 中放几个字段,就需要借助 wxs 在 wxml 中生成计算后的数据。

但是其中有一个坑就是,定义变量的时候,不能使用 es6 的 letconst 只能使用 var,否则也不报错,但就是无法显示。

绝对定位的居中

绝对定位的左右两边没有占位,所以 align-items 是无效的 align-self 可能无效。这种居中分为两种情况:

  1. 设置了控件宽度
  2. 不设置控件宽度

设置了 width 的情况下。我们只要使用 left 设置控件的位置即可。设置 left: 50% 可以让控件向右移动父视图的 50% 宽度。这样会移动过头,还需要设置 margin-left: -{50%的控件宽度} 修正控件的位置。

如果没有设置 width 的情况下。可以同时设置 leftright,让视图撑满剩余的空间。

在外部修改组件库的样式

项目中引入了有赞的组件库。但是使用的时候有些样式不太符合要求。这个时候不要放弃使用,我们可以在外部修改内部的样式。

通过调试工具,找到不符合要求的视图的样式,在外部直接修改其样式:

1
2
3
.van-popup--bottom {
background: transparent !important;
}

这里我们修改了 van-popup--bottom 的背景颜色。同时在后面加上了 !important,这样是为了将我们的样式的权重提升,就不会被组件内部的样式覆盖了。

wx:key 取值

我们知道,无论是 react 还是 vue,在类表渲染的时候都提供了 key 这个关键字。key 相等的时候用来移动和改变状态,key 不等的则会进行销毁和重新创建。

React:

1
2
3
4
5
const listItems = numbers.map((number) =>
<li key={number.toString()}>
{number}
</li>
);

vue:

1
<my-component v-for="item in items" :key="item.id"></my-component>

可以看到,这两种都是拿到 item,然后设置 item 的属性为 key。

小程序则默认key就是 item 中的属性了:

1
<block wx:for="{{itemList}}" wx:key="id"/>

这样就是直接设置 item 中的 id 为 key。这样和 React 和 Vue 不统一的写法,让我有点难受。

scroll-view 横向排布排布

scroll-view 横向排布有一些坑,如果按照常规的方式,你会发现 scroll-view 中的视图还是纵向排布的。我们需要做一些设置:

1
2
3
4
5
6
7
8
9
.scroll-view {
white-space: nowrap;
display: flex;
flex-direction: row;
}

.scroll-item {
display: inline-block;
}

外部的 scroll-view 要设置 white-space,否则会换行。内部的 item 要把显示设置为 inline-block,否则还是纵向排布了。

如何只显示3行,超出显示省略号

要对 text 设置样式,一个也不能少:

1
2
3
4
5
6
7
8
9
.text {
line-height: 36rpx;
text-overflow: ellipsis;
overflow: hidden;
-webkit-line-clamp: 3;
height: 108rpx;
-webkit-box-orient: vertical;
display: -webkit-box;
}

要注意一定要设置 line-heightheight ,并且要显示几行 height 就是 line-height 的几倍。

如果是单行的省略号改为:

1
2
3
4
5
6
.text {
text-overflow: ellipsis;
overflow: hidden;
width: 123rpx;
white-space: nowrap;
}

注意,宽度一定要有。

滚动监听

小程序中滚动有两种方式。一种是直接视图的平铺,还有一种配合 scroll-view。

获取视图平铺时的滚动 offset

在平铺时候的滚动回调中每次都通过 selectViewport().scrollOffset() 拿到

1
2
3
4
5
6
7
onPageScroll: function () {
let query = wx.createSelectorQuery()
query.selectViewport().scrollOffset(function (res) {
console.log(res.scrollTop) // 竖直的滚动位置
})
query.exec()
}

获取 scrollView 的 offset

1
2
3
4
5
6
7
// scroll-view 中绑定方法
<scroll-view bindscroll="onScroll"/>

// js 中滚动方法
onScroll (e) {
console.log(e.detail.scrollTop)
}

平铺滚动到某一个位置

1
wx.pageScrollTo({scrollTop: 一个offset})

scroll-view 滚动到某一个 view 或者某一个位置

1
2
3
4
5
6
7
8
9
10
// wxml 中
<scroll-view scroll-top="{{offset}}" scroll-into-view="{{id}}"/>

// js 中
this.setData({
offset: 1000,
})
this.setData({
id: '一个view的id'
})

获取平铺或者 scroll-view 中距离顶部的 offset

1
2
3
4
5
6
7
let query = wx.createSelectorQuery()
query.select('#select').boundingClientRect(res => {
this.setData({
top: res.top
})
})
query.exec()

#select 表示当前视图的 id <view id="select />"

绝对定位设置 padding-right 不符合预期

padding 的宽度不在 width 的范围内。所以如果你绝对定位设置宽度为 100% 时,如果设置了 padding-left ,那么视图整体会向右移,即实际宽度大于 720rpx。

这种情况下,只能把视图的宽度设置为 720-2*padding 值,才能让视图正常显示。

CSS 中设置高度为屏幕大小

有时候我们要设置高度或者宽度为屏幕大小,但是使用 100% 不好用。这个时候,我们可以使用 vhvw

这是 CSS3 涌入的属性,分别表示高度/宽度基于窗口的大小,vh = view height,vw = view width:

1
2
3
4
.window {
height: 100vh;
width: 100vw;
}

设置多行子元素

为了让子元素能够自动换行,可以使用样式 flex-wrap: wrap。当然这是第一步,这么做后你会发现,换行后的子元素贴在了一起。如图所示:

你需要设置元素间间隔。你会设置元素的 margin-bottom: 20rpx。 但是这样的话父元素的 bottom 也会向下移动 20rpx。所以,你可以把父元素的 margin-bottom: -20rpx。 这样,正负抵消,就是理想的间隔了。

小程序的button的边框

小程序默认的button是有一个边框的,需要设置一个样式:

1
2
3
button::after {
border: none;
}

这样,所有的 button 标签都没有 border了

小程序捕捉事件

小程序默认的事件是冒泡的,也就是说点击了子控件触发方法后,父控件的点击事件还是会触发的,如果要阻止事件冒泡,可以把子控件的 bind:tap 改为 catch:tap

组件中的 class 选择

很多时候我们需要根据组件传入的值,来设置组件使用不同的 class。我们可以使用或或者三元选择符,但是需要使用双括号包裹,只要返回一个 class 的名字即可:

1
2
3
<button
class="someclass {{ item.disabled || item.loading ? 'some-class-disabled' : '' }} {{ item.className || '' }}"
>

组件中的 style

很多时候,我们需要传入值来控制 style,传入的值需要使用双括号包裹:

1
style="width: 100%; height: {{ itemHeight * visibleItemCount + 'px' }}"

引入 iconfont

首先把 iconfont 下载下来,打开其中的 iconfont.css 文件,应该是下面的样子,把它复制到 app.wxss 中作为全局样式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@font-face {font-family: "ChameIconfont";
... 省略了其中很多的 src 和 url
}

.ChameIconfont {
font-family:"ChameIconfont" !important;
font-size:16px;
font-style:normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

.icon-ickehu:before { content: "\e60a"; }

.icon-icshouye:before { content: "\e610"; }

由于小程序不会把 ttf 文件打包,所以需要修改 @font-face 中的 src 和 url 为网络路径。我们还是到 iconfont 的网站上,生成一个线上的文件地址,用它替换上面的 @font-face

这样项目中就可以使用了:

1
<view class='ChameIconfont icon-ickehu'/>

发布 npm 包

小程序用 npm 包需要把代码放在一个文件夹下,然后在 package.json 中增加一个 miniprogram 字段,标识代码所在目录:

1
2
3
{
"miniprogram": "dist"
}

当小程序开发工具 工具->构建npm 的时候,将该目录下的文件从 node_modules 移动到主目录下。

工具->构建npm 后,项目最外层目录生成 miniprogram_npm 文件夹,文件夹内包含所有 node_modules 中的模块。

app.json 中注册要使用的组件:

1
2
3
4
5
{
"usingComponents": {
"van-button": "/miniprogram_npm/vant-weapp/button/index"
}
}

或者直接 import

隐藏 scroll-view 滚动条

直接在相应的 wxss 中写入样式:

1
2
3
4
5
::-webkit-scrollbar {
width: 0;
height: 0;
color: transparent;
}

组件相关

外部全局样式影响组件内样式

在注册组建的时候添加一个选项,addGlobalClass 为 true:

1
2
3
4
5
Component({
options: {
addGlobalClass: true,
}
})

监听组件内数据改变

类似于 watch,组件内通过 observers 字段定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Component({
attached: function() {
this.setData({
numberA: 1,
numberB: 2,
})
},
observers: {
'numberA, numberB': function(numberA, numberB) {
// 在 numberA 或者 numberB 被设置时,执行这个函数
this.setData({
sum: numberA + numberB
})
}
}
})

父子组件的通信

小程序的父子组件提供了双向通信的方式。子组件可以通过 link 的回调,以及 getRelationNodes 两种方式获取父组件。父组件可以通过 link 回调获取子组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 子组件 path/to/custom-li.js
Component({
relations: {
'{相对路径}/custom-ul-component': {
type: 'parent', // 关联的目标节点应为父节点
linked: function(target) {
// 保存父节点
this.parent = target
},
unlinked: function(target) {
// 移除子节点
this.parent = null
}
}
},
methods: {
someFunc: function () {
// 获取页面上与子组件相关的组件的节点数组,取其中第一个节点,就是它的父节点
let group = this.getRelationNodes('{相对路径}/custom-ul-component')[0]
// 调用父节点方法
group.doSomeFunc()
}
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 父组件
Component({
relations: {
'{相对路径}/custom-ul': {
type: 'child', // 关联的目标节点应为子节点
linked: function(target) {
// 保存子节点
this.childs.push(target)
},
unlinked: function(target) {
// 移除子节点
this.childs = this.childs.filter(function(item) {
return item !== target
})
}
}
},
methods: {
someFunc: function () {
// 调用子节点方法
this.childs.doSomeFunc()
}
}
})

页面与组件的通信

组件调用页面提供的方法

  1. 页面将方法绑定给组件:

    1
    2
    3
    4
    5
    6
    7
    <component-tag-name bind:myevent="onMyEvent" />

    Page({
    onMyEvent: function(e){
    e.detail // 自定义组件触发事件时提供的detail对象
    }
    })
  2. 组件内部触发事件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <!-- 在自定义组件中 -->
    <button bindtap="onTap">点击这个按钮将触发“myevent”事件</button>

    Component({
    properties: {},
    methods: {
    onTap: function(){
    var param = {} // 提供给页面的 e.detail 的参数
    this.triggerEvent('myevent', param)
    }
    }
    })

页面调用组件提供的方法

页面调用组件方法需要获取组件实例,可以通过 page 的 selectComponent 方法获取:

1
2
3
4
5
6
7
8
9
<some-component id="custom-selector" />


const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
let component = currentPage.selectComponent('custom-selector')

// 调用组件方法
component.customFunction()