# 你真的需要微信小程序吗?

首先,在入坑微信小程序之前,你需要想清楚这个问题。

微信小程序的优点有:

  • 只需要写前端(Web App 一般需要前、后端),开发周期短
  • 跨 Android/iOS 平台
  • 有成熟的 WeUI 框架和控件可以使用,并且微信原生 API 非常多,官方文档为中文且很详尽
  • 能够轻松做到 Serverless 云开发
  • 云服务的各项服务都有免费额度,流量不大的开发者能够白嫖

为数不多但非常致命的缺点是:

  • 需要严格审核,甚至于某些功能(如任意用户均可上传的留言板/公开相册/云盘等)无法上架小程序
  • 微信小程序 API 随时可能变化,而小程序必须使用最新的 API(而不像 Pypinpm 等可以使用指定版本的插件),可能导致原来的程序完全无法工作。如 wx.getUserInfo() 函数改版后,在用户未授权过的情况下调用此接口,将不再出现授权弹窗,会直接进入 fail 回调(详见《公告》 (opens new window))。

如图

# 常用文档链接

因为太详尽了,因此本博文很多部分都是直接复制教程。

# 文件结构和页面组成

# 文件结构

在开发者工具的编辑器里可以看到小程序源文件的根目录下有 app.jsapp.jsonapp.wxss

  • app.json:小程序的公共设置,可以对小程序进行全局配置,决定页面文件的路径、窗口表现、设置多 tab 等;
  • app.wxss:小程序的公共样式表,可以配置整个小程序的文字的字体、颜色、背景,图片的大小等样式;
  • app.js:小程序的逻辑(这个可以先放着,不用管)
  • pages 文件夹:这里存放着小程序的所有页面,展开 pages 文件夹就可以看到有 indexlogs 两个页面文件夹;

# 页面组成

在每一个页面文件夹里都有四个文件,这四个文件的名称都是一样的,它们分别为:

  • json 文件,和上面的 app.json 作用基本相同,只是 app.json 控制的是整个小程序的设置,而页面的 json 文件只控制单个页面的配置(因为有时候全局配置就够用了,所以页面配置有时候是空的);
  • wxml 文件(类似于 xml),小程序的页面结构,文字、图片、音乐、视频、地图、轮播等组件都会放在这里;
  • wxss 文件(类似于 css),小程序的页面样式,和 app.wxss 一样是控制样式,而页面的 wxss 文件是控制单个页面的样式;
  • js 文件,控制小程序页面的逻辑。

# app.json

# json 基本语法

  • 大括号 {} 保存对象;
  • 中括号 []保存数组;
  • 各个数据之间由英文字符逗号 , 隔开;
  • 字段名称(属性名)与值之间用 : 隔开,字段名称在前,字段的取值在后;
  • 字段名称用 "" 给包着;

# app.json 配置

app.json 的配置文档:https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/app.html

默认的 app.json 如下:

{
  "pages":[
    "pages/index/index",
    "pages/logs/logs"
  ],
  "window":{
    "backgroundTextStyle":"light",
    "navigationBarBackgroundColor": "#fff",
    "navigationBarTitleText": "test",
    "navigationBarTextStyle":"black"
  },
  "style": "v2",
  "sitemapLocation": "sitemap.json"
}

window 是配置全局的页面设置,见 https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/app.html。

pages 则是小程序的页面。默认展示第一项(这里是 pages/index/index)的内容。在 pages 中新增一项 pages/home/home,编译后会自动创建 pages/home 文件及页面组成中的四个文件。

# wxml 和 wxss

# wxml

刚才创建的 pages/home/home.wxml 的语法类似于 xml

  • <text>pages/home/home.wxml</text> 被称作组件。
  • <text> 被称为标签。
  • 组件需要被开始标签和闭合标签包含,前面的 <text> 是开始标签,后面的 </text> 是结束标签。
  • <view> 组件是可以嵌套写的,如:
<view>
    <view>
        <view>WXML 模板</view>
        <view>从事过网页编程的人知道,网页编程采用的是 HTML + CSS + JS 这样的组合,其中 HTML 是用来描述当前这个页面的结构,CSS 用来描述页面的样子,JS 通常是用来处理这个页面和用户的交互。</view>
    </view>
</view>

# wxml 模板

WXML 支持模板,即定义好会复用的 WXML 片段,然后在其他地方调用即可。

教程:https://cloudbase.net/community/guides/handbook/tcb09.html 官方文档:https://developers.weixin.qq.com/miniprogram/dev/reference/wxml/import.html

# wxss

wxss 的官方文档:https://developers.weixin.qq.com/miniprogram/dev/framework/view/wxss.html

wxss 美化的知识和 css 相同,css 的参考手册:https://www.w3school.com.cn/cssref/index.asp

