《发现》页面

调用组件外部的样式

components 内部的组件无法直接调用外部的样式。可通过以下方式调用组件外部样式:

方法一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
父组件wxml:
<!-- iconfont 和 icon-sousuo 是传入组件内部的样式名称,iconfont(自定义名称)="iconfont(外部样式文件中定义的样式名)" -->
<x-search iconfont="iconfont" icon-sousuo="icon-sousuo"/>


子组件js:
// 组件外部样式
externalClasses: [
'iconfont', // 对应的是上面等号前面的名称
'icon-sousuo'
],

子组件wxml: 即可实现调用组件外的样式
<i class="iconfont icon-sousuo" />


注意:如果想在组件内部再次修改样式,不能够引用外部传进来的class名称进行修改,可以另起一个class名称进行修改。

方法二:

消除样式隔离

1
2
3
4
5
组件内: Component({
options: {
styleIsolation: "apply-shared",
},
});

组件插槽 slot

单个插槽

1
2
3
4
5
6
7
8
9
10
11
12
13
父组件调用传入插槽内容:
<组件标签>
<view>
<view>插槽内容</view>
<view>插槽内容</view>
</view>
</组件标签>

组件内部定义slot标签:
<view>
<!-- slot插槽 -->
<slot></slot>
</view>

如果需要实现多个插槽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

父组件调用传入插槽内容:
<组件标签>
<view slot="slot2">
<view>插槽1内容</view>
<view>插槽1内容</view>
</view>

<view slot="slot1">
<view>插槽2内容</view>
<view>插槽2内容</view>
</view>
</组件标签>

组件js :
options: {// 设置
multipleSlots: true // 打开多个插槽功能
},


组件内部定义slot标签:
<view>
<!-- slot插槽 具名插槽-->
<slot name="slot1"></slot>
<slot name="slot2"></slot>
</view>

判断用户授权

授权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 判断用户是否授权
wx.getSetting({
success: (res) => {
// 这里使用箭头函数可改变内部this指向为外部的this
console.log(res);
if (res.authSetting["scope.userInfo"]) {
// 已授权
wx.getUserInfo({
// 获取用户信息
success(res) {
console.log(res);
},
});
} else {
// 未授权
}
},
});

button 的开发能力(获取用户信息)1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<button class="login"
open-type="getUserInfo"
bindgetuserinfo="onGetUserInfo" // bindgetuserinfo 为固定的
>
获取微信授权信息
</button>


bindgetuserinfo 事件会询问用户是否同意授权


js中:
onGetUserInfo(event) { // 获取用户信息
const userInfo = event.detail.userInfo
if (userInfo) { // 用户允许授权
this.setData({
modalShow: false
})
this.triggerEvent('loginSuccess', userInfo) // 给父组件传用户数据
} else { // 用户拒绝授权
this.triggerEvent('loginFail')
}
}

原生组件

原生组件

1
2
3
4
5
6
7
8
9
10
11
auto-focus 自动获取焦点

<textarea
class="content"
placeholder="分享新鲜事..."
maxlength="140"
auto-focus
bindinput="onInput"
bindfocus="onFocus"
bindblur="onBlur"
></textarea>

选择上传图片

上传图片

1
2
3
4
5
6
7
8
9
10
let max = 9 - this.data.images.length; // 还能再选几张图片
wx.chooseImage({
count: max, // 还能再选几张图片
sizeType: ["original", "compressed"], // 初始值 and 压缩过的
sourceType: ["album", "camera"], // 手机相册选择 and 拍照选择
success: (res) => {
// 箭头函数改变this指向
console.log(res);
},
});

图片裁剪

图片裁剪

1
2
<!-- mode 图片裁剪 aspectFill 保证短边完整显示 -->
<image class="image" src="{{item}}" mode="aspectFill"></image>

获取标签自定义属性 data-* (删除图片的实现)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- 显示图片 -->
<block wx:for="{{images}}" wx:key="*this">
<view class="image-wrap">
<!-- mode 图片裁剪 aspectFill 保证短边完整显示 -->
<image class="image" src="{{item}}" mode="aspectFill"></image>
<icon class="iconfont icon-shanchu" bindtap="onDelImage" data-index="{{index}}"></icon>
</view>
</block>


// 删除图片
onDelImage(event) {
// event.target.dataset.index 获取标签属性data-index的值
this.data.images.splice(event.target.dataset.index, 1) // splice会改变原有数组
this.setData({
images: this.data.images
})
},

全屏预览图片(点击图片放大预览)

全屏预览图片

1
2
3
4
5
6
7
// 全屏预览图片
onPreviewImage(event) {
wx.previewImage({
urls: this.data.images, // 图片地址列表
current: event.target.dataset.imgsrc // 当前预览图片地址
})
},

