微信小程序入门与实战

文章目录

  • 微信小程序入门与实战
    • 1 初识微信小程序
      • 1-1 2020版重录说明
      • 1-2 下载小程序开发工具
      • 1-3 新建小程序项目
      • 1-4 小程序appid的注册
      • 1-5 新版小程序开发工具的一些基本设置
      • 1-6 小程序的基本单位-Page页面
    • 2 小程序的基本目录结构与文件作用剖析
      • 2-1 小程序页面的4种基本文件类型详解
      • 2-2 小程序的全局配置文件、全局样式表和应用程序级别 js 文件
      • 2-3 认识一下我们要开发的Flower项目
    • 3 rpx响应式单位与flex布局
      • 3-1 JSON对象与JS对象的区别(重点)
      • 3-2 作业 目的:充分学习Flex布局的技巧
        • Flex 布局教程:语法篇
          • 一、Flex 布局是什么?
          • 二、基本概念
          • 三、容器的属性
            • 3.1 flex-direction属性
            • 3.2 flex-wrap属性
            • 3.3 flex-flow
            • 3.4 justify-content属性
            • 3.5 align-items属性
            • 3.6 align-content属性
          • 四、项目的属性
            • 4.1 order属性
            • 4.2 flex-grow属性
            • 4.3 flex-shrink属性
            • 4.4 flex-basis属性
            • 4.5 flex属性
            • 4.6 align-self属性
        • Flex 布局教程:实例篇
          • 一、骰子的布局
            • 1.1 单项目
            • 1.2 双项目
            • 1.4 四项目
            • 1.5 六项目
            • 1.6 九项目
          • 二、网格布局
            • 2.1 基本网格布局
            • 2.2 百分比布局
          • 三、圣杯布局
          • 四、输入框的布局
          • 五、悬挂式布局
          • 六、固定的底栏
          • 七,流式布局
      • 3-3 新建页面的技巧与规则
      • 3-4 image标签显示一张图片
      • 3-5 小程序rpx响应式单位的特点(非常重要)
      • 3-6 分离CSS样式到WXSS文件中
      • 3-7 初识flex布局进行垂直分布布局
      • 3-8 flex布局的align-items
      • 3-9 自己编写一个 Button 组件
      • 3-10 聊聊小程序的方便性与灵活性的悖论
    • 4 阅读列表与setData数据绑定
      • 4-1 LinUI组件库介绍
      • 4-2 安装、编译 LinUI
        • 安装
          • 方式一: 使用npm安装 (推荐)
          • 方式二:下载代码
      • 4-3 如何使用自定义组件
      • 4-4 l-avatar头像和昵称组件
      • 4-5 添加新的编译模式
      • 4-6 初步了解Swiper和Swiper-Item组件
      • 4-7 Swiper组件(1)通过插槽设置轮播内容
      • 4-8 Swiper组件(3)Swiper组件内容的高宽设置技巧
      • 4-9 属性设置一定要注意字符串以 JS 表达式的区别(重要)
      • 4-10 布尔属性值赋值的良好建议
      • 4-11 Swiper组件的其他重要属性设置
      • 4-12 用Flex布局组织布局思路(1)
      • 4-13 用Flex布局组织布局思路(2)
      • 4-14 用Flex布局组织布局思路(3)
      • 4-15 用Flex布局组织布局思路(4)
      • 4-16 用Flex布局组织布局思路(5)
    • 5 条件渲染、列表渲染与小程序事件
      • 5-1 使用LinUI的Icon组件代替图片ICON
      • 5-2 LinUI Icon组件的颜色和大小设置
      • 5-3 小程序开发数据的流向(1)
      • 5-4 单向数据绑定与双向数据绑定
      • 5-5 数据绑定与setData函数(1)
        • 1 初始化数据绑定
        • 2 数据绑定更新
      • 5-6 DOM优先 VS 数据优先/数据驱动
      • 5-7 作业 目的:提前了解 ES6 的常用语法
      • 5-8 理解Page.data与this.SetData的关系
      • 5-9 预先在data中定义绑定数据的初始值是值得推荐的做法
      • 5-10 小程序的生命周期函数与特殊回调函数
      • 5-11 数据绑定的实战应用
      • 5-12 Mustache语法解析
      • 5-13 条件渲染
      • 5-14 列表渲染(上)
      • 5-15 列表渲染(下)
      • 5-16 什么是事件
      • 5-17 bind来捕捉事件执行回调函数
    • 6 路由函数与事件冒泡
      • 6-1 路由函数 NavigateTo 和 RedirectTo 的区别
      • 6-2 作业 目的:JS模块的概念与导入导出的方式
      • 6-3 Catch 与 Bind 事件的区别
      • 6-4 js模块的导入导出(require与import)_1
      • 6-5 用列表渲染展示导入的数组数据
      • 6-6 从列表页面跳转到文章详情页面
      • 6-7 列表渲染的wxkey赋值规则
      • 6-8 事件冒泡的具体应用
    • 7 构建阅读详情页面
      • 7-1 构建文章详情页面(1)
      • 7-2 构建文章详情页面(2)
      • 7-3 构建文章详情页面(3)
      • 7-4 Flex的高级应用(1)
      • 7-5 Flex高级应用(2)主轴和交叉轴
      • 7-6 Flex高级应用(3)
      • 7-7 组件的自定义属性data
      • 7-8 自定义属性data-的命名规则
      • 7-9 在页面的onLoad函数中获取查询参数
      • 7-10 加载详情数据并填充页面
      • 7-11 为什么只传post-id
    • 8 缓存机制与异步API的async和await
      • 8-1 app.js 的意义和作用
        • **开发者文档**
        • **App 参考文档**
        • AppObject getApp(Object object)
      • 8-2 目的:了解缓存的概念
        • 4.6 本地数据缓存
      • 8-3 在 app.js 中保存全局变量
      • 8-4 作业 目的: 熟悉 async 和 await 的用法和意义
      • 8-5 小程序缓存的增删改查与清除 (同步)
      • 8-6 异步函数的几个方案:回调函数、promise 与 await
      • 8-7 文章收藏(1)分析思路
      • 8-8 文章收藏(2)JS的动态属性
      • 8-9 收藏未收藏的切换
      • 8-10 初始化收藏状态
      • 8-11 缓存如何不被覆盖?
      • 8-12 同步文章缓存状态
      • 8-13 showToast 接口的应用
      • 8-14 牢记setData对于data属性的影响
      • showToast更换成showModal(1)
        • **wx.showModal(Object object)**
        • 自己尝试编写
      • 8-16 showModal的回调函数与Promise
      • 8-17 showActionSheet的使用
    • 9 音乐播放
      • 9-1 浮动居中方案-通过 left 和 top 定位音乐图标
      • 9-2 小程序音乐播放API介绍
          • **wx.playVoice(Object object)**
          • wx.pauseVoice(Object object)
          • wx.stopVoice(Object object)
          • **InnerAudioContext wx.createInnerAudioContext(Object object)**
        • **InnerAudioContext 文档**
          • **属性**
          • **方法**
      • 9-3 小试音乐播放API
        • 自己尝试
        • **老师讲解**
      • 9-4 切换音乐播放图标的两种方案:条件渲染与 js 表达式
      • 9-5 音乐播放状态切换
      • 9-6 背景音乐的监听相关API
      • 9-7 同步音乐总控开关与自有播放开光的状态
      • 9-8 音乐控制面板的暂停与停止逻辑
      • 9-9 全局变量解决音乐播放状态初始化不正确的问题
      • 9-10 分析一个不是问题的问题
      • 9-11 让每篇文章音乐独立显示状态
    • 10 初识小程序的自定义组件
      • 10-1 文章列表顶部轮播图跳转
      • 10-2 小程序tabBar选项卡配置基础
      • 10-3 小程序tabBar的其他配置选项
      • 10-4 跳转到带有选项卡的页面需要使用 switchTab
      • 10-5 初识自定义组件
      • 10-6 新建第一个自定义组件
      • 10-7 创建自定义组件的属性
      • 10-8 自定义组件属性的简化定义
      • 10-9 自定义属性可以接收一个 Object 对象
      • 10-10 分离文章到单独的自定义组件中
      • 10-11 自定义组件的嵌套引用
    • 11 电影页面与实战自定义组件
      • 11-1 Movie自定义组件的构建
      • 11-2 使用 LinUI 的评分组件快速实现分数预览
      • 11-3 简易评分组件的实现思路(选看)
      • 11-4 巧用Flex布局的Space-Between进行分布排列
      • 11-5 调整自定义组件间距
      • 11-6 外部样式类externalClasses的使用
      • 11-7 小试牛刀访问服务端数据
        • wx.request 使用
      • 11-8 从服务器加载数据分页数据并传入自定义组件
      • 11-9 使用ES6箭头函数解决this指代的问题
      • 11-10 绑定电影数据(1)两种不同的评分方式
      • 11-11 绑定服务端电影数据(2)
      • 11-12 完成即将上映和 top250
    • 12 电影列表与电影搜索
      • 12-1 wx.request的更多参数讲解
        • 微信官方文档
      • 12-2 更多电影(1)分析更多电影页面的逻辑
      • 12-3 更多电影(2)加载更多数据
      • 12-4 更多电影(3)Flex-Wrap 的应用
        • 3.2 flex-wrap属性
      • 12-5 更多电影(4)Flex布局对于普通样式类的影响
      • 12-6 更多电影(5)加载不同类型的电影数据
      • 12-7 使用LinUI快速构建搜索栏
      • 12-8 向服务器请求搜索数据
      • 12-9 搜索结果与电影数据的切换显示
      • 12-10 显示搜索的电影数据
      • 12-11 修复 Space-Between 2个元素两端分布的问题
    • 13 电影详情与滑动加载数据、下拉刷新数据
      • 13-1 上滑加载更多数据(1)onReachBottom
      • 13-2 上滑加载更多数据(2)showloading提示
      • 13-3 下拉刷新数据(1)
      • 13-4 配置标题与动态配置标题
      • 13-5 谈组件的独立性
      • 13-6 自定义组件的自定义事件产生
      • 13-7 获取自定义组件的detail参数
      • 13-8 同时获取自定义属性和自定义组件的detail参数
      • 13-9 电影详情页面(1)获取电影详情数据
      • 13-10 电影详情页面(2)顶部区域
      • 13-11 电影详情页面(3)头部元素浮动技巧
      • 13-12 电影详情页面(4)图片预览效果只做
      • 13-13 电影详情页(5)图片的多种 mode 模式
      • 13-14 数据预处理
      • 13-15 电影简介部分的CSS构建
      • 13-16 处理影人信息
      • 13-17 多层Flex布局的嵌套应用
      • 13-18 调整影人信息的CSS细节
      • 13-19 Scroll-View 组件的应用与结束语
      • 13-15 电影简介部分的CSS构建
      • 13-16 处理影人信息
      • 13-17 多层Flex布局的嵌套应用
      • 13-18 调整影人信息的CSS细节
      • 13-19 Scroll-View 组件的应用与结束语

视频地址:https://coding.imooc.com/class/chapter/75.html#Anchor

个人源码地址:暂未公布

1 初识微信小程序

本章,我们初步了解什么是微信小程序。同时,我们会下载微信小程序工具、申请appid,并进行一些列开发的准备工作。

1-1 2020版重录说明

  • 自定义组件
  • 小程序组件库的使用(LinUI)
  • 一些新特性

1-2 下载小程序开发工具

初学者建议:稳定版

1-3 新建小程序项目

AppID

学习时可使用测试号,但建议申请一个账号,因为测试号使用的能力有限

小程序云开发(无服务端开发模式,适合仅需要小程序平台的小项目) 或 不使用云服务(传统 Web 开发,服务端需要后端开发,适合大项目)

1-4 小程序appid的注册

1-5 新版小程序开发工具的一些基本设置

一些重要设置

编译器:

  • Tab 大小
  • 自动保存

项目设置:

  • 本地设置:增强编译、将 JS 编译成 ES5(新版本将二者合一了)、不校验合法域名(在小程序上线时再取消勾选)一定要勾选上,其他保留默认

1-6 小程序的基本单位-Page页面

不是放在 page 目录下的才是页面,具体哪些文件夹是页面,由 app.json 配置

{"pages":["pages/index/index","pages/logs/logs"]
}

小程序是基于组件开发而非基于页面开发。

utils 是自定义逻辑函数的目录(非必须)。可以在小程序下新建任意需要的目录和文件。

一个最基本的页面由四个文件构成。(js wxml wxss json)

2 小程序的基本目录结构与文件作用剖析

本章我们会了解小程序官方种子项目的目录结构,并介绍Page页面。同时我们还需要了解页面级别的文件和应用程序级别的文件有什么区别。

2-1 小程序页面的4种基本文件类型详解

wxml:与 html 类似,结构

wxss:与 css 类似,样式

js:与 JavaScript 类似,逻辑

json:小程序用作配置(独特设定),例如,小程序顶部状态栏颜色、页面背景颜色

在相同页面目录下,按照这四个拓展名,依次新建四个相同页面名的文件,即可以自动引用,无需像网页开发那样额外引用。

2-2 小程序的全局配置文件、全局样式表和应用程序级别 js 文件

app.js app.json app.wxss

这三个文件的文件名不能随意修改。它们是应用程序级别(全局)的逻辑、样式、配置,会在所有页面中生效。

如果在 app.wxss 中写入

text {color: blue;
}

则所有页面的文本都会变成蓝色。对于特定页面的文本颜色样式,可以在特定页面的 wxss 文件中修改。js/json 的逻辑或配置的作用范围与此类似。如,在全局范围内修改顶部导航栏的标题

{..."window":{..."navigationBarTitleText": "Weixin",...},
...
}

而在 logs 页面单独设置标题

{"navigationBarTitleText": "查看启动日志",...
}

2-3 认识一下我们要开发的Flower项目

3 rpx响应式单位与flex布局

本章我们通过制作第一个welcome欢迎页面来学习如何在小程序中做响应式。同时,我们需要学习非常重要的布局语法——flex布局。这是在小程序中用的最多也是 最为好用的布局方案。

3-1 JSON对象与JS对象的区别(重点)

删除文件(pages、utils、app.js、app.json、app.wxss),从零开始。

新建一个 app.json 文件(必需),还是在原位置。此时报错:

[ app.json.json 文件错误] app.json: Empty file is NOT a valid json file
(env: Windows,mp,1.05.2111084; lib: 2.19.4)

所以需要在里头加上:

{}

即使什么都不写(空),也要加上,否则在格式方面会报错。至于其他内容,晚点添加。

3-2 作业 目的:充分学习Flex布局的技巧

来源:

阮一峰的博客

Flex 布局教程:实例篇 - 阮一峰的网络日志 (ruanyifeng.com)

Flex 布局教程:语法篇 - 阮一峰的网络日志 (ruanyifeng.com)

Flex 布局教程:语法篇

网页布局(layout)是 CSS 的一个重点应用。

布局的传统解决方案,基于盒状模型,依赖 display 属性 + position属性 + float属性。它对于那些特殊布局非常不方便,比如,垂直居中就不容易实现。

2009年,W3C 提出了一种新的方案----Flex 布局,可以简便、完整、响应式地实现各种页面布局。目前,它已经得到了所有浏览器的支持,这意味着,现在就能很安全地使用这项功能。

Flex 布局将成为未来布局的首选方案。本文介绍它的语法,下一篇文章给出常见布局的 Flex 写法。网友 JailBreak 为本文的所有示例制作了 Demo,也可以参考。

以下内容主要参考了下面两篇文章:A Complete Guide to Flexbox 和 A Visual Guide to CSS3 Flexbox Properties。

一、Flex 布局是什么?

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

任何一个容器都可以指定为 Flex 布局。

.box{display: flex;
}

行内元素也可以使用 Flex 布局。

.box{display: inline-flex;
}

Webkit 内核的浏览器,必须加上-webkit前缀。

.box{display: -webkit-flex; /* Safari */display: flex;
}

注意,设为 Flex 布局以后,子元素的floatclearvertical-align属性将失效。

二、基本概念

采用 Flex 布局的元素,称为 Flex 容器(flex container),简称"容器"。它的所有子元素自动成为容器成员,称为 Flex 项目(flex item),简称"项目"。

容器默认存在两根轴:水平的主轴(main axis)和垂直的交叉轴(cross axis)。主轴的开始位置(与边框的交叉点)叫做main start,结束位置叫做main end;交叉轴的开始位置叫做cross start,结束位置叫做cross end

项目默认沿主轴排列。单个项目占据的主轴空间叫做main size,占据的交叉轴空间叫做cross size

三、容器的属性

以下6个属性设置在容器上。

  • flex-direction
  • flex-wrap
  • flex-flow
  • justify-content
  • align-items
  • align-content
3.1 flex-direction属性

flex-direction属性决定主轴的方向(即项目的排列方向)。

.box {flex-direction: row | row-reverse | column | column-reverse;
}

它可能有4个值。

  • row(默认值):主轴为水平方向,起点在左端。
  • row-reverse:主轴为水平方向,起点在右端。
  • column:主轴为垂直方向,起点在上沿。
  • column-reverse:主轴为垂直方向,起点在下沿。
3.2 flex-wrap属性

默认情况下,项目都排在一条线(又称"轴线")上。flex-wrap属性定义,如果一条轴线排不下,如何换行。

.box{flex-wrap: nowrap | wrap | wrap-reverse;
}

它可能取三个值。

(1)nowrap(默认):不换行。

(2)wrap:换行,第一行在上方。

(3)wrap-reverse:换行,第一行在下方。

3.3 flex-flow

flex-flow属性是flex-direction属性和flex-wrap属性的简写形式,默认值为row nowrap

.box {flex-flow: <flex-direction> || <flex-wrap>;
}
3.4 justify-content属性

justify-content属性定义了项目在主轴上的对齐方式。

.box {justify-content: flex-start | flex-end | center | space-between | space-around;
}

它可能取5个值,具体对齐方式与轴的方向有关。下面假设主轴为从左到右。

  • flex-start(默认值):左对齐
  • flex-end:右对齐
  • center: 居中
  • space-between:两端对齐,项目之间的间隔都相等。
  • space-around:每个项目两侧的间隔相等。所以,项目之间的间隔比项目与边框的间隔大一倍。
3.5 align-items属性

align-items属性定义项目在交叉轴上如何对齐。

.box {align-items: flex-start | flex-end | center | baseline | stretch;
}

它可能取5个值。具体的对齐方式与交叉轴的方向有关,下面假设交叉轴从上到下。

  • flex-start:交叉轴的起点对齐。
  • flex-end:交叉轴的终点对齐。
  • center:交叉轴的中点对齐。
  • baseline: 项目的第一行文字的基线对齐。
  • stretch(默认值):如果项目未设置高度或设为auto,将占满整个容器的高度。
3.6 align-content属性

align-content属性定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。

.box {align-content: flex-start | flex-end | center | space-between | space-around | stretch;
}

该属性可能取6个值。

  • flex-start:与交叉轴的起点对齐。
  • flex-end:与交叉轴的终点对齐。
  • center:与交叉轴的中点对齐。
  • space-between:与交叉轴两端对齐,轴线之间的间隔平均分布。
  • space-around:每根轴线两侧的间隔都相等。所以,轴线之间的间隔比轴线与边框的间隔大一倍。
  • stretch(默认值):轴线占满整个交叉轴。
四、项目的属性

以下6个属性设置在项目上。

  • order
  • flex-grow
  • flex-shrink
  • flex-basis
  • flex
  • align-self
4.1 order属性

order属性定义项目的排列顺序。数值越小,排列越靠前,默认为0。

.item {order: <integer>;
}

4.2 flex-grow属性

flex-grow属性定义项目的放大比例,默认为0,即如果存在剩余空间,也不放大。

.item {flex-grow: <number>; /* default 0 */
}

如果所有项目的flex-grow属性都为1,则它们将等分剩余空间(如果有的话)。如果一个项目的flex-grow属性为2,其他项目都为1,则前者占据的剩余空间将比其他项多一倍。

4.3 flex-shrink属性

flex-shrink属性定义了项目的缩小比例,默认为1,即如果空间不足,该项目将缩小。

.item {flex-shrink: <number>; /* default 1 */
}

如果所有项目的flex-shrink属性都为1,当空间不足时,都将等比例缩小。如果一个项目的flex-shrink属性为0,其他项目都为1,则空间不足时,前者不缩小。

负值对该属性无效。

4.4 flex-basis属性

flex-basis属性定义了在分配多余空间之前,项目占据的主轴空间(main size)。浏览器根据这个属性,计算主轴是否有多余空间。它的默认值为auto,即项目的本来大小。

.item {flex-basis: <length> | auto; /* default auto */
}

它可以设为跟widthheight属性一样的值(比如350px),则项目将占据固定空间。

4.5 flex属性

flex属性是flex-grow, flex-shrinkflex-basis的简写,默认值为0 1 auto。后两个属性可选。

.item {flex: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]
}

该属性有两个快捷值:auto (1 1 auto) 和 none (0 0 auto)。

建议优先使用这个属性,而不是单独写三个分离的属性,因为浏览器会推算相关值。

4.6 align-self属性

align-self属性允许单个项目有与其他项目不一样的对齐方式,可覆盖align-items属性。默认值为auto,表示继承父元素的align-items属性,如果没有父元素,则等同于stretch

.item {align-self: auto | flex-start | flex-end | center | baseline | stretch;
}

该属性可能取6个值,除了auto,其他都与align-items属性完全一致。

Flex 布局教程:实例篇

上一篇文章介绍了Flex布局的语法,今天介绍常见布局的Flex写法。

你会看到,不管是什么布局,Flex往往都可以几行命令搞定。

我只列出代码,详细的语法解释请查阅《Flex布局教程:语法篇》。我的主要参考资料是 Landon Schropp 的文章和 Solved by Flexbox。

一、骰子的布局

骰子的一面,最多可以放置9个点。

下面,就来看看Flex如何实现,从1个点到9个点的布局。你可以到codepen查看Demo。

如果不加说明,本节的HTML模板一律如下。

<div class="box">
<span class="item"></span>
</div>

上面代码中,div元素(代表骰子的一个面)是Flex容器,span元素(代表一个点)是Flex项目。如果有多个项目,就要添加多个span元素,以此类推。

1.1 单项目

首先,只有左上角1个点的情况。Flex布局默认就是首行左对齐,所以一行代码就够了。

.box {display: flex;
}

设置项目的对齐方式,就能实现居中对齐和右对齐。

.box {display: flex;
justify-content: center;
}

.box {display: flex;
justify-content: flex-end;
}

设置交叉轴对齐方式,可以垂直移动主轴。

.box {display: flex;
align-items: center;
}

.box {display: flex;
justify-content: center;
align-items: center;
}

.box {display: flex;
justify-content: center;
align-items: flex-end;
}

.box {display: flex;
justify-content: flex-end;
align-items: flex-end;
}
1.2 双项目

.box {display: flex;
justify-content: space-between;
}

.box {display: flex;
flex-direction: column;
justify-content: space-between;
}

.box {display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
}

.box {display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-end;
}

.box {display: flex;
}.item:nth-child(2) {align-self: center;
}

.box {display: flex;
justify-content: space-between;
}.item:nth-child(2) {align-self: flex-end;
}

####### 1.3 三项目

.box {display: flex;
}.item:nth-child(2) {align-self: center;
}.item:nth-child(3) {align-self: flex-end;
}
1.4 四项目

.box {display: flex;
flex-wrap: wrap;
justify-content: flex-end;
align-content: space-between;
}

HTML代码如下。

<div class="box">
<div class="column"><span class="item"></span><span class="item"></span>
</div>
<div class="column"><span class="item"></span><span class="item"></span>
</div>
</div>

CSS代码如下。

.box {display: flex;
flex-wrap: wrap;
align-content: space-between;
}.column {flex-basis: 100%;
display: flex;
justify-content: space-between;
}
1.5 六项目

.box {display: flex;
flex-wrap: wrap;
align-content: space-between;
}

.box {display: flex;
flex-direction: column;
flex-wrap: wrap;
align-content: space-between;
}

HTML代码如下。

<div class="box">
<div class="row"><span class="item"></span><span class="item"></span><span class="item"></span>
</div>
<div class="row"><span class="item"></span>
</div>
<div class="row"><span class="item"></span><span class="item"></span>
</div>
</div>

CSS代码如下。

.box {display: flex;
flex-wrap: wrap;
}.row{flex-basis: 100%;
display:flex;
}.row:nth-child(2){justify-content: center;
}.row:nth-child(3){justify-content: space-between;
}
1.6 九项目

.box {display: flex;
flex-wrap: wrap;
}
二、网格布局
2.1 基本网格布局

最简单的网格布局,就是平均分布。在容器里面平均分配空间,跟上面的骰子布局很像,但是需要设置项目的自动缩放。

HTML代码如下。

<div class="Grid">
<div class="Grid-cell">...</div>
<div class="Grid-cell">...</div>
<div class="Grid-cell">...</div>
</div>

CSS代码如下。

.Grid {display: flex;
}.Grid-cell {flex: 1;
}
2.2 百分比布局

某个网格的宽度为固定的百分比,其余网格平均分配剩余的空间。

HTML代码如下。

<div class="Grid">
<div class="Grid-cell u-1of4">...</div>
<div class="Grid-cell">...</div>
<div class="Grid-cell u-1of3">...</div>
</div>
.Grid {display: flex;
}.Grid-cell {flex: 1;
}.Grid-cell.u-full {flex: 0 0 100%;
}.Grid-cell.u-1of2 {flex: 0 0 50%;
}.Grid-cell.u-1of3 {flex: 0 0 33.3333%;
}.Grid-cell.u-1of4 {flex: 0 0 25%;
}
三、圣杯布局

圣杯布局(Holy Grail Layout)指的是一种最常见的网站布局。页面从上到下,分成三个部分:头部(header),躯干(body),尾部(footer)。其中躯干又水平分成三栏,从左到右为:导航、主栏、副栏。

HTML代码如下。

<body class="HolyGrail">
<header>...</header>
<div class="HolyGrail-body"><main class="HolyGrail-content">...</main><nav class="HolyGrail-nav">...</nav><aside class="HolyGrail-ads">...</aside>
</div>
<footer>...</footer>
</body>

CSS代码如下。

.HolyGrail {display: flex;
min-height: 100vh;
flex-direction: column;
}header,
footer {flex: 1;
}.HolyGrail-body {display: flex;
flex: 1;
}.HolyGrail-content {flex: 1;
}.HolyGrail-nav, .HolyGrail-ads {/* 两个边栏的宽度设为12em */
flex: 0 0 12em;
}.HolyGrail-nav {/* 导航放到最左边 */
order: -1;
}

如果是小屏幕,躯干的三栏自动变为垂直叠加。

@media (max-width: 768px) {.HolyGrail-body {flex-direction: column;flex: 1;
}
.HolyGrail-nav,
.HolyGrail-ads,
.HolyGrail-content {flex: auto;
}
}
四、输入框的布局

我们常常需要在输入框的前方添加提示,后方添加按钮。

HTML代码如下。

<div class="InputAddOn">
<span class="InputAddOn-item">...</span>
<input class="InputAddOn-field">
<button class="InputAddOn-item">...</button>
</div>

CSS代码如下。

.InputAddOn {display: flex;
}.InputAddOn-field {flex: 1;
}
五、悬挂式布局

有时,主栏的左侧或右侧,需要添加一个图片栏。

HTML代码如下。

<div class="Media">
<img class="Media-figure" src="" alt="">
<p class="Media-body">...</p>
</div>

CSS代码如下。

.Media {display: flex;
align-items: flex-start;
}.Media-figure {margin-right: 1em;
}.Media-body {flex: 1;
}
六、固定的底栏

有时,页面内容太少,无法占满一屏的高度,底栏就会抬高到页面的中间。这时可以采用Flex布局,让底栏总是出现在页面的底部。

HTML代码如下。

<body class="Site">
<header>...</header>
<main class="Site-content">...</main>
<footer>...</footer>
</body>

CSS代码如下。

.Site {display: flex;
min-height: 100vh;
flex-direction: column;
}.Site-content {flex: 1;
}
七,流式布局

每行的项目数固定,会自动分行。

CSS的写法。

.parent {width: 200px;
height: 150px;
background-color: black;
display: flex;
flex-flow: row wrap;
align-content: flex-start;
}.child {box-sizing: border-box;
background-color: white;
flex: 0 0 25%;
height: 50px;
border: 1px solid red;
}

3-3 新建页面的技巧与规则

承接上回(3-1),新建了 app.json文件并加入 pages 后,小程序提示

