9

本文将对微信小程序从开始开发到发布上线的详细过程进行讲解,欢迎大家加入微信群交流讨论,点击获取入群二维码

《超级颜值计算器》小程序源码托管在Github上,可自行clone修改: https://github.com/dreamans/B...

开发前的准备工作

开发前准备工作请参考以下文章:

微信小程序开发: 开发前准备工作
微信小程序开发: 小程序源文件结构及含义

小程序预览

小程序名称:《超级颜值计算器》
小程序码:
图片描述

UI预览:
图片描述图片描述

账号申请

注册小程序

注册小程序, 注册成功后进入邮箱查找激活邮件,如实填写相关信息,如下:

图片描述
图片描述

设置小程序信息

注册成功后会进入小程序发布流程,按要求设置小程序相关信息即可,如下:

图片描述

AppID&&AppSecret

设置 - 开发设置 中获取小程序的AppID和AppSecret

其他接口申请

小程序依赖face++的图像面部识别接口,小伙伴可到其官网上自行申请账号,申请地址接口文档地址

注册成功后登录face++的Console平台,进入应用管理 - API Key 获取接口所需的key和secret。

图片描述

有一点问题需要注意,免费版用户被限制了调用并发量,会经常出现 CONCURRENCY_LIMIT_EXCEEDED 并发数超过限制 错误。

开始开发

开发者工具

运行 开发者工具, 选择 小程序项目,填写项目目录、AppID(mp平台中获取)、项目名称等。

图片描述

小程序配置文件

在根目录下创建小程序配置文 app.json,并配置相关信息

小程序页面列表

每一项代表一个页面,第一项代表程序的初始页面,此处我们设置了两个页面,分别是:

pages/index/index 程序主页面
pages/reward/reward 程序打赏页面

"pages": [
    "pages/index/index",
    "pages/reward/reward"
],

window 设置

接下来我们来设置小程序的导航栏样式和标题名称等window属性:

"window": {
    "navigationBarTitleText": "超级颜值计算器",
    "navigationBarBackgroundColor": "#ff8c00",
    "navigationBarTextStyle": "white",
    "navigationStyle": "default",
    "backgroundColor": "#eeeeee",
    "backgroundTextStyle": "light",
    "enablePullDownRefresh": false
},

tabBar 设置

此次我们开发的小程序是一个多tab的应用,以下是tabBar的设置信息:

"tabBar": {
    "color": "#9ca0a3",
    "selectedColor": "#00ae66",
    "list": [
        {
            "pagePath": "pages/index/index",
            "text": "首页",
            "iconPath": "asset/icon/home.png",
            "selectedIconPath": "asset/icon/home_selected.png"
        },
        {
            "pagePath": "pages/reward/reward",
            "text": "打赏",
            "iconPath": "asset/icon/reward.png",
            "selectedIconPath": "asset/icon/reward_selected.png"
        }
    ]
},

其他配置信息请见源文件

全局样式设置

在根目录下创建全局样式文件 app.wxss,会作用于当前小程序的所有页面。

当前小程序所使用的全局样式:

.icon-arrow:after{
    content: "\2715";
}
.icon-love:after {
    content: "\2740";
}
.icon-male:after {
    content: "\2642";
}
.icon-female:after {
    content: "\2640";
}

全局注册小程序函数

小程序启动之后,在 app.js 中执行App函数用来注册一个小程序,onLaunch回调会在小程序初始化完成时触发,并且全局只触发一次。

在根目录下创建 app.js

App({
    onLaunch() {
        let self = this;
        wx.getSystemInfo({
            success: function (res) {
                self.globalData.screenWidth = res.windowWidth;
                self.globalData.screenHeight = res.windowHeight;
            }
        });
    },
    globalData: {
        screenWidth: null,
        screenHeight: null
    }
});

代码说明:

小程序在初始化完成后会调用微信获取系统信息API(wx.getSystemInfo) ,获取窗口的宽高,并存放到globalData属性中,供其他page使用。

主页面开发

文件列表:
pages/index/index.js 核心js业务逻辑
pages/index/index.wxml 模板文件
pages/index/index.wxss 样式文件
pages/index/index.json (暂未用到)

创建视图

创建 pages/index/index.wxml 文件,代码如下:

