一个微信小程序的完整实现思路
(Go、iris、xorm、Frp)

由于自己本身是在外地工作,同城的老同学也是各顾各家、各忙事业,极少有机会能够碰面,更加珍惜一年到头少有的聚会。能否利用位置共享,在某一个中午的工作餐馆或下班路上的地铁口,增加一下熟人的偶遇机会。亦或是过年长假宅在老家,发现附近一公里有哪些老同学正在喝(cuō)茶(má)。


鉴于此,基于Golang开发一个微信小程序——“偶遇一下”。

先把自己凌乱的需求进行简要梳理:

result

资源需求

1、至少一台4G+5M外网服务器
2、域名+https SSL证书

下面进入正题


目录:


现有资源、技术实现

1、手上有一台1G+5M腾讯云服务器,完全达不到部署要求,决定暂时用frp做本地服务器的外网穿透
2、现有域名+SSL证书 heltoo.xyz

部署架构

result

首先将网络环境调通,着重记录一下frpnginx的部分配置

frp

分别将内网服务器和云服务器安装frp,下载64位Linux版本放在两个服务器上解压,有frpc(Cilent端)和frps(Server端)的相关文件。

现拿frps进行举例:

frps为启动程序;frps.ini为配置文件;frps_full.ini为供参考的全量配置文件

在有外网的云服务器上将frps启动

cd /opt/frp/
./frps -c ./frps.ini
1
2

如需要进行后台启动,将systemd文件夹下的frps.service拷至/etc/systemd/system/frps.service并注意修改相关路径,将frp注册进service。如:










 




[Unit]
Description=Frp Server Service
After=network.target

[Service]
Type=simple
User=nobody
Restart=on-failure
RestartSec=5s
ExecStart=/opt/frp/frps -c /opt/frp/frps.ini

[Install]
WantedBy=multi-user.target
1
2
3
4
5
6
7
8
9
10
11
12
13

配置好之后,直接systemctl start frps即可。

简要贴一下frps.ini的配置

[common]
bind_port = 7000
vhost_http_port = 8090
token = 12345678
log_file = ./frps.log
log_level = info
log_max_days = 3
1
2
3
4
5
6
7

在内网服务器上安装frp后,配置frpc.ini并后台启动,frpc.ini配置如下

[common]
#云服务器地址
server_addr = 111.111.111.111
server_port = 7000
token = 12345678
log_file = ./frpc.log
log_level = info
log_max_days = 3

#云音乐代理
[ssh01]
type = tcp
local_ip = 127.0.0.1
local_port = 2333
remote_port = 2333

#配置SSH远程端口
[ssh02] 
type = tcp
local_ip = 127.0.0.1
local_port = 22
remote_port = 7022

#配置MySQL外网端口
[ssh03]
type = tcp
local_ip = 127.0.0.1
local_port = 3306
remote_port = 3306

#配置Golang应用http服务并指定域名
[web]
type = http
local_port = 8080
custom_domains = heltoo.xyz
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

至此,我们可以使用云服务器的7022端口远程至内网服务器,使用3306端口远程操作内网Mysql,使用heltoo.xyz:8090(frps配置为8090)访问内网Golang后台http服务

nginx

nginx在本项目的作用就一个,将指向https的请求反向代理至刚才frp配置的http8080端口上,即希望通过https://heltoo.xyz来访问内网Golang后台http服务。nginx配置如下:
















 









