1. 程式人生 > >前端筆記之微信小程式(四)WebSocket&Socket.io&搖一搖案例&地圖|地理位置

前端筆記之微信小程式(四)WebSocket&Socket.io&搖一搖案例&地圖|地理位置

一、WebSocket概述

http://www.ruanyifeng.com/blog/2017/05/websocket.html

 

Workerman一款開源高效能非同步PHP socket即時通訊框架https://workerman.net

 

HTTP是無連線的:有請求才會有響應,如果沒有請求,伺服器想主動推送資訊給瀏覽器是不可能的。

 

比如圖文直播、聊天室原理:長輪詢。

setInterval(function(){
$.get()
},1000)

間隔一定的時間,主動向伺服器發起請求,詢問是否有新訊息。

 

WebSocket是一種網路通訊協議,是HTML5中的新協議。需要伺服器和瀏覽器共同支援,實現全雙工通訊。

 

伺服器:PHP5.6、Java1.7、Nodejs 6以上。

瀏覽器:Android 6.0及以上版本。

WebSocket 是 HTML5 開始提供的一種在單個 TCP 連線上進行全雙工通訊的協議。

 

WebSocket 使得客戶端和伺服器之間的資料交換變得更加簡單,允許服務端主動向客戶端推送資料。在 WebSocket API 中,瀏覽器和伺服器只需要完成一次握手,兩者之間就直接可以建立永續性的連線,並進行雙向資料傳輸。

在 WebSocket API 中,瀏覽器和伺服器只需要做一個握手的動作,然後,瀏覽器和伺服器之間就形成了一條快速通道。兩者之間就直接可以資料互相傳送。


二、Socket.io

socket.io是一個跨瀏覽器支援WebSocket的實時通訊的JS。Nodejs中實現socket非常好用的包。

 

API:

https://www.npmjs.com/package/socket.io

https://socket.io/

npm install --save socket.io

預設有一個自動路由的js檔案http://127.0.0.1:3000/socket.io/socket.io.js

 

 

前端程式碼(從官網抄的模板):

<!doctype html>
<html>
  <head>
    <title>Socket.IO chat</title>
    <style>
      * { margin: 0; padding: 0; box-sizing: border-box; }
      body { font: 13px Helvetica, Arial; }
      form { background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%; }
      form input { border: 0; padding: 10px; width: 90%; margin-right: .5%; }
      form button { width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; }
      #messages { list-style-type: none; margin: 0; padding: 0; }
      #messages li { padding: 5px 10px; }
      #messages li:nth-child(odd) { background: #eee; }
    </style>
  </head>
  <body>
    <ul id="messages"></ul>
    <form action="">
          <input id="m" autocomplete="off" />
        <button>Send</button>
    </form>
    <script type="text/javascript" src="/socket.io/socket.io.js"></script>
    <script type="text/javascript">
       var socket = io();
    </script>
  </body>
</html>

 

後端:

var express = require('express');
var app = express();
var http = require('http').Server(app);
var io = require('socket.io')(http);

app.get('/', function(req, res){
      res.sendFile(__dirname + '/index.html');
});

//監聽客戶端,有使用者連線的時候觸發(建立前後端連線)
io.on('connection', function(socket){
      console.log('有個使用者連線了');
});

http.listen(3000);
node app.js

 

現在兩個端已經實時通訊連線上了:

 

 

訊息收發的響應:

前端emit發:

<script type="text/javascript">
      var socket = io();
      $("button").click(function(){
             socket.emit("info", "你好");
             return false;
      });
</script>

 

服務端on收:

io.on('connection', function(socket){
      console.log('有個使用者連線了');
      socket.on("info", function(data){
          console.log(data);
      });
});

接下來的事情:

實現聊天室功能:如果有某個客戶端使用者將訊息發給了服務端,服務端要發給所有已經連線的客戶端,這裡就涉及到廣播,廣播就是給所有已經連線服務端的socket物件進行集體的訊息傳送。

 

完整的聊天室前端:

