写点什么

「工作小记」小程序开发的喜怒哀乐

作者:叶一一
  • 2022 年 9 月 06 日
    北京
  • 本文字数:10036 字

    阅读完需:约 33 分钟

「工作小记」小程序开发的喜怒哀乐

前言

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

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

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

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

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

如何拥有一个小程序

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

新建小程序

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

新增页面

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


实际的文件结构如下图:


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

"pages": [    "pages/home/home"  ],
复制代码

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

底部 tab 栏

基础底部 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"
}
]
},
复制代码

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


自定义底部 tab 栏

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

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

custom-tab-bar/index.js 
custom-tab-bar/index.json
custom-tab-bar/index.wxml
custom-tab-bar/index.wxss
复制代码

目录结构如下:


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": "我的"
}
]
},
复制代码

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

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

用户微信信息授权

大多数时候,我们的产品经理希望开发获取用户的微信头像和昵称,用于小程序内的信息展示,前端会根据微信提供的 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

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

  • 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',
});
},
复制代码

小程序内页面跳转

小程序内的页面跳转主要有两个方法 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,
});
}
};
复制代码

动态更换页面标题

常规的页面标题

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

{
"usingComponents": {},
"navigationBarTitleText": "说走就走"
}
复制代码

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


动态设置页面标题

比如旅游的攻略文章,前端开发使用的同一个页面渲染不同的文章,标题需要动态的设置为文章标题,需要用 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,
});
},
复制代码

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



退出小程序

退出小程序有两种方式

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>
复制代码

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);
},
});
}
复制代码

日期选择器

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

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


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

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

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

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

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}月`,
});
},
});
复制代码

modal 弹窗

基础的 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',
});
}
}
复制代码

最终的弹窗提示如图:


自定义 modal 弹窗

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

我加了一个个人成就的栏位,功能是展示文章被赞和被阅读的数量,交互如下:



实现的方案就是在 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

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

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

/**
* 页面的初始数据
*/
data: {
achievement: {}, // 个人成就
achievementShowFlag: false, // 个人成就弹窗是否展示 默认-false
},
// 个人成就弹窗-打开
remarkShow() {
this.setData({
achievementShowFlag: true,
achievement: {
praise: 30,
view: 1200,
},
});
},
// 个人成就弹窗-关闭
remarkClose() {
this.setData({
achievementShowFlag: false,
});
}
复制代码

拨打手机号

小程序拨打电话的 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,
});
},
复制代码


wxs

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

类似 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>
复制代码

页面展示

蓝色


灰色


其他用法

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.17.3 开始支持的;某些功能需要在小程序管理后台申请,比如分享数据到微信运动,需要到小程序管理后台,「开发」-「接口设置」中自助开通该组件权限。 只针对「体育-在线健身」类目的小程序开放。

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

总结

个人小程序 git 地址:wxmp-travel

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

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

发布于: 刚刚阅读数: 2
用户头像

叶一一

关注

苍生涂涂,天下缭燎,诸子百家,唯我纵横。 2022.09.01 加入

非职业传道受业解惑前端程序媛,华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。

评论

发布
暂无评论
「工作小记」小程序开发的喜怒哀乐_小程序_叶一一_InfoQ写作社区