1. 程式人生 > 其它 >乾貨 | 前端常用的通訊技術

乾貨 | 前端常用的通訊技術

作者簡介

陳為平,攜程市場部前端工程師,目前主要負責“攜程運動”專案的大前端相關工作。

前段時間在忙開發攜程運動專案和相應的微信小程式,其中和後端通訊猶為頻繁。get、post請求方法是很多前端童鞋使用最頻繁的;websocket在11年盛行後方便了客戶端和伺服器之間傳輸,……and so on ,除了這些,還有很多我們不常使用的其他方式,但是在實際的業務場景中卻真實需要。

本文總結了目前前端使用到的資料交換方式,闡述了業務場景中如何選擇適合的方式進行資料交換( form ,xhr, fetch, SSE, webstock, postmessage, web workers等),並列舉了一些示例程式碼, 可能存在不足的地方,歡迎大家指正。

本文用到的原始碼都放在Github上,點選下方閱讀原文可直達。

關於HTTP協義基礎可以參考阮一峰老師的《HTTP協議入門》一文。

前端經常使用的HTTP協議相關(1.0 / 1.1)

method

· GET ( 對應 restful api 查詢資源, 用於客戶端從服務端取資料 )

· POST(對應 restful api中的增加資源, 用於客戶端傳資料到服務端)

· PUT (對應 restful api中的更新資源)

· DELETE ( 對應 restful api中的刪除資源 )

· HEAD ( 可以用於http請求的時間什麼,或者判斷是否存在判斷檔案大小等)

· OPTIONS (在前端中常用於 cors跨域驗證)

· TRACE * (我這邊沒有用到過,歡迎補充)

· CONNECT * (我這邊沒有用到過,歡迎補充)

enctype

· application/x-www-form-urlencoded (預設,正常的提交方式)

· multipart/form-data(有上傳檔案時常用這種)

· application/json (ajax常用這種格式)

· text/xml

· text/plain

