上一节中,我们完成了「🎭头像定制」小程序「简单版本」所有页面的开发工作,但在代码层面上还有一些显而易见的问题,大量重复的样式代码分散在各个页面的样式文件中、缺少统一的设计规范、随手使用了网络上的图片链接等,这是一个正在变得更加优秀的开发者所不能容忍的。
本节中我们要首先解决上述问题,对内容结构和组件样式进行优化和整合,伴随内容结构的优化,我们会见识 WXML 更为强大的一面 —— 作为模板的 WXML,并以此为基础着手进行头像选择功能的实现,最终让页面动起来。
马上开始叭 🐱🏍
本节内容开始我们的开发重点将转移到 JavaScript 中,但这里不会对 JavaScript 的基础知识进行讲解,请大家移步 JavaScript | MDN Web Docs 自行了解变量、基本类型、对象、数组、条件语句、循环语句、函数、事件等核心概念和语法。
组件样式的优化和整合
在前面的内容中,我们的四个页面是分别进行开发的,虽然我们在组织页面内容和结构、设置 class 名称、添加样式信息的时候有主动去维持各页面和组件之间的一致性,但就最终的结果来说,每个页面的样式信息仍然分散在各个页面「专属」的样式表文件中。这一状况为我们带来的麻烦主要有两点,以按钮的样式为例:
- 其一,目前我们四个页面的按钮样式看起来一致,实则是手动统一的,如果要统一修改其字号、宽高、背景色、圆角半径等,还需要分别去修改每份样式表的相关样式信息;
- 其二,未来我们势必会添加更多的页面,用到同样的按钮样式是在所难免的,为了保持应用观感的一致性,届时我们还需要为每个新页面添加同样的样式信息,而添加的方式很有可能是
Ctrl + C
&Ctrl + V
;
以上两点总结来说就是代码的「复用性」问题,我们应该努力让每一行代码都尽可能发挥它最大的价值,最直接的做法就是提高代码的复用性,Write once,Use everywhere。具体到当前的例子中,就是尽可能减少重复代码的书写,将共同的样式信息提取到一个单独的、所有页面都可以访问的样式表中, app.wxss
文件用来存放全局可用的样式信息,刚好可以满足我们的需求。或者说, app.wxss
文件存在的意义就是满足这样的需求 🤔
对于上面第一个麻烦,虽然我们也可以通过在
app.wxss
中通过定义「按钮」同名选择器的样式信息来达到覆盖页面专属样式表文件中样式信息达到统一修改的目的,但这种方式最终的结果也会是app.wxss
中拥有一份共通的样式文件,而页面专属样式表文件中对应的样式信息实质上就失效了,徒增应用体积,稍不留意还会给将来留下意料之外的样式冲突问题。既然如此,不如提前就规划好,使用全局样式表来定义通用样式信息,页面专属样式表用来定义该页面独有的样式信息,当某个页面独有的样式信息需要复用的时候,便提升至全局样式表中。「专属」并不真正意味着该样式表只能被该页面使用,其它页面的样式表仍然能够
@import
引用该样式表,从而使用其中定义的样式信息,但原则上为了应用结构的「组织性、纪律性」,我们不会这么做。
将共同的样式信息统一提取到全局样式表中还让我们能够更容易地统一设计规范,为应用打造一致的观感和操作体验。
软件设计中,确保应用各个部分获取到的同一份数据始终一致的原则叫做 Single Source of Truth,通过提取共同的全局样式信息确保设计规范一致性其实也是遵守了 Single Source of Truth 原则。
提取共同布局样式
page {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
}
如上所示,目前所有的页面中, page
的样式信息都是相同的,一方面设置 height
& width
撑满视图,另一方面设置 page 子组件的排布方式为纵向排布的 flex
,我们可以将其统一提取至 app.wxss
中。提取完成之后,小程序中所有的 page 默认都会使用上述宽高和布局,将来如果有个别页面布局与此不同的话,在该页面的样式文件中覆写 page
相关样式信息即可。
app.wxss
中可能会有创建项目时模板的遗留代码,其中唯一可能有用的是为page
添加的背景颜色,除此之外的其它样式,全部清空即可。
但是,如果有另外一种通用的页面宽高和布局呢,总不能让这些页面都去覆写叭,这样的话我们又陷入到刚刚「解决掉」的麻烦之中 😣 合理的方案应该是将这些与页面布局相关的样式信息单独提取成一个通用的 class,页面要使用什么布局,就添加什么 class。但是,page 组件是小程序框架自动添加的,开发者难以直接为 page 组件指定 class,因此我们需要对页面内容结构做一点小的调整,在 page 组件以里、页面主要内容以外添加一个 view 组件作为中间容器,专门用来处理布局的问题,和 page 相比我们对它的控制力更强!以「首页」为例,改进之后我们的页面结构和 app.wxss
中相关的样式分别如下:
<!--miniprogram/pages/index/index.wxml-->
<view class="fullview flex-column-around-center">
<view class="avatar">
<open-data type="userAvatarUrl"></open-data>
</view>
<view class="btn-group">
<view class="btn">选择当前头像</view>
<view class="btn">选择其它图片</view>
<view class="btn">拍照作为头像</view>
</view>
</view>
/* app.wxss */
page {
background-color: #F6F6F6;
}
.fullview {
height: 100vh;
width: 100vw;
}
.flex-column-around-center {
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
}
在单独提取样式类(class)的时候,类名最好不要与它在页面中发挥的功能有关,恰如其分地描述清楚它包含的样式信息即可,这样更有利于样式信息的复用!
index.wxss
中 page
的样式就可以移除啦!其它三个页面也是同理!
提取共同头像样式
在所有四个页面中都涉及到了「头像」这一内容的呈现,后三个页面由于涉及到头像的编辑流程,所以头像区域较大,占了页面中一半以上的位置,在首页中头像的作用只是为用户提供对当前头像的直接感知,故大小适当即可。
在「选择贴图」页面,头像区域还作为贴图预览的参考定位容器。
针对后三个页面,我们像提取共同的布局样式信息一样将「头像区域」作为一个 class 提取至 app.wxss
中即可,而对于首页的头像有两种处理方式:
- 使用与其它页面相同的头像区域 class,在
index.wxss
中重写必要的样式项,使其变小; - 单独为其定义一个 class,在另一个页面需要用到该 class 之前暂时放在
index.wxss
中;
考虑到首页与其它页面相同,都是对头像的呈现,而且在各页面中头像区域的位置也大体一致,这里我选择采用第一种方案,大家按照自己的喜好和判断来。改进完成之后,主要涉及到的代码如下,注释中注明了改动分别位于哪个文件中:
/* app.wxss */
.area-avatar {
width: 80vw;
height: 80vw;
position: relative;
}
.area-avatar image {
width: 100%;
height: 100%;
}
.area-avatar image.sticker-preview {
position: absolute;
right: 0;
bottom: 0;
}
/* index.wxss */
.area-avatar {
width: 30vw;
height: 30vw;
}
<!--miniprogram/pages/index/index.wxml-->
<view class="area-avatar">
<open-data type="userAvatarUrl"></open-data>
</view>
<!--miniprogram/pages/avatar-confirm/avatar-confirm.wxml-->
<view class="area-avatar">
<image src="https://tencentcloudbase.github.io/favicon.png"></image>
</view>
<!--miniprogram/pages/sticker-select/sticker-select.wxml-->
<view class="area-avatar">
<image src="https://tencentcloudbase.github.io/favicon.png"></image>
<image class="sticker-preview" src="https://tencentcloudbase.github.io/favicon.png"></image>
</view>
<!--miniprogram/pages/composition-complete/composition-complete.wxml-->
<view class="area-avatar">
<image src="https://tencentcloudbase.github.io/favicon.png"></image>
</view>
提取共同按钮样式
目前为止,我们的应用中按钮的排布方式共两种,分别是横向和纵向,纵向排布我们可以复用从页面布局中提取出来的 flex-column-around-center
样式类,横向排布再补充一个 flex-row-around-center
样式类,按钮排布的问题就解决啦。
然后是按钮尺寸的问题,主要是按钮宽度,在我们为 btn
定义的样式信息中,按钮宽度为父组件宽度的 60%(父组件宽度均为撑满页面宽度),如果按钮是纵向排布的,当然不会出任何问题,但如果按钮横向排布的话,按照我小学二年级水平的计算能力,恐怕连两个按钮都放不下 😎
然鹅,还记得 Flex 布局的特点嘛 —— 动态调整,当我们将父组件设置为 Flex 布局的时候( display: flex
),子组件可以根据页面实际的空间大小进行自适应,即自动伸缩以适应空间大小。这个特性是可控的,涉及到 flex-grow、 flex-shrink、 flex-basis 三个样式项,它们需要设置在 flex 布局中的子组件上发挥作用,分别用来调整子组件进行放大、缩小时的程度和基本尺寸。默认情况下,作为 Flex 布局中的子组件会自动缩小以适应狭小空间,但不会自动放大以占据额外空间。也就是说,当我们为页面中盛放按钮的容器设置为 flex 布局的时候,按钮原始宽度过大无法并排放置的问题已然解决啦 ~ 这就是动态布局的魅力 👏
关于 Flex 布局更详细的介绍,请查阅 Flex 布局 | MDN Web Docs 或者 弹性盒子布局教程语法篇 | 阮一峰的网络日志 和 弹性盒子布局教程实例篇 | 阮一峰的网络日志。
其实人家官方名字叫 Flexible Box Layout,弹性盒子布局。。。
我们的应用中目前有三种样式,分别是常规样式( .btn
)、次要样式( .btn-vice
)、主要样式( .btn-primary
)。实际上次要样式是不必要的,当我们有主要样式的时候,常规样式也就相当于次要样式了,而我们目前还不需要比常规样式还次要的样式,所以可以先将三种按钮样式合并为两种。最终的样式表中, .btn
作为按钮基础样式类包含了所有的按钮样式信息, .btn-primary
覆写了其中的部分样式,被覆写的这部分样式使按钮区别于常规按钮而更像是主要按钮,在使用的时候需要与 .btn
一起使用。避免后续使用的时候带来困惑,索性将为主要按钮定义样式的选择器写成 .btn.btn-primary
,准确又得当地表达了该样式类的意图和使用方式。一视同仁,我们同样会设置一个 .btn.btn-default
的样式类,将其样式信息留空即可 🤗
对咯,顺便美化一下样式!
完成之后,涉及到的代码如下,注释中注明了改动分别位于哪个文件中:
/* app.wxss */
.width-full {
width: 100%;
}
.flex-row-around-center {
display: flex;
flex-direction: row;
justify-content: space-around;
align-items: center;
}
.btn {
width: 60%;
margin: 2.5vh 5vw;
line-height: 3;
border-radius: 99999px;
text-align: center;
color: hsla(264, 0%, 10%, 1);
background-color: hsla(264, 0%, 90%, 1);
border: 2px solid hsla(264, 0%, 90%, 1);
}
.btn.btn-default {}
.btn:active, .btn.btn-default:active {
color: hsla(264, 0%, 30%, 1);
background-color: hsla(264, 0%, 80%, 1);
border: 2px solid hsla(264, 0%, 70%, 1);
}
.btn.btn-primary {
color: hsla(264, 0%, 100%, 1);
background-color: hsla(144, 60%, 50%, 1);
border: 2px solid hsla(144, 60%, 50%, 1);
}
.btn.btn-primary:active {
color: hsla(264, 0%, 90%, 1);
background-color: hsla(144, 60%, 40%, 1);
border: 2px solid hsla(144, 60%, 30%, 1);
}
<!--miniprogram/pages/index/index.wxml-->
<view class="width-full flex-column-around-center">
<view class="btn">选择当前头像</view>
<view class="btn">选择其它图片</view>
<view class="btn">拍照作为头像</view>
</view>
<!--miniprogram/pages/avatar-confirm/avatar-confirm.wxml-->
<view class="width-full flex-row-around-center">
<view class="btn">更换图片</view>
<view class="btn">选择贴图</view>
</view>
<!--miniprogram/pages/sticker-select/sticker-select.wxml-->
<view class="width-full flex-column-around-center">
<!-- scroll-view -->
<view class="btn">确认</view>
</view>
<!--miniprogram/pages/composition-complete/composition-complete.wxml-->
<view class="width-full flex-column-around-center">
<view class="btn btn-default">再做一个</view>
<view class="btn btn-primary">保存至本地</view>
</view>
作为模板的 WXML
之前我们为了满足一时的 image 组件效果预览需求,顺手嫖用了腾讯云开发的 Logo 🙃 也是时候换成自己的图片了,我准备了五个贴图,因为还没有做头像选择功能,所以还需要再找一张图片作为头像,找齐之后,在 pages 目录同级建一个 assets 目录,把这些图片都丢到 assets 目录下,完成之后目录结构示例如下:
| - miniprogram\
| - assets\
| - avatar.png
| - tcb-sticker-round-withcorner.png
| - tcb-sticker-square.png
| - tcb-sticker.png
| - thoughts-sticker-square.png
| - thoughts-sticker.png
| - pages\
| ...
五张贴图如下,使用 PPT 即可制作,大家可随意取用:
图片文件准备好之后,可以开始改造代码啦,首页中的头像使用了 open-data 开放能力组件,不需要改动,所以我们从「确认图片」页面开始。
相对路径 & 绝对路径
要让 image 组件呈现图片,需要给它的 src
属性传递一个真实有效的图片「地址」,此处「地址」的含义跟现实生活中「地址」的含义相同,都是一种描述资源位置的方式。而描述资源位置的方式,根据参考系的不同可以分为两种,类比日常生活中的问路情境,当某位朋友问我中国传媒大学在哪儿的时候,我有两种解答方式:
- 一种是回复「北京市朝阳区定福庄东街一号」,这种解答方式的背后有一个确定的参考系,在这个参考系中每一处场所、每一个资源都有具体的「坐标」,我们通常用「地址」来指代这个「坐标」;
- 另一种是回复「出门直走,路口右转直走,过天桥左转下桥,直走一段路右转就到」,这种解答方式以当前位置(或其它某个位置)为原点建立参考系,描述当前位置与目标位置的相对关系,这种描述是「路径」。
在实际的使用中,「地址」和「路径」两者一贯你中有我、我中有你,难以区分,当我们总是以同一个出发点出发描述目标资源的位置时,路径俨然就是一种地址了,而两个或两个以上的地址天然具备比较关系,也自然能够以路径的思路描述二者的相对位置了。
出于「地址」和「路径」两者之间你侬我侬的关系,它们也经常被混用,但还是有一点点的规律可循,在相对较大的范围内描述位置时,通常基于「地址」的思路进行,字面上也将「地址」与描述资源的方式等同,在相对较小的范围内描述位置时, 通常基于「路径」的思路进行,字面上也将「路径」与描述资源的方式等同。互联网是一张由无数计算机连接而成的大网,对于我们来说网络中的资源就属于相对较大的范围,所以我们习惯于将资源位置称作「网址」,而对于单个计算机中的文件资源,对我们来说属于相对较小的范围,所以我们习惯于将资源位置称作「路径」。
绝大多数教程中对两者不加厘清和界定,抓来就用,这种做法不值得提倡,作为一名正在变得更加优秀的开发者,在任何一个细节上都应该尽可能刨根问底、究其根源,我认为对这些细节的思考极有利于增进我们对技术的理解。
描述单个计算机中的文件,同样有「地址」和「路径」两种思路,地址的思路是描述所有文件的时候都以盘符开始,逐级目录下钻,直到目标文件为止,而「路径」的思路是从「问路的文件」开始,告诉它从当前文件出发如何抵达目标文件,以如下目录结构为例:
| - C 盘/
| - cigaret/
| - mobius/
| - apple.txt
| - thoughts/
| - walnut.txt
假如我们要向 walnut.txt 描述 apple.txt 的位置,「地址」的思路是告诉它 apple.txt 位于 C:/cigaret/mobius/apple.txt
,意即「C 盘下面有个名为 cigaret 的文件夹,cigaret 文件夹中有一个名为 account 的文件夹,account 文件夹中就是 apple.txt」,「路径」的思路是告诉它 apple.txt 位于 ../mobius/apple.txt
,意即「从当前位置出发,先回到上级目录,然后找一个名为 mobius 的文件夹,mobius 文件夹中就有 apple.txt」。两者的优劣非常明显,「地址」思路传达资源位置更加明确,但过于绝对和刻板,其中涉及到的任何一级目录发生变化都会导致定位失效,而「路径」思路允许资源在保持一定范围内目录结构不变的情况下整体随便移动,比较灵活,但准确定位需要「当前位置」作为「相对位置」的基准,正因为这种特点,两者通常被称作「绝对路径」和「相对路径」(更大范围内就是「绝对地址」和「相对地址」)。
根据其定位特性,绝对路径通常用于在较大范围内描述资源位置,描述确定之后为了保障「问路者」能够准确找到目标资源,资源位置一般不会再进行变动,而相对路径通常用于在较小范围内描述资源位置,描述确定之后整个「较小范围」在保持内部结构不变的情况下还可以整体进行移动。在上一个例子中,如果出于某种原因我们将 cigaret 文件夹移到了 D 盘中,绝对路径便失效了,而相对路径还能够正常工作 🤗
我们在开发项目的时候,由于开发一般是在本地计算机进行的,最终项目运行在其它机器上,两者之间的文件目录结构不一定一致,为了保证项目可以正常运作,要么将目标机器的目录结构调整至与本地开发环境完全一致,要么在项目范围内使用相对路径描述资源位置,显然后者更靠谱一点。
计算机中,相对路径和绝对路径有特定的书写规范, .
意即当前目录,..
意即上级目录,目录后面要使用 /
作为目录标识和多级之间的分隔,关键内容大致如此,更多详细的资料大家可以自行搜索了解。
具体到我们的项目中,要在 miniprogram/pages/avatar-confirm/avatar-confirm.wxml
中引用 miniprogram/assets/avatar.jpg
,从当前位置即 avatar-confirm.wxml
出发,我们需要先向上翻两级目录,即 ../../
,然后可以找到 assets/
文件夹,avatar.jpg
就躺在里面,于是我们得到的相对路径为 ../../assets/avatar.jpg
, 将它传递给 image 的 src 属性就可以正常显示图片啦。
在开发者工具中输入路径的时候,也会有自动的语法提示。
其它页面中的图片路径同理,大家自行处理,到这进一步结束,以 avatar-confirm.wxml
为例,处理前后代码片段变化如下:
<!--miniprogram/pages/avatar-confirm/avatar-confirm.wxml-->
- <!-- 处理之前 -->
- <image src="https://tencentcloudbase.github.io/favicon.png"></image>
+ <!-- 处理之后 -->
+ <image src="../../assets/avatar.jpg"></image>
image 组件充当的是「问路者」的角色,它需要的是一种能够找到目标图片资源的方式,为了保证让「问路者」能够正常到达目的地,即保证 image 组件总能够找到并稳定地显示目标图片,我们传达给它的寻找目标资源的方式也应该是稳定可靠的。使用现成的腾讯云开发 Logo 的网址显然不那么稳定,它的可访问权不在我们的手里,如果云开发团队更换了 Logo 的地址,该链接也就失效了,而更换为本地图片是获得访问控制权的一种手段,条件成熟的时候我们还会建立自己的「图床」。
数据绑定
现在我们的图片路径是「硬编码」(写死)在 WXML 中的,将来接入用户操作之后,头像的路径是动态获取的,我们如何将动态获取的头像路径即时传递给 image 组件呢?在小程序中解决这个问题会涉及到一个叫做「数据绑定」的概念。
在介绍数据绑定之前,我们先捋一捋在 Web 应用开发中是如何完成上述操作的,还是以 avatar-confirm.wxml
为例:
<!--miniprogram/pages/avatar-confirm/avatar-confirm.wxml-->
<view class="fullview flex-column-around-center">
<view class="area-avatar">
<image src="../../assets/avatar.jpg"></image>
</view>
<view class="width-full flex-row-around-center">
<view class="btn">更换图片</view>
<view class="btn">选择贴图</view>
</view>
</view>
JavaScript 主要负责的就是 Web 应用中涉及到「行为」的部分,页面中绝大多数的内容变更都属于行为,自然也就通过 JavaScript 实现。要为 image 传递一个最新的路径如 ../demo.jpg
,最基础的操作就是先找到目标 image 元素,JavaScript 寻找它的方式与 CSS 中使用的选择器一致,找到之后就可以对它进行更改,伪代码如下:
let aimImageElement = document.getElementsByTagName("image")[0]
aimImageElement.setAttribute("src", "../demo.jpg")
第一行代码意即在页面中查找标签名为 image 的元素,然后取其中的第一个,暂时用 aimImageElement
指代它,第二行代码意即将 ../demo.jpg
传递给 aimImageElement
的 src 属性,至此操作完成。语法非常明确可读对叭,开发者把要做的事情描述成步骤,计算机拿到之后就会按部就班地执行,这种与页面中的元素直接打交道的行为就叫做 「DOM 操作」。
DOM 操作在页面元素数量较少,结构简单的时候使用起来非常舒适,一旦页面内容和结构复杂起来,动辄涉及到成百上千、成千上万的元素,它的缺点就会暴露出来:
- 其一是语法啰嗦,随着页面中需要动态修改的内容不断增加,开发者会疲于教导计算机如何去找到目标元素,以及如何修改它,这些重复的语法就像妈妈的叮嘱一样,简单但吵耳朵,开发者在那一刻仿佛都活成了自己讨厌的样子;
- 其二是操作效率的问题,虽然在几百几千个元素中找到一个符合条件的目标元素对于计算机来说并不是一件非常困难的事情,但好汉架不住人多,随着寻找的元素数量和寻找频率不断增加,操作的用时也不断增加,随之而来的就是用户操作的卡顿和体验流畅程度的下滑。
对于不擅长为计算机安排任务的开发者来说,混乱且不合理的 DOM 操作很快会演变成一场噩梦。好在总有优秀的开发者在想方设法地「偷懒」,在 DOM 操作这件事情上,从结果来说,偷懒大概分为两个阶段,第一个阶段有好心人缓解了语法啰嗦的问题,他重新包装了操作 DOM 的语法,以如上操作为例,改进前后伪代码比较为:
// 改进之前
document.getElementsByTagName("image")[0].setAttribute("src", "../demo.jpg")
// 改进之后
$("image")[0].src("../demo.jpg")
显然,同样是为 image 元素传递新的图片路径这件事情,后者在表达上比前者简洁了不是一星半点,这种语法的核心代表就是 jQuery 啦。jQuery 是一个 JavaScript 库,类似于 CSS 样式库,都是将通用的一些东西包装在一起,可以即引即用,不需要自己再皱着眉头从零编写,不同之处在于大多数 CSS 样式库只是一副皮囊,而 JavaScript 库是相当于一个活生生的人,事实上你完全可以将它拟人化,没有任何违和感,我非常喜欢这样做。jQuery 作为开发者与计算机的中间人,可以听懂计算机本来听不懂的「高级话」,这些高级话是我们与它约定好的,在我们使用高级话的时候,它会转换为计算机能够听懂的啰嗦话并代为传达。
第二个阶段,Web 应用变得更加复杂,jQuery 虽然极大地缓解了原生语法啰嗦的问题,但并没有为构建大型应用提供显而易见的帮助,开发者们不仅要应对日益庞大的业务逻辑架构带来的挑战,还要留意合理安排 DOM 操作,避免影响到应用流畅程度。在这种背景下,又有好心人站出来提供了一个更加强大的中间人,它可以完全免除手动操作 DOM 的烦恼,从而让开发者专注于业务逻辑的处理,其核心思想是单独维护一份虚拟的页面结构,这份虚拟页面结构与真实的页面结构相对应,中间人会始终保证二者的一致性,开发者只要将准备更新的内容告诉中间人,它就会以尽可能高效的方式更新到页面中。这种中间人包含在近年来我们使用的各种「框架」中,包括但不限于 Angular、Vue、React 等,以及我们正在交涉的「小程序框架」。
框架更像是一个组织,其中包含着各种各样的中间人,各司其职。
在实际使用中,并不是页面中所有的内容都有动态更新的需求,为了减轻中间人的工作量,让它能够更加专注而精准地工作,我们需要协助它标识出页面中的关键内容,让它特别留意。标识关键内容的时候使用的就是「模板语法」,模板语法在「标记语言」(HTML 就是一种标记语言,全称 HyperText Markup Language)之上进行了扩展,允许开发者使用特定的标识符「额外标注」页面中涉及到动态处理的部分。在应用运行的时候,中间人会解读模板,基于模板建立虚拟页面结构,模板中被标注的部分会得到额外关照,中间人会将它们记在自己的小本本上。除了解读模板之外,中间人还会带着小本本去 JavaScript 中特定的地方寻找开发者为模板准备的填充物,填充物的标记与模板中的标记一一对应,中间人会将它们匹配起来,渲染出真实的页面呈现给用户。之后,在应用运行过程中,开发者只要按照特定的方式提醒中间人「填充物有更新」,它就会自动处理页面的更新工作。
<view>{{用户名字}}</view>
+ + + + + + + + + + + +
{
用户名字:'cigaret'
}
|| || || || || || || ||
<view>cigaret</view>
在小程序中,小程序框架接手 DOM 操作的能力,就是引入了完成上述工作的中间人。WXML 全称 WeiXin Markup Language,它本身就是扩展了模板语法的标记语言。 .wxml
文件其实就是页面内容的模板文件,在应用运行的时候小程序框架会根据这个模板、结合我们给出的填充物,构建出真实完整的页面呈现给用户,同时默默地维护着虚拟页面结构和真实页面内容的一致性。其中将 JavaScript 中的特定数据字段(填充物)与页面中某一特定内容(正常文本、属性值等)保持一致的特性就叫做「数据绑定」,具体使用如下:
<!--miniprogram/pages/avatar-confirm/avatar-confirm.wxml-->
<view class="area-avatar">
<image src="{{avatarUrl}}"></image>
</view>
// miniprogram/pages/avatar-confirm/avatar-confirm.js
Page({
data: {
avatarUrl: "../../assets/avatar.jpg"
},
...
})
在 WXML 中,使用双花括号—— {{}}
作为数据绑定的标识符,双括号中的内容描述了该处内容与 JavaScript 中相关数据字段的关系,页面模板的额外标注中涉及到的变量与相应页面 JavaScript 文件中特定位置的同名数据字段之间会建立绑定关系。
在上述例子中,avatar-confirm.wxml
里将 image 的 src 属性值使用变量名 avatarUrl
标识为动态内容,它会与 avatar-confirm.js
中传递给 Page()
的页面对象的 data 属性下的同名数据字段 avatarUrl
之间建立对应关系,之后我们只要使用特定的方式告诉小程序框架更新 avatarUrl
的值,页面中 image 的 src 属性就会自动被更新。
以上内容中我尽可能将数据绑定的核心思想和原理较为通俗地呈现给大家,对于没有任何开发经验的同学来说,可能会有点生涩,但没有关系,我们的主线依然是以使用为主,在这一方面也许官方文档的介绍更加友好一点,大家可以查阅:框架 - WXML 语法参考 - 数据绑定 | 微信开放文档。
动态渲染
除了页面内容以外,页面结构也会有动态变更的需求,一般来说有两个场景:
- 其一是条件渲染,即让某个组件在符合特定条件的时候显示,不符合条件的时候隐藏,并且可以在显示与隐藏之间切换;
- 其二是列表渲染,即向页面列表中动态追加或删除元素;
在我们的小程序中,以上两个场景都存在于「选择贴图」页面中,以下是当前 sticker-select.wxml
的代码,其中预览用的贴图刚进入页面的时候应该是不显示的,只有在用户选中一个贴图之后才会出现,这个属于条件渲染,而选择贴图区域的贴图,目前是硬编码在页面中的,贴图的数量不可更改,如果要修改就必须手动更改 WXML 代码,将来我们的贴图也是动态获取的,如何把获取到的所有图片一个不多一个不少地使用 image 组件呈现出来并支持动态变更就属于列表渲染。
<!--miniprogram/pages/sticker-select/sticker-select.wxml-->
<view class="fullview flex-column-around-center">
<view class="area-avatar">
<image src="{{avatarUrl}}"></image>
<image class="sticker-preview" src="{{stickerSelected}}"></image>
</view>
<view class="width-full flex-column-around-center">
<scroll-view scroll-x="true">
<image src="https://tencentcloudbase.github.io/favicon.png"></image>
<image src="https://tencentcloudbase.github.io/favicon.png"></image>
<image src="https://tencentcloudbase.github.io/favicon.png"></image>
<image src="https://tencentcloudbase.github.io/favicon.png"></image>
<image src="https://tencentcloudbase.github.io/favicon.png"></image>
</scroll-view>
<view class="btn">确认</view>
</view>
</view>
相较传统的 Web 应用开发方式中,我们要么通过手动控制页面元素的增删来实现,要么通过更改目标元素的特定样式来实现,都属于直接操作 DOM,颇为麻烦。而 WXML 只是作为模板,在应用运行过程中还会再经过一遍处理才会变为真实的页面内容,得益于这个「二次处理」环节的存在,我们也可以将「条件渲染」和「列表渲染」的能力直接在模板中实现。
Talk is cheap. Show me the code.
添加条件渲染之后代码为:
- <!-- 改动之前 -->
- <image class="sticker-preview" src="{{stickerSelected}}"></image>
+ <!-- 改动之后 -->
+ <image wx:if="{{stickerSelected}}" class="sticker-preview" src="{{stickerSelected}}"></image>
// miniprogram/pages/avatar-confirm/avatar-confirm.js
Page({
data: {
avatarUrl: "../../assets/avatar.jpg",
stickerSelected: ""
},
...
})
添加列表渲染之后代码为:
- <!-- 改动之前 -->
- <scroll-view scroll-x="true">
- <image src="https://tencentcloudbase.github.io/favicon.png"></image>
- <image src="https://tencentcloudbase.github.io/favicon.png"></image>
- <image src="https://tencentcloudbase.github.io/favicon.png"></image>
- <image src="https://tencentcloudbase.github.io/favicon.png"></image>
- <image src="https://tencentcloudbase.github.io/favicon.png"></image>
- </scroll-view>
+ <!-- 改动之后 -->
+ <scroll-view scroll-x="true">
+ <image wx:for="{{stickers}}" src="{{item}}"></image>
+ </scroll-view>
// miniprogram/pages/avatar-confirm/avatar-confirm.js
Page({
data: {
avatarUrl: "../../assets/avatar.jpg",
stickerSelected: "",
stickers: [
"../../assets/tcb-sticker-round-withcorner.png",
"../../assets/tcb-sticker-square.png",
"../../assets/tcb-sticker.png",
"../../assets/thoughts-sticker-square.png",
"../../assets/thoughts-sticker.png"
]
},
...
})
关于动态渲染的官方介绍,移步:框架 - WXML 语法参考 - 列表渲染 | 微信开放文档、框架 - WXML 语法参考 - 条件渲染 | 微信开放文档。
WXML 还支持模板的模板——「Template」,详细介绍大家可以查阅 框架 - WXML 语法参考 - 模板 | 微信开放文档 😎 之后遇到合适的场景我们也会进行介绍。
初步实现头像选择功能
到此为止,对页面内容和结构的优化和改造工作就告一段落啦 ~ 我们的代码相当清爽利落,但小程序目前还是「中看不中用」,接下来一起为它注入活力吧!到本节结束的时候,我们将实现「头像选择」的功能。
生命周期 & 事件
相传孔明生平好给人派发锦囊,蜀吴联姻的时候,他曾塞给子龙三个锦囊,分别让他在「进入吴地」、「备哥哥乐不思蜀」、「离开吴地遭强留」的时候打开,按照锦囊中的吩咐办事。每个小程序在被用户打开到关闭的整个过程中,大到小程序应用本身,中到每个页面,小到所有组件,在完成它们各自功能的时候,都会有不同的时间节点,这些时间节点被形象地称作「生命周期」,应用层面生命周期各阶段包括应用启动、应用切换到后台、应用运行出错等,页面生命周期各阶段包括页面加载、页面显示、页面渲染完成、页面隐藏、页面卸载等,组件生命周期各阶段包括组件被创建、组件被插入到页面、组件被移动、组件被移除等,在开发的时候,我们每个人都是孔明,可以在应用、页面、组件的各个运行阶段安排任务,应用运行至特定阶段的时候,我们安排的任务就会被执行。除了生命周期这类可以明确预测的时间节点之外,应用在运行过程中还会接受用户的操作,几乎用户所有的操作都会作用于页面中的特定组件,包括点击、滑动、长按、输入等,这些操作的发生在开发中叫做「事件」,我们同样可以为程序指定当这些事件发生的时候需要执行的任务。
关于生命周期的更多介绍,请查阅:框架 - 框架接口 - App | 微信开放文档、框架 - 框架接口 - Page | 微信开放文档、框架 - 框架接口 - Component | 微信开放文档、指南 - 小程序框架 - 页面生命周期 | 微信开放文档。
只有自定义组件的生命周期允许开发者插手,关于事件的更多介绍,请查阅:指南 - 小程序框架 - 事件系统 | 微信开放文档 和各个组件相应的文档。
回到「首页」,在这个页面中有三个按钮,用户在点击的时候分别可以完成相应的三个操作。在模板语法的加持下,为应用程序安排在特定事件发生的时候需要执行的任务非常简单,示例如下:
<!--miniprogram/pages/index/index.wxml-->
<view class="btn" bindtap="chooseCurrentAvatar">选择当前头像</view>
Page({
data: {
...
},
chooseCurrentAvatar() {
// 此处定义获取当前头像的逻辑
},
...
})
上述代码中,开始标签中的 bindtap="chooseCurrentAvatar"
属于「事件绑定」语法,「tap」是小程序中「点击事件」的名称,当这个 view 组件被点击的时候,程序就会执行在 JavaScript 页面配置对象中预定义的同名「方法」, chooseCurrentAvatar() {}
是在对象中定义方法的简便方式。
对象中的函数属性通常称作「方法」。
每个页面「专属」的 JavaScript 文件中,都需要执行一个
Page()
函数,通过这个函数,小程序框架将构造出一个页面,我们称其为「页面实例」。在页面构建的时候,Page()
需要接受一个「对象」,这是一个供开发者配置页面实例用的对象,故称其为「页面配置对象」,其作用类似于app.json
,我们为页面模板准备的「填充物」和各种「锦囊」,都放在这个地方。
接口 & 回调
在 chooseCurrentAvatar() {}
这个方法中,我们要完善点击事件发生后需要执行的任务逻辑,对于「使用当前头像」这个按钮来说,首先是「获取用户头像」,然后通过某种方式将头像的地址传递给「确认图片」页面。
我们开发小程序的时候,是在「小程序框架」这个地基之上进行的,应用中需要用到的诸如与手机系统交互、与微信客户端交互、与小程序框架本身交互等功能都由「地基」提供,提供的方式叫做「接口」,全称 Application Programming Interface,简称 API。体现在具体的微信小程序开发中,接口就是挂载在 wx
这个全局对象上的方法,开发者可以通过 wx.方法名
来调用,API 被调用之后框架就会去执行对应的逻辑,如果有返回结果的话,就将执行的结果返回给开发者,开发者可以定义拿到这些结果之后要做的事情,承载拿到结果之后要做的事情的函数叫做「回调函数」(Callback),接下来我们通过实际使用来详细了解。
小程序框架提供了 wx.getUserInfo
这个接口,通过它我们就能够拿到用户头像、昵称、性别等非敏感用户信息。不过,在获取用户信息之前,需要征得用户授权,在很久很久之前,开发者直接调用这个接口的时候框架会自动判断当前用户是否已经有过授权,如果没有,会弹出授权弹窗提醒用户授权,用户确认授权之后框架会将已授权的信息存在后台,然后再正常完成获取用户信息的逻辑,将操作的结果反馈给开发者,如果用户之前已经完成授权的话会正常执行获取信息的逻辑,同样将操作的结果反馈给开发者。但在 2018 年 4 月 30 日之后,为了提升用户体验,官方调整了授权弹窗的逻辑,在用户没有授权的情况下直接调用 wx.getUserInfo
不会再自动弹出授权弹窗,而是默认调用失败,而自动弹出授权弹窗的功能作为开放能力被封装在 button 组件中,也就是说,我们现在必须使用 button 按钮来引导用户完成授权。
<!--miniprogram/pages/index/index.wxml-->
<button open-type="getUserInfo" bindgetuserinfo="chooseCurrentAvatar" class="btn" style="width: 60%; margin: 2.5vh 5vw; padding: 0; font-size: inherit; font-weight: inherit; ">选择当前头像</button>
// miniprogram/pages/index/index.js
Page({
chooseCurrentAvatar() {
wx.getSetting({
success(res) {
if (res.authSetting['scope.userInfo']) {
wx.getUserInfo({
success: function (res) {
const avatarUrl = res.userInfo.avatarUrl;
// 此处定义拿到用户头像地址之后的逻辑
}
})
} else {
wx.showToast({
title: '授权访问之后才可以获取当前头像',
icon: 'none'
})
}
}
})
},
...
})
上述代码中,我们将原先的 view 组件更换为 button 组件,按钮类型设置为 getUserInfo
开放能力,并为它指定了 chooseCurrentAvatar
作为获取到用户信息之后的「锦囊妙计」。目前,请求用户授权获取个人信息这个按钮只会在这里出现一次,所以我们使用内联样式与 btn
样式类结合将它装饰得跟正常按钮无异。 虽然在chooseCurrentAvatar
中我们可以通过事件相关的参数直接拿到用户信息,但为了保持整个应用范围内获取用户信息方式的一致性,这里仍然选择使用 wx.getUserInfo
接口来获取。同时为了兼容用户拒绝授权的情况,在调用 wx.getUserInfo
接口之前,还使用 wx.getSetting
做一遍用户确实进行了授权的核查,只有确认用户通过了授权才会去获取信息,否则弹出消息提示框提醒用户进行授权。
上面涉及到的 API 包括:API - 开放接口 - 用户信息 | 微信开放文档、API - 开放接口 - 设置 | 微信开放文档、API - 界面 - 交互 | 微信开放文档。
关于 API 的介绍,大家可以查阅:指南 - 小程序框架 - API | 微信开放文档、API | 微信开放文档。
页面通信
拿到头像地址之后,我们需要将它传递到「确认图片」页面,数据的传递通常叫做「通信」。
页面之间进行通信有两种方式,其一是直接传递,即在页面跳转的时候将数据镶嵌在页面路径中传递给目标页面,目标页面可以在 onLoad
生命周期处理函数中拿到位于路径中的数据,另外一种是间接传递,找个中间人代为处理通信或者找个公共的地方存起来。小程序框架为开发者提供了专门用来处理页面间通信的中间人,但这个中间人有点死板,只有在参与通信的页面双方都存活的情况下才会工作,在我们目前的情境中,在页面跳转之前目标页面还没出生,所以这个中间人使用起来有点麻烦,暂不考虑。由于「确认图片」页面和「选择贴图」页面之间也存在头像地址的传递关系,所以直接传递也不是特别方便,我们自然就选择使用公共位置存取的方案啦。
直接传递如何使用请查阅:API - 路由 - navigateTo | 微信开放文档、框架 - 框架接口 - Page - onLoad | 微信开放文档。
小程序框架提供的通信中间人请查阅:API - 路由 - EventChannel | 微信开放文档。
类似于 app.wxss
用于放置应用中所有页面都能够访问的样式信息, app.js
中也可以放置所有页面都可以访问的数据。在应用的任何一个角落,都可以调用 getApp()
获取到 App 实例,我们通常将公共的数据写在 App 实例的 globalData
对象属性中,读取公共数据也是同理。
// miniprogram/pages/index/index.js
const appInstance = getApp()
Page({
chooseCurrentAvatar() {
wx.getSetting({
success(res) {
if (res.authSetting['scope.userInfo']) {
wx.getUserInfo({
success: function (res) {
const avatarUrl = res.userInfo.avatarUrl;
appInstance.globalData['avatar'] = avatarUrl;
wx.navigateTo({
url: '../avatar-confirm/avatar-confirm',
})
}
})
} else {
wx.showToast({
title: '授权访问之后才可以获取当前头像',
icon: 'none'
})
}
}
})
},
...
})
上述代码中,我们将获取到的头像地址存在 App 实例 globalData
对象属性的 avatar
字段中,然后使用 wx.navigateTo
方法进行页面的跳转。
在「确认图片」页面中,我们让程序在页面完成加载( onLoad
生命周期)之后去 App 实例中拿取头像地址,然后通知小程序框架更新页面中的 image 组件。
// miniprogram/pages/avatar-confirm/avatar-confirm.js
const appInstance = getApp()
Page({
/**
* 页面的初始数据
*/
data: {
avatarUrl: "../../assets/avatar.jpg"
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
this.setData({
avatarUrl: appInstance.globalData.avatar
})
},
...
})
上面的代码中,我们让页面实例在加载完成之后去 App 实例中取得用户选择的头像地址,通过 setData
方法通知小程序框架更新「视图」。
小程序框架中也存在分工,其中主要负责页面呈现的部分称作「视图层」,主要负责应用逻辑的部分称作「逻辑层」。
以上内容完成之后,我们的小程序运行如下:
小结
本节内容中,我们做了很多事情,emmm,好像确实非常多,语雀编辑器它都卡了。
首先我们对上节内容中页面初步实现的代码进行了调整和优化,其中涉及到提高代码复用性的技巧和考虑,顺便还介绍了 Flex 动态布局的特点。我们一起了解了「地址」和「路径」两种资源定位思路的异同,并将对在线图片资源的引用切换为本地图片资源。随后引入了 WXML 的模板语法特性,同时也为大家梳理了 DOM 操作的前世今生,结合数据绑定和动态渲染的特性,我们对页面中的动态内容进行了提取,为下一步实现头像选择功能做足准备。最后当然是我们一起实现了部分头像选择功能,期间学习了生命周期和事件、接口与回调、页面通信等概念。
即使本节内容如此充实,我们的小程序也只是刚刚能动而已,到正式使用也许还需要这么干的一篇内容,大家备好小板凳期待叭 🤗 在下节开始之前,好好回顾和熟悉我们目前为止涉及到的诸多概念哦!
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.