乐趣区

从入门到撞门手把手教你开发一款记录类小程序

最近开发了一款记录类的小程序“我我”,决定把这段时间里学到的知识由浅到深的总结一下,分享给大家,如有不对之处还望大佬指出。

目录

  • 小程序的申请
  • 开发者工具的使用
  • 小程序的开发
  • 统计
  • 发布
  • 进阶

1. 小程序的申请

小程序的申请流程可以照着官网的教程走,需要注意的是,后台的设置里,很多项目一个月只能改 3 / 5 次,开发 –> 开发设置 –> 服务器域名一个月也只能改 5 次,所以建议提前规划好要修改的内容,比如服务器域名,应该提前把接口地址、图片资源地址、统计打点地址等域名统一填好(审核有延迟,一般为 1 小时左右)。

另外,申请的账号邮箱最好不要用个人邮箱,建议专门注册一个产品用的,不然这个同事离职了的话账号处理会很蛋疼。

2. 开发者工具的使用

右上角详情里,可以把这些选项都点上,如果有使用 npm 模块,则把那个选项也点上。如果有域名还未申请或还未通过审核,可以把最后一个选项点上,就可以跳过白名单检查。

如果有使用 npm 模块,还需要点击工具 –> 构建 npm,生成一个 miniprogram_npm 文件。

如果有用到 shell 命令去控制开发者工具,则需要把设置 –> 安全设置 –> 安全里的服务端口开启,至于如何用 shell 命令,以及用它的好处是什么,让我们放后面慢慢聊。

3. 小程序的开发

3.1 小程序文件结构

项目
├── components   // 组件
├── node_modules // 项目依赖
├── miniprogram_npm
├── page         // 页面
├── images
├── .gitignore
├── app.json
├── app.wxss
├── app.js
├── package.json
├── project.config.json
└── README.md

3.2 自定义 topbar 的开发

因为“我我”项目需要自定义 topbar,所以需要解决这些问题:

获取胶囊到手机顶部的距离,使返回按钮和标题能自适应与胶囊处于同一行。

wx.getSystemInfo({success: function(res) {
    that.globalData.comPostInfo = res;
    if (res.system.indexOf('iOS') > -1) {
      that.globalData.system = 'ios';
      that.globalData.paddingTop = res.statusBarHeight + 6;
    } else {
      that.globalData.system = 'android';
      that.globalData.paddingTop = res.statusBarHeight + 10;
    }
  }
});

通过 wx.getSystemInfo 的 statusBarHeight 能获取到胶囊到手机顶部的距离,再根据需要加上几像素令文字与胶囊居中。

另外,返回按钮并不是所有情况都该出现,当用户处于一级页面,或者通过二维码进入时,这时候就不该有返回按钮了(如果是通过二维码等方式进入,还应该加上一个返回首页的按钮),这里我们这样判断一下:

let that = this;
let tabs = ['pages/index/index', 'pages/find/find', 'pages/msg/msg', 'pages/me/me'] //“我我”里的 4 个一级页面
let pages = getCurrentPages();
for (let i = 0; i < pages.length; i++) {let page = pages[i];
  if (tabs.indexOf(page.route) > -1) {break;} else {
    that.setData({show: true});
  }
}

通过 getCurrentPages 来获取当前页面列表进而判断是否有一级页面存在。

顺带一提,现在使用自定义 topbar 会有个 bug,就是安卓机首次分享的时候会获取不到截屏照片,得点一次分享然后取消再点分享才能有截屏照片,要解决的话只能自己用 canvas 去生成一张分享图或者干脆不用自定义 topbar。(补充:安卓最新的更新已经解决这个 bug 了。)

3.3 tabbar 的一些使用技巧

“我我”tabbar 的图标使用 176×176 的大小,有个要注意的点是,图片内容不要撑满整个图片,这样会导致 tabbar 的图标显得很大,最好是在图标周围留下一圈空白,这样图标的大小就刚好合适了。

另外,之前还做过一个换肤功能,可以改变图标以及文字的样式、颜色,可以这样实现:

wx.setTabBarItem({
  index: 0,
  iconPath: `/images/focus${str}.png`,
  selectedIconPath: `/images/focus-on-${color}.png`
});
wx.setTabBarStyle({
  selectedColor: colorStr,
  color: '#767477',
  backgroundColor: '#ffffff'
});

