微信小程序WeSJTU开发心得

前两天,腾讯释放了微信小程序公测的消息,和很多开发者一样,我也非常兴奋。十一期间没什么事,就试着写了一个微信小程序WeSJTU,现在,终于可以在手机微信上跑起来了(以前是在电脑的微信开发者工具上模拟的)。

构思

微信小程序的文档,比较难的部分,如数据绑定,其思想和Angular是非常相近的,而所谓的WXSS,基本上可以认为是CSS,只是不方便用SCSS了——WXSS原则上不支持级联,所以整个文档看下来基本上就能上手了。

先从简单有意思的开始吧,所以我设想了三个Tab

  • 同去同去网是交大活动发布的主要阵地,它有很完善的API,可以通过GET请求返回活动列表、活动详情的JSON数据,另外还支持分页。
  • 2048. 根据别人写的数字2048,打算改成文字2048——苟利国家生死以,岂因祸福避趋之,蛤蛤…
  • 反馈。这一部分,想做一个简单的反馈功能,用户可以在小程序里反馈,我可以在网页上查看反馈内容列表。

代码

同去

活动列表页面,前端可以用小程序的列表渲染很方便地实现。

<scroll-view class="acts-list" scroll-y="true" bindscrolltolower="lower">
<block wx:for="{{actsList}}">
<view class="act-item" index="{{index}}" id="{{item.actid}}" catchtap="redictDetail">
...
</view>
</block>
<view class="load-more" hidden="{{moreHidden}}">
<view class="load-content">
<text class="weui-loading"></text>
<text class="loading-text">玩命加载中</text>
</view>
</view>
</scroll-view>

小程序的wx.request发起的是HTTPS请求,在后端用Node.js将请求转发一下。

const tqUrl = 'https://tongqu.me';
const defaultPosterArray = [1, 2, 4, 5, 7, 8, 9];

router.get('/acts', (req, res, next) => {
let query = url.parse(req.url, true).query;
let offset = query.offset;
let order = query.order;
let indexUrl = `${tqUrl}/index.php/api/act/type?type=0&offset=${offset}&order=${order}`;
let options = url.parse(indexUrl);
options.headers = {
'User-Agent': 'javascript'
};
https.get(options, (response) => {
let source = "";
response.on('data', (data) => {
source += data;
});
response.on('end', () => {
source = JSON.parse(source);
let actsList = [];
let acts = source.result.acts;
let actsLength = acts.length;
for (i = 0; i < actsLength; i++) {
let poster = acts[i].poster;
let status = acts[i].time_status_str;
let status_style = '';
if (!poster) {
poster = `${tqUrl}/images/act_default_poster/${defaultPosterArray[Math.floor(Math.random()*defaultPosterArray.length)]}.jpg`;
}
if (status == "人数已满") {
status_style = "act-status-full";
}
if (status == "未开始报名") {
status_style = "act-status-todo";
}
if (status == "报名已结束") {
status_style = "act-status-done";
}
let act = {
actid: acts[i].actid,
poster: poster,
name: acts[i].name,
status: status,
status_style: status_style,
time: acts[i].start_time.substr(5) + " ~ " + acts[i].end_time.substr(5),
location: acts[i].location,
source: acts[i].source,
views: acts[i].view_count,
members: acts[i].member_count
};
actsList.push(act);
}
res.send({
actsList: actsList
});
});
});
});

活动详情页面,由于活动内容有文字有图片,所以列表渲染的思路是——文字就渲染文字,图片就渲染图片。

<scroll-view class="act-contents" scroll-y="true">
<block wx:for="{{actContents}}">
<view wx:if="{{item.textIf || item.imageIf}}" class="act-paragraph">
<text wx:if="{{item.textIf}}" class="act-text">{{item.text}}</text>
<view wx:if="{{item.imageIf}}" class="image-center"><image mode="aspectFit" src="{{item.image}}" id="{{item.image}}" catchtap="previewImg"></image></view>
</view>
</block>
<view class="act-end">
<view class="act-end-line"><view class="act-end-line-wrap"></view></view>
<view class="act-end-text"></view>
<view class="act-end-line"><view class="act-end-line-wrap"></view></view>
</view>
</scroll-view>

为了配合前端,后端需要做好数据的处理。

