小程序笔记
基础课程部分
微信公众平台
https://mp.weixin.qq.com/
注册时可选择类型:订阅号、服务号、小程序、企业微信
每个邮箱仅能注册一个小程序。
个人类型小程序:无法使用微信支付、无法使用卡包功能
小程序文档API
小程序开发文档
微信开放社区
微信开发社区
目录说明
默认目录
pages———————–页面相关
index —————– 首页文件夹
index.js ————首页js
index.json———首页配置
index.wxml——-首页html
index.wxss——–首页css
utils————————工具相关
app.js ———————-项目总js
app.json——————-全局配置( 页面路由以及头部、底部导航的配置等)
app.wxss —————–项目总样式css
project.config.json —-项目配置
代码构成
.json :配置文件,以json格式存储配置
项目中有三种配置:项目配置(project.config.json)、全局配置(app.json)、页面配置(index.json)
.wxml: 相当于html文件
.wxss: 相当于css
.js : 就是js
文件说明
project.config.json项目配置 部分代码说明
setting:{
urlCheck 是否检测安全的域名
es6 是否把es6转es5
postcss 是否把css样式自动补全
minified 是否压缩
}
app.json 全局配置
全局配置API
wxml 相关介绍
wxmlAPI
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
| <view>{{motto}}</view>
循环渲染 <view wx:for="{{list}}" wx:key="{{index}}"> {{index}} {{item}} </view>
改变for循环item和index的名称 <block wx:for="{{list}}" wx:for-item="data" wx:for-index="inx"> {{inx}} {{data}} </block>
条件渲染 (类似vue的v-if、v-else) <view wx:if="{{isLogin}}">已登录</view> <view wx:else>请登录</view>
条件显示(类似vue的v-show) <view hidden="{{isLogin}}">显示内容</view>
绑定点击事件 <button bindtap=“tapName”>按钮</button>
Page({ tapName: function(event) { console.log(event) } }) ...
|
wxss 相关介绍
wxssAPI
尺寸单位:rpx,根据屏幕宽度自适应。
引入外部wxss:@import ’…‘
js相关介绍
WXS(WeiXin Script)是小程序的一套脚本语言
wxsAPI
绑定点击事件
1 2
| <button bindtap=“onTapHandler”>点我+1</button> <view>{{count}}</view>
|
1 2 3 4 5 6 7 8 9 10
| Page({ data: { count: 0 }, onTapHandler: function() { this.setData({ count: this.data.count++ }) } })
|
阻止事件冒泡
把绑定方式 bindtap 换成 catchtap 即可。
第三方库
WeUI
weUI是一套同微信原生视觉体验一致的基础样式库
iView Weapp
一套高质量的微信小程序UI组件库
Vant Weapp
轻量、可靠的小程序UI组件库
云开发
小程序传统开发模式
客户端 —–> 服务端(后端代码、数据库)——> 运维(DB维护、文件存储、内容加速、网络防护、容器服务、负载均衡、安全加固等…)
小程序云开发模式
客户端 —–> 云开发(云函数、云数据库、云存储)
传统开发 VS 云开发
开发效率低 Serverless(无服务)
运维成本高 开发者更关注业务逻辑
无服务(Serverless)开发是未来的发展趋势
云开发三大基础能力
云函数
(相当于传统开发中的后台接口)
获取appid、获取openid、生成分享图、调用腾讯云SDK …
云数据库
数据的增、删、改、查 …
云存储
管理文件、上传文件、下载文件、分享文件 …
每个小程序账号可免费创建两个环境,建议:开发环境、生成环境
云数据库能力
云开发提供了一个json数据库,提供2GB免费存储空间。
数据类型
String 字符串
Number 数字
Object 对象
Array 数组
Boolean 布尔值
GeoPoint 地理位置点
Date 时间 (精确到毫秒ms,客户端时间)
Null 空
操作云数据库
小程序控制(读写数据库受权限限制)
云函数控制(拥有所有读写数据库的权限)
控制台控制(拥有所有读写数据库的权限)
云数据库权限管理
仅创建者可写,所有人可读 (适合于文章)
仅创建者可读写 (适用于私密内容)
仅管理端可写,所有人可读(适用于商品信息)
仅管理端可读写(适用于后台敏感数据)
操作云数据库
1 2 3 4 5 6 7
| const db = wx.cloud.database()
const testDB = wx.cloud.database({ env: 'test' })
|
实战课程部分
serverless(无服务)
概念:函数即服务,当需要后端服务的时候,不需要关心后端的IP地址、域名,只需要像调用普通函数一样既可以实现调用。
云开发优势
快速上线、专注核心业务、独立开发一个完整的微信小程序、不需要学习新的语言,只需要会javascript、无需运维, 节约成本、数据安全、
云开发提供能力
云函数:在云端运行的代码,微信私有协议天然鉴权 (理解:相当于后端部分)
云数据库:一个既可以在小程序端操作又可以在云函数中操作的JSON数据库
云存储:在云端存储文件,可以在云端控制台可视化管理
云调用:基于云函数免鉴权使用小程序开放接口的能力(比如说给用户推送消息等)
HTTP API:使用HTTP API开发者可在已有服务器上访问云资源,实现与云开发的互通(作用:对原有传统模式下开发的小程序,可以与云开发进行互通)
appID
每个小程序唯一的id
云开发项目默认目录结构
cloudfunctions —————————-云函数
callback ———————————- 回调函数
config.json —————————
index.js ——————————–
package.json ————————
echo —————————————-
login —————————————-
openapi ———————————–
miniprogram ——————————- 小程序
images ————————————- 图片
pages ————————————— 页面
style —————————————– 样式
app.js ————————————— 项目js
app.json ———————————– 全局配置
app.wxss ———————————- 项目样式
sitemap.json —————————– (小程序SEO相关)
project.config.json ———————– 项目配置
云开发环境
云开发可创建两个环境,建议一个为开发环境,一个为生产环境
开发前的准备
开发工具 > 右上角详情 > 本地设置 > 调试基础库 设置为最新版本
app.js > wx.cloud.init > env 设置环境ID
project.config.json 文件说明
miniprogramRoot 小程序前端代码目录
cloudfunctionRoot 云函数代码目录
app.json
pages 设置页面 ,设置后会自动在pages目录下生成相应的目录和文件
设置底部导航按钮:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| "tabBar": { "color": "#474747", "selectedColor": "#d43c43", "list": [{ "pagePath": "pages/playlist/playlist", "text": "音乐", "iconPath": "images/music.png", "selectedIconPath": "images/music-actived.png" }, { "pagePath": "pages/blog/blog", "text": "发现", "iconPath": "images/blog.png", "selectedIconPath": "images/blog-actived.png" }, { "pagePath": "pages/profile/profile", "text": "我的", "iconPath": "images/profile.png", "selectedIconPath": "images/profile-actived.png" }] }
|
图标来自于 https://www.iconfont.cn
阿里巴巴图标库,包含矢量图标、字体图标、字体等
代码规范
很多公司借鉴的代码规范:https://github.com/airbnb/javascript
《音乐》页面开发
1 2 3 4 5 6 7 8 9
| <swiper indicator-dots="true" circular="true" interval="3000" duration="500"> <block wx:for="{{swiperImgUrls}}" wx:key="{{index}}"> <swiper-item> <image src="{{item.url}}" mode="widthFix" class="img"></image> </swiper-item> </block> </swiper>
|
自定义组件
创建组件
创建目录 components > 组件目录名称 > 右键 新建Component
引入组件
在page的json文件中:
1 2 3 4 5
| { "usingComponents": { "x-playlist":"/components/playlist/playlist" } }
|
在page的wxml中:
1
| <x-playlist> </x-playlist>
|
页面引入组件以及组件内部在引用子组件的方法是一样的,同样需要设置json文件。
组件传值
父组件中:在引入组件的时候自定义属性名称,并把数据传入子组件
1 2
| <x-playlist playlist="{{传入的数据}}"></x-playlist>
|
子组件中:
子组件的js文件:
1 2 3 4 5 6 7 8 9 10
|
properties: { playlist:{ type: Object } },
|
wx:key 的使用
key的值不建议使用index,因为当数据发生变化会dom结构产生变化时,使用index的地方不会随之变化。
可以使用数据内部每项不一样的一个数值,如id
1 2 3 4 5 6 7 8 9 10 11 12 13
| <block wx:for="{{swiperImgUrls}}" wx:key="url"> 这里url不需要双大括号,如使用index则需要{{}} <view> <image src="{{item.url}}" mode="widthFix" class="img"></image> </view> </block>
<view class="playlist-container"> <block wx:for="{{playlist}}" wx:key="_id"> <x-playlist playlist="{{item}}"></x-playlist> </block> </view>
|
async/await 语法
目前,在云函数里,由于 Node 版本最低是 8.9,因此是天然支持 async/await 语法的。而在小程序端则不然。在微信开发者工具里,以及 Android 端手机(浏览器内核是 QQ浏览器的 X5),async/await是天然支持的,但 iOS 端手机在较低版本则不支持,因此需要引入额外的 文件。
可把这个 runtime.js 文件引用到有使用 async/await 的文件当中。
1 2
| import regeneratorRuntime from '../../utils/runtime.js'
|
云函数的使用
cloudfunctions目录 右键 新建 Node.js 云函数 > 输入目录名 getPlaylist
在云函数中向第三方服务器发送请求要依赖第三方库
安装依赖包
云函数目录 getPlaylist 右键 在终端打开 打开命令行 输入命令:
1 2
| npm install --save request npm install --save request-promise
|
github request-promise:https://github.com/request/request-promise
然后写相应代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const cloud = require('wx-server-sdk')
cloud.init()
const rp = require('request-promise')
const URL = 'http://musicapi.xiecheng.live/personalized'
exports.main = async (event, context) => { const playlist = await rp(URL).then((res) => { return JSON.parse(res).result }) console.log(playlist) }
|
写完代码,云函数目录 getPlaylist 右键 上传并部署:云端安装依赖(不上传node_modules) 进行上传部署代码到云端,等待上传成功,打开云开发控制台即可看到已经上传的云函数,并可对云函数进行测试。
数据库操作
数据库> 创建集合 > playlist
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 72 73 74 75 76 77 78 79 80 81
| const cloud = require('wx-server-sdk')
cloud.init()
const db = cloud.database()
const rp = require('request-promise')
const URL = 'http://musicapi.xiecheng.live/personalized'
const playlistCollection = db.collection('playlist')
const MAX_LIMIT = 10
exports.main = async (event, context) => {
const countResult = await playlistCollection.count() const total = countResult.total const batchTimes = Math.ceil(total / MAX_LIMIT) const tasks = [] for(let i = 0; i < batchTimes; i++) { let promise = playlistCollection.skip(i * MAX_LIMIT).limit(MAX_LIMIT).get() tasks.push(promise) } let list = { data: [] } if (tasks.length > 0) { list = (await Promise.all(tasks)).reduce((acc, cur) => { return { data: acc.data.concat(cur.data) } }) }
const playlist = await rp(URL).then((res) => { return JSON.parse(res).result })
const newData = [] for(let i = 0, len1 = playlist.length; i < len1; i++) { let flag = true for(let j = 0, len2 = list.data.length; j < len2; j++) { if(playlist[i].id === list.data[j].id){ flag = false break } } if(flag){ newData.push(playlist[i]) } }
for (let i = 0, len = newData.length; i < len; i++) { await playlistCollection.add({ data: { ...newData[i], createTime: db.serverDate(), } }).then((res) => { console.log('数据添加成功') }).catch((err) => { console.error(err) }) } return newData.length }
|
查询数据库
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
|
const cloud = require('wx-server-sdk')
cloud.init()
const TcbRouter = require('tcb-router') const db = cloud.database() const blogCollection = db.collection('blog')
exports.main = async (event, context) => { const app = new TcbRouter({ event })
app.router('list', async (ctx, next) => { ctx.body = await blogCollection.skip(event.start).limit(event.count) .orderBy('createTime', 'desc').get().then((res) => { return res.data })
})
return app.serve() }
|
云函数调试
云控制台中可会云函数进行云端测试
在小程序调用云函数后,可查看云函数日志
定时触发云函数
如果云函数需要定时 / 定期执行,也就是定时触发,我们可以使用云函数定时触发器。配置了定时触发器的云函数,会在相应时间点被自动触发,函数的返回结果不会返回给调用方
云函数目录下新建 config.json
API
1 2 3 4 5 6 7 8 9
| { "triggers": [ { "name": "myTriggers", "type": "timer", "config":"0 0 10,14,16,20 * * * *" } ] }
|
编辑好触发器之后,要在云函数目录 > 右键 > 上传触发器
配置云函数超时时间
当云函数比较复杂的时候,默认的超时时间3秒可能不能够满足需求,可以适当的设置更为合理的时间
云开发控制台 > 云函数 > 配置 > 超时时间
上拉加载与下拉刷新
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
| page页面json中: "enablePullDownRefresh": true
page页面js中有这两个函数:
onPullDownRefresh: function() { this.setData({ playlist: [] }) this._getPlaylist() },
onReachBottom: function() { this._getPlaylist() },
下拉刷新请求完数据后 wx.stopPullDownRefresh()
|
云函数路由优化tcb-router
一个用户在一个云环境只能创建50个云函数
假如小程序非常复杂,50个云函数不能够满足业务需求怎么办?
相似的请求归类到同一个云函数处理
tcb-router是一个koa风格的云函数路由库
通俗理解就是可以把很多个接口归类到同一个云函数内。
github-tcb-router: https://github.com/TencentCloudBase/tcb-router
koa洋葱模型…
安装:
1 2
| 在使用到tcb-router的云函数目录下打开命令行,输入命令进行安装 npm install --save tcb-router
|
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
| const TcbRouter = require('router');
exports.main = (event, context) => { const app = new TcbRouter({ event });
app.use(async (ctx, next) => { ctx.data = {}; ctx.data.openId = event.userInfo.openId await next(); });
app.router(['user', 'timer'], async (ctx, next) => { ctx.data.company = 'Tencent'; await next(); });
app.router('user', async (ctx, next) => { ctx.data.name = 'heyli'; await next(); }, async (ctx, next) => { ctx.data.sex = 'male'; await next(); }, async (ctx) => { ctx.data.city = 'Foshan'; ctx.body = { code: 0, data: ctx.data}; });
app.router('timer', async (ctx, next) => { ctx.data.name = 'flytam'; await next(); }, async (ctx, next) => { ctx.data.sex = await new Promise(resolve => { setTimeout(() => { resolve('male'); }, 500); }); await next(); }, async (ctx)=> { ctx.data.city = 'Taishan';
ctx.body = { code: 0, data: ctx.data }; });
return app.serve();
}
小程序端:
wx.cloud.callFunction({ name: "router", data: { $url: "user", other: "xxx" } }).then((res) => { console.log(res) })
|
上面tcb-router代码会按照洋葱模型执行,即先从上往下逐个进入中间件,再从下往上逐个退出中间件。
本地存储(缓存)
1 2 3 4 5 6 7 8
| wx.setStorageSync(key, data) wx.setStorage(key, data)
wx.getStorageSync(key) wx.setStorage(key)
|
api设置title
1 2 3
| wx.setNavigationBarTitle({ title: '', })
|
背景播放音
BackgroundAudioManager 全局唯一的背景音频管理器
1 2 3
|
"requiredBackgroundModes": ["audio", "location"]
|
1 2 3 4 5 6 7
| const backgroundAudioManager = wx.getBackgroundAudioManager()
backgroundAudioManager.src = 音频链接 backgroundAudioManager.title = 音频标题
|
createSelectorQuery查询节点信息
createSelectorQuery 小程序的方法,用于查询节点等操作
1 2 3 4 5 6 7
| const query = wx.createSelectorQuery() query.select('#the-id').boundingClientRect() query.selectViewport().scrollOffset() query.exec(function(res){ res[0].top res[1].scrollTop })
|
组件内的方法
Component(Object object)
组件生命周期
lifetimes
1 2 3 4 5 6
| lifetimes: { ready() { ... } },
|
组件所在页面的生命周期
1 2 3 4 5 6 7 8 9 10 11 12 13
| Component({ pageLifetimes: { show: function() { }, hide: function() { }, resize: function(size) { } } })
|
组件对数据的监听
observers
1 2 3 4 5
| observers: { 监听的数据对象(newData){ console.log(newData) } },
|
子组件自定义事件传递给父组件
1 2 3 4 5 6 7 8 9 10 11 12
| 子组件js: // 触发自定义事件 向父组件传值, 参数x(可选,传递给父组件的参数,可以是对象或其他) this.triggerEvent('自定义事件名', 参数x)
父组件wxml: <子组件标签 bind:自定义事件名="执行的事件" />
父组件js: 执行的事件(event) { console.log(event.detil.参数) }
|
父组件自定义事件传递给子组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 父组件wxml: <子组件标签 class="子组件类名">
父组件JS: // 选择组件,并传入事件和参数 this.selectComponent('.子组件类名').自定义事件名(传入参数)
子组件js: methods: { 自定义事件名(参数x){ console.log(参数x) } }
|
兄弟组件间传递事件和传值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 例子:子组件1向子组件2传递参数
父组件wxml中: <子组件标签1 bind:自定义事件名1="执行的事件"> <子组件标签2 class="子组件2类名">
父组件js: 执行的事件(event) { this.selectComponent('.子组件2类名').自定义事件名2(event.detil.参数x) // 向子组件2传值 }
子组件1js: // 触发自定义事件 向父组件传值, 参数x(可选,传递给父组件的参数,可以是对象或其他) this.triggerEvent('自定义事件名1', 参数x)
子组件2js: methods: { 自定义事件名2(参数x){ console.log(参数x) // 接收父组件传入的值 } }
|
获取手机信息
wx.getSystemInfo(Object object)
1 2 3 4 5
| wx.getSystemInfo({ success(res){ console.log(res) } })
|
滚动组件
scroll-view
1 2
| <scroll-view scroll-y scroll-top="{{scrollTop}}" scroll-with-animation="true"> </scroll-view>
|
全局属性、方法(类似vuex)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 在app.js中:
onLaunch: function () { this.globalData = {// 设置全局属性、方法 test: 0 } }, setGlobalData(dataItem, val) { // 设置全局属性 this.globalData[dataItem] = val }, getGlobalData(dataItem) { // 获取全局属性 return this.globalData[dataItem] }
在需要调用的页面js中: const app = getApp() // 在最顶部先调用app方法
// 设置全局属性 app.setGlobalData('test', 1)
// 获取全局属性 app.getGlobalData('test')
|
消息提示框
showToast
1 2 3 4 5
| wx.showToast({ title: '成功', icon: 'success', duration: 2000 })
|
《发现》页面
调用组件外部的样式
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 6
| 组件内: 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
| wx.getSetting({ success: (res) => { console.log(res) if (res.authSetting['scope.userInfo']) { wx.getUserInfo({ success(res) { console.log(res) } }) } else {
} } })
|
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" > 获取微信授权信息 </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 12
| 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
| let max = 9 - this.data.images.length wx.chooseImage({ count: max, sizeType: ['original', 'compressed'], sourceType: ['album', 'camera'], success: (res) => { console.log(res) }, })
|
图片裁剪
图片裁剪
1 2
| <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() === '') { wx.showToast({ title: '请输入内容', icon: 'none' }) return } wx.showLoading({ title: '发布中', })
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 + 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, 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
| module.exports = (date) => { let fmt = 'yyyy-MM-dd hh:mm:ss' const o = { 'M+': date.getMonth() + 1, 'd+': date.getDate(), 'h+': date.getHours(), 'm+': date.getMinutes(), 's+': date.getSeconds() }
if (/(y+)/.test(fmt)) { fmt = fmt.replace(RegExp.$1, date.getFullYear()) }
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 (中国标准时间)'))
|
阻止事件冒泡
bind 和 catch 都可以绑定事件,它们的区别是 bind 有事件冒泡,而 catch 没有
返回上一个页面并执行方法
API
1 2 3 4 5
| wx.navigateBack() const pages = getCurrentPages() const prevPage = pages[pages.length - 2] prevPage.onPullDownRefresh()
|
图片懒加载
API
1 2 3 4 5 6
| 给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
| app.router('blogList', async (ctx, next) => { const keyword = event.keyword let w = {} if (keyword.trim() != '') { w = { content: db.RegExp({ regexp: keyword, options: 'i' }) } }
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
| 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
| 目录集合: [ { 'id':"11", 'name': '产品1', 'xqs': ['111','222','333', ... ] } ]
详情集合: [ {'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', 'content': '评价内容1' }, { 'id':'222222' 'blogId':'11', 'content': '评价内容2' }, { 'id':'33333' 'blogId':'11', 'content': '评价内容3' }, ... ]
|
云调用
通过云函数调用服务端的开发接口
这些接口如:模板消息推送、生成小程序码…
模板消息推送
1、使用from表单才能触发消息推送,并设置report-submit=”true”
1 2 3 4 5
| <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 6 7
| { "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
| exports.main = async (event, context) => { const { OPENID } = cloud.getWXContext()
const result = await cloud.openapi.templateMessage.send({ touser: OPENID, page: `/pages/blog-comment/blog-comment?blogId=${event.blogId}`, data: { keyword1: { value: event.context }, keyword2: { value: event.time } }, templateId: 'LNwKMcYwlz-0HabgBhmZi6CWZrlNSBiNJ2h0SMorcxQ', formId: event.formId })
return result }
|
4、在提交表单事件完成后调用消息推送云函数
1 2 3 4 5 6 7 8 9 10
| 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
| 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) { 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
| <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}`, } }
|
不同场景获取用户信息的方式
场景一:只想在界面上显示自己的昵称和头像
以组件的方式:根据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) => { if (res.authSetting['scope.userInfo']) { wx.getUserInfo({ success: (res) => {
app.setGlobalData('userInfo', res.userInfo)
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
| 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 在小程序和公众号下都是一样的
《我的》页面
json文件
1 2
| "navigationBarTitleText": "我的", "disableScroll": true
|
导航页面链接跳转
navigator
背景图片
wxss背景图片不支持本地相对路径的图片,只支持网络图片和base64图片
建议使用base64图片,图片文件最好不要太大。
每个页面都有的page标签
1 2 3
| page { background-color: #f1f1f1; }
|
播放历史与本地存储
方案一:播放历史存储在数据库当中,这样在不同设备访问都可查看播放历史。读取速度相对较慢
方案二:播放历史存储在本地,仅当前设备可查看播放历史。读取速度较快
本项目采用本地存储:
使用openid作为本地存储的key,播放历史存入value
在app.js中获取openid,即打开小程序就获取openid。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| onLaunch: function () { this.getOpenid() }, getOpenid() { wx.cloud.callFunction({ name: 'login' }).then((res) => { const openid = res.result.openid this.globalData.openid = openid if (wx.getStorageSync(openid) == '') { wx.setStorageSync(openid, []) } }) }
|
歌曲播放时
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| savePlayHistory() { const currentSong = musiclist[nowPlayingIndex] const openid = app.globalData.openid const playHistory = wx.getStorageSync(openid)
for (let i = 0, len = playHistory.length; i < len; i++) { if (playHistory[i].id === currentSong.id) { playHistory.splice(i, 1) break } }
playHistory.unshift(currentSong) wx.setStorage({ key: openid, data: playHistory })
},
|
播放历史页面获取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| onLoad: function (options) {
const openid = app.globalData.openid const playHistory = wx.getStorageSync(openid)
if (playHistory.length !== 0) { this.setData({ playHistory }) wx.setStorage({ key: 'musiclist', data: playHistory, }) }
},
|
我的发现
代码分别演示了从云函数和小程序端获取数据,从小程序端获取数据享有权限管理的能力,不需要传openid。
小程序码
获取小程序码
本项目演示使用接口 B:适用于需要的码数量极多的业务场景 云调用 的方式。
步骤:
1 2 3 4 5 6 7
| { "permissions":{ "openapi":[ "wxacode.getUnlimited" ] } }
|
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
| exports.main = async (event, context) => { const wxContext = cloud.getWXContext()
const result = await cloud.openapi.wxacode.getUnlimited({ scene: wxContext.OPENID, })
const upload = await cloud.uploadFile({ cloudPath: 'qrcode/qrcode' + Date.now() + Math.random() + '.png', fileContent: result.buffer })
return upload.fileID }
|
判断是从扫码小程序码进入,以及参数获取
1 2 3 4 5
|
onLoad: function (options) { console.log(options.scene) }
|
版本更新检测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| onLaunch: function(options) { this.checkUpate() }, checkUpate(){ const updateManager = wx.getUpdateManager() updateManager.onCheckForUpdate((res)=>{ if (res.hasUpdate){ updateManager.onUpdateReady(()=>{ wx.showModal({ title: '更新提示', content: '新版本已经准备好,是否重启应用', success(res){ if(res.confirm){ updateManager.applyUpdate() } } }) }) } }) },
|
性能优化
官网文档优化建议
使用开发者工具的调试器,Audits进行评分,然后根据提示针对项目进行优化。
场景值scene的作用与应用场景
场景值
场景值用来描述用户进入小程序的路径。完整场景值的含义请查看场景值列表。
可根据不同场景进入实现不同业务处理,比如一个点餐小程序,店家内贴了小程序码,用户通过扫码进入,可立即进入点餐页面,等等
在app.js中的onLaunch(options) 、onShow(options),options包含scene场景值
开发者工具中,切后台,可模拟进入场景。
小程序的”SEO”—页面收录sitemap
在app.js的同级目录下有sitemap.json文件,用于配置收录规则
stiemap配置
作用:
使小程序搜索可根据小程序的内容进行搜索到
使用方法:
1、在微信公众平台,小程序信息 > 页面收录设置 > 打开 (默认是已开启)
2、打开sitemap.json文件,配置收录规则
1 2 3 4 5 6 7
| { "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", "rules": [{ "action": "allow", "page": "*" }] }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| { "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", "rules": [{ "action": "allow", "page":"pages/player/player", "params": ["musicId","index"], "matching":'exact' },{ "action": "disallow", "page":"*", / }] }
|
小程序上线审核流程
微信公众平台,版本管理>把小程序上传为体验版》提交审核》上线
后台管理系统
架构示意图
前端 后台 小程序云开发
vue-admin-template <—通过ajax–> 基于Koa2;HTTP API 或 tcb-admin-node —->云函数、云数据库、云存储
vue-admin-template构建管理系统前端
vue-element-admin 基于element的后台管理系统模板
vue-admin-template 是 vue-element-admin的简化版
使用方法查看官方文档。
Koa2构建管理系统后端
官网: https://koa.bootcss.com/
新建空文件夹wx-music-admin-backend,打开终端:
1 2 3 4 5 6 7 8 9
| npm init -y
npm install koa
type nul > app.js
|
app.js:
1 2 3 4 5 6 7 8 9 10 11
| const Koa = require('koa') const chalk = require('chalk') const app = new Koa()
app.use(async (ctx) => { ctx.body = 'Hello Wolrd' }) const port = 3000 app.listen(port, () => { console.log(chalk.green(`> 服务已开启,访问:http://localhost:${port}`)) })
|
终端:
接口调用凭证 access_token 的缓存与更新
access_token,微信的接口调用凭证,详情:https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/access-token/auth.getAccessToken.html
回到项目wx-music-admin-backend,打开终端:
1 2 3
| npm i request npm i request-promise
|
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
|
const rp = require('request-promise') const fs = require('fs') const path = require('path')
const fileName = path.resolve(__dirname, './access_token.json')
const APPID = 'wxc4e0b2d98063b103' const APPSECRET = 'xxx'
const URL = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${APPID}&secret=${APPSECRET}`
const updateAccessToken = async () => { const resStr = await rp(URL) const res = JSON.parse(resStr)
if (res.access_token) { fs.writeFileSync(fileName, JSON.stringify({ access_token: res.access_token, createTime: new Date() })) } else { await updateAccessToken() } }
const getAccessToken = async () => { try { const readRes = fs.readFileSync(fileName, 'utf8') const readObj = JSON.parse(readRes)
const createTime = new Date(readObj.createTime).getTime() const nowTime = new Date().getTime() if((nowTime - createTime) / 1000 / 60 / 60 >= 2) { await updateAccessToken() await getAccessToken() return } return readObj.access_token
} catch (error) { await updateAccessToken() await getAccessToken() } }
setInterval(async () => { await updateAccessToken() }, (7200 - 300) * 1000)
module.exports = getAccessToken
|
后端代码通过HTTP API 触发云函数获取数据
HTTP API 触发云函数
产生跨域和后端解决跨域问题
管理系统前端向管理系统后端请求数据,产生了跨域问题
管理系统后端,安装
1 2
| // 解决跨域问题的koa包 npm i koa2-cors
|
app.js
1 2 3 4 5
| app.use(cors({ origin: ['http://localhost:9528'], credentials: true }))
|
云数据库的增删改查接口
数据库查询记录
后端获取前端post请求传来的数据
get请求可以直接通过ctx.request.query获取,但是post请求需要安装koa-body
app.js
1 2 3 4 5 6 7
| const koaBody = require('koa-body')
app.use(koaBody({ multipart: true }))
|
接口.js
1 2 3 4 5
| router.post('/updatePlaylist', async (ctx, next) => {
const params = ctx.request.body
})
|
后端获取云存储图片
云存储中上传图片,云数据库中新建图片的集合,并添加数据字段,字段包含云文件的fileid。
后端项目通过调用云数据库的方式获取数据
1 2 3 4 5 6 7
| router.get('/list', async (ctx, next) => { const query = `db.collection('swiper').get()` const res = await callCloudDB(ctx, 'databasequery', query) console.log(res)
})
|
但获取到的数据为fileid,并不能用于显示图片,需要通过微信HTTP API获取云存储的接口来获取图片地址
获取云存储
后端上传图片到云存储
文件上传
来源:本文导入自 xugaoyi/vuepress-theme-vdoing 的 docs/01.前端/40.学习笔记/40.小程序笔记.md。
原作者:xugaoyi。许可证:MIT。