和 CSS 相同的知识就放在 CSS 中了。

较 CSS,WXSS 定义了新的单位:rpx。规定 750rpx 是一屏幕宽。如在 iPhone6(宽度为 375px)上,750rpx = 375px

对于全局样式和局部样式(来源 (opens new window)):

定义在 app.wxss 中的样式为全局样式,作用于每一个页面。在 pagewxss 文件中定义的样式为局部样式,只作用在对应的页面,并会覆盖 app.wxss 中相同的选择器。

# 链接和图片

# 链接 navigator

将一个小程序页面链接到另一个小程序页面的组件叫 navigator

我们添加一个页面,路径为 /page/imgshow/imgshow。新的 app.json 部分代码如下:

  "pages":[
    "pages/home/home",
    "pages/index/index",
    "pages/logs/logs",
    "pages/imgshow/imgshow"
  ],

/pages/home/home.wxml 加入以下代码:

<view class="index-link">
  <navigator url="../imgshow/imgshow" class="item-link">让小程序显示图片</navigator>
</view>

此时编译运行,点击让小程序显示图片即可跳转到 /pages/imgshow/imgshow.wxml

但是这里的 让小程序显示图片 并没有以超链接的形式给出,可以修改 wxss 以更改其样式。在 /pages/imgshow/imgshow.wxss 中加入以下代码:

.item-link{
  margin: 20px;
  padding:10px 15px;
  background-color: #4ea6ec;
  color: #fff;
  border-radius: 4px;
}

编译。效果如下:

navigator 组件

点击蓝色部分即可跳转到对应页面。

# 图片 image

对于图片,只需要知道怎么插入图片就行了。其他仅作了解,用到的时候再查。

# 插入图片

先放文档:https://developers.weixin.qq.com/miniprogram/dev/component/image.html

将以下代码复制到 imgshow.wxml

<view class="imglist">
    <image class="imgitem" src="https://hackwork.oss-cn-shanghai.aliyuncs.com/lesson/weapp/4/weapp.jpg"></image>
</view>

图片展示

# 显示模式

其中,image 组件共有 13 种 mode(其中 5 种缩放、8 种裁剪。参见 image 的文档)。如想“宽度不变,高度自动变化,保持原图宽高比不变”,则可以修改代码:

<view class="imglist">
    <image class="imgitem" mode="widthFix" src="https://hackwork.oss-cn-shanghai.aliyuncs.com/lesson/weapp/4/weapp.jpg"></image>
</view>

然后在 imgshow.wxss 给图片添加 wxss,指定宽度为占满屏幕:

.imglist .imgitem{
  width: 100%;
}

效果如下:

缩放模式

# 圆角和阴影

还可以给图片添加圆角和阴影。

.imglist .img{
  border-radius: 8px;
  box-shadow: 5px 8px 30px rgba(53,178,225,0.26);
  /* rgba 中的 alpha 为透明度,取值 0~1 之间,越靠近 0 越透明 */
}

圆角和阴影

# 圆形图片

示例 XML:

<view class="imglist">
    <image class="circle" mode="widthFix" src="https://hackwork.oss-cn-shanghai.aliyuncs.com/lesson/weapp/4/logo.jpg"></image>
</view>

示例 CSS:

.imglist .circle{
  width: 200px;
  height: 200px;
  border-radius: 100%;
}

圆形图片

# 背景

此外,还支持修改文字的背景。参见 CSS

# 卡片风格示例

官方示例,有点好看。代码见网页最后 (opens new window)

示例

# WeUI 框架

WeUI 是一套小程序的 UI 框架,所谓 UI 框架就是一套界面设计方案。

有了组件,我们可以用它来拼接出一个内容丰富的小程序,而有了一个 UI 框架,就能让我们的小程序变得更加美观。

# 体验 WeUI 小程序

这是微信发布的基于 WeUI 的模板小程序。

微信小程序码

代码地址:https://github.com/Tencent/weui-wxss

下载以后将 dist 文件夹下的代码导入 微信开发者工具,运行,即可得到上述效果。

不过,需要注意的是,WeUI 模板小程序中很多元素是使用 view 组件模拟,其效果不如对应的原生组件(如 button searchbar tabbar 等)。因此,使用模板前可以在文档 (opens new window)中搜索一下是否有对应的原生组件。

# 使用 WeUI

  1. 在我们的项目的根目录新建 style 文件夹;
  2. 将模板中的 /dist/style/weui.wxss 导入我们项目中的 style 文件夹;
  3. 在我们项目的 app.wxss 添加一行代码:
@import 'style/weui.wxss';
  1. 按照官方代码的形式编写自己的代码,就能渲染成模板小程序的样子。