3.4 首页数据缓存

为了使用户每次进入首页时不会出现白屏,我在每次用户拉去首屏数据的时候,把最新的数据缓存在本地,用户下次打开小程序的时候,会先读取本地数据,如果拉取到新的数据,再进行一次渲染,这样白屏时间就会大大减少:

that.data.list = wx.getStorageSync('firstIndexData').list || []; // 渲染本地数据
....
util.fetch({
  url: url,
  type: 'get',
  cb: function(e) {if (that.data.pageCount === 0) {wx.setStorageSync('firstIndexData', e.data); // 存储数据
    }
    cb(e);
    that.setData({
      num: e.data.total,
      hideNum: e.data.total,
      hasMore: !!(e.data.more / 1)
    });
  }
});

3.5 图片居中渲染

在渲染数据的时候,需要让各自尺寸的图片居中自适应,在小程序里有两种方法,一是直接用小程序的 image 标签:

<image class="item-img" lazy-load="true" mode="aspectFill" src="{{itemData.user.avatar}}"></image>

或者是通过将图片作为背景:

<view class="img-item" style="background-image: url({{item.url}})" data-index="{{index}}" catchtap="showImg"></view>

.card-item .item-card-imglist .img-item {
  position: relative;
  padding-top: 100%;
  background-position: 50% 50%;
  background-repeat: no-repeat;
  background-size: cover;
  border-radius: 12rpx;
}

3.6 用户登录状态判断

因为是记录型小程序,要保证用户是处于登录状态才能让用户去获取相关的数据,所以要随时对用户的登录状态进行判断,这是小程序官方的登录流程图:

需要一个 session 来判断用户的登录是否过期,而 session 的获取则是需要 wx.login 来获取几个参数,向后端发起请求获取 session:

function getLoginCode() {return new Promise(function (resolve, reject) {
    wx.login({success: function (res) {resolve(res);
      },
      fail: function (e) {reject(e);
      }
    });
  });
}

function getLogin(opt) {
  wx.request({
    url: opt.url,
    method: 'post',
    data: opt.data,
    success: function (res) {console.log('login:' + res.data.code);
      if (res.data.code === 0) {opt.cb(res.data);
      } else {checkSession(opt);
      }
    }
  });
}

function checkSession(opt) {getLoginCode().then(e => {
    wx.getUserInfo({
      success: res => {
        getLogin({
          url: ......,
          data: {
            code: e.code,
            encryptedData: res.encryptedData,
            iv: res.iv,
            signature: res.signature
          },
          cb: function(e) {if (e) {opt.cb(e);
            }
          }
        });
      }
    });
  });
}

接着便可通过 wx.checkSession 来判断是否已过期,不过“我我”的处理方式是前端每次请求接口后,后端直接返回结果,若未过期,则返回正常数据,若已过期,则返回相应的错误码,前端再去通过 wx.login 重新获取 session。

3.7 用户授权

而要让用户登录,我们得先获取用户的信息,这就需要用户的授权了,现在小程序不支持直接弹出授权框让用户授权,需要用户点击 button 标签后才会弹出,“我我”是通过一个专门的欢迎页来引导用户授权:

<button class="welcome-login-btn" open-type="getUserInfo" lang="zh_CN" bindgetuserinfo="onGotUserInfo"></button>

除了获取信息的授权,“我我”还用到了保存图片到相册的权限,这个在后面海报部分会讲到。

3.8 实现一个输入框

既然是个记录类的小程序,自然会有评论功能,方便他人对你的记录做出评价,我们就需要实现一个评论框,一开始我用的是 input 标签,但只能显示一行的 input 并不能满足想进行大段评论的人的需求,故改为 textarea 标签:

<textarea auto-height adjust-position="{{false}}" name="commentInput" class="detail-input" value="{{inputVal}}" show-confirm-bar="{{false}}" fixed focus maxlength="500" bindfocus="inputFocus" bindinput="inputChange" placeholder="{{placeholder ? placeholder :' 输入你想说的话吧 '}}"></textarea>

