### 彪哥

爱好前端

- JavaScript文章

- 学习笔记

  - 《JavaScript教程》笔记
  - 《JavaScript高级程序设计》笔记
  - 《ES6 教程》笔记
  - 《Vue》笔记
  - 《React》笔记
  - 《TypeScript 从零实现 axios》
  - 《Git》学习笔记
  - TypeScript笔记
  - 小程序笔记
    - 基础课程部分
      - 微信公众平台
      - 小程序文档API
      - 微信开放社区
      - 目录说明
      - 文件说明
      - 阻止事件冒泡
      - 第三方库
      - 云开发
      - 云开发三大基础能力
      - 云函数
      - 云数据库
      - 云存储
      - 云数据库能力
      - 数据类型
      - 操作云数据库
      - 云数据库权限管理
      - 操作云数据库
    - 实战课程部分
      - serverless（无服务）
      - 云开发优势
      - 云开发提供能力
      - appID
      - 云开发项目默认目录结构
      - 云开发环境
      - 开发前的准备
      - project.config.json 文件说明
      - app.json
      - 代码规范
      - 《音乐》页面开发
      - 自定义组件
      - 引入组件
      - 组件传值
      - wx:key 的使用
      - async/await 语法
      - 云函数的使用
      - 数据库操作
      - 查询数据库
      - 云函数调试
      - 定时触发云函数
      - 配置云函数超时时间
      - 上拉加载与下拉刷新
      - 云函数路由优化tcb-router
      - 本地存储（缓存）
      - api设置title
      - 背景播放音
      - createSelectorQuery查询节点信息
      - 组件内的方法
      - 组件生命周期
      - 组件所在页面的生命周期
      - 组件对数据的监听
      - 子组件自定义事件传递给父组件
      - 父组件自定义事件传递给子组件
      - 兄弟组件间传递事件和传值
      - 获取手机信息
      - 滚动组件
      - 全局属性、方法（类似vuex）
      - 消息提示框
    - 《发现》页面
      - 调用组件外部的样式
      - 组件插槽slot
      - 判断用户授权
      - button的开发能力（获取用户信息）1
      - 原生组件
      - 选择上传图片
      - 图片裁剪
      - 获取标签自定义属性data-* （删除图片的实现）
      - 全屏预览图片(点击图片放大预览)
      - 文件上传云存储（发布博客例子）
      - js模块化 （时间格式化）
      - 阻止事件冒泡
      - 返回上一个页面并执行方法
      - 图片懒加载
      - 模糊查询
      - 提升模糊查询的效率 （添加索引，对数据量大的查询效果明显）
      - 小程序端调用云数据库
      - 云数据库权限管理
      - 数据库中1对N关系的三种设计方式
      - 第一种：N的数量较少 几十个以内
      - 第二种：N的数量较多 几十到几百个
      - 第三种：N的数量巨大 几百成千上万个
      - 云调用
      - 模板消息推送
      - 云函数多集合查询数据库
      - 分享功能
      - 不同场景获取用户信息的方式
      - 场景一：只想在界面上显示自己的昵称和头像
      - 场景二：在JS中获取用户信息
      - 场景三：获取openId
    - 《我的》页面
      - 导航页面链接跳转
      - 背景图片
      - 每个页面都有的page标签
      - 播放历史与本地存储
      - 我的发现
      - 小程序码
      - 判断是从扫码小程序码进入，以及参数获取
      - 版本更新检测
      - 性能优化
      - 场景值scene的作用与应用场景
      - 小程序的"SEO"---页面收录sitemap
      - 小程序上线审核流程
    - 后台管理系统
      - 架构示意图
      - vue-admin-template构建管理系统前端
      - Koa2构建管理系统后端
      - 接口调用凭证 access_token 的缓存与更新
      - 后端代码通过HTTP API 触发云函数获取数据
      - 产生跨域和后端解决跨域问题
      - 云数据库的增删改查接口
      - 后端获取前端post请求传来的数据
      - 后端获取云存储图片
      - 后端上传图片到云存储
  - JS设计模式总结笔记

- 前端
- 学习笔记

heBody
2019-12-25
目录

# 小程序笔记

# # 小程序笔记

## # 基础课程部分

#### # 微信公众平台

https://mp.weixin.qq.com/
(opens new window)

注册时可选择类型：订阅号、服务号、小程序、企业微信

每个邮箱仅能注册一个小程序。

个人类型小程序：无法使用微信支付、无法使用卡包功能

##### # 小程序文档API

小程序开发文档
(opens new window)

##### # 微信开放社区