<view class="container">
    <view class="pic-info" style="width: {{ picSelfAdaptWidth }}vw; height: {{ picSelfAdaptHeight }}vh">
        <image wx:if="{{ !testPicFile }}" class="pic-none" src="../../asset/icon/pic_bg.png"></image>
        <image wx:else class="pic-inner" src="{{ testPicFile }}"></image>
    </view>
    <view wx:if="{{ testPicFile && !testPicResult }}" class="close" bindtap="handleCancelPic">
        <i class="arrow">×</i>
    </view>
    <view class="pic-result" wx:if="{{ testPicResult }}">
        <view class="score-box"><i class="icon-love"></i> 颜值 <span class="score">{{ testPicResult.beauty }}分</span>, 击败 {{ testPicResult.defeat}}% 用户</view>
        <view class="score-ext">
            <span wx:if="{{ userInfo }}">用户: {{userInfo.nickName}}</span> 
            <span>性别: <i class="icon-{{ testPicResult.gender }}"></i></span>
            <span>年龄: {{ testPicResult.age }}</span>
        </view>
    </view>
    <view>
        <button wx:if="{{ !testPicFile }}" class="btn btn-select" bindtap="handleUploadPic">选择照片/拍照</button>
        <button wx:if="{{ testPicFile && !testPicResult }}" class="btn btn-compute" open-type="getUserInfo" bindgetuserinfo="handleGetUserInfo">计算颜值</button>
        <button open-type="share" wx:if="{{ testPicResult }}" class="btn btn-share">邀请好友来玩</button>
        <button wx:if="{{ testPicResult }}" class="btn btn-bottom" bindtap="handlePlayAgain">再试一次</button>
    </view>
</view>
变量说明

picSelfAdaptWidth 为图片容器显示宽度,默认为 70vw

picSelfAdaptHeight 为图片容器显示高度,根据图片真实宽高和默认宽度计算得来,具体计算方法见 源代码

testPicFile 为用户选取的照片临时地址;

testPicResult 为照片识别结果集对象,所包含的属性有 beauty:颜值(0-100), defeat: 击败用户百分比, gender: 性别, age: 年龄

userInfo 为用户基本信息,包括 nickName: 用户昵称;

事件说明

handleUploadPic 点击选取照片时触发的事件;

handleGetUserInfo 点击计算颜值时触发的事件;

handlePlayAgain 点击再试一次时触发的事件;

handleCancelPic 选取完照片后点击右上角红叉时触发的事件;

创建样式

创建 pages/index/index.wxss 文件,代码如下:

page {
    background-color: #ff8c00;
}
.container {
    height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: space-between;
    box-sizing: border-box;
}
.pic-info {
    margin-top: 6vh;
    border: solid #fff;
    border-width: 30rpx 30rpx 30rpx 30rpx;
    box-shadow: 10rpx 10rpx 15rpx #333;
    background: #fff;
    display: flex;
    align-items: center;
    justify-content:center;
    border-radius: 20rpx;
}
.btn {
    background: #dd5900;
    color: #fff;
    border: 1px solid #fff;
}
.btn-select {
    margin-top: 80rpx;
}
.btn-compute {
    margin-top: 50rpx;
    margin-bottom: 50rpx
}
.btn-share {
    margin-top: 50rpx;
}
.btn-bottom {
    margin-top: 30rpx;
    margin-bottom: 50rpx;
}
.pic-none {
    width: 90%;
    height: 90%;
}
.pic-inner {
    width: 100%;
    height: 100%;
}
.pic-result {
    display: flex;
    flex-direction: column;
    align-items: center;
    margin-top: 50rpx;
    color: #fff;
}
.pic-result .score-box{
    font-size: 34rpx;
}
.pic-result .score {
    font-size: 50rpx;
}
.pic-result .score-ext {
    margin-top: 30rpx;
    font-size: 28rpx;
}
.pic-result .score-ext span {
    margin-right: 30rpx;
}
.pic-result .score-box {
    background: #fff;
    padding: 10rpx 20rpx;
    border-radius: 20rpx;
    color: #dd5900;
}
.close {
    position: absolute;
    top: 7vh;
    right: 12vw;
}
.close .arrow {
    display: flex;
    align-items: center;
    justify-content:center;
    font-size: 38rpx;
    width: 50rpx;
    height: 50rpx;
    border-radius: 25rpx;
    background: #dd5900;
    color: #fff;
}

创建视图JS逻辑文件