需要注意的是,这个小程序只使用 viewinput 控件就渲染了绝大多数页面。包括 button 等也是用 view 渲染的。

不过实际编写中,由于 button 在配合表单等有更多的功能,可以使用 button,然后使用 WeUI 中的 class 作用于 button 之上。

# Flex 布局(1)

Flex 是 Flexible Box 的缩写,意为”弹性布局”,用来为盒状模型提供最大的灵活性。

官方教程上对示例代码有更加清晰的解释。

WXML:

<view class="flex-box">
  <view class='list-item'>Python</view>
  <view class='list-item'>小程序</view>
  <view class='list-item'>网站建设</view>
</view>

WXSS:

.list-item{
  background-color: #82c2f7;
  height: 100px;
  text-align: center;
  border:1px solid #bdd2f8;

  flex: 1; /*各版块 1:1 等分*/
  display: flex; /*给 list-item 组件添加 display: flex 后,文字呈现水平居中的结构*/
  align-items:center; /*垂直居中*/
  justify-content: center; /*水平居中*/
}

.flex-box{
   display: flex; /*给 flex-box 组件添加 display: flex 后,三个 item 呈现左右而不是上下结构*/
}

display: flex; 对于 flex-box 来说,就是让其内容左右分布;对于 list-item 来说,就是让其文字水平居中。

Flex 布局

# Flex 布局(2)

下面参考官方的 WeUI 小程序中的 Flex 重新编写以上代码。

其中 WXML 和 WXSS 都是参考 Git 仓库 (opens new window) 的对应文件进行修改/照搬。

XML:

<view class="weui-flex">
    <view class="weui-flex__item"><view class="placeholder">Python</view></view>
    <view class="weui-flex__item"><view class="placeholder">小程序</view></view>
    <view class="weui-flex__item"><view class="placeholder">网页建设</view></view>
</view>

WXSS 直接复制对应的文件:

.placeholder{margin:5px;padding:0 10px;text-align:center;background-color:var(--weui-BG-1);height:2.3em;line-height:2.3em;color:var(--weui-FG-1)}

最后记得在 /app.wxss@import "/style/weui.wxss";

编译运行,效果图如下:

Flex 布局

这也太香了吧。

# JavaScript 数据绑定

教程 (opens new window)

什么是数据绑定呢?就是把 WXML 中的一些动态数据分离出来放到对应的 js 文件的 Page 的 data 里。

只需要将 WXML 的代码写成:

<view>{{username}},您已登录,欢迎</view>

然后同名 JS 写成:

Page({
  data: {
    username:"张明"
  }
})

即可导入变量。

常用变量类型有:字符串、数字、布尔值、对象、数组。

其中对象类似于 Python 字典的形式,数组类似于 Python 列表的形式(下标从 0 开始)。

在 WXML 中访问对象的方法是 .,访问数组的方法是 [i]

如下是对象和数组嵌套的一个示例:

  data: {
    movies: [
      {
        name: "肖申克的救赎",
        englishname: "The Shawshank Redemption",
        country: "美国",
        year: 1994,
        image: "https://img3.doubanio.com/view/photo/s_ratio_poster/public/p480747492.webp",
        desc: "有的人的羽翼是如此光辉,即使世界上最黑暗的牢狱,也无法长久地将他围困!",
        actor: [
          {
            name:"蒂姆·罗宾斯",
            role:"安迪·杜佛兰"
          },
          {
            name:"摩根·弗里曼",
            role:"艾利斯·波伊德·瑞德"
          },
        ]
      },
      {
          // ...
      }
    ],
  }

访问

{{movies[0].actor[0].role}}

可以得到 "安迪·杜佛兰"

由于比较简单就不多说了,详细教程可看本节开头的教程链接。

# 列表渲染和条件渲染

文档 (opens new window)

这是 WXML 的功能,可以对 JS 的数据渲染的时候进行 for 遍历和 if 判断。

下面的九九乘法表是一个很好的例子:

<view wx:for="{{[1, 2, 3, 4, 5, 6, 7, 8, 9]}}" wx:for-item="i">
  <view wx:for="{{[1, 2, 3, 4, 5, 6, 7, 8, 9]}}" wx:for-item="j">
    <view wx:if="{{i <= j}}">
      {{i}} * {{j}} = {{i * j}}
    </view>
  </view>
</view>

也可以使用 hidden 代替 if 进行条件渲染:

    <view hidden="{{i > j}}">
      {{i}} * {{j}} = {{i * j}}
    </view>

将上述 [1, 2, 3, 4, 5, 6, 7, 8, 9] 放到 js 的 data 中即可实现渲染 js 的数据。

不过,如果 js 的 data 是动态变化的,每次 data 改变就会导致重新渲染列表。解决方法是增加 wx:key 作为列表中 item 的唯一标识符。详见:https://developers.weixin.qq.com/miniprogram/dev/reference/wxml/list.html

