【工作小记】小程序开发的喜怒哀乐

1,027 阅读20分钟

一、前言

好久没写原生小程序了,最近新项目,重新体验了一把,感觉还挺好。

小程序生态发展到现在,功能很全,但是正因为它的全,所以,在开发者初次开发的时候会不太适应,很多API都不熟悉,包括一些API的历史变迁也不了解。所以,我趁着新项目开发的间隙,将一些常用的功能整理下来。即方便后面使用时查阅,也希望为大家开发提供一点帮助。

二、一个“五脏俱全”的小程序

我对新技术应用可以说是非常喜爱的。我对于一门新技术的学习,如果只是一味的学习,我的吸收不能达到最佳状态,后续就会出现疲惫心理。但是如果结合实践,我的大脑会比较兴奋,这个时候对技术的理解和吸收都会有显著提升。

所以我之前也专门写过我对SVG的学习方法是边学边做,掌握程度也会比单纯的看技术点高很多。

2.1 如何拥有一个小程序

这一章是将小程序从无到有的过程拆解成多个步骤,主要是写给第一次做小程序开发的朋友,可以通过下面的步骤熟悉小程序的开发生态。有经验的开发朋友,可以跳过这一章。

2.1.1 新建小程序

微信开发者工具自带创建小程序的功能,如果还没有申请小程序可以使用测试号的方式获取AppID。小程序从申请到使用开发者工具开发可以查看微信的官方文档大而全。

image.png

2.1.2 新增页面

根据微信小程序的开发文档,一个小程序页面由四个文件组成,分别是:

文件类型必需作用
js页面逻辑
wxml页面结构
json页面配置
wxss页面样式表
实际的文件结构如下图:

image.png

那么每次新增页面都需要新增四个文件吗?其实不用,只需要定义好需要新增的文件路径放到app.json文件中pages数组中即可

"pages": [
  "pages/home/home"
],

新增之后保存,微信开发者工具边帮大家自动生成文件了,为微信开发者工具点赞(哈哈哈,我好像没见过什么世面的样子)。

2.1.3 底部tab栏

1.基础底部tab栏

底部导航依旧是在app.json文件中配置,我再自己的小程序里面配置了两个入口,首页和我的,这两个入口链接、展示文字、未选中的icon、选中的icon、选中文字的颜色都设置了,这样基本就满足了一个小程序对底导航的需求。

"tabBar": {
  "selectedColor": "#007AF5",
  "list": [
    {
      "pagePath": "pages/home/home",
      "text": "首页",
      "iconPath": "/images/icon/home-unselected.png",
      "selectedIconPath": "/images/icon/home-selected.png"
    },
    {
      "pagePath": "pages/mine/mine",
      "text": "我的",
      "iconPath": "/images/icon/mine-unselected.png",
      "selectedIconPath": "/images/icon/mine-selected.png"
    }
  ]
},

实际可以配置的属性有很多,微信开发文档我把参数都复制出来放到下面:

属性类型必填默认值描述最低版本
colorHexColortab 上的文字默认颜色,仅支持十六进制颜色
selectedColorHexColortab 上的文字选中时的颜色,仅支持十六进制颜色
backgroundColorHexColortab 的背景色,仅支持十六进制颜色
borderStylestringblacktabbar 上边框的颜色, 仅支持 black / white
listArraytab 的列表,详见 list 属性说明,最少 2 个、最多 5 个 tab
positionstringbottomtabBar 的位置,仅支持 bottom / top
custombooleanfalse自定义 tabBar,见详情2.5.0

2.自定义底部tab栏

还可以根据业务需求,进行tabBar的自定义开发,微信的官方文档也给了很详细的开发步骤,我们来一起写一个自定义的底导航

1)首先按照文档提供的方案,在代码根目录下添加入口文件:

custom-tab-bar/index.js 
custom-tab-bar/index.json 
custom-tab-bar/index.wxml 
custom-tab-bar/index.wxss

目录结构如下:

image.png

index.js

相较官方提供的代码,我加入了tabChange方法,因为直接使用官方的代码,会出现底部tab高亮不准的问题(点击我的,高亮让在首页)。大家可以注释掉tabChange方法试试就能发现问题了。

