在知乎上,有人提问“如何使用 Python 开发微信小程序”。
其实微信小程序作为一个前端的机制,Python 并不能插上边。只不过可以作为后端接口为微信小程序提供数据服务而已。
那么在本篇,我们就将结合微信小程序开发与 Python Web 开发,来完成一个朋友圈神器微信小程序的开发,这个微信小程序作为一个工具型的应用,供用户输入姓名或其他字段,生成一个带有炫耀成分的照片。
比如,移民申请表照片:
高额工资单照片:
豪车订单照片:
下面,就开干吧!
开发微信小程序,首先肯定需要去微信公众平台上注册一个微信小程序了,我们在微信公众平台的注册页面选择“小程序”进行注册。
接着有三个步骤:邮箱注册、邮箱激活和信息登记:
完成上述三个步骤后,就可以登录进入管理中心:
在基本设置中,我们可以设置微信小程序的名称、头像、说明等基本信息。
在开发设置中,我们可以获取到小程序的 AppID 和 AppSecret,这在后续的开发中会使用到,同时我们可以在此设置小程序服务器的域名:
开发微信小程序需要使用到微信 Web 开发工具这一软件。我们下载并安装好。
启动之后,需要我们使用微信扫码进行登录:
之后,新建一个小程序项目:
指定小程序的项目目录、输入小程序的 AppID(管理页面中获取)、输入项目名称,之后我们就进入了微信开发工具的主界面了:
因为我们使用了快速启动的模板,所以自动生成了一个 Hello World 的 Demo。接下来,我们创建我们的票圈神器的小程序页面。
在创建小程序的页面之前,我们先来了解一下微信小程序的代码结构。
根据微信小程序开发文档的介绍:小程序包含一个描述整体程序的 app 和多个描述各自页面的 page。
一个小程序主体部分由以下三个文件组成,必须放在项目的根目录。
同时,一个小程序的页面由四个文件组成:
微信小程序的视图层负责页面的展示,由 WXML 文件描述页面结构和 WXSS 文件描述页面的样式。
WXML 和 WXSS 是什么东西呢?我们可以拿 HTML 和 CSS 来与之进行类比。虽然它们不一样,但是它们真的很相似。
WXML 是一套微信定义的可嵌套的标记语言,而 WXSS 则具备 CSS 的大部分特性,并对 CSS 进行了扩充和修改。
接下来我们来规划一下我们的小程序的页面构成:
首先,我们在项目目录结构的 pages 路径下新建一个 detail 目录,其下包含三个同名的 JS 文件、WXML 文件、WXSS 文件;一个 result 目录,其下包含三个同名的 JS 文件、WXML 文件、WXSS 文件;最后 pages 目录下的结构如下图所示:
然后,在创建的 JS 文件中输入以下代码:
Page({ /** * 页面的初始数据 */ data: { }, /** * 生命周期函数--监听页面加载 */ onLoad: function (options) { }, /** * 生命周期函数--监听页面初次渲染完成 */ onReady: function () { }, /** * 生命周期函数--监听页面显示 */ onShow: function () { }, /** * 生命周期函数--监听页面隐藏 */ onHide: function () { }, /** * 生命周期函数--监听页面卸载 */ onUnload: function () { }, /** * 页面相关事件处理函数--监听用户下拉动作 */ onPullDownRefresh: function () { }, /** * 页面上拉触底事件的处理函数 */ onReachBottom: function () { }, /** * 用户点击右上角分享 */ onShareAppMessage: function () { }})
Page() 函数用来注册一个页面。接受一个 object 参数,以指定页面的初始数据、生命周期函数、事件处理函数等。
使用微信开发者工具的智能提示可以快速生成这些代码:
完成这一步之后,我们打开项目根目录的 app.json 文件:
在pages列表中添加如下代码:
"pages/detail/detail","pages/result/result",
最后 app.json 文件中 pages 列表的值应该为:
[ "pages/index/index", "pages/detail/detail", "pages/result/result", "pages/logs/logs" ],
首先,我们在列表页面放置一个轮播图,让我们的页面不显单调。准备三张图片:
在项目根路径下新建一个名为 imgs 的目录,将三张轮播图片复制进去:
在 index.js 文件的 Page 实例中,在 data 字典添加一个键值对,用于指定本地轮播图片的位置:
删除 index.wxml 文件中的所有代码,输入以下代码以创建一个轮播图:
<swiper class="swiper" indicator-dots="true" autoplay="true" interval="5000" duration="1000"> <block wx:for="{{headimg}}" wx:for-index="index"> <swiper-item> <image src="{{item.url}}" class="slide-image" mode="aspectFill"/> </swiper-item> </block> </swiper>
其中:
我们可以看到,在 block 标签中,我们为其设置了 wx:for 属性,这个属性用于列表渲染,绑定了 Page 的 data 中的 headimg 数组(在微信小程序中,WXML 中的动态数据都来自于对应 JS 文件 Page 中的 data 数据)。
接着,调整轮播图的样式,在 index.wxss 文件中输入以下代码:
.swiper { height: 400rpx; width: 100%;}.swiper image { height: 100%; width: 100%;}
最后保存文件,在微信开发者工具中可以预览到我们的轮播图已经创建成功:
创建完轮播图之后,我们继续编辑创建图片列表结构。
<view class="temp_box"> <block wx:for="{{templist}}"> <view class="temp_item"> <navigator url="../detail/detail?tid={{item.id}}"> <image src="https://www.huabandata.com/{{item.icon}}"></image> <view class="content"> <text>{{item.name}}</text> </view> </navigator> </view> </block></view>
在页面的列表结构中,我们使用一个 view 标签作为外部容器,里面定义了一个 block 标签用于遍历图片模板数据生成多个图片信息,定义渲染的数组为 templist,这个我们将在 index.js 文件中进行定义和声明。
同时使用了 navigator 标签,用于页面的跳转,设置一个参数 tid 并将模板的 id 作为值,使其能够跳转到具体模板的详情页面。
然后在 index.wxss 文件中添加以下样式:
/* 模板图片列表 */.temp_box { margin: 3px; width: 100%;}.temp_item { display: inline-block; width: 48%; margin: 0.5%; background-color: white;}.temp_item image { width: 100%; height: 160px;}.temp_item .content { width: 100%; height: 32px; margin: 5px;}.temp_item .content text { font-size: 12px; line-height: 16px; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;}
如果 templist 数组有合适的数据,那么其渲染出来的页面应该是下面这样的:
详情页面相较于列表页面简单很多,由两个结构组成:
所以我们的详情页面 detail.wxml 的结构为:
<view class='temp'> <image src='https://www.xxx.com/{{tempData.img}}'></image></view><form bindsubmit='generate'> <view class="form"> <view class='li'> <image class='icon' src='http://www.xxx.com/media/resume/icon_user.png'></image> <input class='input' placeholder='请输入{{tempData.hint}}' name='content'></input> </view></view><button class="button" form-type='submit'>立刻生成照片</button></form>
在这里,我们通过 <view>
标签包裹着 <image>
标签来显示图像,然后用一个 form 表单标签包裹着 input 输入框和 button 提交按钮。
在 button 按钮中,我们通过设置 form-type 属性为 submit,使得点击这个按钮时会提交表单。
表单的提交处理由 <form>
标签的 bindsubmit 属性来控制,在此我们指定了处理函数为 generate,我们将在 detail.js 中对 generate 函数进行定义。
然后,在 detail.wxss 中对其进行样式设计:
page{background-color: #efeff4}.temp{ background-color: white; border: #e5e5e5 solid 1px; display: flex; align-items: center; justify-content:center;}.temp image{ height: 320px;}.form{ margin-top: 20rpx; background-color: white; border: #e5e5e5 solid 1px; border-right: none; border-left: none; }.li{ height: 100rpx; border-bottom: #e5e5e5 solid 1px; width: 90%; margin: auto; display: flex; align-items: center }.input{padding-left: 20rpx; width: 94%; color: black}.icon{ width: 50rpx; height: 50rpx;}.button{ background-color: #09bb07; margin: auto; margin-top:20rpx; margin-bottom: 20rpx; width: 90%; color: white }
如果 tempData 数组中有正确的数据,那么详情页面的显示效果如下所示:
结果页面与详情页面类似,同样是显示一个图片,和一个按钮(用于保存照片至本地),所以将 detail 页面的结构和样式稍作修改即可。
result.wxml:
<view class='temp'> <image src='https://www.xxx.com/{{imgurl}}'></image></view><button class="button" form-type='submit' bindtap='savePhoto'>保存照片</button>
reault.wxss:
page{background-color: #efeff4}.temp{ background-color: white; border: #e5e5e5 solid 1px; display: flex; align-items: center; justify-content:center;}.temp image{ height: 320px;}.button{ background-color: #09bb07; margin: auto; margin-top:20rpx; margin-bottom: 20rpx; width: 90%; color: white }
如果 imgurl 数组有正确的数据,那么结果页面的显示效果如下所示:
在小程序的页面创建和设计好之后,我们需要创建一个 API 数据接口,供小程序请求获取,以生成供 WXML 页面进行渲染的数组。
因为本篇讲的是《Python 开发者的微信小程序开发实践》,自然使用的是 Python 语言,而我对 Django 相对比较熟悉,所以我们使用的是 Django 框架来对小程序提供 API 接口。
我已经有一个 Django 项目在运行,所以在此直接在此项目下创建 Django App:
python3 manage.py startapp pyq_tool
要生成一张那样可用来炫耀的图片,其实很简单,利用图像处理模块,在图片上添加相应字体的文字即可。
在 Python 中,我们使用 PIL 模块对图像进行处理,其可以通过 pip 命令进行安装:
pip install pillow
下面我们介绍一下使用 PIL 库生成一张装 X 图片的过程:
(1)引入核心模块:
from PIL import Image,ImageDraw,ImageFont
其中,Image 是图像类的包装器,ImageDraw 用于对图片界面进行操作,ImageFont 用于进行光栅字体管理。
(2)根据图片大小,创建一个新的图像:
bg = Image.new("RGB",(640,853))
(3)打开图片:
im = Image.open(r"toutu3.jpg")
(4)合并两个图片为一个新的图像:
draw2 = Image.blend(bg,im,1.0)
(5)创建一个图像绘制对象:
draw = ImageDraw.Draw(draw2)
(6)添加一个字体并指定字体大小:
ttfont = ImageFont.truetype(r"gb.ttf",30)
(7)在图像绘制对象中添加文字:
draw.text((230,120),'州的现实', fill=(20,20,20),font=ttfont)
(8)最后显示图像:
draw2.show()
这样,我们就使用 Python 的 PIL 模块完成了图像的操作从而制作出了一个新的图像,结果为:
如果我们需要保存图像,则第8步改成:
draw2.save()
根据上面生成照片的代码,我们可以发现不同照片之间共同的部分和不同的部分,共同的部分就是代码的结构和图像处理的逻辑,不同的部分则是图片、图片的大小,字体的类型和大小,文本的内容和颜色等。
将这些不同之处,提取出来,我们可以创建一个数据模型用于保存不同模板图片的信息,以根据不同的模板,使用同一台代码生成不同的照片。所以,我们在 pyq_tool 目录下的 models.py 文件中创建一个数据模型来保存不同模板图片的数据:
from django.db import models# Create your models here.class Tempinfo(models.Model): name = models.CharField(max_length=20,verbose_name="模板名称") font = models.CharField(max_length=5,verbose_name='字体代码') fontsize = models.IntegerField(verbose_name="字体大小") img = models.ImageField(upload_to='pyq_tool',verbose_name="模板图片") imgsize = models.CharField(max_length=10,verbose_name='图片大小') icon = models.ImageField(upload_to='pyq_tool',verbose_name="图标") hint = models.CharField(max_length=10,verbose_name="输入提示") textcolor = models.CharField(max_length=15,verbose_name='文本颜色') textplace = models.CharField(max_length=10,verbose_name='文本位置') text2 = models.CharField(max_length=15,verbose_name='日期文本',null=True,blank=True) text2hint = models.CharField(max_length=10,verbose_name="文本2提示",null=True,blank=True) text2place = models.CharField(max_length=10,verbose_name='日期位置',null=True,blank=True) def __str__(self): return self.name class Meta: verbose_name = '票圈神器' verbose_name_plural = verbose_name
然后将 pyqtool 添加进 settings.py 文件中的 INSTALLEDAPPS 列表中,使用 makemigrations 和 migrate 命令生成数据模型。最后,每一个图片模板的信息如下图所示:
根据收集好的装 X 图片模板,向数据库中添加几条数据:
在数据库中有了数据之后,我们来创建视图函数,在创建具体视图函数之前,我们先引入需要使用到的模块:
from django.shortcuts import renderfrom django.http import JsonResponsefrom django.views.decorators.csrf import csrf_exemptfrom .models import Tempinfofrom dss.Serializer import serializerfrom django.views.generic import ListViewfrom dss.Mixin import MultipleJsonResponseMixinfrom PIL import Image,ImageDraw,ImageFontimport datetimeimport os
为了简单快捷的序列化模型数据,在此我们使用一个第三方的 Django 序列化模块——Django Simple Serializer,一个由国人开发的 Django 数据快速序列化方案。
首先是模板列表视图,用于返回一个多条数据组成数组供小程序的首页列表页进行渲染 WXML:
# 获取模板列表class GetTempList(MultipleJsonResponseMixin,ListView): model = Tempinfo queryset = Tempinfo.objects.all() paginate_by = 6
创建一个基于类的视图,借助 Django Simple Serializer 生成一个分页接口,每页6条数据。
然后是模板详情视图,根据模板 id 查询并返回模板的数据:
@csrf_exemptdef get_temp_detail(request): if request.method == 'POST': id = request.POST.get('tid','') if id is not '': try: detail = Tempinfo.objects.get(id=id) detail = serializer(detail,datetime_format='string') return JsonResponse({'sucess':True,'data':detail}) except Exception as e: return JsonResponse({'success':False,'data':str(e)}) else: return JsonResponse({'success':False,'data':'没有数据'})
最后是照片的生成视图:
@csrf_exemptdef generate_photo(request): if request.method == 'POST': id = request.POST.get('tid','') content = request.POST.get('content','') if id != '' and content != '': try: temp = Tempinfo.objects.get(id=id) fontpath = os.path.join(BASE_DIR,'media/pyq_font/{font}.ttf'.format(font=temp.font)) ttfont = ImageFont.truetype(fontpath,int(temp.fontsize)) # 图片大小 imgsize = temp.imgsize.split(",") try: bg = Image.new("RGB",(int(imgsize[0]),int(imgsize[1]))) except Exception as e: return JsonResponse({'success':False,'data':'图片大小出错:'+str(e)}) im = Image.open(temp.img) draw2 = Image.blend(bg,im,1.0) draw = ImageDraw.Draw(draw2) try: textplace = temp.textplace.split(",") textcolor = temp.textcolor.split(",") draw.text((int(textplace[0]), int(textplace[1])),content, fill=(int(textcolor[0]), int(textcolor[1]), int(textcolor[2])),font=ttfont) except Exception as e: return JsonResponse({'success':False,'data':'文字颜色位置出错:'+str(e)}) if temp.text2 != '': text2place = temp.text2place.split(",") draw.text((int(text2place[0]), int(text2place[1])), datetime.date.strftime(datetime.date.today(),"%Y-%m-%d"), fill=(int(textcolor[0]), int(textcolor[1]), int(textcolor[2])), font=ttfont) filename = str(datetime.datetime.today()).replace(':','-').replace(' ','-').replace('.','') photoname = os.path.join(BASE_DIR,'media/pyq_photo/{0}.jpg'.format(filename)) draw2.save(photoname) return JsonResponse({'success':True,'data':'media/pyq_photo/{0}.jpg'.format(filename)}) except Exception as e: return JsonResponse({'success':False,'data':str(e)}) else: return JsonResponse({'success':False,'data':'不能为空'})
我们的小程序目前只需要使用到这三个视图函数。接下来创建 URL 路由。
在 views.py 同级目录下新建一个 pqy_tool 应用的 urls.py 文件,将以下代码写入:
from django.conf.urls import url, includefrom . import viewsurlpatterns = [ url(r'^templist/',views.GetTempList.as_view()), url(r'^tempdetail/',views.get_temp_detail), url(r'^generate/',views.generate_photo),]
然后,再在 settings.py 文件同级目录下的 urls.py 中,将 pyq_tool 应用的 urls.py 包含进项目的 URl 路由中:
# 票圈神器 url(r'^pyq/',include('pyq_tool.urls')),
完成这一步,我们的 Django API 接口就完成了。测试一下列表接口,结果显示正常:
在完成后台 API 数据接口的创建之后,我们需要做的就是让微信小程序请求这些接口并解析数据。这些步骤,都分别在 index.js、detai.js 和 result.js 中完成。
在 index.js 文件中,我们在 data 字典中定义两个空的变量:
data: { headimg: [ { url: '/imgs/head1.png' }, { url: '/imgs/head2.png' }, { url: '/imgs/head3.png' }, ], templist:[], next:'' },
templist 用于填充模板列表数据,next 用于标识分页。
然后在 onload() 函数中,使用小程序的网络请求接口 wx.request() 方法对模板列表的 API 进行请求,将获取到的数据赋值给 templist 和 next:
onLoad: function () { var that = this; //获取模板列表 wx.request({ url: 'https://www.huabandata.com/pyq/templist/', method: 'GET', // OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT header: { 'content-type': 'application/x-www-form-urlencoded' }, // 设置请求的 header success: function (res) { console.log(res) // success if (res.data) { that.setData({ templist: res.data.tempinfo_list, next: res.data.page_obj.next }) console.log(that.data) } else { wx.showToast({ title: '服务器开了小差,下拉刷新一下', icon: 'waring', duration: 3000 }) } }, fail: function (res) { // fail wx.showToast({ title: '服务器开了小差,下拉刷新一下', icon: 'waring', duration: 3000 }) }, complete: function (res) { // complete } }) },
完成这一步,我们就可以在首页看到通过 wx.request 请求、经由 WXML 的 wx:for 属性渲染后的数据了:
这样,列表页面的功能基本实现了,我们还需要添加一个下拉刷新和上拉加载下一页的功能。首先在 app.json 中的 window 字典中添加:
"enablePullDownRefresh": true
以开启下拉刷新的功能。然后在 index.js 的 onPullDownRefresh 函数中添加下拉刷新的代码,与 onLoad() 函数的代码类似:
onPullDownRefresh: function (i) { var that = this; wx.request({ url: 'https://www.huabandata.com/pyq/templist/', method: 'GET', // OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT header: { 'content-type': 'application/x-www-form-urlencoded' }, // 设置请求的 header success: function (res) { console.log(res) // success if (res.data) { that.setData({ templist: res.data.tempinfo_list, next: res.data.page_obj.next }) console.log(that.data) } else { wx.showToast({ title: '服务器开了小差,下拉刷新一下', icon: 'waring', duration: 3000 }) } }, fail: function (res) { // fail wx.showToast({ title: '服务器开了小差,下拉刷新一下', icon: 'waring', duration: 3000 }) }, complete: function (res) { // complete } }) wx.showToast({ title: '刷新成功', icon: 'success', duration: 2000 }) console.log('刷新') },
再是上拉加载下一页的 onReachBottom() 函数,通过 next 来判断是否存在下一次,如果存在则传入页面参数并请求,如果不存在则提示“没有更多了”:
onReachBottom: function (i) { var that = this; if (that.data.next != null) { wx.request({ url: 'https://www.huabandata.com/pyq/templist/', data: { 'page': that.data.next }, method: 'GET', // OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT header: { 'content-type': 'application/x-www-form-urlencoded' }, // 设置请求的 header success: function (res) { console.log(res) // success if (res.data.is_paginated) { that.setData({ templist: that.data.templist.concat(res.data.tempinfo_list), next: res.data.page_obj.next }) console.log(that.data) } else { wx.showToast({ title: '服务器开了小差,下拉刷新一下', icon: 'waring', duration: 3000 }) } }, fail: function (res) { // fail wx.showToast({ title: '服务器开了小差,下拉刷新一下', icon: 'waring', duration: 3000 }) }, complete: function (res) { // complete } }) } else { wx.showToast({ title: '没有更多了', icon: 'info', duration: 2000 }) } console.log('加载下一页') },})
完成这两个函数的编写,那么效果将如下所示:
详情页面在 detail.js 中主要有两个函数进行数据请求:
第一个请求我们直接可以在 onLoad() 函数中完成:
onLoad: function (options) { var that = this; var tid = options.tid //获取模板详情 wx.request({ url: 'https://www.huabandata.com/pyq/tempdetail/', data:{ 'tid':tid }, method:'POST', header: { 'content-type': 'application/x-www-form-urlencoded' }, success:function(res){ console.log(res) that.setData({ tempData:res.data.data }) },fail:function(res){ //fail },complete:function(res){ //complete } }) },
对表单数据的提交,我们在 detail.wxml 文件的 form 标签中绑定了 generate 函数,所以,我们在 detail.js 文件的 Page() 中新建一个 generate() 函数,将模板 id 和表达内容传递给照片生成的 URL,如果请求成功获取到了照片的 URL,那么使用小程序的重定向方法 wx.wx.redirectTo() 附带上照片的 URL 跳转到结果页面,如果请求失败则提示生成出错:
generate:function(e){ var that = this; console.log('提交的表单信息为', e) console.log('当前数据为:', that.data) var tid = that.data.tempData.id; var content = e.detail.value.content; //显示加载框 wx.showLoading({ title: '照片制作中', }) wx.request({ url: 'https://www.huabandata.com/pyq/generate/', method:'POST', data:{'tid':tid,'content':content}, header: { 'content-type': 'application/x-www-form-urlencoded' }, success: function (res) { console.log(res) if(res.data.success){ wx.hideLoading() wx.redirectTo({ url: '../result/result?imgurl=' + res.data.data }) }else{ wx.hideLoading() wx.showToast({ title: '生成出错' }) } }, fail: function (res) { //fail wx.hideLoading() wx.showToast({ title: '生成出错' }) }, complete: function (res) { //complete } }) },
详情页面最后呈现为:
结果页面相对比较简单,获取到页面跳转过来附带的照片 URL 将其赋值给imgURL,就可以完成生成后的照片的显示:
onLoad: function (options) { var that = this console.log(options) console.log('点击某个模板跳转到', options.imgurl) const imgurl = options.imgurl that.setData({ imgurl: imgurl }) },
接着,定义保存照片的函数 savePhoto(),在微信小程序中,保存一个图片需要两个步骤:
所以,我们的 savePhoto() 函数为:
savePhoto:function(i){ var that = this; // wx.authorize({ // scope: 'scope.writePhotosAlbum', // }) wx.getImageInfo({ src: 'https://www.huabandata.com/'+that.data.imgurl, success:function(i){ var path = i.path; wx.saveImageToPhotosAlbum({ filePath: path, success(result){ console.log(result) wx.showToast({ title: '保存成功', icon: 'success', duration: 2000 }) } }) } }) },
其效果如下所示:
到了这一步,我们的票圈神器小程序就开发完成了,接下来可以进行上传和上架了。
点击微信开发者工具工具栏的“上传”按钮:
其会提示我们上传成功后,需要将本次上传的版本设置为体验版:
接着输入此次上传的版本信息:
提示上传成功后,我们就可以在微信小程序管理后台的“开发管理”栏目下看到了:
我们可以直接提交审核,或者是将其选为体验版本,供指定的体验者进行前提体验测试。
我们直接提交审核,会要求我们填写相关的信息:
接着点击“提交审核”按钮即可。完成提交后,我们就可以在“开发管理”的审核版本中看到我们刚刚提交的版本了:
耐心等待审核通过吧。
如果不通过怎么办?嗯,改内容呗,毕竟:
联系客服