[ app.json 文件内容错误] app.json: pages 字段需为 array
(env: Windows,mp,1.05.2111084; lib: 2.19.4)

因此,将 pages 设为数组。小程序提示不能非空。我们需要注册小程序的页面,即在 pages 中写入我们的页面。

新建一个文件夹 pages,在其下新建一个文件夹 index。在 index 中需要建立 index 为名的四个文件。

建立四个文件的快捷方式:

  1. 在 index 文件夹目录右键,
  2. 点击“新建 page”,
  3. 输入“index”后回车。

新建完页面后,小程序会在 app.json 中自动加入页面的路径,非常方便。但是注意,这是建立在应用程序没有错误的情况下,否则新建页面和自动加入路径的功能不会生效。

页面删除后,也不会自动删除配置,需要手动删除。

{"pages": ["pages/index/index","pages/welcome/welcome"]
}

小程序启动时显示的页面,是 pages 数组中的第一项。

将 pages 中的 index 文件夹及配置中的 index 那项删除,进入下一步。

3-4 image标签显示一张图片

放置头像图片:image 标签,并新建一个 images 文件夹以放置图片。

文本:text 标签

按钮:button 标签

然后添加该有的内容。最后结果如下:

<image src="/images/avatar/1.png"></image>
<text>Hello, world!</text>
<button></button>

3-5 小程序rpx响应式单位的特点(非常重要)

相对路径和绝对路径(略)

问题引入:为什么小程序把一个正圆的图案默认变成了椭圆?

因为 image 组件默认宽度:320px * 240px

微信小程序尺寸单位:

  • rpx单位是微信小程序中css的尺寸单位,rpx可以根据屏幕宽度进行自适应。规定屏幕宽为750rpx。如在 iPhone6 上,屏幕宽度为375px,共有750个物理像素,则750rpx = 375px = 750物理像素,1rpx = 0.5px = 1物理像素。
  • rpx 比网页中的 em 更简易和灵活。

3-6 分离CSS样式到WXSS文件中

将样式写在网页的 wxss 中。wxss 的语法与 css 相同。

类名命名方法:小写单词 + 连字符,不推荐像 js 那样的驼峰命名法。有网站将类名的连字符改为下划线,了解即可。

建议写双引号包裹属性、类名,js 的字符串推荐用单引号。

.avatar{width: 200rpx;height: 200rpx;margin-top: 160rpx;
}

3-7 初识flex布局进行垂直分布布局

要有大局观,不能盯着一两个元素死磕。从大到小、从整体到局部地去看。

用 view 标签将欢迎页三个包裹起来。它无任何语义,只用于添加样式。类似于 HTML 中的 div 元素。

在小程序中,不区分 标签/组件 名字的区别。

通过操纵容器 view,完成其子元素的布局。

.container{display: flex;flex-direction: column;
}

设置以后,元素垂直方向排布(默认水平方向:row)。在显示时,button 组件居中,而不像其他组件那样左对齐排布。因此不推荐使用 button,这是小程序内置的组件,一些奇怪的特性未知,可能会导致产生与预期不一样的效果。原生 button 要更改样式很难。

如果想要实现按钮效果,是不一定非要用它的,更多的时候可以自己写一个 view,这样做的自由度也会更大一些。

  <!-- <button>开启小程序之旅</button> --><view><text>开启小程序之旅</text></view>

样式这块交给 wxss 完成,所以虽然简陋,但不必担心。

也可以使用第三方组件库里头比较好用的 button。

3-8 flex布局的align-items

相关 wxml 和 wxss 如下

<!--pages/welcome/welcome.wxml--><view class="container"><image class="avatar" src="/images/avatar/1.png"></image><text class="motto">Hello, world!</text><!-- <button>开启小程序之旅</button> --><view class="journey-container"><text>开启小程序之旅</text></view>
</view>
/* pages/welcome/welcome.wxss */.container {display: flex;flex-direction: column;align-items: center;
}.avatar {width: 200rpx;height: 200rpx;margin-top: 160rpx;
}.motto {/* color: #666; */margin-top: 100rpx;font-size: 32rpx;font-weight: bold;
}

align-items 先前已叙述过,不再重复叙述。在这里,将 view 组件的交叉轴上的对齐方式设为水平居中。在这里,交叉轴是水平轴(已将主轴通过 flex-direction 属性将主轴设为垂直轴)。

格言一栏的元素文本、、样式中,没有设置文字颜色,原因是将其转移到了 app.wxss 中,作为全局样式。

text {color: #666;
}

其他细节见代码本身(不多)

3-9 自己编写一个 Button 组件

先给对应组件添加一个类名:

  <view class="journey-container"><text>开启小程序之旅</text></view>

接着添加对应的样式:

.journey-container {/* 设置边框 */border: 1px solid #405f80;width: 200rpx;height: 80rpx;/* 设置边框圆角 */border-radius: 5px;
}

此时,边框内的文本仍需要调整。

 <text class="journey">开启小程序之旅</text>
.journey {font-size: 22rpx;color: #405f80;
}

此时,文本还没有居中。可以用 flex,但是这个组件很简单,没必要使用;可以用 text-align 属性实现水平居中,用 line-height 属性实现垂直居中。这种方式非常适合做文本居中。

.journey-container {.../* 将文本水平对齐设置在文本之外的容器中 */text-align: center;
}.journey {font-size: 22rpx;color: #405f80;/* 在文本上设置行高 */ line-height: 80rpx;font-weight: bold;
}

3-10 聊聊小程序的方便性与灵活性的悖论

缺失视频(相对不重要,被动略过),借此地写写自己漏掉的代码。

全局导航条的颜色配置(app.json):

{..."window": {"navigationBarBackgroundColor": "#b3d4db"},"sitemapLocation": "sitemap.json",
}

sitmap.json 为系统自动配置。暂时不作了解。

4 阅读列表与setData数据绑定

4-1 LinUI组件库介绍

组件库地址:Lin UI (talelin.com)

4-2 安装、编译 LinUI

安装

Lin UI提供两种安装方法,满足不同开发者的需求。如果您需要使用npm安装,请确保您已经在本机安装了npm

WARNING

使用 Lin UI 务必勾选增强编译Es6 转 ES5以及使用 npm 模块选项

方式一: 使用npm安装 (推荐)

打开小程序的项目根目录,执行下面的命令(如果使用了云开发,需要进入miniprogram文件夹下执行下面的命令)。

npm init

此时,会生成一个package.json文件,命令行里会以交互的形式让你填一些项目的介绍信息,你可以耐心填完,当然也可以忽略,全部按回车键来快速完成项目初始化。

如果之后想要修改,可以在 package.json 文件中进行修改。

{"name": "miniprogram-1","version": "1.0.0","description": "","main": ".eslintrc.js","scripts": {"test": "echo \"Error: no test specified\" && exit 1"},"author": "","license": "ISC"
}

注意事项

  • 1.执行npm init进行初始化,此时会生成一个package.json文件,如果不进行npm init,在构建npm的时候会报一个错误:没有找到 node_modules 目录
  • 2.不建议使用cnpm,这样会带来一些未知的错误。如果网络情况不佳,可以使用下面的命令行更换为淘宝源。
npm config set registry https://registry.npm.taobao.org

接着,继续执行下面的命令。

npm install lin-ui@0.8.7

执行成功后,会在根目录里生成项目依赖文件夹 node_modules/lin-ui (小程序IDE的目录结构里不会显示此文件夹。虽然在根目录可见,但是可以把它看作是隐藏起来的)。
然后用小程序官方IDE打开我们的小程序项目,找到 工具 选项,点击下拉选中 构建npm ,等待构建完成即可。

出现上图所示的结果后,可以看到小程序IDE工具的目录结构里多出了一个文件夹 miniprogram_npm(之后所有通过 npm 引入的组件和 js 库都会出现在这里),打开后可以看到 lin-ui 文件夹,也就是我们所需要的组件。

如果后续需要修改版本,可以在 package.json 配置文件中修改版本号,然后再执行一次 npm 即可。

{..."license": "ISC","dependencies": {"lin-ui": "^0.8.7"}
}
npm install lin-ui@指定版本号
方式二:下载代码

直接通过git下载 Lin UI 源代码,并将 dist 目录(Lin-UI 组件库)拷贝到自己的项目中。

git clone https://github.com/TaleLin/lin-ui.git

4-3 如何使用自定义组件

如果想要自定义组件库,首先要进行配置。在 welcome.json 配置文件中,由开发者自定义组件的名字。通常建议自定义组件前有一个前缀,以记录其来源于哪个组件库(方便阅读)。

配置前:

{"usingComponents": {}
}

配置后:

{"usingComponents": {"l-avatar": "/miniprogram_npm/lin-ui/avatar/index"}
}

注意 URL 的写法,目录与其下的文件不一定同名。至此,相当于引入了一个名为 l-avatar 的组件/标签。

4-4 l-avatar头像和昵称组件

组件支持三种类型,Icon、图片以及open-data,三种类型的优先级依次是open-data、Icon和图片。

open-data接收 JavaScript 数组,通过传入userAvatarUrluserNickName控制显示用户头像和昵称。

在 wxml 中写出组件及其属性

<l-avatar open-data="{{['userAvatarUrl']}}" />

在 wxml 中引用 JavaScript 变量或常量时,需用 {{}} 包裹。编辑时注意大小写。userAvatarUrl 作为数组中的一个字符串常量被输入。userNickName 同理可被传入。

<l-avatar open-data="{{['userAvatarUrl', 'userNickName']}}" />

属性示例:

<l-avatar size="200" placement="bottom" open-data="{{['userAvatarUrl', 'userNickName']}}" />

其他可选属性见文档:头像 Avatar | Lin UI (talelin.com)

4-5 添加新的编译模式

新建一个 posts 页面目录及其文件。

需要编写 posts 页面。但是模拟器默认显示的仍是 welcome ,如何修改?两种方式:

  1. (推荐)添加编译模式
  2. 在 app.json 中将 posts 页面作为第一个页面
  3. 在 app.json 中写入 entryPagePath: “pages/posts/posts”

第二种方法改变了想要的页面顺序,不推荐,推荐第一种。新建页面目录及文件后,要先保存,然后才能在新建编译模式时找到页面;第三种方式可在发布阶段删除,但是仍是麻烦。

4-6 初步了解Swiper和Swiper-Item组件

先来更改下顶部的导航条颜色,将其改为红色,但是首页的导航条颜色仍保留为青蓝色。

在 welcome 配置中单独设置:

"navigationBarBackgroundColor": "#b3d4db"

在 app.json 中更改其颜色

"navigationBarBackgroundColor": "#C22A1E"

接下来开始制作顶部的轮播图。

使用组件:swiper

滑块视图容器。其中只可放置 swiper-item 组件,否则会导致未定义的行为。

即:swipter 中需要放置且只能放置 swiper-item 组件,否则无法正常使用。它是个容器,真正需要实现的内容(可见的部分)由 swiper-item 组件完成。

<view><swiper><swiper-item>Content1</swiper-item><swiper-item>Content2</swiper-item><swiper-item>Content3</swiper-item></swiper>
</view>

4-7 Swiper组件(1)通过插槽设置轮播内容

swiper-item 有插槽的特性,其内几乎可以放入任何想要加入的内容,而且每个组件内放置的组件或内容也可不一样,非常灵活。所以,swiper 是一种“滑动视图容器”,而不仅仅是一种轮播图。插槽的功能往往在第三方组件库用到,因为开发者需要在组件中放置什么组件是未知的,这时候用插槽比指定用某种组件就好得多。就像 swiper-item,允许开发者往插槽中放置自由度高的内容。

<!--pages/posts/posts.wxml--><view><swiper><swiper-item><image src="/images/bestplayers.png"></image></swiper-item><swiper-item>Content2</swiper-item><swiper-item>Content3</swiper-item></swiper>
</view>

此时的样式需要修改,默认的图片宽高往往不是我们想要的。

4-8 Swiper组件(3)Swiper组件内容的高宽设置技巧

轮播图中每张图片大小都可能不一样,为了统一,不应该根据原生图片调整大小,而是要根据轮播图的需要,所有图片调整为统一的固定大小。

需要同时给 swiper 和 image 同时设置样式,以保证达到预期效果。

/* pages/posts/posts.wxss */swiper,
swiper image {width: 100%;height: 460rpx;
}

4-9 属性设置一定要注意字符串以 JS 表达式的区别(重要)

Swiper 组件的常见属性设置:

indicator-dots

是否显示面板指示点。默认值:false

autoplay

是否自动切换。默认值:false

interval

自动切换时间间隔。默认值:5000(毫秒)

<!--pages/posts/posts.wxml--><view><swiper autoplay="true" indicator-dots="true">...</swiper>
</view>

但是,如果将相应的变量改为 “false”

<swiper autoplay="false" indicator-dots="false">
</swiper>

对应的效果并未消失。原因是,不管是 true 还是 false,都被解析为字符串,而非空字符串都被解析为布尔类型的 true。所以,我们在设置属性值时,一直设置的都是 true。如果想要严格人工输入布尔类型的值,正确的写法应该是

<swiper autoplay="{{false}}" indicator-dots="{{false}}">
</swiper>

4-10 布尔属性值赋值的良好建议

即使值是 true,也推荐使用 {{true}} 方式,所见即所得,方便阅读和查错。在 {{}} 中的内容,被当做是 JavaScript 变量/表达式。

另一种推荐的书写方式是

<swiper autoplay indicator-dots>
</swiper>

这样也会设置为 true,而且更简洁,也容易理解。

4-11 Swiper组件的其他重要属性设置

<swiper interval="3000" vertical circular>
</swiper>

4-12 用Flex布局组织布局思路(1)

开始文章目录的分析制作,具体分块及元素安排如下

<!--pages/posts/posts.wxml--><!--=== 轮播图 ===-->
<view>...
</view><!--=== 文章目录界面 ===-->
<view><!-- 作者头像及时间 --><view><image></image><text></text></view><!-- 文章标题 --><text></text><!-- 文章图片 --><image></image><!-- 文章摘要 --><text></text><!-- 图标 --><view></view>
</view>

4-13 用Flex布局组织布局思路(2)

在每个模块写入具体的数据。

4-14 用Flex布局组织布局思路(3)

首先为文章目录总体设置样式

/* 文章目录界面 */
.post-container{/* 设置容器布局 */display: flex;flex-direction: column;/* 设置细节 */margin-top: 20rpx;margin-bottom: 40rpx;background-color: #fff;border-top: 1px solid #ededed;border-bottom: 1px solid #ededed;padding-bottom: 10rpx;
}

接下来为第一行元素设置样式

/* 作者头像及时间 */
.post-author-date {/* 设置边距 */margin: 10rpx 0 20rpx 10rpx;
}

对于第一行元素内的作者头像,单独设置样式

/* 作者头像 */
.post-author {width: 60rpx;height: 60rpx;
}

此时,头像并不垂直居中。有一个简单但并不非常通用的一个方式

/* 作者头像 */
.post-author {...vertical-align: middle;
}

但是这里的居中是有些问题的,所以还是建议采用 flex 布局。居中方法很多,flex 大多数情况下是最好用的,其他对齐方式可能收到元素及浮动影响( text 设置为 vertical-align: middle 后,也可以完成一样的居中效果)。

采用 flex 布局的情况如下

.post-author-date {/* 设置边距 */margin: 10rpx 0 20rpx 10rpx;/* 设置布局 */display: flex;align-items: center;
}

4-15 用Flex布局组织布局思路(4)

承接上文,对于第一行元素内的文本,单独设置样式

/* 时间戳 */
.post-date {margin-left: 20rpx;font-size: 26rpx;/* vertical-align: middle; */
}

接下来设置第二行元素:标题样式

/* 文章标题 */
.post-title {font-size: 34rpx;font-weight: 600;margin-bottom: 20rpx;margin-left: 20rpx;color: #333;
}

4-16 用Flex布局组织布局思路(5)

设置图片样式

/* 文章图片 */
.post-image {width: 100%;height: 340rpx;margin-bottom: 30rpx;
}

设置摘要样式

/* 文章摘要 */
.post-content {color: #666;font-size: 28rpx;margin-bottom: 20rpx;margin-left: 20rpx;/* 行距 */line-height: 40rpx;/* 字间距 */letter-spacing: 2rpx;
}

设置收藏、阅读图标样式

/* 收藏图标 */
.post-like-image {height: 32rpx;width: 32rpx;margin-right: 16rpx;
}/* 阅读图标 */
.post-like-font {margin-right: 20rpx;font-size: 26rpx;
}

5 条件渲染、列表渲染与小程序事件

5-1 使用LinUI的Icon组件代替图片ICON

初级使用方式

  1. 创建 l-icon 组件
  2. 添加 name 属性以指定图标

5-2 LinUI Icon组件的颜色和大小设置

将原来的图标换为内置图标,添加相关属性,如下

  <!-- 图标 --><view class="post-like"><!-- <image class="post-like-image" src="/images/icon/chat.png"></image> --><l-icon color="#666" class="post-like-image" size="28" name="favor"/><text class="post-like-font">92</text><!-- <image class="post-like-image" src="/images/icon/view.png"></image> --><l-icon color="#666" class="post-like-image" size="32" name="eye" /><text class="post-like-font">102</text></view>

也可以在全局配置文件中添加 lin-ui 相关组件,例如,可以将 l-icon 添加至 app.json 中

{"pages": ["pages/welcome/welcome","pages/posts/posts"],"window": {"navigationBarBackgroundColor": "#C22A1E"},"sitemapLocation": "sitemap.json","usingComponents": {"l-icon": "/miniprogram_npm/lin-ui/icon/index"}
}

5-3 小程序开发数据的流向(1)

数据需要从服务器传到前端。最终的数据不能写死。

5-4 单向数据绑定与双向数据绑定

前端数据有两个来源:服务器传给前端的数据(动态数据)、前端自身有的数据(静态数据)。

关于动态数据

小程序中,js 是最先拿到动态数据的。小程序提供了一种数据绑定机制,使得 Js 中的数据可以呈现出来。未来几节课的重点就是使 JS 中的数据可以呈现。

数据绑定的实现方式:

  • 使用 JavaScript 的核心函数 setData 完成数据绑定。

单向数据绑定:从 JS 层(逻辑层) 传向 View 层(视图层)。

双向数据绑定:用户在 View 层操作之后,可以传回 JS 层,使得 JS 层的数据也发生改变。vue 是双向数据绑定中做得最好的。

5-5 数据绑定与setData函数(1)

视频缺失,以下是网络资料及课程源码:

setData 函数用于将数据从逻辑层发送到视图层(异步),同时改变对应的 this.data 的值(同步)。

参数说明:

Object 以 key:value(键值对) 的形式表示,将 this.data 中的 key 对应的值改变成 value。

1 初始化数据绑定

初始化数据绑定是将业务数据放在 page 方法参数的 data 对象下面,然后在 wxml 文件中引用这些数据。小程序使用双大括号 {{}} 在 wxml 组件里进行数据的绑定。双括号里的名称对应 data 对象里的属性名。可见,数据绑定非常简单,只要将data对象的属性名填入到双大括号 {{}} 中即可。MINA框架会自动在运行时用data数据替换这些 {{}} 。

开发工具为我们提供了一个AppData面板用来查看和调试数据绑定变量.建议当读者遇到数据绑定相关问题时,首先打开这个面板查看具体的数据绑定情况.

值得一提的是,data对象的属性可以是简单的文本和数字,也可以是一个对象或者数组。

2 数据绑定更新

还可以用 setData 函数来做数据绑定。这种方法可以理解伪"数据更新"。大多数情况下,我们使用 this.setData 的方式来调用这个函数。
setData 的参数结构一个对象,以 key 和 value 的形式将 page 下 data 对象中的 key 对应的内容设置成 value。这样说可能有点难以理解,下面举个具体的例子:

在上面这个例子中,在 data 对象里,title 属性的值为"小时候的冰棍儿和雪糕",然后我们用了 setData 函数把 title 属性更新为"令人怀念的冰棍",同时其他属性的值保持不变。

我们还可以更进一步,把 data 里的属性全都放到 setData 函数里。

或者,把数据存到一个对象里,然后在 setData 函数里引用这个对象

5-6 DOM优先 VS 数据优先/数据驱动

依据文档对象模型,要对对象的数据进行修改时,需要在 js 中获取对象,然后进行相应更改。如果需要再其他对象中使用数据,这种操作又需要来一次,极不方便。如果数据优先,可以做到,js 中的变量,任意地在 wxml 中引用。

现在的小程序版本也可以利用一些 API,做到在 js 中获取某一个组件,但是传统的小程序开发一般不需要这样做。在开发组件库,构建框架时可能需要用到,初学者可以暂时不了解。

5-7 作业 目的:提前了解 ES6 的常用语法

暂时忽略,此项为作业

5-8 理解Page.data与this.SetData的关系

setData 接受的是一个 JavaScript 对象。

this.SetData 与 data 中都可以定义一个变量,推荐 data 中定义好变量,this.setData 只用于修改。

data 中包含的是页面的初始数据。this.SetData 定义的初始变量最终也会加入 data 对象中。

5-9 预先在data中定义绑定数据的初始值是值得推荐的做法

思考:我们需要绑定的变量是否都需要预先在 data 中定义?

建议,所有需要在前台(wxml)使用的变量,都在 data 中进行定义。即,data 也可以包含不需要在前台中使用的变量,而需要在前台中使用的变量都需要在 data 有定义。将 setData 作为更新而非创建数据的函数(虽然它有创建的功能)。

5-10 小程序的生命周期函数与特殊回调函数

小程序的生命周期函数包括:

  • onLoad,监听页面加载
  • onReady,监听页面初次渲染完成
  • onShow,监听页面显示
  • onHide,监听页面隐藏
  • onUnload,监听页面卸载

由小程序系统内部自动调用。

调用顺序:

  1. onLoad
  2. onShow
  3. onReady

大多数情况下,初始化代码写在 onLoad 中。

手机从后台重新切回前台时,会触发 onShow,但不会触发 onLoad 和 onReady。这也很好理解。

页面在跳转时可能会触发页面卸载。

5-11 数据绑定的实战应用

现在 onLoad 中模拟一个从服务器传过来的数据:

onLoad: function (options) {var content = {title: "2020LPL夏季赛季后赛观赛指南",content: "8月9号,LPL常规赛收官之战结束,在事关季后赛轮次的比赛中关键对局中,SN战胜了FPX,为本赛季常规赛画上句号。进入季后赛的战队依次为,TES、JDG、IG、SN、V5、LGD、WE、FPX",imgSrc: "/images/lpl.png",reading: 102,detail: "8月9号,LPL常规赛收官之战结束,在事关季后赛轮次的比赛中关键对局中,SN战胜了FPX,为本赛季常规赛画上句号。进入季后赛的战队依次为,TES、JDG、IG、SN、V5、LGD、WE、FPX。既有传统四强,又有新崛起的黑马。本文主要是从上路的大改动展开,引发对所有其他的影响。牵一发而动全身,上路一旦回归carry上单版本,对野区和中路的影响是显而易见的。而下路在艾希大砍一刀之后,女警的过于强势,使她只能出现在BAN位上,因此主流下路还是会回归功能性下路英雄。由此,可以对应各位选手的英雄池,对应各支战队的战术储备,漫长的季后赛,考验的就是各队适应版本的能力。",collection: 92,dateTime: "24小时前",headImgSrc: "/images/lpl.png",author: "猫是猫的猫",date: "Nov 20 2020",avatar: "/images/avatar/5.png",postId: 0}this.setData(content);},

5-12 Mustache语法解析

如果在 JS 中定义了如下变量

    var content = {title: "2020LPL夏季赛季后赛观赛指南",content: "8月9号,LPL常规赛收官之战结束,在事关季后赛轮次的比赛中关键对局中,SN战胜了FPX,为本赛季常规赛画上句号。进入季后赛的战队依次为,TES、JDG、IG、SN、V5、LGD、WE、FPX",imgSrc: "/images/lpl.png",dataNum: {reading: 102, collection: 92},detail: "8月9号,LPL常规赛收官之战结束,在事关季后赛轮次的比赛中关键对局中,SN战胜了FPX,为本赛季常规赛画上句号。进入季后赛的战队依次为,TES、JDG、IG、SN、V5、LGD、WE、FPX。既有传统四强,又有新崛起的黑马。本文主要是从上路的大改动展开,引发对所有其他的影响。牵一发而动全身,上路一旦回归carry上单版本,对野区和中路的影响是显而易见的。而下路在艾希大砍一刀之后,女警的过于强势,使她只能出现在BAN位上,因此主流下路还是会回归功能性下路英雄。由此,可以对应各位选手的英雄池,对应各支战队的战术储备,漫长的季后赛,考验的就是各队适应版本的能力。",dateTime: "24小时前",headImgSrc: "/images/lpl.png",author: "猫是猫的猫",date: "Nov 20 2020",avatar: "/images/avatar/5.png",postId: 0}

如何在对应的 wxml 中引用 reading 和 collection

<text class="post-like-font">{{dataNum.reading}}</text>

事实上,我们可以在 {{}} 进行一些简单的运算。比如

<text class="post-like-font">{{dataNum.reading + 3}}</text>

加上 {{}} 后,其内容会被当作 JS 表达式。可以进行运算(包括三元运算)。

更详细和更复杂的用法见官方文档。

5-13 条件渲染

页面渲染分为:条件渲染 列表渲染

例如:

在 data 对象中加入 flag 属性,默认为 true

  /*** 页面的初始数据*/data: {flag: true},

在 wxml 对应组件中写入语法,语法如下

<text wx:if="{{flag}}" class="post-date">{{date}}</text>
  <view class="post-author-date"><image wx:if="{{flag}}" class="post-author" src="{{avatar}}"></image><text wx:else class="post-date">{{date}}</text></view>

这样写等同于

  <!-- 作者头像及时间 -->
<view class="post-author-date"><image wx:if="{{flag}}" class="post-author" src="{{avatar}}"></image><text wx:if="{{!flag}}" class="post-date">{{date}}</text>
</view>

if–elseif 的语法在 wxml 可表示为

  <!-- 作者头像及时间 --><view class="post-author-date"><image wx:if="{{flag}}" class="post-author" src="{{avatar}}"></image><view wx:else><text wx:if="{{wing}}" class="post-date">{{date}}</text></view></view>

也可以使用 wx:elif

    <!-- 作者头像及时间 --><view class="post-author-date"><image wx:if="{{flag}}" class="post-author" src="{{item.avatar}}"></image><text wx:elif="{{wing}}" class="post-date">{{item.date}}</text></view>

5-14 列表渲染(上)

用循环的方式,生成结构相同的一个列表。

首先,准备好数据:

  /*** 生命周期函数--监听页面加载*/onLoad: function (options) {var content = [{title: "2020LPL夏季赛季后赛观赛指南",content: "8月9号,LPL常规赛收官之战结束,在事关季后赛轮次的比赛中关键对局中,SN战胜了FPX,为本赛季常规赛画上句号。进入季后赛的战队依次为,TES、JDG、IG、SN、V5、LGD、WE、FPX",imgSrc: "/images/lpl.png",reading: 102,detail: "8月9号,LPL常规赛收官之战结束,在事关季后赛轮次的比赛中关键对局中,SN战胜了FPX,为本赛季常规赛画上句号。进入季后赛的战队依次为,TES、JDG、IG、SN、V5、LGD、WE、FPX。既有传统四强,又有新崛起的黑马。本文主要是从上路的大改动展开,引发对所有其他的影响。牵一发而动全身,上路一旦回归carry上单版本,对野区和中路的影响是显而易见的。而下路在艾希大砍一刀之后,女警的过于强势,使她只能出现在BAN位上,因此主流下路还是会回归功能性下路英雄。由此,可以对应各位选手的英雄池,对应各支战队的战术储备,漫长的季后赛,考验的就是各队适应版本的能力。",collection: 92,dateTime: "24小时前",headImgSrc: "/images/lpl.png",author: "猫是猫的猫",date: "Nov 20 2020",avatar: "/images/avatar/5.png",postId: 0,},{date: "Sep 18 2020",title: "正是虾肥蟹壮时",imgSrc: "/images/post/crab.png",avatar: "/images/avatar/1.png",content: "菊黄蟹正肥,品尝秋之味。徐志摩把,“看初花的荻芦”和“到楼外楼吃蟹”,并列为秋天来杭州不能错过的风雅之事;用林妹妹的话讲是“螯封嫩玉双双满,",reading: "112",collection: "96",headImgSrc: "/images/post/crab.png",author: "林白衣",dateTime: "24小时前",detail: "菊黄蟹正肥,品尝秋之味。徐志摩把“看初花的荻芦”和“到楼外楼吃蟹”并列为秋天来杭州不能错过的风雅之事;用林妹妹的话讲是“螯封嫩玉双双满,壳凸红脂块块香”;在《世说新语》里,晋毕卓更是感叹“右手持酒杯,左手持蟹螯,拍浮酒船中,便足了一生矣。”漫漫人生长路,美食与爱岂可辜负?于是作为一个吃货,突然也很想回味一下属于我的味蕾记忆。记忆中的秋蟹,是家人的味道,弥漫着浓浓的亲情。\n\n是谁来自山川湖海,却囿于昼夜,厨房与爱? 是母亲,深思熟虑,聪明耐心。吃蟹前,总会拿出几件工具,煞有介事而乐此不疲。告诉我们螃蟹至寒,需要佐以姜茶以祛寒,在配备的米醋小碟里,亦添入姜丝与紫苏,前者驱寒后者增香。泡好菊花茶,岁月静好,我们静等。",postId: 1,}]this.setData({posts:content});},

