到目前为止, 我们进行的主要是微信小程序前端(客户端)程序的开发, 而一个真正的微信小程序当然还应该有服务器端(后端), 才能实现更复杂的信息保存, 多用户之间信息交流等功能. 除非你就是想开发一个”单机版”的小程序, 那我无言以对…

本部分开始我们将进行”阅读打卡”小程序服务器端程序的开发, 并实现前/后端程序之间的数据交换.

15. 数据绑定背后的故事

在前一部分, 我们学习了所谓的数据绑定, 在前面的故事里, 我们可以隐约察觉到这样一个事实:

最终呈现在用户面前的界面其实是由 “死” 的模板(Template) 和 “活” 的数据(Data) 组合而成的.

所谓”死”的模板, 就是那些界面上相对不会改变的元素, 比如: 为了进行页面布局而使用的一些组件, 样式表…

而”活”的数据, 则就是那些会变化的数据, 比如: 日记标题, 内容, 用户的关注数, 粉丝数…

为了把死/活两部分东西分离, 我们建立了所谓的数据模型(Model)用于结构化地存储数据, 并引入数据绑定机制实现将”活”的数据组装到”死”的模板中去. 大概是这样子滴:

数据组装

目前为止, 我们的模板与数据都已经放在微信小程序的前端程序中 ( index.wxml / index.js ).

而大多数实际项目中, 数据往往来自服务器端. 如下图所示, 数据通过网络从服务器端传送到前端, 并注入到前端数据模型中, 在数据绑定机制的加持下, 最终组装成用户看到的界面.

数据组装

明白了这个道理之后, 我们的工作重点就转移到如何去实现一个服务器端程序, 让它可以向前端返回数据… 这就是下一小节的内容.

16. 服务器端实现

对于目前的需要来说, 我们的服务器端只需要负责向前端提供数据.

数据是现成的, 只需要把小程序端的整个数据模型的复制过来即可. 关键是如何提供一个”接口”, 让小程序端来对接, 从而取得数据.

能完成上述功能服务器端程序可以有很多种实现方法, 其中, 最常见的即是基于HTTP/HTTPS协议实现的 Web 服务器端程序. 说白了, 就是一个网站的服务器端(后端).

如果你曾经开发过网站, 那么应该知道, 有 N 种技术可以实现网站服务器端: ASP, ASP.NET, J2EE, PHP …

本教程中, 将基于 Node.js 来实现我们的小程序服务器端. 因为, 无须使用别的程序设计语言, JavaScript 即可.

关于 Node.js 的优势, 本文就不安利了… 另外, Node.js 的开发环境搭建, 请参看教程: 使用 Node.js 开发 Web 应用程序

好了, 现在假设你已经看完并简单实践了一下上述 “使用Node.js 开发 Web 应用程序” 的过程…

让我们快速开始吧 ~

16.1 创建项目

安装好 Node.js 环境后, 找一个你喜欢的位置, 新建一个文件夹: daily-reading-server

在控制台中进入 daily-reading-server 文件夹, 执行 npm init, 一路回车, 初始化 Node.js 项目.

然后, 使用你喜欢的集成开发工具 ( 本教程使用 Visual Studio Code ) 打开 daily-reading-server 文件夹.

Server

初始化好的 Node.js 项目中只有一个 package.json 文件, 它的是项目的配置文件, 内容如上图.

在项目根目录下手动创建 index.js, 根据 package.json 中的配置 ( 上图第 5 行 ), 这将是你程序的入口.

按理说, 我们完全可以使用 Node.js 的原生功能模块 ( http ), 即不借助第3方模块实现 Web 服务器端. 但这样做的话, 在程序变得越来越复杂的时候, 将变成不容易管理, 同时, 代码写起来也越来越麻烦.

因此, 这里, 我们借助 Express 框架, 来帮助我们实现服务器端程序.

Express 是 Node.js 世界中大家常用的一个轻量级的 Web 框架. 参见: https://www.expressjs.com.cn/

除 Express 外还有很多优秀的第三方Web框架, 比如: KOA, Egg … 这里力求简单, 暂使用 Express, 其它更高大上的东西, 等你长大后自学吧…