微信开发社区
(opens new window)

#### # 目录说明

默认目录

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
(opens new window)

wxml 相关介绍

wxmlAPI
(opens new window)

    <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)
      }
    })
    ...

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

wxss 相关介绍

wxssAPI
(opens new window)

尺寸单位：rpx，根据屏幕宽度自适应。

引入外部wxss：@import ’...‘

js相关介绍

WXS（WeiXin Script）是小程序的一套脚本语言

wxsAPI
(opens new window)

绑定点击事件

    <button bindtap=“onTapHandler”>点我+1</button>
    <view>{{count}}</view>

1
2

    Page({
      data: {
        count: 0
      },
      onTapHandler: function() {
      	this.setData({
      		count: this.data.count++
      	})
      }
    })

1
2
3
4
5
6
7
8
9
10

##### # 阻止事件冒泡

把绑定方式 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 空

#### # 操作云数据库

小程序控制（读写数据库受权限限制）

云函数控制（拥有所有读写数据库的权限）

控制台控制（拥有所有读写数据库的权限）

#### # 云数据库权限管理

仅创建者可写，所有人可读 （适合于文章）

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

仅管理端可写，所有人可读（适用于商品信息）

仅管理端可读写（适用于后台敏感数据）

#### # 操作云数据库

    //初始化数据库
    const db = wx.cloud.database() // 小程序端初始化数据库，如果在云函数端不需要加wx

    //切换环境(开发环境/生产环境)
    const testDB = wx.cloud.database({
    	env: 'test'
    })

1
2
3
4
5
6
7

## # 实战课程部分

#### # 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目录下生成相应的目录和文件

设置底部导航按钮：

    "tabBar": {
        "color": "#474747", // 文字颜色
        "selectedColor": "#d43c43", // 文字选中颜色
        "list": [{ // 按钮列表，2-5项
          "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"
        }]
      }

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

> 图标来自于 https://www.iconfont.cn
> (opens new window)

> 阿里巴巴图标库，包含矢量图标、字体图标、字体等

#### # 代码规范

很多公司借鉴的代码规范：https://github.com/airbnb/javascript
(opens new window)

#### # 《音乐》页面开发

     <!-- 轮播图组件 参数：indicator-dots 小圆点，autoplay 自动播放, interval 间隔时间，duration 动画时长 -->
    <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>

1
2
3
4
5
6
7
8
9

#### # 自定义组件

创建组件

创建目录 components > 组件目录名称 > 右键 `新建Component`

##### # 引入组件

在page的json文件中：

    {
      "usingComponents": {
        "x-playlist":"/components/playlist/playlist"
      }
    }

1
2
3
4
5

在page的wxml中：

    <x-playlist> </x-playlist>

1

页面引入组件以及组件内部在引用子组件的方法是一样的，同样需要设置json文件。

##### # 组件传值

父组件中：在引入组件的时候自定义属性名称，并把数据传入子组件

    <!-- 参数：playlist 自定义名称，传入组件的数据 -->
    <x-playlist playlist="{{传入的数据}}"></x-playlist>

1
2

子组件中： 子组件的js文件：

      /**
       * 组件的属性列表
       */
      properties: {
        playlist:{ // 接收父组件传输的数据
          type: Object // 数据类型
        }
       },

     //子组件的wxml文件可直接引入数据{{playlist}}

1
2
3
4
5
6
7
8
9
10

#### # wx:key 的使用

key的值不建议使用index，因为当数据发生变化会dom结构产生变化时，使用index的地方不会随之变化。

可以使用数据内部每项不一样的一个数值，如id

    <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">
        <!-- 参数：playlist 自定义名称，传入组件的数据 -->
        <x-playlist playlist="{{item}}"></x-playlist>
      </block>
    </view>

1
2
3
4
5
6
7
8
9
10
11
12
13

#### # async/await 语法

目前，在云函数里，由于 Node 版本最低是 8.9，因此是天然支持 async/await 语法的。而在小程序端则不然。在微信开发者工具里，以及 Android 端手机（浏览器内核是 QQ浏览器的 X5），async/await是天然支持的，但 iOS 端手机在较低版本则不支持，因此需要引入额外的 文件。

可把这个 runtime.js
(opens new window) 文件引用到有使用 async/await 的文件当中。

    // 注意，必须命名为 regeneratorRuntime
    import regeneratorRuntime from '../../utils/runtime.js'

1
2

#### # 云函数的使用

cloudfunctions目录 右键 `新建 Node.js 云函数` > 输入目录名 `getPlaylist`

在云函数中向第三方服务器发送请求要依赖第三方库