小程序很多自带的功能还是很实用,textarea 的 auto-height 能支持评论框在内容变多时自动增高,show-confirm-bar 能选择是否隐藏键盘上面完成那一栏,但 adjust-positon 这个功能就有点问题了,在我使用的时候,它会导致用户点击输入框时,textarea 会有一段被键盘挤上去的动画,有的安卓机上 textarea 还会直接不被挤上来,导致用户无法输入,所以我们需要找个办法替代这个功能。

首先让 adjust-positon=false,关闭它,然后通过调起键盘时,获取键盘的高度,并把输入框的 bottom 设为这个高度,就能实现聚焦时输入框自动上浮了:

inputFocus: function(e) {
  this.setData({inputBottom: e.detail.height || 0});
},
inputBlur: function(e) {
  this.setData({inputBottom: 0});
}

另外,为了用户更好的体验,我们应该在用户已在输入框内输入内容,没有提交却关闭了输入框时,帮用户将未输入完的内容对应好 id 保存在本地,下次用户再打开这篇文章想要评论时,能直接展示出之前未写完的内容:

let cmtStoreId = `${that.data.feedid}-${id}`; // feedid + 评论 id 来用于定位
let cmtStoreObj = wx.getStorageSync('cmtStoreId') || {};
let oldVal = cmtStoreObj[cmtStoreId] || '';

that.setData({
  inputVal: oldVal,
  submitDisabled: !util.trim(oldVal),
  placeholder: e.detail.name ? '回复' + e.detail.name + ':' : '',
  inputShow: true
});

3.9 分享一张海报

终于到海报了,其实海报对于一个小程序还是很重要的,因为小程序的分享只能通过分享给好友或者制作一张带有二维码的海报,所以我们来看一看制作海报时会有哪些知识点吧:

既然是海报,肯定是要包含二维码的,而要如何让二维码是指定的页面并带上参数呢?这就需要我们把对应的参数传给后端,让后端去调用接口生成一个二维码,而我们的参数则写在一个 scene 里

let scene = encodeURIComponent('feedid=' + that.data.detail.id);
let pageStr = that.data.detail.id ? 'pages/detail/detail' : '';
that.data.qrCode = util.getQrCode(scene, pageStr) || '/images/share-default.jpg';
....
function getQrCode(scene, page) {
  let pageStr = page ? 'page=' + page : '';
  return `${g.rootUrl}/v1/user/orcode?${pageStr ? pageStr + '&' : ''}scene=${scene}`;
}

当用户通过扫二维码进入时,这个页面的 onLoad 的参数里会有个 scene(注意,二维码只有项目上线后才会生效,无法在小程序未上线时测试)。

制作海报有许多功能可以作为通用方法使用,比如多行文字渲染、头像制作、图片自适应,下面和大家分享一下我写的图片自适应的方法:

/**
 * 功能:让图片居中自适应
 * canvasBuild
 * scale
 * res: 
 * url: 图片地址(应是本地地址)* dx: 图像的左上角在目标 canvas 上 x 轴的位置
 * dy: 图像的左上角在目标 canvas 上 y 轴的位置
 * sWidth: 源图像的矩形选择框的宽度,选填
 * sHeight: 源图像的矩形选择框的高度,选填
 * dWidth: 在目标画布上绘制图像的宽度,允许对绘制的图像进行缩放
 * dHeight: 在目标画布上绘制图像的高度,允许对绘制的图像进行缩放
 * cb: callback
**/
function autoDrawImage(canvasBuild, scale, res) {
  let opt = {
    url: res.url,
    dx: res.dx,
    dy: res.dy,
    sWidth: res.sWidth || 0,
    sHeight: res.sHeight || 0,
    dWidth: res.dWidth,
    dHeight: res.dHeight,
    cb: res.cb
  };

  if (!opt.url) {return;}

  let backData = function(width, height) {
    let imgProp = width / height;
    let prop = opt.dWidth / opt.dHeight;
    if (imgProp > prop) {
      // 原图比例更宽
      opt.sHeight = height;
      opt.sWidth = height * prop;
      opt.sy = 0;
      opt.sx = (width - opt.sWidth) / 2;
    } else {
      // 原图比例更高
      opt.sWidth = width;
      opt.sHeight = width / prop;
      opt.sx = 0;
      opt.sy = (height - opt.sHeight) / 2;
    }
    opt.cb(opt);
  };

  if (opt.sWidth && opt.url !== '/images/avatar-default.png') {backData(opt.sWidth, opt.sHeight);
  } else {
    wx.getImageInfo({
      src: opt.url,
      success: function(res) {backData(res.width, res.height);
      }
    });
  }
}

