位元詩人 [網頁設計] 教學:以 CGI 程式回應 Ajax 請求

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

在程式設計者以 CGI 程式寫網頁程式時 Ajax 尚未問世,那時候的網頁程式都是以表單 (form) 傳送資料。相對來說,在 Ajax 大量地出現在網頁程式時,程式設計者已經不太寫 CGI 程式。所以,網路上有關 CGI 程式的教學不會提到 Ajax 的部分。

然而,在 CGI 界面中,並沒有寫死 CGI 程式所用的 HTTP 方法 (HTTP method),保留了使用新的 HTTP 方法的彈性。雖然在實務上使用 CGI 程式來回應 Ajax 請求的機會甚少,但並非不可行。在本文中,我們展示以 CGI 程式回應 Ajax 請求的方法。

在 CGI 程式中偵測 HTTP 方法 (HTTP Method)

在實際撰寫 CGI 程式前,我們先做一個簡單的小試驗,確認 CGI 程式可以偵測 HTTP 方法。

在傳送表單中,能使用的 HTTP 方法只有 GETPOST。但在 Ajax 呼叫中,可以使用其他的 HTTP 方法,像是 PUTDELETE。我們現在撰寫一隻小型 CGI 程式,用來確認 CGI 程式可以接收新的 HTTP 方法。

同樣地,我們刻意用 Pascal 來實作 CGI 程式。此 CGI 程式的程式碼如下:

program main;
uses
  Dos;