如不提供 wx:key,会报一个 warning。但是如果明确知道该列表是静态,或者不必关注其顺序,可以选择忽略。

wx:key 可以有两种:

  • 如果是 item 本身(如上例),则可以用 wx:key="*this"
  • 如果 item 是一个对象,想用其属性如 name,则可以用 wx:key="name"
  • 除此之外,还可以用其下标:wx:key="index"
<view wx:for="{{[1, 2, 3, 4, 5, 6, 7, 8, 9]}}" wx:for-item="i" wx:key="*this">
  <view wx:for="{{[1, 2, 3, 4, 5, 6, 7, 8, 9]}}" wx:for-item="j">
    <view wx:if="{{i <= j}}">
      {{i}} * {{j}} = {{i * j}}
    </view>
  </view>
</view>

# 获取表单内容

WeUI 小程序提供了表单,但是没有提供 JS。而按照这个小程序的写法,是不能在点击按钮触发的 bindtap 函数中获取表单的信息的。因此,对于表单,更推荐另一种写法:链接 (opens new window)

<form bindsubmit="formSubmit" bindrest="formReset">
    <view>开关选择器按钮</view>
    <switch name="switch"/>
    <view>滑动选择器按钮slider</view>
    <slider name="process" show-value ></slider>
    <view>文本输入框</view>
    <input name="textinput" placeholder="要输入的文本" />
    <view>单选按钮radio</view>
    <radio-group name="sex">
      <label><radio value="male"/></label>
      <label><radio value="female"/></label>
    </radio-group>
    <view>多选按钮checkbox</view>
    <checkbox-group name="gamecheck">
      <label><checkbox value="game1"/>王者荣耀</label>
      <label><checkbox value="game2"/>欢乐斗地主</label>
      <label><checkbox value="game3"/>连连看</label>
      <label><checkbox value="game4"/>刺激战场</label>
      <label><checkbox value="game5"/>穿越火线</label>
      <label><checkbox value="game6"/>天天酷跑</label>
    </checkbox-group>
    <button form-type="submit">提交</button>
    <button form-type="reset">重置</button>
</form>

这里需要重点注意的是 formbutton 控件。formbindsubmit 配合 <button form-type="submit">,可以使得在按下这个 button 的时候,form 将表单中的内容作为参数,调用 bindsubmit 函数。

不过需要注意的是,组件中的 name 项必须写,否则是不会记录其内容的。

# 更多组件:轮播、音频、视频、cover、地图

教程链接 (opens new window)

如果没有需要则可以跳过。

# JavaScript 微信 API

JavaScript 入门知识就放在另一篇博客了。

wx 是小程序的全局对象,用于承载小程序能力相关 API。

小程序开发框架提供丰富的微信原生 API,可以方便的调起微信提供的能力,如获取用户信息,了解网络状态等。

可以在微信开发者工具的控制台 Console 里输入 wx,了 解 一 下 这 个 对 象(共 700+ 个成员函数)。

文档 (opens new window)

# 获取信息

# 登录

# 获取网络状态

下面列举的代码,可以直接在微信开发者工具的调试器中的 Console 输入。在电脑模拟器和真机调试的输出也会不同。

文档就不给出了,随手百度/谷歌,第一个就是。

wx.getNetworkType({
      success(res) {
        console.log(res)
      }
    });

# 获取用户信息

wx.getUserInfo({
  success(res) {
      console.log(res);
  }
});

# 获取设备信息

wx.getSystemInfo({
  success (res) {
    console.log(res.model)
    console.log(res.pixelRatio)
    console.log(res.windowWidth)
    console.log(res.windowHeight)
    console.log(res.language)
    console.log(res.version)
    console.log(res.platform)
  }
})

# 获取场景值

文档 (opens new window)

场景值用来描述用户进入小程序的路径。如扫二维码、搜索等。场景值获取方式和对应含义可见文档。

# 设置当前页面

# 设置页面标题

wx.setNavigationBarTitle({
  title: '控制台更新的标题'
})

# 下拉操作

需要在 app.json 或页面对应的 json 设置 "enablePullDownRefresh": true

Page({
  onPullDownRefresh: function () { //下拉时触发的函数
    this.onShow();                 // 触发 onShow 函数,可根据需要改为其他代码
    function sleep (time) {
      return new Promise((resolve) => setTimeout(resolve, time));
    }
    sleep(500).then(() => {        //睡眠 500ms
      wx.stopPullDownRefresh()     //停止下拉的状态
    })
  },
})

# 图片、缓存和文件

教程 (opens new window)

# 点击事件