此时,content 不再是一个对象,而是一个数组,但是 setData 只能接受一个对象。所以,需要将 content 放入一个对象中。只要是存在非对象类型的,都考虑是否能通过这种方式解决数据的传输问题。这个时候,content 需要作为对象的属性值。所以设立了 posts 属性存储它,相应的在 data 初始值中添加一个 posts 空对象。

基础语法:

wx:for

通常将 wx:for 设置在 block 组块中。

<block wx:for="{{posts}}">
</block>

posts要加双括号!!!

记住!凡是需要引用到 JS 的,一定要加 {{}}

循环体内的每一一个数组的子元素,都用 item 表示,所以,item 就是当次循环的对象。

完整使用 wx:for 的应用实例如下

<block wx:for="{{posts}}"><!--=== 文章目录界面 ===--><view class="post-container"><!-- 作者头像及时间 --><view class="post-author-date"><image class="post-author" src="{{item.avatar}}"></image><text class="post-date">{{item.date}}</text></view><!-- 文章标题 --><text class="post-title">{{item.title}}</text><!-- 文章图片 --><image class="post-image" src="{{item.imgSrc}}"></image><!-- 文章摘要 --><text class="post-content">{{item.content}}</text><!-- 图标 --><view class="post-like"><!-- <image class="post-like-image" src="/images/icon/chat.png"></image> --><l-icon color="#666" class="post-like-image" size="28" name="favor" /><text class="post-like-font">{{item.collection}}</text><!-- <image class="post-like-image" src="/images/icon/view.png"></image> --><l-icon color="#666" class="post-like-image" size="32" name="eye" /><text class="post-like-font">{{item.reading}}</text></view></view>
</block>

可以用 wx:for-item 修改子数组变量的名称,默认值是 item。如

<block wx:for="{{posts}}" wx:for-item="i"></block>

5-15 列表渲染(下)

当前 item 的序号怎么获取呢?

有一个内置变量:index。注意,这个变量也是在 JS 中的,引用时需要用 {{}}。类似于 item,index 也可以修改名字。

<block wx:for="{{posts}}" wx:for-index="idx"><text>{{idx}}</text>
</block>

如果有多个 view 需要循环,可以用 block 包裹;即使只有一个 view,也推荐写个 block。但是要记住,wx:for 也可以写在 view 上。

block 与 view 有何区别?

用在block中可以渲染一个包含多节点的结构块。比如:


<block wx:for="{{[1, 2, 3]}}"><view> {{index}}: </view><view> {{item}} </view>
</block>

输出

0:
1
1:
2
2:
3

用在view中只能应用单个节点。比如:

<view wx:for="{{[1, 2, 3]}}" wx:for-index="idx" wx:for-item="itemName">{{idx}}: {{itemName}}
</view>

输出

0:1
1:2
2:3

再如:

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

输出

1 * 1 = 1
1 * 2 = 2
...
8 * 9 = 72
9 * 9 = 81

如果将 block 改为 view,会有什么现象呢

<!-- <block wx:for="{{[1, 2, 3]}}"><view> {{index}}: </view><view> {{item}} </view>
</block> -->
<view wx:for="{{[1, 2, 3]}}"><view> {{index}}: </view><view> {{item}} </view>
</view>

在视觉显示效果上有区别:

两者的区别是, 是一个组件,会在页面上做渲染;不是一个组件,它仅仅是一个包装元素,只接受控制属性,不会在页面中做任何渲染。

由于 view 是组件,所以页面显示时会因此占据一定空间。而 block 不会被渲染,因此无任何样式效果。

<!-- 代码 1 -->
<view class="test" wx:for="{{[1, 2, 3]}}"><view> {{index}}: </view><view> {{item}} </view>
</view>
<!-- 代码 2 -->
<block class="test" wx:for="{{[1, 2, 3]}}"><view> {{index}}: </view><view> {{item}} </view>
</block>

上述的两个代码块均通过类名被 css 修改

.test {margin-top: 100rpx;
}

对 block 无效果。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5BDklcRx-1642044979987)(C:\Users\王茜娜\Desktop\360截图172905029710590.png)]

而 view 的三次循环生成的结果,均产生了 margin-top 的效果。因此,view 不仅会被渲染,而且每次循环都会渲染一次,由此作用到页面中。如果需要借助循环完成此效果,则考虑用 view 进行多节点循环。但是,通过循环产生样式而非通过 css 控制,窃以为这种方式不太好。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UtnBPR9K-1642044979988)(C:\Users\王茜娜\Desktop\360截图17901116728462.png)]

下面是使用的例子:

wx:if

<block wx:if="{{true}}"><view> text </view><view> text </view>
</block>

wx:for

<block wx:for="{{[1, 2, 3]}}"><view> {{index}}: </view><view> {{item}} </view>
</block>

wx:elif 和 wx:else

<block wx:if="{{boolean==true}}"><view class="bg_black"></view>
</block>
<block wx:elif="{{boolean==false}}"><view class="bg_red"></view>
</block>
<block wx:else><view class="bg_red"></view>
</block>

5-16 什么是事件

我们从 posts 页面回到 welcome 页面。

目标:从按钮处获取用户的点击事件。

当用户用手指点击后触发 tap 事件。如何捕捉这个 tap 事件呢?

5-17 bind来捕捉事件执行回调函数

在 js 的 page 中写入一个函数:

(由于page 存储的是一个对象,函数是在对象中编写,所以实际上我们是定义了一个该对象的方法)