文件上传云存储(发布博客例子)

文件上传云存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
//  结合'发布'的例子:
send() {
// 验证是否输入内容
if (content.trim() === '') { // trim() 去掉字符串空格
wx.showToast({
title: '请输入内容',
icon: 'none'
})
return
}
wx.showLoading({
title: '发布中',
})
/**
* 实现思路及步骤:
* 1、图片 -> 上传 云存储 -> 生成 图片fineID(云文件ID)
* 2、数据 -> 录入 云数据库
* 数据包括:文字内容、图片fineID、昵称、头像、发布时间、openId(用户唯一标识,在插入数据库是系统会自动添加_openId字段,不需要另外插入)
*/
let promiseArr = []
let fileIds = []
// 图片上传云存储
this.data.images.forEach((item) => {
let p = new Promise((resolve, reject) => {
let suffix = /\.\w+$/.exec(item)[0] // 文件扩展名(文件后缀)
wx.cloud.uploadFile({ // 每次只能上传一个文件
/**
* cloudPath 云路径。如果路径相同,后上传的文件会覆盖原文件
* 路径:blog/云存储中的文件夹 + Date.now()时间戳 + Math.random()*1000000随机数 + 文件后缀
*/
cloudPath: 'blog/' + Date.now() + '-' + Math.random() * 1000000 + suffix,
filePath: item, // 文件本地临时路径
success: (res) => {
fileIds.push(res.fileID)
resolve()
},
fail: (err) => {
console.error(err)
reject()
}
})
})
promiseArr.push(p)
})

// 存入云数据库
Promise.all(promiseArr).then((res) => {
db.collection('blog').add({
data: {
...userInfo, // 昵称、头像
content, // 内容
img: fileIds, // 图片fileID列表
createTime: db.serverDate() // 创建时间,取服务端时间
}
}).then((res) => {
wx.hideLoading()
wx.showToast({
title: '发布成功',
})
// 返回博客页面,并刷新
wx.navigateBack()

})
}).catch((err) => {
wx.hideLoading()
wx.showToast({
title: '抱歉,发布失败',
icon: 'none'
})
})
},

js 模块化 (时间格式化)

在目录 utils 中新建 formatTime.js 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 时间格式化 模块封装
module.exports = (date) => {
// date 数据格式为 date
let fmt = "yyyy-MM-dd hh:mm:ss"; // 预定格式
const o = {
// + 正则中的1个或多个
"M+": date.getMonth() + 1,
"d+": date.getDate(),
"h+": date.getHours(),
"m+": date.getMinutes(),
"s+": date.getSeconds(),
};

if (/(y+)/.test(fmt)) {
// $1 表示正则中的第一个,即(y+)
fmt = fmt.replace(RegExp.$1, date.getFullYear()); // replace 替换
}

for (let k in o) {
if (new RegExp("(" + k + ")").test(fmt)) {
fmt = fmt.replace(
RegExp.$1,
o[k].toString().length === 1 ? "0" + o[k] : o[k]
);
}
}

return fmt;
};

在组件引入 js 模块

1
2
3
4
import formatTime from '../../utils/formatTime.js'

使用:
formatTime(new Date('Wed Aug 28 2019 16:23:06 GMT+0800 (中国标准时间)'))

阻止事件冒泡

bindcatch 都可以绑定事件,它们的区别是 bind 有事件冒泡,而 catch 没有

返回上一个页面并执行方法

API

1
2
3
4
5
// 返回博客页面,并刷新
wx.navigateBack();
const pages = getCurrentPages(); // 获取当前页面栈
const prevPage = pages[pages.length - 2]; // 取到上一个页面
prevPage.onPullDownRefresh(); // 执行上一个页面的方法 onPullDownRefresh

图片懒加载

API

1
2
3
4
给image标签设置 lazy-load 为 true
<image class="img" src="{{item}}" lazy-load="true"></image>