Component({
  data: {
    selected: 0,
    color: '#A0A3B1',
    selectedColor: '#007AF5',
    list: [
      {
        pagePath: '/pages/home/home',
        iconPath: '/images/icon/home-unselected.png',
        selectedIconPath: '/images/icon/home-selected.png',
        text: '首页',
      },
      {
        pagePath: '/pages/mine/mine',
        iconPath: '/images/icon/mine-unselected.png',
        selectedIconPath: '/images/icon/mine-selected.png',
        text: '我的',
      },
    ],
  },
  ready() {
    this.tabChange();
  },
  attached() {},
  methods: {
    switchTab(e) {
      const data = e.currentTarget.dataset;
      const url = data.path;
      wx.switchTab({ url });
    },
    tabChange() {
      const pages = getCurrentPages(); //获取加载的页面
      const currentPage = pages[pages.length - 1]; //获取当前页面的对象
      const url = currentPage.route; //当前页面url

      const list = this.data.list;
      let selected = 0;
      list.forEach((item, index) => {
        if (item.pagePath.indexOf(url) != -1) {
          selected = index;
        }
      });
      this.setData({
        selected: selected,
      });
    },
  },
});

index.wxml

相较官方文档,我把cover-view替换成了view,把cover-image替换成了image,因为我在滑动页面的时候发现底部会出现两个tab,所以将视图容器进行了更换。微信的官方文档也有关于cover-view替换成view的建议,可以查看cover-view的官方文档

<!--miniprogram/custom-tab-bar/index.wxml-->
<view class="tab-bar">
  <view class="tab-bar-border"></view>
  <view wx:for="{{ list }}" wx:key="index" class="tab-bar-item" data-path="{{ item.pagePath }}" data-index="{{ index }}" bindtap="switchTab">
    <image src="{{ selected === index ? item.selectedIconPath : item.iconPath }}"></image>
    <view style="color: {{ selected === index ? selectedColor : color }}">{{ item.text }}</view>
  </view>
</view>

index.wxss

.tab-bar {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  height: 48px;
  background: white;
  display: flex;
  padding-bottom: env(safe-area-inset-bottom);
  z-index: 99;
}

.tab-bar-border {
  background-color: rgba(0, 0, 0, 0.06);
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 1px;
  transform: scaleY(0.5);
}

.tab-bar-item {
  flex: 1;
  text-align: center;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
}

.tab-bar-item image {
  width: 24px;
  height: 24px;
}

.tab-bar-item view {
  font-size: 10px;
}

别忘了app.json中加入"custom": true,自定义底部tab才能生效

"tabBar": {
  "custom": true,
  "list": [
    {
      "pagePath": "pages/home/home",
      "text": "首页"
    },
    {
      "pagePath": "pages/mine/mine",
      "text": "我的"
    }
  ]
},

经过上面几步,一个基础的小程序就完成了。可以根据实际的开发需求进行功能开发了。

2.2 如何拥有一个功能齐全的小程序

2.2.1 用户微信信息授权

大多数时候,我们的产品经理希望开发获取用户的微信头像和昵称,用于小程序内的信息展示,前端会根据微信提供的API进行用户授权、信息展示、数据缓存等系列操作。今年4月份,微信对授权功能进行了调整,调整通知可见官方文档,使用新的wx.getUserProfile(可以参见官方文档)替换之前的wx.getUserInfo。

由于wx.getUserProfile需要用户操作进行授权,所以我的处理是获取授权之后,将数据进行缓存,避免需要用户频繁操作授权的不佳体验。如果页面有退出登录操作,还需要清除缓存数据。

mine.wxml

wxml文件中绑定getUserProfile方法

<view class="mine-header" bindtap="getUserProfile">
  <view class="mine-header-img">
    <image src="{{ userInfo.avatarUrl }}"></image>
  </view>
  <view class="mine-header-name">
    <text class="mine-header-nickname">{{ userInfo.nickName }}</text>
    <image class="mine-header-edit" src="../../images/mine/edit.png"></image>
  </view>
</view>
<view class="mine-content-btn">
    <button class="btn-gray" bindtap="loginOut">退出登录</button>
</view>

mine.js

1)getUserInfo方法是在页面加载时调用的,用户回显缓存中的数据;

2)getUserProfile方法是调取用户授权的操作,授权成功之后需要将授权数据进行缓存处理;

添加完这两个方法一个完整的授权处理就完成了。如果有退出功能,需要再退出的方法中清除缓存(这个具体功能视业务实际需求而定,我遇到的业务需求一般是需要清除授权信息缓存展示默认界面UI的)。