控制台项目根目录下 ( daily-reading-server ) 执行 npm install express --save

一顿下载后, 若控制台没有输出什么错误信息的话, Express 就安装好了…

你可以在 package.json 中看到多出一段代码, 类似…

1
2
3
"dependencies": {
"express": "^4.17.1"
}

同时, 项目文件夹下出现一个新的文件夹: node_modules

package.json 中记录了本项目依赖于 Express 4.17.1 以上版本, 而 node_modules 文件夹中是 Express 以及它所依赖的别的项目代码 (无须过份关心).

执行 npm install 时有可能因国内网络环境影响, 下载过程异常漫长, 或直接安装失败.

可以考虑使用淘宝 npm 镜像, 控制台下依次执行如下两行命令, 然后再执行 npm install

1
2
npm config set registry http://registry.npm.taobao.org/
yarn config set registry http://registry.npm.taobao.org/

OK ~ 如果 Express 安装一切正常, 你就可以接着下面的操作了…

index.js 文件中输入如下代码:

**Code 16-1: **

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const express = require('express');       // 引入Express
const app = express(); // 初始化Express
const port = 8000; // 使用8000端口

// 路由注册
app.get('/', function(req, resp) {
const yourName = req.query.name; // 获得前端传来的"query"参数
resp.send(`Welcome ${yourName}!`);
});

// 启动服务器端程序
app.listen(port, function() {
console.log('启动成功! 快试试吧~ http://localhost:' + port + '/');
});
  • 第 6 行, app 是 Express 实例(对象), app.get('/', function(){ ... });进行所谓的路由注册, 即: 若前端向 http://localhost:8000/ 发起 HTTP GET请求, 则将触发其第2个参数 function() { … } 所定义的回调函数. ( 呵呵, 又是回调 ~ )

    • app.get() 监听 HTTP GET 请求, 而若使用 app.post() 则监听 HTTP POST 请求. ( 关于 GET / POST 的区别, 参看: HTTP 方法:GET 对比 POST )
    • 回调函数的两个参数: reqresp分别对应前端请求(Request)和服务器端回应(Response) . 所以, 通过 req 对象可获得前端信息, 而 resp 对象可用于向前端返回信息.
    • resp.send()向前端发送回应数据.
  • 第 6 ~ 9 行进行的仅是监听注册, 真正服务在第 12 行执行 app.listen()后 才启动的. ( 又见回调函数 ~ )

  • 关于端口的知识, 可参阅: 网络通信简明教程

好的, 现在… 启动程序: 控制台下项目根目录执行 node index.js启动程序.

当然, 建议使用调试工具, 比如Visual Studio Code运行(调试)工具:

Server

我想上面的截图已经清楚说明了如何使用 VS Code 运行(调试)工具, 跟着上图的步骤做一遍就清楚了.

当然, 你还可以设置”断点”, 方便调试程序:

Server

最后, 打开浏览器, 在地址栏中输入 http://localhost:8000/?name=Bailey, 回车即可看到效果.

Server

无论你使用什么开发工具(IDE), 学写程序首先要学会怎么调试程序, 别程序一出问题, 就一脸无辜的样子…

16.2 做向小程序端提供数据的服务端

上一小节主要介绍如何创建项目, 写了一个小例子, 算是测试下服务器端开发环境是否正常.

接下来, 我们来实现真正能向小程序端提供数据的服务器端程序.

index.js 的代码(Code 16-1)改成如下的样子:

**Code 16-2 : **

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const express = require('express');       // 引入Express
const app = express(); // 初始化Express
const port = 8000; // 使用8000端口