安装依赖包

云函数目录 `getPlaylist` 右键 `在终端打开` 打开命令行 输入命令：

    npm install --save request
    npm install --save request-promise

1
2

github request-promise：https://github.com/request/request-promise
(opens new window)

然后写相应代码

    // 云函数入口文件
    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)
    }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

写完代码，云函数目录 `getPlaylist` 右键 `上传并部署：云端安装依赖（不上传node_modules）` 进行上传部署代码到云端，等待上传成功，打开云开发控制台即可看到已经上传的云函数，并可对云函数进行测试。

##### # 数据库操作

数据库> 创建集合 > playlist

    // 云函数入口文件
    const cloud = require('wx-server-sdk')

    cloud.init()

    const db = cloud.database() // 初始化数据库,如果在小程序端初始化数据库需要在前面加wx.

    const rp = require('request-promise') // 需安装依赖包

    const URL = 'http://musicapi.xiecheng.live/personalized' // 第三方服务器地址（老师从网易云获取的数据部署在其服务器，每天的数据会更新）

    const playlistCollection = db.collection('playlist') // 获取到数据库playlist集合

    const MAX_LIMIT = 10 // 定义常量，获取数据库条数最大的限制

    // 云函数入口函数
    exports.main = async (event, context) => {
      /**
       * 注：
       * - 关于数据库的操作都是异步操作，都需添加await关键字
       * - console.log 打印在云开发控制台 云函数测试内查看
       * - 单次获取数据库数据有条数限制，云函数端最多获取100条，小程序端最多获取20条
       */

      // const list = await playlistCollection.get() // 获取数据库集合的数据 （因为有条数限制，不直接用此方法）

      // 突破条数限制 （为了读取到全部数据然后与第三方服务器获取的数据进行对比去重）
      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() // 从第 skip 条开始取，最多取 limit 条数据
        tasks.push(promise)
      }
      let list = {
        data: []
      }
      if (tasks.length > 0) {
        list = (await Promise.all(tasks)).reduce((acc, cur) => { // reduce数组方法 累积拼接
          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(), // 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
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 TcbRouter = require('tcb-router')
    const db = cloud.database() // 初始化数据库
    const blogCollection = db.collection('blog') // 博客的数据库集合

    // 云函数入口函数
    exports.main = async (event, context) => {
      const app = new TcbRouter({ event }) // 初始化TcbRouter

      app.router('list', async (ctx, next) => {
        // skip 从第几条开始查，limit 查几条数据，orderBy(排序字段，排序方式) 排序，排序方式desc降序/asc升序
        ctx.body =  await blogCollection.skip(event.start).limit(event.count)
        .orderBy('createTime', 'desc').get().then((res) => {
          return res.data
        })

      })

      return app.serve() // 必需返回
    }

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

##### # 云函数调试

云控制台中可会云函数进行云端测试

在小程序调用云函数后，可查看云函数日志

#### # 定时触发云函数

如果云函数需要定时 / 定期执行，也就是定时触发，我们可以使用云函数定时触发器。配置了定时触发器的云函数，会在相应时间点被自动触发，函数的返回结果不会返回给调用方

云函数目录下新建 config.json

API
(opens new window)

    {
      "triggers": [
        {
          "name": "myTriggers",
          "type": "timer",
          "config":"0 0 10,14,16,20 * * * *" //表示每天的10点、14点、16点、20点触发一次
        }
      ]
    }

1
2
3
4
5
6
7
8
9

编辑好触发器之后，要在云函数目录 > 右键 > `上传触发器`

#### # 配置云函数超时时间

当云函数比较复杂的时候，默认的超时时间3秒可能不能够满足需求，可以适当的设置更为合理的时间

云开发控制台 > 云函数 > 配置 > 超时时间

#### # 上拉加载与下拉刷新

    page页面json中：
    "enablePullDownRefresh": true

    page页面js中有这两个函数：

     /**
       * 页面相关事件处理函数--监听用户下拉动作
       */
      onPullDownRefresh: function() {
    	 this.setData({
          playlist: []
        })
        this._getPlaylist()
      },

      /**
       * 页面上拉触底事件的处理函数
       */
      onReachBottom: function() {
        this._getPlaylist()
      },

     下拉刷新请求完数据后
     wx.stopPullDownRefresh() // 停止下拉刷新动画

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

#### # 云函数路由优化tcb-router

一个用户在一个云环境只能创建50个云函数

假如小程序非常复杂，50个云函数不能够满足业务需求怎么办？

相似的请求归类到同一个云函数处理

tcb-router是一个koa风格的云函数路由库

通俗理解就是可以把很多个接口归类到同一个云函数内。

github-tcb-router: https://github.com/TencentCloudBase/tcb-router
(opens new window)

koa洋葱模型...

安装：

    在使用到tcb-router的云函数目录下打开命令行，输入命令进行安装
    npm install --save tcb-router

1
2

    // 云函数的 index.js
    const TcbRouter = require('router'); // 必需

    exports.main = (event, context) => {
        const app = new TcbRouter({ event });// 必需

        // app.use 表示该中间件会适用于所有的路由(全局中间件) 非必需
        app.use(async (ctx, next) => { // 这个中间件表示所有路由都会调用到，而路由中间件为单独调用
            ctx.data = {}; // 获取要传给小程序端的数据
            ctx.data.openId = event.userInfo.openId // 这里获取到的openId将分布到所有路由
            await next(); // 执行下一中间件
        });

        // 路由为数组表示，该中间件适用于 user 和 timer 两个路由
        app.router(['user', 'timer'], async (ctx, next) => {
            ctx.data.company = 'Tencent'; // 这里获取到的数据将分布到 user 和 timer 两个路由
            await next(); // 执行下一中间件
        });

        // 路由为字符串，该中间件只适用于 user 路由
        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 返回数据到小程序端
            ctx.body = { code: 0, data: ctx.data};  // 要传给小程序端的数据
        });

        // 路由为字符串，该中间件只适用于 timer 路由
        app.router('timer', async (ctx, next) => {
            ctx.data.name = 'flytam';
            await next(); // 执行下一中间件
        }, async (ctx, next) => {
            ctx.data.sex = await new Promise(resolve => {
            // 等待500ms，再执行下一中间件
            setTimeout(() => {
                resolve('male');
            }, 500);
            });
            await next(); // 执行下一中间件
        }, async (ctx)=>  {
            ctx.data.city = 'Taishan';

            // ctx.body 返回数据到小程序端
            ctx.body = { code: 0, data: ctx.data };
        });

        return app.serve(); // 必需

    }

    小程序端：

    // 调用名为 router 的云函数，路由名为 user
    wx.cloud.callFunction({
        // 要调用的云函数名称
        name: "router",
        // 传递给云函数的参数
        data: {
            $url: "user", // 要调用的路由的路径，传入准确路径或者通配符*
            other: "xxx"
        }
    }).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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

上面tcb-router代码会按照洋葱模型执行，即先从上往下逐个进入中间件，再从下往上逐个退出中间件。

#### # 本地存储（缓存）

    // 存储：
    wx.setStorageSync(key, data) // 同步存储（存储成功再继续下一步操作）
    wx.setStorage(key, data) // 异步存储（即使存储没成功也会执行下一步代码）、

    // 读取：
    wx.getStorageSync(key) // 同步 （读取到数据在进行下一步操作）
    wx.setStorage(key) // 异步

1
2
3
4
5
6
7
8

#### # api设置title

    wx.setNavigationBarTitle({
          title: '',
    })

1
2
3

#### # 背景播放音

BackgroundAudioManager
(opens new window) 全局唯一的背景音频管理器

    // 需要在app.json配置，才能使用后台音乐播放的能力

    "requiredBackgroundModes": ["audio", "location"]

1
2
3

    // 获取全局唯一的背景音频管理器
    const backgroundAudioManager = wx.getBackgroundAudioManager()

    backgroundAudioManager.src = 音频链接
    backgroundAudioManager.title = 音频标题

1
2
3
4
5
6
7

#### # createSelectorQuery查询节点信息

createSelectorQuery
(opens new window) 小程序的方法，用于查询节点等操作

    const query = wx.createSelectorQuery()
    query.select('#the-id').boundingClientRect() // 节点的布局信息
    query.selectViewport().scrollOffset()
    query.exec(function(res){
      res[0].top       // #the-id节点的上边界坐标
      res[1].scrollTop // 显示区域的竖直滚动位置
    })

1
2
3
4
5
6
7

#### # 组件内的方法

Component(Object object)
(opens new window)

#### # 组件生命周期

lifetimes
(opens new window)

    // 生命周期
    lifetimes: {
        ready() { // 在组件在视图层布局完成后执行
       	 ...
        }
    },

1
2
3
4
5
6

##### # 组件所在页面的生命周期

    Component({
      pageLifetimes: {
        show: function() {
          // 页面被展示
        },
        hide: function() {
          // 页面被隐藏
        },
        resize: function(size) {
          // 页面尺寸变化
        }
      }
    })

1
2
3
4
5
6
7
8
9
10
11
12
13

#### # 组件对数据的监听

observers
(opens new window)

    observers: { // 对数据的监听(数据初次加载完成也会执行)
        监听的数据对象(newData){
          console.log(newData)
        }
    },

1
2
3
4
5

#### # 子组件自定义事件传递给父组件

    子组件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

#### # 父组件自定义事件传递给子组件

    父组件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

#### # 兄弟组件间传递事件和传值

    例子：子组件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)  // 接收父组件传入的值
    	}
    }

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