.img { background: #eee; }

懒加载占位图可以给 image 设置背景图或背景色

模糊查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 获取博客列表
app.router("blogList", async (ctx, next) => {
const keyword = event.keyword; // 搜索关键字 调用接口时传递来的数据
let w = {};
if (keyword.trim() != "") {
w = {
content: db.RegExp({
// 正则
regexp: keyword,
options: "i", // i表示忽略大小写
}),
};
}

// where查询条件 skip 从第几条开始查,limit 查几条数据,orderBy(排序字段,排序方式) 排序,排序方式desc降序/asc升序
ctx.body = await blogCollection
.where(w)
.skip(event.start)
.limit(event.count)
.orderBy("createTime", "desc")
.get()
.then((res) => {
return res.data;
});
});

提升模糊查询的效率 (添加索引,对数据量大的查询效果明显)

云开发控制台 > 数据库相应的集合 > 索引管理 > 添加索引 > 输入自定义索引名称、该字段的值是否唯一、被查询的字段名、升序/降序 > ok

小程序端调用云数据库

一般调用云数据库的操作都写在云函数内,其实小程序端也可以对数据库进行操作。

小程序端一次最多只能查询 20 条数据,云函数端最多可查询 100 条数据,可使用多次查询拼接的方式突破限制。

1
2
3
4
5
6
7
8
// 小程序端调用云数据库示例
const db = wx.cloud.database(); // 初始化数据库
db.collection("blog")
.orderBy("createTime", "deac")
.get()
.then((res) => {
console.log(res);
});

云数据库权限管理

注意:云控制台和服务端(云函数)始终有所有数据读写权限,

但权限的管理仅对小程序端发起的请求有效。

  • 仅创建者可写,所有人可读 (适合于文章)

  • 仅创建者可读写 (适用于私密内容)

  • 仅管理端可写,所有人可读(适用于商品信息)

  • 仅管理端可读写(适用于后台敏感数据)

数据库中 1 对 N 关系的三种设计方式

第一种:N 的数量较少 几十个以内

1 条记录存储 N 个子数据

​ 如一条博客中,最多有 9 张图片,这 9 张图片可和其他数据放在一个记录中。

1
2
3
4
5
6
7
8
[
{
id:...
img:[
'...', '...', '...', '...', '...', '...', '...', '...', '...'
]
}
]
第二种:N 的数量较多 几十到几百个

1 存储 每个 N 的 id

可分两个数据库集合,

一个为 ‘目录’ 集合,存放 ‘详情’ 集合下的每条数据的 id 目录

一个为 ‘详情’ 集合,每条数据对应一个单独的 id 和 详细数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
目录集合:
[
{
'id':"11",
'name': '产品1',
'xqs': ['111','222','333', ... ] // 存放 详情集合 中的每条数据 id
}
]



详情集合:
[
{'id':"111",name:'零件1',title:'...' ...},
{'id':"222",name:'零件2',title:'...' ...},
{'id':"333",name:'零件3',title:'...' ...},
...
]


如歌单列表,与歌曲详情的数据组合设计。

第三种:N 的数量巨大 几百成千上万个

每个 N 都存储 1 的 id

如新浪博客中的一条博客下面有几千条评论

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
一条新浪博客:
[{
'id':'11',
'content':'博客内容'
...
}]


上千条评价:
[
{
'id':'111111'
'blogId':'11', // 这个id对应的是那一条博客的id
'content': '评价内容1'
},
{
'id':'222222'
'blogId':'11', // 这个id对应的是那一条博客的id
'content': '评价内容2'
},
{
'id':'33333'
'blogId':'11', // 这个id对应的是那一条博客的id
'content': '评价内容3'
},
...
]

云调用

通过云函数调用服务端的开发接口

这些接口如:模板消息推送、生成小程序码…

模板消息推送

1、使用 from 表单才能触发消息推送,并设置 report-submit=”true”

1
2
3
4
5
6
7
8
9
10
<form slot="modal-content" report-submit="true" bind:submit="onSend">
<textarea
name="content"
class="comment-content"
placeholder="写评论"
value="{{content}}"
fixed="true"
></textarea>
<button class="send" form-type="submit">发送</button>
</form>

2、需要到微信公众平台做相应的设置:

微信公众平台 > 功能 > 模板消息 > 添加模板 > 选择相应的模板> 添加成功后会有一个模板 ID

3、新建一个云函数,用于云调用。在该云函数下新建配置文件:config.json ,用于配置权限

config.json :

1
2
3
4
5
{
"permissions": {
"openapi": ["templateMessage.send"]
}
}

云函数设置消息推送:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 云函数入口函数
exports.main = async (event, context) => {
// 获取openid
const { OPENID } = cloud.getWXContext();

// 模板推送消息
const result = await cloud.openapi.templateMessage.send({
touser: OPENID,
page: `/pages/blog-comment/blog-comment?blogId=${event.blogId}`, // 用户点击推送消息打开的页面
data: {
// 模板的内容,keyword为在公众平台设置模板时对应的字段
keyword1: {
// 评价内容
value: event.context,
},
keyword2: {
// 评价时间
value: event.time,
},
},
templateId: "LNwKMcYwlz-0HabgBhmZi6CWZrlNSBiNJ2h0SMorcxQ", // 模板id,到公众平台模板消息上获取
formId: event.formId, // 触发消息推送的form表单的id
});

return result;
};

4、在提交表单事件完成后调用消息推送云函数

1
2
3
4
5
6
7
8
9
10
11
12
wx.cloud
.callFunction({
name: "sendMessage",
data: {
content,
formId,
blogId: this.properties.blogId,
},
})
.then((res) => {
console.log(res);
});

云函数多集合查询数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 博客详情(博客内容、评论)
app.router("blogDetail", async (ctx, next) => {
let blogId = event.blogId;

// 博客内容
let detail = await blogCollection
.where({
_id: blogId,
})
.get()
.then((res) => {
return res.data;
});

// 评论查询
const countResult = await blogCollection.count();
const total = countResult.total;
let commentList = {
data: [],
};
if (total > 0) {
// 突破100条限制
const batchTimes = Math.ceil(total / MAX_LIMIT);
const tasks = [];
for (let i = 0; i < batchTimes; i++) {
let promise = db
.collection("blog-comment")
.skip(i * MAX_LIMIT)
.limit(MAX_LIMIT)
.where({
blogId,
})
.orderBy("createTime", "desc")
.get();
tasks.push(promise);
}
if (tasks.length > 0) {
commentList = (await Promise.all(tasks)).reduce((acc, cur) => {
return {
data: acc.data.concat(cur.data),
};
});
}
}
ctx.body = {
detail,
commentList,
};
});

分享功能

分享功能需要 button 标签,设置 open-type=”share”

1
2
3
4
5
6
7
8
9
10
<button
open-type="share"
data-blogid="{{blogId}}"
data-blog="{{blog}}"
class="share-btn"
hover-class="share-hover"
>
<i class="iconfont icon-fenxiang icon"></i>
<text>分享</text>
</button>

在 js 中有 onShareAppMessage 方法,点击 button 会自动执行此方法

1
2
3
4
5
6
7
8
9
10
11
onShareAppMessage: function (event) {
console.log(event)

// 对分享卡片的设置
let blogObj = event.target.dataset.blog
return {
title: blogObj.content,
path: `/pages/blog-comment/blog-comment?blogId=${blogObj._id}`,
// imageUrl: '' // 自定义图片,不支持云存储的图片
}
}

不同场景获取用户信息的方式

场景一:只想在界面上显示自己的昵称和头像

以组件的方式:根据 type 类型获取不同用户数据

该方式不需要授权,只能用于在 wxml 显示自己的信息

open-data

1
2
3
<open-data type="userAvatarUrl"></open-data>
<open-data type="userNickName"></open-data>
...
场景二:在 JS 中获取用户信息

该方式要在用户授权以后才能获取用户信息

wx.getUserInfo

1
2
3
4
5
wx.getUserInfo({
success: (res) => {
console.log(res);
},
});

在未授权的情况下需要用户先授权:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 判断用户是否授权
wx.getSetting({
success: (res) => { // 这里使用箭头函数可改变内部this指向为外部的this
if (res.authSetting['scope.userInfo']) { // 已授权
wx.getUserInfo({ // 获取用户信息
success: (res) => { // 这里使用箭头函数可改变内部this指向为外部的this

app.setGlobalData('userInfo', res.userInfo) // 设置app全局属性

this.onLoginSuccess({
detail: res.userInfo
})
}
})
} else { // 未授权
this.setData({ // 打开弹出层,显示获取用户信息按钮
modalShow: true
})
}
}
})


授权按钮
<button class="login" open-type="getUserInfo" bindgetuserinfo="onGetUserInfo">获取微信授权信息</button>


onGetUserInfo(event) { // 获取用户信息
const userInfo = event.detail.userInfo
if (userInfo) { // 用户允许授权
this.setData({
modalShow: false
})
this.triggerEvent('loginSuccess', userInfo) // 给父组件传用户数据
} else { // 用户拒绝授权
this.triggerEvent('loginFail')
}
}

注意:上面这种方式没有获取到 openId

场景三:获取 openId

获取 openId 不需要用户授权

1、传统开发方式获取 openId,后台服务器由自己开发,没使用云开发

小程序端 微信服务器 后端服务器

步骤:

小程序端 调用 wx.login 向微信服务器 获取 code

小程序端 调用 wx.request 将 code 传递给 后端服务器

后端服务器 使用 code 向微信服务器 换取 openid 和 session_key

后端服务器 将 openid 发送给 小程序端

2、云开发方式获取 openId

云函数 login 中

1
2
3
4
5
6
7
8
9
// 获取 WX Context (微信调用上下文),包括 OPENID、APPID、及 UNIONID(需满足 UNIONID 获取条件)
const wxContext = cloud.getWXContext();

return {
event,
openid: wxContext.OPENID,
appid: wxContext.APPID,
unionid: wxContext.UNIONID,
};
1
2
3
4
5
6
7
8
9
10
普通按钮
<button bindtap="getOpenid">获取openid</button>

getOpenid() {
wx.cloud.callFunction({
name: 'login'
}).then((res) => {
console.log(res)
})
}

openid 在小程序和公众号下是不一样的

unionid 在小程序和公众号下都是一样的