一个定制微信朋友圈截图的小程序示例
微博上经常会看到一些模仿名人的朋友圈截图,比如民国时期的文学大家朋友圈,春秋战国时期思想家的朋友圈。因此萌发了做一个定制微信朋友圈截图的小程序的想法,当然也顺便熟悉小程序的框架及开发流程。
功能说明
界面按照微信朋友圈布局,但是并没有完全严格参照,边距以及长宽比可能略有不同。
功能开发分为两个阶段:第一阶段实现定制朋友圈并截图的功能,第二阶段加入账户授权,获取用户信息,分析用户数据。
第一阶段的主要功能:
- 设置本人封面图片;
- 设置本人头像;
- 设置发朋友圈者头像;
- 设置发朋友圈名字;
- 设置朋友圈内容(包含文字和图片);
- 设置朋友圈发送时间;
- 设置点赞人列表;
- 设置朋友圈回复对话(包含回复人名称与回复文字);
截屏生成图片并保存。
最终呈现效果见下图
功能开发
此部分阐述如何实现小程序中一些关键功能。 开发微信小程序需要一点预备知识:
- 前端语言:HTML(用于页面布局)
- 版面样式:CSS(用于指定页面中元素的样式)
- 脚本语言:javascript(用于处理页面逻辑)
- 数据格式:json(用于数据传递)
如果只有简单的前端(比如本文的小程序),懂上面四点就够了,如果需要与服务器端的交互,还涉及后台的开发。
目录结构
沿用小程序默认目录结构。
- asserts
- icons //存放图标
- nav_img //存放页面默认图片
- component //存放自定义组件
- comments //替换回复的对话框
- dialoginput //替换文字的对话框
doc- pages
- index //小程序首页
- index.js
- index.json
- index.wxml
- index.wxss
- logs
- utils
- app.js
- app.json
- app.wxss
- project.config.json
- sitemap.json
NOTE: 小程序代码打包上传会将此目录中所有文件都上传,且为了保证加载速度,大小必须控制在2M以内。之前在目录/doc下放了一些说明文档和图片,总大小大于2M,导致无法上传代码。猜测因为小程序中使用的都是解释性语言,不存在编译一说,大概默认此路径下所有的文件都与小程序的运行有关。(可以在微信开发者工具的 版本管理 里,找到每一次的提交记录。)
js/wxml/wxss三者间的关系
在小程序中,wxml, wxss为视图层,而js则是逻辑层。 wxml的每一个元素都可设置一个对应的class,在wxss中对每个class都可进行样式定义,java中的数值可以直接传入wxml中。
js传值给wxml
数据绑定
java中的参数值可以通过setData直接传入wxml,wxml中以 ** 的形式访问数据,详见 **官方文档 。
<!--pages/index/index.wxml-->
<image class="cover-image" mode="aspectFill" data-count="1" data-type="cover-image" bindtap="onChoosePic" src=""/>
// pages/index/index.js
//初始化值
data: {
coverImage: "../../assets/nav_img/cover.png"
}
//设置新值
var that = this;
that.setData({
coverImage: tempFilePaths
});
列表渲染
wxml 可以通过wx:for或wx:key语句直接访问列表中的每个值。
// pages/index/index.wxml
<view wx:for="">
<image class="photo-item" mode="scaleToFill" src=""/>
</view>
// pages/index/index.js
//初始化
data: {
photos: [
'../../assets/nav_img/photo1.png', '../../assets/nav_img/photo2.png',
'../../assets/nav_img/photo3.png', '../../assets/nav_img/photo4.png',
'../../assets/nav_img/photo5.png',
],
}
//设置新值
that.setData({
photos: tempFilePaths
});
条件判断
wxml 中,可以使用wx:if作为条件判断语句。
<!--pages/index/index.wxml-->
<!--判断comments列表是否为空,若不为空,则显示分隔符,若为空,则不不显示分隔符-->
<view wx:if="">
<view class="div-line"></view><!-- end div-line -->
<view class="comments" bindtap="onShowDialogComments">
<view wx:for="">
<view class="comment-richtext">
<rich-text nodes="<b></b>"></rich-text>
</view><!-- end comment-richtext -->
</view><!-- end for=" -->
</view><!-- end comments -->
</view><!-- end comments -->
</view><!-- end if="" -->
本示例中,在朋友圈内容、图片以及回复中均有判断是否为空,其余不作判断。
wxss定义wxml样式
wxss 的样式定义与css类似,具备css大部分特性,对css做了两点扩展:
- 尺寸单位
- 样式导入
尺寸单位 小程序提供rpx作为尺寸单位,可以根据屏幕宽度自适应调整,类似android中的dp。所以在wxss中尽量使用rpx作为尺寸单位,可以更好的适应不同分辨率的设备。
样式导入 使用 @import 语句可以导入外联样式表,@import后跟需要导入的外联样式表的相对路径,用 ; 表示语句结束。
页面总体布局
微信小程序的页面布局与其他前端的布局类似,如果之前做过网页、Android、iOS或者flutter的页面开发,小程序的页面布局便不是难事。
小程序的页面布局采用HTML+CSS,可以直接使用大部分CSS样式,在小程序中为wxml+wxss。
本小程序功能简单,只有一个主页面,直接在index中实现。
页面的总体布局示意图见下。
页面横竖排版
通常手机页面都是以竖排版的方式进行页面布局,由上到下划分为不同的行,再在每行中进行细分。可以在公共样式表(app.wxss)中规定页面的默认竖排版布局,单独的页面直接应用即可。
/**app.wxss**/
.container {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
box-sizing: border-box;
}
图片上叠加图片和文字
顶部cover,包括封面,用户头像和用户名称三个元素。
封面图片 cover-image 是 cover-bar 的子节点, 用户的头像 avatar-img 和名称 avatar-name 则是 subsection-avatar 的子节点, subsection-avatar 与 cover-bar 同级。
cover-bar 的高度设置为510rpx, cover-image 的高度设置为600rpx, subsection-avatar 是 cover-bar 之后的同级节点,起始位置为511rpx,这样 subsection-avatar 中的元素即可覆盖在cover-image之上显示。
图片或文字的重叠部分,可能对点击效果有影响,因此需要将 avatar-img 和 avatar-name 设置在 cover-image 之上,即,z-index: 1; ,这样在点击用户头像和名称时,才会找到该元素对应的事件处理函数。
小程序官方文档中说明,后插入的原生组件可覆盖之前的原生组件,目前看来显示确实覆盖了,但事件响应并不保证。(模拟器中点击响应正常,但在手机中,头像重叠部分点击无响应。)
主要代码见下:
<!-- pages/index/index.wxml -->
<view class="container">
<!-- 顶部Cover -->
<view class="section-cover">
<view class="cover-bar">
<image class="cover-image" mode="aspectFill" data-count="1" data-type="cover-image" bindtap="onChoosePic"src=""/>
</view><!-- end cover-bar -->
<view class="subsection-avatar">
<image class="avatar-img" data-type="avatar-img" data-count="1" bindtap="onChoosePic" src=""/>
<text class="avatar-name" id="avatarName"bindtap="onShowDialog"></text>
</view><!-- end subsection-avatar -->
</view><!-- end section-cover -->
...
</view><!-- end container -->
// pages/index/index.wxss
.cover-bar{
justify-content: center;
height: 510rpx;
}
.cover-image{
width: 100%;
height: 600rpx;
}
.subsection-avatar{
display: flex;
flex-direction: row-reverse;
}
.avatar-img{
height: 120rpx;
width: 120rpx;
margin-right: 30rpx;
border-radius: 12rpx;
z-index: 1;
}
.avatar-name{
color: #ffffff;
justify-content: center;
margin-right: 20rpx;
align-items: center;
display: flex;
font-size: 35rpx;
z-index: 1;
}
功能实现
文本替换
文本替换是本小程序的主要功能,交互方式为通过点击需要更改的文字,弹出对话框,在对话框中对文字内容进行修改,确定后更新到页面上。
对话框采用自定义样件实现,分为两类,一类为单行文本替换的对话框,例如朋友圈用户名、内容、发布时间、发布地点等信息的文本替换;一类为回复内容的文本替换。
单行文本替换对话框的功能非常简单,只需要替换原有文本;回复消息替换稍微复杂一点,需要增、删、替换文本。
单行文本替换
功能点:
- 点击文本后,弹出对话框
- 对话框中显示原有文本
- 更改原有文本,点击确定后产生界面变化
小程序的原生组件提供三种弹出框:消息提示框、模态对话框和loading提示框。这三种原生弹出框不支持修改,无法满足输入文本的需求。
因此,需要用到 自定义组件 来满足弹出框中输入文本的要求。
创建自定义组件dialoginput
首先,在小程序目录下创建用于存放自定义组件的目录,再在小程序开发工具中,右键自定义组件目录,弹出的菜单中选择“新建 Component”,将新的component命名为dialoginput,则目录中自动创建四个文件:dialoginput.js, dialoginput.json, dialoginput.wxml, dialoginput.wxss
在 dialoginput.json 文件中对自定义组件进行声明
// component/dialoginput/dialoginput.json
{
"component": true,
"usingComponents": {}
}
dialoginput的布局和样式
组件的布局及样式与页面类似,在 dialoginput.wxml 与 dialoginput.wxss 中对自定义组件页面进行设计,详细可参考 官方文档 。
对话框竖排版,由上到下分为三个部分
title, input cell, footer
- title 显示该对话框的标题
- input cell 输入框,显示已有内容,可输入新内容
- footer 包含确认和取消两个按键
<!--component/dialoginput/dialoginput.wxml-->
<view class='wx_dialog_container' hidden="">
<view class='wx-mask'></view>
<view class='wx-dialog'>
<view class='wx-dialog-title'></view>
<view class="input-cell">
<textarea class='weui-input' bindinput='bindKeyInput' auto-height="true" value=''/>
</view><!-- end input-cell -->
<view class='wx-dialog-footer'>
<view class='wx-dialog-btn' catchtap='_cancelEvent'></view>
<view class='wx-dialog-btn' catchtap='_confirmEvent'></view>
</view><!-- end wx-dialog-footer -->
</view><!-- end wx-dialog -->
</view><!-- end wx_dialog_container -->
dialoginput的构造器
自定义组件的逻辑层,即 diagloginput.js 中的内容,可指定组件的属性、数据、方法等,这里只挑选用到的参数介绍,详细内容参见 官方文档 。
properties
dialoginput 的对外属性,使用该组件的页面可以在创建组件时向组件传值。
// component/dialoginput/dialoginput.js
properties: {
// 弹窗标题
title: { // 属性名
type: String, // 类型(必填),可以是任意类型
value: '请输入替换内容' // 属性初始值(可选)
},
// 输入框提示内容
hint: {
type: String,
value: '例:张三'
},
// 弹窗取消按钮文字
cancelText: {
type: String,
value: '取消'
},
// 弹窗确认按钮文字
confirmText: {
type: String,
value: '确定',
}
},
data
自定义组件 dialoginput 的内部数据,类似 index .js 中的 data,同样也可以用 setData 改变其值。
// component/dialoginput/dialoginput.js
/**
* 组件的初始数据
*/
data: {
// 弹窗显示控制
isShow: false,
tapVal:"default",
onTapBtn:""
},
methods
自定义组件 dialoginput 的方法,包括事件响应函数和自定义方法。
// component/dialoginput/dialoginput.js
/**
* 组件的方法列表
*/
methods: {
/*
* 公有方法
*/
//隐藏弹框
hideDialog() {
this.setData({
isShow: !this.data.isShow
})
},
//展示弹框
showDialog(btnName, value) {
this.setData({
isShow: !this.data.isShow,
onTapBtn: btnName,
tapVal: value
})
console.log(this.data.onTapBtn)
},
...
},
组件间的通信
组件间的 基本通信方式 有以下三种:
WXML 数据绑定:用于父组件向子组件的指定属性设置数据,仅能设置 JSON 兼容数据(自基础库版本 2.0.9 开始,还可以在数据中包含函数)。具体在 组件模板和样式 章节中介绍。
事件:用于子组件向父组件传递数据,可以传递任意数据。
如果以上两种方式不足以满足需要,父组件还可以通过 this.selectComponent 方法获取子组件实例对象,这样就可以直接访问组件的任意数据和方法。
本示例中只用到了前两种通信方式。
page index 中多处引用了自定义组件 dialoginput。
数据绑定
在index.wxml中引用自定义组件dialoginput时,向自定义组件传递数据。 hint, cancelText, confirmText 都是在 dialoginput.js properties中定义的属性,id是组件的通用属性。
<!--pages/index/index.wxml-->
<dialoginput
id="dialoginput" <!--【数据绑定】传递id值-->
hint="例:可以点我" <!--【数据绑定】传递hint值-->
cancelText="取消" <!--【数据绑定】传递取消按钮的显示内容-->
confirmText="确定" <!--【数据绑定】传递确定按钮的显示内容-->
bindcancelEvent="_cancelEvent" <!--【事件】绑定dialoginput中的cancelEvent事件,_cancelEvent为回调函数-->
bindconfirmEvent="_confirmEvent" <!--【事件】绑定dialoginput中的confirmEvent事件,_confirmEvent为回调函数-->
>
</dialoginput>
事件
以事件通信,分为监听事件和触发事件两大块。
监听事件
自定义组件可以触发任意的事件,引用组件的页面可以监听这些事件,本示例中用于 dialoginput 组件向 index 页面传递用户输入的文本信息。
首先,index.json 中声明使用 dialoginput 组件。
// pages/index/index.json
{
"usingComponents": {
"dialoginput":"/component/dialoginput/dialoginput",
"commentsinput": "/component/comments/comments"
}
}
其次,index.wxml 绑定模板中的 confirmEvent 事件,在监听到该事件时回调方法 _confirmEvent() ,该回调函数在 index.js 中定义。
<!-- pages/index/index.wxml -->
<dialoginput
...
bindcancelEvent="_cancelEvent" <!--【事件】绑定dialoginput中的cancelEvent事件,_cancelEvent为回调函数-->
bindconfirmEvent="_confirmEvent" <!--【事件】绑定dialoginput中的confirmEvent事件,_confirmEvent为回调函数-->
>
</dialoginput>
// pages/index/index.js
pages{
//确认事件
_confirmEvent: function (options) {
...
},
},
参数 options 中可以拿到 id 、this.triggerEvent(‘confirmEvent’, myEventDetail) 中 myEventDetail 包含的数据等。
触发事件
在组件 dialoginput 中,点击确定按钮,触发事件 confrimEvent ,将数据传递给 index 页面。
<!-- component/dialoginput/dialoginput.wxml-->
<view class='wx-dialog-btn' catchtap='_confirmEvent'></view>
// component/dialoginput/dialoginput.js
Component({
properties: {},
data: {},
methods: {
_confirmEvent() {
...
this.triggerEvent('confirmEvent', myEventDetail)//trigger 'confirmEvent'事件,并将myEventDetail传递出去
}
}
})
触发对话框
参照 ** js/wxml逻辑交互** 方式实现
index.wxml 使用 bindtap 触发对话框,index.js 的回调函数 onShowDialog() 作逻辑处理。
<!-- pages/index/index.wxml-->
<text class="post-avatar-name" id="postUserName" bindtap="onShowDialog"></text>
// pages/index/index.js
onShowDialog: function(e) {
console.log(e);
var btnId = e.currentTarget.id;
switch (btnId) {//区分被点击文本
case 'avatarName':
this.dialoginput.showDialog(btnId, this.data.avatarName);//显示对话框
break;
case 'postUserName':
this.dialoginput.showDialog(btnId, this.data.postUserName);
break;
...
default:
console.log('Unknown tap name:' + btn);
break;
}
},
文字替换
确定按钮的回调函数 _confirmEvent() 功能:
- 判断点击对象
- 设置新显示文字
- 隐藏对话框
具体代码见下:
// pages/index/index.js
//确认事件
_confirmEvent: function (e) {
var btn = e.detail.btn;
var val = e.detail.val;
switch(btn){//判断被点击的文本
case 'avatarName':
this.setData({
avatarName: val
})
break;
case 'postUserName':
this.setData({
postUserName: val
})
break;
...
default:
console.log('Unknown txt name:' + btn);
break;
}
this.dialoginput.hideDialog();//隐藏对话框
},
回复消息替换
除输入区域外,回复消息替换的对话框与单行文本替换对话框几乎一致。
功能点:
- 点击回复文本后,弹出对话框
- 对话框中显示原有回复信息,一条一行
- 可更改原有回复,或删除原有回复,添加新回复,点击确定后产生界面变化
title 和 footer 与 dialoginput 相同,input cell由上到下包含三部分
回复人、回复信息、添加按钮
布局示意图见下
布局文件见下:
<!-- component/comments/comments.wxml -->
<view class='wx_dialog_container' hidden="">
<view class='wx-mask'></view>
<view class='wx-dialog'>
<view class='wx-dialog-title'></view>
<view wx:for="">
<view class="input-cell">
<textarea class='input-cell-name' id="inputCellName" bindinput='bindKeyInput' auto-height="true" value='' data-index=""/>
<textarea class='input-cell-msg' id="inputCellMsg" bindinput='bindKeyInput' auto-height="true" value='' data-index=""/>
<view class="block-btn-add">
<image class="btn-add" src="../../assets/icons/add.svg" mode="aspectFill" bindtap="onAddNewLine" data-index=""/>
</view>
</view>
</view>
<view class='wx-dialog-footer'>
<view class='wx-dialog-btn' catchtap='_cancelEvent'></view>
<view class='wx-dialog-btn' catchtap='_confirmEvent'></view>
</view>
</view>
</view>
如果用户删除了某一行的内容,则页面上会相应减少一行。特别的,如果没有comment,则页面上分隔符包括comment的显示区域都应该清除。
<!-- pages/index/index.wxml -->
<view wx:if=""><!--如果comments为空,不显示下面内容-->
<view class="div-line"></view><!-- end div-line -->
<view class="comments" bindtap="onShowDialogComments">
<view wx:for="">
<view class="comment-richtext">
<rich-text nodes="<b></b>"></rich-text>
</view><!-- end comment-richtext -->
</view><!-- end for=" -->
</view><!-- end comments -->
</view><!-- end wx:if="" -->
图片选择替换
使用微信原生接口 wx.chooseImage(Object object) 从本地相册选择图片或使用相机拍照。
本示例使用到的 wx.chooseImage(Object object) 参数见下:
属性 | 取值 | 说明 |
---|---|---|
count | 1-9 | 最多可以选择的图片张数 |
sizeType | [‘original’, ‘compressed’] | 可选图片尺寸:原图或压缩 |
sourceType | [‘album’, ‘camera’] | 图片来源:相机或相册 |
success | 接口调用成功的回调函数数,返回所选图片路径列表 | |
fail | 接口调用失败的回调函数数 |
图片选择并替换的实现代码见下:
<!-- pages/index/index.wxml
bindtap: 设置触发的回调函数
data-count: 传入回调函数的参数,最多可允许选择几张图片
data-type:传入回调函数的参数,当前触发的类型
-->
<image class="post-avatar-img" data-count="1" data-type="post-avatar-img" bindtap="onChoosePic" src=""/>
// pages/index/index.js
onChoosePic: function (options) {
var that = this;
var cnt = options.currentTarget.dataset.count;//获取最大选择的图片数量
var type = options.currentTarget.dataset.type;//获取触发类型
wx.chooseImage({//调用原生接口
count: cnt,
sizeType: ['compressed'], // 指定为压缩图
sourceType: ['album', 'camera'], // 指定来源为相册和相机
success: function (res) {
// 返回选定照片的本地文件路径列表,tempFilePath可以作为img标src属性显示图片
var tempFilePaths = res.tempFilePaths;
switch(type){
case 'cover-image':
that.setData({
coverImage: tempFilePaths
});
break;
case 'avatar-img':
that.setData({
avatarPhoto: tempFilePaths
});
break;
case 'post-avatar-img':
that.setData({
postAvatarPhoto: tempFilePaths
});
break;
case 'post-photo':
that.setData({
photos: tempFilePaths
});
break;
default:
console.log('Unknown type name:' + type);
break;
}
},
fail: function (res) {
console.log('chooseImage failed:' + res.errMsg)
}
})
},
屏幕截图
截屏可以使用原生API canvas 绘制。但canvas绘制太麻烦了,相当于重新按照布局将所有元素都绘制一遍。累了暂时不想搞了。
截图保存
既然截图不弄了,保存也不需要了。
- CentOS8服务器上部署Docker memos+Nginx反向代理实现外网访问
- macOS搭建Apache2.4 + PHP7.3 + MySQL8.0 + ThinkPHP6.0开发环境
- macOS系统Apache配置虚拟主机vhost
- Win10下部署Apache+PHP+MySQL环境
- ubuntu18.04下安装ss-qt5并配置Chrome和Terminal科学上网
- 一个定制微信朋友圈截图的小程序示例
- Windows上RobotFramework&RIDE的环境部署
- [解决]Github pages 无法自动更新
- 利用 Github Pages 搭建博客
- Windows上部署Jekyll