#### # 获取手机信息

wx.getSystemInfo(Object object)
(opens new window)

    wx.getSystemInfo({
    	success(res){
    		console.log(res) //手机信息
    	}
    })

1
2
3
4
5

#### # 滚动组件

scroll-view
(opens new window)

    <scroll-view scroll-y scroll-top="{{scrollTop}}" scroll-with-animation="true">
    </scroll-view>

1
2

#### # 全局属性、方法（类似vuex）

    在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')

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

#### # 消息提示框

showToast
(opens new window)

    wx.showToast({
      title: '成功',
      icon: 'success', //图标： success 成功、loading 加载中、none 无
      duration: 2000
    })

1
2
3
4
5

## # 《发现》页面

#### # 调用组件外部的样式

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

方法一：

    父组件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
7
8
9
10
11
12
13
14
15
16
17

方法二：

消除样式隔离
(opens new window)

    组件内:
    Component({
      options: {
        styleIsolation: 'apply-shared'
      }
    })

1
2
3
4
5
6

#### # 组件插槽slot

单个插槽

    父组件调用传入插槽内容:
    <组件标签>
        <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

如果需要实现多个插槽

    父组件调用传入插槽内容:
    <组件标签>
        <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
19
20
21
22
23
24
25
26

#### # 判断用户授权