router.get('/act', (req, res, next) => {
let query = url.parse(req.url, true).query;
let id = query.id;
let indexUrl = `${tqUrl}/index.php/api/act/detail?id=${id}`;
let options = url.parse(indexUrl);
options.headers = {
'User-Agent': 'javascript'
};
https.get(options, (response) => {
let source = "";
response.on('data', (data) => {
source += data;
});
response.on('end', () => {
source = JSON.parse(source);

let main_info = source.main_info;
let poster = main_info.photolink;
let status = main_info.time_status_str;
let status_style = '';
if (!poster) {
poster = `${tqUrl}/images/act_default_poster/${defaultPosterArray[Math.floor(Math.random()*defaultPosterArray.length)]}.jpg`;
}
if (status == "人数已满") {
status_style = "act-status-full";
}
if (status == "未开始报名") {
status_style = "act-status-todo";
}
if (status == "报名已结束") {
status_style = "act-status-done";
}

let actContents = [];
let images = [];
let $ = cheerio.load(source.body);

if ($("p").length) {
$("p").each(function() {
let text = $(this).text().trim();
let textIf = true;
if (!text) {
textIf = false;
}
let $img = $(this).children("img");
let imageIf = false;
let image = "";
if ($img.length) {
imageIf = true;
image = $img.attr("src").replace(/^https?:\/\/(www.)?tongqu.me/, tqUrl);
images.push(image);
}
if (textIf || imageIf) {
let actContent = {
text: text,
textIf: textIf,
image: image,
imageIf: imageIf
};
actContents.push(actContent);
}
});
} else {
let actContent = {
text: $.text().trim(),
textIf: true,
image: "",
imageIf: false
};
actContents.push(actContent);
}

let actDetail = {
poster: poster,
name: main_info.name,
status: status,
status_style: status_style,
time: main_info.start_time.substr(5) + " ~ " + main_info.end_time.substr(5),
location: main_info.location,
source: main_info.source,
actContents: actContents,
images: images
};
res.send(actDetail);
});
});
});

2048

2048,代码的思路还是比较清晰的——通过触摸动作返回的坐标值判断手指滑动方向,然后移动合并,判断输赢,随机插入等。其中,数字转文字的代码是通过循环实现的,简单粗暴。

number2word: (arr) => {
let numbersArray = [0, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384];
let wordsArray = ['他', '苟', '利', '国', '家', '生', '死', '以', '岂', '因', '祸', '福', '避', '趋', '之'];
let wordnumbers = arr;
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
let index = numbersArray.indexOf(wordnumbers[i][j].number);
wordnumbers[i][j].word = wordsArray[index];
}
}
return wordnumbers;
}

用模态框提醒游戏结束。

<modal class="modal" confirm-text="吼啊" cancel-text="不吼" hidden="{{failHidden}}" bindconfirm="modalConfirm" bindcancel="modalCancle">
<view>图样图森破!</view><view>重新开始,吼不吼啊?</view>
</modal>
<modal class="modal" confirm-text="吼啊" cancel-text="不吼" hidden="{{successHidden}}" bindconfirm="modalConfirm" bindcancel="modalCancle">
<view>比一般人不知高到哪里去!</view><view>再来一次,吼不吼啊?</view>
</modal>

感觉膜得也太暴力了…

反馈

前端是一个表单,反馈内容由用户输入,反馈者的基本信息由小程序的wx.getUserInfo获得。

<form class="feedback-form" bindsubmit="formSubmit">
<view class="feedback-form-wrapper">
<view class="feedback-title">期待您的反馈</view>
<view class="input-wrapper">
<input class="feedback-input {{inputStyle}}" name="feedback" placeholder="建议或吐槽..." bindfocus="inputFocus" bindblur="inputBlur" />
</view>
<view class="btn-wrapper">
<button class="feedback-btn" formType="submit" type="primary" plain="true">提交</button>
</view>
</view>
</form>

后端将前端通过POST请求提交的表单数据写入到SQLite中。

router.post('/feedback', function(req, res, next) {
let nickname = req.body.nickname;
let gender = req.body.gender;
let content = req.body.content;
if (gender == 1) {
gender = "男";
} else if (gender == 2) {
gender = "女";
} else {
gender = "未知";
}
db.run("INSERT INTO feedback (time, nickname, gender, content) VALUES ($time, $nickname, $gender, $content)", {
$time: moment().format('MM-DD HH:mm'),
$nickname: nickname,
$gender: gender,
$content: content
}, function(err) {
let data = "success";
if (err) {
data = "fail";
}
res.send(data);
});
});

然后,通过网页就可以很方便地查看了。

<div class="feedback">
<div class="feedback-wrap">
<div class="feedback-title">
<p>反馈列表</p>
</div>
<table class="feedback-table">
<thead>
<tr>
<th class="col1">时间</th>
<th class="col2">昵称</th>
<th class="col3">性别</th>
<th class="col4">内容</th>
</tr>
</thead>
<tbody>
<% let i = 0; feedbackList.forEach(function(item) { if (i < 5) { %><tr>
<td class="col1td"><%= item.time %></td>
<td class="col2td"><%= item.nickname %></td>
<td class="col3td"><%= item.gender %></td>
<td class="col4td"><%= item.content %></td>
</tr><% } i++; }) %>
</tbody>
</table>
</div>
</div>

效果

同去

同去

活动详情

2048

2048-入口

2048

反馈

关于

小程序WeSJTU的前端代码已经在GitHub上开源了——点此前往,欢迎学习交流。