// 路由注册
app.get('/', function(req, resp) {
const data = {
userInfo: {
favCnt : 19,
fansCnt: 37,
diaryCnt: 188,
},
diaries: [{
title: '冰雪奇缘',
publishTime: new Date().getTime(),
cover: 'https://bailey.pinruikm.com/images/miniprogram/dr1.gif',
content: '小国阿伦黛尔因一个魔咒永远地被冰天雪地覆盖,为了寻回夏天,安娜公主和山民克里斯托夫以及他的驯鹿搭档组队出发,展开一段拯救王国的历险。',
readCnt: 19,
praiseCnt: 8,
author: 'Bailey'
},{
title: '小王子',
publishTime: new Date().getTime(),
cover: 'https://bailey.pinruikm.com/images/miniprogram/dr2.jpg',
content: '小王子从自己星球出发前往地球的过程中,所经历的各种历险。作者以小王子的孩子式的眼光,透视出成人的空虚、盲目,愚妄和死板教条,用浅显天真的语言写出了人类的孤独寂寞、没有根基随风流浪的命运。',
readCnt: 4,
praiseCnt: 3,
author: '小可爱'
},{
title: '白雪公主',
publishTime: new Date().getTime(),
cover: 'https://bailey.pinruikm.com/images/miniprogram/dr3.jpg',
content: '白雪公主受到继母皇后,逃到森林里,遇到七个小矮人的故事。',
readCnt: 12,
praiseCnt: 7,
audioPath: 'https://bailey.pinruikm.com/images/miniprogram/audio3.mp3',
audioDuration: 256,
author: '小宝贝'
}]
};
resp.send(data);
});

// 启动服务器端程序
app.listen(port, function() {
console.log('启动成功! 快试试吧~ http://localhost:' + port + '/');
});

这段代码只是替换了 Code 16-1 中的第 7 ~ 8 行.

可能你已经注意到了, 我们把小程序端的所谓”数据模型”搬了过来… 放在 7 ~ 40 行, 然后通过 41 行的 resp.send 方法向前端输出.

现在, 在浏览器中打开 http://localhost:8000, 嘿嘿, 是不是看到数据被输出到了浏览器窗口中了.

这里我们其实是在使用浏览器来模拟微信小程序端的操作, 下一小节将真正要使用小程序来对接服务器端了.

很激动吧! 赶快开始…

16.3 微信小程序对接服务器端

回到微信开发者工具, 将 /pages/index/index.js中的代码修改为如下内容(可直接复制去覆盖):

**Code 16-3 : **

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Page({
data: { },
onLoad: function () {
const _this = this;
wx.request({
url: 'http://localhost:8000', // 请求地址
success: function(resp) { // 请求成功的回调函数
const dataFromServer = resp.data;
_this.setData({
userInfo: dataFromServer.userInfo,
diaries: dataFromServer.diaries
});
}
})
}
})

这段代码突然变得有些复杂了, 我们缓下脚步, 慢慢分析…

  • 第 2 行, 原先 data 里的内容被删除了, 因为现在的数据应该来自服务器端了, 即: 小程序端不应再持有原先那些数据, 故删除之

  • 第 3 ~ 15 行, onLoad回调函数, 前面我们说过, 此函数将在小程序页面加载时自动执行. 我们把从服务器端请求数据的代码 ( 4 ~ 14 行) 写在 onLoad 函数中, 即意味着, 当页面加载时将自动从服务器端获取数据, 并填充到小程序前端的数据模型(data)中.

  • 第 5 ~ 14 行, 使用微信提供的, 向服务器端发起 HTTPS 网络请求的 API :wx.request(), 从服务器端获取数据. ( 如果你曾经使用过 jQuery 之类的东东, 应该很熟悉这段代码的结构 )

    • 调用 wx.request() 函数时需要传入一个 JavaScript 对象作为其参数, 此对象参数中包含了请求的配置信息
    • 第 6 行, url 服务器端接口的地址, 应与Code 16-2中第 6 行的路由地址一致. 即: 上一小节中, 我们在浏览器中测试时使用的地址.
    • 第 7 ~ 12 行, 数据成功从服务器端返回的回调函数. 注意, 服务器端返回的数据从回调函数的第1个形参(resp)处注入. 从第 8 行可以出, 我们需要的从服务器端返回的数据在 resp.data 中.
    • 第 9 ~ 12 行, 使用 this.setData() 方法, 将从服务器端返回的数据注入到小程序前端的数据模型(data)中, 借由本教程第三部分已经完成的数据绑定, 数据最后呈现在界面上…
  • 特别说明: 前文曾经说过, JavaScript 中的 this代词指向的是代码执行时的”宿主” ( 专业说法: scope ) . 也就是说, 在上述代码的第 4 行处, this 指向的是当前页面, 因为此处处于页面的 onLoad 生命周期回调函数中.

    而观察第 9 行, 我们需要使用 this.setData() 方法将从服务器取得的数据填充到小程序端数据模型中, 问题来了… 如果直接使用 this.setData() 方法, 那么此时的 this 并非指向当前页面 (page对象), 因为这小段代码处于 wx.request() 的 success 回调函数中.

    所以, 我们需要在第 4 行处使用一个临时常量 _this来暂存 page 对象, 然后在第 9 行时使用 _this.setData()来填充数据.

    上述小技巧在小程序的开发过程中经常会使用到, 因为在 JavaScript 的世界里, 函数回调随处可见. 一定要保持头脑清楚, 明白 this 到底指向谁 ?