授权
(opens new window)

    // 判断用户是否授权
        wx.getSetting({
          success: (res) => { // 这里使用箭头函数可改变内部this指向为外部的this
            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

#### # button的开发能力（获取用户信息）1

    <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
12
13
14
15
16
17
18
19
20
21
22
23

#### # 原生组件

原生组件
(opens new window)

    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
11
12

#### # 选择上传图片

上传图片
(opens new window)

    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
3
4
5
6
7
8
9

#### # 图片裁剪

图片裁剪
(opens new window)

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

1
2

#### # 获取标签自定义属性data-* （删除图片的实现）

    <!-- 显示图片 -->
        <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
8
9
10
11
12
13
14
15
16
17
18
19

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

全屏预览图片
(opens new window)

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

1
2
3
4
5
6
7

#### # 文件上传云存储（发布博客例子）

文件上传云存储
(opens new window)

     //  结合'发布'的例子：
      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'
          })
        })
      },

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

#### # js模块化 （时间格式化）

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

    // 时间格式化 模块封装
    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
    }

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

在组件引入js模块

    import formatTime from '../../utils/formatTime.js'

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

1
2
3
4

#### # 阻止事件冒泡

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

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

API
(opens new window)

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

1
2
3
4
5

#### # 图片懒加载

API
(opens new window)

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

    .img {
      background: #eee;
    }

1
2
3
4
5
6

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

#### # 模糊查询

    // 获取博客列表
      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
        })

      })

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

#### # 提升模糊查询的效率 （添加索引，对数据量大的查询效果明显）

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

#### # 小程序端调用云数据库

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

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

    // 小程序端调用云数据库示例
        const db = wx.cloud.database() // 初始化数据库
        db.collection('blog').orderBy('createTime','deac').get().then((res) => {
          console.log(res)
        })

1
2
3
4
5

#### # 云数据库权限管理

注意：云控制台和服务端（云函数）始终有所有数据读写权限，

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

- 仅创建者可写，所有人可读 （适合于文章）

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

- 仅管理端可写，所有人可读（适用于商品信息）

- 仅管理端可读写（适用于后台敏感数据）

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

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

1 条记录存储 N 个子数据

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

    [
    	{
    		id:...
    		img:[
    		'...', '...', '...', '...', '...', '...', '...', '...', '...'
    		]
    	}
    ]

1
2
3
4
5
6
7
8

##### # 第二种：N的数量较多 几十到几百个

1 存储 每个N的 id

可分两个数据库集合，

一个为 '目录' 集合，存放 '详情' 集合下的每条数据的 id 目录