/**
   * 生命周期函数--监听页面加载
   */
 onLoad: function (options) {
  this.getUserInfo();
},
// 设置用户信息
getUserInfo() {
  let userInfo = wx.getStorageSync('userInfo') || {};
  if (JSON.stringify(userInfo) === '{}') {
    userInfo = {
      nickName: '可爱的网友',
      avatarUrl: '../../images/mine/defaultAvatarUrl.jpeg',
    };
  }

  this.setData({
    userInfo: userInfo,
  });
},
// 获取用户的授权信息
getUserProfile(e) {
  // 推荐使用wx.getUserProfile获取用户信息,开发者每次通过该接口获取用户个人信息均需用户确认,开发者妥善保管用户快速填写的头像昵称,避免重复弹窗
  wx.getUserProfile({
    desc: '展示用户信息', // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写
    success: res => {
      console.log(res, 'getUserProfile');
      const userInfo = res.userInfo;
      wx.setStorageSync('userInfo', userInfo);
      this.setData({
        userInfo: userInfo,
      });
    },
  });
},
// 退出登录
loginOut() {
  // 清除缓存,并回到首页
  wx.setStorageSync('userInfo', {});
  wx.switchTab({
    url: '../home/home',
  });
},

2.2.2 小程序内页面跳转

小程序内的页面跳转主要有两个方法wx.redirectTo和wx.switchTab,这两个的主要区别是wx.switchTab跳转到 tabBar 页面,而非tabBar 页面需要用wx.redirectTo进行跳转,且不能混用,混用会失效。

如果在一个公共的处理路由跳转的工具类方法中怎么区别使用两个方法呢?

我在以往的功能开发中加了switchTab的路由列表,如果检测出当前页面的路由在列表中则使用wx.switchTab,不在列表中则使用wx.redirectTo。这种处理一般在需要登录拦截的业务下会用到,比如某些页面需要登录才能查看到数据,比如旅游网站的酒店订单等,如果订单入口配在了公众号上面,那么跳转的时候小程序里面要做拦截处理,判断当前用户是否已登录,如果订单属于底部tab,那么就要使用wx.switchTab了。下面的代码就是我写的一个工具类:

/**
 * 公共跳转处理
 * @param {string} url 最终跳转链接
 * @return {void} 无
 */
const commonRedirectToNext = url => {
  /** @name 底部tab的路由 */
  const tabList = ['pages/mine/mine', 'pages/home/home'];
  // =>true: 属于底部tab的路由使用wx.switchTab
  if (tabList.filter(item => url.indexOf(item) !== -1).length) {
    wx.switchTab({
      url: '/' + url,
    });
  } else {
    wx.redirectTo({
      url: '/' + url,
    });
  }
};

2.2.3 动态更换页面标题

1.常规的页面标题

以首页pages/home/home为例,页面的标题在home.json中设置,navigationBarTitleText可以设置页面标题。可见官方文档页面配置项。

{
  "usingComponents": {},
  "navigationBarTitleText": "说走就走"
}

如下图为我为页面设置的标题:

image.png

2.动态设置页面标题

比如旅游的攻略文章,前端开发使用的同一个页面渲染不同的文章,标题需要动态的设置为文章标题,需要用wx.setNavigationBarTitle这个API设置页面标题。

我在页面中通过详情接口请求到详情数据,调用设置标题的setBarTitle方法设置页面标题:

/**
 * 生命周期函数--监听页面加载
 */
onLoad: function (options) {
  httpUtil.http(configUtil.travelDetailById, { id: options.id }, res => {
    this.setBarTitle(res.title);
    let detail = res;
    detail.announceTime = util.dateFormatter(res.announceTime || 0, 'yyyy-MM-dd');
    this.setData({
      detail: detail,
    });
  });
},
// 设置页面标题
setBarTitle(title) {
  wx.setNavigationBarTitle({
    title: title,
  });
},

对于旅游文章页,不同的文章,页面会展示不同的标题,下面两个图分别是故宫和青城山的跳转:

image.png

image.png

2.2.4 退出小程序

退出小程序有两种方式

1.navigator

navigator导航组件也可以实现退出小程序的功能,将open-type设置为exit,且配合target="miniProgram"时生效。官方文档

页面使用navigator,点击按钮会退出小程序,注意真机模拟才有效。

mine.wxml

<view class='mine-content-btn'>
  <navigator class='btn' open-type='exit' target='miniProgram'>
    退出登录
  </navigator>
</view>

2.wx.exitMiniProgram

wx.exitMiniProgram可以实现退出小程序公布,在点击事件中调用,不过这个方法需要基础库 2.17.3及以上才行。官方文档

使用很简单,成功之后会直接关闭小程序。注意真机模拟才有效。

mine.wxml

<view class='mine-content-btn'>
  <button class='btn-gray' bindtap='loginOut'>
    退出登录
  </button>
</view>

mine.js

// 退出登录
loginOut() {
  wx.exitMiniProgram({
    success: function (res) {
        // 成功之后关闭小程序
        console.log(res);
    },
  });
}