<!doctype html>
<html>
  <head>
    <title>Socket.IO chat</title>
    <style>
     ...
    </style>
  </head>
  <body>
    <ul id="messages"></ul>
    <form action="">
          <input id="m" autocomplete="off" />
        <button>Send</button>
    </form>
    <script type="text/javascript" src="/socket.io/socket.io.js"></script>
    <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
    <script type="text/javascript">
        var socket = io();

        //點選按鈕發出資料
        $("button").click(function(){
            socket.emit("info" , {
                content : $("#m").val()
            });
            $("#m").val("");
            return false;  //阻止瀏覽器預設請求行為,禁止重新整理頁面
        });

        //客戶端收到服務端的msg廣播訊息的時候觸發的函式
        socket.on("msg", function(data){
            $("<li>" + data.content + "</li>").prependTo("ul");
        });
    </script>
  </body>
</html>

 

後端:

io.on('connection', function(socket){
      console.log('有個使用者連線了');
      //服務端收到了info的訊息
      socket.on("info" , function(data){
          console.log(data.content);
          //立即廣播通知所有已連線的客戶端
          io.emit('msg', data);
      });
});

http.listen(3000);

三、微信小程式和WebSocket

小程式的上線版本,必須是https協議會wss協議(即websocket的安全版本)

 

如果結合微信小程式使用,Nodejs不能使用socket.io,因為socket.io在前端需要用script引入一個js檔案,但是小程式不支援這樣引入。但是沒有關係,因為小程式自帶websocket的API。

 

 

前端開啟:

Page({
    onLoad(){
        wx.connectSocket({
            url: 'ws://127.0.0.1:8080',
        })
    }
})
示例程式碼

 

後端需要安裝一個依賴:

https://www.npmjs.com/package/ws

npm install --save ws

後端app.js  

const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', function connection(ws){
    console.log("有人鏈接了");
});
示例程式碼

 

<!--index.wxml-->
<view class="container">
    <view class="t">{{a}}</view>
    <button bindtap="sendmsg">按我</button>
</view>
//index.js
Page({
    data: {
        a: 0
    },
    onLoad(){
        //前端發起websocket連線
        wx.connectSocket({
            // 可以在WiFi環境下的IP地址測試
            // url: 'ws://192.168.0.150:8080', 
            url: 'ws://127.0.0.1:8080'
        })

        //監聽WebSocket接受到伺服器的廣播訊息通知事件
        wx.onSocketMessage((res)=>{
            console.log(res.data)
            this.setData({
                a:res.data
            })
        })
    },
    //點選按鈕傳送訊息給服務端
    send(){
        wx.sendSocketMessage({
            data: "你好!",
        })
    }
})
示例程式碼

 

後端app.js

Nodejs的ws這個庫沒有廣播功能,必須讓開發者將socket物件存為陣列,要廣播的時候,遍歷陣列中每個項,依次給他們傳送資訊即可。

const WebSocket = require('ws');
//建立連線和監聽埠
const wss = new WebSocket.Server({port:8080});

var ws_arr = []; //儲存所有已經連線的人的ws物件
var a = 0;

//響應客戶端的連線
wss.on('connection', function(ws){
    console.log("有人連線了");
    ws_arr.push(ws); //將當前進來的人儲存到陣列

    //監聽客戶端傳送的訊息
    ws.on("message", function(message){
        console.log("服務端收到了訊息:" + message)

        a++;
        //遍歷所有人,廣播通知所有客戶端,把訊息傳送給他們
        ws_arr.forEach(item=>{
            item.send(a);
        })
    })
})
示例程式碼

四、搖一搖大PK

微信沒有提供搖一搖API,必須使用加速計,自己寫程式碼感應x、y、z的變化。

加速計的座標軸如圖,是個三維的座標。我們需要通過x、y、z三個軸的方向的加速度計算出搖動手機時,手機搖動方向的加速度。

 

 

 

index.js

page({
data:{
   x:0,
   y:0,
   z:0
},
onLoad(){
    var lastX = 0;
    var lastY = 0;
    wx.onAccelerometerChange((res)=>{
            //如果當前的x或y減去上一次x或y的差 大於0.5,就設定為搖一搖成功
            if(Math.abs(res.x - lastX) > 0.5 || Math.abs(res.y - lastY) > 0.5){
                wx.showToast({
                   title: "成功"
                });
                lastX = res.x;
                lastY = res.y;

                this.setData({
                   x : res.x,
                   y : res.y,
                   z : res.z 
                })
            }
            
})
}
})
示例程式碼

後端app.js

const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