一个为 '详情' 集合，每条数据对应一个单独的 id 和 详细数据

    目录集合：
    [
    	{
    		'id':"11",
    		'name': '产品1',
    		'xqs': ['111','222','333', ... ]  // 存放 详情集合 中的每条数据 id
    	}
    ]

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

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

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

每个 N 都存储 1 的 id

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

    一条新浪博客：
    [{
    	'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
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

#### # 云调用

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

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

#### # 模板消息推送

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

    <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>

1
2
3
4
5

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

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

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

config.json ：

    {
      "permissions": {
        "openapi": [
          "templateMessage.send"
        ]
      }
    }

1
2
3
4
5
6
7

云函数设置消息推送：

    // 云函数入口函数
    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
    }

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

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

     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

#### # 云函数多集合查询数据库

    // 博客详情(博客内容、评论)
      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
        }
      })

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

#### # 分享功能

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

    <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>

1
2
3
4
5

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

    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: '' // 自定义图片，不支持云存储的图片
        }
      }

1
2
3
4
5
6
7
8
9
10
11

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

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

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

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

open-data
(opens new window)

    <open-data type="userAvatarUrl"></open-data>
    <open-data type="userNickName"></open-data>
    ...

1
2
3

##### # 场景二：在JS中获取用户信息

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

wx.getUserInfo
(opens new window)

    wx.getUserInfo({
          success: (res) => {
            console.log(res)
          }
        })

1
2
3
4
5

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

    // 判断用户是否授权
          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')
          }
        }

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

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

##### # 场景三：获取openId

获取openId不需要用户授权

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

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

步骤：

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

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

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

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

2、云开发方式获取openId

云函数login中

    // 获取 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

    普通按钮
    <button bindtap="getOpenid">获取openid</button>

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

1
2
3
4
5
6
7
8
9
10

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

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

## # 《我的》页面

json文件

    "navigationBarTitleText": "我的",
      "disableScroll": true  // 使页面无法滚动

1
2

#### # 导航页面链接跳转

navigator
(opens new window)

#### # 背景图片

wxss背景图片不支持本地相对路径的图片，只支持网络图片和base64图片

建议使用base64图片，图片文件最好不要太大。

#### # 每个页面都有的page标签

    page {
      background-color: #f1f1f1;
    }

1
2
3

#### # 播放历史与本地存储

方案一：播放历史存储在数据库当中，这样在不同设备访问都可查看播放历史。读取速度相对较慢

方案二：播放历史存储在本地，仅当前设备可查看播放历史。读取速度较快

本项目采用本地存储：

使用openid作为本地存储的key，播放历史存入value

在app.js中获取openid，即打开小程序就获取openid。

    // app.js
    onLaunch: function () {
    	this.getOpenid() // 获取openid并存储
    },
    getOpenid() { // 获取openid并存储
        wx.cloud.callFunction({
          name: 'login'
        }).then((res) => {
          const openid = res.result.openid
          this.globalData.openid = openid // 保存到全局变量
          if (wx.getStorageSync(openid) == '') { // 该用户从未打开过小程序，未存储过openid在本地
            wx.setStorageSync(openid, []) // 存储openid到本地
          }
        })
      }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

歌曲播放时

    // 保存播放历史到本地存储
      savePlayHistory() {
        const currentSong = musiclist[nowPlayingIndex] // 当前播放歌曲
        const openid = app.globalData.openid // 从全局属性获取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
17
18
19
20

播放历史页面获取

    onLoad: function (options) {

        const openid = app.globalData.openid //从全局属性获取openid
        const playHistory = wx.getStorageSync(openid) // 读取本地播放历史数据

        if (playHistory.length !== 0) { // 有播放历史
          this.setData({
            playHistory
          })
          wx.setStorage({ // storage里把musiclist（播放列表）的内容换成播放历史的列表
            key: 'musiclist',
            data: playHistory,
          })
        }

      },

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

#### # 我的发现

代码分别演示了从云函数和小程序端获取数据，从小程序端获取数据享有权限管理的能力，不需要传openid。

#### # 小程序码

获取小程序码
(opens new window)

本项目演示使用接口 B：适用于需要的码数量极多的业务场景
(opens new window) `云调用` 的方式。

步骤：

- 创建云函数 gteQRCode

- gteQRCode云函数下创建config.json配置权限，代码如下：

    {
      "permissions":{
        "openapi":[
          "wxacode.getUnlimited"
        ]
      }
    }

1
2
3
4
5
6
7

    // 云函数入口函数
    exports.main = async (event, context) => {
      const wxContext = cloud.getWXContext()

      const result = await cloud.openapi.wxacode.getUnlimited({
        scene: wxContext.OPENID, // 链接参数 不一定传openid，可传其他任意数据，然后通过此数据，在别人扫码进入时就可用于判断
        // page: "pages/blog/blog" // 默认进入主页
        // lineColor: { // 线条颜色
        //   'r': 211,
        //   'g': 60,
        //   'b': 57
        // },
        // isHyaline: true // 是否透明
      })

      // result为二进制数据, 先上传到云存储

      // 上传云存储
      const upload = await cloud.uploadFile({
        cloudPath: 'qrcode/qrcode' + Date.now() + Math.random() + '.png',
        fileContent: result.buffer
      })

      return upload.fileID
    }

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

##### # 判断是从扫码小程序码进入，以及参数获取

    // 在从小程序码进入的页面js，onLoad方法中，

    onLoad: function (options) {
    	console.log(options.scene) // 获取到小程序码进入的参数
    }

1
2
3
4
5

### # 版本更新检测

    // app.js
    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()
                  }
                }
              })
            })
          }
        })
      },

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