事件是视图层到逻辑层的通信方式,当我们点击 tap、触摸 touch、长按 longpress 小程序绑定了事件的组件时,就会触发事件,执行逻辑层中对应的事件处理函数。

小程序框架的视图层由 WXML 与 WXSS 来编写的,由组件来进行展示。 将逻辑层的数据反应成视图,同时将视图层的事件发送给逻辑层。 逻辑层将数据进行处理后发送给视图层,同时接受视图层的事件反馈。

# 点击按钮触发页面滚动

首先在测试页面的 WXML 首尾增加两个 button(两个 button 之间沿用九九乘法表的代码):

<button type="primary" bindtap="scrollToPosition">滚动到指定位置</button>
<view class="pagetop" style="background-color:#333;width:100%;height:400px"></view>

<view wx:for="{{[1, 2, 3, 4, 5, 6, 7, 8, 9]}}" wx:for-item="i" wx:key="*this">
  <view wx:for="{{[1, 2, 3, 4, 5, 6, 7, 8, 9]}}" wx:for-item="j" wx:key="*this">
    <view wx:if="{{i <= j}}">
      {{i}} * {{j}} = {{i * j}}
    </view>
  </view>
</view>

<button type="primary" bindtap="scrollToTop">滚动到页面顶部</button>
<view id="pageblock" style="background-color:#333;width:100%;height:400px"></view>

注意 button 属性里有一个 bindtap,它的值对应的 js 函数就是当按钮按下时调用的函数。

于是我们在同名 JS 下写两个函数调用 wx.pageScrollTo

