微信小程序代码复用技术-模板/组件/插件

微信小程序代码复用技术-模板/组件/插件

MD by Jimbowhy and you can visit my articles on https://www.jianshu.com/u/92a796ff3db5

准备工作

这篇文章主要关注小程序的代码复用技术, 为何要关注代码复用? 很直观的两个观点就是避免重复书写代码提高开发效率, 重复利用现有功能代码块增强程序的稳健度. 代码复用是一种常见技术, 小程序的模板/组件/插件本质上都是相同的, 只是在组织结构上有些差别. 插件也是一种常见的软件开发模式, 用好插件模式, 可以极大地扩展程序功能也利于集思广益, 修复软件原设计者没有考虑到的一些不足. 如我现在写MD文档用的 sublime, 它的插件机制就极大的丰富了软件的功能, 直接给用户提供了极大的便利. 还有我常用的 editplus, 它的用户工具也是插件模式, 只是名字叫发和使用上有些差异.

注意官方社区

微信小程序是一个新东西, 体系还不成熟, 处于发育多变阶段, 这不, 我文章还没写完, 手上的一个项目的登录功能已经废掉了, 又得给小项目动手术了:( 修改参考小程序•小故事(6)——微信登录能力优化. 接口变动官方昨天给出公告 获取用户信息接口优化调整

用户在没有任何操作的情况直接弹出授权的登录方式将逐渐不再支持,受影响的有 wx.getUserInfo 接口,以及 wx.authorize 接口传入 scope="scope.userInfo" 的情况。 开发者仍然可以使用 wx.getUserInfo 接口获取用户信息。 具体优化调整如下:

1. 获取用户头像昵称,第一次需要使用 button 组件授权,之后 wx.getUserInfo 可直接返回用户数据,无需重复授权弹窗。
2. 如果没有用 button 组件授权,wx.getUserInfo 调用接口返回失败,提醒开发者需要先使用 button 组件授权。
3. 用户可在设置中,取消授权。 取消授权后需重新用 button 组件拉起授权。

此次调整仅会影响开发者工具、体验版和开发版,正式版本小程序暂不受影响。 详细可见如下接口文档:

小程序:

1. [使用 button 组件](https://developers.weixin.qq.com/miniprogram/dev/component/button.html) 并将 open-type 指定为 getUserInfo 类型,用户允许授权后,可获取用户基本信息。
2. [使用 open-data 展示用户基本信息](https://developers.weixin.qq.com/miniprogram/dev/component/open-data.html)

小游戏:

1. [使用用户信息按钮 UserInfoButton](https://developers.weixin.qq.com/minigame/dev/document/open-api/user-info/wx.createUserInfoButton.html)
2. [开放数据域下的展示用户信息。](https://developers.weixin.qq.com/minigame/dev/document/open-api/data/wx.getUserInfo.html)

很多开发者会把login和getUserInfo捆绑调用当成登录使用, 其实login已经可以完成登录, 可以建立账号体系了, getUserInfo只是获取额外的用户信息. 在login获取到code, 然后发送到开发者后端, 开发者后端再通过接口去微信后端换取到 openid 和 sessionKey 现在会将unionid也一并返回. 之后把 3rd_session 返回给前端,就已经完成登录行为. login行为是静默, 不必授权的, 不会对用户造成骚扰.

我的按官方模板程序的处理方法做, 使用 button 让用户手动授权, 注意按钮的绑定事件 bindgetuserinfo 而不是 bindtap! 因为用户信息是联网获取, 有延时, bindtap 却是立即调用, 并不合适.

<view wx:if="{{ungetUserInfo}}">
    <button bindgetuserinfo="getUserInfo" open-type="getUserInfo">getUserInfo</button>
</view>
<view wx:else class="user-name">{{userInfo.nickName}}</view>

getUserInfo: function(e){
    var userInfo = e.detail.userInfo;
    getApp().globalData.userInfo = userInfo
    getApp().globalData.hasUserInfo = true
    //t.post("/updateUserInfo", res.detail.userInfo, function(res){} );
    this.setData({
      userInfo: userInfo,
      hasUserInfo: true
    });
},

小程序基本结构

新建一个模板小程序, 打开微信开发者工具, 选择一个空目录 component, 新建一个普通快速启动模板, 然后会自动生成一下目录结构:

+--component
    +--pages
    |   +--index
    |   |   +--index.js
    |   |   +--index.wxml
    |   |   +--index.wxss
    |   +--logs
    |       +--logs.js
    |       +--logs.json
    |       +--logs.wxml
    |       +--logs.wxss
    +--utils
    |   +--util.js
    +--project.config.json
    +--app.js
    +--app.json
    +--app.wxss
新建快速启动模板工程

四种文件类型, 模块文件 js, 视图模板文件 wxml, 样式定义文件 wxss. 三者相当WEB开发中的JavaScript脚本, HTML文件, CSS样式表. WXML(WeiXin Markup Language) 是框架设计的一套标签语言, 结合基础组件, 事件系统, 可以构建出页面的结构, 具有数据绑定, 列表渲染, 条件渲染, 模板, 事件, 引用等能力. WXSS(WeiXin Style Sheets)是一套样式语言, 用于描述 WXML 的组件样式. 为了适应广大的前端开发者,WXSS 具有 CSS 大部分特性. 同时为了更适合开发微信小程序, WXSS 对 CSS 进行了扩充以及修改. 配置文件 json 则以 json 格式保存配置信息, 页面的配置文件对本页面的窗口表现进行配置, 可以设置页面标题等, 配置能力参考以下列表:

属性                              类型          默认值         描述
navigationBarBackgroundColor    HexColor    #000000         导航栏背景颜色如"#000000"
navigationBarTextStyle          String      white           导航栏标题颜色仅支持 black/white
navigationBarTitleText          String                      导航栏标题文字内容
backgroundColor                 HexColor    #ffffff         窗口的背景色
backgroundTextStyle             String      dark            下拉 loading 的样式仅支持 dark/light    
enablePullDownRefresh           Boolean     false           是否开启下拉刷新事件
disableScroll                   Boolean     false           设置为 true 则页面整体不能上下滚动
onReachBottomDistance           Number      50              页面上拉触底事件触发时距页面底部距离单位为px

这个模板小程序是完整可用的, 只包含了 index 和 logs 两个页面. index 展示了 wx.login 和 wx.getSetting API实现用户登录和用户基本信息获取功能, 页面会展示用户头像, 点击头像后会进入 logs 页面, 里面展示如何利用本地存储API wx.getStorageSync() 来读取日志内容, 而这些内容是在小程序入口模块 app.js 中通过API wx.setStorageSync() 写入的.

// 展示本地存储能力
var logs = wx.getStorageSync('logs') || []
logs.unshift(Date.now())
wx.setStorageSync('logs', logs)

页面可以在小程序配置文件, 即 app.json 中设置, 小程序配置文件还可设置其他UI外观属性, 其中 pages 中的第一个页面就是小程序入口 app.js 启动后进入的页面:

{
  "pages":[
    "pages/index/index",
    "pages/logs/logs"
  ],
  "window":{
    "backgroundTextStyle":"light",
    "navigationBarBackgroundColor": "#fff",
    "navigationBarTitleText": "WeChat",
    "navigationBarTextStyle":"black"
  }
}

小程序入口 app.js 中定义了以个 App 对象, 默认设置了 onLaunch 事件的代码, 这就是微信加载小程序后执行的入口. 在入口事件执行之前, 配置文件会事先读取运行, 包括项目配置 project.config.json 和 小程序配置文件 app.json. 配置文件就是以 json 为扩展名, 这也是 JavaScript 的 JSON 对象的名字, 因为配置文件的内容就是 JSON. 每一个 js 文件都是一个模块, 模块间作用域相互独立, 只能同过模块的 exports 来实现模块间的交互:

module.exports = {
  // JSON FORMAT
}

如根目录下的 util.js 模块导出了一个用来格式化的代码块 formatTime:

module.exports = {
  formatTime: formatTime
}

程序 (app.js)模块文件和页面模块文件 js 定义了程序对象 App 和页面对象 Page 的生命周期事件, 数据及其它相关功能. App 实例通过全局方法 getApp() 获取. 页面模块文件中通过 Page({jsonObject}) 方法来注册/构造页面实例. jsonObject 中除了以下内置的参数或数据, 用户可以自行添加其他需要的内容:

data                Object      页面的初始数据
onLoad              Function    生命周期函数--监听页面加载
onReady             Function    生命周期函数--监听页面初次渲染完成
onShow              Function    生命周期函数--监听页面显示
onHide              Function    生命周期函数--监听页面隐藏
onUnload            Function    生命周期函数--监听页面卸载
onPullDownRefresh   Function    页面相关事件处理函数--监听用户下拉动作
onReachBottom       Function    页面上拉触底事件的处理函数
onShareAppMessage   Function    用户点击右上角转发
onPageScroll        Function    页面滚动触发事件的处理函数
onTabItemTap        Function    当前是 tab 页时,点击 tab 时触发

例如例子中注册 logs 页面的代码, require 是加载其他模块, 这时被加载的模块中通过 module.exports 导出的 json 对象就是返回值:

const util = require('../../utils/util.js')

Page({
  data: {
    logs: []
  },
  onLoad: function () {
    this.setData({
      logs: (wx.getStorageSync('logs') || []).map(log => {
        return util.formatTime(new Date(log))
      })
    })
  }
})

util.js 内容, 注意传首行格式:

const formatTime = date => {
  const year = date.getFullYear()
  const month = date.getMonth() + 1
  const day = date.getDate()
  const hour = date.getHours()
  const minute = date.getMinutes()
  const second = date.getSeconds()

  return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':')
}

const formatNumber = n => {
  n = n.toString()
  return n[1] ? n : '0' + n
}

module.exports = {
  formatTime: formatTime
}

有CSS基础就很容易理解 WXSS, 基本可以等价来看待, 目前支持的选择器有:

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

例如 WXML 中的 button 边框是用 :after 方式实现的,用户如果在 button 上定义边框会出现两条线,需用 :after 选择器覆盖默认值.

微信脚本语言介绍

这里的 js 是微信小程序推出最新脚本语言 WXS(WeiXin Script), 和 JavaScript 只是在语法上有类似点, 因为他们都是 ECMAScript 规范, 但更多的不同在运行环境和细节上. WXS除了以上 js 模块的使用方式, 可以直接在WXML中编写 <wxs> 标签, 通过 src 引用外部文件, 或将代码写在 <wxs> 标签内.

<!-- some.wxml -->
<wxs module="foo">
    var hi = "hello world"; 
    module.exports = {    msg : hi, }
</wxs>
<view> {{foo.msg}} </view>

<!-- some.wxml -->
<wxs src="./../foo.wxs" module="foo" />
<view> {{foo.msg}} </view>

<wxs> 模块只能在定义模块的 WXML 文件中被访问到. 使用 <include> 或 <import> 包含 WXML 文件时, <wxs> 模块不会被引入到对应的 WXML 文件中.

WXS语言目前共有8种数据类型:

number:数值       boolean:布尔值     array:数组        string:字符串
object:对象       function:函数     date:日期     regexp:正则

使用变量的构造器属性 constructor, 或运算符 typeof 来判定变量的类型:

var number = 10;
console.log( "Number" === number.constructor );
console.log( 'number' === typeof number );

和 JavaScript 类似 function 里面可以使用 arguments 关键词, 该关键词目前只支持 length 属性, 传递给函数的参数个数.
通过下标可以遍历传递给函数的每个参数 arguments[i]。

详细的内容参考官方文档.

视图WXML介绍

  • 数据绑定

小程序框架的核心是一个响应的数据绑定系统, 整个系统分为两块, WXML对应视图层(View)和脚本模块对应的逻辑层(App Service). 框架可以让数据与视图保持同步, 当做数据修改的时候,只需要在逻辑层修改数据, 视图层就会做相应的更新。

WXML 中的动态数据均来自对应 Page 的 data 属性, 视图的更新只能通过页面对象的 setData() 方法进行, 不像HTML可以进行 DOM 操作. 由于 setData() 会导致页面重新渲染, 所以不要频繁调用它, 或者要避免用它更新大量的数据. 数据绑定使用 Mustache 语法(双大括号)将变量包起来,可以作用于:

<!-- some.xml -->
<view> {{ index }} </view>

/* some.js */
Page({
  data: { enabled: true }
})

组件属性, 控制属性, 关键字等需要在双引号之内, 注意: 花括号和引号之间如果有空格,将最终被解析成为字符串. 特别注意不要直接写 checked="false", 其计算结果是一个字符串, 转成 boolean 类型后代表真值:

<view id="item-{{enabled}}"> </view>
<view wx:if="{{enabled}}"> </view>
<checkbox checked="{{false}}"> </checkbox>

可以在 {{}} 内进行简单的运算,支持的有如下几种方式, 三元运算, 算数运算, 逻辑判断, 字符串运算, 数据路径运算:

<view hidden="{{flag ? true : false}}"> Hidden </view>
<view> {{a + b}} + {{c}} + d </view>
<view wx:if="{{length > 5}}"> </view>
<view>{{"hello" + name}}</view>
<view>{{object.key}} {{array[0]}}</view>

也可以在 Mustache 内直接进行组合,构成新的对象或者数组。 以下示例组合数组 [0, 1, 2, 3, 4] 和对象:

<!-- some.xml -->
<view wx:for="{{[zero, 1, 2, 3, 4]}}"> {{item}} </view>
<template is="objectCombineDemo" data="{{for: a, bar: b}}"></template>

/* some.js */
Page({
  data: { zero: 0 }
})

也可以用扩展运算符 ... 来将一个对象展开, 一下例子最终组合成的对象是 {a: 1, b: 2, c: 3, d: 4, e: 5}:

<!-- some.xml -->
<template is="objectCombine" data="{{...obj1, ...obj2, e: 5}}"></template>

/* some.js */
Page({
  data: {
    obj1: {a: 1, b: 2 },
    obj2: {c: 3, d: 4 }
  }
})
  • 列表渲染

在组件上使用 wx:for 控制属性绑定一个数组, 即可使用数组中各项的数据重复渲染该组件. 默认数组的当前项的下标变量名默认为 index, 数组当前项的变量名默认为 item. 使用 wx:for-item 可以指定数组当前元素的变量名, 使用 wx:for-index 可以指定数组当前下标的变量名, wx:for 也可以嵌套:

<view wx:for="{{array}}" wx:for-item="item" >
  {{index}}: {{item.message}}
</view>

block wx:for 类似 block wx:if,也可以将 wx:for 用在<block/>标签上,以渲染一个包含多节点的结构块. block 本身是不可见节点, 不会产生渲染消耗.

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

当 wx:for 的值为字符串时, 会将字符串解析成字符串数组, 以下 wx:for 两者等价. 注意: 花括号和引号之间如果有空格, 将最终被解析成为字符串

<view wx:for="array">
  {{item}}
</view>

<view wx:for="{{['a','r','r','a','y']}}">
  {{item}}
</view>

如果列表中项目的位置会动态改变或者有新的项目添加到列表中, 并且希望列表中的项目保持自己的特征和状态, 如 input 标签中的输入内容 switch 标签的选中状态. 这时 wx:for 需要使用 wx:key 来指定列表中项目的唯一的标识符. wx:key 的值以两种形式提供, 一,是字符串代表在 for 循环的 array 中 item 的某个属性. 该属性的值需要是列表中唯一的字符串或数字, 且不能动态改变, 也就是说这个属性可以当作hash表的key来使用. 二是保留关键字 *this, 代表在 for 循环中的 item 本身,这种表示需要 item 本身是一个唯一的字符串或者数字. 当数据改变触发渲染层重新渲染的时候, 会校正带有 key 属性的组件, 框架会确保他们被重新排序, 而不是重新创建, 以确保使组件保持自身的状态, 并且提高列表渲染时的效率. 如不提供 wx:key, 会报一个 warning, 如果明确知道该列表是静态, 或者不必关注其顺序, 可以选择忽略. 以下代码示例了如何使用 wx:key 来保持组件与数据 objectArray 的联系. shuffle 为洗牌方法, 可以将 objectArray 的数据排序打乱, 然后通过调用 setData 方法更新视图. 试将 switch 组件的 wx:key 删去, 再触发 shuffle, 会发现视图和 objectArray 的关联丢失了, 因为视图重新构造了组件并且没有将组件与特定的数据对象关联起来.

<!-- some.wxml -->
<switch wx:for="{{objectArray}}" wx:key="unique" style="display: block;"> {{item.id}} </switch>
<button bindtap="shuffle"> Switch </button>

/* some.js */
Page({
    data: {
        objectArray: [
            { id: 5, unique: 'unique_anykey' },
            { id: 4, unique: 'unique_4' },
            { id: 3, unique: 'unique_3' },
            { id: 2, unique: 'unique_a' },
            { id: 1, unique: 'unique_1' },
            { id: 0, unique: 'unique_abc' },
        ]
    },
    shuffle: function(e) {
        const length = this.data.objectArray.length
        for (let i = 0; i < length; ++i) {
            const x = Math.floor(Math.random() * length)
            const y = Math.floor(Math.random() * length)
            const temp = this.data.objectArray[x]
            this.data.objectArray[x] = this.data.objectArray[y]
            this.data.objectArray[y] = temp
        }
        this.setData({
            objectArray: this.data.objectArray
        })
    }
}
  • 条件渲染

在框架中,使用 wx:if="{{condition}}" 来判断是否需要渲染该代码块, 还可以使用任意个 wx:elif 和一个 wx:else, 这两项是可选的.

<view wx:if="{{length > 5}}"> 1 </view>
<view wx:elif="{{length > 2}}"> 2 </view>
<view wx:else> 3 </view>

可以使用 block 标签将多个组件包装起来,并在上边使用 wx:if 控制属性, 注意 block 标签并不是一个组件, 它仅仅是一个包装元素, 不会在页面中做任何渲染, 只接受控制属性.

因为被包裹模板可能包含数据绑定, 所有当 wx:if 的条件值切换时, 框架有一个局部渲染的过程, 因为它会确保条件块在切换时销毁或重新渲染. 如果在初始渲染条件为 false 框架什么也不做, 在条件第一次变成真的时候才开始局部渲染.

相比之下, 使用 hidden 属性来控制组件的显示或隐藏就简单的多, 组件始终会被渲染, 只是简单的控制显示与隐藏. 一般来说, wx:if 有更高的切换消耗而 hidden 有更高的初始渲染消耗. 因此需要频繁切换的情景下, 用 hidden 更好, 如果在运行时条件不大可能改变则 wx:if 较好.

  • import 和 include 引用

include 可以将目标文件除了 template 和 wxs 标签外的整个代码引入,相当于是将 WXML 拷贝到 include 位置. import 可以在该文件中使用目标文件定义的template, 有作用域的概念, 即只会 import 目标文件中定义的 template, 而不会 import 目标文件 import 的 template. 如:C import B,B import A,在C中可以使用B定义的template, 在B中可以使用A定义的template, 但是C不能使用A定义的template. 如下代码示例在 item.wxml 中定义了一个叫item的template:在 index.wxml 中引用了 item.wxml,就可以使用item模板:

<!-- item.wxml -->
<template name="item">
  <text>{{text}}</text>
</template>

<!-- index.wxml -->
<import src="item.wxml"/>
<template is="item" data="{{text: 'forbar'}}"/>
  • 事件

事件是视图层到逻辑层的通讯方式, 可以理解为用户动作的响应. 事件可以绑定在组件上, 当达到触发事件, 就会执行逻辑层中对应的事件处理函数. 事件对象可以携带额外信息, 如组件的 id, dataset, touches. 在组件中绑定一个事件处理函数, 如常见的 bindtap, 当用户点击该组件的时候会在该页面对应的 Page 中找到相应的事件处理函数, 还有前面提到的 bindgetuserinfo. 执行事件处理函数时, 函数会得到一个 event 参数:

<!-- some.wxml -->
<view id="tapTest" data-hi-message="WeChat" bindtap="tapHandler"> Click me! </view>

<!-- some.js -->
Page({
    tapHandler: function(event) {
        console.log(event)
    }
})

上面 bindtap 这种事件绑定的写法分解出来, 就是 bind 表示事件绑定, tap 表示事件类型. 事件绑定还可以有 catch 这种方式, 如 catchtouchstart. 自基础库版本 1.5.0 起, bind 和 catch 与事件类型可以用一个冒号分隔, 其含义不变, 如 bind:tap, catch:touchstart. 这两种事件绑定的差别在于 bind 事件绑定不会阻止冒泡事件向上冒泡, catch 事件绑定可以阻止冒泡事件向上冒泡.

自基础库版本 1.5.0 起, 触摸类事件支持事件捕获, 捕获阶段位于冒泡阶段之前, 且在捕获阶段中, 事件到达节点的顺序与冒泡阶段恰好相反, 即从更节点向末节点传递. 需要在捕获阶段监听事件时, 可以采用 capture-bind, capture-catch 关键字, 后者将中断捕获阶段和取消冒泡阶段.

一个典型的 TouchEvent( 继承 CustomEvent, BaseEvent) 对象包含的信息可以从 log 输出的信息观察到. 其中 type 代表事件的类型. timeStamp 页面打开到触发事件所经过的毫秒数. target 是触发事件的组件对象信息, currentTarget 是当前接收事件并处理事件的组件对象信息, 因为事件触发后可以在组件节点上传递, 所以 target 和 currentTarget 并不一定是同一个组件节点. 在组件中可以定义数据, 这些数据将会通过事件传递给 SERVICE. 数据属性以 data 开头, 多个单词由连字符-链接, 使用小写字母(大写会自动转成小写)如data-element-type, 最终会转化为驼峰写法 elementType 出现在 event.currentTarget.dataset 中. detail 为自定义事件所携带的数据, 如表单组件的提交事件会携带用户的输入, 媒体的错误事件会携带错误信息, 详见组件定义中各个事件的定义. 点击事件的 detail 带有的 x, y 同 pageX, pageY 代表距离文档左上角的距离. touches 是一个数组, 每个元素为一个 Touch 对象. 组件 canvas 的触摸事件中携带的是 CanvasTouch, 表示当前停留在屏幕上的触摸点, identifier 表示触摸点的标识符, 就是个数字编号, 表示触摸的先后顺序. pageX, pageY 指示触摸点到文档左上角的距离, 文档的左上角为原点, 横向为X轴, 纵向为Y轴. clientX, clientY 距离页面可显示区域, 左上角不涵括导航条的距离:

{
    "type":"tap", // =======BaseEvent part========
    "timeStamp":895,
    "target": {
        "id": "tapTest",
        "offsetLeft": 118,
        "offsetTop": 323,
        "dataset":  { "hiMessage":"WeChat" }
    },
    "currentTarget":  {
        "id": "tapTest",
        "offsetLeft": 118,
        "offsetTop": 323,
        "dataset": { "hi":"WeChat" }
    },
    "detail": { // =======CustomEvent part========
        "x":53,
        "y":14
    },
    "touches":[{ // =======TouchEvent part========
        "identifier":0,
        "pageX":53,
        "pageY":14,
        "clientX":53,
        "clientY":14
    }],
    "changedTouches":[{
        "identifier":0,
        "pageX":53,
        "pageY":14,
        "clientX":53,
        "clientY":14
    }]
}

那些被触发后会向父节点传递的称为冒泡事件, 否则就是非冒泡事件, 如 form 的submit事件, input 的input事件, scroll-view 的scroll事件. 特殊事件如 canvas 中的触摸事件不可冒泡,所以没有 currentTarget. 以下是常用的冒泡事件:

类型              触发条件            最低版本
touchstart          手指触摸动作开始    
touchmove           手指触摸后移动 
touchcancel         手指触摸动作被打断   
touchend            手指触摸动作结束    
tap                 手指触摸后马上离开   
longpress           手指触摸后, 超过350ms再离开, 如果指定了事件回调函数并触发了这个事件, tap事件将不被触发  1.5.0
longtap             手指触摸后, 超过350ms再离开, 推荐使用longpress事件代替    
transitionend       会在 WXSS transition 或 wx.createAnimation 动画结束后触发 
animationstart      会在一个 WXSS animation 动画开始时触发 
animationiteration  会在一个 WXSS animation 一次迭代结束时触发   
animationend        会在一个 WXSS animation 动画完成时触发 
touchforcechange    在支持 3D Touch 的 iPhone 设备,重按时会触发 1.9.90

WXML的模板

WXML提供模板 template, 可以在模板中定义代码片段, 然后在不同的地方调用. 和页面一样, 模板也可以使用 js, json, wxml, wxss, 但只有 wxml 定义是必须的.使用 name 属性,作为模板的名字。然后在 template 内定义代码片段,如:

<template name="timeTip">
  <view>
    <text> Time: {{time}} </text>
  </view>
</template>

使用模板时, 通过 import 引用模板文件, 然后同样使用 template 标签来实例化模板, 使用 is 属性声明需要的使用的模板,然后将模板所需要的数据 data 传入,如:

<!-- page.wxml -->
<import src="path/to/template.wxml" />
<template is="timeTip" data="{{...item, ...moreArgs}}"/>

/* page.js */
Page({
  data: {
    item: { time: '2018-05-11 14:30:00' },
    more: "Hi More!"
  }
})

注意, item 本身是 json, ... 是 MXML 扩展运算符, 传入模板后, item 的属性 time 是可以直接被模板访问的, 不使用 item.time 这样来访问. is 属性可以使用 Mustache 语法,来动态决定具体需要渲染哪个模板:

<block wx:for="{{[1, 2, 3, 4, 5]}}">
    <template is="{{item % 2 == 0 ? 'msgItem' : 'otherTemplate'}}"/>
</block>

模板拥有自己的作用域,只能使用 data 传入的数据以及模版定义文件中定义的 wxs 模块。

实现一个组件

从小程序基础库版本 1.6.3 开始, 小程序支持简洁的组件化编程. 开发者可以将页面内的功能模块抽象成自定义组件, 以便在不同的页面中重复使用. 通過页面功能拆分, 實現低耦合的模块有助于代码维护. 自定义组件在使用时与基础组件非常相似,.

这裡要實現的组件只有时间提示功能, 还可以设置到点提示. 在 utils 目录下建立一个 TimeTip 目录, 再弹出这个目录的右键菜单, 选择建立一个 Component. 这样 utils 目录就变成如下结构:

+--utils
    +--TimeTip
    |   +--TimeTip.js
    |   +--TimeTip.json
    |   +--TimeTip.wxml
    |   +--TimeTip.wxss
    +--util.js
模板目录结构

在配置文件 TimeTip.json 声明自定义组件, 可选项 usingComponents 用于在組件中引用其他组件, 在 wxml 文件中编写组件視圖模版, 在 wxss 文件中加入组件样式,它们的写法与页面的写法类似. 考慮低耦合, 在组件wxss中使用类选择器, 不应使用ID选择器, 属性选择器和标签名选择器:

{
  "component": true,
  "usingComponents": {}
}

然后在模块文件 js 中使用构造器方法 Component() 来注册组件并提供组件的属性定义, 内部数据和自定义方法. 骨架如下, 组件的属性值 properties 和内部数据 data 将被用于组件 wxml 的渲染, 属性值则可由组件外部传入. 在组件的代码中可以通过 this 引用组件实例包含的一些通用属性和方法, 除 data 外, 还有组件的文件路径属性值 is 和 组件在页面中的 id 属性, 还有组件节点的 dataset 属性, 都是字符串类型.

Component({
    properties: {
        caption: {
            type: String,
            value: '现在时间',
        }
    },
    data: {
        someData: {}
    },
    methods: {
        customMethod: function(){}
    }
})

使用自定义组件时, 先在页面的配置文件 index.json 中进行引用声明, 需要指定自定义组件的标签名和对应的自定义组件文件路径, 文件路径要包含到 wxml 文件名, 这里指定的标签名和组件的文件名同名 TimeTip. 然后在页面的视图文件 wxml 中就可以像使用基础组件一样使用自定义组件, 节点名即自定义组件的标签名, 节点属性即传递给组件的属性值:

{
    "usingComponents": {
        "TimeTip": "/utils/TimeTip/TimeTip"
    }
}

配置好组件引用定义后, 就可像内置组件一样使用自定义组件了, 例如在 index.wxml 中添加以下标签:

<button bindtap="updateTimetip">update</button>
<TimeTip caption="Time Now" seconds="{{countdown}}"></TimeTip>

再修改以下页面模块文件, data 设置 countdown 默认值为 3, 这样页面一打开计时3秒就会弹出时间提示对话框, 另外增加一个 updateTimetip 方法, 用来测试组件.

updateTimetip: function(){
    this.setData({countdown:4});
},

后面就剩下自定义组件的编写工作了, 这也是自定义组件的主要内容.

组件的 wxml 中可以包含 slot 节点, 用于承载组件使用者提供的wxml结构, 默认情况下, 一个组件的wxml中只能有一个 slot, 需要使用多 slot 可以在组件js中声明启用, 然后可以在这个组件的 wxml 中使用多个slot 以不同的 name 来区分, 使用自定义组件时通过 slot 属性来关联:

<!-- component.js -->
Component({
    options: {
        multipleSlots: true
    },
})

<!-- component.wxml -->
<view class="wrapper">
    <slot name="before"></slot>
    <view>inner view</view>
    <slot name="after"></slot>
</view>

<!-- index.wxml -->
<view>
    <component-tag-name>
        <view slot="before"> text for slot before </view>
        <view slot="after"> text for slot after </view>
    </component-tag-name>
</view>

组件希望接受外部传入的样式类, 类似于 view 组件的 hover-class 属性, 此时可以在组件模块中使用 externalClasses 选项定义段定义若干个外部样式类, 这个特性从小程序基础库版本 1.9.90 开始支持. 这样, 组件的使用者可以指定这个样式类对应的 class 就像使用普通属性一样.

/* component.js */
Component({
    externalClasses: ['my-class']
})

<!-- component.wxml -->
<custom-component class="my-class">这段文本的颜色由组件外的 class="xxx" 决定</custom-component>

组件中使用 behaviors 特性, behaviors 是用于组件间代码共享的特性, 类似于一些编程语言中的“mixins”或“traits”. 每个 behavior 可以包含一组属性, 数据, 生命周期函数和方法, 组件引用它时, 它的属性, 数据和方法会被合并到组件中, 生命周期函数也会在对应时机被调用. 每个组件可以引用多个 behavior, behavior 也可以引用其他 behavior. behavior 需要使用 Behavior() 构造器定义. 完整的参考资料自定义组件

有了以上基础, 后续基本就可以着手编写组件代码了, 这里直接贴上完整的 TimeTip 组件代码. 首先是视图部分, 以下代码以尽量使用精简的标签来实现一个带有遮罩层的弹框视图, 相当于一个模态对话框, view-wrap 就是遮罩层, tip-dialog 充当对话框主体:

<!--utils/TimeTip/TimeTip.wxml-->
<view class="view-wrap" hidden="{{hideDialog}}">
    <view class="tip-dialog">
        <text class="caption">{{caption}}</text>
        <text class="content">{{dateTime}}</text>
        <button class="button" bindtap="onTapOK">OK</button>
    </view>
</view>

对应的样式定义如下, 没有特别难懂的属性设置, 基本都是和级联样式表CSS样式表一致的功能:

/* utils/TimeTip/TimeTip.wxss */
.view-wrap { 
    width: 100%; height: 100%; background: #226644; opacity: 0.9;
    position: absolute; left: 0; top: 0; z-index: 2;
}
.tip-dialog {
    width: 50%; height: 20%; background: #eeeeee; 
    border-radius: 12px; padding: 8px;
    position: absolute; top: 25%; left:25%; margin: 8px 0;
}
.caption, .content {
    display: block; color: #2288FF; font-size: 14px;
}
.caption { font-weight: bold; line-height: 32px; }
.content { text-align: center; }
.button { line-height: 32px; padding: 4px; margin: 8px; font-size: 14px; }

最后也是最重要的模块定义, 逻辑功能的代码实现部分, 我写的代码具有一定的自解析功能, 请结合前面的内容自行阅读:

// utils/TimeTip/TimeTip.js
Component({
    properties: {
        caption: {
            type: String,
            value: '现在时间',
        },
        seconds:{
            type: Number,
            value: 3,
            observer: 'timeChanged'
            // observer: function (newVal, oldVal) { ... }
        }
    },
    data: {
        dateTime: "loading..." 
    },
    // life-cycle functions or functions in methods
    created: function() { console.log("created (never setData() here)") },
    attached: function () { console.log("attached") },
    ready: function () {
        this.setData({hideDialog:true});
        this.taskSchedule();
        console.log("ready id:"+this.id+" is:"+this.is+" dataset:"+this.dataset);
        console.log(this.dataset);
    },
    moved: function () { console.log("moved") },
    detached: function () { console.log("detached") },
    methods: {
        timeChanged: function(newValue, oldValue){
            console.log("timeChanged:"+newValue+","+oldValue);
            console.log("properties seconds:"+this.properties.seconds);
            this.taskSchedule();
            // this.replaceDataOnPath(['A', 0, 'B'], 'something') // data.A[0].B='something
            // this.applyDataUpdates()
        },
        taskSchedule: function(){s
            var ctx = this;
            setTimeout(function () {
                ctx.setData({
                    dateTime: new Date().toLocaleString(),
                    hideDialog: false
                });
            }, this.properties.seconds * 1000);         
        },
        onTapOK: function(){
            this.setData({hideDialog:true});
        },
    }
})
小程序运行效果图

如果页面引用了组件,那么就可以使用这个API:

js:
onLoad: function(e) {
var ctx = this;
ctx.selectComponent && null == ctx.notification && (ctx.notification = ctx.selectComponent("#notification"));

wxml:
<dialog id="notification" bindonConfirm="onConfirm"></dialog>

json:
{
"usingComponents": {
"dialog": "/utils/Dialog/Dialog"
}
}

插件

插件的开发和使用自小程序基础库版本 1.9.6 开始支持, 是对一组 js 接口或自定义组件的封装, 用于提供给第三方小程序调用. 开发者可以将一些复用性强的代码抽象成自定义组件, 它们的使用方法与基础库内置的 view, button等基础组件非常相似, 这样的组件化非常有利于代码逻辑的解耦合. 开发者可以像开发小程序一样编写一个插件并上传代码, 在插件发布之后, 其他小程序方可调用, 插件必须嵌入在其他小程序中才能被用户使用. 小程序平台会托管插件代码, 其他小程序调用时, 上传的插件代码会随小程序一起下载运行. 相对于普通 js 文件或自定义组件, 插件拥有更强的独立性, 拥有独立的 API 接口、域名列表等, 但同时会受到一些限制, 如一些 API 无法调用或功能受限.

小程序开发者无需重新注册帐号, 可直接在小程序管理后台开通插件功能, 完成基本信息填写后完成开通。

小技巧

微信小程序开发过程中, 要用好控制台, 它可不止于使用 console.log() 或 console.error() 输出调试信息, 在控制台面板上的最后一行是个命令交互输入区, 这里可以键入交互命令, 或者当成一个手动运行的小程序. 实际开发中, 比如一个分享卡片发到微信群, 其他用户点击后发生什么了呢? 通常用户会被带到一个指定的页面, 而作为开发者, 这个页面的地址是可以事先获知的, 这时可以同过 wx.redirectTo() API 来实现手动模拟进入分享入口:

wx.redirectTo({url:"/page/index/pages/share/share?orderId=56"});
微信扫一扫打赏坚果

推荐阅读更多精彩内容