### # 性能优化

官网文档优化建议
(opens new window)

使用开发者工具的调试器，Audits进行评分，然后根据提示针对项目进行优化。

### # 场景值scene的作用与应用场景

场景值
(opens new window)

场景值用来描述用户进入小程序的路径。完整场景值的含义请查看场景值列表
(opens new window)。

可根据不同场景进入实现不同业务处理，比如一个点餐小程序，店家内贴了小程序码，用户通过扫码进入，可立即进入点餐页面，等等

在app.js中的onLaunch(options) 、onShow(options)，options包含scene场景值

开发者工具中，切后台，可模拟进入场景。

### # 小程序的"SEO"---页面收录sitemap

在app.js的同级目录下有sitemap.json文件，用于配置收录规则

stiemap配置
(opens new window)

作用：

使小程序搜索可根据小程序的内容进行搜索到

使用方法：

1、在微信公众平台，小程序信息 > 页面收录设置 > 打开 (默认是已开启)

2、打开sitemap.json文件，配置收录规则

    {
      "desc": "关于本文件的更多信息，请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
      "rules": [{ // 收录规则
      "action": "allow",// 是否被收录，allow允许被收录，disallow不允许
      "page": "*" // *星号表示所有页面都被收录
      }]
    }

1
2
3
4
5
6
7

    {
      "desc": "关于本文件的更多信息，请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
      "rules": [{ // 收录规则，可添加多条
      "action": "allow", // 是否被收录
      "page":"pages/player/player",  // 页面
      "params": ["musicId","index"], // 链接的动态参数
       "matching":'exact' // 表示params的参数是否要准确的匹配
      }，{
      "action": "disallow", // 是否被收录
      "page":"*",  /
      }]
    }

    // 上面配置规则表示除了player页面被收录外，其他页面不被收录

1
2
3
4
5
6
7
8
9
10
11
12
13
14

### # 小程序上线审核流程

微信公众平台，版本管理>把小程序上传为体验版》提交审核》上线

## # 后台管理系统

#### # 架构示意图

​ 前端 后台 小程序云开发

vue-admin-template <---通过ajax--> 基于Koa2；HTTP API 或 tcb-admin-node ---->云函数、云数据库、云存储

​

#### # vue-admin-template构建管理系统前端

vue-element-admin
(opens new window) 基于element的后台管理系统模板

vue-admin-template
(opens new window) 是 vue-element-admin
(opens new window)的简化版

使用方法查看官方文档。

#### # Koa2构建管理系统后端

官网： https://koa.bootcss.com/
(opens new window)

新建空文件夹wx-music-admin-backend，打开终端：

    # 生成package.json文件，-y 表示默认的配置
    npm init -y

    # 安装koa
    npm install koa

    # 新建app.js文件 (win10系统命令)，作为项目入口文件
    type nul > app.js

1
2
3
4
5
6
7
8
9

app.js:

    const Koa = require('koa')
    const chalk = require('chalk') // 使console.log打印文字有颜色的插件，需: npm i 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}`))
    })

1
2
3
4
5
6
7
8
9
10
11

终端：

    # node启动项目
    node app.js

    # 访问：http://localhost:3000

1
2
3
4

#### # 接口调用凭证 access_token 的缓存与更新

access_token，微信的接口调用凭证，详情：https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/access-token/auth.getAccessToken.html
(opens new window)

回到项目wx-music-admin-backend，打开终端：

    # HTTP 请求 插件
    npm i request
    npm i request-promise