Page({

  scrollToTop() {
    wx.pageScrollTo({
      scrollTop: 0,
      duration: 300
    })
  },

  scrollToPosition() {
    wx.pageScrollTo({
      scrollTop: 6000,
      duration: 300
    })
  })

然后编译。

按钮

点击“滚动到指定位置”可以滚到 6000px 的位置(由于总长没有 6000,因此就是滚到末尾);点击“滚动到页面顶部”可以滚动到顶部。

也可以滚动到指定 XML 选择器的位置。

  scrollToTop() {
    wx.pageScrollTo({
      duration: 3000,
      selector:".pagetop"
    })
  },

  scrollToPosition() {
    wx.pageScrollTo({
      duration: 300,
      selector:"#pageblock"
    })
  },

# 消息提示框 Toast

依旧是 button 通过 bindtap 绑定到调用 wx.showToast 的函数。

<button type="primary" bindtap="toastTap">点击弹出消息对话框</button>
  toastTap() {
    wx.showToast({
      title: '购买成功',
      icon: 'success',
      duration: 2000
    })
  },

# 模态对话框

依旧是 button 通过 bindtap 绑定到调用 wx.showModel 的函数。不过这里增加了用户的两个选项。

<button type="primary" bindtap="modalTap">显示模态对话框</button>
  modalTap() {
    wx.showModal({
      title: '学习声明',
      content: '学习小程序的开发是一件有意思的事情,我决定每天打卡学习',
      showCancel: true,
      confirmText: '确定',
      success(res) {
        if (res.confirm) {
          console.log('用户点击了确定')
        } else if (res.cancel) {
          console.log('用户点击了取消')
        }
      }
    })
  },

模态对话框

# 手机震动

一样的套路,把函数改为 wx.vibrateLong() 即可。

Navigator 组件可以做到的事情,使用 JavaScript 调用小程序也能路由 API 也可以做到。Navigator 组件的内容是写死的,而 JavaScript 则可以提供动态的数据。

页面路由 API Navigator open-type 值 含义
wx.redirectTo redirect 关闭当前页面,跳转到应用内的某个页面。但是不允许跳转到 tabbar 页面。
wx.navigateTo navigate 保留当前页面,跳转到应用内的某个页面。但是不能跳到 tabbar 页面。
wx.navigateBack navigateBack 关闭当前页面,返回上一页面或多级页面。
wx.switchTab switchTab 跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面
wx.reLaunch reLaunch 关闭所有页面,打开到应用内的某个页面

# 事件对象

教程链接 (opens new window) 事件 文档 (opens new window)

在前面的列表渲染里,我们知道点击电影列表里的某一部电影,要进行页面跳转显示该电影的详情,我们需要给该电影创建一个页面,那如果要显示数千部的电影的详情,一一创建电影详情页显然不合适,毕竟所有电影的详情页都是同一一个结构,有没有办法所有电影详情都共用一个页面,但是根据点击的链接的不同,渲染相应的数据?答案是肯定的,要解决这个问题,首先我们要了解链接组件的点击信息。

当点击组件触发事件时,逻辑层绑定该事件的处理函数会收到一个事件对象,通过 event 对象可以获取事件触发时候的一些信息,比如时间戳、 detail 以及当前组件的一些属性值集合,尤其是事件源组件的 id

如,在 WXML 中有一个按钮如下:

<view id="tapTest" data-hi="Weixin" bindtap="tapName"> Click me! </view>

在 JS 中定义一个 tapName 函数:

Page({
  tapName: function(event) {
    console.log(event)
  }
})

点击按钮可以看到以下信息:

{
  "type":"tap",
  "timeStamp":895,
  "target": {
    "id": "tapTest",
    "dataset":  {
      "hi":"Weixin"
    }
  },
  "currentTarget":  {
    "id": "tapTest",
    "dataset": {
      "hi":"Weixin"
    }
  },
  "detail": {
    "x":53,
    "y":14
  },
  "touches":[{
    "identifier":0,
    "pageX":53,
    "pageY":14,
    "clientX":53,
    "clientY":14
  }],
  "changedTouches":[{
    "identifier":0,
    "pageX":53,
    "pageY":14,
    "clientX":53,
    "clientY":14
  }]
}

对比 WXML 可以注意到:

<view id="tapTest" data-hi="Weixin" bindtap="tapName"> Click me! </view>
  • event.currentTarget.id 对应 WXML 中的 id
  • event.currentTarget.dataset.hi 对应 data-hi;事实上,在 WXML 中,这些自定义数据以 data- 开头,多个单词由连字符 - 连接。在 JS 中,连字符写法会转换成驼峰写法,而大写字符会自动转成小写字符。参考 (opens new window)

这样就可以访问 event.currentTarget.idevent.currentTarget.dataset.hi 来实现传递按钮的参数了。

还注意到,在本例中,currentTargettarget 是一样的。

但是,如果在多个组件中触发了 bindtap(如外层的 view 和内层的 image 组件都有 bindtap,点击了图片,则两个 bindtap 对应的函数都会被触发)。

我们点击的是图片 image 组件,却分别触发了绑定在 image 组件以及 image 的父级(上一级)组件 view 的事件处理函数,我们称这为事件冒泡

此时,两个函数中的 currentTarget 都对应 image 组件的信息,而 target 对应的是本身组件(viewimage)的信息。

# 变量传递

这节讲的是组件是如何携带数据的,事件对象数据的作用以及数据如何跨页面渲染。

# 在链接中携带数据

最简单的就是在链接中携带数据。

如,新建两个页面:pages/home/detail/detailpages/lifecyle/lifecycle

pages/lifecyle/lifecycle.wxml 中写一个跳转组件:

<navigator id="detailshow" url="./../home/detail/detail?id=lesson&uid=tcb&key=tap&ENV=weapp&frompage=lifecycle" class="item-link">点击链接看控制台</navigator>

点击即可跳转到 pages/home/detail/detail。那,如何在 detail 获取数据呢?

detail.js 写一个 onLoad 函数即可(这个 onLoad 函数和 data 平级):

onLoad: function (options) {
  console.log(options)
},

显示 {id: "lesson", uid: "tcb", key: "tap", ENV: "weapp", frompage: "lifecycle"}。即,参数被解析为了一个 JS 对象,就可以用 options.id 访问数据。

# 跨页面数据渲染

讲到这里,其实就已经可以实现了。流程如下:

  1. detail.js 中的 data 添加一个 detail 对象,如下:
    detail:{
      name:"",
      image:"",
      desc:""
    },
  1. detail.jsonload 函数中,将参数值赋给 detail
  2. detail.wxml 中使用 {{detail.img}} 等进行渲染。

# 小程序和页面的生命周期

App() 函数注册小程序,Page() 函数注册小程序中的一个页面,他们都接受的是对象 Object 类型的参数,包含一些生命周期函数和事件处理函数。App() 必须在 app.js 中调用,必须调用且只能调用一次。开发者可以添加任意的函数或数据变量到 Object 参数中,用 this 可以访问。

App.js 中的函数及触发时刻如下:

  onLaunch(opts) {
    console.log('onLaunch监听小程序初始化。',opts)
  },

  onShow(opts) {
    console.log('onShow监听小程序启动或切前台',opts)
  },

  onHide() {
    console.log('onHide监听小程序切后台')
  },

onLaunch 是监听小程序的初始化,初始化完成时触发,全局只会触发一次,所以在这里我们可以用来执行获取用户登录信息的函数等一些非常核心的数据,如果 onLaunch 的函数过多,会影响小程序的启动速度。 onShow 是在小程序启动,或从后台进入前台显示时触发,也就是它会触发很多次,在这里就不大适合放获取用户登录信息的函数啦。这两者的区别要注意。

每个页面的 JS 中的函数及触发时刻如下:

  onLoad: function(options) {
    console.log("onLoad监听页面加载",options)
  },

  onReady: function() {
    console.log("onReady监听页面初次渲染完成")
  },

  onShow: function() {
    console.log("onShow监听页面显示")
  },

  onHide: function() {
    console.log("onHide监听页面隐藏")
  },

  onUnload: function() {
    console.log("onUnload监听页面卸载")
  },

推荐自己测试一下。

# globalData

app.js 中还有一个 globalData,顾名思义就是全局数据。

可以直接在 app.js 中定义 globalData 的参数。

App({
  globalData: {
    userInfo: null
  }
})

可以在 app.js 的其他函数中修改:

App({
  onLaunch: function() {
    var that = this;
    wx.getUserInfo({
      success(res){
        console.log("wx.getUserInfo得到的数据",res)
        that.globalData.userInfo = res.userInfo
      }
    })
  }
})

在其他页面中访问全局变量可以使用 getApp()文档 (opens new window)):

