【譯】經過JavaScript發送表單

在[發送表單數據]()一文中,HTML表單能夠聲明式地發送一個HTTP請求。但表單也能夠用JavaScript來準備一個HTTP請求。本文將探索如何作到這點。前端

表單,不僅是表單

隨着開放式Web應用的出現,如今提供HTML表單、而不是文字表單供用戶填寫的作法,已經愈加廣泛了。而愈來愈多的開發者也得到了數據傳輸的控制權。html5

得到對數據傳輸的控制權

標準的HTML表單提交操做會加載數據發送到的URL,這就意味着瀏覽器的窗口中會進行整個頁面的從新加載。而若是避免了頁面的從新加載,就會避免頁面的閃爍和網絡延遲,進而提供更順暢的用戶體驗。node

在許多現代的UI設計中,HTML表單只是用來收集用戶的輸入。當用戶要發送數據時,Web應用會進行控制,並在後臺異步地發送數據,只更新UI中須要更改的部分。git

能異步地發送任意數據的技術稱爲AJAX,表示「異步的JavaScript和XML」。github

和傳統表單處理的不一樣

AJAX使用了XMLHttpRequest(XHR)DOM對象,它能夠創建HTTP請求、發送請求並處理結果。web

注意: 老的AJAX技術可能不是用XMLHttpRequest。好比JSONPeval()函數結合起來使用。雖然該方法可行,但不推薦使用它,由於其存在嚴重的安全問題。因此除非爲了兼容那些特別老舊、不支持XMLHttpRequest或JSON的瀏覽器,仍是避免使用該技術segmentfault

因爲歷史緣由,XMLHttpRequest本是設計用來獲取和發送交換格式爲XML的數據的。但如今JSON取代了XML,有着更廣泛的使用。後端

不過XML和JSON都不符合做爲表單數據請求的編碼。表單數據(application/x-www-form-urlencoded)是用於構造鍵值對的URL編碼列表的,如果要傳輸二進制數據,HTTP請求會被重塑爲 multipart/form-data

若你能掌控前端(運行在瀏覽器上的代碼)和後端(運行在服務器上的代碼),你就能發送JSON或XML、併爲所欲爲地處理它們。

但若是你使用的是第三方服務,這就沒那麼容易了,由於某些服務只接受表單數據。固然也有使用表單數據處理起來更方便的狀況,好比數據是鍵值對或二進制數據時,用現成的後端工具就能處理它們、不須要額外的代碼。

那麼,具體該如何發送數據呢?

發送表單數據

目前有三種方式來發送表單數據,既有老舊的技術、也有新特性FormData對象,接下來就來深刻了解下它們。

在隱藏的iframe中構建DOM

發送表單數據最古老的方法,是用DOM API創建一個表單,而後發送數據到一個隱藏的<iframe>。爲了訪問你提交內容的處理結果,應檢索下<iframe>的內容。

警告:避免使用該技術。在使用第三方服務時,該技術有安全風險,由於它會致使你面臨腳本注入攻擊。若你使用HTTPS,使用<ifrme>發送表單還會影響同源策略,並致使內容被髮送到一個沒法訪問的<ifrme>中。固然若是你要兼容很老舊的瀏覽器,這項技術可能就是你惟一的選擇了。

下面是一個例子:

<button onclick="sendData({test:'ok'})">Click Me!</button>
// 建立一個iFrame來發送咱們的數據
var iframe = document.createElement("iframe");
iframe.name = "myTarget";

// 將iFrame添加到文檔流中
window.addEventListener("load", function () {
  iframe.style.display = "none";
  document.body.appendChild(iframe);
});

// 用來發送數據的函數
// 須要一個參數,是一個由鍵值對組成的對象
function sendData(data) {
  var name,
      form = document.createElement("form"),
      node = document.createElement("input");

  // 定義響應加載時的行爲
  iframe.addEventListener("load", function () {
    alert("Yeah! Data sent.");
  });
    
  form.action = "http://www.cs.tut.fi/cgi-bin/run/~jkorpela/echo.cgi";
  form.target = iframe.name;

  for(name in data) {
    node.name  = name;
    node.value = data[name].toString();
    form.appendChild(node.cloneNode());
  }

  // 要發送數據,表單得添加到文檔流中
  form.style.display = "none";
  document.body.appendChild(form);

  form.submit();

  // 表單一發送就移除它
  document.body.removeChild(form);
}

結果以下:
效果

手動建立一個XMLHttpRequest

XMLHttpRequest是發送HTTP請求最安全和可靠的方式。要想用XMLHttpRequest發送表單數據,得先用URL編碼要發送的數據,並遵循表單數據請求的規範。