1
2
3

    /**
     * 获取微信接口调用凭证
     * 详情：https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/access-token/auth.getAccessToken.html
     */

    const rp = require('request-promise') // node发送http请求的插件
    const fs = require('fs') // node文件模块
    const path = require('path') // node 路径模块

    //fileName = __dirname 当前文件所在目录的绝对路径, 加上 './access_token.json'
    const fileName = path.resolve(__dirname, './access_token.json')

    // 这两个参数的获取：微信公众平台>开发>开发设置
    const APPID = 'wxc4e0b2d98063b103'
    const APPSECRET = 'xxx' //小程序密钥，注意保密!

    // 微信 access_token 请求地址
    const URL = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${APPID}&secret=${APPSECRET}`

    // 发送请求获取AccessToken
    const updateAccessToken = async () => {
      const resStr = await rp(URL)
      const res = JSON.parse(resStr)

      if (res.access_token) {
        // node写文件,参数：1 文件路径，2 文件内容, 首次写文件为新建，往后为覆盖
        fs.writeFileSync(fileName, JSON.stringify({
          access_token: res.access_token,
          createTime: new Date()
        }))
      } else { // 如获取不到，再次获取
        await updateAccessToken()
      }
    }

    // 读取access_token
    const getAccessToken = async () => {
      try {
         // node读取文件,参数：1 读取的文件，2 字符集
        const readRes = fs.readFileSync(fileName, 'utf8')
        const readObj = JSON.parse(readRes)

        // 如果服务器宕机导致setInterval无法定时更新，这里需要再次判断access_token的有效性
        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()
      }
    }

    // access_token有效期为2个小时，定时更新
    setInterval(async () => {
      await updateAccessToken()
    }, (7200 - 300) * 1000)

    module.exports = getAccessToken

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

#### # 后端代码通过HTTP API 触发云函数获取数据

HTTP API 触发云函数
(opens new window)

#### # 产生跨域和后端解决跨域问题

管理系统前端向管理系统后端请求数据，产生了跨域问题

      // 产生跨域的几种情况
      // http://www.a.com  https://www.a.com 协议不同
      // http://www.a.com  http://www.b.com 域名不同
      // http://www.a.com  http://news.a.com 主域与子域不同
      // http://www.a.com:8080  http://www.a.com:3000 端口不同

    // 解决跨域的几种方法
    // jsonp
    // iframe
    // postMessage跨域
    // 跨域资源共享（CORS）

1
2
3
4
5
6
7
8
9
10
11

管理系统后端，安装

    // 解决跨域问题的koa包
    npm i koa2-cors

1
2

app.js

    //处理跨域
    app.use(cors({
      origin: ['http://localhost:9528'], // 允许访问本服务的域
      credentials: true
    }))

1
2
3
4
5

#### # 云数据库的增删改查接口

数据库查询记录
(opens new window)

#### # 后端获取前端post请求传来的数据

get请求可以直接通过ctx.request.query获取，但是post请求需要安装koa-body

    npm i koa-body

1

app.js

    const koaBody = require('koa-body') // 对post请求前端传来的数据的获取，需要此依赖

    // 接收post参数解析
    app.use(koaBody({
      multipart: true
    }))

1
2
3
4
5
6
7

接口.js

    router.post('/updatePlaylist', async (ctx, next) => {

      const params = ctx.request.body // post请求获取前端传来的数据，需安装和配置koa-body

    })

1
2
3
4
5

#### # 后端获取云存储图片

云存储中上传图片，云数据库中新建图片的集合，并添加数据字段，字段包含云文件的fileid。

后端项目通过调用云数据库的方式获取数据

    router.get('/list', async (ctx, next) => {
      // 接口中读取数据库默认最多10条数据
      const query = `db.collection('swiper').get()`
      const res = await callCloudDB(ctx, 'databasequery', query)
      console.log(res)

    })

1
2
3
4
5
6
7

但获取到的数据为fileid，并不能用于显示图片，需要通过微信HTTP API获取云存储的接口来获取图片地址

获取云存储
(opens new window)

#### # 后端上传图片到云存储

文件上传
(opens new window)

编辑
(opens new window)
#小程序
上次更新: 2022/12/14, 19:36:42
TypeScript笔记
JS设计模式总结笔记

← TypeScript笔记 JS设计模式总结笔记→

最近更新

  01
Git修改分支名
08-11

  02
CSS给table的tbody添加滚动条
06-29

  03
我做了一个手写春联小网页，祝大家虎年暴富 原创
01-28

更多文章>

Copyright © 2016-2022 Hesb | MIT License

- 跟随系统
- 浅色模式
- 深色模式
- 阅读模式