创建 pages/index/index.js 文件,代码如下:


const app = getApp()
const picDefaultWidth = 50;
const picDefaultHeight = 30;
const picTestFileDefaultWidth = 70;

Page({
    onShareAppMessage(res) {
        return {
            title: '我的颜值击败了全国 94% 的用户,不服来战!',
            path: '/pages/index/index',
            imageUrl: '../../asset/img/prview.png'
        }
    },
    data: {
        picSelfAdaptWidth: picDefaultWidth,
        picSelfAdaptHeight: picDefaultHeight,
        testPicFile: '',
        testPicResult: null,
        userInfo: null
    },
    handleGetUserInfo(e) {
        this.setData({
            userInfo: e.detail.userInfo
        });
        this.handleComputePic();
    },
    handleCancelPic() {
        this.setData({
            testPicFile: '',
            testPicResult: null,
            picSelfAdaptHeight: picDefaultHeight,
            picSelfAdaptWidth: picDefaultWidth
        });
    },
    handleUploadPic() {
        let self = this;
        let ret = wx.chooseImage({
            count: 1,
            sizeType: "compressed",
            success: function(res) {
                self.setData({
                    testPicFile: res.tempFiles[0].path
                });
                self.getImageInfo(res.tempFiles[0].path, function(res) {
                    self.setPicAdaptHeight(res.width, res.height);
                });
            }
        });
    },
    handlePlayAgain() {
        this.setData({
            testPicFile: '',
            testPicResult: null,
            picSelfAdaptHeight: picDefaultHeight,
            picSelfAdaptWidth: picDefaultWidth
        });
    },
    handleComputePic() {
        let self = this;
        wx.showLoading({
            title: "颜值计算中",
            mask: true
        });
        wx.uploadFile({
            url: 'https://api-cn.faceplusplus.com/facepp/v3/detect', //仅为示例,非真实的接口地址
            filePath: self.data.testPicFile,
            name: 'image_file',
            formData: {
                'api_key': 'DVc8JblEbcBjgq55TtDW0sheUhBeCaGe',
                'api_secret': 'lMUVhSAg_ruN4PmwgNCk0IiWPNAF2_Sr',
                'return_attributes': 'gender,age,beauty'
            },
            success: function (res) {
                if (res.statusCode != 200) {
                    wx.showToast({
                        title: '服务器被挤爆了,请稍后再试',
                        icon: 'none',
                        duration: 2000
                    });
                    return false;
                }
                let data = JSON.parse(res.data);
                console.log(data)
                console.log(res)
                if (!data.faces || data.faces.length == 0) {
                    wx.showToast({
                        title: '没有识别到人脸,请更换照片重试',
                        icon: 'none',
                        duration: 2000
                    });
                    return false;
                }
                let human = [];
                data.faces.forEach(item => {
                    let beauty = 50;
                    if (item.attributes.gender.value == 'Male') {
                        beauty = item.attributes.beauty.female_score;
                    } else {
                        beauty = item.attributes.beauty.male_score;
                    }
                    human.push(beauty);
                });
                let beautyIndex = human.indexOf(Math.max.apply(null, human));
                let maxBeautyHuman = data.faces[beautyIndex];
                
                let humanAttr = {
                    age: maxBeautyHuman.attributes.age.value,
                    gender: maxBeautyHuman.attributes.gender.value,
                    beauty: 50
                };
                if (humanAttr.gender == 'Male') {
                    humanAttr.beauty = maxBeautyHuman.attributes.beauty.female_score;
                } else {
                    humanAttr.beauty = maxBeautyHuman.attributes.beauty.male_score;
                }

                humanAttr.gender = humanAttr.gender == 'Male' ? "male" : "female";
                humanAttr.beauty = Math.ceil(humanAttr.beauty) + 15;
                humanAttr.beauty = humanAttr.beauty > 97 ? 97 : humanAttr.beauty;
                humanAttr.defeat = self.computeBeautyDefeatRatio(humanAttr.beauty);

                self.setData({
                    testPicResult: humanAttr
                });

                wx.hideLoading();
            },
            fail: function() {
                wx.showToast({
                    title: '网络异常,请稍后再试',
                    icon: 'none',
                    duration: 2000
                });
            }
        })
    },
    getImageInfo(imgSrc, scb, ecb) {
        wx.getImageInfo({
            src: imgSrc,
            success: scb,
            fail: ecb
        });
    },
    setPicAdaptHeight(picWidth, picHeight) {
        let h = (app.globalData.screenWidth * 0.7 / picWidth) * picHeight / app.globalData.screenHeight * 100;
        this.setData({
            picSelfAdaptHeight: h,
            picSelfAdaptWidth: picTestFileDefaultWidth
        });
    },
    computeBeautyDefeatRatio(beauty) {
        return Math.ceil(Math.sqrt(beauty) * 10);
    }
})