2.2.5 日期选择器

picker组件可以实现日期选择器的功能,可以设置可选的日期范围,只要设置开始和结束两个时间点即可,注意,时间范围在真机模拟中才生效。官方文档

如下图为我在小程序中加入的日期选择器的交互界面,我设置的是往前可选到两年前,往后只能选到当前月。

image.png

travelList.wxml

使用picker组件将事件点击区域包裹起来,这样点击就可以触发日期选择的弹窗,bindchange事件绑定的是进行日期选择之后的操作

<picker mode='date' fields='month' value='{{ date }}' start='{{ createStartTime }}' end='{{ createEndTime }}' bindchange='getDateTime'>
  <view class='head'>
    <view class='head-left'>{{ searchTimeText }}</view>
    <view class='arrow-icon'>
      <image src='../../images/icon/icon-arrow.png'></image>
    </view>
  </view>
</picker>

travelList.js

1)searchTimeText:页面展示的日期变量;

2)createStartTime:日期选择器可选的最早日期,格式为yyyy-MM;

3)createEndTime:日期选择器可选的最晚日期,格式为yyyy-MM;

4)getDateTime: 日期选择器的确定操作的回调方法,返回值格式为yyyy-MM。

const date = new Date();
var year = date.getFullYear();
var month = date.getMonth();
var minValue = new Date().getTime() - 2 * 365 * 24 * 60 * 60 * 1000;
var maxValue = new Date().getTime();

Page({
  /**
   * 页面的初始数据
   */
  data: {
    searchTimeText: `${year}${month + 1}月`, // 页面展示日期
    createStartTime: util.dateFormatter(minValue, 'yyyy-MM'), // 可选的最早日期
    createEndTime: util.dateFormatter(maxValue, 'yyyy-MM'), // 可选的最晚日期
  },
  // 选择日期
  getDateTime: function (e) {
    console.log('picker发送选择改变,携带值为', e.detail.value);
    let value = e.detail.value;
    let list = value.split('-');
    let syear = list[0];
    let smonth = list[1];
    // 重新请求列表数据并设置年份回显
    this.setData({
      searchTimeText: `${syear}${smonth}月`,
    });
  },
});

2.2.6 modal弹窗

1.基础的modal弹窗

wx.showModal可以实现modal弹窗,直接在点击事件中调用即可。官方文档

可以自定义弹窗标题、弹窗内容、成功回调处理、失败回调处理等

比如在我的旅游小程序中,用户跳转自己的游记页面,我需要判断这个用户有没有写过游记,如果没有需要给出提示。

mine.html

<view class="mine-content-item" bindtap="gotoTravel">
  <view class="content-item-label">
    <text>我的游记</text>
    <text class="ml5">({{ travelList.length }}篇)</text>
  </view>
  <view class="arrow-icon">
    <image src="../../images/icon/icon-arrow.png"></image>
  </view>
</view>

mine.js

// 跳转我的游记列表
gotoTravel() {
  if (this.data.travelList.length === 0) {
    wx.showModal({
      title: '温馨提示',
      content: '您暂时还没有发表游记,是否跳转热门游记?',
      success(res) {
        if (res.confirm) {
          wx.redirectTo({
            url: '../travelList/travelList?type=hot',
          });
        } else if (res.cancel) {
          console.log('用户点击取消');
        }
      },
    });
  } else {
    wx.redirectTo({
      url: '../travelList/travelList?type=self',
    });
  }
}

最终的弹窗提示如图:

image.png

2.自定义modal弹窗

wx.showModal提供的能力中,展示内容是字符串类型,如果我们需要展示的内容比较复杂,那么wx.showModal就无法满足我们的需求了,这个时候需要自定义弹窗。

我仿照掘金的展示模块,给我的旅游小程序加了一个个人成就的栏位,功能是展示文章被赞和被阅读的数量,交互如下:

image.png

image.png

实现的方案就是在wxml中把弹窗的布局写出来,通过wx:if进行内容是否展示的控制。

mine.wxml

<view class="mine-content-item" bindtap="remarkShow">
    <view class="content-item-label">个人成就</view>
    <view class="arrow-icon">
      <image src="../../images/icon/icon-arrow.png"></image>
    </view>
