微信小程序的更新迭代非常频繁,几乎每个月都会有新版本发布,这就会让初学者感觉到学习的压力和难度。其实,我们小程序的每次版本迭代都是在现有小程序架构基础之上进行更新的,如果想要学好小程序开发技术,打牢基础是必不可少的学习环节。本章就对小程序的基础架构进行详细地讲解。熟练掌握本章的小程序框架基础知识,对后面学习小程序开发至关重要。
小程序开发与传统的前端开发有着很大的区别,不管什么类型的前端技术,都是由以下三种技术组成:
- 静态标签文件(HTML),静态标签决定了前端页面的基本骨架是如何构成的;
- 样式文件(CSS),样式文件可以让前端的页面凸显自身的美术风格;
- 动态脚本文件(Javascript),动态脚本可以让前端页面与用户进行交互。
静态标签组成了前端的骨架,让渲染工具明白前端是由哪些标签组成的。但是原始的静态标签是没有样式的,并不具备很强的观赏性,如果想要让前端产品更加美观,并且具有独特的美术风格,那么就需要使用到样式文件。样式文件主要由不同类型的选择器组成,开发人员可以将样式通过不同范围的选择器添加到页面的UI组件上。如果只是静态标签和样式是无法让一个前端页面动起来的,例如用户点击页面中的一个按钮,需要弹出一个提示框,这就需要使用到动态脚本,动态脚本决定了前端页面如何与用户进行交互,例如弹窗的时机、广告图片的滚动速度,以及向后端请求数据等,这些都是由动态脚本来实现的。
小程序虽然与传统的前端开发有所区别,但是也脱离不了前端的固定模式。小程序拥有四种文件类型,分别是:
- wxml文件,类似于传统前端的HTML文件,用于静态标签的编写;
- wxss文件,与传统前端的CSS文件功能类似,用于页面样式的编写;
- js文件,与传统前端的Javascript脚本功能类似,用于页面交互逻辑的编写;
- json文件,在传统前端页面开发中没有json文件,小程序的json文件主要用于页面配置,如页面标题、颜色、样式的配置等。
新建一个小程序就会默认创建index和logs模块,每个模块都以单独的文件夹形式保存。页面文件在微信开发者工具中的效果如图1所示。 图1 首页下的四种文件
除了页面文件对应的模块文件夹之外,小程序还支持将一些工具型js独立保存,通过导入的方式为模块提供功能支持,例如新建小程序中自动创建的utils,效果如图2所示。 图2 utils模块
所有的全局文件都以app命名开头,全局文件内部声明的资源可以作用到所有模块中,效果如图3所示。 图3 小程序应用的全局文件
在过去,开发人员所积累的前端开发经验其实有很大一部分可以继续应用在小程序的开发上,例如小程序和普通网页都需要书写静态标签页面。小程序的样式和普通网页基本相同,而且小程序和普通网页都遵循了Javascript的ES6标准,很多语法在两个平台都可以一起使用,例如模块的导入导出、箭头函数等。
但是小程序和传统网页开发毕竟还是两种不同的技术,二者之间还是有些许的区别。在普通网页中渲染线程和脚本线程是互斥的,而在小程序中二者不是互斥的。普通网页可以操作DOM和BOM对象,但是小程序的逻辑层运行在JSCore中,无法操作DOM和BOM对象,所以小程序在使用JS选择UI时,就没有父节点、子节点、ID选择器这些概念了。网页开发者需要面对的环境是各式各样的浏览器,在PC端需要面对IE、Chrome、QQ浏览器等,在移动端也需要面对各个系统中的WebView,而小程序开发过程中主要面对的是IOS和Android的微信客户端。目前小程序也支持在微信的PC客户端上运行,所以在开发过程中也需要考虑Windows或Mac环境的UI适配,以及代码兼容性的问题。
WXML(WeiXin Markup Language)是小程序框架设计的一套标签语言,结合小程序的基础组件、事件系统,可以构建出页面的结构。虽然在书写方式上WXML和HTML有很多相似之处,但是二者之间的语法结构又有很大的区别,WXML仅能在微信小程序开发工具中预览,而HTML可以在浏览器内预览。传统的HTML标签在WXML中是无法之间使用的,WXML对组件进行了重新封装,为后续的性能优化提供了可能,同时避免开发者写出低质量的代码。
WXML文件以 .wxml 作为后缀,一个完整的 WXML 语句由一段开始标签和结束标签组成,在标签中可以是内容,也可以是其他的WXML语句,这一点上与HTML是一致的。WXML基本语法如例1所示。
【例1】WXML基本语法
WXML的语法校验是非常严格的,要求标签必须是严格闭合的,没有闭合将会导致编译错误。
WXSS(WeiXin Style Sheets)是一套用于小程序的样式语言,用于描述WXML的组件样式,提升视觉上的效果。WXSS与传统前端开发中的CSS类似,为了更适合小程序开发,WXSS对CSS做了一个补充和扩展,例如尺寸单位、样式导入等。
在WXSS中使用rpx(responsive pixel)作为尺寸单位,可以根据屏幕宽度进行自适应。小程序中的rpx与传统CSS尺寸单位的px是以 1rpx = 0.5px 进行的换算。
小程序的主要开发语言是Javascript,开发者使用Javascript开发业务逻辑以及调用小程序的API,以此来完成业务需求。在大部分开发者看来,Javascript和ECMAscript是指同一回事,但是从严格意义上来讲,二者之间的意义是完全不同的。ECMAscript是由ECMA国际组织通过ECMA-262标准化的脚本程序设计语言,该标准规定了ECMAscript主要包括脚本语法、数据类型、语句、关键字、操作符以及对象等基本的编程语言规范,而Javascript是ECMAscript的一种具体实现。理解了这些概念,有助于开发者理解小程序中的Javascript同浏览器的Javascript以及Node中的Javascript之间的区别。
浏览器中的Javascript是由BOM(浏览器对象模型,全称 Browser Object Model)、DOM(文档对象模型,全称 document Object Model)以及ECMAscript组成的,对于Web前端开发者来说,应该非常熟悉BOM和DOM这两个对象模型,它使得开发者可以去操作浏览器的一些表现,比如修改URL、修改页面呈现、记录数据等等。
Node中的Javascript是由NPM、Native模块以及ECMAscript组成的,Node的开发者非常熟悉NPM的包管理工具,通过各种扩展包来快速实现一些功能,同时通过使用一些原生的模块来赋予Node语言本身不具有的能力,例如FS、HTTP、OS等。
小程序的Javascript是由ECMAscript以及小程序的框架和API来实现的,与浏览器中的Javascript相比没有BOM和DOM对象,所以类似于jQuery、Zepto这种浏览器类库是无法在小程序中运行起来的,同样的缺少Native模块和NPM包管理的机制。所以这就导致小程序中无法加载原生库,也无法直接使用大部分的NPM依赖包。
JSON(JS对象简谱,全称 Javascript Object Notation)是一种轻量级的数据交换格式,是基于ECMAscript的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。JSON的语法易于阅读和编写,同时也易于程序解析和生成,是一种理想的网络传输格式,也可以作为项目的配置文件。由此可见,JSON仅是一种数据格式而非编程语言,在小程序中也作为一种重要的配置文件而存在。
JOSN文件作为小程序中的静态配置文件,在小程序运行之前就决定了小程序的一些表现,需要注意的是小程序无法在运行过程中去动态更新JSON配置,如果JSON配置文件的内容发生了更改,需要重新编译当前的项目才能生效。
小程序是基于双线程模型的,包括渲染层和逻辑层。在这个模型中,小程序的渲染层和逻辑层是分开在不同的线程中运行的,这与传统的Web单线程模型有很大的区别。小程序渲染层的界面使用了WebView 进行渲染;逻辑层采用JsCore线程运行JS脚本。一个小程序存在多个界面,所以渲染层存在多个WebView线程,这两个线程的通信会经由微信客户端做中转,逻辑层发送网络请求也经由Native转发,小程序的通信模型如图4所示。
图4 渲染层和逻辑层通信模型
在小程序的开发中,开发者对小程序最大的期望就是当用户点击某个小程序时,可以让小程序在最短的时间内加载完毕小程序的界面。由于小程序的宿主是微信,所以我们不太可能用纯客户端原生技术来编写小程序,因此,我们需要像Web技术那样,有一份随时可更新的资源包放在云端,通过下载到本地,动态执行后即可渲染出界面。
我们了解过模型背后的原理,下面再来看一下小程序是如何把脚本中的数据渲染到界面上的。小程序的WXML模板使用view标签,其子节点用“{{}}”的语法绑定一个msg的变量,如例2所示。
【例2】渲染WXML代码
在JS脚本中使用 方法把msg字段设置为“Hello World”,如例3所示。
【例3】用于渲染的JS脚本
上面的例子中,WXML页面通过模板语法的方式绑定了JS脚本的msg变量,当msg变量被修改后,页面展示的内容也会自动发生改变。在UI界面开发过程中,程序需要维护很多变量状态,同时还需要操作变量所对应的UI元素。但是随着界面的结构越来越复杂,程序需要维护的变量也随之增加,同时还要处理更多界面上的交互事件,整个程序就变得特别地复杂。如果使用某种方法将变量的状态和UI视图绑定在一起,当状态变更时,视图也会自动变更,那么开发者就可以省去编写修改视图的代码,提高开发效率。这种方法就是“数据驱动”。
小程序数据驱动的原理就是通过JS对象来表达DOM树的结构,而这棵DOM树实际上就是WXML的结构。WXML可以先转换成JS对象,然后再渲染出真正的DOM树,例2与例3的转换效果如图5所示。 图5 WXML结构转换示例图
假如我们把msg变量的值从“Hello World”改为“Hi”,这个过程必须通过调用 方法来完成,其产生的JS对象所对应的节点发生了变化,此时DOM树也会随之更改,从而达到更新UI界面的目的,这就是小程序“数据驱动”的原理。
通过上述讲解,我们理解了小程序的渲染层和逻辑层为什么是分开的,而且在渲染层中,小程序的宿主环境会把WXML转换成JS对象,而JS脚本是运行在逻辑层的。当逻辑层的数据发生变化时,通过 方法把数据从逻辑层再传递到渲染层,经过对比前后的差异,把更改后的数据应用在原来的DOM树上,以此实现UI界面的渲染,这就是小程序的渲染机制。
小程序的运行环境被分为了渲染层和逻辑层,渲染层主要是用于渲染页面视图,而逻辑层主要负责处理业务逻辑,这就要求我们必须要分清楚小程序中的程序与页面。站在逻辑组成的角度来说,一个小程序是由多个页面组成的程序,那么我们又要分清楚“小程序”和“程序”的概念。
我们常说的“小程序”其实指的是一个应用,这个应用是从产品的层面上来理解的,一个小程序就是一个软件应用的产品。而我们在本小节中所讲的“程序”是指在小程序应用内部的代码层面的程序实例。小程序的宿主环境提供了 函数作为程序的构造方法,以此来注册一个程序的 App 对象,所以在本小节中,我们就以 App 作为代码层面的“程序”。
构造方法 需要声明在小程序项目的根目录下的 app.js 文件中,App 实例也是一个单例对象,其构造方法接收一个 Object 对象参数,参数对象中可以声明小程序全局生命周期函数,代码如例4所示。
【例4】全局生命周期方法
在其他的 JS 脚本中需要使用 方法来获取 App 的实例,具体方法如例5所示。
【例5】获取 App 实例
我们都知道,一个小程序有很多页面组成,每个页面都是有界面、配置、逻辑三部分构成,这些页面的业务逻辑都需要编写到当前页面文件夹下的 page.js 文件中。宿主环境也提供了一个构造方法 来实现注册小程序页面, 在页面脚本 page.js 中调用。与 相同, 方法也是要接收一个 Object 对象参数,参数对象的属性中除了要声明页面的生命周期方法之外,还可以声明事件方法和页面的初始化数据data 属性,代码如例6所示。
【例6】页面构造方法
开发者不需要主动调用 构造器中定义的生命周期方法,而是由微信客户端根据监听用户的操作而主动触发的,这就避免了程序调用上的混乱。学习过了小程序界面渲染的基本原理后,我们知道小程序的页面结构是由 WXML 进行描述的,WXML 可以通过数据绑定的语法来绑定逻辑层定义的数据对象,这个数据对象就是在 构造方法的参数中定义的 data 属性字段,data 字段的值也是在页面第一次被渲染时从逻辑层传递到渲染层的。
组件化是前端最常见的一种开发方式,组件就是对应用视图层的拆分,一个小程序的页面也可以被拆分成多个组件,组件是小程序页面的基本组成单元。为了方便开发者可以快速进行开发,小程序的宿主环境提供了一系列的基础组件。除了小程序宿主环境提供的组件,还有开发者自行封装的视图组件和引入的外部第三方组件,所以为了区分这些组件,我们就把小程序宿主环境提供的基础组件称为小程序的内置组件。
组件是在 WXML 模板文件中声明使用的,其语法和 HTML 语法非常相似,但又有一些区别。小程序的 WXML 模板文件遵循 JSX(Javascript XML) 语法规范,规定了每个组件标签都必须有开始标记和结束标记,所有组件名称和属性名称都必须是小写,多个单词之间使用“-”进行连接。WXML组件标签代码如例6所示。
【例7】WXML组件标签
组件标签的属性主要包含样式和事件绑定,除了一些公共属性之外,还可以拥有各自自定义的属性,组件可以使用这些属性对自身就行样式修饰和功能封装,以image组件为例,可以为图片标签上面定义图片的模式和加载方式,具体代码如8所示。
【例8】image图片组件
在 image 图片组件上可以使用 src 属性加载图片资源,还可以使用 mode 属性来定义图片的裁剪、缩放模式,组件上的 lazy-load 属性决定了图片是否开启懒加载。我们还可以定义图片的事件属性,例如 binderror、bindload 等。
为了方便开发者调用微信提供的能力和手机硬件能力,小程序宿主环境提供了丰富的 API(Application Programming Interface,应用程序接口)。小程序提供的 API 按照功能主要分为几大类:网络、媒体、文件、数据缓存、位置、设备、界面、微信开放能力等,而且对于 API 的调用小程序也做了约定,例如:
- 小程序所有的API都挂载到名为wx的全局对象下;
- 用于监听事件的 API 函数都是以 开头;
- API的Object参数一般由success、fail、complete三个回调函数来接收接口调用的结果;
- 在API中凡是以 和 开头的都是用于写入数据和获取数据的接口;
- 如果没有特殊说明,大部分的API函数都是异步函数,并且都接受一个Object作为参数。
以小程序发起网络请求为例,API接口调用的代码如例9所示。
【例9】发起网络请求
事件就是被控件所识别的操作,例如在页面中点击了确定按钮,小程序中的事件与传统Web开发的事件机制是一样的。当小程序UI界面的程序与用户之间发生了交互,渲染层就会通知逻辑层执行对应的事件方法,然后逻辑层再将处理好的结果传递给渲染层并向用户展示。但是有时候程序上的“行为反馈”不一定是用户主动触发的,例如视频播放过程中的进度变化,也需要对开发者进行返回,方便开发者在逻辑层中所响应的处理。
在小程序中,任何渲染层的行为事件都需要向开发者反馈,这种事件行为有可能是用户主动触发的,也有可能是组件状态改变而触发的,无论哪种状态的事件触发。无论哪种状态的事件触发行为,都需要被微信客户端捕获,然后由开发者在逻辑层中处理。整个事件传递过程如图6所示。 图6 渲染层与逻辑层的事件传递
以页面按钮点击事件交互为例,具体代码如例10所示。
【例10】页面按钮点击事件
事件通过组件上绑定的 bindtap 属性触发,同时在页面构造方法 中声明对应的 方法来处理对应的事件,当用户点击页面的 button 按钮时就会触发 事件方法,同时得到event 事件对象,组件上的 data-msg 属性的值也会被封装到 event 事件对象中。
当事件回调函数触发时,会接收到一个事件对象。事件对象的属性如表1所示。
这里需要注意的是target和currentTarget的区别,currentTarget为当前事件所绑定的组件,而target则是触发该事件的源头组件。
在传统网页开发中,我们可以使用 CSS 的display、position、float等属性来实现页面布局,但是在小程序中需要考虑各种终端的尺寸适配,如果还是使用定位、浮动这类布局的话很难实现不同终端的适配,缺乏灵活性。在微信小程序中,建议使用flex弹性盒子布局。如果小程序需要兼容IOS8以下版本的话,需要开启样式自动补全,在小程序菜单栏中选择设置、项目设置,勾选“上传代码时样式自动补全”选项。
flex弹性盒子布局提供了一种灵活的布局模型,使容器能通过改变里面项目的高宽、顺序,来对可用空间实现最佳的填充,方便适配不同大小的内容区域。flex不单是一个属性,它包含了一套新的属性集。属性集包括用于设置容器,和用于设置项目两部分。设置容器的属性如表2所示。
设置项目的属性如表3所示。
flex在页面布局设计中应用非常广泛,例如在不固定高度的情况下,只需要在容器中设置flex的排列方向和主轴的对齐方式,即可实现内容不确定下的垂直居中效果,示例代码如例11所示。
【例11】flex设置容器内容垂直居中
微信小程序中常用的界面交互行为包括屏幕触摸反馈、弹框提示、界面滚动等。由于受到终端设备性能等因素的影响,频繁的用户与小程序交互的操作会导致系统延迟,操作的反馈耗时较长的情况,我们在开发小程序时应该尽可能的考虑到用户的使用体验。
一般在用户触摸某个事件按钮或view区域时,会改变对应区域的颜色,例如用户手指触摸view区域时,将该view区域的底色设置成浅灰色或其他具有明显对比的颜色,效果如图7所示。
图7 可触摸区域的用户操作反馈效果
这样做的目的就是为用户及时提示触摸的结果,以免用户触摸后不知道结果而反复的触发执行。设置了用户操作的反馈效果,大大提升了用户的使用体验。
除了这种设置区域不同的触发样式外,还有些常用的用户触发效果反馈,例如为button组件设置loading属性,在完成某个操作后弹出Toast提示框等效果。如果使用弹出框作为用户操作后的提示效果,需要在错误提示时明确告知用户具体出现错误的原因,并且需要用户手动关闭弹出框,如有需要的话还会附带下一步操作的引导。
在前后端分离开发的项目中,前端需要通过发送异步请求从服务器获取数据,小程序中也不例外。小程序作为客户端,需要通过宿主环境提供的 函数发起网络请求来实现从服务器拉取信息。小程序宿主环境要求request发起的网络请求必须是https协议请求,因此开发者服务器必须提供HTTPS服务的接口,同时为了保证小程序不乱用任意域名的服务, 请求的域名需要在小程序管理平台进行配置,如果小程序正式版使用 请求未配置的域名,在控制台会有相应的报错。
方法的参数是一个Object对象,对象中最重要的属性包括:
- url,服务器请求接口;
- data,请求参数;
- header,设置请求的header;
- method,请求方法,默认值是GET;
- success,收到开发者服务成功返回的回调函数。
小程序发出一个HTTPS网络请求,有时网络存在一些异常或者服务器存在问题,在经过一段时间后仍然没有收到网络回包,我们把这一段等待的最长时间称为请求超时时间。小程序request默认超时时间是60秒,一般来说,我们不需要这么长的一个等待时间才收到回包,可能在等待3秒后还没收到回包,就需要给用户一个明确的“服务不可用”的提示。在小程序项目根目录里边的app.json可以指定request的超时时间。
小程序的本地数据缓存能力在实际开发中应用非常广泛,本地数据缓存就是通过小程序将数据存储到当前设备的硬盘上,开发者可以使用本地数据缓存来存储一些服务端非实时的数据,从而提高小程序的渲染速度,减少用户的等待时间。 小程序提供了读写本地数据缓存的接口,通过 读取本地缓存,通过 写数据到缓存,其中Sync后缀的接口表示是同步接口,执行完毕之后会立马返回。小程序宿主环境会管理不同小程序的数据缓存,不同小程序的本地缓存空间是分开的,每个小程序的缓存空间上限为10MB,如果当前缓存已经达到10MB,再通过 写入缓存会触发fail回调。
小程序的本地缓存不仅仅通过小程序这个维度来隔离空间,考虑到同一个设备可以登录不同微信用户,宿主环境还对不同用户的缓存进行了隔离,避免用户间的数据隐私泄露。由于本地缓存是存放在当前设备,用户换设备之后无法从另一个设备读取到当前设备数据,因此用户的关键信息不建议只存在本地缓存,应该把数据放到服务器端进行持久化存储。
移动终端设备不同于PC端,在移动终端没有了PC端的键盘、鼠标等常用的输入设备和一些输出设备,但是移动终端中有很多传感器。而且移动终端屏幕尺寸也比PC端小了很多,所以在移动端屏幕上输入复杂信息的效率会很低。小程序的宿主环境提供了很多操作移动终端设备的能力,从而帮助开发者实现某些特定场景下的高效操作能力,例如扫描二维码、蓝牙连接、GPS定位等能力。
但是有的设备操作能力并不仅仅是为了解决高效输入的问题,更多的是提升用户的使用体验,例如获取设备的网络状态。手机连接网络的方式有2G、3G、4G、5G和wifi,每种连接方式的上传和下载速度有着很大的差异,而且计费方式不同。wifi连接相对于其他的移动网络连接来说,不仅访问速度快,而且不会对用户产生流量费用。用户在使用小程序观看视频或下载体积较大的文档时,为了避免用户耗费太多的数据流量,开发者就需要通过小程序提供的获取网络状态的能力,做出一些更加友好的提示,供用户自行选择。
小程序是以微信为基座的一种应用,在很多场景下都需要获取微信的一些能力,所以小程序的宿主环境就提供了开放微信部分权限的能力,这种开放能力包括:获取微信登录凭证、获取微信用户的基本信息、分享到朋友圈或转发消息、收藏、卡券、发票、生物认证、微信运动等能力。以微信登录为例,开发者在已有的互联网产品中接入小程序时会面临一些与登录状态有关的问题,微信就对小程序开发了微信登录的接口。
小程序的视图是在WebView里渲染的,所以小程序的视图搭建离不开HTML标记语言。如果在小程序中直接使用HTML的话,其安全性就会大大降低,并且无法使用微信小程序的双线程模型实现数据绑定和页面的动态渲染。为了解决这一问题,小程序设计了一套名为Exparser的组件框架,基于这个框架,在小程序内设计了一套涵盖大部分功能的组件,方便开发者快速搭建出满足需求的界面。
基于Exparser框架设计的小程序内置组件,涵盖了视图容器类、表单类、导航类、媒体类、开放类等几十种组件。所有的内置组件都可以使用WXSS修饰,这样就解决了大部分的项目需求。
在实际的项目开发中,小程序的内置组件不一定能满足所有的需求,为了实现更高效的代码复用,小程序还允许开发者自行扩充组件,这些由开发者自行设计的组件被称为自定义组件。
在小程序中,每个组件都具有独立的逻辑空间,分别拥有自己的独立数据和setData方法调用。在使用自定义组件的小程序页面中,Exparser框架将接管所有的自定义组件注册和实例化。小程序的基础库中提供了Page和Component两个构造器,自定义组件使用的是Component构造器。
小程序从基础库版本 2.2.1 开始支持使用 npm 安装第三方包,因此也支持开发和使用第三方自定义组件包。我们在开发微信小程序时,选择一款好用的UI组件库,可以达到事半功倍的效果。目前,市面上常用的小程序UI组件库有以下几款:
- WeUI,是一套同微信原生视觉体验一致的基础样式库,由微信官方设计团队为微信 Web 开发量身设计,可以令用户的使用感知更加统一。
- Vant Weapp,是有赞移动端组件库 Vant 的小程序版本,两者基于相同的视觉规范,提供一致的 API 接口,助力开发者快速搭建小程序应用。
- iView Weapp,是由 TalkingData 发布的组件库,一套高质量的微信小程序 UI 组件库。
- TaroUI,是由京东凹凸实验室倾力打造的多端开发解决方案,使用 Taro 可以将源代码分别编译出可以在不同端(微信小程序、H5、RN等)运行的代码。