let app = getApp()
console.log('user页面打印的app', app)
console.log('user页面打印的globalData', app.globalData.userInfo)

# 网络 API

教程 (opens new window)

# 云函数

云函数即在云端(腾讯提供的临时服务器)执行的 JavaScript 函数。

某些函数需要调用 npm 包,但我们不能在每台手机上安装所有 npm 组件。
因此,我们将 npm 包和需要这些包的函数部署在云上,手机上只需要调用这些云函数即可。

教程 (opens new window)

# wx.cloud

wx.cloud 对象有以下成员。云函数、云数据库、云存储都是通过这个对象进行使用。

CloudID: ƒ () //用于云调用获取开放数据
callFunction: ƒ () //调用云函数
database: ƒ () //获取数据库的引用
deleteFile: ƒ () //从云存储空间删除文件
downloadFile: ƒ () //从云存储空间下载文件
getTempFileURL: ƒ () //用云文件 ID 换取真实链接
init: ƒ ()  //初始化云开发能力
uploadFile: ƒ () //上传文件至云存储空间

# 开通云开发服务

见上教程。需要记住环境 ID。环境即是云函数、数据库等需要上传的一个地方。

# 指定小程序的 ID

以云开发的形式创建项目,然后在 app.js 可以看到:

App({
  onLaunch: function () {
    
    if (!wx.cloud) {
      console.error('请使用 2.2.3 或以上的基础库以使用云能力')
    } else {
      wx.cloud.init({
        // env 参数说明:
        //   env 参数决定接下来小程序发起的云开发调用(wx.cloud.xxx)会默认请求到哪个云环境的资源
        //   此处请填入环境 ID, 环境 ID 可打开云控制台查看
        //   如不填则使用默认环境(第一个创建的环境)
        // env: 'my-env-id',
        traceUser: true,
      })
    }

    this.globalData = {}
  }
})

取消注释第 12 行,并且修改为自己的环境 ID。

# 编写云函数

cloudfunctions 目录下,每个文件夹都对应一个云函数。

我们关注 login 函数。打开 cloudfunctions/login/index.js

// 云函数模板
// 部署:在 cloud-functions/login 文件夹右击选择 “上传并部署”

const cloud = require('wx-server-sdk')

// 初始化 cloud
cloud.init({
  // API 调用都保持和云函数当前所在环境一致
  env: cloud.DYNAMIC_CURRENT_ENV
})

/**
 * 这个示例将经自动鉴权过的小程序用户 openid 返回给小程序端
 * 
 * event 参数包含小程序端调用传入的 data
 * 
 */
exports.main = async (event, context) => {
  console.log('服务端打印的event',event)
  console.log('服务端打印的context',context)
  // 可执行其他自定义逻辑
  // console.log 的内容可以在云开发云函数调用日志查看

  // 获取 WX Context (微信调用上下文),包括 OPENID、APPID、及 UNIONID(需满足 UNIONID 获取条件)等信息
  const wxContext = cloud.getWXContext()

  return {
    event,
    openid: wxContext.OPENID,
    appid: wxContext.APPID,
    unionid: wxContext.UNIONID,
    env: wxContext.ENV,
  }
}
  • 1-10 行初始化云环境;
  • 18-35 行则导出了一个叫做 main 的箭头函数,其接受两个参数 eventcontext,可通过 console.log 查看:
    • event 包含用户的 openid 和小程序的 appid, openid 就相当于用户的身份证,我们可以根据 openid 获取到用户的昵称、头像等信息;
    • context 对象则是云函数的调用信息和运行状态。
  • 这段函数执行过程只是调用 cloud.getWXContext() 并返回其结果,在调用云函数的地方可以接收。

需要强调的是,云函数的 console.log 的返回结果是在云开发控制台而不是开发者工具的控制台里。