关于 wx.request() 的完整信息参见: https://developers.weixin.qq.com/miniprogram/dev/api/network/request/wx.request.html

是不是已经迫不及待的想看看效果了? 那就保存一下代码吧 ~ ( 事到如今, 你应该知道微信开发者工具在保存时会自动刷新了吧… )

见鬼啦! 是不是小程序主界面变成一遍空白… 而且还报错…

嘿嘿, 别急 ~

微信小程序在进行网络通信时必须使用 HTTPS 协议, 而我们目前的服务器端实现仅支持 HTTP 协议请求. 怎么办?

要么… 把让服务器端支持 HTTPS 请求. 这个有点复杂, 我们暂不讨论…

要么… 在开发阶段, 你可以点击微信开发者工具右上角的”详情”按钮, 然后在弹出的面板中选中下图中 ② 所示的复选框.

Server

简单来说, 就是让微信小程序平台先别烦你…

现在, 应该可以看到小程序主界面上有内容了吧 ~ 但愿如此…

出于安全考虑, 微信小程序正式发布时, 将被强制使用 HTTPS 协议进行网络通信, 同时访问的服务器域名也必须在小程序管理平台中进行配置. 这个, 我们以后再说…

16.4 完善一下

在 16.2 节中, 我们实现了小程序端与服务器端的对接. 但可能你已发现, 如果一个手贱把代码写错了, 那就只有通过观察控制台(Console) 的输出才能知道原因…

如果是上线交付的产品, 显然, 这不科学… 总不可能让客户来看控制台吧 ~ 何况, 有些时候, 可能你的代码并没有问题, 仅只是因为网络的原因, 导致和服务器端的通信出现故障, 也同样会报错.

所以, 很有必要把程序改得”文雅”一些… 在/pages/index/index.js中添加几行代码:

**Code 16-4 : **

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Page({
data: { },
onLoad: function () {
const _this = this;
wx.showLoading({
title: '数据加载中...',
});
wx.request({
url: 'http://localhost:8000', // 请求地址
success: function(resp) { // 请求成功的回调函数
const dataFromServer = resp.data;
_this.setData({
userInfo: dataFromServer.userInfo,
diaries: dataFromServer.diaries
});
wx.hideLoading();
},
fail: function(resp) {
wx.showToast({
title: '数据请求失败',
icon: 'none'
});
}
})
}
})

在 Code 16-3 的基础上添加了 3 段代码: 5 ~ 7 行, 16 行, 18 ~ 23 行

  • 第 5 ~ 7 行, 发起网络请求前显示一个 “数据加载中…” 的进度提示. 考虑到网络因素, 可能半天数据都还没从服务器端返回, 此时为让用户不至于一脸的懵, 显示一个进度提示, 让他好受点…
  • 第 16 行, 数据已从服务器端返回, 并且已刷新了界面, 隐藏进度提示
  • 第 18 ~ 23 行, 这是 wx.request() 函数的失败回调函数, 即: 当网络请求失败时回调此函数(fail). 其中的 wx.showToast() 用于显示一个消息提示, 至于是个什么样子的东西… 你迟早会见到 ~ ( 故意把第 9 行的地址写错试试 )