const
  newline = ANSIChar(#10);
var
  method : string;
begin
  method := GetENV('REQUEST_METHOD');

  (* Response header *)
  Write('Content-Type: text/plain; charset=utf-8' + newline + newline);

  (* Resonse body *)
  if '' = method then
    Write('Unknown HTTP method')
  else
    Write('HTTP method: ' + method);
end.

以 Free Pascal 編譯此程式碼:

$ fpc -oindex.cgi detectHTTPMethod.pas

當我們以瀏覽器存取此 CGI 程式時,會顯示以下結果:

HTTP method: GET

由於瀏覽器只支援 GET,若要使用其他 HTTP 方法,就要用 HTTP 客戶端程式 (HTTP client)。透過 HTTP 客戶端程式來存取 CGI 程式,就不用寫 JavaScript 程式。一些可用的 HTTP 客戶端程式如 curlHTTPie

由於 HTTPie 比較易用,這裡藉由 HTTPie 以 POST 方法對 CGI 程式發出請求:

$ http POST http://localhost:8080/cgi-bin/index.cgi

不過,POST 方法透過表單也可以達成。我們改用 PUT 方法對 CGI 程式發出請求:

$ http PUT http://localhost:8080/cgi-bin/index.cgi

必要時,也可以用 DELETE 方法:

$ http DELETE http://localhost:8080/cgi-bin/index.cgi

並不是每個 CGI 伺服器都支援所有的 HTTP 方法。像 Python 內建的 HTTP 伺服器只支援 GETPOST。而 fcgiwrap 則支援較多的 HTTP 方法。如果讀者使用其他 CGI 或 FastCGI 伺服器,也可以自己測試一下。

以 CGI 程式搭配 Ajax 呼叫

當我們確認 CGI 程式可以接收各種 HTTP 方法後,就可以撰寫 CGI 程式來回應 Ajax 呼叫。

為了簡化範例,我們的網頁程式相當簡單,只有一個輸入框和一個按鈕:

頁面部分的程式碼和本文的主題關係不大,我們把頁部的部分放在這裡,有興趣的讀者可以自己追蹤一下程式碼。

為了簡化範例,我們略去呼叫函式庫的部分,直接展示網頁前端程式的部分,並加上行號:

function showMessage(message) {                            /*  1 */
    let msg = document.getElementById('message');          /*  2 */
    msg.innerHTML =                                        /*  3 */
        '<div class="alert alert-warning" role="alert">'   /*  4 */
        + message                                          /*  5 */
        + '</div>';                                        /*  6 */
}                                                          /*  7 */

function clearMessage() {                                  /*  8 */
    let msg = document.getElementById('message');          /*  9 */
    msg.innerHTML = '';                                    /* 10 */
}                                                          /* 11 */

function showInfo(data) {                                  /* 12 */
    let info = document.getElementById('info');            /* 13 */
    info.innerHTML =                                       /* 14 */
        '<div class="alert alert-info" role="alert">'      /* 15 */
        + data                                             /* 16 */
        + '</div>';                                        /* 17 */
}                                                          /* 18 */

function clearInfo() {                                     /* 19 */
    let info = document.getElementById('info');            /* 20 */
    info.innerHTML = '';                                   /* 21 */
}                                                          /* 22 */

let btn = document.getElementById('btnInch2CM');           /* 23 */
btn.addEventListener('click', function () {                /* 24 */
    /* Get value from input. */                            /* 25 */
    let value = document.getElementById('inch2cm').value;  /* 26 */

    if ('' === value) {                                    /* 27 */
        clearInfo()                                        /* 28 */
        showMessage('No data');                            /* 29 */
    } else {                                               /* 30 */
        clearMessage();                                    /* 31 */

        /* Clear old value. */                             /* 32 */
        document.getElementById('inch2cm').value = '';     /* 33 */

        superagent                                         /* 34 */
            .put('/cgi-bin/inch2cm.cgi')                   /* 35 */
            .send({ data: value })                         /* 36 */
            .then(function (res) {                         /* 37 */
                showInfo(res.body.data);                   /* 38 */
            })                                             /* 39 */
            .catch(function (err) {                        /* 40 */
                if (err.response) {                        /* 41 */
                    showMessage(err.response.message);     /* 42 */
                } else {                                   /* 43 */
                    showMessage(err);                      /* 44 */
                }                                          /* 45 */
            });                                            /* 46 */
    }                                                      /* 47 */
});                                                        /* 48 */

第 1 行到第 11 行的部分和錯誤訊息有關。因為網頁不是命令列環境,我們要自己設計輸出入。我們用 Bootstrap Alert 輸出錯誤訊息。同理,第 12 行到第 22 行的部分和結果有關。為了區分錯誤訊息和結果,我們使用不同顏色的 Bootstrap Alert。

第 24 行到最後的部分是按鈕的事件處理器 (event handler)。在網頁使用者按下按鈕時,該事件處理器會從輸入框收集資料,接著觸發 Ajax 呼叫,該 Ajax 呼叫會向後端的 CGI 程式發出請求。

第 34 行到第 46 行的部分是 Ajax 呼叫。為了簡化 Ajax 呼叫,我們沒有使用原生 JavaScript 程式來寫,而用 SuperAgent 簡化 Ajax 相關的程式碼。傳入的 JOSN 字串範例如下:

{
    "data": 100
}

由於傳遞 JSON 物件在 Ajax 呼叫中相當常見,SuperAgent 會自動處理這個部分,程式設計者不用自己轉換和解析 JSON 物件。

為了強調 CGI 是語言中立的,本範例 CGI 程式同樣用 Pascal 來寫,而且不用 CGI 模組。以下是此 CGI 程式的程式碼:

program main;                                                     (*   1 *)
{$mode objfpc}                                                    (*   2 *)
uses                                                              (*   3 *)
  dos, fpjson, jsonparser, sysUtils, termio;                      (*   4 *)

function inchToCM(input: real): real;                             (*   5 *)
begin                                                             (*   6 *)
  inchToCM := input * 2.54;                                       (*   7 *)
end;                                                              (*   8 *)

const                                                             (*   9 *)
  newline = ANSIChar(#10);                                        (*  10 *)

var                                                               (*  11 *)
  method : string;                                                (*  12 *)
  data : TJSONData;                                               (*  13 *)
  json : TJSONObject;                                             (*  14 *)
  c : char;                                                       (*  15 *)
  s : ansistring;                                                 (*  16 *)
  value : real;                                                   (*  17 *)
  out : real;                                                     (*  18 *)

begin                                                             (*  19 *)
  method := GetEnv('REQUEST_METHOD');                             (*  20 *)

  (* Check whether HTTP method is valid. *)                       (*  21 *)
  if 'PUT' <> method then                                         (*  22 *)
  begin                                                           (*  23 *)
    Write('HTTP/1.1 405 Method Not Allowed' + newline);           (*  24 *)
    Write('Content-Type: application/json' + newline + newline);  (*  25 *)

    json := TJSONObject.Create();                                 (*  26 *)
    json.Add('message', 'Method Not Allowed');                    (*  27 *)
    Write(json.AsJSON);                                           (*  28 *)
    Halt(0);                                                      (*  29 *)
  end;                                                            (*  30 *)

  s := '';                                                        (*  31 *)
  while not eof(input) do                                         (*  32 *)
  begin                                                           (*  33 *)
    read(c);                                                      (*  34 *)
    s := s + c;                                                   (*  35 *)
  end;                                                            (*  36 *)

  (* Check whether the format of input is valid. *)               (*  37 *)
  try                                                             (*  38 *)
    data := GetJSON(s);                                           (*  39 *)
    json := TJSONObject(data);                                    (*  40 *)
  except                                                          (*  41 *)
    Write('HTTP/1.1 422 Unprocessable Entity' + newline);         (*  42 *)
    Write('Content-Type: application/json' + newline + newline);  (*  43 *)

    json := TJSONObject.Create();                                 (*  44 *)
    json.Add('message', 'Wrong data format');                     (*  45 *)
    Write(json.AsJSON);                                           (*  46 *)
    Halt(0);                                                      (*  47 *)
  end;                                                            (*  48 *)

  (* Check whether input data is available. *)                    (*  49 *)
  try                                                             (*  50 *)
    s := json.Get('data');                                        (*  51 *)
  except                                                          (*  52 *)
    Write('HTTP/1.1 422 Unprocessable Entity' + newline);         (*  53 *)
    Write('Content-Type: application/json' + newline + newline);  (*  54 *)

    json := TJSONObject.Create();                                 (*  55 *)
    json.Add('message', 'No data');                               (*  56 *)
    Write(json.AsJSON);                                           (*  57 *)
    Halt(0);                                                      (*  58 *)
  end;                                                            (*  59 *)

  (* Check whether input data is not empty. *)                    (*  60 *)
  if '' = s then                                                  (*  61 *)
  begin                                                           (*  62 *)
    Write('HTTP/1.1 422 Unprocessable Entity' + newline);         (*  63 *)
    Write('Content-Type: application/json' + newline + newline);  (*  64 *)

    json := TJSONObject.Create();                                 (*  65 *)
    json.Add('message', 'Empty data');                            (*  66 *)
    Write(json.AsJSON);                                           (*  67 *)
    Halt(0);                                                      (*  68 *)
  end;                                                            (*  69 *)

  (* Check whether the format of input data is valid. *)          (*  70 *)
  try                                                             (*  71 *)
    value := StrToFloat(s);                                       (*  72 *)
  except                                                          (*  73 *)
    Write('HTTP/1.1 422 Unprocessable Entity' + newline);         (*  74 *)
    Write('Content-Type: application/json' + newline + newline);  (*  75 *)

    json := TJSONObject.Create();                                 (*  76 *)
    json.Add('message', 'Wrong data');                            (*  77 *)
    Write(json.AsJSON);                                           (*  78 *)
    Halt(0);                                                      (*  79 *)
  end;                                                            (*  80 *)

  out := inchToCM(value);                                         (*  81 *)

  (* Render the output as JSON. *)                                (*  82 *)
  Write('Content-Type: application/json' + newline + newline);    (*  83 *)

  json := TJSONObject.Create();                                   (*  84 *)
  json.Add('data', format('%.3f', [out]));                        (*  85 *)
  Write(json.AsJSON);                                             (*  86 *)
end.                                                              (*  87 *)

此 CGI 程式的程式碼看起來比較長,這是因為我們做了許多檢查的工作。其實英吋 (inch) 轉公分的程式很短,在範例程式的第 5 行至第 8 行。

由於此範例程式有用到 try ... except ... 區塊,得用 objfpc 模式來編譯。我們在第 2 行打開這項編譯器指示詞。

此程式在第 20 行取得傳入的 HTTP 方法,並在第 22 行檢查傳入的 HTTP 方法是否為 PUT。當傳入的 HTTP 方法不是 PUT 時,此程式會回傳 HTTP 狀態碼 405 及代表錯誤訊息的 JSON 字串,然後提早結束程式。和回傳相關的程式碼在第 24 行至第 28 行。

由於本範例程式以 PUT 接收網頁前端程式的資料,資料會從標準輸入 (standard input) 傳入。第 31 行至第 36 行是將資料一口氣讀入程式的方法。

將資料讀入後,要把資料從 JSON 字串轉為 Pascal 程式可處理的型態。所幸 Pascal 已經提供相關的套件,我們就不需自行實作了。由於傳入的資料有可能是錯的,我們用 try ... except 區塊去檢查異常情境,並在資料格式出錯時提早結束程式。同樣地,要以網頁程式所規範的方式來回傳錯誤訊息。相關的程式碼在第 38 行在第 48 行。

即使資料的格式是正確的,不代表有資料,所以我們在取得資料時同樣要考慮異常情境,在異常發生時提早結束程式。相關的程式碼在第 50 行至第 59 行。

如果有資料但資料有空字串,仍然是無效的資料。所以我們檢查資料是否為空。當資料為空字串時,得提早結束程式。相關的程式碼在第 61 行至第 69 行。

接著,要把傳入的字串轉為浮點數。由於轉換的過程不保證正確無誤,在這裡仍然要用 try ... except ... 區塊去檢查異常情境。相關的程式碼在第 71 行至第 80 行。

最後,才進行真正的運算,並將結果回傳。相關的程式在第 81 行至第 86 行。

結語

在本文中,我們展示了使用 CGI 程式回應 Ajax 呼叫的方式。雖然這是一個少見的使用情境,只要了解網頁程式的原理,仍然可以實作出來。

關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。