微信小程序

Lv2

小程序的发展历史

什么是小程序

  • 小程序是什么呢?

    • 小程序(Mini Program)是一种不需要下载安装即可使用的应用,它实现了 “触手可及” 的梦想,使用起来方便快捷,用完即走
  • 最初我们提到小程序时,往往指的是 微信小程序

    • 但是目前小程序技术本身已经被各个平台所实现和支持
  • 那么目前常见的小程序有哪些呢?

    • 微信小程序、支付宝小程序、淘宝小程序、抖音小程序、头条小程序、QQ 小程序、美团小程序等等

各个平台小程序的时间线

  • 各个平台小程序大概的发布时间线
    • 2017 年 1 月 微信小程序上线,依附于微信 App
    • 2018 年 7 月 百度小程序上线,依附于百度 App
    • 2018 年 9 月 支付宝程上线,依附于支付宝 App
    • 2018 年 10 月 抖音小程序上线,依附于抖音 App
    • 2018 年 11 月 头条小程序上线,依附于头条 App
    • 2019 年 5 月 QQ 小程序上线,依附于 QQApp
    • 2019 年 10 月 美团小程序上线,依附于美团 App

各个平台为什么都需要支持小程序呢

  • 第一:你有,我也得有
    • 大厂竞争格局中一个重要的一环
  • 第二:小程序作为介于 H5 页面和 App 之间的一项技术,它有自身很大的优势
    • 体验比传统 H5 页面要好很多
    • 相当于传统的 App,使用起来更加方便,不需要在应用商店中下载安装,甚至注册登录等麻烦的操作
  • 第三:小程序可以间接的动态为 App 添加新功能
    • 传统的 App 更新需要先打包,上架到应用商店之后需要通过审核(App Store)
    • 但是小程序可以在 App 不更新的情况下,动态为自己的应用添加新的功能需求
  • 那么目前在这么多小程序的竞争格局中,哪一个是使用最广泛的呢
    • 显示是微信小程序,目前支付宝、抖音小程序也或多或少有人在使用
    • 其实我们透过小程序看本质,他们本身还是应用和平台之间的竞争,有最大流量入口的平台,对应的小程序也是用户更多一些
    • 目前在公司开发小程序主要开发的还是微信小程序,其他平台的小程序往往是平台本身的一些公司或者顺手开发的