</view>
  ...
  <!-- 个人成就-弹窗 -->
  <view wx:if="{{ achievementShowFlag }}" class="remark-view">
    <view class="remark-view-background">
      <view class="remark-view-top">个人成就</view>
      <view class="remark-view-middle">
        <view class="item">
          <view class="label">文章被点赞:</view>
          <view class="text">{{ achievement.praise }}</view>
        </view>
        <view class="item">
          <view class="label">文章被阅读:</view>
          <view class="text">{{ achievement.view }}</view>
        </view>
      </view>
      <view class="remark-view-bottom">
        <view class="remark-view-cancel" bindtap="remarkClose">关闭</view>
      </view>
    </view>
  </view>

mine.wxss

.remark-view {
  height: 100vh;
  width: 100%;
  background: rgba(0, 0, 0, 0.5);
  z-index: 999;
  position: fixed;
  top: 0;
}
.remark-view-background {
  background: white;
  height: 30%;
  margin: 60% 48rpx 0;
  border-radius: 8rpx;
}
.remark-view-top {
  padding: 20rpx 32rpx 0;
  font-size: 36rpx;
  color: #0b1d32;
  text-align: center;
  font-weight: bold;
}
.remark-view-middle {
  margin: 40rpx 32rpx 0;
  height: 50%;
  border-radius: 8px;
  font-size: 32rpx;
  font-weight: 200;
}
.remark-view-middle .item {
  margin-bottom: 10rpx;
  display: flex;
  justify-content: space-between;
  justify-items: center;
}
.remark-view-blank {
  height: 1rpx;
  margin-top: 65rpx;
  background: #dfe2e4;
}
.remark-view-bottom {
  height: 80rpx;
  border-radius: 8px;
  font-size: 18px;
  width: 100%;
  border-top: 2rpx solid #f4f4f4;
}
.remark-view-cancel {
  line-height: 80rpx;
  color: #0b1d32;
  font-weight: 200;
  font-size: 30rpx;
  text-align: center;
}
.remark-view-confim {
  width: 50%;
  line-height: 80rpx;
  color: #0a73f5;
  font-weight: 200;
  font-size: 30rpx;
}

mine.js

1)设置两个参数分别是个人成就内容对象-achievement和人成就弹窗是否展示布尔值-achievementShowFlag;

2)两个方法,打开弹窗方法:remarkShow和关闭弹窗方法:remarkClose

/**
   * 页面的初始数据
   */
 data: {
  achievement: {}, // 个人成就
  achievementShowFlag: false, // 个人成就弹窗是否展示 默认-false
},

// 个人成就弹窗-打开
remarkShow() {
  this.setData({
      achievementShowFlag: true,
      achievement: {
          praise: 30,
            view: 1200,
        },
    });
},
// 个人成就弹窗-关闭
remarkClose() {
  this.setData({
    achievementShowFlag: false,
  });
}

2.2.7 拨打手机号

小程序拨打电话的API是wx.makePhoneCall,官方文档

值如果是固定的比如客服,可以定义全局的常量,方便统一维护,如果是需要从接口动态获取的,直接通过接口获取并赋值即可。

我再util文件下面新增了一个常量文件-constant.js,用于维护我的项目中的公共常量。

constant.js

定义全局的客服电话

/**
 * @description 公共常量
 */

/** @name 客服电话 */
export const servicePhone = '400-xxx-xxx';

mine.wxml

在需要拨打电话的页面,绑定唤起拨打电话功能的方法:phoneCall

<view class='mine-content-item' bindtap='phoneCall'>
  <text class='content-item-label'>联系客服(待开发)</text>
  <view class='arrow-icon'>
    <image src='../../images/icon/icon-arrow.png'></image>
  </view>
</view>

mine.js

const constant = require('../../utils/constant.js');

// 拨打客服电话
phoneCall() {
    const phoneNumber = constant.servicePhone;
    wx.makePhoneCall({
      phoneNumber: phoneNumber,
    });
},

2.2.8 多选

1.普通多选

官方提供了checkbox组件,支持单个多选框和多选组,官方文档

官方提供的示例基本把用法都列出来了

wxml文件

<view class="container">
  <view class="page-body">
    <view class="page-section page-section-gap">
      <view class="page-section-title">默认样式</view>
      <label class="checkbox">
        <checkbox value="cb" checked="true"/>选中
      </label>
      <label class="checkbox">
        <checkbox value="cb" />未选中
      </label>
    </view>
  
    <view class="page-section">
      <view class="page-section-title">推荐展示样式</view>
      <view class="weui-cells weui-cells_after-title">
        <checkbox-group bindchange="checkboxChange">
          <label class="weui-cell weui-check__label" wx:for="{{items}}" wx:key="{{item.value}}">
            <view class="weui-cell__hd">
              <checkbox value="{{item.value}}" checked="{{item.checked}}"/>
            </view>
            <view class="weui-cell__bd">{{item.name}}</view>
          </label>
        </checkbox-group>
      </view>
    </view>
  </view>  