enctype示例說明( form , ajax, fetch 三種示例 )

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>enctype</title>
    <style>
        .box{border: 1px solid #ccc;padding:20px;}
        .out{background: #efefef; padding:10px 20px; margin-top: 20px;}
    </style>
    <script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.2.3.min.js"></script>
    
    <script>

    $(function(){

        $('#b1').on('click', function(){
            $.ajax({
                method: "POST",
                contentType:'application/x-www-form-urlencoded;charset=UTF-8',
                url: "form_action.php",
                data: {username: "John", password: "Boston" }
            }).done(function( msg ) {
                $('#msg1').html(msg);
            });
        });

        $('#f1').on('click', function(){
            fetch("form_action.php", {
                method: "POST",
                credentials: 'include', //帶上cookie
                headers: {
                    "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
                },
                body: "username=John&password=Boston"
            })
            .then(function(response){
                return response.text();
            })
            .then(function(msg) {
                $('#msg1').html(msg);

            }, function(e) {
                alert("Error submitting form!");
            });
        });


        $('#b2').on('click', function(){
            var formData = new FormData(document.querySelector("#data2"));
            $.ajax({
                method: "POST",
                processData:false, //無需讓jquery正處理一下資料
                contentType:false, //已經是formData就預設為 multipart/form-data
                cache: false,
                url: "form_action.php",
                data: formData
            }).done(function( msg ) {
                $('#msg2').html(msg);
            });
        });

        $('#f2').on('click', function(){
            var formData = new FormData(document.querySelector("#data2"));
            fetch("form_action.php", {
                method: "POST",
                headers: {
                    "Content-Type": "multipart/form-data;charset=UTF-8"
                },
                body: formData
            })
            .then(function(response){
                return response.text();
            })
            .then(function(msg) {
                $('#msg2').html(msg);
            }, function(e) {
                alert("Error submitting form!");
            });
        });


        $('#b3').on('click', function(){
            $.ajax({
                method: "POST",
                contentType:'application/json;charset=UTF-8',
                url: "form_action.php",
                data: JSON.stringify({username: "John", password: "Boston" })
            }).done(function( msg ) {
                $('#msg3').html(msg);
            });
        });

        $('#f3').on('click', function(){
            var formData = new FormData(document.querySelector("#data2"));
            fetch("form_action.php", {
                method: "POST",
                headers: {
                    "Content-Type": "application/json;charset=UTF-8"
                },
                body: JSON.stringify({username: "John", password: "Boston" })
            })
            .then(function(response){
                return response.text();
            })
            .then(function(msg) {
                $('#msg3').html(msg);
            }, function(e) {
                alert("Error submitting form!");
            });
        });

        $('#b4').on('click', function(){
            $.ajax({
                method: "POST",
                contentType:'text/plain;charset=UTF-8',
                processData:false, //無需讓jquery正處理一下資料
                url: "form_action.php",
                data: "我是一個純正的文字功能!rn我第二行"
            }).done(function( msg ) {
                $('#msg4').html(msg);
            });
        });

        $('#f4').on('click', function(){
            var formData = new FormData(document.querySelector("#data2"));
            fetch("form_action.php", {
                method: "POST",
                headers: {
                    "Content-Type": "text/plain;charset=UTF-8"
                },
                body: "我是一個純正的文字功能!rn我第二行"
            })
            .then(function(response){
                return response.text();
            })
            .then(function(msg) {
                $('#msg4').html(msg);
            }, function(e) {
                alert("Error submitting form!");
            });
        });

        $('#b5').on('click', function(){
            $.ajax({
                method: "POST",
                contentType:'text/xml;charset=UTF-8',
                // processData:false, //無需讓jquery正處理一下資料
                url: "form_action.php",
                data: "<doc><h1>我是標籤</h1><p>我是內容</p></doc>"
            }).done(function( msg ) {
                $('#msg5').html(msg);
            });
        });

        $('#f5').on('click', function(){
            var formData = new FormData(document.querySelector("#data2"));
            fetch("form_action.php", {
                method: "POST",
                headers: {
                    "Content-Type": "text/xml;charset=UTF-8"
                },
                body: "<doc><max>我是XML標籤</max><min>我是XML內容</min></doc>"
            })
            .then(function(response){
                return response.text();
            })
            .then(function(msg) {
                $('#msg5').html(msg);
            }, function(e) {
                alert("Error submitting form!");
            });
        });


    });
    </script>
</head>
<body>

<h1>enctype測試</h1>

<h2>表單提交: application/x-www-form-urlencoded</h2>
<div class="box">
    <form action="form_action.php" enctype="application/x-www-form-urlencoded" method="post">
        <p>使用者: <input type="text" name="username" /></p>
        <p>密碼: <input type="text" name="password" /></p>
        <input type="submit" value="提交" />
        <button type="button" id="b1">AJAX提交</button>
        <button type="button" id="f1">fetch提交</button>
    </form>
    <div id="msg1" class="out"></div>
</div>

<h2>multipart/form-data</h2>
<div class="box">
    <form id="data2" action="form_action.php" enctype="multipart/form-data" method="post">
        <p>使用者: <input type="text" name="username" /></p>
        <p>密碼: <input type="text" name="password" /></p>
        <p>檔案: <input type="file" name="file" id="file1" /></p>
        <input type="submit" value="提交" />
        <button type="button" id="b2">AJAX提交</button>
        <button type="button" id="f2">fetch提交</button>
    </form>
    <div id="msg2" class="out"></div>
</div>

<h2>application/json</h2>
<div class="box">
    <form action="form_action.php" enctype="application/json" method="post">
        <p>使用者: <input type="text" name="username" /></p>
        <p>密碼: <input type="text" name="password" /></p>
        <input type="submit" value="提交" />
        <button type="button" id="b3">AJAX提交</button>
        <button type="button" id="f3">fetch提交</button>
    </form>
    <div id="msg3" class="out"></div>
</div>

<h2>text/plain</h2>
<div class="box">
    <form action="form_action.php" enctype="text/plain" method="post">
        <p>使用者: <input type="text" name="username" /></p>
        <p>密碼: <input type="text" name="password" /></p>
        <input type="submit" value="提交" />
        <button type="button" id="b4">AJAX提交</button>
        <button type="button" id="f4">fetch提交</button>
    </form>
    <div id="msg4" class="out"></div>
</div>

<h2>text/xml</h2>
<div class="box">
    <form action="form_action.php" enctype="text/xml" method="post">
        <p>使用者: <input type="text" name="username" /></p>
        <p>密碼: <input type="text" name="password" /></p>
        <input type="submit" value="提交" />
        <button type="button" id="b5">AJAX提交</button>
        <button type="button" id="f5">fetch提交</button>
    </form>
    <div id="msg5" class="out"></div>
</div>

</body>
</html>

服務端 form_action.php

<?php
echo '<pre>';

if($_POST){
    echo "<h1>POST</h1>";
    print_r($_POST);
    echo "<hr>";
}

if(file_get_contents("php://input")){
    echo "<h1>php://input</h1>";
    print_r(file_get_contents("php://input"));
    echo "<hr>";
}

if($_FILES){
    echo "<h1>file</h1>";
    print_r($_FILES);
    echo "<hr>";
}

* fetch api是基於Promise設計 * fetch 的一些例子 mdn/fetch-examples

伺服器到客戶端的推送 - Server-sent Events

這個是html5的一個新特性,主要用於伺服器推送訊息到客戶端, 可以用於監控,通知,更新庫存之類的應用場景, 在攜程運動專案中我們主要應用於線上被預訂後通知下發通知到場館的操作介面上的即時改變狀態。

圖片來源於網路,侵刪

優點: 基於http協義無需特別的改造,除錯方便, 可以CORS跨域 server-send events 是服務端往客戶端單向推送的,如果客戶端需要上傳訊息可以使用 WebSocket

客戶端程式碼

var source = new EventSource('http://localhost:7000/server');source.onmessage = function(e) {
    console.log('e', JSON.parse( e.data));
    document.getElementById('box').innerHTML += "SSE notification: " + e.data + '<br />';
};

服務端程式碼

<?php 
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
//資料
$time = date('Y-m-d H:i:s');
$data = array(
    'id'=>1,
    'name'=>'中文',
    'time'=>$time
);
echo "data: ".json_encode($data)."nn";
flush();
?>
echo "event: pingn"; // 增加 event可以多送多個事件
js使用 source.addEventListener('ping', function(){}, false); 來處理對應的事件

對於低版本的瀏覽器可以使用 eventsource polyfill

  • Yaffle/EventSource by yaffle
  • https://github.com/remy/polyfills/blob/master/EventSource.js by Remy Sharp
  • rwaldron/jquery.eventsource by Rick Waldron
  • amvtek/EventSource by AmvTek

客戶端與伺服器雙向通訊 WebSocket

特點

1. websocket 是個雙向的通訊。 2. 常用於應用於一些都需要雙方互動的,實時性比較強的地方(如聊天,線上客服) 3. 資料傳輸量小 4. websocket 是個 持久化的連線

原理圖

圖片來源於網路. 侵刪

這個的服務端是基於 nodejs實現的(不要問為什麼不是php,因為 nodejs 簡單些!)

server.js

var WebSocketServer = require('ws').Server;
var wss = new WebSocketServer({port: 2000});

wss.on('connection', function(ws) {
    ws.send('服務端發來一條訊息');
    ws.on('message', function(message) {
        //轉發一下客戶端發過來的訊息
        console.log('收到客戶端來的訊息: %s', message);
        ws.send('服務端收到來自客戶端的訊息:' + message);
    });
    ws.on('close', function(event) {
        console.log('客戶端請求關閉',event);
    });
});

client.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>WebSocket 雙向通訊</title>
    <style>
      #boxwarp > div{
        border: 1px solid #ccc;
        padding:10px;
        margin:10px;
      }
    </style>

</head>
<body>
<button id="btn">發點什麼</button>
<div id="boxwarp"></div>
<script>
var ws = new WebSocket("ws://127.0.0.1:2000/");
document.getElementById('btn').addEventListener('click', function() {
  ws.send('cancel_order');
});
function addbox(msg){
  var box = document.createElement('div');
      box.innerHTML = msg;
  document.getElementById('boxwarp').append(box);
}
ws.onopen = function() {
    var msg = 'ws已經聯接';
    addbox(msg);
    ws.send(msg);
};
ws.onmessage = function (evt) {
    console.log('evt');
    addbox(evt.data);
};

ws.onclose = function() {
   console.log('close');
   addbox('服務端關閉了ws');
};
ws.onerror = function(err) {
   addbox(err);
};
</script>
</body>
</html>

說完了客戶端與服客端之間的通訊,現在我們來聊聊客戶端之間的通訊。

客戶端與客戶端頁面之間的通訊 postMessage

主要特點

1. window.postMessage() 方法可以安全地實現跨域通訊 2.主要用於兩個頁面之間的訊息傳送 3. 可以使用iframe與window.open開啟的頁面進行通訊.

特別的應用場景 我們的頁面引用了其他的人頁面,但我們不知道他們的頁面高度,這時可以通過window.postMessages 從iframe 裡面的頁面來傳到 當前頁面. 語法

otherWindow.postMessage(message, targetOrigin, [transfer]);

示例程式碼 postmessage.html (入口)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>postmessage示例</title>
    <style>
        html,body{height: 100%;}
        *{padding: 0; margin:0;}
        .warp{ display: flex; }
        .warp > div,
        .warp > iframe{
            flex: 1;
            margin:10px;
        }
        iframe{
            height: 600px;
            border: 1px solid #ccc;
        }
    </style>
</head>
<body>
<div class="warp">
    <div class="left">
        左邊頁面    </div>
    <div class="right">
        右邊頁面    </div></div><div class="warp">
    <div class="left warp">
        <iframe src="./post1.html" frameborder="0" id="post1" name="post1"></iframe>
    </div>
    <div class="right warp">
        <iframe src="./post2.html" frameborder="0" id="post2" name="post2"></iframe>
    </div>

<!-- window.frames[0].postMessage('getcolor','http://lslib.com'); -->
</div>

<div class="warp">
    <div class="left"><button id="postBtn1">向左邊的(iframe)推送資訊程式碼</button></div>
    <div class="right"><button id="postBtn2">向右邊的(iframe)推送資訊程式碼</button></div>
</div>

<script>
document.getElementById('postBtn1').addEventListener('click', function(){
    console.log('postBtn1');
    var date = new Date().toString();
    window.post1.postMessage(date,'*');});document.getElementById('postBtn2').addEventListener('click', function(){
    console.log('postBtn2');
    var date = new Date().toString();
    window.post2.postMessage(date,'*');});window.addEventListener('message',function(e){
    if(e.data){
        console.log(e.data);
        console.log(e);
        window.post1.postMessage(e.data,'*');
    }},false);
</script>
</body>
</html>

post1.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <style>
        .sendbox{
            background: #efefef;
            margin-bottom: 10px;
        }
    </style>
</head>
<body>
<div class="sendbox">
    <button id="sendbox2">直接傳送到右邊iframe</button>
    左邊的iframe
</div>
<div id="box2">
</div>
<script>
    document.getElementById('sendbox2').addEventListener('click', function(){
        window.parent.post2.postMessage('收到來自左邊ifarme的訊息' + +new Date(),'*');
    });
    function addbox(html){
        var item = document.createElement('div');
        item.innerHTML = html;
        document.getElementById('box2').append(item);
    }
    window.addEventListener('message',function(e){
        if(e.data){
            addbox(e.data);
        }
    },false);
</script>

</body>
</html>

post2.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <style>
        .sendbox{
            background: #ccc;
            margin-bottom: 10px;
        }
    </style>
</head>
<body>
<div class="sendbox" style="text-align: right;">
    <button id="sendbox">中轉到左邊</button>
    <button id="sendbox2">直接到左邊</button>
    右邊的iframe
</div>
<div id="box"></div>

<script>
    document.getElementById('sendbox').addEventListener('click', function(){
        /*- 向父級頁面傳 -*/
        window.parent.postMessage('來自post2的訊息' + +new Date(),'*');
    });
    document.getElementById('sendbox2').addEventListener('click', function(){
        window.parent.post1.postMessage('直接來自右邊' + +new Date(),'*');
    });
    function addbox(html){
        var item = document.createElement('div');
        item.innerHTML = html;
        document.getElementById('box').append(item);
    }
    window.addEventListener('message',function(e){
        if(e.data){
            addbox(e.data);
        }
    },false);
</script>

</body>
</html>

Web Workers 程序通訊(html5中的js的後臺程序)

javascript設計上是一個單線,也就是說在執行js過程中只能執行一個任務, 其他的任務都在佇列中等待執行。

如果我們執行大量計算的任務時,就會阻止瀏覽器執行js,導致瀏覽器假死。 html5的 web Workers 子程序 就是為了解決這種問題而設計的。把大量計算的任務當作類似ajax非同步方式進入子程序計算,計算完了再通過 postmessage通知主程序計算結果。

圖片來源於網路. 侵刪

主執行緒程式碼(index.html)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <style>
        .box-warp > div{
            border: 1px solid #ccc;
            margin:10px;
            padding:10px;
        }
    </style>
</head>
<body>
    <button id="btn">開啟一個後臺執行緒(點選外框中止執行緒)</button>
    <div class="box-warp" id="boxwarp"></div>

    <script>
    var id = 1;
    function init_works(){
        var warpid = 'box'+id;
        var box = document.createElement('div');
            box.id = warpid;
        document.getElementById('boxwarp').append(box);
        var worker = new Worker('./compute.js');
        //監聽後臺程序發過來的訊息
        worker.onmessage= function (event) {
            // 把子執行緒返回的結果新增到 div 上
            document.getElementById(warpid).innerHTML += event.data+"<br/>";
        };
        //點選中止後端程序
        box.addEventListener('click', function(){
            worker.postMessage("oh, 我被幹掉了" + warpid);
            var time = setTimeout(function(){
                worker.terminate();
                clearTimeout(time);
            },0);
        });
        //往後臺執行緒傳送訊息
        worker.postMessage("hi, 我是" + warpid);
        id++;
    }

    document.getElementById('btn').addEventListener('click', function(){
        init_works();
    });
    </script>
</body>
</html>

後臺程序程式碼( compute.js )

var i=0;
function timeX(){
    i++;
    postMessage(i);
    if(i>9){
        postMessage('no 我不想動了');
        close(); //中止執行緒
    }
    setTimeout(function(){
        timeX();
    },1000);
}

timeX();

//收到主執行緒的訊息

onmessage = function (oEvent) {
  postMessage(oEvent.data);
};

上述程式碼簡單的說明一下, 主程序與後臺程序之間的互相通訊。