注意: 若想了解更多關於XMLHttpRequest,這幾篇文章可能對你有用:An introductory article to AJAX,以及一個關於使用XMLHttpRequest的高級教程。

來重構下咱們先前的例子:

<button type="button" onclick="sendData({test:'ok'})">Click Me!</button>

如你所見,HTML部分並未真的有所改變,但JavaScript部分就徹底不一樣了:

function sendData(data) {
  var XHR = new XMLHttpRequest();
  var urlEncodedData = "";
  var urlEncodedDataPairs = [];
  var name;

  // 將data對象轉爲一個URL編碼的鍵值對數組
  for(name in data) {
    urlEncodedDataPairs.push(encodeURIComponent(name) + '=' + encodeURIComponent(data[name]));
  }

  // 將鍵值對組合成一個字符串,並把全部通過URL編碼的空格替換爲'+'號;以符合瀏覽器表單提交的行爲
  // the '+' character; matches the behaviour of browser form submissions.
  urlEncodedData = urlEncodedDataPairs.join('&').replace(/%20/g, '+');

  // 定義成功的數據提交後會發生什麼
  XHR.addEventListener('load', function(event) {
    alert('Yeah! Data sent and response loaded.');
  });

  // 定義失敗的狀況會發生什麼
  XHR.addEventListener('error', function(event) {
    alert('Oups! Something goes wrong.');
  });

  // 配置請求
  XHR.open('POST', 'https://example.com/cors.php');

  // 添加表單數據POST請求所需的HTTP請求頭
  XHR.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');

  // 最後,發送數據
  XHR.send(urlEncodedData);
}

結果以下:
效果

注意: 若你想使用此處的XMLHttpRequest方法發送數據到第三方網站,也會受到同源策略的限制。要實現跨域請求,還須要CORS和HTTP訪問控制

使用XMLHttpRequest和FormData對象

手動構建HTTP請求是挺麻煩的,幸運的是,最近的一項XMLHttpRequest標準提供了一個處理表單請求的簡便方法--FormData對象。

FormData對象可被用來創建要傳輸的表單數據,或者從表單元素中獲取數據來管理如何發送。要注意FormData對象是「只寫」的,這意味着你能夠改變它們、但不能訪問它們的內容。

使用FormData對象的方法會在[使用FormData對象]()一文中詳細介紹,下面有兩個例子:

使用獨立的FormData對象

<button type="button" onclick="sendData({test:'ok'})">Click Me!</button>

如今你該很熟悉這個HTML的樣子了。

function sendData(data) {
  var XHR = new XMLHttpRequest();
  var FD  = new FormData();

  // 將數據添加到FormData對象中
  for(name in data) {
    FD.append(name, data[name]);
  }

  // 定義成功的數據提交後會發生什麼
  XHR.addEventListener('load', function(event) {
    alert('Yeah! Data sent and response loaded.');
  });

  // 定義失敗的狀況會發生什麼
  XHR.addEventListener('error', function(event) {
    alert('Oups! Something went wrong.');
  });

  // 配置請求
  XHR.open('POST', 'https://example.com/cors.php');

  // 發送FormData對象,HTTP頭會自動設置
  XHR.send(FD);
}

結果以下:
效果

將FormData綁到表單元素上使用

你也能夠將FormData對象綁定到一個表單元素上,這樣作會建立一個FormData對象來表示表單中的數據。

HTML是典型的表單:

<form id="myForm">
  <label for="myName">Send me your name:</label>
  <input id="myName" name="name" value="John">
  <input type="submit" value="Send Me!">
</form>

但JavaScript會接管表單的提交操做:

window.addEventListener("load", function () {
  function sendData() {
    var XHR = new XMLHttpRequest();

    // 綁定FormData對象和表單元素
    var FD = new FormData(form);

    // 定義成功的數據提交後會發生什麼
    XHR.addEventListener("load", function(event) {
      alert(event.target.responseText);
    });

    // 定義失敗的狀況會發生什麼
    XHR.addEventListener("error", function(event) {
      alert('Oups! Something goes wrong.');
    });

    // 配置請求
    XHR.open("POST", "https://example.com/cors.php");

    // 發送的數據是用戶在表單中提供的
    XHR.send(FD);
  }
 
  // 訪問表單元素
  var form = document.getElementById("myForm");

  // 並接管其submit事件
  form.addEventListener("submit", function (event) {
    event.preventDefault();

    sendData();
  });
});

結果以下:
效果

處理二進制數據

若你在一個含有<input type="file">組件的表單中使用FormData對象,那麼數據會被自動處理。但要手動發送二進制數據的話,還有不少額外工做要作。