</view>

js文件

Page({
  onShareAppMessage() {
    return {
      title: 'checkbox',
      path: 'page/component/pages/checkbox/checkbox',
    };
  },

  data: {
    items: [
      { value: 'USA', name: '美国' },
      { value: 'CHN', name: '中国', checked: 'true' },
      { value: 'BRA', name: '巴西' },
      { value: 'JPN', name: '日本' },
      { value: 'ENG', name: '英国' },
      { value: 'FRA', name: '法国' },
    ],
  },

  checkboxChange(e) {
    console.log('checkbox发生change事件,携带value值为:', e.detail.value);

    const items = this.data.items;
    const values = e.detail.value;
    for (let i = 0, lenI = items.length; i < lenI; ++i) {
      items[i].checked = false;

      for (let j = 0, lenJ = values.length; j < lenJ; ++j) {
        if (items[i].value === values[j]) {
          items[i].checked = true;
          break;
        }
      }
    }

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

展示效果

image.png

2.二维数组的多选

我的小程序里面用到多选的数据是一个二维数组,选择城市的页面,城市数据含城市首字母大写,我尝试之后发现checkbox组件对二维数组也是支持的

cityList.wxml

1)checkbox-group需要把整个数组包起来,才能正常获取选中项,我刚开始只把最里面的循环包起来了,发现拿到的选中项的数据不对。

<checkbox-group bindchange="checkboxChange" class="checkbox-box">
  <view class="weui-cells weui-cells_after-title" wx:for="{{ list }}" wx:key="index" wx:for-item="item" wx:if="{{ list.length > 0 }}">
    <text class="scroll-letter">{{ item.firstLetter }}</text>
      <label class="weui-cell weui-check__label scroll-item checkbox-item" wx:for="{{item.list}}" wx:key="cityIndex" wx:for-item="city">
        <view class="weui-cell__hd">
          <checkbox value="{{city.cityId}}" checked="{{city.checked}}"/>
        </view>
        <view class="weui-cell__bd">{{ city.cityName }}</view>
      </label>
  </view>
</checkbox-group>

cityList.js

1)list:城市列表二维数组,包含城市首字母大写

// pages/cityList/cityList.js
Page({
  /**
   * 页面的初始数据
   */
  data: {
    list: [
      {
        firstLetter: 'A',
        list: [
          {
            cityId: 3,
            cityName: '毕节市',
            provinceId: 3,
            provinceName: '贵州省',
          },
        ],
      },
      {
        firstLetter: 'B',
        list: [
          {
            cityId: 4,
            cityName: '安顺市',
            provinceId: 3,
            provinceName: '贵州省',
          },
        ],
      },
      {
        firstLetter: 'G',
        list: [
          {
            cityId: 5,
            cityName: '贵阳市',
            provinceId: 3,
            provinceName: '贵州省',
          },
        ],
      },
      {
        firstLetter: 'L',
        list: [
          {
            cityId: 6,
            cityName: '六盘水市',
            provinceId: 3,
            provinceName: '贵州省',
          },
        ],
      },
      {
        firstLetter: 'T',
        list: [
          {
            cityId: 7,
            cityName: '铜仁市',
            provinceId: 3,
            provinceName: '贵州省',
          },
        ],
      },
      {
        firstLetter: 'Z',
        list: [
          {
            cityId: 8,
            cityName: '遵义市',
            provinceId: 3,
            provinceName: '贵州省',
          },
        ],
      },
    ], // 城市列表
  },
});

展示:

image.png

3.全选、部分选中、删除已选项

在多选的基础上做全选、部分选中的功能是可行的,处理会复杂一些,由于文章篇幅的问题,我就不具体列出来了,文章底部我放置了我的小程序git地址,大家可以拉代码看,交互效果如下

1)部分选中

image.png

2)全选

image.png

2.2.9 自定义组件

组件化编程,重复功能组件化,复杂功能组件化拆分,都有利于代码的维护和提升开发效率,微信也提供了自定义组件的功能。官方文档

1.创建自定义组件

1)打开微信开发者工具,我把我的自定义组件都放到了component目录下,在该目录下右键点击,选新建Component,然后命名组件,命名成功之后就会生成组件文件结构,如下图,我命名的组件是stars;

2)类似于页面,一个自定义组件由 json、wxml、wxss、js 4个文件组成。

image.png

3)要编写一个自定义组件,首先需要在 json 文件中进行自定义组件声明(将 component 字段设为 true 可将这一组文件设为自定义组件):

