作者:《腾讯大家》前端工程师 王杰
| 导语 腾讯大家小程序与公众号精密结合运营,通过传播引发网友关注,扩大腾讯大家内容的出口。
腾讯大家产品背景
《腾讯大家》是公司推出的中文互联网专栏写作服务产品。由于寻找有效信息的成本是非常大的,一些真正具有传播价值的内容,却往往淹没于信息洪流之中。如何将最有价值的信息以最快的速度呈现给用户,正是《大家》产品设计的初衷。《大家》更关注互联网用户更深入、更持久的思考与表达。我们希望呈现给用户的,是经得起时间考验的文章,是时代最前沿的思想。它的表现,可能是一个专栏、一部电子书、一个属于个人的频道,甚至是一款小程序。
所谓“大家”,意在集华语写作之大家手笔,为中文互联网用户提供最具魅力的经典文字,打造最有力量的互联网言论阵地、最有价值的网络阅读品牌。
腾讯大家小程序使用场景
腾讯大家小程序根植于微信小程序功能,与公众号精密结合运营,通过传播引发网友关注,扩大腾讯大家内容的出口。
腾讯大家是腾讯内容出品部的优质栏目,内容以质量取胜,每日产出数篇精品内容,因此,腾讯大家更加注重每一篇文章的传播效率以及传播速度。 在小程序之前,腾讯大家内容主要通过单篇推荐的方式在腾讯新闻客户端、微信公众号等途径传播,而PC端的聚合页面(栏目、作者文章列表等)难以适应目前移动传播的形式,因此访问数据量较少,历史内容价值缺失。 基于聚合页面的传播需求,腾讯大家小程序解决了在移动端聚合及快速查找历史内容的需求。绑定小程序以后,在推送单篇文章时,可以配合推送作者文章列表、相关文章列表等定制页面。 用户可以通过微信文章入口、小程序收藏等功能,可以随时随地查看腾讯大家的最新内容及历史内容。
解决的实际问题
腾讯大家小程序呈现内容包含:首页首页聚合(tab)、作者列表(tab)、专栏聚合(tab)、个人中心(tab)、内容底层、作者底层、栏目底层、活动底层(战队底层,专题底层)。
一、功能分析
腾讯大家小程序要与大家官网(http://dajia.qq.com)呈现给用户内容保持一致,新的开发在不影响原有内容原创平台的基础上,增加小程序用户中心,开发用户对作者(栏目)的收藏、对文章评分、对文章历史浏览记录等功能。
感谢Ninja团队在开发过程中的支持,感谢saturnzhao(赵冬明)、杰哥jillywang(王杰)对接口数据及标准文档的指导。
三处小技巧:
<image class="choice-image" mode="aspectFill" src="{{item.n_image}}"></image>
mode="aspectFill"模式纵横比缩放图片,只保证图片的短边能完全显示出来。也就是说,图片通常只在水平或垂直方向是完整的,另一个方向将会发生截取。
Api.fetchGet(dajaMorEchoiceUrl, (err, res) => {
...
vm.$set({ firstData: vm.$data().firstData.concat(res.data) })
...
});
父组件注册onReachBottom方法使用下拉加载后,将每次请求的返回数据使用concat与原数据拼接。
小程序的核心是一个响应的数据绑定系统,所以我们要展示一篇资讯详情,需要有一份数据,通过这份数据来判断这篇资讯是要渲染段落、表格、列表、图片、还是视频。
腾讯大家的内容原创发布系统对底层的文章属性有良好的标准json数据输出能力。举例一篇正文部分输出:
{tag: "text", value: "对于不认识Ayawawa的人,很难用一两句话介绍她的理论。"}, {tag: "text", value: "简单来说,她是个情感网红,真名叫杨冰阳。"},
{tag: "text", value: "对于不认识Ayawawa的人,很难用一两句话介绍她的理论。"}
{tag: "text", value: "前些天她接受姜思达采访的视频播出后, 激进派女权主义者们对她开始了新一轮的批评:说她跪舔“男权”、固化性别不平等、“物化”婚恋关系、“直男癌”。"}
{tag: "text", value: "但我觉得Ayawawa不应该被骂得那么惨,而且重点是:她被骂的角度也不够准。"}
{tag: "image", title: "", src: "//img1.gtimg.com/cul/pics/hv1/92/70/2270/147624692.jpg"}
{tag: "title", level: "H2", value: "婚姻对不同阶层的女性,根本不是一回事"}
对应展示模板:
<block wx:if="{{detail.length > 0}}">
<block wx:for="{{detail}}" wx:key="item">
<view wx:if="{{item.tag == 'title' || item.tag == 'text'}}" class="{{item.tag}} {{item['level'] ? 'h2' : ''}}">{{item.value}}</view>
<block wx:if="{{item.tag == 'image'}}">
<image class="{{item.tag}}" mode="widthFix" src="{{item.src}}"></image>
<view class="imgalt">{{item.title}}</view>
</block>
</block>
</block>
<block wx:else>
<view class="p"></view>
</block>
另外,常规模式下资讯内容从技术角度看是带有html标签的富文本内容。在小程序中是不能将这些带有html标签的富文本内容直接展示的。另外大家小程序有从公众号、腾讯新闻移动站点抓取页面某个区域的内容的需求,推荐网页资讯转小程序展示SDK。此sdk在以往开发“企鹅数钱”、“何润锋工作室”小程序时做了很多改进,大家如有特殊需求,可同我一起交流。
感谢dereksu(苏秋宏)和shaunwu(吴泽贤)此小程序开发过程中征对大家项目的SDK无私奉献的开发和改进。
腾讯大家作者有近千名,而作者的新增并不频繁,在数据端做以下处理:
在作者列表展示上做如下处理:
响应到的数据格式:
作者模板结构:
<view class="oloading" wx:if="{{ready}}" style="height:{{wHeight}}px">
</view>
<view class="body" wx:if="{{body}}">
<view class="num_index" wx:if="{{colAutData.length !== 0}}">
我的收藏
</view>
<view class="box_author" wx:if="{{colAutData.length !== 0}}">
<view class="no_author" wx:if="{{!dataReady}}">暂无收藏</view>
<block wx:for="{{colAutDatas}}" wx:key="item" wx:if="{{colAutData.length !== 0}}">
<view class="colltloading" wx:if="{{!dataReady}}"></view>
<view class="num_author" bindtap="onAuthor" data-id="{{item.id}}">
<image class="num-image" src="{{item.image}}"></image>
<view class="num_name">{{item.name}}</view>
<view class="box_start {{item.have == 1 ? 'on':'out'}}" catchtap="onRecommStart" data-id="{{item.id}}" data-parentid="{{item.first_letter}}" data-have="{{item.have}}"></view>
</view>
</block>
</view>
<block wx:for="{{authors}}" wx:key="item">
<view class="num_index">
{{index}}
</view>
<view class="box_author">
<block wx:for="{{item}}" wx:key="author">
<view class="num_author" bindtap="onAuthor" data-id="{{item.id}}">
<image class="num_image" src="{{item.image}}"></image>
<view class="num_name">{{item.name}}</view>
<view class="box_start {{item.have == 1 ? 'on':'out'}}" catchtap="onRecommStart" data-id="{{item.id}}" data-parentid="{{item.first_letter}}" data-have="{{item.have}}"></view>
</view>
</block>
</view>
</block>
</view>
下拉加载时数据数组处理方法
extendKey:function(array) {
let arr = [];
for(let p in array){
arr.push(p);
};
return arr;
},
getAuthorDataMore: function() {
...
wx.showNavigationBarLoading();
Api.fetchPost(Api.getAllAuthor, { userid: user.openid, perpage: 20, page: (vm.$data().page += 1) }, (err, res) => {
if (res.ret == 200) {
let list = res.data;
let alist = vm.$data().authors;
let akeyArr = that.extendKey(alist);
let keyArr = that.extendKey(list);
keyArr.map(item => {
akeyArr.map(eitem => {
if (item == eitem) {
alist[eitem] = alist[eitem].concat(list[item]);
keyArr.shift();
} else {}
});
keyArr.map(_item => {
alist[_item] = list[_item]
})
});
vm.$set({ authors: alist });
wx.hideNavigationBarLoading();
} else {}
});
...
}
引用小程序官方文档的登录流程图
流程简而言之:
为什么有用户登录态:
获取用户数据示例:
getUsrAppId: function() {
let user = wx.getStorageSync('user') || {};
let userInfo = wx.getStorageSync('userInfo') || {};
wx.getUserInfo({
success: function(res) {
let objz = {};
objz.avatarUrl = res.userInfo.avatarUrl;
objz.nickName = res.userInfo.nickName;
wx.setStorageSync('userInfo', objz); //userInfo
},
fail: function() {
console.log('用户拒绝');
wx.setStorageSync('allow', { 'user': 'notallow' });
}
});
wx.login({
success: function(res) {
if (res.code) {
Api.fetchPost(Api.getOpenid, { code: res.code }, (err, res) => {
let _userObj = JSON.parse(res.data);
let obj = {};
obj.openid = _userObj.openid;
obj.expires_in = _userObj.expires_in;
wx.setStorageSync('user', obj);
});
} else {
console.log('获取用户登录态失败!' + res.errMsg)
}
}
});
if ((!user.openid || (user.expires_in || Date.now()) < (Date.now() + 600)) && (!userInfo.nickName)) {
...
};
}
因为文章的评分、个人中心的头像和昵称都需要用到用户信息。所以大家小程序在第一次打开后会自动弹出授权窗口。当用户授权后,信息缓存在Storage里,缓存的过期时间由具体的功能场景来控制。
首先将含有收藏功能的地方标注出来:
1处收藏按钮为作者收藏与栏目收藏,当用户按下按钮后会变为已收藏、再按下去则为取消收藏。在开发过程中,主要是对按钮状态的判断
模板结构:
<view class="writer-collet {{have == false ? 'edd':'cdd'}}" bindtap="writerColletButton" data-have="{{have}}" data-id="{{load.wid}}" data-parentid="{{author.first_letter}}" hover-class="writer-collet-hover"><view class="add" wx:if="{{!have}}"></view>{{allReady}}</view>
处理逻辑:
ColletButton: function(e) {
let id = e.currentTarget.dataset.id;
let have = e.currentTarget.dataset.have;
let parentid = e.currentTarget.dataset.parentid;
let user = wx.getStorageSync('user') || {};
wx.showLoading({
title: '正在处理',
});
if (!have) {
Api.fetchPost(Api.collection, { userid: user.openid, id: id, type: 1, have: 1 }, (err, res) => {
if (res.ret == 200) {
wx.hideLoading();
vm.$set({ allReady: "已经收藏", have: true })
} else {...}
});
} else {
Api.fetchPost(Api.collection, { userid: user.openid, id: id, type: 1, have: 0 }, (err, res) => {
if (res.ret == 200) {
wx.hideLoading();
vm.$set({ allReady: "收藏", have: false })
} else {...}
});
};
}
2处和3处的收藏逻辑基本相同,只是3会判断是否已经收藏此作者
模板结构:
<view class="box_start {{item.have == 1 ? 'on':'out'}}" catchtap="onRecommStart" data-id="{{item.id}}" data-parentid="{{item.first_letter}}" data-have="{{item.have}}"></view>
收藏按钮处理方法:
onRecommStart:function(e){
let that = this;
let user = wx.getStorageSync('user') || {};
let id = e.currentTarget.dataset.id;
let parentid = e.currentTarget.dataset.parentid;
let have = e.currentTarget.dataset.have;
let list = vm.$data().authors;
//新增
let alist = {"pid":parentid,"aid":id}
wx.showLoading({
title: '正在处理',
});
if (!have){
Api.fetchPost(Api.collection,{userid:user.openid,id:id,type:1,have:1}, (err, res) => {
if (res.ret == 200){
wx.hideLoading();
} else {...}
});
}else{
Api.fetchPost(Api.collection,{userid:user.openid,id:id,type:1,have:0}, (err, res) => {
if (res.ret == 200){
wx.hideLoading();
} else {...}
});
}
//此处找到操作的元素位置
list[parentid].map(item => {
if(item.id == id){
item.have = !item.have;
}
return item
});
vm.$set({authors:list});
setTimeout(function(){
//此处为刷新顶部收藏栏数据
that.getColAutData();
},1000)
},
4处为栏目收藏区域,使用了scroll-view组件,左右滑动方式方便用户查看自己已经收藏的栏目。需要注意的是需要在小程序onLoad或onShow时,取到栏目的个数,再计算组件整体宽度。
模板:
<scroll-view scroll-x class="scrollcolumns">
<view class="scroll-view" style="width:{{wWidhth}}rpx">
<view class="first"></view>
<block wx:for="{{columnColtDatas}}" wx:key="item">
....
</block>
</view>
</scroll-view>
逻辑(请注意wWidhth值的计算):
...
colData.data.map(item => {
Api.fetchGet(Api.column + item, (err, res) => {
if (res.data) {
columnAutData.push(res.data.channel);
if (columnAutData.length == colData.data.length) {
vm.$set({ columnColtDatas: columnAutData, wWidhth: (colData.data.length * 694) + 44, dataReady: true });
};
};
});
});
...
个人中心主简单呈现个人信息、用户收藏的作者/栏目统计、用户已浏览的文章记录。值得注意的是,页面onShow周期时需要刷新用户的收藏统计信息。
浏览记录模块在个人中心页面中:
因为信息审核和登录态的问题,腾讯大家小程序评论功能折中选择调用【OMG珊瑚评论】记录接口,仅做评论内容展示。
分享功能都在onShareAppMessage()函数里,不同于右上角分享按钮,如果在页面中某个地方添加分享功能,需要button绑定属性open-type="share"。除此之外,还需要相关分享属性如:
<button class="choice-share-b" catchtap="onShareAppMessage" open-type="share" data-title="{{item.title}}" data-tid="{{item.tid}}"></button>
评分功能在文章底层页中,用户对文章的评分操作会形成:
在开发中,评分功能由多个功能函数组成,大致可以分为渲染、用户操作、服务器操作回调、还有数据换算等一些函数方法。
此功能报用于单篇文章及作者朋友圈传播海报生成。
生成功能需要注意以下:
对于文字类型的canvas绘图,需要经常计算字符多少,换行计算。分享一下这两个函数:
getTrueLength: function(str) {
let len = str.length,
truelen = 0;
for (let x = 0; x < len; x++) {
if (str.charCodeAt(x) > 128) {
truelen += 2;
} else {
truelen += 1;
}
}
return truelen;
},
cutString: function(str, leng) {
let len = str.length,
tlen = len,
nlen = 0;
for (let x = 0; x < len; x++) {
if (str.charCodeAt(x) > 128) {
if (nlen + 2 < leng) {
nlen += 2;
} else {
tlen = x;
break;
}
} else {
if (nlen + 1 < leng) {
nlen += 1;
} else {
tlen = x;
break;
}
}
}
return tlen;
}
消息模板根据产品的实际业务来做开发,建议低频的推送用户。
小程序模板功能中需要向接口传递formId。在发送给用户id上,建议合理的进行分组(如用户订阅栏目或者作者openid进行分组)。如果发生文章更新,推送文章更新的消息模板。
小程序中出现了一个新单位rpx(responsive pixel),官方规定屏幕宽度为20rem,规定屏幕宽为750rpx。(在开发前尽量和视觉设计老师约定好设计文稿,例如750像素宽的设计稿能方便我们开展工作)。
雪碧图:
雪碧图自动生成图片及代码(建议灵活使用,本工具用于移动端项目雪碧图生成):
工具链接:链接
tabBar常规为图标搭配标题,具体配置可参考官方文档:链接
对比官方参考,大家小程序略去了tabBar.list.text配置,这样的处理方式主要是为还原设计稿。每个图标素材的像素大小为81px*81px,通过尝试:文字区域建议35px。
合理的使用三元运算,使代码更简洁。
样式模板举例:
<view class="box_start {{item.have == 1 ? 'on':'out'}}" catchtap="onRecommStart">...</view>
<view wx:if="{{item.tag == 'title' || item.tag == 'text'}}" class="{{item.tag}} {{item['level'] ? 'h2' : ''}}">...</view>
<view class="choice-dajia-view" style='height:{{viewIsshow == false ? windowHeight:"auto"}}' wx:if="{{!choiceWarp}}">...</view>
内容显示:
<view ...>{{item.have == 1 ? '取消收藏:'收藏''}}</view>
腾讯大家小程序选用wxpage框架。【链接】
WXPage 是一个极其轻量的微信小程序开发框架,其中的API蕴含了“极致页面打开速度的思想”,为可维护性与开发效率而设计的功能,框架来自“腾讯视频”小程序的项目沉淀。
框架对小程序生命周期的扩展(如onNavigate、onAppLaunch等很多有意思的扩展)、组件的依赖、实例方法(如$take、$emit、$put)、实用函数等都有一系列独特的包装,适用于组件开发。
特别感谢sendguan(关开设)在大家小程序开发中无私支持。
这里的工具方法指的是一些公用的方法或代码。通常根据业务的需要,我们可以建立一到多个模块,在模块里封装一些公用方法,一来方便调用,二来方便维护,如:
//Api.js
let Api = {
fun1: function() {
...
},
fun2: function() {
...
},
};
//接口
module.exports = {
fun1: fun1
};
这里分享一些大家小程序开发中封装的方法:
function downFile(url, callback) {
wx.downloadFile({
url: url,
success: function(res) {
callback(res.tempFilePath)
}
})
}
// get请求方法
function fetchGet(url, callback) {
wx.request({
url: url,
header: { 'Content-Type': 'application/json' },
success(res) {
callback(null, res.data)
},
fail(e) {
console.error(e)
callback(e)
}
})
}
// post请求方法
function fetchPost(url, data, callback) {
wx.request({
method: 'POST',
url: url,
header: { "Content-Type": "application/x-www-form-urlencoded" },
data: data,
success(res) {
callback(null, res.data)
},
fail(e) {
console.error(e)
callback(e)
}
})
}
function fetchData(url, data, callback) {
wx.request({
method: 'GET',
url: url,
data: data,
success(res) {
callback(res.data)
},
fail(e) {
console.error(e)
callback(e)
}
})
}
function removeBlock(s) {
let regex = "\\((.+?)\\)";
return s.match(regex)[1]
};
function removeAt(target, index) {
return !!target.splice(index, 1).length;
}
function remove(target, item) {
let index = target.indexOf(item);
return index > -1 ? removeAt(target, index) : false;
}
function getDateDay(str) {
let string = str.toString().substr(0, 10)
return string.replace(/-/g, '.');
}
function sliceArray(array, size) {
let result = [];
for (let x = 0; x < Math.ceil(array.length / size); x++) {
let start = x * size;
let end = start + size;
result.push(array.slice(start, end));
}
return result;
};
function getArrayItems(arr, num) {
let temp_array = new Array();
for (let index in arr) {
temp_array.push(arr[index]);
}
let return_array = new Array();
for (let i = 0; i < num; i++) {
//判断如果数组还有可以取出的元素,以防下标越界
if (temp_array.length > 0) {
let arrIndex = Math.floor(Math.random() * temp_array.length);
return_array[i] = temp_array[arrIndex];
temp_array.splice(arrIndex, 1);
} else { break; }
}
return return_array;
}
function reWirteUrl(url) {
//console.log(url);
if (url !== null) {
if (!/^(http:\/\/)/i.exec(url)) {
return url.replace(/(http:)?(?=\/\/img1\.gtimg\.com\/)/g, 'https:');
} else if (url.indexOf("https://") == -1) {
return url.replace("http://", "https://");
}
} else {
return 'https://mat1.gtimg.com/news/images/static/weixin/wxss/basicprofile_r3.png';
}
}
wxml支持import,在大家小程序开发过程中,实际结合了Wxpage对子模板、组件的定义方法。
模板示例(代码来源:何润锋工作室小程序):
<!-- 引入子组件模板 -->
<import src="/comps/header.wxml" />
<import src="/comps/player.wxml" />
<import src="/comps/playerintro.wxml" />
<import src="/comps/recommvideo.wxml" />
<import src="/comps/recommnote.wxml" />
<import src="/comps/comment.wxml" />
<view class="wxpage" style="height:{{windowHeight}}px" disable-scroll="false">
<!-- 使用子组件并传递数据 -->
<template is="header" data="{{...header}}"></template>
<!-- 播放 -->
<template is="player" data="{{...player}}"></template>
<!-- 播放 -->
<scroll-view class="scroll-posts-list {{select == false ? 'up':'down'}} {{upper == false ? 'in':'on'}}" scroll-y="true" style="height:800rpx;" bindscrolltolower="lower" scroll-top="{{scrollviewTop}}" bindscroll="upper">
<!-- 播放信息 -->
<template is="playerintro" data="{{...playerintro}}"></template>
<!-- 播放信息 -->
<!-- 推荐视频 -->
<template is="recommvideo" data="{{...recommvideo}}"></template>
<!-- 推荐视频 -->
<!-- 手记 -->
<template is="recommnote" data="{{...recommnote}}"></template>
<!-- 手记 -->
<!-- 评论 -->
<template is="comment" data="{{...comment}}"></template>
<!-- 评论 -->
</scroll-view>
</view>
子组件示例:
<template name="header">
<!-- 子组件事件绑定 -->
<view data-c="{{$id}}" bindtap="onTap" class="header">{{title}}</view>
</template>
let Api = require('../lib/api');
let Txv = require("../lib/video");
let C = require('../lib/wxpage').C
module.exports = C('player', function(vm) {
return {
/**
* 子组件独立的数据
*/
data: {
tvpVid: "",
tvpUrl: "",
tvpVideoError: "",
tvpState: "",
tvpIsAd: "",
tvpReportUrl: "",
select: false,
autoplaytype: false
},
getNetWork: function() {
wx.getNetworkType({
success: function(res) {
// 返回网络类型, 有效值:
// wifi/2g/3g/4g/unknown(Android下不常见的网络类型)/none(无网络)
networkType = res.networkType;
console.log(networkType);
if (networkType == 'wifi') {
vm.$set({
autoplaytype: true
})
}
}
})
},
/**
* 子组件独立的生命周期方法
*/
onLoad: function(query) {
that = this;
this.getNetWork();
}
}
});
备注:在开发过程或者调试过程中,可以使用使用全局函数getApp()、getCurrentPages()等获取栈内的所有页面,然后根据业务需求进行页面数据设置。
目前有两种办法
用户分享次数的统计功能主要利用onShareAppMessage()回调,通过绑定页面元素后对e.from !== 'menu'方式判断是否为右上角分享功能或页面自定义分享。
对于栏目和人群、时间段等数据的上报可以根据自己的实际业务要求来处理。
日常业务的渠道多来源于公众号打开、扫描小程序B码等。在开发的时候,对在onLoad后进行对sence进行解析。例如:
onLoad: function(options) {
let scene = decodeURIComponent(options.scene);
if (scene.indexOf("tid") !== -1) {
tidNum = scene.match(/\d+/);
if (tidNum && tidNum[0]) {
options.tid = tidNum[0]
}
}
vm.$set({ load: options,... });
}
对于特别的统计需求,我们可以利用接口给B码增加参数的形式去完成,在onLoad阶段进行参数解析。
例如:通过以下的传参生成的二维码我们就可以解析form:house方式扫码形成的访问渠道。
...QRCode&pages=pages/view&scene=tid%2C62585&form%2C=house
小程序数据分析,是面向小程序开发者、运营者的数据分析工具,提供关键指标统计、实时访问监控、自定义分析等功能,帮助小程序产品迭代优化和运营。
除url携带参数外,小程序与webview暂时没其它通信方法。因此,在小程序和webview的跳转中,如何同步登录状态、地址信息状态等,是一个难以解决的问题。微信小程序的webview支持的功能是比较完善的,与浏览器环境差距不大。目前看来,webview更适合完成一些静态展示页面或更新迭代较频繁的运营活动页面的需求(src参数value控制可以动态刷新webview)。未来功能完善后,可能会作为解决小程序跳转层级不够用、小程序代码过大等问题的解决方案。
小时候写作文,老师总让我们先列提纲。对于项目开发,先收集各种相关资料,选定框架、理清自己的项目结构、项目中可能的难点、可优化的地方等等,这对我们对开发的风险把控、开发进度估算都非常有益。
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。