开发小程序的技术选型

  • 原生小程序开发

  • 选择框架开发小程序

    • mpvue

      • mpvue 是一个使用 Vue 开发小程序的前端框架,也是 支持 微信小程序、百度智能小程序,头条小程序 和 支付宝小程序
      • 该框架在 2018 年之后就不再维护和更新了,所以目前已经被放弃
    • wepy

      • WePY (发音: /'wepi/)是由腾讯开源的,一款让小程序支持组件化开发的框架,通过预编译的手段让开发者可以选择自己喜欢的开发风格去开发小程序
      • 该框架目前维护的也较少,在前两年还有挺多的项目在使用,不推荐使用
    • uni-app

      • DCloud团队开发和维护
      • uni-app 是一个使用 Vue 开发所有前端应用的框架,开发者编写一套代码,可发布到 iOS、Android、Web(响应式)、以及各种小程序(微信/支宝/百度/头条/飞书/QQ/快手/钉钉/淘宝)、快应用等多个平台
      • uni-app 目前是很多公司的技术选型,特别是希望适配移动端 App 的公司
    • taro

      • 京东团队开发和维护
      • taro 是一个开放式 跨端 跨框架 解决方案,支持使用 React/Vue/Nerv 等框架来开发 微信 / 京东 / 百度 / 支付宝 / 字节跳动 / QQ / 飞书 小程序 / H5 / RN 等应用
      • taro 因为本身支持 React、Vue 的选择,给了我们更加灵活的选择空间
        • 特别是在 Taro3.x 之后,支持Vue3、React Hook写法等
      • taro['tɑ:roʊ],泰罗·奥特曼,宇宙警备队总教官,实力最强的奥特曼
  • uni-app 和 taro 开发原生 App

    • 无论是适配原生小程序还是原生 App,都有较多的适配问题,所以你还是需要多了解原生的一些开发知识
    • 产品使用体验整体相较于原生 App 差很多
    • 也有其他的技术选项来开发原生 App:ReactNative、Flutter

小程序的架构和配置

架构模型

  • 谁是小程序的宿主环境呢?微信客户端

    • 宿主环境为了执行小程序的各种文件:wxml 文件、wxss 文件、js 文件
  • 当小程序基于 WebView 环境下时,WebView 的 JS 逻辑、DOM 树创建、CSS 解析、样式计算、Layout、Paint (Composite) 都发生在同一线程,在 WebView 上执行过多的 JS 逻辑可能阻塞渲染,导致界面卡顿

  • 以此为前提,小程序同时考虑了性能与安全,采用了目前称为「双线程模型」的架构

  • 双线程模型

    • WXML 模块和 WXSS 样式运行于 渲染层,渲染层使用WebView 线程渲染(一个程序有多个页面,会使用多个 WebView 的线程)
    • JS 脚本(app.js/home.js 等)运行于 逻辑层,逻辑层使用 JsCore 运行 JS 脚本
    • 这两个线程都会经由微信客户端(Native)进行中转交互

配置文件

全局配置文件

  • window:全局的默认窗口展示

    • 用户指定窗口如何展示,其中还包含了很多其他的属性
  • tabBar:底部 tab 栏的展示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    "tabBar": {
    "selectedColor": "#ff8189",
    "list": [
    {
    "pagePath": "pages/index/index",
    "text": "首页",
    "iconPath": "assets/tabbar/home.png",
    "selectedIconPath": "assets/tabbar/home_active.png"
    },
    {
    "pagePath": "pages/my/my",
    "text": "我的",
    "iconPath": "assets/tabbar/my.png",
    "selectedIconPath": "assets/tabbar/my_active.png"
    }
    ]
    },

页面配置文件

  • 每一个小程序页面也可以使用 .json 文件来对本页面的窗口表现进行配置

    1
    2
    3
    4
    5
    6
    7
    {
    "usingComponents": {},
    "navigationBarBackgroundColor": "#000",
    "navigationBarTitleText": "我的",
    "enablePullDownRefresh": true,
    "backgroundTextStyle": "dark"
    }

App 函数

  • 每个小程序都需要在 app.js 中调用 App 函数 注册小程序实例
  • 注册 App 时,我们一般会做什么呢?
    • 判断小程序的进入场景
    • 监听生命周期函数,在生命周期中执行对应的业务逻辑,比如在某个生命周期函数中进行登录操作或者请求网络数据
    • 因为 App()实例只有一个,并且是全局共享的(单例对象),所以我们可以将一些共享数据放在这里

判断打开场景

定义全局数据

  • 可以在 App 函数中定义全局的数据,定义的数据可以在其他任何页面中访问

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // app.js
    App({
    // 见名知意
    globalData: {
    token: "globalData - token",
    },
    });

    // pages/index/index.js
    const app = getApp();
    Page({
    data: {
    token: app.globalData.token,
    },
    });

    <!--pages/index/index.wxml-->
    <view>{{ token }}</view>

生命周期函数

  • 在生命周期函数中,完成应用程序启动后的初始化操作

    • 比如登录操作
    • 比如读取本地数据(类似于 token,然后保存在全局方便使用)
    • 比如请求整个应用程序需要的数据
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // app.js
    onLaunch() {
    // 1.登录逻辑
    wx.login({ success() {} });

    // 2.读取本地数据
    const token = wx.getStorageSync("token");
    this.globalData.token = token;

    // 3.请求数据
    wx.request({ url: "url" });
    }

Page 函数

  • 小程序中的每个页面,都有一个对应的 js 文件,其中调用Page 函数注册页面实例

  • 注册一个 Page 页面时,我们一般需要做什么呢?

    • 生命周期函数中发送网络请求,从服务器获取数据
    • 初始化一些数据,以方便被 wxml 引用展示
    • 监听 wxml 中的事件,绑定对应的事件函数
    • 其他一些监听(比如页面滚动、下拉刷新、上拉加载更多等)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <view>
    <view>当前计数: {{counter}}</view>
    <button size="mini" type="primary" bindtap="increment">+1</button>
    <button size="mini" type="primary" bindtap="decrement">-1</button>
    </view>

    <view>
    <text>{{info.name}} - {{info.age}}</text>
    </view>

    <block wx:for="{{100}}" wx:key="*this">
    <view>内容 - {{item + 1}}</view>
    </block>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    Page({
    data: { counter: 100, info: {} },
    onLoad() {
    setTimeout(() => {
    this.setData({ info: { name: "shy", age: 18 } });
    }, 500);
    },
    increment() {
    this.setData({ counter: this.data.counter + 1 });
    },
    decrement() {
    this.setData({ counter: this.data.counter - 1 });
    },
    onPullDownRefresh() {
    console.log("下拉刷新");
    },
    onReachBottom() {
    console.log("上拉加载更多");
    },
    onPageScroll() {
    console.log("页面滚动");
    },
    });
    1
    2
    3
    4
    5
    {
    "enablePullDownRefresh": true,
    "backgroundTextStyle": "dark",
    "usingComponents": {}
    }

生命周期

内置组件

text

  • text 组件用于显示文本,类似于 span 标签,是行内元素

  • https://developers.weixin.qq.com/miniprogram/dev/component/text.html

    • user-select 属性决定文本内容是否可以让用户选中
    • space 有三个取值(了解)
    • decode 是否解码(了解)
    1
    2
    3
    // decode可以解析的有 &nbsp; &lt; &gt; &amp; &apos; &ensp; &emsp;
    <text user-select>{{ message }}</text>
    <text decode>&nbsp; &lt; &gt; &amp; &apos; &ensp; &emsp;</text>

button

  • button 组件用于创建按钮,默认块级元素

  • https://developers.weixin.qq.com/miniprogram/dev/component/button.html

    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
    <!-- 1.基本使用 -->
    <button size="mini">size属性</button>
    <button size="mini" type="primary">type属性</button>
    <button size="mini" type="warn">type属性</button>
    <button size="mini" class="btn">自定义属性</button>

    <button size="mini" plain>plain属性</button>
    <button size="mini" disabled>disabled属性</button>
    <button size="mini" loading class="btn">loading属性</button>
    <button size="mini" hover-class="active">hover效果</button>

    <!-- 2.open-type属性 -->
    <button size="mini" type="primary" open-type="contact">打开会话</button>
    <button
    size="mini"
    type="primary"
    open-type="getUserInfo"
    bindgetuserinfo="getUserInfo"
    >
    用户信息 - 已废弃
    </button>
    <button size="mini" type="primary" bindtap="getUserInfo">
    用户信息 - 最新方式
    </button>
    <button
    size="mini"
    type="primary"
    open-type="getPhoneNumber"
    bindgetphonenumber="getPhoneNumber"
    >
    手机号码 - 需要企业认证才行
    </button>

view

image

  • image 组件用于显示图片

  • Mode 属性使用也非常关键,详情查看官网

  • 注意:image 组件默认宽度 320px、高度 240px

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <!-- 1.图片的基本使用 -->
    <!-- image组件宽度和高度: 320x240 -->
    <image src="/assets/zznh.png" />
    <image
    src="https://pic3.zhimg.com/v2-9be23000490896a1bfc1df70df50ae32_b.jpg"
    />

    <!-- 2.图片重要的属性: mode -->
    <image src="/assets/zznh.png" mode="aspectFit" />
    <!-- image基本都是设置widthFix -->
    <image src="/assets/zznh.png" mode="widthFix" />
    <image src="/assets/zznh.png" mode="heightFix" />

scroll-view

  • scroll-view 可以实现局部滚动

  • https://developers.weixin.qq.com/miniprogram/dev/component/scroll-view.html

  • 注意事项

    • 实现滚动效果必须添加 scroll-x 或者 scroll-y 属性(只需要添加即可,属性值相当于为 true 了)
    • 垂直方向滚动必须设置 scroll-view 一个高度
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <!-- 上下滚动(y轴) -->
    <scroll-view class="container scroll-y" scroll-y>
    <block wx:for="{{viewColors}}" wx:key="*this">
    <view class="item" style="background: {{item}};">{{item}}</view>
    </block>
    </scroll-view>

    <view>
    <text decode>&nbsp;</text>
    </view>

    <!-- 左右滚动(x轴) -->
    <scroll-view class="container scroll-x" scroll-x enable-flex>
    <block wx:for="{{viewColors}}" wx:key="*this">
    <view class="item" style="background: {{item}};">{{item}}</view>
    </block>
    </scroll-view>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    .container {
    background-color: orange;
    height: 150px;
    }

    .scroll-x {
    display: flex;
    }

    .item {
    width: 100px;
    height: 100px;
    color: #fff;
    flex-shrink: 0;
    }

共同的属性

wxss

小程序的样式写法

  • 页面样式的三种写法

    • 行内样式、页面样式、全局样式
    • 三种样式都可以作用于页面的组件
  • 如果有相同的样式

    • 优先级依次是:行内样式 > 页面样式 > 全局样式
    1
    2
    3
    4
    5
    6
    7
    8
    <!-- 1.1.应用全局样式 -->
    <view class="title">learn wxss title</view>

    <!-- 1.2.页面中的样式 -->
    <view class="message">learn wxss message</view>

    <!-- 1.3.行内的样式 -->
    <view style="color: blue">inline style</view>
    1
    2
    3
    4
    5
    6
    7
    .title {
    color: red;
    }

    .message {
    color: pink;
    }

支持的选择器

  • 目前支持的选择器有

    选择器样例样例描述
    .class.intro选择所有拥有 class=“intro” 的组件
    #id#firstname选择拥有 id=“firstname” 的组件
    elementview选择所有 view 组件
    element, elementview, checkbox选择所有文档的 view 组件和所有的 checkbox 组件
    ::afterview::after在 view 组件后边插入内容
    ::beforeview::before在 view 组件前边插入内容

尺寸单位

  • 尺寸单位

    • rpx(responsive pixel): 可以根据屏幕宽度进行自适应,规定屏幕宽为 750rpx
    • 如在 iPhone6 上,屏幕宽度为 375px,共有 750 个物理像素,则 750rpx = 375px = 750 物理像素,1rpx = 0.5px = 1 物理像素
    设备rpx 换算 px (屏幕宽度/750)px 换算 rpx (750/屏幕宽度)
    iPhone51rpx = 0.42px1px = 2.34rpx
    iPhone61rpx = 0.5px1px = 2rpx
    iPhone6 Plus1rpx = 0.552px1px = 1.81rpx
  • 建议: 开发微信小程序时设计师可以用 iPhone6 作为视觉稿的标准

wxml

mustache

  • WXML 基本格式

    • 类似于 HTML 代码:比如可以写成单标签,也可以写成双标签
    • 必须有严格的闭合:没有闭合会导致编译错误
    • 大小写敏感:class 和 Class 是不同的属性
  • 开发中,界面上展示的数据并不是写死的,而是会根据服务器返回的数据,或者用户的操作来进行改变

    • 如果使用原生 JS 或者 jQuery 的话,我们需要通过操作 DOM 来进行界面的更新
    • 小程序和 Vue 一样,提供了插值语法: Mustache 语法(双大括号)
    1
    2
    3
    <view>{{ message }}</view>
    <view>{{ firstname + " " + lastname }}</view>
    <view>{{ date }}</view>
    1
    2
    3
    4
    5
    6
    data: {
    message: "Hello World",
    firstname: "kobe",
    lastname: "bryant",
    date: new Date().toLocaleDateString()
    }

wx:if、wx:elif、wx:else

  • 某些时候,我们需要根据条件来决定一些内容是否渲染

    • 当条件为 true 时,view 组件会渲染出来
    • 当条件为 false 时,view 组件不会渲染出来
    1
    2
    3
    4
    5
    6
    7
    8
    <!-- 
    data: { score: 10 }
    -->

    <view wx:if="{{score > 90}}">优秀</view>
    <view wx:elif="{{score > 80}}">良好</view>
    <view wx:elif="{{score >= 60}}">及格</view>
    <view wx:else>不及格</view>

hidden

  • hidden 属性

    • hidden 是所有的组件都默认拥有的属性
    • 当 hidden 属性为 true 时,组件会被隐藏
    • 当 hidden 属性为 false 时,组件会显示出来
    1
    <view hidden>我是hidden的view</view>
  • hidden 和 wx:if 的区别

    • hidden 控制隐藏和显示是控制是否添加 hidden 属性
    • wx:if 是控制组件是否渲染的

wx:for

  • 为什么使用 wx:for?

    • 我们知道,在实际开发中,服务器经常返回各种列表数据,我们不可能一一从列表中取出数据进行展示
    • 需要通过for 循环的方式,遍历所有的数据,一次性进行展示
  • 在组件中,我们可以使用 wx:for 来遍历一个数组 (字符串 - 数字)

    • 默认情况下,遍历后在 wxml 中可以使用一个变量 index,保存的是当前遍历数据的下标值
    • 数组中对应某项的数据,使用变量名 item获取
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <view class="books">
    <view wx:for="{{books}}" wx:key="id">
    <!-- item: 每项内容, index: 每项索引 -->
    {{item.name}}-{{item.price}}
    </view>
    </view>

    <!-- 遍历数字 -->
    <view class="number">
    <view wx:for="{{10}}" wx:key="*this"> {{ item }} </view>
    </view>

    <!-- 遍历字符串 -->
    <view class="str">
    <view wx:for="theshy" wx:key="*this"> {{ item }} </view>
    </view>
    1
    2
    3
    4
    5
    6
    7
    data: {
    books: [
    { id: 111, name: "代码大全", price: 98 },
    { id: 112, name: "你不知道JS", price: 87 },
    { id: 113, name: "JS高级设计", price: 76 },
    ];
    }

item/index 名称

  • 默认情况下,item – index 的名字是固定的

    • 但是某些情况下,我们可能想使用其他名称
    • 或者当出现多层遍历时,名字会重复
  • 这个时候,我们可以指定 item 和 index 的名称

    1
    2
    3
    4
    5
    <view class="books">
    <block wx:for="{{books}}" wx:key="id" wx:for-item="book" wx:for-index="i">
    <view>{{ book.name }} - {{ book.price }} - {{ i }}</view>
    </block>
    </view>

key

  • 我们看到,使用 wx:for 时,会报一个警告

    • 这个提示告诉我们,可以添加一个key 来提供性能
  • 为什么需要这个 key 属性呢?

    • 这个其实和小程序内部也使用了虚拟 DOM 有关系(和 Vue、React 很相似)
    • 当某一层有很多相同的节点时,也就是列表节点时,我们希望插入、删除一个新的节点,可以更好的复用节点
  • wx:key 的值以两种形式提供

    • 字符串,代表在 for 循环的 array 中 item 的某个 property,该 property 的值需要是列表中唯一的字符串或数字,且不能动态改变

    • 保留关键字 *this 代表在 for 循环中的 item 本身,这种表示需要 item 本身是一个唯一的字符串或者数字

block

  • 什么是 block 标签?
    • 某些情况下,我们需要使用 wx:if 或 wx:for 时,可能需要包裹一组组件标签
    • 我们希望对这一组组件标签进行整体的操作,这个时候怎么办呢?
  • 注意:<block/> 并不是一个组件,它仅仅是一个包装元素,不会在页面中做任何渲染,只接受控制属性
  • 使用 block 有两个好处
    • 将需要进行遍历或者判断的内容进行包裹
    • 将遍历和判断的属性放在 block 便签中,不影响普通属性的阅读,提高代码的可读性

wxs

  • WXS(WeiXin Script)是小程序的一套脚本语言,结合 WXML,可以构建出页面的结构
    • 官方:WXS 与 JavaScript 是不同的语言,有自己的语法,并不和 JavaScript 一致。(不过基本一致)
  • 为什么要设计 WXS 语言呢?
    • 在 WXML 中是不能直接调用 Page/Component 中定义的函数的
    • 但是某些情况,我们可以希望使用函数来处理 WXML 中的数据(类似于 Vue 中的过滤器),这个时候就使用 WXS 了
  • WXS 使用的限制和特点
    • WXS 不依赖于运行时的基础库版本,可以在所有版本的小程序中运行
    • WXS 的运行环境和其他 JavaScript 代码是隔离的,WXS 中不能调用其他 JavaScript 文件中定义的函数,也不能调用小程序提供的 API
    • 由于运行环境的差异,在 iOS 设备上小程序内的 WXS 会比 JavaScript 代码快 2 ~ 20 倍。在 android 设备 上二者运行效率无差异

WXS 的写法

  • WXS 有两种写法

    • 写在<wxs>标签中
    • 写在以.wxs 结尾的文件中
  • <wxs>标签的属性

    属性名类型默认值说明
    moduleString当前 <wxs> 标签的模块名。必填字段。
    srcString引用 .wxs 文件的相对路径。仅当本标签为单闭合标签标签的内容为空时有效
  • 每一个 .wxs 文件和 <wxs> 标签都是一个单独的模块

    • 每个模块都有自己独立的作用域。即在一个模块里面定义的变量与函数,默认为私有的,对其他模块不可见
    • 一个模块要想对外暴露其内部的私有变量与函数,只能通过 module.exports 实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <!-- 1.方式一: 标签 -->
    <!--
    <wxs module="format">
    function Price(price) {
    return "¥" + (price + 1)
    }

    // 必须导出后, 才能被其他地方调用: 必须使用CommonJS导出
    module.exports = { Price: Price }
    </wxs>
    -->

    <!-- 2.方式二: 独立的文件, 通过src引入 -->
    <wxs module="format" src="/utils/format.wxs"></wxs>

    <view>
    <block wx:for="{{10}}" wx:key="*this">
    <view>{{format.Price(item)}}</view>
    </block>
    </view>

小程序的事件处理

事件的监听

  • 什么时候会产生事件呢?

    • 小程序需要经常和用户进行某种交互,比如点击界面上的某个按钮或者区域,比如滑动了某个区域
    • 事件是视图层到逻辑层的通讯方式
    • 事件可以将用户的行为反馈到逻辑层进行处理
    • 事件可以绑定在组件上,当触发事件时,就会执行逻辑层中对应的事件处理函数
    • 事件对象可以携带额外信息,如 id,dataset,touches
  • 事件时如何处理呢?

    • 事件是通过 bind/catch 这个属性绑定在组件上的的(和普通的属性写法很相似,以 key=“value”形式)
    • key 以 bind 或 catch 开头,从 1.5.0 版本开始,可以在 bind 和 catch 后加上一个冒号
    • 同时在当前页面的 Page 构造器中定义对应的事件处理函数,如果没有对应的函数,触发事件时会报错
    • 比如当用户点击该 button 区域时,达到触发条件生成事件 tap,该事件处理函数会被执行,同时还会收到一个事件对象 event

组件的特殊事件

  • 某些组件会有自己特性的事件类型,可以在使用组件时具体查看对应的文档

    • 比如 input 有bindinput/bindblur/bindfocus
    • 比如 scroll-view 有bindscrolltowpper/bindscrolltolower
  • 这里我们讨论几个组件都有的,并且也比较常见的事件类型

    • WXML 的冒泡事件列表
    类型触发条件最低版本
    touchstart手指触摸动作开始
    touchmove手指触摸后移动
    touchcancel手指触摸动作被打断,如来电提醒,弹窗
    touchend手指触摸动作结束
    tap手指触摸后马上离开
    longpress手指触摸后,超过 350ms 再离开,如果指定了事件回调函数并触发了这个事件,tap 事件将不被触发1.5.0
    longtap
  • https://developers.weixin.qq.com/miniprogram/dev/framework/view/wxml/event.html#事件详解

事件对象

  • 当某个事件触发时,会产生一个事件对象,并且这个对象被传入到回调函数中,事件对象有哪些常见的属性呢?

  • BaseEvent 基础事件对象属性列表

    属性类型说明基础库版本
    typeString事件类型
    timeStampInteger事件生成时的时间戳
    targetObject触发事件的组件的一些属性值集合
    currentTargetObject当前组件的一些属性值集合
    markObject事件标记数据2.7.1

target 和 currentTarget 的区别

  • target:触发事件的组件

  • currentTarget:处理事件的组件

    1
    2
    3
    4
    5
    6
    7
    <view id="orange" class="outer" data-name="shy" bindtap="onOuterViewTap">
    <view id="red" class="inner"></view>
    </view>

    <view id="orange" class="outer" data-name="shy" bindtap="onOuterViewTap">
    <view id="red" class="inner" bindtap="onInnerViewTap"></view>
    </view>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    .outer {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 400rpx;
    height: 400rpx;
    background-color: orange;
    margin-bottom: 40rpx;
    }

    .inner {
    width: 200rpx;
    height: 200rpx;
    background-color: red;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    Page({
    data: {},
    onOuterViewTap(event) {
    // 1.target触发事件的组件
    // 2.currentTarget处理事件的组件
    console.log("orange:target", event.target);
    console.log("orange:currentTarget", event.currentTarget);

    // 3.获取自定义属性: name
    const name = event.currentTarget.dataset.name;
    console.log(name);
    },
    onInnerViewTap(event) {
    console.log("red:target", event.target);
    console.log("red:currentTarget", event.currentTarget);
    },
    });

touches 和 changedTouches 的区别

  • 在 touchend 中不同

  • 多手指触摸时不同

    1
    2
    3
    4
    5
    6
    7
    8
    <view
    class="touches"
    bindtap="onTouchTap"
    bindlongpress="onLongPress"
    bindtouchend="onTouchEnd"
    >
    多指触摸
    </view>
    1
    2
    3
    4
    5
    6
    7
    8
    .touches {
    height: 100rpx;
    color: white;
    }

    .touches {
    background-color: green;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Page({
    data: {},
    // 监听触摸事件
    onTouchTap(event) {
    console.log("tap:", event);
    },
    onLongPress(event) {
    console.log("long:", event);
    },
    onTouchEnd(event) {
    console.log("end:", event);
    },
    });

事件参数的传递

  • 当视图层发生事件时,某些情况需要事件携带一些参数到执行的函数中,这个时候就可以通过 data-属性来完成

    • 格式:data-属性的名称
    • 获取:e.currentTarget.dataset.属性的名称
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <view
    class="arguments"
    bindtap="onArgumentsTap"
    data-name="shy"
    data-age="24"
    data-height="1.88"
    >
    参数传递
    </view>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    .touches,
    .arguments {
    height: 100rpx;
    color: white;
    }

    .touches {
    background-color: green;
    }

    .arguments {
    background-color: purple;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    Page({
    data: {},
    // 监听事件, 并且传递参数
    onArgumentsTap(event) {
    console.log("onArgumentsTap:", event);
    const { name, age, height } = event.currentTarget.dataset;
    console.log(name, age, height);
    },
    });

事件冒泡和事件捕获

  • 当界面产生一个事件时,事件分为了捕获阶段冒泡阶段

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <view class="view1" capture-bind:tap="onView1CaptureTap" bindtap="onView1Tap">
    <view
    class="view2"
    capture-bind:tap="onView2CaptureTap"
    bindtap="onView2Tap"
    >
    <view
    class="view3"
    capture-bind:tap="onView3CaptureTap"
    bindtap="onView3Tap"
    ></view>
    </view>
    </view>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    .view1 {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 600rpx;
    height: 600rpx;
    background-color: orange;
    }

    .view2 {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 400rpx;
    height: 400rpx;
    background-color: purple;
    }

    .view3 {
    width: 200rpx;
    height: 200rpx;
    background-color: red;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    Page({
    data: {},
    // 捕获和冒泡过程
    onView1CaptureTap() {
    console.log("onView1CaptureTap");
    },
    onView2CaptureTap() {
    console.log("onView2CaptureTap");
    },
    onView3CaptureTap() {
    console.log("onView3CaptureTap");
    },
    onView1Tap() {
    console.log("onView1Tap");
    },
    onView2Tap() {
    console.log("onView2Tap");
    },
    onView3Tap() {
    console.log("onView3Tap");
    },
    });

小程序的组件化开发

小程序组件化开发

  • 小程序在刚刚推出时是不支持组件化的,也是为人诟病的一个点
    • 但是从 v1.6.3 开始,小程序开始支持自定义组件开发,也让我们更加方便的在程序中使用组件化
  • 组件化思想的应用
    • 有了组件化的思想,我们在之后的开发中就要充分的利用它
    • 尽可能的将页面拆分成一个个小的、可复用的组件
    • 这样让我们的代码更加方便组织和管理,并且扩展性也更强

创建一个组件

  • 自定义组件的步骤

    • 首先需要在 json 文件中进行自定义组件声明(将 component 字段设为 true 可将这一组文件设为自定义组件)
    • 在 wxml 中编写属于我们组件自己的模板
    • 在 wxss 中编写属于我们组件自己的相关样式
    • 在 js 文件中,可以定义数据或组件内部的相关逻辑
    1
    2
    3
    4
    <view class="section">
    <view class="title">我是组件标题</view>
    <view class="content">我是组件内容,哈哈哈哈</view>
    </view>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    .section .title {
    font-size: 40rpx;
    font-weight: 700;
    color: red;
    }

    .section .content {
    font-size: 24rpx;
    color: purple;
    }
    1
    2
    3
    4
    {
    "component": true,
    "usingComponents": {}
    }
    1
    <section-info></section-info> <section-info></section-info>
    1
    2
    3
    4
    5
    {
    "usingComponents": {
    "section-info": "/components/section-info/section-info"
    }
    }

使用自定义组件和细节注意事项

  • 一些需要注意的细节
    • 自定义组件也是可以引用自定义组件的,引用方法类似于页面引用自定义组件的方式(使用 usingComponents 字段)
    • 自定义组件和页面所在项目根目录名 不能以 “wx-” 为前缀,否则会报错
    • 如果在 app.json 的 usingComponents 声明某个组件,那么所有页面和组件可以直接使用该组件

组件的样式细节

  • 课题一:组件内的样式 对 外部样式 的影响

    • 结论一:组件内的 class 样式,只对组件 wxml 内的节点生效,对于引用组件的 Page 页面不生效
    • 结论二:组件内不能使用 id 选择器、属性选择器、标签选择器
  • 课题二:外部样式 对 组件内样式 的影响

    • 结论一:外部使用 class 的样式,只对外部 wxml 的 class 生效,对组件内是不生效的
    • 结论二:外部使用了 id 选择器、属性选择器不会对组件内产生影响
    • 结论三:外部使用了标签选择器,会对组件内产生影响
  • 课题三:如何让 class 可以相互影响

    • Component 对象中,可以传入一个options 属性,其中 options 属性中有一个styleIsolation(隔离)属性
    • styleIsolation 有三个取值:
      • isolated 表示启用样式隔离,在自定义组件内外,使用 class 指定的样式将不会相互影响(默认取值)
      • apply-shared 表示页面 wxss 样式将影响到自定义组件,但自定义组件 wxss 中指定的样式不会影响页面
      • shared 表示页面 wxss 样式将影响到自定义组件,自定义组件 wxss 中指定的样式也会影响页面和其他设置了
    1
    2
    <view>----------测试样式的组件-----------</view>
    <view class="title">我是 组件 的标题</view>
    1
    2
    3
    .title {
    color: pink;
    }
    1
    2
    3
    4
    5
    Component({
    options: {
    styleIsolation: "shared",
    },
    });
    1
    <test-style /> <view class="title">我是 页面 的标题</view>
    1
    2
    3
    .title {
    font-size: 40rpx;
    }
    1
    2
    3
    4
    5
    {
    "usingComponents": {
    "test-style": "/components/test-style/test-style"
    }
    }

向组件传递数据 - properties

  • 给组件传递数据

    • 大部分情况下,组件只负责布局和样式,内容是由使用组件的对象决定的
    • 所以,我们经常需要从外部传递数据给我们的组件,让我们的组件来进行展示
  • 如何传递呢?

    • 使用properties 属性
  • 支持的类型:String、Number、Boolean、Object、Array、null(不限制类型)

  • 默认值:可以通过value 设置默认值

    1
    2
    3
    4
    <view class="section">
    <view class="title">{{ title }}</view>
    <view class="content">{{ content }}</view>
    </view>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    Component({
    properties: {
    title: {
    type: String,
    value: "默认标题",
    },
    content: {
    type: String,
    value: "默认内容",
    },
    },
    data: {},
    methods: {},
    });
    1
    2
    3
    4
    5
    6
    7
    8
    <section-info
    title="我与地坛"
    content="要是有些事我没说,地坛,你别以为是我忘了,我什么也没忘,但是有些事只适合收藏"
    ></section-info>
    <section-info
    title="黄金时代"
    content="在我一生的黄金时代,我有好多奢望。我想爱,想吃,还想在一瞬间变成天上半明半暗的云"
    ></section-info>

向组件传递样式 - externalClasses

  • 给组件传递样式:有时候,我们不希望将样式在组件内固定不变,而是外部可以决定样式

  • 这个时候,我们可以使用 externalClasses 属性:

    • 在 Component 对象中,定义externalClasses 属性
    • 在组件内的 wxml 中使用 externalClasses 属性中的 class
    • 在页面中传入对应的 class,并且给这个 class 设置样式
    1
    2
    3
    4
    <view class="section">
    <view class="title">{{ title }}</view>
    <view class="content info">{{ content }}</view>
    </view>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    Component({
    properties: {
    title: {
    type: String,
    value: "默认标题",
    },
    content: {
    type: String,
    value: "默认内容",
    },
    },
    externalClasses: ["info"],
    data: {},
    methods: {},
    });
    1
    2
    <section-info info="info"></section-info>
    <section-info info="info"></section-info>
    1
    2
    3
    .info {
    background-color: #0f0;
    }

组件向外传递事件 – 自定义事件

  • 有时候是自定义组件内部发生了事件,需要告知使用者,这个时候可以使用自定义事件:

    1
    2
    3
    4
    <view class="section">
    <view class="title" bindtap="onTitleTap">{{ title }}</view>
    <view class="content info">{{ content }}</view>
    </view>
    1
    2
    3
    4
    5
    6
    7
    8
    Component({
    methods: {
    onTitleTap() {
    console.log("title被点击了~");
    this.triggerEvent("titleclick", "哈哈哈哈");
    },
    },
    });
    1
    <section-info bind:titleclick="onSectionTitleClick"></section-info>
    1
    2
    3
    4
    5
    Page({
    onSectionTitleClick(event) {
    console.log(event);
    },
    });

页面直接调用组件方法

  • 可在父组件里调用 this.selectComponent,获取子组件的实例对象

    • 调用时需要传入一个匹配选择器 selector,如:this.selectComponent(“.section-info”)
    1
    2
    3
    4
    5
    <section-info class="section-info" />

    <button size="mini" type="primary" bindtap="getTheInstance">
    获取子组件的实例对象
    </button>
    1
    2
    3
    4
    5
    6
    7
    getTheInstance() {
    // 1.获取对应的组件实例对象
    const sectionInfo = this.selectComponent(".section-info");

    // 2.调用组件实例的方法
    sectionInfo.onTitleTap();
    },

什么是插槽

  • slot 翻译为插槽
    • 在生活中很多地方都有插槽,电脑的 USB 插槽,插板当中的电源插槽
    • 插槽的目的是让我们原来的设备具备更多的扩展性
    • 比如电脑的 USB 我们可以插入 U 盘、硬盘、手机、音响、键盘、鼠标等等
  • 组件的插槽
    • 组件的插槽也是为了让我们封装的组件更加具有扩展性
    • 让使用者可以决定组件内部的一些内容到底展示什么
  • 栗子:移动网站中的导航栏
    • 移动开发中,几乎每个页面都有导航栏
    • 导航栏我们必然会封装成一个组件,比如 nav-bar 组件
    • 一旦有了这个组件,我们就可以在多个页面中复用了
  • 但是,每个页面的导航是一样的吗?

单个插槽的使用

  • 除了内容和样式可能由外界决定之外,也可能外界想决定显示的方式

    • 比如我们有一个组件定义了头部和尾部,但是中间的内容可能是一段文字,也可能是一张图片,或者是一个进度条
    • 在不确定外界想插入什么其他组件的前提下,我们可以在组件内预留插槽
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <view class="slot-wrap">
    <view class="header">Header</view>
    <view class="content">
    <!-- 小程序中插槽是不支持默认值的 -->
    <slot></slot>
    </view>
    <view class="default">默认内容</view>
    <view class="footer">Footer</view>
    </view>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    .slot-wrap {
    margin-bottom: 20px;
    }

    .default {
    display: none;
    }

    .content:empty + .default {
    display: block;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <my-slot>
    <text>哈哈哈哈</text>
    </my-slot>

    <my-slot>
    <image src="/assets/zznh.png" mode="widthFix"></image>
    </my-slot>

    <my-slot></my-slot>
    1
    2
    3
    4
    5
    {
    "usingComponents": {
    "my-slot": "/components/slot/slot"
    }
    }

多个插槽的使用

  • 有时候为了让组件更加灵活,我们需要定义多个插槽

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <view class="multiple-slot">
    <view class="left">
    <slot name="left"></slot>
    </view>
    <view class="center">
    <slot name="center"></slot>
    </view>
    <view class="right">
    <slot name="right"></slot>
    </view>
    </view>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    .multiple-slot {
    display: flex;
    text-align: center;
    }

    .left,
    .right {
    width: 160rpx;
    }

    .center {
    flex: 1;
    }
    1
    2
    3
    4
    5
    <my-slot>
    <view slot="left">left</view>
    <view slot="center">center</view>
    <view slot="right">right</view>
    </my-slot>
    1
    2
    3
    4
    5
    {
    "usingComponents": {
    "my-slot": "/components/slot/slot"
    }
    }

behaviors

  • behaviors 是用于组件间代码共享的特性,类似于一些编程语言中的 “mixins”

    • 每个 behavior 可以包含一组属性、数据、生命周期函数和方法
    • 组件引用它时,它的属性、数据和方法会被合并到组件中,生命周期函数也会在对应时机被调用
    • 每个组件可以引用多个 behavior ,behavior 也可以引用其它 behavior
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    export const counterBehavior = Behavior({
    data: {
    counter: 100,
    },
    methods: {
    increment() {
    this.setData({ counter: this.data.counter + 1 });
    },
    decrement() {
    this.setData({ counter: this.data.counter - 1 });
    },
    },
    });
    1
    2
    3
    4
    5
    <view>
    <view class="counter">当前计数: {{counter}}</view>
    <button bindtap="increment">+1</button>
    <button bindtap="decrement">-1</button>
    </view>
    1
    2
    3
    4
    5
    import { counterBehavior } from "../../behaviors/counter";

    Component({
    behaviors: [counterBehavior],
    });

组件的生命周期

  • 组件的生命周期,指的是组件自身的一些函数,这些函数在特殊的时间点或遇到一些特殊的框架事件时被自动触发

    • 其中,最重要的生命周期是 created attached detached ,包含一个组件实例生命流程的最主要时间点
  • 自小程序基础库版本 2.2.3 起,组件的的生命周期也可以在 lifetimes 字段内进行声明(这是推荐的方式,其优先级最高)

  • https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/lifetimes.html

    生命周期参数描述最低版本
    created在组件实例刚刚被创建时执行1.6.3
    attached在组件实例进入页面节点树时执行1.6.3
    ready在组件在视图层布局完成后执行1.6.3
    moved在组件实例被移动到节点树另一个位置时执行1.6.3
    detached在组件实例被从页面节点树移除时执行1.6.3
    errorObject Error每当组件方法抛出错误时执行2.4.1
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Component({
    lifetimes: {
    created() {
    console.log("组件被创建created");
    },
    attached() {
    console.log("组件被添加到组件树中attached");
    },
    detached() {
    console.log("组件从组件树中被移除detached");
    },
    },
    });

组件所在页面的生命周期

  • 还有一些特殊的生命周期,它们并非与组件有很强的关联,但有时组件需要获知,以便组件内部处理

    • 这样的生命周期称为 “组件所在页面的生命周期”,在 pageLifetimes 定义段中定义
  • 其中可用的生命周期包括

    生命周期参数描述最低版本
    show组件所在的页面被展示时执行2.2.3
    hide组件所在的页面被隐藏时执行2.2.3
    resizeObject Size组件所在的页面尺寸变化时执行2.4.0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Component({
    pageLifetimes: {
    show() {
    console.log("page show");
    },
    hide() {
    console.log("page hide");
    },
    },
    });

网络请求域名配置

  • 每个微信小程序需要事先设置通讯域名,小程序只可以跟指定的域名进行网络通信
    • 小程序登录后台 – 开发管理 – 开发设置 – 服务器域名
  • 服务器域名请在 「小程序后台 - 开发 - 开发设置 - 服务器域名」 中进行配置,配置时需要注意
    • 域名只支持 https (wx.request、wx.uploadFile、wx.downloadFile) 和 wss (wx.connectSocket) 协议
    • 域名不能使用 IP 地址(小程序的局域网 IP 除外)或 localhost
    • 可以配置端口,如 https://myserver.com:8080 ,但是配置后只能向 https://myserver.com:8080 发起请求。如果向 https://myserver.com https://myserver.com:9091 等 URL 请求则会失败
    • 如果不配置端口。如 https://myserver.com ,那么请求的 URL 中也不能包含端口,甚至是默认的 443 端口也不可以。如果向 https://myserver.com:443 请求则会失败
    • 域名必须经过 ICP 备案
    • 出于安全考虑,api.weixin.qq.com 不能被配置为服务器域名,相关 API 也不能在小程序内调用。 开发者应将 AppSecret 保存到后台服务器中,通过服务器使用 getAccessToken 接口获取 access_token,并调用相关 API
    • 不支持配置父域名,使用子域名
 评论