server {
    listen 443 ssl;
    server_name heltoo.xyz;
	ssl on;
	root /usr/share/nginx/html;
    
    #证书路径
    ssl_certificate "/etc/pki/nginx/1_www.heltoo.xyz_bundle.crt";
    ssl_certificate_key "/etc/pki/nginx/private/2_www.heltoo.xyz.key";

    ssl_session_cache    shared:SSL:1m;
    ssl_session_timeout  5m;
	
	#反向代理frp所配置的8090
	location / {
      proxy_pass http://127.0.0.1:8090;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP 111.111.111.111;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
	
    #让http请求重定向到https请求
    error_page 497  https://$host$request_uri;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

环境准备完成

至此,已完全满足微信小程序对发布环境的所有要求,网络一切通畅!下面将进行开发方面的准备工作

本文严禁以任何形式转载!


开发准备

形成部署整体思路后,开始进行开发技术选型及准备:

  1. 技术选型,列出市场主流小程序前端框架:

    Chameleon:滴滴出品的跨平台MVVM框架,支持平台:web、微信小程序、支付宝小程序、百度小程序、android(weex)、ios(weex)、qq 小程序、头条小程序(alpha版本)、快应用(进行中)

    一端所见即多端所见——多端高度一致,无需关注各端文档。

    result

    uniapp:是一个使用 Vue.js 开发所有前端应用的框架,开发者编写一套代码,可编译到iOS、Android、H5、以及各种小程序等多个平台。即使不跨端,uni-app同时也是更好的小程序开发框架。是很多公司的框架所选

    result

    taro:遵循 React 语法规范的多端统一开发框架,本身比较完备,但我的当前知识体系是vue,故暂不考虑

    wepy:听说坑比较多

    mpvue:美团出品,官方维护很慢,文档非常少。上手非常慢

    但既然是微信小程序练手项目,还是基于原生微信小程序框架开发,这样对微信SDK更为了解。后期会考虑尝试chameleon多端。

  2. Golang后台框架:

    选择iris框架,主要目的是搭建HTTPS接口作为小程序后端接口

    关系数据库选择MySQL,用于存储微信及业务数据

    选择go-xorm作为ORM库来操作MySQL

  3. WebSocket实现方案探索:

    本项目由于和社交有关,必须要有实时推送等互动效果,如:位置共享功能、活动发布需要实时显示。关于WebSocket,由于已使用iris框架,暂不考虑其他ws相关框架,如非常完善的gorilla/websocket的封装库。决定使用iris自身对websocket的封装,官方示例在:

    github.com/kataras/iris/_examples/websocket/chat
    
    1

    需要注意的一点是:

    前端iris使用的是自己封装的类库,event等交互相关的参数在前后台都做了封装,前台使用原生window.。如果将其运行在普通web环境和框架中没有任何问题,但微信小程序官方限制了window对象的使用,小程序前端无法使用原生WebSocket对象,只能使用微信封装的wx.connectSocket等系列方法。

    基于此,需要阅读iris websocket相关前后台源码,改造iris的js类库,将小程序对websocket的使用匹配iris

开发示例

websocket

不要在每个页面需要时建立websocket连接,而是应在小程序onLaunch事件的同时,初始化Socket,相关app.js代码如下:

globalData: {
    //全局变量定义callback函数
    callback: function() {}
},
//socket初始化
initSocket() {
    let that = this
    that.globalData.localSocket = wx.connectSocket({
      //此处 url 可以用来测试
      url: config.wsUrl
    })
    //版本库需要在 1.7.0 以上
    that.globalData.localSocket.onOpen(function(res) {
      console.log('WebSocket连接已打开!readyState=' + that.globalData.localSocket.readyState)
    })
    that.globalData.localSocket.onError(function(res) {
      console.log('WebSocket连接异常,readyState=' + that.globalData.localSocket.readyState)
    })
    that.globalData.localSocket.onClose(function(res) {
      console.log('WebSocket连接已关闭!readyState=' + that.globalData.localSocket.readyState)
      that.initSocket()
    })
    that.globalData.localSocket.onMessage(function(res) {
      // 用于在其他页面监听 websocket 返回的消息
      that.globalData.callback(res)
    })
},
//统一发送消息,可以在其他页面调用此方法发送消息
sendSocketMessage: function(evt, msg) {
    let that = this
    return new Promise((resolve, reject) => {
      if (this.globalData.localSocket.readyState === 1) {
        console.log('发送消息evt:', evt, " msg:", msg)
        this.globalData.localSocket.send({
          data: 'prefix:' + evt + ';0;' + msg,
          success: function(res) {
            resolve(res)
          },
          fail: function(e) {
            reject(e)
          }
        })
      } else {
        console.log('已断开')
      }
    })
  }
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

本文严禁以任何形式转载!

在业务代码的index.js中调用send,并且监听onmessage

// 发送和接收 socket 消息
sendSocketMessage: function (msg) {
    let that = this
    return new Promise((resolve, reject) => {
      app.sendSocketMessage('chat',msg)
      app.globalData.callback = function (res) {
        var resData = ws.messageReceivedFromConn(res)
        if (resData.msg == msg && resData.evt == "chat") {
          console.log('收到服务器内容chat ', res)
          that.setData({
            isInitScale: false
          })
          that.selectUserGroup()
        }
        resolve(res)
      }
    })
  },
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

改造iris原有的对websocket的参数处理,放在util文件夹作为公共方法库:ws.js

var websocketMessagePrefix = "prefix:";
var websocketMessageSeparator = ";";
var websocketMessagePrefixLen = websocketMessagePrefix.length;
var websocketMessageSeparatorLen = websocketMessageSeparator.length;
var websocketMessagePrefixAndSepIdx = websocketMessagePrefixLen + websocketMessageSeparatorLen - 1;
//获取自定义事件名,与后台对应
function getWebsocketCustomEvent(websocketMessage) {
  if (websocketMessage.length < websocketMessagePrefixAndSepIdx) {
    return "";
  }
  var s = websocketMessage.substring(websocketMessagePrefixAndSepIdx, websocketMessage.length);
  var evt = s.substring(0, s.indexOf(websocketMessageSeparator));
  return evt;
};
//获取自定义消息字符串
function getCustomMessage(event, websocketMessage) {
  var eventIdx = websocketMessage.indexOf(event + websocketMessageSeparator);
  var s = websocketMessage.substring(eventIdx + event.length + websocketMessageSeparator.length + 2, websocketMessage.length);
  return s;
};
//处理websocket返回消息
function messageReceivedFromConn(data) {
  var message = data.data;
  if (message.indexOf(websocketMessagePrefix) != -1) {
    var event_1 = getWebsocketCustomEvent(message);
    if (event_1 != "") {
      return {
        evt: event_1,
        msg: getCustomMessage(event_1, message)
      }
    }
  }
};

module.exports = {
  messageReceivedFromConn
}
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

生成Xorm映射对象

cd C:\work\workspace_go\src\github.com\go-xorm\cmd\xorm
xorm reverse mysql root:123@tcp(127.0.0.1:3306)/ouyuyixia?charset=utf8 templates/goxorm
1
2

Go编译在Linux下

cd C:\work\workspace_go\src\ouyuyixia
set GOOS=linux
set GOARCH=amd64
go build main.go
1
2
3
4

未完待续...

上次更新: 2019-8-27 08:55:07