{ "component": true }

2.开发自定义组件

Component 构造器可用于定义组件,调用 Component 构造器时可以指定组件的属性、数据、方法等。详细的参数含义和使用请参考 Component,参考文档

以我小程序中的评价功能为例。评价功能设计的交互为点了星星,界面展示如下,选择了三个城市,点亮了两个城市的评价最后一个没有点亮:

image.png

stars.wxml

星星使用的是图片,点亮和不点亮是两张不同的图片

<view class='stars-view'>
  <view class='flex' wx:for='{{ imgs }}' wx:key='id' wx:for-item='item' bindtap='_changeColor' data-index='{{ item.id }}'>
    <image class='canSelect ? big-star : litter-star' src='{{ item.id > starCount ? starNoSelect : starSelect }}'></image>
  </view>
</view>

stars.wxss

/* component/stars/stars.wxss */
.stars-view {
  display: flex;
  align-items: center;
}
.flex {
  display: flex;
  align-items: center;
}
.big-star {
  width: 44rpx;
  height: 44rpx;
  margin: 0px 8rpx;
  vertical-align: middle;
}

.litter-star {
  width: 36rpx;
  height: 36rpx;
  margin: 0px 4rpx;
  vertical-align: middle;
}

stars.js

1)properties:组件的对外属性,是属性名到属性设置的映射表;

2)methods:组件的方法,包括事件响应函数和任意的自定义方法,关于事件响应函数的使用,参见 组件间通信与事件。其中微信的建议是内部方法建议以下划线开头。

// component/stars/starts.js
Component({
  /**
   * 组件的属性列表
   */
  properties: {
    //是否可以点击
    canSelect: {
      type: Boolean,
      value: true,
    },
    //星星个数
    starCount: {
      type: Number,
      value: 5,
    },
    // 当前引用组件的子项的索引 当父组件是数组时有效
    index: {
      type: Number,
      value: 0,
    },
  },

  /**
   * 组件的初始数据
   */
  data: {
    imgs: [
      {
        id: 1,
      },
      {
        id: 2,
      },
      {
        id: 3,
      },
      {
        id: 4,
      },
      {
        id: 5,
      },
    ],
    starNoSelect: '/images/icon/star-noSelect.png',
    starSelect: '/images/icon/star-select.png',
  },

  /**
   * 组件的方法列表
   */
  methods: {
    // 更新星星颜色 内部方法建议以下划线开头
    _changeColor: function (e) {
      let starCount = e.currentTarget.dataset.index;
      this.setData({
        starCount: starCount,
      });
      let myEventDetail = {
        starCount: starCount,
        index: this.data.index,
      };
      this.triggerEvent('myevent', myEventDetail);
    },
  },
});

3.使用自定义组件

使用已注册的自定义组件前,首先要在页面的 json 文件中进行引用声明。此时需要提供每个自定义组件的标签名和对应的自定义组件文件路径:

activityFavorite.json

{
  "usingComponents": {
    "stars": "../../component/stars/stars"
  },
  "navigationBarTitleText": "评选喜爱城市"
}

在页面的 wxml 中就可以像使用基础组件一样使用自定义组件。节点名即自定义组件的标签名,节点属性即传递给组件的属性值。

activityFavorite.wxml

<view wx:for="{{ cityList }}" wx:key="index" wx:for-item="item" class="item">
  <view class="item-left">{{ item.cityName }}</view>
  <stars canSelect="{{ item.stars == 0 }}" starCount="{{ item.stars }}" bind:myevent="setStartCount" index="{{ index }}"></stars>
</view>

4.自定义组件见通信

组件间的基本通信方式有以下几种。

  • WXML 数据绑定:用于父组件向子组件的指定属性设置数据,仅能设置 JSON 兼容数据(自基础库版本 2.0.9 开始,还可以在数据中包含函数)。具体在 组件模板和样式 章节中介绍。
  • 事件:用于子组件向父组件传递数据,可以传递任意数据。
  • 如果以上两种方式不足以满足需要,父组件还可以通过 this.selectComponent 方法获取子组件实例对象,这样就可以直接访问组件的任意数据和方法。

上面的评价组件中,

1)子组件向父组件通信中,使用 triggerEvent 方法进行事件触发,每次操作评价点亮时,都会触发通信。

// 更新星星颜色 内部方法建议以下划线开头
_changeColor: function (e) {
  let starCount = e.currentTarget.dataset.index;
  this.setData({
    starCount: starCount,
  });
  let myEventDetail = {
    starCount: starCount,
    index: this.data.index,
  };
  this.triggerEvent('myevent', myEventDetail);
},

