使用 GET 請求


HTTP 定義 GET 應用於安全(Safe)操作,使用者採取的動作必須避免有他們非預期的結果。慣例上,GET 與 HEAD(與 GET 同為取得資訊,不過僅取得回應標頭)對使用者來說就是「取得」資訊,不應該被用來「修改」與使用者相關的資訊,像是進行轉帳之類的動作。

HTTP 的定義中,GET 也應當用於等冪(idempotent)操作,也就是單一請求產生的副作用,與同樣請求進行多次的副作用必須是相同的。

如果使用傳統表單發送 GET 請求,GET 的請求參數會出現在網址列並更新頁面,但使用 XMLHttpRequest 時,GET 的請求參數並不會影響網址列,所以使用者無法直接將請求參數當作書籤網址列的一部份。

要使用非同步物件透過 GET 發送請求參數,只要在第二個 url 參數中以請求參數格式附加,而 send 時不傳入引數設為 null 即可。一個例子如下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
</head>
<body>
    圖書:<br>
    <select id="category">
        <option>-- 選擇分類 --</option>
        <option value="theory">理論基礎</option>
        <option value="language">程式語言</option>
        <option value="web">網頁技術</option>
    </select><br><br>
    採購:<div id="book"></div>

<script type="text/javascript">    

    document.getElementById('category').onchange = function(evt) {
        var request = new XMLHttpRequest();

        request.onload = function(evt) {
            let req = evt.target;
            if(req.status === 200) {
                document.getElementById('book').innerHTML = req.responseText;
            }
        };

        let time = new Date().getTime();
        let url = `GET-1.php?category=${evt.target.value}&time=${time}`;
        request.open('GET', url);
        request.send(null);
    };
</script>

</body>
</html>

按我觀看執行結果

這個例子是連動選單,下一個下拉選單的選項是根據上一個下拉選單的選擇而定,事先不在網頁中寫死第二個選單的選項,而是根據上一個選單所發送的請求參數而定,例如若請求參數為 category=theory,會傳回以下的HTML片段:

<select>
    <option value="algorithm">常見演算</option>
    <option value="graphic">電腦圖學</option>
    <option value="pattern">設計模式</option>
</select>

當然,直接傳回 HTML 片段,並不是很好的方式,因為伺服端綁死了客戶端的頁面設計。這個範例只是用來示範 GET 的請求發送,之後會看到若傳回 XMLJSON 等其他資料格式,客戶端將有彈性自行決定頁面設計方式。

另外要注意的是,GET 請求時若 URL 相同,瀏覽器可能會作快取,為了避免取得舊的資料,可以在 URL 上附加時間戳記,讓每次 URL 不同,以避免瀏覽器作快取的動作。

Web 的世界中,故事往往不會這樣就結束。GET 在發送請求時,必須注意編碼的問題,因為 /?@、空白等字元,在 URL 中是保留字,RFC 3986 規範了哪些字作為保留字,如果要在 URL 表達這些保留字或一些非 ASCII 字元,必須使用 %hexhex 編碼形式。例如 url=https://openhome.cc,若要在 URL 中表示,必須處理為 url=https%3A%2F%2Fopenhome.cc,其中 %3A%2F%2F 分別就是 :// 三個字元編碼處理後的結果。

在 JavaScript 中,可以使用 encodeURIComponent 作這些字元的編碼,編碼後的結果是遵守 RFC 3986 的規範,然而在 RFC 3986 之前,HTTP 亦規範了 GETPOST 在發送請求參數時的編碼,大致上也是編碼為 %hexhex,不過空白字元是編碼為 + 而不是 RFC 3986 的 %20

如果直接透過瀏覽器按下發送按鈕來送出表單,瀏覽器會自動處理編碼(依網頁上指定的編碼來處理),並將空白字元編碼為 +,但透過 XMLHttpRequest 發送請求參數時,必須自行處理。

發送請求參數時,若使用 encodeURIComponent 編碼後,要再將 %20 取代為 +,以符合 HTTP 的規範。要注意的是,在字串處理方面,JavaScript 支援 Unicode,內部實作上採用 16 位元編碼每個字串元素,大致上可視為 UCS-2/UTF-16(這當中還有些歷史因素造成的細節,詳見 Effective JavaScript 一書條款七),不過,傳入 encodeURIComponent 的字串最後會以 UTF-8 進行編碼,若將 encodeURIComponent 的結果透過非同步物件發送出去,伺服端必須以 UTF-8 來處理接收到的字串。

下面這個範例是 GET 的另一個示範,在新增書籤時,若 URL 已重複(已有的書籤是 http://caterpillar.onlyfun.net 與 https://openhome.cc)則以訊息提示:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
</head>
<body>

    新增書籤:<br>
    網址:<input id="url" type="text">
    <span id="message" style="color:red"></span><br>
    名稱:<input type="text">

<script type="text/javascript">    

    // 組合與編碼請求參數
    function params(paraObj) {
        return Object.keys(paraObj)
                     .map(name => {
                         let paraName = encodeURIComponent(name);
                         let paraValue = encodeURIComponent(paraObj[name]);                         
                         return `${paraName}=${paraValue}`.replace(/%20/g, '+');
                     })
                     .join('&');
    }

    document.getElementById('url').onblur = function() {
        let request = new XMLHttpRequest();

        request.onload = function(evt) { 
            let req = evt.target;
            if(req.status === 200 && req.responseText === 'existed') {
                document.getElementById('message').innerHTML = 'URL 已存在';
            }
        };

        let reqParams = params({ 
            url : document.getElementById('url').value 
        });
        let time = new Date().getTime();

        request.open('GET', `GET-2.php?${reqParams}&time=${time}`); 
        request.send(null);
    };

</script>

</body>
</html>

按我觀看執行結果