以及将海报保存至相册:

/**
 * 功能:储存到相册
 * storeImgPath: 图片地址
 * successCb: 成功后的 callback
**/
function savePhotosAlbum(storeImgPath, successCb) {setTimeout(function() {
    wx.saveImageToPhotosAlbum({
      filePath: storeImgPath,
      success: function(res) {successCb(res);
      },
      fail: function(res) {console.log('fail:' + res);
        wx.showToast({
          title: '保存相册失败',
          icon: 'none',
          duration: 2000
        });
      }
    })
  }, 500);
}

这是最后制作出来的海报效果:

顺带一提,安卓下(没错又是安卓,安卓的坑是真的多),如果直接调用 canvasBuild.draw()渲染 canvas,会出现样式混乱的情况,所以需要一个 setTimeout 来执行 canvasBuild.draw()。

3.10 模板消息(暂未上线)

当用户收到点赞、评论的时候,可以给用户发送消息提醒,这就是小程序的模板消息。

3.11 内容审核

用户能自己发内容的话,是需要做内容审核的,不然会被打回(即使通过了,你也不敢让人用吧,有心人发点不和谐的内容就凉了),“我我”用的是腾讯新闻的内容审核平台,一般的话可以直接用小程序自带的内容审核接口,缺点就是没有后台来看和操作用户发的消息,全靠小程序处理。

另外,“我我”对内容的处理一开始是“先审后发”,但发现这样做可能会导致用户辛辛苦苦发的内容突然就被和谐没了,而且点了发布后要等审核过了才看得到发的内容体验也不好,所以我们后来改成了“先发后审”,用户在发布后,这条消息的状态会默认是仅用户自己可见,如果内容有问题,会在通知处告诉用户这条内容不符合规范,这样就能避免刚才的问题,用户体验会好上很多。

4. 统计

小程序官方是自带有统计的,但很多需要统计的功能却没法支持,而且只能在小程序上线后才能看到,不方便上线前的测试,所以“我我”选择了再引入另外的统计工具:mta:

mta:优点是多了分享、下拉刷新、页面触底的统计,还能查看用户轨迹、停留时长等等,其他小程序自带统计有的功能 mta 也都有,算是官方统计的加强版,而且不用小程序上线就能进行统计。

5. 发布

这时候,小程序的功能基本开发的差不多了,如果测试没有问题,就可以准备上线了,小程序一共有三种版本:开发版、体验版、正式版:

  • 开发版:在开发者工具上开发时用的版本,一般通过开发者工具的预览生成的二维码进入;
  • 体验版:点击开发者工具里的上传后,会将项目上传至后台,生成一个体验版,体验版二维码长期有效,每次更新体验版本地会去自动更新最新版本,不用重新扫二维码,体验版只能项目成员和体验成员使用,体验成员最多 90 个,可以看官方文档;
  • 正式版:在确认没问题后,可以将体验版提交审核,审核通过后就能发布正式版。

首次审核的项目可能会审核很严格,可能会被多次打回,根据提示改好后重新提交便可,一般第一次审核会久一点,要 2 天左右,后面每次上线的审核可能就在 1 天左右了。

另外,最近小程序新增了个小程序评测,如果性能和用户评测都是优秀,则可以进行快速审核,几小时内就能完成审核。

6. 进阶

我们在 web 开发过程中,都会或多或少遇到过一些非常规需求,需要巧妙的应用我们所掌握的技术那不为人知的一面,这类奇技淫巧,我们称之为黑魔法。awesome-blackmagic 项目将为 web 开发人员定期分享这些神奇的技术应用。

欢迎大家来围观~

广告时间

大家要是对“我我”小程序感兴趣,可以来围观呀,项目还在初始阶段,后面会慢慢增加更多有趣的内容的,么么哒~

退出移动版