执行流程

启动小程序
  • App()函数执行并且 onLaunch 回调被触发,获取window的宽高,存入 globalData 属性中;
首页渲染
  • picSelfAdaptWidthpicSelfAdaptHeight 分别赋默认值,显示照片显示区显示默认图片;
  • 此时 testPicFile, testPicResult 均为空,视图将只显示 "选择照片/拍照" 按钮;
点击"选择照片/拍照"按钮
  • 触发 handleUploadPic 事件, 通过调用 wx.chooseImage 来选择本地图片或者拍照,然后将选取的照片临时路径赋值给 testPicFile;
  • 然后调用自定义方法 getImageInfo 来获取图片的宽高等信息,再调用 setPicAdaptHeight 传入宽高来计算图片自适应高的值 picSelfAdaptHeight
  • 此时由于 testPicFile 非空,"选择照片/拍照"按钮将隐藏,"计算颜值"按钮显示;
  • 同时图片预览区右上角出现"红叉"按钮;
点击"红叉"按钮
  • 将会触发 handleCancelPic 事件,会重置data对象中所有属性值,回到最初始状态;
点击"计算颜值"按钮
  • 首先使用open-type(微信开放能力)开放的功能获取用户信息(从bindgetuserinfo绑定的回调函数中获取用户信息),此时会触发 handleGetUserInfo , 获取用户信息后会执行 this.handleComputePic 颜值计算方法,
颜值计算方法核心逻辑
  • 调起Loading,显示颜值计算中;
  • 通过微信API wx.uploadFile 向face++接口发送请求,查看源代码
  • 对face++API的返回值进行进一步处理:

    • 颜值数据中会有同性颜值打分和异性颜值打分,我们取异性颜值打分数据;
    • 返回数据中若有多组面部打分数据的话,我们取颜值最高一组;
    • 为原始颜值分 +15 分的 buff, 最高不超过97分;
    • 计算击败率,具体算法请见源码 ;
邀请好友
  • 使用 button 组件的 open-type 功能来触发用户转发;
  • 在 Page 中定义 onShareAppMessage 函数,设置该页面的转发信息 参见文档
Page({
    onShareAppMessage(res) {
        return {
            title: '我的颜值击败了全国 94% 的用户,不服来战!',
            path: '/pages/index/index',
            imageUrl: '../../asset/img/prview.png'
        }
    },
    ...
点击"再试一次"按钮
  • 触发 handlePlayAgain 事件,重置data中全部属性值;
    handlePlayAgain() {
        this.setData({
            testPicFile: '',
            testPicResult: null,
            picSelfAdaptHeight: picDefaultHeight,
            picSelfAdaptWidth: picDefaultWidth
        });
    }

打赏页面开发

打赏页面比较简单,不进行讲解,可自行参考源码

测试

在开发者工具中有 测试 按钮,可点击申请测试报告,如下:

图片描述

报告每24小时可申请一次,内容如下:

图片描述

发布

上传代码

首先需要上传小程序代码,点击 上传 按钮进行上传

图片描述

图片描述

填写版本号,项目备注后就开始上传代码

图片描述

提交审核

登录微信公众平台,进入 开发管理 找到开发版本卡片,点击 提交审核

图片描述

如实填写审核信息

图片描述

等待审核通过后会有微信通知,大概1个工作日我就收到审核通过的通知了(赞一下微信的审核速度),然后登陆公众平台,进入 开发管理 ,你会看小程序的状态是审核通过待发布,点击 发布即可。

可以打开微信在搜索框中输入小程序名称,如果能正常搜索到,说明此次发布成功。

图片描述

结尾

至此,小程序就顺利诞生了,希望此文章对刚入坑小程序开发的伙伴们有所帮助。



dreamans
725 声望847 粉丝

专注PHP、Go、微服务、数据库、分布式系统、算法等后端技术。