//儲存所有人的ws物件
var ws_arr = [];
//儲存所有人的分數
// var score_arr = ["nickName":"測試賬戶","avatarUrl":"x.jpg", "n":0];
var score_arr = [];
var a = 0;

wss.on('connection', function(ws){
    console.log("有人鏈接了");
    ws_arr.push(ws); //將每個進來的使用者儲存到陣列


    ws.on('message', function(message){
        console.log("伺服器收到了推送:" + message);
        //變為JSON物件
        var messageObj = JSON.parse(message);
        //當搖一搖時,判斷陣列中有沒有這個人,有就讓這個人的n++
        var isHave = false;
        score_arr.forEach(item=>{
            if(item.nickName == messageObj.nickName){
                item.n ++;
                isHave = true;
            }
        });
        //如果沒有就新增到陣列中
        if(!isHave){
            score_arr.push({
                nickName : messageObj.nickName,
                avatarUrl: messageObj.avatarUrl,
                n : 0
            })
        }

        console.log({"score_arr" : score_arr})
        //廣播發送給客戶端(前端)
        ws_arr.forEach(item=>{
            item.send(JSON.stringify({
                "score_arr" : score_arr
            }));
        });
    });
});

 

使用者搖一搖案例:

<!--index.wxml-->
<view class="container">
    <view class="userinfo">
        <button open-type="getUserInfo" bindgetuserinfo="getUserInfo">獲取頭像暱稱</button>
    </view>
    
    <view wx:for="{{arr}}">
        {{item.nickName}}
        <image style="width:90px;height:90px;" src="{{item.avatarUrl}}"></image>
        {{item.n}}
    </view>
</view>

 

index.js

const app = getApp()

Page({
    data: {
        userInfo: {},
        hasUserInfo: false,
        canIUse: wx.canIUse('button.open-type.getUserInfo'),
        arr : []
    },
    onLoad: function () {
        if (app.globalData.userInfo) {
            this.setData({
                userInfo: app.globalData.userInfo,
                hasUserInfo: true
            })
        } else if (this.data.canIUse) {
            // 由於 getUserInfo 是網路請求,可能會在 Page.onLoad 之後才返回
            // 所以此處加入 callback 以防止這種情況
            app.userInfoReadyCallback = res => {
                this.setData({
                    userInfo: res.userInfo,
                    hasUserInfo: true
                })
            }
        } else {
            // 在沒有 open-type=getUserInfo 版本的相容處理
            wx.getUserInfo({
                success: res => {
                    app.globalData.userInfo = res.userInfo
                    this.setData({
                        userInfo: res.userInfo,
                        hasUserInfo: true
                    })
                }
            })
        }

        //連結socket伺服器(可以填WiFi的IP地址測試)
        wx.connectSocket({
            url: 'ws://127.0.0.1:8080'
        });

        //當socket連線開啟後,監聽搖一搖:
        var self = this;
        var lastX = 0;
        wx.onSocketOpen(function(res){
            wx.onAccelerometerChange(function(res){
                //如果當前的x 減去上一次x的差 大於0.6,就設定為搖一搖成功
                if(Math.abs(res.x - lastX) > 0.6){
                    wx.showToast({
                        title: "搖一搖成功"
                    });
                    //告訴伺服器我是誰
                    wx.sendSocketMessage({
                        data: JSON.stringify({
                            "nickName": self.data.userInfo.nickName,
                            "avatarUrl": self.data.userInfo.avatarUrl
                        })
                    })
                }
                lastX = res.x;
            });
        });

        //接收到伺服器廣播資訊的時候做的事情
        wx.onSocketMessage(function(res){
            var obj = JSON.parse(res.data); //轉物件
            var arr = obj.score_arr;
            //按照n值大小排序
            arr.sort((a,b)=>{
                return b.n - a.n
            })
            self.setData({arr}); //儲存到本地data中的arr陣列
        });
    },
    getUserInfo: function (e) {
        console.log(e)
        app.globalData.userInfo = e.detail.userInfo
        this.setData({
            userInfo: e.detail.userInfo,
            hasUserInfo: true
        })
    } 
});

五、地圖和地理位置

騰訊地理位置服務https://lbs.qq.com/ 

地圖自己是不能定位的,需要獲取地理位置定位,而且地圖API和地理位置API是分開。