// pages/welcome/welcome.js
Page({...onTap: function() {// 跳转页面},...
})

捕捉事件并执行相应函数:

bind:捕捉事件的名称=函数名称

  <!-- <button>开启小程序之旅</button> --><view bind:tap="onTap" class="journey-container"><text class="journey">开启小程序之旅</text></view>
</view>

在这个例子中,按钮组件一旦捕捉到 tap 事件,就会执行 onTap 函数。

这里的 onTap 称为事件的回调函数。

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。

6 路由函数与事件冒泡

6-1 路由函数 NavigateTo 和 RedirectTo 的区别

在捕捉用户事件时,bind 语法允许不加冒号

  <!-- <button>开启小程序之旅</button> --><view bindtap="onTap" class="journey-container"><text class="journey">开启小程序之旅</text></view>
</view>

但是还是建议加上,意义更清晰。之后要学的 catch 也类似,都加上冒号。

跳转页面通过 wx.navigateTo() 函数实现

  onTap: function() {// 跳转页面wx.navigateTo({url: '/pages/posts/posts',})},

wx.navigateTo() 接受一个 JS 对象,对象中有一个很重要的属性:URL,用于指定跳转的页面。跳转后的页面相当于原页面的子页面。最多容纳 10 个页面。(页面栈最多容纳 10 个)

如果这两个页面其实并无父子关系时,可以用 redirectTo 更好一些

  onTap: function() {// 跳转页面// wx.navigateTo({//   url: '/pages/posts/posts',// });wx.redirectTo({url: '/pages/posts/posts',});},

使用 navigateTo,父页面不会被销毁;使用 redirectTo,父页面会被销毁。

6-2 作业 目的:JS模块的概念与导入导出的方式

略(通过视频学习。有关 JavaScript 模块化及导入导出的知识在 JS 学习中再说)

6-3 Catch 与 Bind 事件的区别

就是冒泡事件和非冒泡事件的区别。catch 和 bind 是两种获取事件的方法,而事件又分为冒泡事件和非冒泡事件。冒泡事件会向父节点传递,而非冒泡事件不会,所以 catch 和 bind 作用于非冒泡事件效果相同,但 catch 作用于冒泡事件时会阻止其向上传递。

现在对应的组件上捕捉两个事件,并在 js 中写入有关函数属性。

  <view bind:tap="onViewTap" class="journey-container"><text bind:tap="onTextTap" class="journey">开启小程序之旅</text></view>
  onViewTap: function() {console.log("on tap View");},onTextTap: function() {console.log("on tap Text");},

此时点击文本,输出

on tap Text
on tap View

如果改为

  <view bind:tap="onViewTap" class="journey-container"><text catch:tap="onTextTap" class="journey">开启小程序之旅</text></view>

此时输出

on tap Text

对于 catch 和 bind,都会执行自身的事件,但是 catch 会阻止事件冒泡(父节点无法捕捉到 tap 事件)。如无特殊情况,推荐用 bind。

新版小程序提供了 mu-bind,解决了 bind 的部分 bug。有需要时再了解。

6-4 js模块的导入导出(require与import)_1

先将数据源从 posts.js 中单独抽取出来,统一放入 /data/data.js 中,模拟后台数据源。

data.js 数据库的数据如下

var local_database = [{title: "2020LPL夏季赛季后赛观赛指南",content: "8月9号,LPL常规赛收官之战结束,在事关季后赛轮次的比赛中关键对局中,SN战胜了FPX,为本赛季常规赛画上句号。进入季后赛的战队依次为,TES、JDG、IG、SN、V5、LGD、WE、FPX",imgSrc: "/images/lpl.png",reading: 102,detail: "8月9号,LPL常规赛收官之战结束,在事关季后赛轮次的比赛中关键对局中,SN战胜了FPX,为本赛季常规赛画上句号。进入季后赛的战队依次为,TES、JDG、IG、SN、V5、LGD、WE、FPX。既有传统四强,又有新崛起的黑马。本文主要是从上路的大改动展开,引发对所有其他的影响。牵一发而动全身,上路一旦回归carry上单版本,对野区和中路的影响是显而易见的。而下路在艾希大砍一刀之后,女警的过于强势,使她只能出现在BAN位上,因此主流下路还是会回归功能性下路英雄。由此,可以对应各位选手的英雄池,对应各支战队的战术储备,漫长的季后赛,考验的就是各队适应版本的能力。",collection: 92,dateTime: "24小时前",headImgSrc: "/images/lpl.png",author: "猫是猫的猫",date: "Nov 20 2020",avatar: "/images/avatar/5.png",postId: 0,music: {url: "http://music.163.com/song/media/outer/url?id=1372060183.mp3",title: "空-徐海俏",coverImg: "https://y.gtimg.cn/music/photo_new/T002R300x300M000002sNbWp3royJG_1.jpg?max_age=2592000",}
},{date: "Sep 18 2020",title: "正是虾肥蟹壮时",imgSrc: "/images/post/crab.png",avatar: "/images/avatar/1.png",content: "菊黄蟹正肥,品尝秋之味。徐志摩把,“看初花的荻芦”和“到楼外楼吃蟹”,并列为秋天来杭州不能错过的风雅之事;用林妹妹的话讲是“螯封嫩玉双双满,",reading: "112",collection: "96",headImgSrc: "/images/post/crab.png",author: "林白衣",dateTime: "24小时前",detail: "菊黄蟹正肥,品尝秋之味。徐志摩把“看初花的荻芦”和“到楼外楼吃蟹”并列为秋天来杭州不能错过的风雅之事;用林妹妹的话讲是“螯封嫩玉双双满,壳凸红脂块块香”;在《世说新语》里,晋毕卓更是感叹“右手持酒杯,左手持蟹螯,拍浮酒船中,便足了一生矣。”漫漫人生长路,美食与爱岂可辜负?于是作为一个吃货,突然也很想回味一下属于我的味蕾记忆。记忆中的秋蟹,是家人的味道,弥漫着浓浓的亲情。\n\n是谁来自山川湖海,却囿于昼夜,厨房与爱? 是母亲,深思熟虑,聪明耐心。吃蟹前,总会拿出几件工具,煞有介事而乐此不疲。告诉我们螃蟹至寒,需要佐以姜茶以祛寒,在配备的米醋小碟里,亦添入姜丝与紫苏,前者驱寒后者增香。泡好菊花茶,岁月静好,我们静等。",postId: 1,music: {url: "http://music.163.com/song/media/outer/url?id=1386866050.mp3",title: "笑看风云",coverImg: "https://y.gtimg.cn/music/photo_new/T002R300x300M000002sNbWp3royJG_1.jpg?max_age=2592000"}},{//按住alt + shift + F 可以格式化代码样式title: "当我们在谈论经济学时,我们在谈论什么?",content: "引言在我跟学生课后交流时,以及我在知乎上阅读有关“经济”问题的论题时,经常会遇到这样的情况:...",detail: "1 引言\n\n在我跟学生课后交流时,以及我在知乎上阅读有关“经济”问题的论题时,经常会遇到这样的情况:有些人套用“经济理论“的知识去解释现实中发生的经济事件,结果发现很多事情讲不通,或者发现”理论告诉我们的“与现实发生的是相反的。也有学生经常跟我说:经济学有什么用?为了说明这个,我经常从两个方面来进行解释,尝试用我个人所擅长的解决问题的视角和他们能够听懂的方法来说明经济学是什么,它的作用边界在哪里:\r\n\n2 ”简笔素描“与”油画肖像“我们给人画肖像画,可以用简笔素描,也可以用油画肖像。油画肖像可以在最大程度上保存了人物的各方面的细节和特点,而简笔素描则忽略了很多细节。尽管简笔素描忽略了人物的许多细节,但我们仍旧能够很容易的认出画中的人物是谁。为什么?因为这种方法保留了人物最显著的特征,以至于我们可以忽略其次要特征而对人物做出判定。\n\n2.1 ”简笔素描“对于绝大多数的非经济学专业大众而言(经济学相关专业硕士学历以上),人们所接触到的经济学都是初级微观经济学。所谓的初级微观经济学,对于经济问题的”画法“就是一种”简笔素描“。比如初级微观经济学教材中广为使用的这种一元一次需求函数:y=bx+a,需求量的唯一变量是产品价格。但仅凭直觉我们就可以断言,现实中影响需求量的因素绝不止价格这一种,因此我们可以认为这个模型对经济问题的描述是失真的。然而但这种失真却是必要的和有意义的,其意义在与它利于揭示价格对于需求的影响,而不在于否定影响需求的其他因素——",imgSrc: "/images/post/sls.jpg",headImgSrc: "/images/post/sls.jpg",reading: 62,collection: 92,author: "知乎",date: "Nov 12 2020",dateTime: "三天前",avatar: "/images/avatar/3.png",postId: 2,music: {url: "http://music.163.com/song/media/outer/url?id=30031580.mp3",title: "原来你也在这里-秦昊",coverImg: "https://y.gtimg.cn/music/photo_new/T002R300x300M000002sNbWp3royJG_1.jpg?max_age=2592000"}},{title: "LPL2020夏季赛一阵阵容",content: "iG在本赛季大胆更换了下路组合,没有人料想到全新阵容能够迅速起飞,iG在常规赛中只输了两场比赛,队伍的整体状态非常出色。",imgSrc: "/images/bestplayers.png",reading: 102,detail: "iG在本赛季大胆更换了下路组合,没有人料想到全新阵容能够迅速起飞,iG在常规赛中只输了两场比赛,队伍的整体状态非常出色。Puff和Southwind的到来,为iG带来了全新的战术打法,而Rookie和TheShy的发挥一如既往稳定。作为世界冠军和LPL卫冕冠军,FPX在本赛季保持了稳定的发挥,Khan和GimGoon轮番上阵,为FPX带来了不同的战术部署。如今的FPX打法非常多变,上中下都能够成为carry的战术点,加上Tian的前期积极进攻,整支队伍在对线方面得到了相当程度的提升。FPX渴望延续良好表现,捍卫他们的冠军头衔。",collection: 26,dateTime: "24小时前",headImgSrc: "/images/bestplayers.png",author: "深白色",date: "Nov 20 2016",avatar: "/images/avatar/3.png",postId: 3,music: {url: "http://music.163.com/song/media/outer/url?id=1334295185.mp3",title: "写给黄淮-邵帅",coverImg: "https://y.gtimg.cn/music/photo_new/T002R300x300M000002sNbWp3royJG_1.jpg?max_age=2592000"}},{title: "飞驰的人生",content: "《飞驰人生》应该是韩寒三部曲的第三部。从《后悔无期》到《乘风破浪》再到《飞驰人生》",imgSrc: "/images/jumpfly.png",reading: 96,detail: "《飞驰人生》应该是韩寒三部曲的第三部。从《后悔无期》到《乘风破浪》再到《飞驰人生》,故事是越讲越直白,也越来越贴近大众。关于理想、关于青春永远是韩寒作品的主题。也许生活确实像白开水,需要一些假设的梦想,即使大多数人都不曾为梦想努力过,但我们依然爱看其他人追梦,来给自己带来些许的慰藉。",collection: 26,dateTime: "21小时前",headImgSrc: "/images/jumpfly.png",author: "深白色",date: "Nov 20 2016",avatar: "/images/avatar/5.png",postId: 4,music: {url: "http://music.163.com/song/media/outer/url?id=1344368486.mp3",title: "奉献-韩寒",coverImg: "https://y.gtimg.cn/music/photo_new/T002R300x300M000002sNbWp3royJG_1.jpg?max_age=2592000"}},
]

一个 JS 文件就是 JavaScript 模块。现在 posts.js 需要将这些数据加载过来。不能进行 this.setData 进行数据绑定,这个函数只有在页面的 js 中才能调用,不能随意调用,如果 JS 需要数据导入导出,那么既要有导入,又要有导出。导入在 JS 中进行,导出在对应页面的 Js 中进行。

在 JS 中导出

module.exports = {postList: local_database
}

postList 由自己取名。

在 posts.js 中导入

// pages/posts/posts.jsvar postData = require("../../data/data.js")Page({...
})

其中,require 内 URL 只支持相对路径,postData 接受 JS 中导出的对象,其属性值 postData.postList 才是所需的内容(这也意味着我们可以同时传输很多内容,只要以 键值对 的形式存储到传输对象中)。

我们还有一种更简洁的导出方式。

在 Js 中用 export 代替 module.exports

export {local_dataBase
}

同时导入方法也要改变

import {local_dataBase} from '../../data/data.js';

这里单引号,也可采用双引号。

需要注意的是,导出的数据名称需要与导入的数据名称同源。如果需要导入多数据,则重复使用 export 和 import 即可。可以在多个文件里单独使用 export,然后在一个文件中用多个 import 导入。

6-5 用列表渲染展示导入的数组数据

原先的代码

    this.setData({posts: content,});

现在改为

import {postList} from '../../data/data.js';Page({.../*** 生命周期函数--监听页面加载*/onLoad: function (options) {this.setData({posts: postList,});},...
})

顺便将原来的 local_dataBase 改为了 postList。对于 ES6 语法而言,setData 提供了一个更为简便的数据绑定方式。如果键值对同名,即属性与属性值同名,那么可以简写。如:

    this.setData({postsList: postList,});

则改写为

    this.setData({postList,});

对应的,在 WXML 文件中的变量名也改改

<block wx:for="{{postList}}">
</block>

ES6 还有一个比较好用的方法,用于简写定义匿名函数(对目前而言,通常是方法)的方式,如

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

可简写为

onload(options) {}

options 是函数的默认参数。

6-6 从列表页面跳转到文章详情页面

先新建一个页面:post-detail。确定用户点击时可以进入此页面的区域。利用事件 onJumpToDetail 跳转。

  <view bind:tap="onGoToDetail" class="post-container">
</view>
  onGoToDetail(event) {// 跳转到文章详情页面wx.navigateTo({url: '/pages/post-detail/post-detail',})},

event 是这个函数的默认参数。

6-7 列表渲染的wxkey赋值规则

当我们使用了 wx:for 而不使用 wx:key 时,小程序会提示

Now you can provide attr `wx:key` for a `wx:for` to improve performance.

官方文档描述

wx:key

如果列表中项目的位置会动态改变或者有新的项目添加到列表中,并且希望列表中的项目保持自己的特征和状态(如 input 中的输入内容,switch 的选中状态),需要使用 wx:key 来指定列表中项目的唯一的标识符。

wx:key 的值以两种形式提供

  1. 字符串,代表在 for 循环的 array 中 item 的某个 property,该 property 的值需要是列表中唯一的字符串或数字,且不能动态改变。
  2. 保留关键字 *this 代表在 for 循环中的 item 本身,这种表示需要 item 本身是一个唯一的字符串或者数字。

当数据改变触发渲染层重新渲染的时候,会校正带有 key 的组件,框架会确保他们被重新排序,而不是重新创建,以确保使组件保持自身的状态,并且提高列表渲染时的效率。

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

在原列表渲染中

<block wx:for-items="{{postList}}">
</block>

我们可以加入 wx:key 属性,加入的一般是文章的 id 号

<block wx:for-items="{{postList}}" wx:key="postId">
</block>

不能写 {{postId}},否则会出现警告,表示内容非法。

也可以用数组的 index,index 也是唯一的。

<block wx:for-items="{{postList}}" wx:key="index">
</block>

wx:for 之后建议永远加个 wx:key,其内容要么是 index,要么是数组的唯一可识别的属性。同时一定记住,不加 {{}}。它用于提升性能之用,以在动态数据发生变化时不用再重新渲染。

6-8 事件冒泡的具体应用

目前,点击文章组件的任意区域都会跳转文章详情页面。如果现在需要用户点击作者头像时,跳转的是作者个人主页而非文章详情页面,应该怎么添加、修改代码呢?只需要在头像组件那块用 catch 获取 tap 事件即可。

...<view bind:tap="onGoToDetail" class="post-container"><!-- 作者头像及时间 --><view class="post-author-date"><image catch:tap="onGoToAuthorHome class="post-author" src="{{item.avatar}}"></image>...

7 构建阅读详情页面

7-1 构建文章详情页面(1)

先静后动

先死后活

文章详情页面的组件分布如下:

<!--pages/post-detail/post-detail.wxml--><view class="container"><!-- 文章图片 --><image></image><!-- 作者信息 --><view><!-- 头像 --><image></image><!-- 作者昵称 --><text></text><!-- 发表时间 --><text>发表于</text><text></text></view><!-- 标题 --><text></text><!-- 收藏、分享图标 --><view><image></image><image></image></view><!-- 正文内容 --><text></text>
</view>

7-2 构建文章详情页面(2)

为 wxml 添加细节

<!--pages/post-detail/post-detail.wxml--><view class="container"><!-- 文章图片 --><image src="/images/bestplayers.png"></image><!-- 作者信息 --><view><!-- 头像 --><image src="/images/avatar/2.png"></image><!-- 作者昵称 --><text>dagang</text><!-- 发表时间 --><text>发表于</text><text>24 小时前</text></view><!-- 标题 --><text>2020LPL夏季赛季后赛观赛指南</text><!-- 收藏、分享图标 --><view><image src="/images/icon/share.png"></image><image src="/images/icon/collection.png"></image></view><!-- 正文内容 --><text>8月9号,LPL常规</text>
</view>

7-3 构建文章详情页面(3)

为各部分添加样式。

<!--pages/post-detail/post-detail.wxml--><!-- 文章 -->
<view class="container"><!-- 文章图片 --><image class="head-image" src="/images/bestplayers.png"></image><!-- 作者信息 --><view class="author-date"><!-- 头像 --><image class="avatar" src="/images/avatar/2.png"></image><!-- 作者昵称 --><text  class="author">dagang</text><!-- 发表时间 --><text class="const-text">发表于</text><text class="date">24 小时前</text></view><!-- 标题 --><text>2020LPL夏季赛季后赛观赛指南</text><!-- 收藏、分享图标 --><view><image src="/images/icon/share.png"></image><image src="/images/icon/collection.png"></image></view><!-- 正文内容 --><text>8月9号,LPL常规</text>
</view>
/* pages/post-detail/post-detail.wxss *//* 文章 */
.container {display: flex;flex-direction: column;
}/* 文章图片 */
.head-image {width: 100%;height: 460rpx;
}/* 作者信息 */
.author-date {display: flex;margin-top: 20rpx;margin-left: 30rpx;align-items: center;
}/* 头像 */
.avatar {width: 64rpx;height: 64rpx;
}/* 作者昵称 */
.author {font-size: 30rpx;font-weight: 300;margin-left: 20rpx;color: #666;
}/* 发表时间 */
.const-text {font-size: 24rpx;color: #999;margin-left: 20rpx;
}
.date {font-size: 30rpx;color: #999;margin-left: 20rpx;
}

7-4 Flex的高级应用(1)

承接上文,接着为标题设置样式

  <!-- 标题 --><text class="title">2020LPL夏季赛季后赛观赛指南</text>
/* 标题 */
.title {margin-left: 40rpx;font-size: 36rpx;font-weight: 700;margin-top: 30rpx;letter-spacing: 2rpx;color: #4b556c;
}

接下来安放图标的位置。在图标之下,有一条灰色横线。如何实现此效果呢?可以请设计师设计一张图片,但是这么简单的没必要,可以通过样式解决。

  <!-- 收藏、分享图标 --><view><image src="/images/icon/share.png"></image><image src="/images/icon/collection.png"></image><!-- 横线 --><view class="horizon"></view></view>
/* 横线 */
.horizon {width: 600rpx;height: 1px;background-color: #e5e5e5;
}

现在,如何使横线处于图标底部呢?

第一种方法:浮动布局

将图标放在一个容器中,使它们浮动。

  <!-- 收藏、分享图标 --><view><view class="circle"><image class="circle-image" src="/images/icon/collection.png"></image><image class="circle-image" src="/images/icon/share.png"></image></view><!-- 图标横线 --><view class="horizon"></view></view><!-- 正文内容 --><text>8月9号,LPL常规</text>
</view>
/* 图标 */
.circle {float: right;margin-right: 40rpx;
}
.circle-image{width: 90rpx;height: 90rpx;vertical-align: middle;
}/* 横线 */
.horizon {width: 600rpx;height: 1px;background-color: #e5e5e5;vertical-align: middle;position: relative;top: 46rpx;margin: 0 auto;z-index: -99;
}

复习:

relative:
相对于原来位置移动,元素设置此属性之后仍然处在文档流中,不影响其他元素的布局
absolute:
元素会脱离文档流,如果设置偏移量,会影响其他元素的位置定位。
在父元素没有设置相对定位或绝对定位的情况下,元素相对于根元素定位(即html元素)(是父元素没有)。
父元素设置了相对定位或绝对定位,元素会相对于离自己最近的设置了相对或绝对定位的父元素进行定位;
或者说离自己最近的不是static的父元素进行定位,因为元素默认是static。

但是 vertical-align 现在用的比较少。现在采用第二种方式:flex 布局

使用 flex 布局时的网页如下

  <!-- 收藏、分享图标 --><view class="tool"><view class="circle"><image class="circle-image" src="/images/icon/collection.png"></image><image class="circle-image" src="/images/icon/share.png"></image></view><!-- 图标横线 --><view class="horizon"></view></view>

只需用考虑 circle 和 horizon 这两层元素之间的布局关系。

/* 图标 */
.tool {display: flex;flex-direction: column;align-items: center;justify-content: center;
}
...
/* 横线 */
.horizon {width: 600rpx;height: 1px;background-color: #e5e5e5;position: absolute;z-index: -99;
}

设置完 absolute 后,横线仍是相对于根元素定位。但是这里设置 absolute 目的是在垂直于页面的方向上的排布,而不是在页面的水平方向上排布,所以这块让元素脱离文档流。但是相对于一般的用 top 等排布而言,这里是用的 flex 布局的 justify-content。

7-5 Flex高级应用(2)主轴和交叉轴

flex 的主轴:justify-content

flex 的交叉轴:align-items

flex 的主轴与 flex 的方向平行,align-items 与 flex 的方向垂直。

现在图标因为样式居中,需要通过设置样式使其向右对齐。同时又由于图标所在容器没有宽度,所以需要设置容器的宽度。

.circle {margin-right: 40rpx;width: 660rpx;display: flex;justify-content: flex-end;
}

7-6 Flex高级应用(3)

现在需要调整这两个小图标的细节。

.share {margin-left: 20rpx;
}

最后是文章的正文内容

/* 正文内容 */
.detail {color: #666;margin: 20rpx 30rpx 0 30rpx;line-height: 44rpx;letter-spacing: 2rpx;
}

7-7 组件的自定义属性data

视频缺失,以下是网络内容。

自定义属性语法以data-开头:

 <block wx:for='{{post_key}}' wx:key="key" wx:for-item='item'><view catch:tap='onPostTap' data-postid="{{item.postId}}"><template is='postItem' data='{{...item}}' /></view></block>

无论你这样写 data-postid="{{item.postId}}" 还是这样写 data-POSTID="{{item.postId}}",获取值的时候都是这样:

 onPostTap(event){let postId=event.currentTarget.dataset.postid;console.log(postId);}

无论你大小写,都转换为小写,所以拿的时候直接写 postid,我刚开始设置和获取写的都是 postId,结果输出undefined,之后检查了元素,全部给我这个 data- 后面的名字转换为小写了,这里记录一下细节。

有一种就是 data-post-name-id 这种获取的时候会自动去掉连字符:postNameId,以驼峰方式去获取。

其实 currentTarget 是你当前点击的对象,dataset 就是你自定义属性的集合,很好理解。

结合学习内容,需要在文章目录界面中设置自定义属性:

<block wx:for-items="{{postList}}" wx:key="postId">
</block>

7-8 自定义属性data-的命名规则

我们已经为循环的文章目录建立了 data-id 自定义属性了。那如何拿到它呢?

从事件对象当中获取。事件对象是调用事件函数时接收的默认参数。我们在界面中可以打印它:

  onGoToDetail(event) {console.log(event);// 跳转到文章详情页面wx.navigateTo({url: '/pages/post-detail/post-detail',})},
{type: "tap", timeStamp: 4426, target: {…}, currentTarget: {…}, mark: {…}, …}
changedTouches: [{…}]
currentTarget:
dataset:
id: 0
__proto__: Object
id: ""
offsetLeft: 0
offsetTop: 240
__proto__: Object
detail:
x: 115.5
y: 375.1500244140625
__proto__: Object
mark: {}
mut: false
target: {id: "", offsetLeft: 0, offsetTop: 318, dataset: {…}}
timeStamp: 4426
touches: [{…}]
type: "tap"
_userTap: true
__proto__: Object

既然是事件对象,那自然包含的都是与事件有关的属性,如 detail,描述的是点击的横纵坐标;type,事件的类型。

currentTarget 是当前点击的对象,dataset 就是自定义属性的集合,我们设置的自定义属性都在其中找到。这里的名字是 id,而不是 data-id。关于名字有两种情况:

无连字符:

名字都会被转为小写

有连字符:

名字会被去除连字符,并以驼峰命名法显示

例如:

在 wxml 中的命名 在 JS 中获取到的属性名
data-id id
data-NiceToMeetYou nicetomeetyou
data-nice-to-meet-you niceToMeetYou

之所以 wxml 和 JS 中命名不同,是因为对应的命名规范不同,小程序自动做了个转换。

可以同时设置多个属性(都存储在 dataset 中)。

7-9 在页面的onLoad函数中获取查询参数

我们知道了怎么设置 id 号和绑定 id 号,那如何将 id 号传给文章详情页面呢?这涉及到页面与页面之间如何通信的问题。

页面与页面间数据通信

大多数情况下,页面间数据通信之间的方式非常单一,我们只要通过 URL 即可完成

  onGoToDetail(event) {// console.log(event);const pid = event.currentTarget.dataset.postId;// 跳转到文章详情页面wx.navigateTo({url: '/pages/post-detail/post-detail?pid=' + pid,})},

const声明 创建一个值的只读引用。但这并不意味着它所持有的值是不可变的,只是变量标识符不能重新分配。例如,在引用内容是对象的情况下,这意味着可以改变对象的内容(例如,其参数)。

这里的 ?pid 中 pid 称为查询参数。查询参数可以有多个,如

      url: '/pages/post-detail/post-detail?pid=3&cid=5'

如果一个页面要获取另一个页面的查询参数,我们可以在相应页面的 onLoad 函数中获取。

我们现在文章详情页面中打印出 olLoad 接受的参数看看(默认名是 options,但是现在需要显式输入,没有默认的参数填入了可能–盲猜一波)。

{pid: "0"}
pid: "0"
__proto__: Object

也就是pid作为属性存储在了 options 对象中。

7-10 加载详情数据并填充页面

首先需要从 data.js 中获取数据

import {postList} from "../../data/data.js";

接下来我们要通过传入的 pid 找到需要的文章。

由于文章的 index 顺序和内容与 pid 一致,所以 postList[options.pid] 就是所需的文章对象。

  const postData = postList[options.pid];

注意每个参数的语法。如果 options.pid 和 index 不一致,那么只能通过循环找出与 pid 对应的 postId 所在的对象,再将其赋值给 postData。所以希望所有的数据的唯一属性,最好与 index 一致。

接下来就是将静态数据替换为动态数据。

首先是 post-detail.js 中

...data: {postData: {},},
...const postData = postList[options.pid];this.setData(postData,);
...

但是建议写成如下形式:

...data: {postData: {}},
...const postData = postList[options.pid];this.setData({postData,});
...

然后再在 post-detail.wxml 页面中将静态数据替换为动态

<!--pages/post-detail/post-detail.wxml--><!-- 文章 -->
<view class="container"><!-- 文章图片 --><image class="head-image" src="{{postData.headImgSrc}}"></image><!-- 作者信息 --><view class="author-date"><!-- 头像 --><image class="avatar" src="{{postData.avatar}}"></image><!-- 作者昵称 --><text class="author">{{postData.author}}</text><!-- 发表时间 --><text class="const-text">发表于</text><text class="date">{{postData.date}}</text></view><!-- 标题 --><text class="title">{{postData.title}}</text><!-- 收藏、分享图标 --><view class="tool"><view class="circle"><image src="/images/icon/collection.png"></image><image class="share" src="/images/icon/share.png"></image></view><!-- 图标横线 --><view class="horizon"></view></view><!-- 正文内容 --><text class="detail">{{postData.detail}}</text>
</view>

7-11 为什么只传post-id

数据之间的传输需要本着非必要不重复的原则,可以以共享数据源的方式获取到的信息是动态的,方便自己获取更多内容;而且页面之间的数据不都是彼此需要或拥有的,有些数据父页面上没有。

8 缓存机制与异步API的async和await

8-1 app.js 的意义和作用

视频缺失,以下内容来源于网络。

开发者文档

每个小程序都需要在 app.js 中调用 App 方法注册小程序实例,绑定生命周期回调函数、错误监听和页面不存在监听函数等。

详细的参数含义和使用请参考 App 参考文档 。

// app.js
App({onLaunch (options) {// Do something initial when launch.},onShow (options) {// Do something when show.},onHide () {// Do something when hide.},onError (msg) {console.log(msg)},globalData: 'I am global data'
})

整个小程序只有一个 App 实例,是全部页面共享的。开发者可以通过 getApp 方法获取到全局唯一的 App 实例,获取App上的数据或调用开发者注册在 App 上的函数。

// xxx.js
const appInstance = getApp()
console.log(appInstance.globalData) // I am global data

App 参考文档

App(Object object)

注册小程序。接受一个 Object 参数,其指定小程序的生命周期回调等。

App() 必须在 app.js 中调用,必须调用且只能调用一次。不然会出现无法预期的后果。

参数

Object object

属性 类型 默认值 必填 说明 最低版本
onLaunch function 生命周期回调——监听小程序初始化。
onShow function 生命周期回调——监听小程序启动或切前台。
onHide function 生命周期回调——监听小程序切后台。
onError function 错误监听函数。
onPageNotFound function 页面不存在监听函数。 1.9.90
onUnhandledRejection function 未处理的 Promise 拒绝事件监听函数。 2.10.0
onThemeChange function 监听系统主题变化 2.11.0
其他 any 开发者可以添加任意的函数或数据变量到 Object 参数中,用 this 可以访问

关于小程序前后台的定义和小程序的运行机制,请参考运行机制章节。

onLaunch(Object object)

小程序初始化完成时触发,全局只触发一次。参数也可以使用 wx.getLaunchOptionsSync 获取。

参数:与 wx.getLaunchOptionsSync一致

onShow(Object object)

小程序启动,或从后台进入前台显示时触发。也可以使用 wx.onAppShow 绑定监听。

参数:与 wx.onAppShow 一致

onHide()

小程序从前台进入后台时触发。也可以使用 wx.onAppHide 绑定监听。

onError(String error)

小程序发生脚本错误或 API 调用报错时触发。也可以使用 wx.onError 绑定监听。

参数:与 wx.onError 一致

onPageNotFound(Object object)

基础库 1.9.90 开始支持,低版本需做兼容处理。

小程序要打开的页面不存在时触发。也可以使用 wx.onPageNotFound 绑定监听。注意事项请参考 wx.onPageNotFound

参数:与 wx.onPageNotFound 一致

示例代码:

App({onPageNotFound(res) {wx.redirectTo({url: 'pages/...'}) // 如果是 tabbar 页面,请使用 wx.switchTab}
})

onUnhandledRejection(Object object)

基础库 2.10.0 开始支持,低版本需做兼容处理。

小程序有未处理的 Promise 拒绝时触发。也可以使用 wx.onUnhandledRejection 绑定监听。注意事项请参考 wx.onUnhandledRejection

参数:与 wx.onUnhandledRejection 一致

onThemeChange(Object object)

基础库 2.11.0 开始支持,低版本需做兼容处理。

系统切换主题时触发。也可以使用 wx.onThemeChange 绑定监听。

参数:与 wx.onThemeChange 一致

示例代码

App({onLaunch (options) {// Do something initial when launch.},onShow (options) {// Do something when show.},onHide () {// Do something when hide.},onError (msg) {console.log(msg)},globalData: 'I am global data'
})

AppObject getApp(Object object)

获取到小程序全局唯一的 App 实例。

参数

Object object

属性 类型 默认值 必填 说明 最低版本
allowDefault boolean false 在 App 未定义时返回默认实现。当App被调用时,默认实现中定义的属性会被覆盖合并到App中。一般用于独立分包 2.2.4

示例代码

// other.js
var appInstance = getApp()
console.log(appInstance.globalData) // I am global data

注意事项

  • 不要在定义于 App() 内的函数中,或调用 App 前调用 getApp() ,使用 this 就可以拿到 app 实例。
  • 通过 getApp() 获取实例之后,不要私自调用生命周期函数。

8-2 目的:了解缓存的概念

作业。以下内容来源于网络。

微信开发文档

4.6 本地数据缓存

本地数据缓存是小程序存储在当前设备上硬盘上的数据,本地数据缓存有非常多的用途,我们可以利用本地数据缓存来存储用户在小程序上产生的操作,在用户关闭小程序重新打开时可以恢复之前的状态。我们还可以利用本地缓存一些服务端非实时的数据提高小程序获取数据的速度,在特定的场景下可以提高页面的渲染速度,减少用户的等待时间。

4.6.1 读写本地数据缓存

小程序提供了读写本地数据缓存的接口,通过wx.getStorage/wx.getStorageSync读取本地缓存,通过wx.setStorage/wx.setStorageSync写数据到缓存,其中Sync后缀的接口表示是同步接口,执行完毕之后会立马返回,示例代码和参数说明如下所示。

代码清单4-13 wx.getStorage/wx.getStorageSync 读取本地数据缓存

wx.getStorage({key: 'key1',success: function(res) {// 异步接口在success回调才能拿到返回值var value1 = res.data},fail: function() {console.log('读取key1发生错误')}})try{// 同步接口立即返回值var value2 = wx.getStorageSync('key2')}catch (e) {console.log('读取key2发生错误')}

表4-4 wx.getStorage/wx.getStorageSync详细参数

参数名 类型 必填 描述
key String 本地缓存中指定的 key
success Function 异步接口调用成功的回调函数,回调参数格式: {data: key对应的内容}
fail Function 异步接口调用失败的回调函数
complete Function 异步接口调用结束的回调函数(调用成功、失败都会执行)

代码清单4-14 wx.setStorage/wx.setStorageSync写入本地数据缓存

// 异步接口在success/fail回调才知道写入成功与否wx.setStorage({key:"key",data:"value1"success: function() {console.log('写入value1成功')},fail: function() {console.log('写入value1发生错误')}})try{// 同步接口立即写入wx.setStorageSync('key', 'value2')console.log('写入value2成功')}catch (e) {console.log('写入value2发生错误')}

表4-5 wx.setStorage/wx.setStorageSync详细参数

参数名 类型 必填 描述
key String 本地缓存中指定的 key
data Object/String 需要存储的内容
success Function 异步接口调用成功的回调函数
fail Function 异步接口调用失败的回调函数
complete Function 异步接口调用结束的回调函数(调用成功、失败都会执行)

4.6.2 缓存限制和隔离

小程序宿主环境会管理不同小程序的数据缓存,不同小程序的本地缓存空间是分开的,每个小程序的缓存空间上限为10MB,如果当前缓存已经达到10MB,再通过wx.setStorage写入缓存会触发fail回调。

小程序的本地缓存不仅仅通过小程序这个维度来隔离空间,考虑到同一个设备可以登录不同微信用户,宿主环境还对不同用户的缓存进行了隔离,避免用户间的数据隐私泄露。

由于本地缓存是存放在当前设备,用户换设备之后无法从另一个设备读取到当前设备数据,因此用户的关键信息不建议只存在本地缓存,应该把数据放到服务器端进行持久化存储。

4.6.3 利用本地缓存提前渲染界面

讨论一个需求:我们要实现了一个购物商城的小程序,首页是展示一堆商品的列表。一般的实现方法就是在页面 onLoad 回调之后通过wx.request 向服务器发起一个请求去拉取首页的商品列表数据,等待 wx.request 的 success 回调之后把数据通过 setData 渲染到界面上,如下代码所示。

代码清单4-15 page.js 拉取商品列表数据展示在界面上

Page({onLoad: function() {var that = thiswx.request({url: 'https://test.com/getproductlist',success: function (res) {if (res.statusCode === 200) {that.setData({list: res.data.list})}}})}})

设想一下当用户退出小程序再进来,界面仍然会有白屏现象,因为我们需要等待拉取商品列表的请求回来才能渲染商品列表。当然我们还可以再做一些体验上的优化,例如在发请求前,可能我们会在界面上显示一个Loading提示用户在加载中,但是并没有解决这个延迟渲染的现象,这个时候我们可以利用本地缓存来提前渲染界面。

我们在拉取商品列表后把列表存在本地缓存里,在onLoad发起请求前,先检查是否有缓存过列表,如果有的话直接渲染界面,然后等到wx.request的success回调之后再覆盖本地缓存重新渲染新的列表,如下代码所示。

代码清单4-16 page.js 利用本地缓存提前渲染界面

Page({onLoad: function() {var that = thisvar list =wx.getStorageSync("list")if (list) { // 本地如果有缓存列表,提前渲染that.setData({list: list})}wx.request({url: 'https://test.com/getproductlist',success: function (res) {if (res.statusCode === 200) {list = res.data.listthat.setData({ // 再次渲染列表list: list})wx.setStorageSync("list",list) // 覆盖缓存数据}}})}})

这种做法可以让用户体验你的小程序时感觉加载非常快,但是你还要留意这个做法的缺点,如果小程序对渲染的数据实时性要求非常高的话,用户看到一个旧数据的界面会非常困惑。因此一般在对数据实时性/一致性要求不高的页面采用这个方法来做提前渲染,用以优化小程序体验。

4.6.4 缓存用户登录态 SessionId

在4.4节我们说到处理用户登录态的一般方法,通常用户在没有主动退出登录前,用户的登录态会一直保持一段时间,就无需用户频繁地输入账号密码。如果我们把 SessionId 记录在 Javascript 中某个内存变量,当用户关闭小程序再进来小程序时,之前内存的 SessionId 已经丢失,此时我们就需要利用本地缓存的能力来持久化存储 SessionId。

代码清单4-17 利用本地缓存持久存储用户登录态 SessionId

//page.jsvar app = getApp()Page({onLoad: function() {// 调用wx.login获取微信登录凭证wx.login({success: function(res) {// 拿到微信登录凭证之后去自己服务器换取自己的登录凭证wx.request({url: 'https://test.com/login',data: { code: res.code },success: function(res) {var data = res.data// 把 SessionId 和过期时间放在内存中的全局对象和本地缓存里边app.globalData.sessionId =data.sessionIdwx.setStorageSync('SESSIONID',data.sessionId)// 假设登录态保持1天var expiredTime = +new Date() +1*24*60*60*1000app.globalData.expiredTime =expiredTimewx.setStorageSync('EXPIREDTIME',expiredTime)}})}})}})

在重新打开小程序的时候,我们把上一次存储的 SessionId 内容取出来,恢复到内存。

代码清单4-18 利用本地缓存恢复用户登录态SessionId

//app.jsApp({onLaunch: function(options) {var sessionId =wx.getStorageSync('SESSIONID')var expiredTime =wx.getStorageSync('EXPIREDTIME')var now = +new Date()if (now - expiredTime <=1*24*60*60*1000) {this.globalData.sessionId = sessionIdthis.globalData.expiredTime = expiredTime}},globalData: {sessionId: null,expiredTime: 0}})

最后一次编辑于 2019年08月19日 (未经腾讯允许,不得转载)

8-3 在 app.js 中保存全局变量

我们现在 app.js 中定义一个全局变量

App({test: 123,})

假设我们现在要在 post-detail.js 中获取这个变量,怎么获取呢?

const app = getApp();

其中,getApp 是小程序提供的内置方法。获取到的是 app 中的对象。打印出 app,会发现

ze {test: 123, onLaunch: ƒ, onShow: ƒ, onHide: ƒ, onUnlaunch: ƒ}
onHide: ƒ ()
onLaunch: ƒ ()
onShow: ƒ ()
onUnlaunch: ƒ ()
test: 123
__proto__: Object

可以看到其中内默认的方法。通过 app.test 即可调用此变量。

用户的收藏状态是否能保存在全局变量中呢?这方法不太好,应该是保存在小程序的本地缓存里。

8-4 作业 目的: 熟悉 async 和 await 的用法和意义

作业。以下内容来源于网络。感觉不好理解,等看完后边几节的视频再反过来消化消化(有些代码属于知识盲区了)

async/await 是什么

async/await 是ES7提出的基于Promise的解决异步的最终方案。

async

async 是一个加在函数前的修饰符,被 async 定义的函数会默认返回一个 Promise 对象 resolve 的值。因此对 async 函数可以直接 then,返回值就是then方法传入的函数。

// async基础语法
async function fun0(){console.log(1);return 1;
}
fun0().then(val=>{console.log(val) // 1,1
})async function fun1(){console.log('Promise');return new Promise(function(resolve,reject){resolve('Promise')})
}
fun1().then(val => {console.log(val); // Promise Promise
})

await

await 也是一个修饰符,只能放在 async 定义的函数内。可以理解为等待。

await 修饰的如果是 Promise 对象:可以获取 Promise 中返回的内容(resolve 或 reject 的参数),且取到值后语句才会往下执行;

如果不是 Promise 对象:把这个非 promise 的东西当做 await 表达式的结果。

async function fun(){let a = await 1;let b = await new Promise((resolve,reject)=>{setTimeout(function(){resolve('setTimeout')},3000)})let c = await function(){return 'function'}()console.log(a,b,c)
}
fun(); // 3秒后输出: 1 "setTimeout" "function"
function log(time){setTimeout(function(){console.log(time);return 1;},time)
}
async function fun(){let a = await log(1000);let b = await log(3000);let c = log(2000);console.log(a);console.log(1)
}
fun();
// 立即输出 undefined 1
// 1秒后输出 1000
// 2秒后输出 2000
// 3秒后输出 3000

async/await 的正确用法

// 使用async/await获取成功的结果// 定义一个异步函数,3秒后才能获取到值(类似操作数据库)
function getSomeThing(){return new Promise((resolve,reject)=>{setTimeout(()=>{resolve('获取成功')},3000)})
}async function test(){let a = await getSomeThing();console.log(a)
}
test(); // 3秒后输出:获取成功

8-5 小程序缓存的增删改查与清除 (同步)

小程序的全局变量的生命周期是整个小程序的生命周期,所以,如果变量属于小程序一次使用时需要存储的,可以考虑全局变量,如果是需要小程序多次使用时都能连续性存储的变量,应该用到本地缓存。

我们在 posts.js 的 onLoad 中进行试验。

同步缓存

语法

wx.setStorageSync('key', data);

key: 缓存的变量名

data: 需要缓存的数据

可以在调试器的 Storage 中查看到缓存的数据。如

    wx.setStorageSync('flag', true);

flag 存储的就是布尔类型的变量。

同步修改变量

也是使用 wx.setStorageSync 进行覆盖重写。

同步删除变量缓存

    wx.removeStorageSync('key');

只需要传入缓存的变量名即可。

同步清空缓存

    wx.clearStorageSync();

同步获取缓存

    wx.getStorageSync('key');

8-6 异步函数的几个方案:回调函数、promise 与 await

开发工具中提供了清除缓存的快捷方法。提供了几个选项:

清除数据缓存
清除文件缓存
清除编译缓存
清除授权数据
清除网络缓存
清除登录状态
全部清除

以 wx.getStorage 方法为例,聊聊异步缓存方法。

有时候我们操作的数据量比较大,异步缓存的方法可以使界面 UI 不至于那么卡顿,系统上会好一些。但是异步的东西写起来麻烦一些。但是数据量少的时候没必要使用,差别不大。

先展示同步缓存的存储与读取

    wx.setStorageSync('flag', 1);wx.getStorageSync('key');

然后我们用异步的方法试试。

异步缓存获取

    const flag = wx.getStorage({key: 'flag',});

接受的是一个对象,这个对象有些复杂,在纯电商应用中有详细讲解,这个课程不涉及。现在的问题是,如何获取到这个 flag 变量。

老式方案

需要用到 success 回调函数。

    const flag = wx.getStorage({key: 'flag',success(data) {console.log(data);}});

得到的是一个对象

{errMsg: "getStorage:ok", data: 1}
data: 1
errMsg: "getStorage:ok"
__proto__: Object

所以,我们所需的变量的值实际被存储在了 data.data 中。为了以示区分,将 data 对象重命名为 res

    const flag = wx.getStorage({key: 'flag',success(res) {console.log(data);}});

如果像下面这样写会打印出什么呢

    const flag = wx.getStorage({key: 'flag',success(res) {console.log(res);return res.data;},});console.log(flag);

结果是打印出

undefined{errMsg: "getStorage:ok", data: 1}

需要注意打印的先后顺序,由此可以体会到异步的特性。如果需要获取 res.data,需要

  data: {postList: {},temp: 0,},
...
onLoad: function (options) {var that = this;wx.setStorageSync('flag', 1);// wx.getStorageSync('key');const flag = wx.getStorage({key: 'flag',success(res) {console.log(res.data);that.setData({temp: res.data,});},});console.log(this.data.temp);this.setData({postList,});},

res.data 就被存储在了 data 对象的 temp 属性中。由于 success 是晚于 console.log(this.data.temp); 执行的,所以此时打印出的 temp 仍是 0。但是在 wxml 引用时,temp 已经变成了 1。

如果仔细观察代码还会发现,在调用 setData 方法时,使用的是 that.setData,直接使用 this.setData 报错

TypeError: Cannot read property 'setData' of undefined

原因是:

success 方法指向闭包,所以 this 属于闭包,由此在 success 回调函数里是不能直接使用 this.setData() 的,如果我们要使用的话,可以在闭包之外先把 this 赋值给另一个变量。

这个方法牵涉到更深的理解,目前还没学到这么深。

当然,不一定要存储在 data 对象中,也可以用个临时变量存储它。

  onLoad: function (options) {var temp = 0;wx.setStorageSync('flag', 1);// wx.getStorageSync('key');const flag = wx.getStorage({key: 'flag',success(res) {console.log(res.data);temp = res.data;},});console.log(temp);

此时仍是先输出 0 后输出 1。所以,如果此时再用 this.setData 方法

    this.setData({...temp,});

此时的 temp 不会是我们想要的 1,而是初始的 0。

综上,虽然可以用临时变量存储 success 函数的数值,但是需用 setData 方法才能真正存储它。关于数据的获取,新版小程序有更简单的方案。

新版方案一

我们已经知道,

    const flag = wx.getStorage({key: 'flag',});

此时的变量 flag 接收到的是一个promise 对象。

V {}
__proto__: Promisecatch: ƒ catch()constructor: ƒ Promise()finally: ƒ finally()then: ƒ then()Symbol(Symbol.toStringTag): "Promise"__proto__: Object

需要注意的是,如果 getStorage 中有回调函数

    const flag = wx.getStorage({key: 'flag',success(res) {console.log(res);return res.data;}});

此时 getStorage 不返回任何内容,flag 打印时显示 undifined。其所有的结果都在该函数中通过 res 来传递,这就是异步的特点。

如果没有 success 回调函数,我们可以通过 promise 对象获取数据,具体是使用其 then() 方法。

  onLoad: function (options) {wx.setStorageSync('flag', 1);// wx.getStorageSync('key');const flag = wx.getStorage({key: 'flag',});flag.then((value)=>{console.log(value)});

打印出的内容是

{errMsg: "getStorage:ok", data: 1}data: 1errMsg: "getStorage:ok"__proto__: Object

then() 方法接受的也是一个回调函数,回调函数的参数内容与 success 回调函数的参数内容相同。

新版方案二

ES7 中还提供了一个方案,在函数体内部加上 await,在方法体前上 async。具体如下

  onLoad: async function (options) {...const flag =await wx.getStorage({key: 'flag',});...

或是

  async onLoad(options) {...const flag =await wx.getStorage({key: 'flag',});
...

打印出 flag

{errMsg: "getStorage:ok", data: 1}data: 1errMsg: "getStorage:ok"__proto__: Object

与先前的一致。

这是最简单的方法。

8-7 文章收藏(1)分析思路

利用本地缓存,应用文章收藏功能。

我们先在编译模式中选择 post-detail 页面。此时,由于并未传入 pid,所以页面无法显示动态信息。可以在编译模式的启动参数中编写 pid=0

目标:获取用户点击事件,执行事件函数

    <view class="circle"><image bind:tap="onCollect" src="/images/icon/collection-anti.png"></image><image class="share" src="/images/icon/share.png"></image></view>
  onCollect() {// 点击收藏图标时触发wx.setStorageSync('collected', true);},

这样写是有问题的:

  • 没有记录收藏的页面,无法确定哪篇文章被收藏
  • 无法存储多篇文章的收藏状态

可以设置一个对象存储所需信息,有两种解决方案

第一种

{key: id, // 存储文章 id 号flag: true | false // 存储文章收藏状态
}

第二种:

id: true | false // 使用一种属性,属性自身带能判断出 id 号的功能,其属性值是布尔类型

第二种方法写着简单,假设有 5 篇文章,只需要设置出 5 个属性就行。第一种方法反而更绕了一些。具体代码如下

  onCollect() {// 点击收藏图标时触发wx.setStorageSync('posts_collected', {pid: true,})},

这里的 pid 需要用真实的 pid 替代,而后者是在 onLoad 函数中

  onLoad: function (options) {// console.log(options);const postData = postList[options.pid];this.setData({postData,});},

这里的 options.pid 才是我们真正想要的。可是如何让另一个函数获取到这个函数的局部变量呢?可以使用 data 对象进行中转。以下代码解决的是变量的转出:

...data: {postData: {},_pid: null,},
...onLoad: function (options) {...this.data._pid = options.pid;
...
...

这里没有将 this.data._pid 用 setData 方法绑定,因为不需要传输给页面,只在 JS 中使用。

现在的问题是:如何获取到 data 中的 _pid 并设置到对象中呢?

8-8 文章收藏(2)JS的动态属性

原代码是

  onCollect() {// 点击收藏图标时触发wx.setStorageSync('posts_collected', {pid: true,})},

如何根据需要修改 pid 的名字呢?

需要用到 JS 中对象属性的设置方式。JS 中定义对象属性时可以通过以下几种方式定义

object {key: value,
}object['key'] = value;

其中,第二种方式允许我们传入一个变量参数,所以,JS 需要通过变量设置属性值时,使用第二种方法。

  onCollect() {// 点击收藏图标时触发const postsCollected = {};postsCollected[this.data._pid] = true;wx.setStorageSync('posts_collected', postsCollected);},

8-9 收藏未收藏的切换

我们现在先把文章的收藏图标随着用户点击事件的发生而改变

  data: {postData: {},collected: false,_pid: null,},onCollect() {// 点击收藏图标时触发const postsCollected = {};this.data.collected = true;this.setData({collected: this.data.collected,});postsCollected[this.data._pid] = this.data.collected;wx.setStorageSync('posts_collected', postsCollected);},
    <view class="circle"><image wx:if="{{collected}}" bind:tap="onCollect" src="/images/icon/collection.png"></image><image wx:else bind:tap="onCollect" src="/images/icon/collection-anti.png"></image><image class="share" src="/images/icon/share.png"></image></view>

注意,虽然我们已经设置了 this.data.collected = true ,但是此后尚未进行数据传输,因此视图层收到的 collected 仍是初始值。因此,对于需要通过 UI 传输的数据而言,都要利用 this.setData() 做好数据绑定。因此,可以写成以下形式

  onCollect() {// 点击收藏图标时触发const postsCollected = {};this.setData({collected: true,});postsCollected[this.data._pid] = this.data.collected;wx.setStorageSync('posts_collected', postsCollected);},

这样就同时做到了改变和传输数据。

8-10 初始化收藏状态

我们目前是通过 data 变量的值对收藏状态进行初始化,但是真正有效的初始值应该是在缓存当中。

在 onLoad 中读取缓存

  onLoad: function (options) {const postData = postList[options.pid];this.data._pid = options.pid;const postsCollected = wx.getStorageSync('posts_collected');this.setData({postData,collected: postsCollected[this.data._pid],});},

以上代码写成下面这种形式可能更清晰些

  onLoad: function (options) {const postData = postList[options.pid];this.data._pid = options.pid;const postCollected = wx.getStorageSync('posts_collected');const collected = postCollected[this.data._pid];this.setData({postData,collected,});},

如果文章并未有相关缓存数据(即加载文章时没有对应的 posts_collected 数据时,collected 的初始值是什么呢?

undefined,并不影响 wx:if 事件的判断,所以可以正确运行,不必非要使得 collected 的初始值是 false。

还有一个问题:现在的点击事件逻辑是,将 collected 的值设为 true。显然,当 collected 为 true,点击时需要转为 false。如果 collected 的值最初是 undefined,用取反的方式,可以使 collected 变为 true 吗?、

有关 collected 的语法如下

    const collected = !postsCollected[this.data._pid];this.setData({collected,});postsCollected[this.data._pid] = this.data.collected;

经检验,当 postsCollected[this.data._pid] 为 undefined 时,经过取反会变为 true。

8-11 缓存如何不被覆盖?

现在的问题是,如下代码

  onCollect() {// 点击收藏图标时触发const postsCollected = {};this.setData({collected: true,});postsCollected[this.data._pid] = this.data.collected;wx.setStorageSync('posts_collected', postsCollected);},

缓存存储时,posts_collected 存储的是一个对象,这个对象由空对象变为只有一个键值对的对象,然后被存储。这样的问题是:对于每一个页面的点击事件而言,都会覆盖旧有缓存,重写一个新缓存。

所以不能是空对象,新的属性,必须添加在包含原有属性的对象之中。

个人想法(老师讲解见下一节)

  onCollect() {// 点击收藏图标时触发const postsCollected = wx.getStorageSync('posts_collected');console.log(postsCollected);console.log(typeof(postsCollected));const collected = !postsCollected[this.data._pid];console.log(postsCollected[this.data._pid]);console.log(collected);this.setData({collected,});// postsCollected[this.data._pid] = this.data.collected;// wx.setStorageSync('posts_collected', postsCollected);},

清空数据缓存,点击收藏图标,查看打印出的值


string
undefined
true

在小程序尚未存储任何本地缓存时,postsCollected 获取到的是一个空string;如果小程序已有本地缓存,

{0: true}

此时打印出的结果为

{0: true}
object
true
false

所以,重点在于当 postsCollected 无法获取到一个不存在的本地缓存时,需要将 postsCollected 设置为空对象用于存储。这就需要检测 postsCollected 在定义是是否成功取到缓存。修改后的代码如下

  onCollect() {// 点击收藏图标时触发let postsCollected = wx.getStorageSync('posts_collected');console.log(postsCollected);if(typeof(postsCollected) != 'object'){postsCollected = {};}console.log(postsCollected);const collected = !postsCollected[this.data._pid];this.setData({collected,});postsCollected[this.data._pid] = this.data.collected;wx.setStorageSync('posts_collected', postsCollected);},

这里将 postsCollected 声明为变量。当它非对象时,需要设置为空对象。

8-12 同步文章缓存状态

老师讲解

我们需要在原有的 postsCollected 中添加内容,首先就是要获取 postsCollected 。由于在 onLoad 函数中,为了将 collected 的初始值与缓存一致,我们已经取过一次缓存,不必再在 onCollect 中再取用一次。所以可以在 data 中设置一个变量,获取 onLoad 中获取到的缓存

  data: {postData: {},collected: false,_pid: null,_postsCollected: {},},

需要做数据绑定的变量,不加下划线;不需要数据绑定的变量,价格下划线,以示区分,会提高运行效率。小程序会默认不对加下划线的变量作数据绑定,提高性能。将此变量用于 onCollect 方法中

  onLoad: function (options) {...const postsCollected = wx.getStorageSync('posts_collected');this.data._postsCollected = postsCollected;
...});
  onCollect() {// 点击收藏图标时触发const postsCollected = this.data._postsCollected;this.setData({collected: true,});postsCollected[this.data._pid] = true;wx.setStorageSync('posts_collected', postsCollected);},

虽然在缓存不存在时,this.data._postsCollected 是个空值,打印其内容和类型时输出的效果如下


string

这种做法同样会遇到缓存不存在时读取的问题,需要在获取缓存后加上条件判断。

8-13 showToast 接口的应用

需求:点击收藏文章时,提示“收藏成功”。

showToast 严格来讲是原生 API,而不是一个组件,所以只要在需要时,在 js 中调用即可

    wx.showToast({title: '收藏成功',});

参数

Object object

属性 类型 默认值 必填 说明 最低版本
title string 提示的内容
icon string success 图标
合法值:success
说明:显示成功图标。此时 title 文本最多显示 7 个汉字长度error:显示失败图标,
此时 title 文本最多显示 7 个汉字长度
loading:显示加载图标,
此时 title 文本最多显示 7 个汉字长度
none:不显示图标,
此时 title 文本最多可显示两行,1.9.0及以上版本支持
image string 自定义图标的本地路径,image 的优先级高于 icon 1.1.0
duration number 1500 提示的延迟时间
mask boolean false 是否显示透明蒙层,防止触摸穿透
success function 接口调用成功的回调函数
fail function 接口调用失败的回调函数
complete function 接口调用结束的回调函数(调用成功、失败都会执行)

简单案例

    wx.showToast({title: this.data.collected?'收藏成功':'取消收藏',duration: 3000});

8-14 牢记setData对于data属性的影响

setData 对作用于数据,相当于对数据同时进行了赋值/存储和绑定。

showToast更换成showModal(1)

showToast:轻提示

showModal:强提示,需要用户操作

    wx.showModal({title: this.data.collected?'是否收藏文章':'是否取消收藏',});

showModal 会提供 确定/取消 两个按钮。如何知道用户点击的是哪个按钮呢?

学习官方文档:

从官方文档蹭蹭内容:

wx.showModal(Object object)

以 Promise 风格 调用:支持

小程序插件:支持,需要小程序基础库版本不低于 1.9.6

微信 Windows 版:支持

微信 Mac 版:支持

显示模态对话框

参数

Object object

属性 类型 默认值 必填 说明 最低版本
title string 提示的标题
content string 提示的内容
showCancel boolean true 是否显示取消按钮
cancelText string 取消 取消按钮的文字,最多 4 个字符
cancelColor string #000000 取消按钮的文字颜色,必须是 16 进制格式的颜色字符串
confirmText string 确定 确认按钮的文字,最多 4 个字符
confirmColor string #576B95 确认按钮的文字颜色,必须是 16 进制格式的颜色字符串
editable boolean false 是否显示输入框 2.17.1
placeholderText string 显示输入框时的提示文本 2.17.1
success function 接口调用成功的回调函数
fail function 接口调用失败的回调函数
complete function 接口调用结束的回调函数(调用成功、失败都会执行)

object.success 回调函数

参数

Object res

属性 类型 说明 最低版本
content string editable 为 true 时,用户输入的文本
confirm boolean 为 true 时,表示用户点击了确定按钮
cancel boolean 为 true 时,表示用户点击了取消(用于 Android 系统区分点击蒙层关闭还是点击取消按钮关闭) 1.1.0

示例代码

wx.showModal({title: '提示',content: '这是一个模态弹窗',success (res) {if (res.confirm) {console.log('用户点击确定')} else if (res.cancel) {console.log('用户点击取消')}}
})

注意

  • Android 6.7.2 以下版本,点击取消或蒙层时,回调 fail, errMsg 为 “fail cancel”;
  • Android 6.7.2 及以上版本 和 iOS 点击蒙层不会关闭模态弹窗,所以尽量避免使用「取消」分支中实现业务逻辑
  • 自基础库 2.17.1 版本起,支持传入 editable 参数,显示带输入框的弹窗

自己尝试编写

async onCollect() {// 点击收藏图标时触发// wx.showToast({//   title: this.data.collected?'收藏成功':'取消收藏',//   duration: 3000// });const selected =await wx.showModal({title: this.data.collected?'取消收藏':'收藏文章',});if(selected.confirm){const postsCollected = this.data._postsCollected;this.setData({collected: !postsCollected[this.data._pid],});postsCollected[this.data._pid] = !postsCollected[this.data._pid];wx.setStorageSync('posts_collected', postsCollected);}},

8-16 showModal的回调函数与Promise

与自己写的相同。

有的 API 不支持 promise/async+await,需要留意

8-17 showActionSheet的使用

官方说明:显示操作菜单

所以这个 API 不仅仅作分享菜单,可以用它实现其他功能。(如果想用其样式和功能,更复杂的建议用 lin-ui)

用户选择的结果以数组序号的形式返回给 promise 对象的 tapIndex

那分享功能用什么 API 呢?onShowAppMessage。但是一般通过自带的小程序菜单分享,没必要做

9 音乐播放

9-1 浮动居中方案-通过 left 和 top 定位音乐图标

现在页面上加上音乐图标

  <!-- 音乐图标 --><image class="audio" src="/images/music/music-start.png"></image>
/* 音乐图标 */
.audio {width: 102rpx;height: 110rpx;position: absolute;left: 50%;margin-left: -51rpx; top: 230rpx;margin-top: -55rpx;opacity: 30%;
}

9-2 小程序音乐播放API介绍

缺失,找找开发文档

wx.playVoice(Object object)

从基础库 1.6.0 开始,本接口停止维护,请使用 wx.createInnerAudioContext 代替

以 Promise 风格 调用:支持

小程序插件:支持,需要小程序基础库版本不低于 1.9.6

开始播放语音。同时只允许一个语音文件正在播放,如果前一个语音文件还没播放完,将中断前一个语音播放。

参数

Object object

属性 类型 默认值 必填 说明 最低版本
filePath string 需要播放的语音文件的文件路径 (本地路径)
duration number 60 指定播放时长,到达指定的播放时长后会自动停止播放,单位:秒 1.6.0
success function 接口调用成功的回调函数
fail function 接口调用失败的回调函数
complete function 接口调用结束的回调函数(调用成功、失败都会执行)

示例代码

wx.startRecord({success (res) {const tempFilePath = res.tempFilePathwx.playVoice({filePath: tempFilePath,complete () { }})}
})
wx.pauseVoice(Object object)

从基础库 1.6.0 开始,本接口停止维护,请使用 wx.createInnerAudioContext 代替

以 Promise 风格 调用:支持

小程序插件:支持,需要小程序基础库版本不低于 1.9.6

暂停正在播放的语音。再次调用 wx.playVoice 播放同一个文件时,会从暂停处开始播放。如果想从头开始播放,需要先调用 wx.stopVoice

参数

Object object

属性 类型 默认值 必填 说明
success function 接口调用成功的回调函数
fail function 接口调用失败的回调函数
complete function 接口调用结束的回调函数(调用成功、失败都会执行)

示例代码

wx.startRecord({success (res) {const tempFilePath = res.tempFilePathwx.playVoice({filePath: tempFilePath})setTimeout(() => { wx.pauseVoice() }, 5000)}
})
wx.stopVoice(Object object)

从基础库 1.6.0 开始,本接口停止维护,请使用 wx.createInnerAudioContext 代替

以 Promise 风格 调用:支持

小程序插件:支持,需要小程序基础库版本不低于 1.9.6

结束播放语音。

参数

Object object

属性 类型 默认值 必填 说明
success function 接口调用成功的回调函数
fail function 接口调用失败的回调函数
complete function 接口调用结束的回调函数(调用成功、失败都会执行)

示例代码

wx.startRecord({success (res) {const tempFilePath = res.tempFilePathwx.playVoice({filePath: tempFilePath,})setTimeout(() => { wx.stopVoice() }, 5000)}
})
InnerAudioContext wx.createInnerAudioContext(Object object)

基础库 1.6.0 开始支持,低版本需做兼容处理

小程序插件:支持,需要小程序基础库版本不低于 1.9.6

微信 Windows 版:支持

微信 Mac 版:支持

创建内部 audio 上下文 InnerAudioContext 对象。

参数

Object object

属性 类型 默认值 必填 说明 最低版本
useWebAudioImplement boolean false 是否使用 WebAudio 作为底层音频驱动,默认关闭。对于短音频、播放频繁的音频建议开启此选项,开启后将获得更优的性能表现。由于开启此选项后也会带来一定的内存增长,因此对于长音频建议关闭此选项。 2.19.0

返回值

InnerAudioContext

InnerAudioContext 文档

InnerAudioContext 实例,可通过 wx.createInnerAudioContext 接口获取实例。注意,音频播放过程中,可能被系统中断,可通过 wx.onAudioInterruptionBeginwx.onAudioInterruptionEnd 事件来处理这种情况。

属性

string src

音频资源的地址,用于直接播放。2.2.3 开始支持云文件ID

number startTime

开始播放的位置(单位:s),默认为 0

boolean autoplay

是否自动开始播放,默认为 false

boolean loop

是否循环播放,默认为 false

boolean obeyMuteSwitch

是否遵循系统静音开关,默认为 true。当此参数为 false 时,即使用户打开了静音开关,也能继续发出声音。从 2.3.0 版本开始此参数不生效,使用 wx.setInnerAudioOption 接口统一设置。

number volume

基础库 1.9.90 开始支持,低版本需做兼容处理。

音量。范围 0~1。默认为 1

number playbackRate

基础库 2.11.0 开始支持,低版本需做兼容处理。

播放速度。范围 0.5-2.0,默认为 1。(Android 需要 6 及以上版本)

number duration

当前音频的长度(单位 s)。只有在当前有合法的 src 时返回(只读)

number currentTime

当前音频的播放位置(单位 s)。只有在当前有合法的 src 时返回,时间保留小数点后 6 位(只读)

boolean paused

当前是是否暂停或停止状态(只读)

number buffered

音频缓冲的时间点,仅保证当前播放时间点到此时间点内容已缓冲(只读)

string referrerPolicy

基础库 2.13.0 开始支持,低版本需做兼容处理。

origin: 发送完整的referrer; no-referrer: 不发送。格式固定为 https://servicewechat.com/{appid}/{version}/page-frame.html,其中 {appid} 为小程序的 appid,{version} 为小程序的版本号,版本号为 0 表示为开发版、体验版以及审核版本,版本号为 devtools 表示为开发者工具,其余为正式版本;

方法

.play()

播放

.pause()

暂停。暂停后的音频再播放会从暂停处开始播放

.stop()

停止。停止后的音频再播放会从头开始播放。

.seek(number position)

跳转到指定位置

.destroy()

销毁当前实例

.onCanplay(function callback)

监听音频进入可以播放状态的事件。但不保证后面可以流畅播放

.offCanplay(function callback)

取消监听音频进入可以播放状态的事件

.onPlay(function callback)

监听音频播放事件

.offPlay(function callback)

取消监听音频播放事件

.onPause(function callback)

监听音频暂停事件

.offPause(function callback)

取消监听音频暂停事件

.onStop(function callback)

监听音频停止事件

.offStop(function callback)

取消监听音频停止事件

.onEnded(function callback)

监听音频自然播放至结束的事件

.offEnded(function callback)

取消监听音频自然播放至结束的事件

.onTimeUpdate(function callback)

监听音频播放进度更新事件

.offTimeUpdate(function callback)

取消监听音频播放进度更新事件

.onError(function callback)

监听音频播放错误事件

.offError(function callback)

取消监听音频播放错误事件

.onWaiting(function callback)

监听音频加载中事件。当音频因为数据不足,需要停下来加载时会触发

.offWaiting(function callback)

取消监听音频加载中事件

.onSeeking(function callback)

监听音频进行跳转操作的事件

.offSeeking(function callback)

取消监听音频进行跳转操作的事件

.onSeeked(function callback)

监听音频完成跳转操作的事件

.offSeeked(function callback)

取消监听音频完成跳转操作的事件

支持格式

格式 iOS Android
flac x
m4a
ogg x
ape x
amr x
wma x
wav
mp3
mp4 x
aac
aiff x
caf x

示例代码

const innerAudioContext = wx.createInnerAudioContext()
innerAudioContext.autoplay = true
innerAudioContext.src = 'http://ws.stream.qqmusic.qq.com/M500001VfvsJ21xFqb.mp3?guid=ffffffff82def4af4b12b3cd9337d5e7&uin=346897220&vkey=6292F51E1E384E061FF02C31F716658E5C81F5594D561F2E88B854E81CAAB7806D5E4F103E55D33C16F3FAC506D1AB172DE8600B37E43FAD&fromtag=46'
innerAudioContext.onPlay(() => {console.log('开始播放')
})
innerAudioContext.onError((res) => {console.log(res.errMsg)console.log(res.errCode)
})

9-3 小试音乐播放API

自己尝试

试试用新的 API 自己先写一波。

先捕捉点击事件

  <!-- 音乐图标 --><image bind:tap="onMusic" class="audio" src="/images/music/music-start.png"></image>

编写函数,使其拥有播放效果。此时,需要知道对应的文章,用 _pid 获取。

  data: {...play: false,
...},onMusic() {const music = wx.createInnerAudioContext({});console.log(music);music.src = postList[this.data._pid].music.url;if(!this.data.play){music.play();this.data.play = true;};},

加入了 play 属性,以判断音乐的播放状态。因为页面的图标要随 play 属性变化,因此需要将其作为数据绑定的内容。这样写其实有问题:每次都创建了一个音乐实例,每次点击都针对新的音乐实例。

问题原因是音频实例创建太多了。创建一个就好了

新代码如下

...data: {..._music: {},},onMusic() {this.data._music.src = postList[this.data._pid].music.url;let play = this.data.play;if(!play){this.data._music.play();play = true;}else {this.data._music.pause();play = false;};this.setData({play,})},/*** 生命周期函数--监听页面加载*/onLoad: function (options) {...this.data._music = wx.createInnerAudioContext({});},

考虑了页面变化细节之后,最终的相关如下

  <!-- 音乐图标 --><image wx:if="{{!play}}" bind:tap="onMusic" class="audio" src="/images/music/music-start.png"></image><image wx:else bind:tap="onMusic" class="audio" src="/images/music/music-stop.png"></image>

老师讲解

老师使用了一个不一样的 API

  <!-- 音乐图标 --><image bind:tap="onMusic" class="audio" src="/images/music/music-start.png"></image>
  onMusic(){console.log("onmusic");const bgm = wx.getBackgroundAudioManager();bgm.src = postList[0].music.url;bgm.title = postList[0].music.title;},

小程序提示

[接口更新提示] 若需要小程序在退到后台后继续播放音频,你需要在 app.json 中配置 requiredBackgroundModes 属性

官方说明:

requiredBackgroundModes

微信客户端 6.7.2 及以上版本支持

申明需要后台运行的能力,类型为数组。目前支持以下项目:

  • audio: 后台音乐播放
  • location: 后台定位

如:

{"pages": ["pages/index/index"],"requiredBackgroundModes": ["audio", "location"]
}

注:在此处申明了后台运行的接口,开发版和体验版上可以直接生效,正式版还需通过审核。

如果是 innerAudioContext,默认会在后台播放。

9-4 切换音乐播放图标的两种方案:条件渲染与 js 表达式

先解决个遗留的小问题,将播放的音乐内容也随数据动态变化

  onMusic(){console.log("onmusic");const bgm = wx.getBackgroundAudioManager();bgm.src = postList[this.data._pid].music.url;bgm.title = postList[this.data._pid].music.title;},

接下来解决音乐图标切换的问题。

条件渲染

现在页面中加入相关图片

  <!-- 音乐图标 --><image bind:tap="onMusic" class="audio" src="/images/music/music-start.png"></image><image class="audio" src="/images/music/music-music-stop.png"></image>

现在 JS 中加入用于判断的变量

  data: {...isPlaying: false,},onMusic(){...this.setData({isPlaying: true,})},

JS 表达式

如果不想在页面中那么多图片标签,也可以允许只写一个

  <image bind:tap="onMusic" class="audio" src="{{isPlaying?'/images/music/music-start.png':'/images/music/music-start.png'}}"></image>

这里要返回的字符串太多,不推荐这样写,但是可以作为一思路,当所需代码不用这么长时,可以考虑;当可以考虑使用三元表达式时,考虑是否适用。

9-5 音乐播放状态切换

废话不多说,代码如下

  <!-- 音乐图标 --><image wx:if="{{!isPlaying}}" bind:tap="onMusicStart" class="audio" src="/images/music/music-start.png"></image><image wx:else bind:tap="onMusicStop" class="audio" src="/images/music/music-stop.png"></image>
  onMusicStart() {const bgm = wx.getBackgroundAudioManager();bgm.src = postList[this.data._pid].music.url;bgm.title = postList[this.data._pid].music.title;this.setData({isPlaying: true,});},onMusicStop() {const bgm = wx.getBackgroundAudioManager();bgm.stop();this.setData({isPlaying: false,});},

可以为背景音乐加上图片

  onMusicStart() {const bgm = wx.getBackgroundAudioManager();const music = postList[this.data._pid].music;bgm.src = music.url;bgm.title = music.title;bgm.coverImgUrl = music.coverImg;this.setData({isPlaying: true,});},

9-6 背景音乐的监听相关API

当用户点击我们定义的图片组件时,我们可以轻松获取这些事件;当用户通过微信自带的背景音乐控制面板操作时,我们如何获取这些动作呢?用 BackgroundAudioManager.onPlay

官方文档:

BackgroundAudioManager.onPlay(function callback)

小程序插件:支持

监听背景音频播放事件

参数

function callback

背景音频播放事件的回调函数

9-7 同步音乐总控开关与自有播放开光的状态

在进入下一步之前,解决代码遗留的小问题:

虽然在 onMusicStart 和 onMusicStop 创建了对象实例,但是这相当于每一次的点击事件都会创建一个实例。之所以 onMusicStart 和 onMusicStop 可以作用于同一音频,但是这是因为 BackgroundAudioManager 的特性。

盲猜一波,这是因为 BackgroundAudioManager 调用了播放器,所以 onMusicStart 和 onMusicStop 作用于播放器上,而不是音乐里。而 InnerAudioContext 是直接指向的音乐,用的相互独立的播放方式,所以实例彼此之间无法影响,需要统一一个实例变量。

虽然背景播放的实例可以有很多个,但是我们也可以先之前那样只生成个统一的实例变量,这样就不用重复创建实例了。

  data: {..._bgm: null,},onLoad: function (options) {const bgm = wx.getBackgroundAudioManager();this.data._bgm = bgm;
...},onMusicStart() {const bgm = this.data._bgm;bgm.onPlay(()=>{console.log(123);});const music = postList[this.data._pid].music;bgm.src = music.url;bgm.title = music.title;bgm.coverImgUrl = music.coverImg;this.setData({isPlaying: true,});},onMusicStop() {const bgm = this.data._bgm;bgm.stop();this.setData({isPlaying: false,});},

当我们播放背景音频时(即产生了背景音频播放事件),就会调用 onPlay 方法。这个方法的参数是一个回调函数。我们先自己写个回调函数进去

    bgm.onPlay(()=>{console.log(123);});

所以,我们用 onPlay() 和 onPause() 监听用户在播放/暂停背景音乐时的动作。将此代码放入 onLoad() 中

  onLoad: function (options) {...bgm.onPlay(this.onMusicStart);bgm.onPause(this.onMusicStop);
...

即,onPlay() 和 onPause() 执行与 onMusicStart 和 onMusicStop 相同的动作。

9-8 音乐控制面板的暂停与停止逻辑

对微信自带的音乐播放,只有播放和暂停两个功能。但是,当 onPause() 调用时其实是调用了音乐停止 onMusicStop,而音乐面板又没有自带的 onStop()。所以需要将 onMusicStop 改为真正的暂停功能。

    const music = postList[this.data._pid].music;bgm.src = music.url;bgm.title = music.title;bgm.coverImgUrl = music.coverImg;this.setData({isPlaying: true,});},onMusicStop() {const bgm = this.data._bgm;bgm.pause();this.setData({isPlaying: false,});},

可以注意到,onMusicStart 的方法中没有 bgm.play(),其实可以有,但是为 bgm 指定 url 后小程序就会默认开启播放,无需再写了。

9-9 全局变量解决音乐播放状态初始化不正确的问题

问题:音乐播放的状态变量在每次进入文章详情页面后都会被初始化一次,而实际上,音乐播放的状态变量应该是和小程序的整个生命周期一致的(在小程序的整个生命周期内,音乐的播放状态不受后台切换和页面间切换的影响)(也就是只有重新进入小程序时,这个变量才需要初始化)

解决方法:将此变量设为全局变量

App({gIsPlayingMusic: false,})
// pages/post-detail/post-detail.jsconst app = getApp();Page({/*** 页面的初始数据*/data: {...isPlaying: false,
...},onMusicStart() {...app.gIsPlayingMusic = true;this.setData({isPlaying: true,});},onMusicStop() {...app.gIsPlayingMusic = false;this.setData({isPlaying: false,});},
...onLoad: function (options) {...this.setData({postData,collected,isPlaying: app.gIsPlayingMusic,});},
...

9-10 分析一个不是问题的问题

视频缺失,无法分析

9-11 让每篇文章音乐独立显示状态

之前解决的是一首歌显示的问题,但是没有针对特定页面,导致所有页面的音乐状态会一齐变化。那如果要为每一篇文章分别设置自己的存储态呢?

为全局新添加一个变量 app.gPlayingMusicId,记录文章 id 号,只有当文章的 id 号和播放状态同时满足时,才显示出相应图片。

App({gIsPlayingMusic: false,gPlayingMusicId: -1,})
// pages/post-detail/post-detail.jsconst app = getApp();import {postList
} from "../../data/data.js";...currentMusicIsPlaying(){if(app.gPlayingMusicId == this.data._pid && app.gIsPlayingMusic){return true;}return false;},onMusicStart() {...app.gPlayingMusicId = this.data._pid;this.setData({isPlaying: true,});},
...onLoad: function (options) {...const collected = postsCollected[this.data._pid];this.setData({postData,collected,isPlaying: this.currentMusicIsPlaying(),});},
...

10 初识小程序的自定义组件

10-1 文章列表顶部轮播图跳转

关于文章的轮播图,需要像点击目录那样能够实现跳转。我们先回顾一下先前文章目录的跳转逻辑

<view bind:tap="onGoToDetail" class="post-container" data-post-id="{{item.postId}}">
</view>
  onGoToDetail(event) {// console.log(event);const pid = event.currentTarget.dataset.postId;// 跳转到文章详情页面wx.navigateTo({url: '/pages/post-detail/post-detail?pid=' + pid,})},

实现逻辑:用户的点击事件传回一个自定义属性——文章的 id 号,函数收到这个 id 号以后,将其传给文章详情页面,由文章详情页面根据这个 id 号跳转到对应的界面。

所以,现在的逻辑实现还是基于相同的原理和功能,只不过就是接收点击事件的组件换了而已。逻辑层的代码无需变化,视图层代码修改如下

<!--=== 轮播图 ===-->
<view><swiper autoplay indicator-dots circular interval="3000"><swiper-item><image bind:tap="onGoToDetail" data-post-id="{{3}}" src="/images/bestplayers.png"></image></swiper-item><swiper-item><image bind:tap="onGoToDetail" data-post-id="{{0}}" src="/images/lpl.png"></image></swiper-item><swiper-item><image bind:tap="onGoToDetail" data-post-id="{{4}}" src="/images/jumpfly.png"></image></swiper-item></swiper>
</view>

这里的参数是常量,因为没有对应的数据库,真实开发时需要使用数据变量。

10-2 小程序tabBar选项卡配置基础

我们现在需要在页面底部加上切换按钮,以跳转到同级页面(文章→电影)。tabBar 属于小程序的一种机制而非组件,如何实现呢?

app.json 中,通过配置的方式生成。

{..."tabBar": {}
}

优点是简单,缺点是自定义性不够强。

小程序其实也提供了一种自定义的 tabBar 组件,但是难度实现起来比较大。

详细代码如下

  "tabBar": {"list": [{"text": "阅读","pagePath":"pages/posts/posts"},{"text": "电影","pagePath":"pages/movies/movies"}]}

有点绕:

tabBar 有个重要的属性 list,包含的是一个数组,数组里头的每一个子元素都是一个对象,对象的属性:

text: 按钮的名称

pagePath: 显示的页面,其表达路径的方式与 pages 一样,是相对路径。

10-3 小程序tabBar的其他配置选项

图标

        "iconPath": "/images/tab/post.png","selectedIconPath": "/images/tab/post@highlight.png"

其中,

iconPath 是未选中时的状态

selectedIconPath 是选中时显示的状态

文字

也有选中和未选中时的两种状态、

  "tabBar": {"selectedColor": "#333","color": "#999","list": [
...]}

注意编写的位置。

边框颜色、位置

  "tabBar": {"borderStyle": "white","position": "top",
..."list": [
...]}

也是,注意编写位置。

10-4 跳转到带有选项卡的页面需要使用 switchTab

现在我们再在 welcome 页面点击按钮时,页面不发生跳转,报错

Unhandled promise rejection {errMsg: "redirectTo:fail can not redirectTo a tabbar page"}

如果想从普通页面跳转到带有 tabbar 的页面,不能用 wx.redirectTo(),需要 switchTab。更改后的代码如下

  onTap: function() {// 跳转页面// wx.navigateTo({//   url: '/pages/posts/posts',// });// wx.redirectTo({//   url: '/pages/posts/posts',// });wx.switchTab({url: '/pages/posts/posts',})},

如果是想跳转到电影页面,一样,因为它们都带有 tabbar

10-5 初识自定义组件

有两个重要的学习要点:

  1. 编辑自定义组件
  2. 从服务器获取数据

最重要的是自定义组件(不考虑从服务器加载数据的封装性和代码的优雅)

老版视频:template,这是在没有自定义组件时的选择,这不是个组件,只是个模板

template 与自定义组件的区别(了解即可)

  • template 可以封装 wxss 和 wxml,但是不能封装 Js、Json,自定义组件可以
  • 对于初学者,pages 下的页面和自定义组件可以当成不同的东西,但其实它们本质上是相同的

自定义组件包含完整的页面所需的 wxml/wxss/js/json。既然如此,全部写到页面不就得了嘛,为什么要单独抽离出来?因为这样写的工作量太大,而且有些功能和代码无法复用。有了自定义组件就简洁很多,还能有利于项目与项目间的使用。

10-6 新建第一个自定义组件

将文章目录整体的组件做成一个自定义组件,初步感受自定义组件

现在根目录下新建一个 components 的文件夹,用于存储自定义组件。

在其下新建一个 post 的文件夹,右键→新建 Component,取名 index 而不是 post(约定俗成),方便使用;严格讲随便取名都行。

index.js 的初始内容

// components/post/index.js
Component({/*** 组件的属性列表*/properties: {},/*** 组件的初始数据*/data: {},/*** 组件的方法列表*/methods: {}
})

与页面有很大不同,但其实它也有生命周期函数,只是未写而已。再次说明,页面也可以做成自定义组件,所以不能把它们看成两个截然不同的东西。

10-7 创建自定义组件的属性

自定义组件的使用先前已经提到过。在 posts.json 中添加配置

{"usingComponents": {"l-icon": "/miniprogram_npm/lin-ui/icon/index","c-post": "/components/post/index"}
}

我们可以使用这个组件了

<!--pages/posts/posts.wxml--><!--=== 轮播图 ===-->
<view>
...
</view>
<!--=== 文章目录界面 ===-->
<block wx:for-items="{{postList}}" wx:key="index">
...
</block><!-- 自定义组件 -->
<c-post />

此时在文章底部的内容是

components/post/index.wxml

这是因为目前在自定义组件中,index.wxml 的内容是

<!--components/post/index.wxml-->
<text>components/post/index.wxml</text>

这样显示的是自定义组件的内容。如何为自定义组件添加属性呢?

在 Js 的属性列表中添加

// components/post/index.js
Component({/*** 组件的属性列表*/properties: {text: {type: String,value: '123'}},
...

现在可以为 text 属性赋值

<!-- 自定义组件 -->
<c-post text="hello" />

在 index.wxml 中更改内容

<!--components/post/index.wxml-->
{{text}}

此时在 posts 页面底部显示

hello

如果不指定则显示默认的 123

应该已经注意到了:在 properties 自定义的属性我们在相应页面中直接使用,没有进行数据绑定,就像在 data 中定义了的属性一样,这是自定义组件与页面的不同之处(严格的说,页面可以转换为自定义组件,所以这不是本质的不同)。

10-8 自定义组件属性的简化定义

text 属性的初始化定义可以简写为

  properties: {// text: {//   type: String,//   value: '123'// }text: String,},

这样写无法指定初始值,其显示的默认值对应该类型的默认值(String 对应空字符串,Number 对应 0,Boolean 对应 false)

10-9 自定义属性可以接收一个 Object 对象

我们现在尝试将 posts 的页面组件化。

将 posts.wxml 的部分代码移动到 index.wxml 中

<!--components/post/index.wxml--><view bind:tap="onGoToDetail" class="post-container" data-post-id="{{item.postId}}"><!-- 作者头像及时间 --><view class="post-author-date"><image class="post-author" src="{{item.avatar}}"></image><text class="post-date">{{item.date}}</text></view><!-- 文章标题 --><text class="post-title">{{item.title}}</text><!-- 文章图片 --><image class="post-image" src="{{item.imgSrc}}"></image><!-- 文章摘要 --><text class="post-content">{{item.content}}</text><!-- 图标 --><view class="post-like"><!-- <image class="post-like-image" src="/images/icon/chat.png"></image> --><l-icon color="#666" class="post-like-image" size="28" name="favor" /><text class="post-like-font">{{item.collection}}</text><!-- <image class="post-like-image" src="/images/icon/view.png"></image> --><l-icon color="#666" class="post-like-image" size="32" name="eye" /><text class="post-like-font">{{item.reading}}</text></view></view>

posts.wxss 里的对应样式也得搬过去

/* components/post/index.wxss *//* 文章目录界面 */
.post-container {/* 设置容器布局 */display: flex;flex-direction: column;/* 设置细节 */margin-top: 20rpx;margin-bottom: 40rpx;background-color: #fff;border-top: 1px solid #ededed;border-bottom: 1px solid #ededed;padding-bottom: 10rpx;
}/* 作者头像及时间 */
.post-author-date {/* 设置边距 */margin: 10rpx 0 20rpx 10rpx;/* 设置布局 */display: flex;align-items: center;
}/* 作者头像 */
.post-author {width: 60rpx;height: 60rpx;/* vertical-align: middle; */
}/* 时间戳 */
.post-date {margin-left: 20rpx;font-size: 26rpx;/* vertical-align: middle; */
}/* 文章标题 */
.post-title {font-size: 34rpx;font-weight: 600;margin-bottom: 20rpx;margin-left: 20rpx;color: #333;
}/* 文章图片 */
.post-image {width: 100%;height: 340rpx;margin-bottom: 30rpx;
}/* 文章摘要 */
.post-content {color: #666;font-size: 28rpx;margin-bottom: 20rpx;margin-left: 20rpx;/* 行距 */line-height: 40rpx;/* 字间距 */letter-spacing: 2rpx;
}/* 图标 */
.post-like {display: flex;align-items: center;margin-left: 20rpx;
}/* 收藏图标 */
.post-like-image {height: 32rpx;width: 32rpx;margin-right: 16rpx;
}/* 阅读图标 */
.post-like-font {margin-right: 20rpx;font-size: 26rpx;
}

现在要解决的是数据的问题。自定义组件的数据从何而来?

个人猜测:需要由用户通过属性或方法传入。

老师:

我们用一段代码测试一下,如何传入并使用对象数据

先在自定义组件内定义一个属性,用于接收对象

  properties: {data: Object,},

并在其 index,wxml 显示对应内容

{{data}}

然后在 posts.wxml 的自定义组件中,添加此属性

<!-- 自定义组件 -->
<c-post data="{{data}}" />

时刻注意 wxml 在引用变量时要加 {{}}

此变量的内容为

    data: {text: 123,}

显示的内容为 [Object object](Object 的初始化内容为 null,所以这个结果显示成功传入了一个对象,覆盖了初始值)

将 index.wxml 改为

{{data.text}}

则对应显示 123

10-10 分离文章到单独的自定义组件中

不建议将属性名设置为 data,容易混淆

  properties: {res: Object,},

现在将 posts.wxml 的循环中的对象传给自定义组件,并改写对应内容

<!--pages/posts/posts.wxml--><!--=== 轮播图 ===-->
<view>
...
</view><!--=== 文章目录界面 ===-->
<block wx:for-items="{{postList}}" wx:key="index"><!-- 自定义组件 --><c-post res="{{item}}" />
</block>
<!--components/post/index.wxml--><view bind:tap="onGoToDetail" class="post-container" data-post-id="{{res.postId}}"><!-- 作者头像及时间 --><view class="post-author-date"><image class="post-author" src="{{res.avatar}}"></image><text class="post-date">{{res.date}}</text></view><!-- 文章标题 --><text class="post-title">{{res.title}}</text><!-- 文章图片 --><image class="post-image" src="{{res.imgSrc}}"></image><!-- 文章摘要 --><text class="post-content">{{res.content}}</text><!-- 图标 --><view class="post-like"><!-- <image class="post-like-image" src="/images/icon/chat.png"></image> --><l-icon color="#666" class="post-like-image" size="28" name="favor" /><text class="post-like-font">{{res.collection}}</text><!-- <image class="post-like-image" src="/images/icon/view.png"></image> --><l-icon color="#666" class="post-like-image" size="32" name="eye" /><text class="post-like-font">{{res.reading}}</text></view></view>

真正在做项目时,应该提前想到这类东西可以做成个自定义组件,然后在自定义组件中编辑,而不是等代码写好以后再想着把它剥离到自定义组件之中。

10-11 自定义组件的嵌套引用

我们开始做电影页面。

我们先将注意力集中到电影首页目录的制作上来(先忽略顶部的搜索)

学会了自定义组件之后,学会用组件的方式去思考页面的构成。

在 components 下新建一个 movie-list 的组件。

自定义组件是跟 UI 相关的,制作的过程中至少需要能预览效果(就像页面那样)。

现在 movies.json 中配置自定义组件

{"usingComponents": {"c-movie-list": "/components/movie-list/index"}
}

然后在 movies.wxml 中使用这个组件

<!--movies/movies.wxml-->
<c-movie-list />

接下来就可以利用这个页面查看 movie-list 组件的显示效果了。

现在开始,先静后动。

<!--components/movie-list/index.wxml--><!-- 页面级容器 -->
<view class="container"><!-- 首栏 --><view><text>正在热映</text><text>更多</text></view><!-- 电影信息栏--></view>

考虑到电影信息栏的内容和样式是重复性的,我们也将其封装成一个组件 movie 使用。

在组件 movie-list 的配置文件中添加

{"component": true,"usingComponents": {"c-movie": "/components/movie/index"}
}

然后再在 movie-list 页面中使用 movie 组件

<!--components/movie-list/index.wxml-->
...<!-- 电影信息栏--><c-movie />
</view>

11 电影页面与实战自定义组件

11-1 Movie自定义组件的构建

总共写三个组件:

  • movie-list
  • movie(图片+电影名称+评分)
  • 评分组件

开发的顺序应该是怎样的呢?

建议从小到大:

评分组件→ movie → movie-list

其中,评分组件可以用 lin-ui,我们需要从 movie 组件开始。

<!--components/movie/index.wxml--><!-- 整体容器 -->
<view class="container"><!-- 封面 --><image class="poster" src="/images/lpl.png"></image><!-- 名称 --><text class="title">盗梦空间</text><!-- 评分 -->
</view>

封装成组件的一个好处是类名不必取得过长,也可以很容易地区分其对应内容。

/* components/movie/index.wxss *//* 整体容器 */
.container {display: flex;flex-direction: column;
}/* 封面 */
.poster {width: 200rpx;height: 270rpx;margin-bottom: 22rpx;
}

有一个问题:如果标题文字很长怎么办呢?

需要将过多的字数转为省略号。

这个有两种处理方式:

  • JS 检测长度,一旦超出此长度,超过的部分就用省略号代替
  • CSS 处理。采用网上通用的代码(这类代码很容易搜到),当然前提是容器有个固定的宽度。这个宽度可以赋给文本容器,也可以赋给整体容器。就这个案例而言,赋给整体容器更好,方便子元素间统一安排。例如,父容器 2000rpx 后,子容器的宽度 100% 即可,而无需用具体数字。
/* components/movie/index.wxss *//* 整体容器 */
.container {display: flex;flex-direction: column;width: 200rpx;
}/* 封面 */
.poster {width: 100%;height: 270rpx;margin-bottom: 22rpx;
}/* 名称 */
.title {white-space: nowrap;text-overflow: ellipsis;overflow: hidden;word-break: break-all;
}

11-2 使用 LinUI 的评分组件快速实现分数预览

movie 需要用到评分组件,所以在对应配置文件中添加

{"component": true,"usingComponents": {"l-rate": "/miniprogram_npm/lin-ui/rate/index"}
}

在页面对应位置添加

  <!-- 评分 --><l-rate />

加上对应的属性

<l-rate disabled="{{true}}" size="22" score="2.1" />

还可以更改其图片与个数。

组件默认可用点击的方式更改星星显示的状态,可以通过设置 disabled 属性使其不起作用;size 属性用于控制图标大小,score 属性用于控制评分。光有图案不够,需要在图标旁加上评分的文字

  <!-- 评分 --><view class="rate-container"><!-- 图标 --><l-rate disabled="{{true}}" size="22" score="2.1" /><!-- 文字 --><text class="score">3.0</text></view>
</view>

对应的样式为

/* 评分 */
.rate-container {margin-top: 6rpx;display: flex;align-items: baseline;
}/* 文字 */
.score {margin-left: 20rpx;font-size: 24rpx;
}

align-items 设置为基线对齐,以在视觉上达到居中的效果(图标本身画得偏下了一点),具体通过何种方式能在视觉上更好就用哪个,虽然基线对齐与居中对齐并不同。

虽然在非用 flex 布局时,文字和图标可以呈现出水平,但是随着文字的变化,它可能换行,即视觉上的水平并不是人为设定的,如果不知道它为什么这样,最好用已知的机制设置(因为有时候这样显示纯粹是种巧合,就本例而言,可能的原因是 lin-ui 的组件与 text 组件都是被内容撑开的,在总宽度小于父容器宽度时显示出居中的效果)

11-3 简易评分组件的实现思路(选看)

旧版设计评分标准的模板

<template name="starsTemplate"><view class="star-container"><view class="stars"><block wx:for="{{stars}}" wx:for-item="i"><image wx:if="{{i}}" src="/images/icon/star.png"></image><image wx:else src="/images/icon/none-star.png"></image></block></view><text class="star-score">{{score}}</text></view>
</template>

这里的 stars 的数组变量示例如下

[1,1,1,0,0]

这里的逻辑比较简单,无法处理得分为小数的情况。

开发项目需要优先选择第三方组件库以提高开发效率。

11-4 巧用Flex布局的Space-Between进行分布排列

现在就算构建出了一个 movie,需要将此作为一个组件,在 movie-list 实现三个组件的排布。现在 movie-list.wxml 中写入组件及相关的类名和容器:

<!--components/movie-list/index.wxml--><!-- 页面级容器 -->
<view class="container"><!-- 首栏 --><view><text>正在热映</text><text>更多</text></view><!-- 电影信息栏--><c-movie /><c-movie /><c-movie />
</view>

接下来要为这三个组件添加样式

/* components/movie-list/index.wxss */.container {padding: 36rpx 36rpx;
}/* 首栏 */
.title-container {display: flex;justify-content: space-between;margin-bottom: 28rpx;
}/* 电影信息栏 */
.movie-container {display: flex;justify-content: space-between;
}

11-5 调整自定义组件间距

更多右边的 “>” 符号:

在老版视频中用的是图片,当然也可以简单粗暴点,在文字中加上 > 即可。

<text class="more-text">更多 ></text>
/* 更多 */
.more-text {color: #1f4ba5;
}

现在 movie-list 就算设置好了,我们再在 movies 的页面中添加更多的 movie-list。

<!--movies/movies.wxml--><c-movie-list />
<c-movie-list />
<c-movie-list />

现在先增加点小细节:

为 movie-list 组件设置白色背景色

/* components/movie-list/index.wxss */
.container {...background-color: #fff;
}

为 movies 页面设置灰色背景

/* movies/movies.wxss */page {background-color: #f2f2f2;
}

现在,如果为 movie-list 的组件之间添加间距

<!--movies/movies.wxml--><c-movie-list />
<c-movie-list />
<c-movie-list />
/* movies/movies.wxss */c-movie-list {margin-bottom: 10rpx;
}

可以从预览效果中看到,元素选择器没有对组件产生效果;类选择器也一样。css 样式不起作用。

解决方法:使用外部样式类

11-6 外部样式类externalClasses的使用

首先来看外部样式类怎么写(建议外部样式类固定以 class 结尾,最前面可以加一个固定的字母)

我们要在 movie-list 中应用样式,需要在 movie-list 的 js 中添加

// components/movie-list/index.js
Component({externalClasses: ['f-class'],/*** 组件的属性列表*/properties: {},
...

注意代码编写的位置。

随后,在 movie-list页面中为对应的元素加上此类名

<!--components/movie-list/index.wxml--><!-- 页面级容器 -->
<view class="container f-class">
...

但是 f-class 目前只有一个名字。由谁来写它呢?哪个页面要用它,就由哪个页面来写

为 movies 页面的 movie-list 组件添加上相应的类名

<!--movies/movies.wxml--><c-movie-list f-class="movie-list" />
<c-movie-list f-class="movie-list" />
<c-movie-list f-class="movie-list" />

并在 wxss 编写对应样式

/* movies/movies.wxss */.movie-list {margin-bottom: 30rpx;
}

对于开发者,只需要在页面中指定类名时,以 f-class 作为属性名即可;对于组件开发者,需要将此属性名写在 exterClasses的数组之中,并将其赋给自定义组件中的某个组件。外部样式类所定义的样式可能会被组件内部的样式覆盖,导致无法生效,这时候我们可以在外部样式的对应样式键值对中加入 !important 即可。如

/* movies/movies.wxss */.movie-list {margin-bottom: 30rpx;background-color: #red !important;
}

11-7 小试牛刀访问服务端数据

首先我们对不同的 movie-list 的细节进行调整。

正在热映
即将上映

解决方式:针对这部分,为 movie-list 添加属性 title

// components/movie-list/index.js
properties: {title: {type: String,value: '正在热映',}},
<!--components/movie-list/index.wxml-->
...<text>{{title}}</text>
...
<!--movies/movies.wxml-->
<c-movie-list title="正在热映" f-class="movie-list" />
<c-movie-list title="即将上映" f-class="movie-list" />
<c-movie-list title="豆瓣Top250" f-class="movie-list" />

强调:小程序如果要上线,链接一定要提供 https 接口

如何从服务器中获取数据呢?使用 wx.request 接口。

wx.request 使用

基本语法

// movies/movies.js
Page({...onLoad: function (options) {wx.request({url: 'url',})},...

url 接受的是服务器 API 地址。

    wx.request({url: 'http://t.talelin.com/v2/movie/in_theaters',success(res){console.log(res);}});

打印出的数据如下

cookies: []
data: {count: 14, start: 0, subjects: Array(14), total: 26}
errMsg: "request:ok"
header: {Content-Length: "36966", Content-Type: "application/json", Date: "Tue, 11 Jan 2022 08:35:59 GMT", Server: "Caddy,gunicorn/20.0.4"}
statusCode: 200
__proto__: Object

其中 data 的内容为

count: 14
start: 0
subjects: (14) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
total: 26
__proto__: Object

cout 和 start 分别表示数据的总数与第一个数据的位置,我们所需的真正数据来源于 subjects

0: {casts: Array(3), comments_count: 642, countries: Array(1), directors: Array(1), genres: Array(2), …}
1: {casts: Array(3), comments_count: 540, countries: Array(1), directors: Array(1), genres: Array(3), …}
2: {casts: Array(3), comments_count: 508, countries: Array(1), directors: Array(1), genres: Array(2), …}
3: {casts: Array(3), comments_count: 42, countries: Array(2), directors: Array(1), genres: Array(4), …}
4: {casts: Array(3), comments_count: 1079, countries: Array(1), directors: Array(1), genres: Array(3), …}
5: {casts: Array(3), comments_count: 159, countries: Array(2), directors: Array(1), genres: Array(1), …}
6: {casts: Array(3), comments_count: 17021, countries: Array(1), directors: Array(1), genres: Array(3), …}
7: {casts: Array(3), comments_count: 156940, countries: Array(1), directors: Array(1), genres: Array(3), …}
8: {casts: Array(3), comments_count: 7500, countries: Array(1), directors: Array(1), genres: Array(2), …}
9: {casts: Array(3), comments_count: 4, countries: Array(1), directors: Array(1), genres: Array(2), …}
10: {casts: Array(3), comments_count: 21589, countries: Array(2), directors: Array(1), genres: Array(3), …}
11: {casts: Array(3), comments_count: 562, countries: Array(2), directors: Array(1), genres: Array(3), …}
12: {casts: Array(3), comments_count: 19476, countries: Array(1), directors: Array(1), genres: Array(2), …}
13: {casts: Array(3), comments_count: 6391, countries: Array(1), directors: Array(1), genres: Array(3), …}

以第 0 号数据为例

casts: (3) [{…}, {…}, {…}]
comments_count: 642
countries: ["中国大陆"]
directors: [{…}]
genres: (2) ["剧情", "爱情"]
id: 326
images: {large: "https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2519994468.jpg"}
original_title: "后来的我们"
rating: {average: 7, max: 10, min: 0, stars: "35"}
reviews_count: 119
summary: "这是一个爱情故事,关于一对异乡漂泊的年轻人。\n十年前,见清和小晓偶然地相识在归乡过年的火车上。两人怀揣着共同的梦想,一起在北京打拼,并开始了一段相聚相离的情感之路。\n十年后,见清和小晓在飞机上再次偶然重逢……\n命运似乎是一个轮回。在一次次的偶然下,平行线交叉,再平行,故事始终有“然后”。可后来的他们,学会如何去爱了吗?"
title: "后来的我们"
warning: "数据来源于网络整理,仅供学习,禁止他用。如有侵权请联系公众号:小楼昨夜又秋风。我将及时删除。"
wish_count: 26250
year: 2018

11-8 从服务器加载数据分页数据并传入自定义组件

视频缺失,自己摸索。

目标:将动态数据传给 movie-list 组件

第一步:获取数据并绑定

确定需要传入数据的 API 地址

http://t.talelin.com/v2/movie/in_theaters

我们已经通过 success 回调函数找到了所需数据,其出于回调函数接收到的对象 res 的 res.data 属性中,详细的情况如下

0: {casts: Array(3), comments_count: 642, countries: Array(1), directors: Array(1), genres: Array(2), …}
1: {casts: Array(3), comments_count: 540, countries: Array(1), directors: Array(1), genres: Array(3), …}
2: {casts: Array(3), comments_count: 508, countries: Array(1), directors: Array(1), genres: Array(2), …}
3: {casts: Array(3), comments_count: 42, countries: Array(2), directors: Array(1), genres: Array(4), …}
4: {casts: Array(3), comments_count: 1079, countries: Array(1), directors: Array(1), genres: Array(3), …}
5: {casts: Array(3), comments_count: 159, countries: Array(2), directors: Array(1), genres: Array(1), …}
6: {casts: Array(3), comments_count: 17021, countries: Array(1), directors: Array(1), genres: Array(3), …}
7: {casts: Array(3), comments_count: 156940, countries: Array(1), directors: Array(1), genres: Array(3), …}
8: {casts: Array(3), comments_count: 7500, countries: Array(1), directors: Array(1), genres: Array(2), …}
9: {casts: Array(3), comments_count: 4, countries: Array(1), directors: Array(1), genres: Array(2), …}
10: {casts: Array(3), comments_count: 21589, countries: Array(2), directors: Array(1), genres: Array(3), …}
11: {casts: Array(3), comments_count: 562, countries: Array(2), directors: Array(1), genres: Array(3), …}
12: {casts: Array(3), comments_count: 19476, countries: Array(1), directors: Array(1), genres: Array(2), …}
13: {casts: Array(3), comments_count: 6391, countries: Array(1), directors: Array(1), genres: Array(3), …}

这里返回的是数组,所以,我们需要在 data 中初始化一个变量,然后直接以 setData 的方式存储并绑定它,让页面直接使用。

// movies/movies.js
Page({/*** 页面的初始数据*/data: {inTheaters: [],},/*** 生命周期函数--监听页面加载*/onLoad: function (options) {var that = this;wx.request({url: 'http://t.talelin.com/v2/movie/in_theaters',success(res){that.setData({inTheaters: res.data.subjects,});}});},
...

对于 movies 页面的正在热映,需要的数据有:

电影图片 对应数组对象中的 images.large
电影名称 对应数组对象中的 title
电影评分 对应数组对象中的 rating.stars(需要除以10)

第二步:将绑定的数据传给 movie-list 组件并绑定动态代码

将数据传输给 movie-list

// components/movie-list/index.js
properties: {title: {type: String,value: '正在热映',},inTheaters: Array,},
<!--movies/movies.wxml-->
<c-movie-list title="正在热映" inTheaters="{{inTheaters}}" f-class="movie-list" />
<c-movie-list title="即将上映"  inTheaters="{{inTheaters}}"  f-class="movie-list" />
<c-movie-list title="豆瓣Top250"  inTheaters="{{inTheaters}}" f-class="movie-list" />

由于数组中有 14 项,而我们只在页面中显示前三项,所以通过三次循环,将前三名电影信息传入。由于循环次数较少,也可以采用复制粘贴的方式。但是微信好像没有提供指定循环次数的方式,再加上 movies 所需传输的数据量并不大,所以在传输时我们就只传输前三项即可。修改后的代码如下:

// movies/movies.jsonLoad: function (options) {var that = this;wx.request({url: 'http://t.talelin.com/v2/movie/in_theaters',data: {start: 0,count: 3},success(res) {that.setData({inTheaters: res.data.subjects,});}});},

即可以指定获取数组的数据长度。这样,movie-list 就收到了长度为 3 的数组,此时我们再用循环,重复性向 movie 组件中传输数据

// components/movie/index.js
Component({/*** 组件的属性列表*/properties: {InTheater: {},},
<!--components/movie-list/index.wxml-->
...<!-- 电影信息栏--><block><view wx:for="{{inTheaters}}" wx:key="index" class="movie-container"><c-movie InTheater="{{item}}" /></view></block>
</view>

这样写是有问题的,我们将循环的功能交给了 view,应该交给 block 的。

这样写完后发生一个问题:movie 的三个组件由设置好的水平又转为垂直排布,原因是样式作用在了每一个单独的 view 中,需要作用在循环完成后的 view 集合里,所以类名应给 block。但是 block 又不负责渲染,所以要将此样式交给 view。需要新的 view 组件。

这是错误的

<!--components/movie-list/index.wxml-->
...<!-- 电影信息栏--><block class="movie-container"><view wx:for="{{inTheaters}}" wx:key="index"><c-movie InTheater="{{item}}" /></view></block>
</view>

这是正确的

  <!-- 电影信息栏--><view class="movie-container"><block><view wx:for="{{inTheaters}}" wx:key="index"><c-movie InTheater="{{item}}" /></view></block></view>

第三步:在 movie 组件中动态化数据

此时,movie 组件中已经收到了传过来的对象,根据对象的属性,改写页面的代码

<!--components/movie/index.wxml--><!-- 整体容器 -->
<view class="container"><!--封面 --><image class="poster" src="{{inTheater.images.large}}"></image><!-- 名称 --><text class="title">{{inTheater.title}}</text><!-- 评分 --><view class="rate-container"><!-- 图标 --><l-rate disabled="{{true}}" size="22" score="{{inTheater.rating.stars/10}}" /><!-- 文字 --><text class="score">{{inTheater.rating.stars/10}}</text></view>
</view>

11-9 使用ES6箭头函数解决this指代的问题

在 success 回调函数时,在以下代码中,遇到了 setData 方法找不到的问题。原因和解决方法以前也提到过

    wx.request({url: 'http://t.talelin.com/v2/movie/in_theaters',data: {start: 0,count: 3},success(res) {console.log(res.data.subjects[0]);this.setData({inTheaters: res.data.subjects,});}});

此时报错

TypeError: Cannot read property 'setData' of undefined

老师提出了一个更简便的解决办法(先前的办法也提到了,而且用的是 const 声明)

在 Java 等语言中,this 指代的对象非常清晰,但是由于 JavaScript 是一种函数型的语言,this 的指代有时是不明确的,有时甚至是 undefined。需要检查时可以用 console.log(this) 检查打印出的内容。

    wx.request({url: 'http://t.talelin.com/v2/movie/in_theaters',data: {start: 0,count: 3},success:(res)=>{console.log(res.data.subjects[0]);this.setData({inTheaters: res.data.subjects,});}});

11-10 绑定电影数据(1)两种不同的评分方式

关于评分的数据绑定,应该是如下属性值

  <!-- 评分 --><view class="rate-container"><!-- 图标 --><l-rate disabled="{{true}}" size="22" score="{{inTheater.rating.stars/10}}" /><!-- 文字 --><text class="score">{{inTheater.rating.average}}</text></view>

rating.average 采用的是十分制,rating.stars 采用的是五十分制(虽然不知道这是为什么)

11-11 绑定服务端电影数据(2)

当前端所需数据和后端传来的并不直接对应时,如何转换呢?

{{inTheater.rating.stars/10}}

由于这个非常简单,所以借助自动类型转换即可。但是如果遇到复杂的呢?

如果一个表达式不能处理问题,还有另外两种方式

  1. wxs 只支持 ES5,不支持 ES6。缺点是写起来麻烦
  2. 将数据预先在 JS 中进行处理,然后再绑定给 wxml

11-12 完成即将上映和 top250

自己先完成一遍。

先前的代码并未针对其他两种情况单独做数据传输,使用的都是 正在热映的数据

<!--movies/movies.wxml-->
<c-movie-list title="正在热映" inTheaters="{{inTheaters}}" f-class="movie-list" />
<c-movie-list title="即将上映"  inTheaters="{{inTheaters}}"  f-class="movie-list" />
<c-movie-list title="豆瓣Top250"  inTheaters="{{inTheaters}}" f-class="movie-list" />

现在将另外两种情况,根据所需获取(也是都分别获取前三个)

  data: {inTheaters: [],comingSoon: [],top250: [],},
onLoad: function (options) {// 正在热映wx.request({url: 'http://t.talelin.com/v2/movie/in_theaters',data: {start: 0,count: 3},success:(res)=>{this.setData({inTheaters: res.data.subjects,});}});// 即将上映wx.request({url: 'http://t.talelin.com/v2/movie/coming_soon',data: {start: 0,count: 3,},success:(res)=>{this.setData({comingSoon: res.data.subjects,});},});// 豆瓣 Top250wx.request({url: 'http://t.talelin.com/v2/movie/top250',data: {start: 0,count: 3,},seccess:(res)=>{this.setData({top250: res.data.subjects,});},})},

发现 top250 的数据源与预期的不符。不熟悉这个数据,将此交给老师讲解。由于地址的重复性很高,老师将其作为全局变量存储,在这里就不再写出对应代码了,改写比较容易。

注意获取 app 全局变量的表达式写到 Page 外头。

老师讲解

先前我们将 start 和 count 的内容放在回调函数中。也可以将其放在网址中,具体是

  onLoad: function (options) {// 正在热映wx.request({url: app.movieUrl + 'in_theaters?start=0&count=3',
...// 即将上映wx.request({url: app.movieUrl + 'coming_soon?start=0&count=3',// data: {// start: 0,// count: 3,// },
...// 豆瓣 Top250wx.request({url: app.movieUrl + 'top250?start=0&count=3',
...

原来 http://t.talelin.com/v2/movie/top250 页面不存在了。关于这块的页面用 comingSoon 代替

搞错了,网页还是在的。代码已修改;是 success 写成 seccess 了/

意外发现,对于同一网址,wx.request 只会请求一次

  onLoad: function (options) {// 正在热映wx.request({url: app.movieUrl + 'in_theaters?start=0&count=3',// data: {//   start: 0,//   count: 3// },success: (res) => {this.setData({inTheaters: res.data.subjects,});},});// 即将上映wx.request({url: app.movieUrl + 'coming_soon?start=0&count=3',success: (res) => {this.setData({comingSoon: res.data.subjects,});},});// 豆瓣 Top250wx.request({url: app.movieUrl + 'coming_soon?start=0&count=3',success: (res) => {console.log(res);this.setData({top250: res.data.subjects,});},fail:(res)=>{console.log('fail');},complete: (res) => {console.log('hello');}})},

此时打印出 hello

12 电影列表与电影搜索

12-1 wx.request的更多参数讲解

目前我们只使用到了 wx.request 的一个参数

    wx.request({url: app.movieUrl + 'coming_soon?start=0&count=3',seccess: (res) => {this.setData({top250: res.data.subjects,});},

或是用到了它的 data 参数

    wx.request({url: app.movieUrl + 'coming_soon',data: {start: 0,count: 3},seccess: (res) => {this.setData({top250: res.data.subjects,});},})

应用了 data 属性后,可以在调试器→ Network → XHR 中查看有关访问信息

注意,只有当属性 method 为 GET(默认值) 时,data 属性才会生效,如果是 POST 则不会生效。

其实它还有很多其他可用的属性和方法。

微信官方文档

RequestTask | 微信开放文档 (qq.com)

12-2 更多电影(1)分析更多电影页面的逻辑

分析:一个个 movie 组件的排布

新建一个 more-movie 的页面,引入 movie 组件。

{"usingComponents": {"c-movie": "/components/movie/index"}
}

当然它也可以叫别的名字,不一定要与别的页面或组件的取名一致。

现在需要自己编写一些假的静态数据吗?不需要,这样反而更麻烦。我们可以直接加载已拥有的静态数据传给 movie。所以先静后动的原则是活的不是死的,要灵活使用。

那需要多少数据呢?即使数据很多,也不一定需要一口气就获取到全部数据,这样做很浪费性能,因为对于用户而言,界面上可视的数据不多。所以可以一次请求部分数据,随着用户向下拉,再加载更多的数据。而且服务器自身也有一些自身的限制,例如上限 30,即使开发者请求了更多。

12-3 更多电影(2)加载更多数据

分析:

  • 对于三个页面,都有更多的选项,每个板块所需请求的数据都不一样。

类似文章通过 id 号请求数据,电影页面请求数据也有对应逻辑。先前请求数据是从数组中请求文章对象,现在是从数组中获取 API 地址。

我们先部分“静”,只考虑 正在热映 的数据获取

// pages/more-movie/more-movie.jsconst app = getApp();Page({/*** 页面的初始数据*/data: {movies: [],},/*** 生命周期函数--监听页面加载*/onLoad: function (options) {// 正在热映wx.request({url: app.movieUrl + 'in_theaters',data: {start: 0,count: 12},success: (res) => {this.setData({movies: res.data.subjects,});},});},

这里将变量定义为 movies。在 movie-list 中也该将变量定义为 movies 的,代表的范围更广,而不是 inTheaters,之前没考虑到,就不改了,但是以后变量名一定要根据其可用的最大范围来,不能根据某次使用确定名字。还是改改吧。movie 中的变量也改名为 movie。

在 more-movie 页面中放置组件

    <block><view wx:for="{{movies}}" wx:key="index"><c-movie movie="{{item}}" /></view></block>

这里可以简写成

<!--pages/more-movie/more-movie.wxml--><block wx:for="{{movies}}" wx:key="index"><c-movie movie="{{item}}" />
</block>

12-4 更多电影(3)Flex-Wrap 的应用

视频缺失,自学

先回顾下 flex-wrap

3.2 flex-wrap属性

默认情况下,项目都排在一条线(又称"轴线")上。flex-wrap属性定义,如果一条轴线排不下,如何换行。

.box{flex-wrap: nowrap | wrap | wrap-reverse;
}

它可能取三个值。

(1)nowrap(默认):不换行。

(2)wrap:换行,第一行在上方。

(3)wrap-reverse:换行,第一行在下方。

也就是,需要用

flex-wrap: wrap

第一步:为组件添加 flex-wrap 属性

<!--pages/more-movie/more-movie.wxml--><view class="container"><block wx:for="{{movies}}" wx:key="index"><c-movie movie="{{item}}" /></block>
</view>
/* pages/more-movie/more-movie.wxss */.container {display: flex;flex-wrap: wrap;
}

第二步:为容器和组件设置样式

首先尝试使整个容器居中。

/* pages/more-movie/more-movie.wxss */.container {display: flex;flex-wrap: wrap;justify-content: center;
}

此时,在视觉上整体偏右。

/* pages/more-movie/more-movie.wxss */.container {display: flex;flex-wrap: wrap;justify-content: space-between;
}

老师用的 space-between。更合理

现在,为 movie 组件添加间距

<!--pages/more-movie/more-movie.wxml--><view class="container"><block wx:for="{{movies}}" wx:key="index"><c-movie class="movie" movie="{{item}}" /></block>
</view>
/* pages/more-movie/more-movie.wxss */.container {display: flex;flex-wrap: wrap;justify-content: space-between;
}.movie {padding: 30rpx 30rpx;
}

先前搞错了,为其添加内部类样式,flex 布局对普通样式类有影响,使用内部样式类反而无效。详见下节

现在的问题是,用 padding 导致一行只有两个组件(即使它们隔得非常开)。假设需要三个组件,此时,组件的总宽度为

(28+100)*3=384

而页面总宽度不过375.2。所以,不能为 movie 添加边距,而应该为容器添加边距

/* pages/more-movie/more-movie.wxss */.container {display: flex;flex-wrap: wrap;padding: 30rpx 28rpx;justify-content: space-between;
}.movie {margin-bottom: 30rpx;
}

由于使用了 space-between,组件内部的间距自保持。

12-5 更多电影(4)Flex布局对于普通样式类的影响

<!--pages/more-movie/more-movie.wxml--><view class="container"><block wx:for="{{movies}}" wx:key="index"><c-movie class="movie" movie="{{item}}" /></block>
</view>

老师猜测:

flex 布局使得普通样式类可产生效果。

flex 对布局的影响可能非常大(可能受到小程序机制的影响)

12-6 更多电影(5)加载不同类型的电影数据

自己完成

先在 movis 页面中绑定点击事件(整个组件均可实现跳转),并设置自定义属性

<!--movies/movies.wxml--><c-movie-list bind:tap="onGoToMore" data-page-id="{{0}}" title="正在热映" movies="{{inTheaters}}" f-class="movie-list" />
<c-movie-list bind:tap="onGoToMore" data-page-id="{{1}}" title="即将上映"  movies="{{comingSoon}}"  f-class="movie-list" />
<c-movie-list bind:tap="onGoToMore" data-page-id="{{2}}" title="豆瓣Top250"  movies="{{top250}}" f-class="movie-list" />
  onGoToMore(event) {const pid = event.currentTarget.dataset.pageId;wx.navigateTo({url: '/pages/more-movie/more-movie?pid=' + pid,})},

然后在 more-movie 中接收对应参数,根据参数选取对应 URL

  data: {movies: [],_url: ['in_theaters','coming_soon','coming_soon',],},onLoad: function (options) {const pageIsd = options.pid;// 正在热映wx.request({url: app.movieUrl + this.data._url[pageIsd],data: {start: 0,count: 12},success: (res) => {this.setData({movies: res.data.subjects,});},});},

老师讲解

直接将

      'in_theaters','coming_soon','coming_soon',

当作参数在页面间传递,省去数组的定义。

12-7 使用LinUI快速构建搜索栏

在 movies.json 中添加配置

{"usingComponents": {"c-movie-list": "/components/movie-list/index","l-search": "/miniprogram_npm/lin-ui/search-bar/index"}
}

添加到页面中

<!--movies/movies.wxml-->
<l-search />

为其添加属性和样式

<l-search placeholder="默认搜索内容" show-cancel="{{false}}" l-calss="ex-search" />
.ex-search {height: 90rpx !important;
}

12-8 向服务器请求搜索数据

搜索分未搜索和搜索后两种状态,前者是电影目录页面,后者是新页面。需要为搜索结果写个新页面。

首先,我们如何获取用户的输入呢?

组件提供了一个 linconfirm 事件以帮助获取用户输入。

<l-search bind:linconfirm="onConfirm" placeholder="默认搜索内容" show-cancel="{{false}}" l-calss="ex-search" />
  onConfirm(event) {console.log(event);},

用户在移动端键盘回车以后触发此事件。但是在开发者工具上如何模拟这个操作呢?

在键盘上回车即可。

打印出一个 event 对象

{type: "linconfirm", timeStamp: 290837, target: {…}, currentTarget: {…}, mark: {…}, …}changedTouches: undefinedcurrentTarget: {id: "", dataset: {…}}detail: {value: "123"}mark: {}mut: falsetarget: {id: "", dataset: {…}}timeStamp: 290837touches: undefinedtype: "linconfirm"_requireActive: undefined__proto__: Object

所需要的数值存储在了 event.detail.value 中。

下面,将此内容传给服务器

  onConfirm(event) {console.log(event);wx.request({url: app.movieUrl + 'search',data: {q: event.detail.value,},})},

默认返回给了 7 条数据。通常来讲,只要是数组类的数据,都以分页形式返回。

12-9 搜索结果与电影数据的切换显示

我们现在要根据搜索到的结果显示数据,可以写一个新页面,也可以将当前页面中的其他部分隐藏。使用条件渲染,使其在不同条件下渲染不同的数据。

<!--movies/movies.wxml--><l-search bind:linconfirm="onConfirm" placeholder="默认搜索内容" show-cancel="{{false}}" l-calss="ex-search" /><view wx:if="{{!searchResult}}">
<c-movie-list bind:tap="onGoToMore" data-page-id="{{0}}" title="正在热映" movies="{{inTheaters}}" f-class="movie-list" />
<c-movie-list bind:tap="onGoToMore" data-page-id="{{1}}" title="即将上映"  movies="{{comingSoon}}"  f-class="movie-list" />
<c-movie-list bind:tap="onGoToMore" data-page-id="{{2}}" title="豆瓣Top250"  movies="{{top250}}" f-class="movie-list" />
</view>
<view wx:else>搜索结果
</view>
  onConfirm(event) {this.setData({searchResult: true,})wx.request({url: app.movieUrl + 'search',data: {q: event.detail.value,},})},

12-10 显示搜索的电影数据

可以用 movie 做一个九宫格排布的内容即可

<view wx:else class="search-container"><block wx:for="{{searchData}}" wx:key="index"><c-movie class="movie" movie="{{item}}" /></block>
</view>
.search-container {display: flex;flex-wrap: wrap;padding: 30rpx 28rpx;justify-content: space-between;
}

记得配置(用得多的可以在全局中配置)

..."usingComponents": {"l-icon": "/miniprogram_npm/lin-ui/icon/index","c-movie": "/components/movie/index"},
...

如何返回呢?

<l-search bind:linconfirm="onConfirm" placeholder="默认搜索内容" show-cancel="{{false}}" l-calss="ex-search" />

不将 show-cancel 设置为 false ,使用 取消 按钮达到效果。

就是要找到 点击取消 的事件:

bind:lincancel

<!--movies/movies.wxml--><l-search bind:linconfirm="onConfirm" bind:lincancel="onSearchCancel" placeholder="默认搜索内容" l-calss="ex-search" />
  onSearchCancel() {this.setData({searchResult: false,})},

12-11 修复 Space-Between 2个元素两端分布的问题

当 movie 组件只有两个时,Space-Between 的布局就显得不太合理了。

解决方式:在 movie 和 more-movie 中使用伪类

.search-container::after {content: '';/* 宽度与组件宽度相同 */width: 200rpx;
}
.container::after {content: '';/* 宽度与组件宽度相同 */width: 200rpx;
}

这样只适合九宫格,如果更多呢?

参照 csdn

flex布局 justify-content:space-between; 解决最后一排数量不够自动向两端排列问题,附录grid栅格布局完美解决_lucky___star的博客-CSDN博客

13 电影详情与滑动加载数据、下拉刷新数据

13-1 上滑加载更多数据(1)onReachBottom

监听用户触底事件函数

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

修改为获取实际所需数据的函数,并绑定此数据

  onReachBottom: function () {const pageId = this.data._pageId;wx.request({url: app.movieUrl + this.data._url[pageId],data: {start: 12,count: 12},success:(res)=>{this.setData({movies: res.data.subjects,});},})},

这样做的问题是:

  • 获取的数据是定死的
  • 新数据会覆盖原有数据

对于第二个问题,我们可以用数组的 contact() 方法

this.setData({movies: this.data.movies.concat(res.data.subjects),
});

接下来解决第一个问题。

13-2 上滑加载更多数据(2)showloading提示

    wx.request({url: app.movieUrl + this.data._url[pageId],data: {start: 12,count: 12},success:(res)=>{this.setData({movies: this.data.movies.concat(res.data.subjects),});},})

start 需要变化,count 不需要变化。

可以通过 movies 数组的长度设置 start

data: {start: this.data.movies.length,count: 12
},

不知道为什么,对于第一个 正在热映 的数组而言,长度为 14,但是有时长度又变为 15(第12个数组为空,或者说,在请求数据时,在数组的第 12 个元素前加了一个空数组)。不知道为啥,老师的项目里也有这个 bug,初步猜测是服务器的问题,详细解决方案以后再找。

现在还有个小细节:目前是一次指定12个数据,如果加载得特别慢,可以提示用户“正在加载,稍等片刻”。

  onReachBottom: function () {wx.showNavigationBarLoading();...wx.request({...success:(res)=>{...wx.hideNavigationBarLoading();},})},

wx.showNavigationBarLoading 和 wx.hideNavigationBarLoading 可以配套使用,在导航条里显示/隐藏加载动态图标。

13-3 下拉刷新数据(1)

在页面中配置下拉属性

{"usingComponents": {},"enablePullDownRefresh": true
}

在 JS 中编写对应代码

  /*** 页面相关事件处理函数--监听用户下拉动作*/onPullDownRefresh: function () {const pageId = this.data._pageId;wx.request({url: app.movieUrl + this.data._url[pageId],data: {start: 0,count: 12},success: (res) => {this.setData({movies: res.data.subjects,});},});},

下拉的时间由小程序自己决定,但事实上应该由开发者决定。写在 success 函数中

      success: (res) => {this.setData({movies: res.data.subjects,});wx.stopPullDownRefresh()},

13-4 配置标题与动态配置标题

如果想要设置文章目录页面的导航条文字,应该在对应页面的配置文件中·添加

{"usingComponents": {"l-icon": "/miniprogram_npm/lin-ui/icon/index","c-post": "/components/post/index"},"navigationBarTitleText": "文与字"
}

类似的,在电影目录界面的配置文件

{"usingComponents": {"c-movie-list": "/components/movie-list/index","l-search": "/miniprogram_npm/lin-ui/search-bar/index"},"navigationBarTitleText": "光与影"
}

但是这样做比较“死”。如何动态设置这种标题呢?

小程序 API:wx.setNavigationBarTitle。建议写在 onReady() 函数中

  onReady: function () {const pageId = this.data._pageId;let title = "";switch (pageId) {case '0':title = "正在热映";break;case '1':title = "即将上映";break;case '2':title = "豆瓣 Top250";break;};wx.setNavigationBarTitle({title: title,})},

13-5 谈组件的独立性

在文章目录界面的跳转事件,有两种选择:

  • 交给组件,跳转页面
  • 交给页面,绑定给组件

事实上,对这个跳转而言,在组件中设置这样一种方法,会破坏组件的独立性,所以交给页面/开发者来做更好。

<!--=== 文章目录界面 ===-->
<block wx:for-items="{{postList}}" wx:key="index"><!-- 自定义组件 --><c-post bind:tap="onGoToDetail" data-post-id="{{item.postId}}" res="{{item}}" />
</block>

13-6 自定义组件的自定义事件产生

怎么样才能保证组件的独立性呢?

组件的开发者不应该决定组件具体要做某些事件,尤其是高度需要小程序页面开发者来决定的

编写自定义事件时,在此事件中抛出一个触发事件,用于在开发者的页面中绑定此事件。

以 posts 页面的点击为例,我们需要做的事是:

点击 post 组件中的头像,能跳转到头像对应的页面。此时就不能单纯针对组件绑定一个事件了,需要组件开发者针对此情况预先在组件中写入有关事件:

  methods: {onGoToAnthor() {this.triggerEvent('chentap');},}
<image bind:tap="onGoTo" class="post-author" src="{{res.avatar}}"></image>

其中,this.triggerEvent(‘chentap’) 用于指定开发者在使用组件时,如果需要触发对应函数所绑定的事件名称。名称建议用小写字母连着写。

小程序开发者可根据组件的此能力自由编写一个事件

  onGoToAnthor(){console.log(1213);},
<!--=== 文章目录界面 ===-->
<block wx:for-items="{{postList}}" wx:key="index"><!-- 自定义组件 --><c-post bind:chentap="onGoToAnthor" bind:tap="onGoToDetail" data-post-id="{{item.postId}}" res="{{item}}" />
</block>

此时输出 1213 并跳转页面

13-7 获取自定义组件的detail参数

自定义组件的事件获取的参数,如通过点击头像,这个会通过 bind:tap 传给 event 对象

  methods: {onGoToAnthor(event) {this.triggerEvent('chentap');},}

如何把它通过 chentap 传出去呢?还是利用 this.triggerEvent()。如下

<image bind:tap="onGoTo" data-post-id="{{res.postId}}" class="post-author" src="{{res.avatar}}"></image>
  methods: {onGoTo(event) {const pid = event.currentTarget.dataset.postId;this.triggerEvent('chentap',{pid,});},}

我们现在将 posts 页面中获取到的传给 posts.js 的 event 对象打印出来看看

  onGoToAnthor(event){console.log(event);},

得到的结果如下

{type: "chentap", timeStamp: 142883, target: {…}, currentTarget: {…}, mark: {…}, …}changedTouches: undefinedcurrentTarget: {id: "", dataset: {…}}detail:pid: 0__proto__: Objectmark: {}mut: falsetarget: {id: "", dataset: {…}}timeStamp: 142883touches: undefinedtype: "chentap"_requireActive: undefined__proto__: Object

传来的内容存储在 event.detail 中。

13-8 同时获取自定义属性和自定义组件的detail参数

先解决个小疑问:在组件中,如何在其他地方拿到属性列表里头的属性?很简单,举例如下

// components/post/index.js
Component({/*** 组件的属性列表*/properties: {res: Object,},
.../*** 组件的方法列表*/methods: {onGoTo(event) {const pid = this.properties.res.postId | event.currentTarget.dataset.postId;
...},}
})

老师的代码将 posts 修改为 pid 从组件的事件中获取,并为其重写了 onGoToDetail 函数,但是轮播图就因此不起效果了。它们之间本质上只有 pid 的获取方式不同,也没必要为此 bug 写出两个函数,只需要在获取 pid 的地方写成

const pid = event.currentTarget.dataset.postId;

13-9 电影详情页面(1)获取电影详情数据

准备开始制作电影详情页面。我们应该如何跳转到 movie-detail 页面呢?很多的地方都使用到了 movie 组件,如果在组件外部编写并绑定跳转事件更复杂,所以选择将跳转作为 movie 组件自带的方法。

    onGoToDetail(event){const mid = this.properties.movie.id;wx.navigateTo({url: '/pages/movie-detail/movie-detail?mid=' + mid,})},
<!--components/movie/index.wxml--><!-- 整体容器 -->
<view bind:tap="onGoToDetail" class="container">

在跳转时,从对应的对象中获取到了其 id 属性,现在需要将此 id 传给对应页面

  onLoad: function (options) {const mid = options.mid;wx.request({url: app.movieUrl + 'subject/' + mid,success:(res)=>{this.setData({movie: res.data,})}})},

根据 id 号即可从服务器中直接获取对应的对象,所以需要事先在服务器中合理安排好各种数据的地址。

13-10 电影详情页面(2)顶部区域

对于这个页面,总体分析一波:评分组件用的自定义组件,其他的对于这个项目没有做成组件的必要。

根据页面写出对应组件

<!--pages/movie-detail/movie-detail.wxml--><!-- 整体容器 -->
<view class="container">
<!-- 背景图片。之前写错了,尴尬 --><image class="head-img" src="{{movie.images.large}}"></image><!-- 信息 --><view class="message"><!-- 主标题 --><text class="main-title">{{movie.title}}</text><!-- 副标题 --><text class="sub-title">{{movie.countries[0] + ' · ' + movie.year}}</text><!-- 喜欢、评论 --><view class="popu"><text class="highlight-font">{{movie.wish_count}}</text><text class="plain-font">人喜欢</text><text class="highlight-font">{{movie.comments_count}}</text><text class="plain-font">评论</text></view><!-- 图 --><image class="movie-img" src="{{movie.images.large}}"></image>
</view>
</view>

有了各个组件,先尝试自己写一些样式,下节再听老师讲。

自己写样式

/* pages/movie-detail/movie-detail.wxss *//* 顶部整体容器 */
.container {display: flex;flex-direction: row-reverse;justify-content: end;margin: 28rpx 36rpx;
}/* 顶部图片 */
.head-img {width: 200rpx;height: 270rpx;
}/* 信息 */
.message {margin-right: 200rpx;display: flex;flex-direction: column;
}/* 主标题 */
.main-title {color: white;font-size: 32rpx;font-weight: bold;letter-spacing: 4rpx;margin-bottom: 20rpx;
}/* 副标题 */
.sub-title {color: white;letter-spacing: 1rpx;margin-bottom: 40rpx;
}/* 喜欢、评论 */
.popu {display: flex;align-items: baseline;
}
.highlight-font {color: red;font-weight: bold;margin-right: 8rpx;
}
.plain-font {color: gray;font-size: 20rpx;margin-right: 20rpx;
}

背景图片不知道咋写

13-11 电影详情页面(3)头部元素浮动技巧

老师的代码

.head-img {width: 100%;height: 320rpx;-webkit-filter: blur(20px);
}.message {width: 100%;height: 320rpx;position: absolute;display: flex;flex-direction: column;
}.movie-img {width: 200rpx;height: 270rpx;position: absolute;top: 160rpx;right: 30rpx;
}

13-12 电影详情页面(4)图片预览效果只做

.head-img {width: 100%;height: 320rpx;-webkit-filter: blur(20px);opacity: 50%;
}.message {width: 100%;height: 320rpx;position: absolute;top: 0rpx;display: flex;flex-direction: column;
}.main-title{font-size:38rpx;color:#fff;font-weight:bold;letter-spacing: 2px;margin-top: 50rpx;margin-left: 40rpx;
}.sub-title{font-size: 28rpx;color:#fff;margin-left: 40rpx;margin-top: 30rpx;
}.like{display:flex;flex-direction: row;margin-top: 30rpx;margin-left: 40rpx;
}.highlight-font{color: #f21146;font-size:22rpx;margin-right: 10rpx;
}.plain-font{color: #666;font-size:22rpx;margin-right: 30rpx;
}.movie-img {width: 200rpx;height: 270rpx;position: absolute;top: 160rpx;right: 30rpx;
}

与老师的代码相比,有以下两点不同:

  • 组件设置 abosolute 后,如果不指定 top 等属性,它还是定位在原位
  • 多用了 opacity 属性,因为视觉效果与老师的不同

现在完成下点击图片然后放大查看的效果。

对于这个,小程序提供了相关的 API。点击图片之后,用相册的功能去预览。

  <!-- 图 --><image bind:tap="onViewPost" class="movie-img" src="{{movie.images.large}}"></image>
  onViewPost() {wx.previewImage({urls: [this.data.movie.images.large],
})},

13-13 电影详情页(5)图片的多种 mode 模式

我们现在看到的背景图的图片,底部颜色还比较深,但是在项目效果图里却是浅的。这个样式不是由 css 写的,它在于图片的填充方式不太一样。目前我们用的还是默认的。

对于不知道宽高的网络图片,如何设置它们的高宽呢?

<image mode="" class="head-img" src="{{movie.images.large}}"></image>

控制图片填充模式的属性由 image 的 mode 属性控制。

官方文档说明

合法值 说明
scaleToFill 缩放模式,不保持纵横比缩放图片,使图片的宽高完全拉伸至填满 image 元素
aspectFit 缩放模式,保持纵横比缩放图片,使图片的长边能完全显示出来。也就是说,可以完整地将图片显示出来。
aspectFill 缩放模式,保持纵横比缩放图片,只保证图片的短边能完全显示出来。也就是说,图片通常只在水平或垂直方向是完整的,另一个方向将会发生截取。
widthFix 缩放模式,宽度不变,高度自动变化,保持原图宽高比不变
heightFix 缩放模式,高度不变,宽度自动变化,保持原图宽高比不变 2.10.3
top 裁剪模式,不缩放图片,只显示图片的顶部区域
bottom 裁剪模式,不缩放图片,只显示图片的底部区域
center 裁剪模式,不缩放图片,只显示图片的中间区域
left 裁剪模式,不缩放图片,只显示图片的左边区域
right 裁剪模式,不缩放图片,只显示图片的右边区域
top left 裁剪模式,不缩放图片,只显示图片的左上边区域
top right 裁剪模式,不缩放图片,只显示图片的右上边区域
bottom left 裁剪模式,不缩放图片,只显示图片的左下边区域
bottom right 裁剪模式,不缩放图片,只显示图片的右下边区域

默认模式是 scaleToFill

13-14 数据预处理

视频缺失,尝试通过代码自学

老师的代码

在 movie-detail.js 中,关于数据的处理代码包括

import {convertToCastString, convertToCastInfos} from '../../utils/util.js'
  processMovieData(movie){const data = {}data.directors = convertToCastString(movie.directors)data.casts = convertToCastString(movie.casts)data.image = movie.images.largedata.title = movie.titledata.subtitle = movie.countries[0]+'·'+movie.yeardata.wishCount = movie.wish_countdata.commentsCount = movie.comments_countdata.rating = movie.rating.stars/10data.average = movie.rating.averagedata.genres = movie.genres.join('、')data.summary = movie.summarydata.castsInfo = convertToCastInfos(movie.casts)this.setData({movie:data})},

所引用的 util.js 文件内容为

function convertToCastString(casts){var castsjoin = "";for (var idx in casts) {castsjoin = castsjoin + casts[idx].name + " / ";}return castsjoin.substring(0, castsjoin.length - 2);
}function convertToCastInfos(casts) {var castsArray = []for (var idx in casts) {var cast = {img: casts[idx].avatars ? casts[idx].avatars.large : "",name: casts[idx].name}castsArray.push(cast);}return castsArray;
}export {convertToCastString,convertToCastInfos
}

尝试分析

主要思路是:

从 utils.js 中接受两个函数,这两个函数用于处理数据,并将处理好的数据返回给 data 对象的属性,做好数据绑定。

这个数据处理的目的是:

  • 将原有杂乱的数据统一整理到方便获取的地方
  • 将格式不正确或不满意的数据正确格式化
  • 可以先将数据分类再传回

现在根据老师所需数据,自己尝试写一个。

先罗列出我们所需数据,以及对应的名称(根据页面加载的信息判断数据内容)和所需数据来源

data.directors   导演 movie.directorsdata.casts   影人名单 movie.castsdata.image   电影图片 movie.images.largedata.title   电影标题 movie.titledata.subtitle 电影副标题 movie.countries[0]+'·'+movie.yeardata.wishCount   喜欢人数 movie.wish_countdata.commentsCount   评论人数 movie.comments_countdata.rating   图标评分 movie.rating.stars/10data.average  文字评分 movie.rating.averagedata.genres  类型 movie.genres.join('、')data.summary   电影简介 movie.summarydata.castsInfo 影人图片及名单 movie.casts

我们需要根据自己的所需获取对应内容。

其中,data.directors、data.casts 和 data.castsInfo 需要大量代码处理,且处理方式类似,所以将其作为方法封装到 utils.js 中

对于 data.directors、data.casts,其原数据分别为

directors: Array(1)0:avatars: {large: null}name: "任鹏远"

可表示为:directors[index].name

casts: Array(3)0:avatars: {large: "https://img3.doubanio.com/view/celebrity/s_ratio_celebrity/public/p1520825133.06.jpg"}name: "任鹏远"...

可表示为:casts[index].name

对于 data.castsInfo ,需要将其图片和姓名封装到一个对象中,并用数组将它们包起来。

在根目录新建文件夹 utils,并在其下新建 utils.js。先解决 data.directors、data.casts 的封装

function wrapArrayName(array) {var names = "";for(var i = 0; i < array.length; i++){names = names + array[i].name + " ";}return names.substring(0, names.length - 2);
}

在 js 页面中导出并引入这个方法

export {wrapArrayName,
}
import {wrapArrayName} from "../../utils/utils.js";

打印出对应结果

  /*** 生命周期函数--监听页面加载*/onLoad: function (options) {const mid = options.mid;wx.request({url: app.movieUrl + 'subject/' + mid,success: (res) => {console.log(wrapArrayName(res.data.directors));// this.setData({//   movie: res.data,// })}})},

结果是 任鹏。符合预期结果。

老师将 movie 对象的赋值专门封装到一个函数中,而不是直接在 success 中调用,这样 success 中的代码更简洁且清晰些。

  onLoad: function (options) {const mid = options.mid;wx.request({url: app.movieUrl + 'subject/' + mid,success: (res) => {this.setMovie(res.data);// this.setData({//   movie: res.data,// })}})},
  setMovie(movie){console.log(wrapArrayName(movie.directors));},

作用与先前代码一致,但更易于编写和阅读,相当于在 success 中传出所需数据,然后再在 setMovie 中处理这些数据。

现在对 data.castsInfo 进行方法封装。目标:用一个对象的两个属性,分别封装两个数组,对应图片地址和人名

function wrapArrayImgAndName(array) {let newArray = [];let object = {};for(let i in array){object.img = array[i].avatars.large;object.name = array[i].name;newArray[i] = object;}return newArray;
}

同样进行导出和引入的操作。测试函数,打印出结果

(3) [{…}, {…}, {…}]0: {img: "https://img3.doubanio.com/view/celebrity/s_ratio_celebrity/public/p18662.jpg", name: "王丽坤"}1: {img: "https://img3.doubanio.com/view/celebrity/s_ratio_celebrity/public/p18662.jpg", name: "王丽坤"}2: {img: "https://img3.doubanio.com/view/celebrity/s_ratio_celebrity/public/p18662.jpg", name: "王丽坤"}length: 3nv_length: (...)__proto__: Array(0)

代码有点小问题,数组的每个元素指向的是对象的地址,导致变量内容一致。老师的解决方案是在循环中创建对象,并用数组的 push 方法加入该对象。

  var castsArray = []for (var idx in casts) {var cast = {img: casts[idx].avatars ? casts[idx].avatars.large : "",name: casts[idx].name}castsArray.push(cast);}

这是思路之一,解决了同地址的问题。如果只想用一个对象实例,可以怎么做呢?

修改后如下

function wrapArrayImgAndName(array) {let newArray = [];for(let i in array){newArray[i] = {};newArray[i].img = array[i].avatars.large;newArray[i].name = array[i].name;}return newArray;
}

与老师的大同小异,不过就是直接使数组的元素初始化为一个空对象,然后直接存储。

不过,即使有多个对象实例也没事,即使两个实例指向同一个地址,实例本身存储的也只是地址,消耗不大。

得到数据后,将数据一一交给临时变量 data,再将其绑定给 movie

  setMovie(movie){let data = {};data.directors = wrapArrayName(movie.directors);data.casts = wrapArrayName(movie.casts);data.images = movie.images.large;data.title = movie.title;data.subtitle = movie.countries[0] + ' · ' + movie.year;data.wishCount = movie.wish_count;data.commentsCount = movie.comments_count;data.rating = movie.rating.stars/10;data.average = movie.rating.average;data.genres = movie.genres.join('、');data.summary = movie.summary;data.castsInfo = wrapArrayImgAndName(movie.casts);this.setData({movie: data,})},

注意在页面进行对应的改写。

成功显示。接下来的页面数据按照所给定的使用。

  <!-- 详细信息 --><view class="summary"><!-- 标题 --><view class="original-title"><text>{{movie.title}}</text></view><!-- 评分 --><view class="flex-row"><text class="mark">评分</text><!-- 图标及评分文本 --><view class="score-container"><l-rate disabled="{{true}}" size="22" score="{{movie.rating}}" /><text class="average">{{movie.average}}</text></view></view><!-- 导演 --><view class="flex-row"><text class="mark">导演</text><text>{{movie.directors}}</text></view><!-- 影人名单 --><view class="flex-row"><text class="mark">影人</text><text>{{movie.casts}}</text></view><!-- 电影类型 --><view class="flex-row"><text class="mark">类型</text><text>{{movie.genres}}</text></view></view>

注意配置

{"usingComponents": {"l-rate": "/miniprogram_npm/lin-ui/rate/index"}
}

13-15 电影简介部分的CSS构建

接下来对新添加部分的样式进行处理

.summary {margin-left: 40rpx;margin-top: 40rpx;color: #777;
}.original-title {color: #1f3463;font-size: 24rpx;font-weight: bold;margin-bottom: 40rpx;
}.flex-row {display: flex;flex-direction: row;align-items: baseline;margin-bottom: 10rpx;
}.mark {margin-right: 30rpx;white-space:nowrap;color: #999999;
}.score-container {display: flex;flex-direction: row;align-items: baseline;
}.average {margin-left:20rpx;margin-top:4rpx;
}

添加分割线

  <!-- 分割线 --><view class="hr"></view>
.hr{margin-top:45rpx;width: 100%;height: 1px;background-color: #d9d9d9;
}

13-16 处理影人信息

完成剧情简介

  <!-- 剧情简介 --><view class="synopis"><text class="synopis-font">剧情简介</text><text class="summary-content">{{movie.summary}}</text></view><!-- 分割线 --><view class="hr"></view>
.synopis {display: flex;margin-left: 40rpx;flex-direction: column;margin-top: 50rpx;
}.synopis-font {color: #999;
}.summary-content {margin-top: 20rpx;margin-right: 40rpx;line-height: 40rpx;letter-spacing: 1px;
}

现在完成影人图片信息。

这块还是比较难的,要求当信息多的时候,可以左右滑动查看。

小程序里提供了一个比较好用的组件:scroll-view

先构建影人图片信息的容器

  <!-- 影人图片信息 --><view class="casts"></view>

13-17 多层Flex布局的嵌套应用

我们先想着取数据,而不去想它的布局样式等。所需数据为:

(3) [{…}, {…}, {…}]0: {img: "https://img3.doubanio.com/view/celebrity/s_ratio_celebrity/public/p1520825133.06.jpg", name: "任鹏远"}1: {img: "https://img1.doubanio.com/view/celebrity/s_ratio_celebrity/public/p43738.jpg", name: "徐峥"}2: {img: "https://img3.doubanio.com/view/celebrity/s_ratio_celebrity/public/p18662.jpg", name: "王丽坤"}length: 3nv_length: (...)__proto__: Array(0)

将数据传给页面使用

  <!-- 影人图片信息 --><view class="casts"><image class="image" src="{{movie.castsInfo[0].img}}"></image><text class="name">{{movie.castsInfo[0].name}}</text></view>

将其动态化循环

  <!-- 影人图片信息 --><view class="casts"><block wx:for="{{movie.castsInfo}}" wx:key="index"><image class="image" src="{{item.img}}"></image><text class="name">{{item.name}}</text></block></view>

现在需要对这些东西进行样式处理。

在这之前先为这块内容加个标题

  <!-- 影人图片信息 --><view class="casts"><text class="cast-font">影人</text><view class="casts-container"><block wx:for="{{movie.castsInfo}}" wx:key="index"><view class="cast-container"><image class="cast-image" src="{{item.img}}"></image><text class="cast-name">{{item.name}}</text></view></block></view></view>

为其添加布局样式

.casts {display: flex;flex-direction: column;
}.casts-container {display: flex;
}.cast-container {display: flex;flex-direction: column;
}

13-18 调整影人信息的CSS细节

为各个部分添加细节样式

.casts {display: flex;flex-direction: column;margin-top: 50rpx;margin-left: 40rpx;
}.cast-font {color: #999;margin-bottom: 40rpx;
}.casts-container {display: flex;margin-bottom: 50rpx;
}.cast-container {display: flex;flex-direction: column;align-items: center;margin-right: 40rpx;
}.cast-image {width: 170rpx;height: 210rpx;margin-bottom: 10rpx;
}

13-19 Scroll-View 组件的应用与结束语

scroll-view 组件的使用。用这个组件替代 view 组件

    <scroll-view scroll-x="{{true}}" class="casts-container"><block wx:for="{{movie.castsInfo}}" wx:key="index"><view class="cast-container"><image class="cast-image" src="{{item.img}}"></image><text class="cast-name">{{item.name}}</text></view></block></scroll-view>

此时,flex 布局样式失效。需要再加上 enable-flex 属性

<scroll-view enable-flex="{{true}}" scroll-x="{{true}}" class="casts-container">

此时,scroll-view 组件的高度为 9940,太高了,给组件指定个固定高度

.casts-container {display: flex;margin-bottom: 50rpx;height: 300rpx;
}

至此,课程结束。

还有个两个小问题:

第一,点击完电影详情中的海报图片时,报错

{errMsg: "previewImage:fail parameter error: parameter.urls[0] should be String instead of Undefined;"}

原因是代码

  onViewPost() {wx.previewImage({urls: [this.data.movie.images.large],})},

需要对应修改为

  onViewPost() {wx.previewImage({urls: [this.data.movie.images],})},

这个相对简单,先前忘检查变量的使用范围了。

第二个问题是,如果电影名字过长,由于浮动布局,所以即使电影标题与海报图片重合,也不会自动换行。

指定个宽度即可

.original-title {color: #1f3463;font-size: 24rpx;font-weight: bold;margin-bottom: 40rpx;width: 400rpx;
}

感谢老师。

终于整理完笔记了,感谢 Typopra 的开发者及其他能让我用上这个软件的所有人;感谢 csdn 给了一个分享笔记和心得的平台,方便记录和分享自己的心路历程与成长。(虽然都它的收费模式不敢苟同)
, names.length - 2);
}


在 js 页面中导出并引入这个方法```js
export {wrapArrayName,
}
import {wrapArrayName} from "../../utils/utils.js";

打印出对应结果

  /*** 生命周期函数--监听页面加载*/onLoad: function (options) {const mid = options.mid;wx.request({url: app.movieUrl + 'subject/' + mid,success: (res) => {console.log(wrapArrayName(res.data.directors));// this.setData({//   movie: res.data,// })}})},

结果是 任鹏。符合预期结果。

老师将 movie 对象的赋值专门封装到一个函数中,而不是直接在 success 中调用,这样 success 中的代码更简洁且清晰些。

  onLoad: function (options) {const mid = options.mid;wx.request({url: app.movieUrl + 'subject/' + mid,success: (res) => {this.setMovie(res.data);// this.setData({//   movie: res.data,// })}})},
  setMovie(movie){console.log(wrapArrayName(movie.directors));},

作用与先前代码一致,但更易于编写和阅读,相当于在 success 中传出所需数据,然后再在 setMovie 中处理这些数据。

现在对 data.castsInfo 进行方法封装。目标:用一个对象的两个属性,分别封装两个数组,对应图片地址和人名

function wrapArrayImgAndName(array) {let newArray = [];let object = {};for(let i in array){object.img = array[i].avatars.large;object.name = array[i].name;newArray[i] = object;}return newArray;
}

同样进行导出和引入的操作。测试函数,打印出结果

(3) [{…}, {…}, {…}]0: {img: "https://img3.doubanio.com/view/celebrity/s_ratio_celebrity/public/p18662.jpg", name: "王丽坤"}1: {img: "https://img3.doubanio.com/view/celebrity/s_ratio_celebrity/public/p18662.jpg", name: "王丽坤"}2: {img: "https://img3.doubanio.com/view/celebrity/s_ratio_celebrity/public/p18662.jpg", name: "王丽坤"}length: 3nv_length: (...)__proto__: Array(0)

代码有点小问题,数组的每个元素指向的是对象的地址,导致变量内容一致。老师的解决方案是在循环中创建对象,并用数组的 push 方法加入该对象。

  var castsArray = []for (var idx in casts) {var cast = {img: casts[idx].avatars ? casts[idx].avatars.large : "",name: casts[idx].name}castsArray.push(cast);}

这是思路之一,解决了同地址的问题。如果只想用一个对象实例,可以怎么做呢?

修改后如下

function wrapArrayImgAndName(array) {let newArray = [];for(let i in array){newArray[i] = {};newArray[i].img = array[i].avatars.large;newArray[i].name = array[i].name;}return newArray;
}

与老师的大同小异,不过就是直接使数组的元素初始化为一个空对象,然后直接存储。

不过,即使有多个对象实例也没事,即使两个实例指向同一个地址,实例本身存储的也只是地址,消耗不大。

得到数据后,将数据一一交给临时变量 data,再将其绑定给 movie

  setMovie(movie){let data = {};data.directors = wrapArrayName(movie.directors);data.casts = wrapArrayName(movie.casts);data.images = movie.images.large;data.title = movie.title;data.subtitle = movie.countries[0] + ' · ' + movie.year;data.wishCount = movie.wish_count;data.commentsCount = movie.comments_count;data.rating = movie.rating.stars/10;data.average = movie.rating.average;data.genres = movie.genres.join('、');data.summary = movie.summary;data.castsInfo = wrapArrayImgAndName(movie.casts);this.setData({movie: data,})},

注意在页面进行对应的改写。

成功显示。接下来的页面数据按照所给定的使用。

  <!-- 详细信息 --><view class="summary"><!-- 标题 --><view class="original-title"><text>{{movie.title}}</text></view><!-- 评分 --><view class="flex-row"><text class="mark">评分</text><!-- 图标及评分文本 --><view class="score-container"><l-rate disabled="{{true}}" size="22" score="{{movie.rating}}" /><text class="average">{{movie.average}}</text></view></view><!-- 导演 --><view class="flex-row"><text class="mark">导演</text><text>{{movie.directors}}</text></view><!-- 影人名单 --><view class="flex-row"><text class="mark">影人</text><text>{{movie.casts}}</text></view><!-- 电影类型 --><view class="flex-row"><text class="mark">类型</text><text>{{movie.genres}}</text></view></view>

注意配置

{"usingComponents": {"l-rate": "/miniprogram_npm/lin-ui/rate/index"}
}

13-15 电影简介部分的CSS构建

接下来对新添加部分的样式进行处理

.summary {margin-left: 40rpx;margin-top: 40rpx;color: #777;
}.original-title {color: #1f3463;font-size: 24rpx;font-weight: bold;margin-bottom: 40rpx;
}.flex-row {display: flex;flex-direction: row;align-items: baseline;margin-bottom: 10rpx;
}.mark {margin-right: 30rpx;white-space:nowrap;color: #999999;
}.score-container {display: flex;flex-direction: row;align-items: baseline;
}.average {margin-left:20rpx;margin-top:4rpx;
}

添加分割线

  <!-- 分割线 --><view class="hr"></view>
.hr{margin-top:45rpx;width: 100%;height: 1px;background-color: #d9d9d9;
}

13-16 处理影人信息

完成剧情简介

  <!-- 剧情简介 --><view class="synopis"><text class="synopis-font">剧情简介</text><text class="summary-content">{{movie.summary}}</text></view><!-- 分割线 --><view class="hr"></view>
.synopis {display: flex;margin-left: 40rpx;flex-direction: column;margin-top: 50rpx;
}.synopis-font {color: #999;
}.summary-content {margin-top: 20rpx;margin-right: 40rpx;line-height: 40rpx;letter-spacing: 1px;
}

现在完成影人图片信息。

这块还是比较难的,要求当信息多的时候,可以左右滑动查看。

小程序里提供了一个比较好用的组件:scroll-view

先构建影人图片信息的容器

  <!-- 影人图片信息 --><view class="casts"></view>

13-17 多层Flex布局的嵌套应用

我们先想着取数据,而不去想它的布局样式等。所需数据为:

(3) [{…}, {…}, {…}]0: {img: "https://img3.doubanio.com/view/celebrity/s_ratio_celebrity/public/p1520825133.06.jpg", name: "任鹏远"}1: {img: "https://img1.doubanio.com/view/celebrity/s_ratio_celebrity/public/p43738.jpg", name: "徐峥"}2: {img: "https://img3.doubanio.com/view/celebrity/s_ratio_celebrity/public/p18662.jpg", name: "王丽坤"}length: 3nv_length: (...)__proto__: Array(0)

将数据传给页面使用

  <!-- 影人图片信息 --><view class="casts"><image class="image" src="{{movie.castsInfo[0].img}}"></image><text class="name">{{movie.castsInfo[0].name}}</text></view>

将其动态化循环

  <!-- 影人图片信息 --><view class="casts"><block wx:for="{{movie.castsInfo}}" wx:key="index"><image class="image" src="{{item.img}}"></image><text class="name">{{item.name}}</text></block></view>

现在需要对这些东西进行样式处理。

在这之前先为这块内容加个标题

  <!-- 影人图片信息 --><view class="casts"><text class="cast-font">影人</text><view class="casts-container"><block wx:for="{{movie.castsInfo}}" wx:key="index"><view class="cast-container"><image class="cast-image" src="{{item.img}}"></image><text class="cast-name">{{item.name}}</text></view></block></view></view>

为其添加布局样式

.casts {display: flex;flex-direction: column;
}.casts-container {display: flex;
}.cast-container {display: flex;flex-direction: column;
}

13-18 调整影人信息的CSS细节

为各个部分添加细节样式

.casts {display: flex;flex-direction: column;margin-top: 50rpx;margin-left: 40rpx;
}.cast-font {color: #999;margin-bottom: 40rpx;
}.casts-container {display: flex;margin-bottom: 50rpx;
}.cast-container {display: flex;flex-direction: column;align-items: center;margin-right: 40rpx;
}.cast-image {width: 170rpx;height: 210rpx;margin-bottom: 10rpx;
}

13-19 Scroll-View 组件的应用与结束语

scroll-view 组件的使用。用这个组件替代 view 组件

    <scroll-view scroll-x="{{true}}" class="casts-container"><block wx:for="{{movie.castsInfo}}" wx:key="index"><view class="cast-container"><image class="cast-image" src="{{item.img}}"></image><text class="cast-name">{{item.name}}</text></view></block></scroll-view>

此时,flex 布局样式失效。需要再加上 enable-flex 属性

<scroll-view enable-flex="{{true}}" scroll-x="{{true}}" class="casts-container">

此时,scroll-view 组件的高度为 9940,太高了,给组件指定个固定高度

.casts-container {display: flex;margin-bottom: 50rpx;height: 300rpx;
}

至此,课程结束。

还有个两个小问题:

第一,点击完电影详情中的海报图片时,报错

{errMsg: "previewImage:fail parameter error: parameter.urls[0] should be String instead of Undefined;"}

原因是代码

  onViewPost() {wx.previewImage({urls: [this.data.movie.images.large],})},

需要对应修改为

  onViewPost() {wx.previewImage({urls: [this.data.movie.images],})},

这个相对简单,先前忘检查变量的使用范围了。

第二个问题是,如果电影名字过长,由于浮动布局,所以即使电影标题与海报图片重合,也不会自动换行。

指定个宽度即可

.original-title {color: #1f3463;font-size: 24rpx;font-weight: bold;margin-bottom: 40rpx;width: 400rpx;
}

感谢老师。

终于整理完笔记了,感谢 Typopra 的开发者及其他能让我用上这个软件的所有人;感谢 csdn 给了一个分享笔记和心得的平台,方便记录和分享自己的心路历程与成长。(虽然都它的收费模式不敢苟同)

【微信小程序】微信小程序入门与实战-个人笔记相关推荐

  1. (第39册)《微信小程序游戏开发快速入门到实战》夏敏捷著

    本书是微信小程序游戏开发的入门教程,通过大量案例介绍微信小程序游戏开发的基础知识和技巧.全书分三篇,基础篇对微信小程序的框架文件.微信小程序逻辑层和视图层.微信小程序组件进行详细介绍,包括JavaSc ...

  2. JetpackCompose从入门到实战学习笔记2——Modifier的简单使用

    JetpackCompose从入门到实战学习笔记2--Modifier的简单使用 1.Image的使用: @Composable fun Image(modifier: Modifier) {Row ...

  3. MongoDB 入门教程实战学习笔记-31-mongo 聚合查询管道 Aggregation Pipieline

    aggregation 聚合操作处理数据记录并返回计算结果. 聚合操作将多个文档中的值组合在一起, 并且可以对分组数据执行各种操作以返回单个结果. mongodb 提供了三种执行聚合的方法: 聚合管道 ...

  4. (上)小程序从0快速入门到实战项目打造个性简历,让你轻松脱颖而出吸引面试官眼球(附源码)

    前言 分享之前我们先来认识一下小程序,官方定义的微信小程序是一种新的开放能力,开发者可以快速地开发一个小程序.更是一种全新的连接用户与服务的方式,它可以在微信内被便捷地获取和传播,同时具有出色的使用体 ...

  5. 《黑马程序员2023新版黑马程序员大数据入门到实战教程,大数据开发必会的Hadoop、Hive,云平台实战项目》学习笔记总目录

    本文是对<黑马程序员新版大数据入门到实战教程>所有知识点的笔记进行总结分类. 学习视频:黑马程序员新版大数据 学习时总结的学习笔记以及思维导图会在后续更新,请敬请期待. 前言:配置三台虚拟 ...

  6. 《netty入门与实战》笔记-02:服务端启动流程

    为什么80%的码农都做不了架构师?>>>    1.服务端启动流程 这一小节,我们来学习一下如何使用 Netty 来启动一个服务端应用程序,以下是服务端启动的一个非常精简的 Demo ...

  7. 《零基础掌握 Python 入门到实战》笔记

    Python 零基础掌握 Python 入门到实战笔记 文章目录 Python 内置对象类型 基本交互语句 常用内置函数 整数与浮点数 基本数学运算 高级数学运算 字符串 序列 索引 切片 成员函数 ...

  8. FPGA入门到实战-学习笔记

    ref:腾讯教育 FPGA入门到实战-录播课-上海V3学院 https://ke.qq.com/course/66019 老师:尤恺元 第1课 掌握Verilog HDL的高级编码知识 授课日期: 老 ...

  9. 微信小程序入门与实战学习(笔记一:第一章)

    什么是微信小程序 张小龙的定义: 1.无需下载安装即可使用 2.用户"用完即走",无需关心是否安装太多应用 3.应用将无处不在,随时可用 [CP2 (人与服务) 小程序] 1.业务 ...

最新文章

  1. 补:小玩文件1-统计文本文件里的字符个数
  2. viewobject_只读ViewObject和声明性SQL模式
  3. 用JSONObject解析和处理json数据
  4. frp + nginx 配置多人共用的http 内网穿透服务
  5. Linux系统中硬盘的管理
  6. 方舟编译器的安装和编译Helloword
  7. 关于读研和生信学科的思考
  8. 制作课件需要哪些软件
  9. (疑似问题)用IDM某些版本可能会导致系统永久性开机黑屏
  10. out在matlab中,在仿真模型中添加一个输出端口模块(Out模块),能够将结果输出到MATLAB工作空间中。...
  11. 测试用例设计-颗粒度
  12. 简析H264编码中的GOP
  13. 移植u-boot到stm32f407
  14. 重磅!Waymo首席执行官离职,自动驾驶商业化打上“问号”
  15. thinkadmin搜索功能/下拉选项
  16. TIFF库的tif图片读写
  17. 【c#基础-MessageBox】MessageBox的使用和消息框
  18. python 总数 和 平方和 的计算
  19. Elasticsearch 6.4 ingest-attachment对文件IK分词器全文检索
  20. 快速了解常见的数据库关系代数

热门文章

  1. WPF--DataGrid控件使用
  2. Python 实现windwos以及ubuntu avi转换mp4
  3. 手机pdf阅读器绿色版使用说明
  4. 基于视觉反馈的步进电机X-Y平台控制
  5. leetcode 1404. 将二进制表示减到 1 的步骤数
  6. ubuntu1404 安装php5.6-fpm
  7. 计算机主机温度,电脑温度太高,如何冷却电脑?
  8. 分享一个打字练习网站
  9. 欧式几何—功能游戏介绍
  10. web服务选择lighttpd,采用fcgi组件技术扩展处理业务层