# 部署云函数

  1. 右键云函数目录,选择在终端中打开,输入 npm install 命令下载依赖文件;
  2. 然后再右键云函数目录,点击“创建并部署:所有文件”;
  3. 在云开发控制台–云函数–云函数列表查看云函数是否部署成功。

# 编写调用云函数的方法

现在将目光转向 pages/index/index.js。这是云开发小程序模板的首页的 JS 文件。其中有一个函数 onGetOpenid 是首页中“获取 openid” 按钮通过 tapbind 绑定的函数。这个函数调用了云函数:

  onGetOpenid: function() {
    // 调用云函数
    wx.cloud.callFunction({
      name: 'login',
      data: {},
      success: res => {
        console.log('[云函数] [login] user openid: ', res.result.openid)
        app.globalData.openid = res.result.openid
        wx.navigateTo({
          url: '../userConsole/userConsole',
        })
      },
      fail: err => {
        console.error('[云函数] [login] 调用失败', err)
        wx.navigateTo({
          url: '../deployFunctions/deployFunctions',
        })
      }
    })
  },

可见,调用云函数是通过 wx.cloud.callFunction 函数,再通过参数指定是哪个函数、参数和成功/失败的回调函数。

# 进阶调试

教程 (opens new window)

# 云服务选取

# 云数据库

教程 (opens new window)

文档 (opens new window)

微信给的云数据库是 JSON 数据库,也就是每条记录以 JSON 的形式上传。这点类似于 MongoDB,甚至某些查询语句也和 MongoDB 类似。

创建项目时,以云开发的形式创建项目,然后在 /pages/databaseGuide/databaseGuide.js 可以看到如何操作数据库。首先需要手动在云开发-数据库页面新增集合(类似于 MySQL 的表)。

云数据库使用有以下注意事项:

  • 小程序一次获取的记录数不能超过 20 条,云函数不能超过 100 条。必要时可通过文档 (opens new window) 给出的示例代码进行多次获取然后拼接;
  • 小程序不支持数据批量更新(即 db.collection().where().update()),仅云函数支持;
  • 云函数不兼容回调函数风格(即 .get({success, fail, complete})),仅支持 Promise 风格(即 .get().then().catch());
  • 并不是每次云函数执行都会在新的环境开始执行,连续执行云函数也可能在上一次的环境下执行,因此变量初始化一定要在 main 函数里完成,才能保证变量每次被初始化了。

# 新增记录

  onAdd: function () {
    const db = wx.cloud.database()
    db.collection('counters').add({
      data: {
        count: 1,
      },
      success: res => {
        // 在返回结果中会包含新创建的记录的 _id
        this.setData({
          counterId: res._id,
          count: 1
        })
        wx.showToast({
          title: '新增记录成功',
        })
        console.log('[数据库] [新增记录] 成功,记录 _id: ', res._id)
      },
      fail: err => {
        wx.showToast({
          icon: 'none',
          title: '新增记录失败'
        })
        console.error('[数据库] [新增记录] 失败:', err)
      }
    })
  },

# 查询记录

  onQuery: function() {
    const db = wx.cloud.database()
    // 查询当前用户所有的 counters
    db.collection('counters').where({
      _openid: this.data.openid
    }).get({
      success: res => {
        this.setData({
          queryResult: JSON.stringify(res.data, null, 2)
        })
        console.log('[数据库] [查询记录] 成功: ', res)
      },
      fail: err => {
        wx.showToast({
          icon: 'none',
          title: '查询记录失败'
        })
        console.error('[数据库] [查询记录] 失败:', err)
      }
    })
  },

更多的查询语法见链接 (opens new window)

# 更新记录

  onCounterInc: function() {
    const db = wx.cloud.database()
    const newCount = this.data.count + 1
    db.collection('counters').doc(this.data.counterId).update({
      data: {
        count: newCount
      },
      success: res => {
        this.setData({
          count: newCount
        })
      },
      fail: err => {
        icon: 'none',
        console.error('[数据库] [更新记录] 失败:', err)
      }
    })
  },

需要注意的是,小程序端为了避免批量更新,不允许对查询的数据进行更新。

  • 如果查询的数据只有一项,可以通过查询获取其 id,再对 id 更新;
  • 如果需要更新很多项,可以考虑使用云函数。

# 删除记录

  onRemove: function() {
    if (this.data.counterId) {
      const db = wx.cloud.database()
      db.collection('counters').doc(this.data.counterId).remove({
        success: res => {
          wx.showToast({
            title: '删除成功',
          })
          this.setData({
            counterId: '',
            count: null,
          })
        },
        fail: err => {
          wx.showToast({
            icon: 'none',
            title: '删除失败',
          })
          console.error('[数据库] [删除记录] 失败:', err)
        }
      })
    } else {
      wx.showToast({
        title: '无记录可删,请见创建一个记录',
      })
    }
  },