<!--index.wxml-->
<view class="container">
    <view class="userinfo">
        <button open-type="getUserInfo" bindgetuserinfo="getUserInfo">獲取頭像暱稱</button>
    </view>
    
   <map markers="{{markers}}" id="map" longitude="{{longitude}}" latitude="{{latitude}}" 
  scale="14" style="width:100%;height:300px;"></map> </view>
Page({
    data: {
        markers : []
    },
    onLoad(){
        //頁面載入進來要先定位
        var self = this;
        wx.getLocation({
            type: 'gcj02',
            success: function(res){
                self.setData({
                    latitude: res.latitude, //緯度
                    longitude: res.longitude //經度
                });
            }
        });

        //連線socket伺服器
        wx.connectSocket({
            url: 'ws://192.168.1.175:8080'
        });

//接收到伺服器廣播資訊的時候做的事情
        wx.onSocketMessage(function (res) {
            var obj = JSON.parse(res.data);
        console.log(obj)
}
//微信沒有提供當某人地理位置改變時候的on事件,所以setInterval()。
//每3秒更新一次定位,然後傳送給服務端,服務端再通知客戶端
clearInterval(timer);
var timer = setInterval(function(){
    wx.getLocation({
        type: 'gcj02',
        success: function(res){
            wx.sendSocketMessage({
                data: JSON.stringify({
                    "nickName": self.data.userInfo.nickName,
                    "avatarUrl": self.data.userInfo.avatarUrl,
                    "latitude": res.latitude,
                    "longitude": res.longitude
                })
            })
        }
    });
},3000);
    }
});

 

後端app.js

const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

var ws_arr = []; //儲存所有人的ws物件
var location_arr = []; //儲存所有人的地理位置

wss.on('connection', function (ws) {
    console.log("有人鏈接了");
    //放入陣列
    ws_arr.push(ws);
       
    ws.on('message', function (message) {
        console.log("伺服器收到了推送" + message);
        //變為JSON物件
        var messageObj = JSON.parse(message);
        //判斷陣列中有沒有我
        var isHave = false;
        location_arr.forEach(item=>{
            if(item.nickName == messageObj.nickName){
                item.latitude = messageObj.latitude;
                item.longitude = messageObj.longitude;
                isHave = true;
            }
        });
        //如果沒有
        if(!isHave){
            location_arr.push({
                nickName : messageObj.nickName,
                avatarUrl: messageObj.avatarUrl,
                latitude : messageObj.latitude,
                longitude: messageObj.longitude
            })
        }
        console.log({"location_arr" : location_arr})
    
        //廣播通知客戶端
        ws_arr.forEach(item=>{
            item.send(JSON.stringify({
                "location_arr" : location_arr
            }));
        });
    });
});

臨時設定大頭針markers

 

var tempPathObj = {}; //儲存這個人的暱稱,根據暱稱獲取頭像,如這個物件沒有這個人就要下載頭像
//接收到伺服器廣播資訊的時候做的事情
wx.onSocketMessage(function(res){
    var obj = JSON.parse(res.data);
    self.setData({
        markers : [] //清空
});
//iconPath不支援網路地址,要通過wx.download()介面下載得到臨時地址
obj.location_arr.forEach(item=>{
    //根據暱稱,判斷這個物件中有沒有這個人,如果有直接用
        if(tempPathObj.hasOwnProperty(self.data.userInfo.nickName)){
    self.setData({
                markers: [
                    ...self.data.markers,
                    {   //如果物件中有這個人,就直接用這個人的頭像
                        iconPath: tempPathObj[self.data.userInfo.nickName], 
                        id: 0,
                        latitude: item.latitude,
                        longitude: item.longitude,
                        width: 50,
                        height: 50
                    }
                ]
            });
        } else {
        //如果沒有就下載頭像,並且將這個人存起來,以後可以直接用
            wx.downloadFile({
                url: item.avatarUrl,
                success(data){
                    console.log(data.tempFilePath);
                    self.setData({
                        markers : [
                            ...self.data.markers,
                            {
                                iconPath: data.tempFilePath,
                                id: 0,
                                latitude: item.latitude,
                                longitude: item.longitude,
                                width: 50,
                                height: 50
                            }
                        ]
                    });
                    // console.log(self.data.markers);
                    //將頭像的臨時地址儲存給這個人
                    tempPathObj[self.data.userInfo.nickName] = data.tempFilePath;
                }
            })
        }
    });
});

&n