現代Web有不少二進制數據源:好比FileReaderCanvasWebRTC。但不幸的是,某些老舊瀏覽器不能訪問二進制數據或者須要複雜的方法才能實現。這些遺留的問題已不在本文討論範圍以內。若你想了解更多關於FileReader API,請閱讀Using files from web applications

用FormData來發送二進制是很直接的,使用append()方法就行了。但要手動作到這點,就須要一些技巧了。

在下面的例子中,咱們會用來FileReader API來訪問二進制數據,而後手動創建多部分的表單數據請求。

<form id="myForm">
  <p>
    <label for="i1">text data:</label>
    <input id="i1" name="myText" value="Some text data">
  </p>
  <p>
    <label for="i2">file data:</label>
    <input id="i2" name="myFile" type="file">
  </p>
  <button>Send Me!</button>
</form>

如你所見,HTML用了標準的<form>,這沒什麼神奇的。「神奇」的部分在JavaScript裏:

// 因爲咱們要訪問DOM結點,因此得在頁面加載完後才初始化腳本
window.addEventListener('load', function () {

  // 這些變量用來存儲表單數據
  var text = document.getElementById("i1");
  var file = {
        dom    : document.getElementById("i2"),
        binary : null
      };
 
  // 使用FileReader API來訪問文件內容
  var reader = new FileReader();

  // 因爲FileReader是異步的,因此得在其完成文件讀取後才存儲結果
  reader.addEventListener("load", function () {
    file.binary = reader.result;
  });

  // 在頁面加載時,若已經選擇了文件就直接讀取它
  if(file.dom.files[0]) {
    reader.readAsBinaryString(file.dom.files[0]);
  }

  // 不然在用戶選擇文件時再讀取它
  file.dom.addEventListener("change", function () {
    if(reader.readyState === FileReader.LOADING) {
      reader.abort();
    }
    
    reader.readAsBinaryString(file.dom.files[0]);
  });

  // sendData是本例的主要函數
  function sendData() {
    // 若已經選擇了文件,就等瀏覽器讀取完
    // 不然就延遲本函數的執行
    if(!file.binary && file.dom.files.length > 0) {
      setTimeout(sendData, 10);
      return;
    }

    // 要構建多部分的表單數據請求,須要一個XMLHttpRequest實例
    var XHR = new XMLHttpRequest();

    // 須要一個分隔符來定義請求體的每部分
    var boundary = "blob";

    // 將請求體存爲一個字符串
    var data = "";

    // 若用戶選擇了文件
    if (file.dom.files[0]) {
      // 開啓請求體的新部分
      data += "--" + boundary + "\r\n";

      // 該部分描述爲表單數據
      data += 'content-disposition: form-data; '
      // 定義表單數據的名字
            + 'name="'         + file.dom.name          + '"; '
      // 提供真實的文件名
            + 'filename="'     + file.dom.files[0].name + '"\r\n';
      // 提供文件的MIME類型
      data += 'Content-Type: ' + file.dom.files[0].type + '\r\n';

      // 元數據和真實數據部分間有一個空行
      data += '\r\n';
      
      // 往請求體裏添加二進制數據
      data += file.binary + '\r\n';
    }

    // 文本數據的組織更加簡單
    // 開啓請求體的新部分
    data += "--" + boundary + "\r\n";

    // 描述爲表單數據並命名
    data += 'content-disposition: form-data; name="' + text.name + '"\r\n';
    // 元數據和真實數據部分間有一個空行
    data += '\r\n';

    // 往請求體裏添加文本數據
    data += text.value + "\r\n";

    // 完成了全部部分,就「閉合」請求體
    data += "--" + boundary + "--";

    // 定義成功的數據提交後會發生什麼
    XHR.addEventListener('load', function(event) {
      alert('Yeah! Data sent and response loaded.');
    });

    // 定義失敗的狀況會發生什麼
    XHR.addEventListener('error', function(event) {
      alert('Oups! Something went wrong.');
    });

    // 配置請求
    XHR.open('POST', 'https://example.com/cors.php');

    // 添加必要的HTTP請求頭來處理多部分表單數據的POST請求
    XHR.setRequestHeader('Content-Type','multipart/form-data; boundary=' + boundary);

    // 最後,發送數據
    XHR.send(data);
  }

  // 訪問表單
  var form = document.getElementById("myForm");

  // 接管submit事件
  form.addEventListener('submit', function (event) {
    event.preventDefault();
    sendData();
  });
});

結果以下:
效果

結論

瀏覽器的不一樣,致使經過JavaScript發送表單數據能夠簡單或很困難。FromData對象是一般的解決方案,並且咱們應該堅決果斷地在老舊瀏覽器上使用其polyfill:

相關文章
相關標籤/搜索