OK ~ 加上上述代码后, 再次保存, 是不是感觉文雅多了?

嘿嘿, 可能你看不到什么变化… 这只能说明 2 点: (1) 你人品比较好, 代码一点错误都没有 (2) 你的电脑好牛B, 运行速度好快!

好傲娇! 好无奈!

文档传送门: wx.showLoading, wx.showToast

微信官方文档的链接是否已经放在你的收藏夹中了? 没事多去逛逛, 学了这么多套路, 应该可以看得懂不少内容了…

真想看效果, 接着看下节…

16.5 静心感悟

这个标题略带文艺小青年的骚气 ~

现在, 先别忙着往前走, 让我们静下心下来, 梳理一下思路…

在你的程序中打满各种断点, 如下图中标注的位置. 注意, 左边是微信小程序前端代码, 右边是服务器端代码.

Server

重新启动服务器端程序, 刷新小程序端…

你应该可以看到, 程序依次在图中各断点处停住, 等待你点击”继续(Resume)”按钮…

注意观察程序在断点处的停留顺序: ① → ② → → ③

建议多重复几遍, 认真体会前/后端的程序是如何协同工作的?

17. 手机上看效果

我猜你已经尝试过, 想在手机上看看你的小程序运行效果了吧 ~

但是… 可能很悲催… 电脑上一切正常, 搬到手机上就死活一片白茫茫…

来吧, 一起看看到底是什么原因?

Server

上图是”微信开发者工具”顶部的截图, 点击 “预览” 或 “真机调试”, 手机微信扫一扫…

“预览” 相当于直接在手机上以运行模式启动(Run), “真机调试” 相当于在手机上以调试模式启动(Debug)

运行速度上当然是预览更快, 但不能像真机调试那样设置断点, 进行调试.

这回看到了吧, 是不是一片白茫茫…

为什么? 答案在这里: 移动 APP / 小程序调试

有空的话, 把上面这篇文章完整看完吧… 反正… 微信小程序这个教程还很长… 也不急于此时.

况且… 这节课已经下课了 ~

到目前为止的代码: daily-reading_part4.zip

18. 小结

学完本部分教程需要掌握的”套路”:

  • 模板(Template) + 数据(Data) = 用户最终看到的界面

  • 在近年的一些网站开发实践中, 服务器端更多地是充当静态资源( HTML, CSS, JavaScript, 图片…) 和 数据的提供者, 静态资源(模板) 与 数据的组装工作通常在前端进行.

    首先, 浏览器从服务器端取得初始的静态模板, 然后通过 Ajax 请求的方式从服务器端获取数据, 最后在前端(客户端)进行组装. ( 两步请求 )

    感觉服务器端做的事情好简单, 而客户端相对复杂. 此外, 客户端代码还要完成那些炫酷的渲染, 响应客户的骚操作… 总之, 客户端好累! 这就是所谓的”胖”客户端.

    对于微信小程序和混生APP ( Hybrid App ) 来说, 又把上述做法更进一步… 直接把静态的东西打包到了小程序(APP)安装包中, 而服务器端基本上只负责处理数据, 响应业务逻辑.

  • 在很多技术 ( 如: ASP, ASP.NET, PHP, JSP… ) 的传统应用中, 上述的组装操作则在服务器端完成. 如果你使用过这些技术开发网站, 回想一下, 是不是经常会在 .asp / .aspx / .php / .jsp 的页面里嵌入所谓的”服务器端脚本”? 这归根结蒂, 难道不是在模板中嵌数据吗?

    怕有人抬杠, 再补充一句: 上一段话有意加粗了 “传统应用” 4 个字. 如果你的网站应用了前端框架(Angular, Vue, React…) , 那么就不是我这里所说”传统”了, 其工作方式更像上面一条所述. 此时的 PHP 也主要是承担”数据源”的角色. 而 JSP 更多时候会使用 Servlet 来替代.

  • 近年来, 很多技术(框架) 中对模板引擎的应用, 又把”组装” 这件事情放在了服务器端完成. 比如: nunjucks , pug

是不是感觉已经学不动了… 呵呵~ 领会第 1 条就行.