2)父组件通过监听拿到子组件传过来的值

// 事件监听
setStartCount: function (e) {
  let detail = e.detail; // 子组件通信传的值
  let cityList = [].concat(this.data.cityList);
  cityList[detail.index].stars = detail.starCount;
  this.setData({
    cityList: cityList,
  });
},

2.2.10 wxs

WXS(WeiXin Script)是小程序的一套脚本语言,结合 WXML,可以构建出页面的结构。我开始理解的它有点像filter过滤器,但是后面做了一些尝试,发现它的功能挺强大的。官方文档

1.类似filter过滤器用法

比如我想做一个颜色过滤器,某些特定文字展示蓝色,其余展示灰色,于是我在comm.wxs文件中写了一个方法

comm.wxs

/**
 * 设置详情页顶部右侧文案颜色
 * @param {string} text 右侧文字内容
 * @return {string} color 文字颜色
 */
function setRightTextColor(text) {
  var color = '#999';
  if (text && text.indexOf('发布') !== -1) {
    color = '#0a73f5';
  }
  return color;
}
module.exports = {
  setRightTextColor: setRightTextColor,
};

cityDetail.wxml

wxml文件中使用,需要先引入,我习惯外部文件放到底部,其实官方放到了顶部。这样一来detail.source的值中有发布二字的会展示蓝色

<wxs src="../../wxs/comm.wxs" module="comm" />
<view class="head">
    <view class="head-left">{{ detail.describe }}</view>
    <view class="head-right" style="color:{{comm.setRightTextColor(detail.source)}}">{{ script.getShowText(detail.source) }}</view>
  </view>

页面展示

1)蓝色

image.png

2)灰色

image.png

2.其他用法

wxs也可以直接在wxml文件里写功能,比如我想加一个默认文案展示的处理,当接口返回的数据中某个变量的值为空时,展示默认文案,可以直接wxml里面写一个方法,然后进行调用

cityDetail.wxml

<!-- wxs显示默认文案 -->
<wxs module="script">
  var getShowText = function(text, defaultText='内容摘自百度百科') {
    var showText = showText ? showText : defaultText;
    return showText;
  }
  module.exports.getShowText = getShowText;
</wxs>

<view class="container">
  <view class="banner">
    <view class="banner-box">
      <view class="head">
        <view class="head-left">{{ detail.describe }}</view>
        <view class="head-right" style="color:{{comm.setRightTextColor(detail.source)}}">{{ script.getShowText(detail.source) }}</view>
      </view>
    </view>
  </view>
</view>

2.2.11 开放能力

小程序开发需要对官方提供的开放能力做一定调研,比如某些功能只有非个人开发者才能使用,如:获取手机号;比如某些功能是特定基础库之后才有的,如小程序加密网络通道功能是从基础库 2.17.3 开始支持的;某些功能需要在小程序管理后台申请,比如分享数据到微信运动,需要到小程序管理后台,「开发」-「接口设置」中自助开通该组件权限。 只针对「体育-在线健身」类目的小程序开放。

所以要关注小程序开放能力以及某些功能的调整。官方文档

2.3 如何解决小程序开发遇到的问题

2.3.1 导航条不展示

我在app.josn里加上导航条之后,如下为导航条代码

"tabBar": {
  "selectedColor": "#007AF5",
  "list": [
    {
      "pagePath": "pages/home/home",
      "text": "首页",
      "iconPath": "/image/icon/home-unselected.png",
      "selectedIconPath": "/image/icon/home-selected.png"
    },
    {
      "pagePath": "pages/mine/mine",
      "text": "我的",
      "iconPath": "/image/icon/mine-unselected.png",
      "selectedIconPath": "/image/icon/mine-selected.png"
    }
  ]
},

发现导航条无法正常展示页面路径也空了,还报了一个错误,如下截图:

image.png

image.png

我尝试把导航条内容删除,页面就正常了,于是我推测是iconpath的问题,果然我的文件夹名字是images但是这里我写成了image,改完重新编译,页面就能正常显示了

image.png

因为导航条这里的代码并不是很多,遇到问题可以首先考虑iconPath的路径是否正确。

三、总结

个人小程序git地址:https://github.com/wxmp-project/wxmp-travel

目前功能开发的还比较简单,后续会持续更新,文章也会持续更新,因为还有些功能由于还在摸索着,所以暂时没加。

虽然,每一次的开发,酸甜苦辣咸,五味俱全。曾为一个功能熬到过凌晨7点,也曾一边哭着一边敲代码。但是,喜也好,哀也罢,抹把脸,明天是个好天气。