第 10 章 - Ajax 的應用
上次更新日期 2020/12/07
JavaScript 還有許多不同的應用,其中應用最多的兩個模組,可能就是 Ajax 以及 JQuery 吧!我們這裡先來介紹 Ajax。 Ajax 的應用,其實主要是透過 XMLHttpRequest 這個物件的處理,將需要的 API 網址寫入到 XMLHttpRequest 物件中, 再透過該物件的功能,來取得 API 的回傳值,進一步將回傳值做處理,並用以更新網頁當中某一個區塊的內容。 這動作應用度很廣!大家來瞧瞧!
學習目標:
- 了解全網頁更新與區塊更新之間的差別
- 了解使用 XMLHttpRequest 物件
- 了解透過 XMLHttpRequest 物件的狀態進行額外工作
- 了解 responseText 與 responseXML 之間的差異
- 實際設計 API 範例程式
- 10.1: 了解什麼是 Ajax
- 10.2: 第一個 Ajax 網頁實做
- 10.3: 取得具有 DOM 文件格式的資料
- 10.4: 一個小 API 界面實做的範例
- 10.5: 課後作業
- 10.6: 參考資料
10.1: 了解什麼是 Ajax
要理解什麼是 Ajax 之前,可能得要先了解一下網頁程式語言才好。理解了網頁程式語言後,接下來,才能了解什麼是『非同步』的資料傳輸運作方式, 之後講到 Ajax 時,大家就會比較有感覺啊~
- 網頁程式語言 | 用戶端瀏覽器程式語言與 | 伺服器端程式語言
網頁程式語言因為運作的平台不同,主要分為用戶端瀏覽器上面執行的網頁程式語言,以及伺服器端網頁伺服器軟體上面執行的程式語言兩種。 先來談談伺服器端的程式語言。
一般來說,如果你要連上購物網站,或者是討論區,或者是社群網站,大多需要讀取伺服器的資料庫,這樣才能夠在網站上面留言、購物、刷卡等等, 這都需要伺服器端的網頁程式,進行某些認證機制,以及進行與資料庫溝通的程序。這些程序通常不能開放給讀者看,讀者只能操作網頁上面可應用的元件, 並不能看到實際的程式碼。否則,你的許多動作被抓出來,包括密碼的查詢方法被查到,總是比較可怕一些。
我們舉個 PHP 這種程式語言連動資料庫的情境好了,假如你要連到鳥哥私房菜的討論區,例如底下的連結:
這個討論區會與資料庫連動,所以,每次你連線時,都會有不同的資料顯示 (如果有新的留言時),這是因為 PHP 程式碼會一直去資料庫撈取最新資料來顯示, 所以,你就可以看到最新討論資料。不過,你看到的都是純文字資料,並沒有程式碼在裡面!程式碼都在伺服器端執行。 所以,如果程式碼寫的不夠好,會操死伺服器喔!
那用戶端的瀏覽器程式語言又是怎麼回事呢?其實,最常見的瀏覽器程式語言,就是 JavaScript 啊!伺服器將程式碼以純文字的方式回傳到用戶端, 之後瀏覽器開始分析網頁內容,並開始執行程式腳本。因為需要讓瀏覽器了解如何執行,因此,伺服器傳來的資料,就包括程式碼本身, 而不是程式碼運作後的 HTML 而已。
整體來說,整個用戶端瀏覽器、伺服器端網頁程式語言,以及資料庫系統,大致上可以使用底下的圖示去了解:
- 伺服器端設計好網頁伺服器軟體、網頁程式語言、資料庫系統、程式碼所在處等,然後等待用戶的連結
- 用戶端透過伺服器提供的連結網址 (URL),使用瀏覽器來連線到該網址,要求資料。
- 伺服器根據 URL 的連結,啟動 PHP 程式碼,連結到資料庫,取得資料庫的數據後,轉成純文字,並搭配 HTML, CSS, JavaScript 或影音資料等, 然後將所有的元件通通送達用戶端,然後就...射後不理,連線中斷。
- 用戶端瀏覽器接收完所有的檔案資料後,將所有元件載入到瀏覽器,然後開始依據 HTML 的標籤 (tag) 搭配 CSS 的樣式, 將網頁呈現出來,之後開始處理 JavaScript 的程式碼任務。
整個流程大概是這樣。所以,你常聽到某些學校單位裡面,若有多人同時連上校務系統、教學系統、選課系統時,會造成大當機! 那就是右側的 SQL 伺服器不夠力,或者是 PHP 等伺服端網頁程式語言寫的不夠好,因此就容易造成伺服器資源損耗的情況。那如果 JavaScript 程式碼寫錯呢? 舉例來說,之前同學練習迴圈時,如果寫錯迴圈的判斷,會造成瀏覽器感覺像是當機的情境,那就是用戶端資源耗盡,與伺服端就無關了。
因此,站在讓我們的網頁空間能夠提供更好的品質的情況下,將可能會損耗系統資源的動作丟給用戶端瀏覽器,藉以降低伺服器的資源使用量, 會是比較好的思考方向。
- Ajax 非同步 JavaScript 行為
Ajax 的全名是『 Ajax: Asynchronous Javascript And XML 』,意思是『非同步 JavaScript 與 XML』的意思,所以,跟資料的『非同步運作』, 透過 JavaScript 與 XML 格式有點關係的資料。字面上的意義是如此,那麼到底這個 Ajax 有什麼重要性呢?這就得要談到上面的圖示顯示的內容。
一般來說,如果你的網頁資料非常豐富,因此有影片資料、圖片資料、文字資料、JavaScript 程式碼資料等等,其中影片與圖片資料可能有非常多, 因此,要下載你的這個網頁 (onload) 時,就得要將所有的元件通通下載完畢之後,才能夠開始正常的顯示你的頁面。整個流程就像上面的圖示, 在用戶端要求了 URL 之後,用戶端得要等伺服器將『所有的元件』傳輸到用戶端系統後,瀏覽器才開始正常顯示頁面。
不過,你不會載入網頁後,全部的影片、圖片、資料通通一口氣就全部要看完!當然是『慢慢個別點擊』對吧!而且,有些資料你不見得會想要點擊, 對吧!所以,這樣傳統的全部資料載入才顯示的方法,就險的有點傷腦筋!因為,相當消耗頻寬啊~對 Server 的 loading 來說,真的很可怕!
這時, Ajax 就顯得很重要了!以 W3 school 網站的 Ajax 說明為例,他說, Ajax 可以作到:
- 更新網頁的資料並不需要全部網頁重新載入 (without reloading)
- 可以在網頁載入完畢後,在該網頁內部向 server 要求資料
- 可以在網頁載入完畢後,在該網頁內部取得 server 回傳的資料來更新自己頁面
- 在背景環境中,傳送資料到伺服器端,不會變動網頁畫面。
W3School 也特別強調,Ajax 並不是一個全新的程式語言,Ajax 只是一個整合了: (1)瀏覽器內建的 XML 的請求物件 (request object), 結合 (2)JavaScript 與 HTML DOM 的顯示特性功能,所以,這只是一個非常有趣的使用方法,全部的用法還是在 JavaScript 的語法當中實行, 只是 Ajax 這個方式,可以讓 Javascript 在背景後台向伺服器要求更新某部份的 div 方塊內容而已,不會整頁翻新!這對使用者體驗來說, 有相當良好的幫助喔!
- Ajax 怎麼運作的?
從 W3School 的圖示裡面,如下圖所示,我們可以來理解一下 Ajax 的運作:
上面的圖示是說,當一整個頁面都載入完成之後,還有其他的資料需要更新時,需要進行的任務有哪些。大致流程是這樣:
- 左上角:在用戶端瀏覽器上面,點擊或拉動等某個事件 (event) 發生後,瀏覽器會建立一個名為 XMLHttpRequest 的物件, 這個物件會包含想要跟伺服器端額外要求的部份資訊,包好這個物件後,就將該物件在背景傳送到伺服器端,然後, 該網頁可以讓使用者持續使用,而沒有任何的延遲情況發生。
- 右側的伺服器端:當收到用戶端傳來的這個 XMLHttpRequest 的小程式,伺服器就會依據要求,提供相對的資料,然後回傳給用戶端。
- 左下角:用戶端瀏覽器在背景接收到這些資料,並且完整接收完畢 (完整接收完畢後,XMLHttpRequest 通常會有個訊息狀態為 200 的情境, 當發生該情境時,瀏覽器就會知道資料接收完畢了) 後,就會透過 JavaScript 預先定義的功能,將該訊息放置到某個元件裡面, 通常是透過 .innerHTML 來更新網頁資料,所以就不會是全部頁面都更新!
了解了基本概念之後,再來讓我們處理一下,第一個 Ajax 的網頁吧!
10.2: 第一個 Ajax 網頁實做
事實上,如果只是靜態的、小小的網頁網站,實在不需要用到 Ajax 的,因為這個 Ajax 與框架 (frame) 完全不一樣, 框架是『載入完整的另一個網頁』,而 Ajax 則是透過 JavaScript 『更新這個網頁的某一個區塊內容』,兩者在應用上完全不同, 資料的設計上也完全不同!因為 Ajax 只是更新網頁當中某個區塊的內容,因此你不應該在該區塊填入完整的網頁資料, 而是拆開個別的資訊放置,才是正常的處理模式。
我們先來實做一個畫面僅有版面設計,以及固定的大按鈕導覽功能的頁面,然後透過該頁面來處理不同子連結的方案:
- 新增名為 unit10-2-1.php,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
- 將 這個檔案內容 加到 unit10-2-1.php 檔案內,並在瀏覽器上檢查輸出的情境。
- 讓瀏覽器載入完畢網頁後,就立刻執行 mymain 函數。
- 在 mymain() 函數內進行資料的設計:
- 先取得 content 元素控制權,然後讓 content 這個元素的高度變成初始化 (initial),亦即 content.style.height = 'initial', 因為未來這個函數會反覆被呼叫,必須要讓這個元素的高度回歸原始後,才重新設定!
- 進行區域變數 subnavY 的設計,內容為取得 subnav 這個元素的 .offsetHeight 數值,且轉為整數型態
- 進行區域變數 mainY 的設計,內容為取得 main 這個元素的 .offsetHeight 數值,且轉為整數型態
- 比較一下 mainY 與 subnavY,若 subnavY 大,則區域變數 yy 會是 subnavY,否則 yy 就是 mainY
- 最終將 content 這個元素的 .style.height 設定為 yy + "px" 即可。
- 最後再次查閱網頁時,就會發現,即使使用 position 為 absolute 的情境下,我們也能輕鬆的設計好外層方塊的高度!
- 建立共用的 XMLHttpRequest 物件建立的函數
回想一下上一個小節談到的 Ajax 的第一步,其實是本機瀏覽器上面要產生一個 XMLHttpRequest 的物件,這個物件是所有事情的開端! 但是,這個物件的建立,由於不同瀏覽器的關係,可能產生物件的方式不太一樣。基本的產生方式有幾個:
// 新的瀏覽器都支援的方式 var xhttp = new XMLHttpRequest(); // 較新的微軟 IE 6+ 以上瀏覽器支援的方式 var xhttp = new ActiveXObject("Msxml2.XMLHTTP"); // 最舊的 IE 5 以前版本的瀏覽器 var xhttp = new ActiveXObject("Microsoft.XMLHTTP");
其實,以新的處理方式而言,你只要撰寫上面第一行也就夠了,連 Windows 7 的 IE 都已經是 version 11 了,實在沒什麼必要支援 IE 5 以前的版本。 不過,從 netmarketshare 網站的追蹤發現,其實 IE 在 2020 年還是有大約 5% 的市占率!不能小覷啊~所以,在這個 XMLHttpRequest 的物件產生, 還是來思考一下怎麼處理比較好。一般建議設定一個函數,用該函數來處理這個物件的建置比較好!大致處理方案如下:
function createXHR() { // 嘗試用最新的方式建立 XMLHttpRequest 物件 try { var xhttp = new XMLHttpRequest(); } // 若產生問題時,才進行底下的動作,若成功,就執行 return 那行 catch(err1) { // 嘗試用 IE 6+ 建立物件 try { var xhttp = new ActiveXObject("Msxml2.XMLHTTP"); } // 若還是錯誤,才進行底下的方式 catch(err2) { // 嘗試使用 IE 5 相容方式建立物件 try { var xhttp = new ActiveXObject("Microsoft.XMLHTTP"); } // 還錯誤時,就設定 xhttp 是錯誤的 catch(err3) { xhttp = false; } } } return xhttp; }
將這個函數放置在 function.js 檔案中,未來只要載入這個檔案,那麼就能使用底下的方式來呼叫建立 XMLHttpRequest 物件了!
var xhttp = createXHR();
- 新增名為 function.js 的檔案,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
- 將上面的函數丟進去即可。
- 資料整理打包並送出需求
現在,讓我們將這個資料打包之後,送到伺服器去處理!而且需要在背景底下處理喔!一般來說,要將這個 XMLHttpRequest 的物件封裝之後, 並送出給伺服器處理,需要底下的幾個步驟:
// 建立需要的物件,透過剛剛建立的 function.js 裡頭的函數處理 var xhttp = createXHR(); // 向伺服器說明,我們需要的,是那一個額外的檔案、程式碼或其他資料等,這裡我們用 open 方式處理 xhttp.open("GET", "unit10-2-subnav-1.txt", true); // 設定好在背景底下監視,當上述的檔案讀回來的結果後,才進行後續的處理函數 xhttp.onreadystatechange = getcontent; // 開始將上述的需求送出 xhttp.send(null);
有玩過網頁表單的都知道,一般取得伺服器資料的基本方式有兩種,一種是從網址列輸入變數資料的 GET 方式,一種是資料量比較大而封裝起來的 POST 方式。 這邊我們使用較為單純的 GET 方式,來向 server 要求一個名為 unit10-2-subnav-1.txt 的檔案。而且先通知瀏覽器, 等等在背景底下嘗試送出要求後,監視這個要求的回傳結果,如果有任何結果,不論正確或錯誤 (onreadystatechange) 時,就使用 getcontent() 函數處理後續的任務,之後就直接送出資料。
那個 unit10-2-subnav-1.txt 檔案,一般大多數都是使用 php 的 API 功能來處理的!我們這裡先單純使用一個文字檔來替代! 此外,我們預計採用 getcontent() 函數來處理後續的任務,那麼這個 getcontent() 的內容,大多數情境又是如何呢?
- 根據回傳的結果進行資料的處理
我們要先知道資料回傳的狀態,亦即這個檔案: unit10-2-subnav-1.txt 到底是正確下載了?還是無法下載?基本上, onreadystatechange 當中, 可能有 readyState 及 status 兩個屬性,裡頭還有相關的數值說明如下:
屬性 | 值與說明 |
---|---|
readyState |
XMLHttpRequest 回傳值的結果,結果狀態如下:
|
status |
整體要求流程完成 (上面的數值 4) 後,那到底該流程的結果是如何?就在這裡展示:
|
statusText | 你可以使用 status 找到數值回應,也可以使用 statusText 找到文字說明。 基本上,就是回應上面的 OK, Forbidden 等等的文字資料而已,一般少用啦! |
當然是程序完成後 (readyState 為 4),以及確定下載完成 (status 為 200) 的情況下,才開始處理資料的放置更新,所以, 通常這個 getcontent() 函數,會這樣處理:
function getcontent() { if ( xhttp.readyState == 4 ) { if ( xhttp.status == 200 ) { subnav.innerHTML = xhttp.responseText; } else { subnav.innerHTML = '無法取得正確的子導覽清單'; } } }
如果回傳值是單純的文字資料,可以使用上面範本的 .responseText 這樣的格式來處理,但如果回傳的資料是一整個網頁內容,並不是資料而已, 該如何是好?沒關係,可以透過 .responseXML 來處理!只是透過 .responseXML 處理時,還可以針對某些元素做拆解! 例如,若回傳的資訊裡面有 id 名為 'res' 的資料時,要將它加入某個元素,可能就得要變成這樣:
xhttp.responseType = 'document'; var myxml = xhttp.responseXML; var res = myxml.getElementById('res'); subnav.appendChild(res);
類似這樣的條件來處理呢!
- 將 unit10-2-1.php 另存新檔 unit10-2-3.php,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
- 先匯入 js 腳本程式:先在 HTML 裡面的 <head> 當中,加入 function.js 檔案的存取,方式為:
<script src='function.js'></script> - 假設要匯入的按鈕為第 7 課,因此,請在第 7 課的 button 當中,加入
onclick='getsubnav("unit07")' - 建立名為 xhttp 的全域變數,先不用給內容。
- 開始建立 getsubnav(unit) 函數內容:
- 讓 xhttp 內容為執行 createXHR() 函數
- 判斷需要下載的檔案,因此,若 unit 為 unit07 時,就設定區域變數 uniturl 為 unit10-2-unit07.txt 這個檔名
- 使用 GET 的方式,下載 uniturl 變數指定的檔案
- 當狀態改變時,就執行 putsubnav 函數 (注意,不能加上小括號喔!)
- 送出要求。
- 開始處理 putsubnav() 函數內容:
- 設定變數 subnav 為取得 subnav 元素的控制權
- 判斷 readyState 是否為 4
- 判斷 status 是否為 200
- 將 .responseText 帶入到 subnav ID 的 innerHTML 內。
- 因為有更新 subnav 元素,因此重新執行一次 mymain 函數
- 開始處理 unit10-2-unit07.txt 這個檔案內容,請先新增這個檔名,然後將 index.php 內的第七章連結,抓下來貼上此處即可。
用這種方式建立網頁的好處是,你可以將第一頁的美工做好之後,未來每個方塊裡面要塞什麼資料,全部都是自己決定, 都透過資料篩選的方式來透過取代的項目處理。你可以看到,在這個範例中,網址列的檔名都沒有改變喔!因為只是『更新』 subnav 的內容而已啊!
10.3: 取得具有 DOM 文件格式的資料
基本上,Ajax 除了可以取得一般正常的純文字資料 (responseText) 之外,實際上還是可以取得完整的網頁文件喔! 然後透過該文件,還能夠進行 DOM 的目錄樹資料查詢,用途非常廣!舉例來說,我們都知道 index.php 裡面有所有章節的連結, 既然都有連結了,為什麼還需要額外抓取資料到不同的檔案去?能不能接從 index.php 將各章節資料分別取得呢?
如前一個小節提到的,我們其實可以透過 javascript 取得的資料格式設定,指定讓資料變成 DOM 格式, 然後依據不同的 id 或者是 name 等方式,就可以擷取部份資料了。來玩一玩下一題,大概就比較清楚怎麼回事喔!
- 將 unit10-2-3.php 另存新檔 unit10-3-1.php,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
- 事先處理:打開 index.php 這個檔案,在個別的章節裡面的 ul 標籤,增加 id 識別碼,第一章為 <ul id='unit01'>, 第二章為 <ul id='unit02'>,以此類推,更新到目前的第 10 章的 ul 標籤。這個目的是,我們會透過按鈕, 在 ajax 取得 index.php 之後,依據這個 id 來擷取個別章節的連結,而不用各自獨立編寫文字檔。
- 新建 contentXY() 函數,目的在取得 content 元素的高度,內容由 mymain 取得:
- 將 mymain 函數內的 (1)content 初始化、(2)subnavY 參數、(3)mainY 參數、(4)content 元素高度的判斷,以及 (5)content 元素的高度設定這 5 行移動到這個函數中, 未來更新 content 的高度將直接使用這個函數即可。
- 修改 mymain 函數
- 將上述的 5 行移植到 contentXY 函數中,且在第一行就先執行 contentXY() 函數;
- 前往 body 的 header 內,將 <button 裡面新增 id 項目,例如第七章就加上『 <button id='unit07'> 』這樣的設定, 這個章節資料可以的話,跟 index.php 內的 id 名稱對應最好。
- 在 mymain 裡面,設定 mybtn 這個變數,內容為取得 header 元素內的 button 這個標籤名稱 (ByTagName)。
簡易的設定方式如下:
var mybtn = document.getElementById('header').getElementsByTagName('button'); - 針對 mybtn 這個陣列,使用 for 迴圈進行設定,增加 onclick 的事件監聽:『 mybtn[i].onclick = getsubnav 』這樣的事件監聽方式。
- 修改 getsubnav() 函數:
- 增加『 unitid = this.id 』的設定,讓被點擊的按鈕的 id 名稱取得為 unitid
- 主要是增加回應的資料必須是 DOM 文件,因此增加底下在 onreadystatechange 前即可:
xhttp.responseType = 'document'; - xhttp.open 的內容,將檔名修訂成為 index.php
- 修改 putsubnav() 函數:
- 設計區域變數 xmldoc ,內容為 xhttp.responseXML 這樣的格式
- 設計區域變數 res,內容為針對 xmldoc 取得 id 為 unitid (getsubnav 設計的變數),因此 res 也是個物件
- 針對 subnav 設計 innerHTML 為空值 (將所有的子元素刪除)
- 針對 subnav 增加子元素 (appendChild) ,子元素設計為 res 這個物件
- 執行 contentXY() 函數,
- 將文件加入 main 的 div 當中
若以上述的案例來看,左側的超連結看起來也是怪怪的!畢竟點選之後,還是跑去其他頁面了...如果你想要讓網頁的『 body 』內的資料可以保持在右側的方塊中, 那可以重複使用 ajax 的功能來處理即可。
- 將 unit10-3-1.php 另存新檔 unit10-3-2.php,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
- 因為預計要修改 subnav 裡面的 a 標籤的點擊情境,所以,在 putsubnav() 函數內,在 subnav 新增子元素後,進行如下的設計:
- 設定名為 ref 的區域變數,內容於取得 subnav 元素內的 A 標籤控制權
- 使用 for 迴圈,針對 ref[i] 設定 onclick 的事件監聽,會執行 gethref 的函數
- 開始處理 gethref() 函數,這個函數的主要目的,是取得『被點擊的那個 a 標籤內的 href 值』為何,
然後將該資料抓下來後,再想辦法透過 ajax 的動作,將遠端的 url 資料下載,並更新本身網頁內某個位置的資料。
- 設定全域變數 myurl,其數值應該是『 this.href 』才對喔!
- 取得這個數值之後,開始執行 getmain() 這個 ajax 函數的呼叫
- 最後務必要回傳 false!讓 a 的點擊失效才行!
- 開始執行 getmain() 函數,主要在進行 ajax 的包裹行為:
- 同樣使用 xhttp 建立 createXHR()
- 設定 xhttp 為 document 類型
- 在 onreadystatechnage 時,執行 putmain 函數
- 使用 xhttp.open,同樣使用 GET 的方式,但下載的檔名為 myurl 這個變數。
- 最後直接送出 (send)。
- 開始撰寫取得資料的 putmain() 函數:
- 先設定 main 區域變數,內容為取得網頁內的 main 元素控制權
- 如果 readyState 為 4 才進行
- 如果 status 為 200 才進行
- 同樣使用 xmldoc 取得 xhttp.responseXML 樣式
- 設定區域變數 res 為 xmldoc 取得 TagName 為 BODY 的內容
- 先清空 main 的 innerHTML
- 讓 main 增加子元素為 res[0] !因為是 TagName 的關係!
- 重複執行 contentXY() 函數
雖然這樣就可以取得 body 的物件,然後將它貼到右側去,可惜的是, script 的部份目前似乎支援度怪怪的, 而且,我們撰寫的 javascript 很容易衝突,因為函數名稱經常設定一樣...所以針對 javascript 來說, 應該是會有點傷腦筋!如果是純粹的 PHP 等,應該就比較沒問題!
10.4: 一個小 API 界面實做的範例
其實, ajax 的應用並不像上面提供的案例,一般來說,Ajax 的應用,通常是透過某些 API 來實做某個區塊的資料更新才對。 因此,ajax 通常會用額外的一些小程式 (API) 來提供網頁上某個區塊的更新才是。如果你使用了自己本身伺服器的 API 應用, 一般來說,問題不大。但是,如果你使用了不同來源的資料,就很可能會發生部份的問題,那就是可能會產生 CORS 的問題!
- 分析資料來源網站的情境
先讓我們來實做一下用 ajax 取得外部資料的方法。現在,假設你想要在你網頁的右上方顯示現在即時的溫度與天氣狀態, 最常見的方法,當然就是跑去中央氣象局取得這些資料了。以 2020 年 12 月份的中央氣象局網站為例,如果想要看台南市的各測站的資料, 可以參考底下的網頁:
不過,我們並不是需要全台南的資料,如果只想要靠近崑山科大的氣象站,可以選擇永康區或者是台南測站,我們以永康測站為例, 點選永康測站,亦即如下的網站,就能得到最近 24 小時的測站天氣資料:
仔細觀察一下上述的連結,你會發現到網址列會有 ?ID=46742 這樣的狀態,其實那個數字就是中央氣象局針對各測站所設定的測站代碼, 因此,你只要輸入不同的測站代碼,那就可以得到該測站最近 24 小時的天氣資料了。這也就是說,上面這個網址, 其實就是中央氣象局為自己寫的一個 API 網頁程式喔!
如果你查看上述網頁的原始碼,可以發現到最底下的位置上,有個很特別的 javascript 腳本程式, 長的有點像底下的模樣:
<script src="/V8/assets/js/observe/OBS_Station.js?v=20201016"></script>
<!--[if lt IE 9]>
<script src="/V8/assets/plugins/IE/respond.js"></script>
<script src="/V8/assets/plugins/IE/html5shiv.js"></script>
<script src="/V8/assets/plugins/IE/placeholder-IE-fixes.js"></script>
<![endif]-->
</body>
</html>
那個網址列其實就是取得不同測站數據的重要來源之一!這是因為關鍵字『 observe 』為『觀測』的意思! 點選該網址,你還可以持續發現很多類似網址列的重要分析,其中最重要的應該是 MOD_DIR 以及 MOD_FILE 的資料! 根據該資料,我們可以得到永康測站的 24 小時連續資料,應該可以從底下的網址列直接取得:
然後持續檢查一下上述的原始碼資料,就可以發現到內容就是一堆 <tr> 與 <td> 等表格資料內容! 然後透過類似 temp 與 weather 這兩個 headers 的表格內屬性,就可以得到我們需要的溫度與天氣狀態了。 之後再以 DOM 的分析方式,分別取得相關的資訊之後,應該有辦法將我們需要的資料直接更新到網站上吧? 現在就讓我們來測試看看。
- 跨站執行 ajax 的問題
我們的目標是:『在網站右上角填入需要的天氣參數』,所以,先來規劃一下 header 裡面的資訊吧!
- 將 unit10-3-2.php 另存新檔 unit10-4-1.php,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
- 先在 header 的 div 裡面,在所有的 button 後面,新增一個 div, id 設定為 cwb,內容直接寫上『天氣』兩個字即可。
- 針對 cwb 的 CSS 設計,大概先設計了:
- 先給予框線,一般的框線設計即可
- 因為要讓方塊『黏』在右上角,因此設計位置為絕對位置,然後上方與右方距離為 0px
- 其餘包括 padding 與字型、字體大小等,請自由設計!自己開心就好!
接下來,準備開始讓 ajax 發揮功效!其實,很簡單耶!就直接設計 ajax 那兩個函數即可!一個就是 get,一個就是 put, 唯一差別是,需要下載的 url 不一樣而已,實驗看看:
- 將 unit10-4-1.php 另存新檔 unit10-4-2.php,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
- 新增兩個函數,第一個主要是設計 ajax 的包裹,設計為 getcwb() 函數,
內容請由 getmain() 複製過來,但是更改部份資料:
- 增加 cwburl 變數,內容就是之前談到的永康測站的網址列
- onreadystatechange 所執行的函數變更名稱為 putcwb
- open 的內容,網址列修訂為 cwburl 即可。
- 撰寫 putcwb() 函數,這個函數的內容先設計為取得 cwb 方塊元素的控制權即可! 其他等等再處理。
- 在 mymain() 函數最下方新增一行,主要執行 getcwb() 即可。
基本上,目前的伺服器預設都是不允許來自非自己網域的 ajax 要求的,這是因為擔心被大量的 ajax 要求服務,導致自己的 loading 增加之外, 也可能會遭受到某些外部的攻擊!你可以上網使用 CORS 與 ajax 這個關鍵字查詢,就會查到一堆介紹的文章! 這裡不再重複說明。
- 自己撰寫中繼網頁,達到避免 CORS 的問題
如果中央氣象局網站是我們能夠控制的,那麼很簡單,就可以在中央氣象局的 web server 上面,增加放行我們上課用網站的使用權, 那就可以直接連到中央氣象局了。可惜的是,不可能啊!那怎辦?沒關係,既然中央氣象局原本就是給我們查閱資料的, 那,把自己當成用戶端不就行了?這就是中繼站的由來!我們可以透過 PHP 後端平台的設計,來達到這個目的! 簡易的設計如下(假設檔名為 unit10-4-3.php):
<?php
if ( isset($_GET['sid']) ) {
$sid = $_GET['sid'];
} else {
$sid = "46742";
}
$url = "https://www.cwb.gov.tw/V8/C//W/Observe/MOD/24hr/" . $sid . ".html";
// url 網址列不能含有類似空格的特殊字元喔!
$data = file_get_contents($url);
?>
<!doctype html>
<html>
<head>
<meta charset='utf-8' />
</head>
<body>
<?php
echo $data;
?>
</body>
</html>
基本上,就完全模仿中央氣象局原本的資料格式,然後,請在 index.php 裡面新增一條連結,以例題 10-4-3 來連結到 unit10-4-3.php 檔案, 之後,請查詢中央氣象局的網站測站代碼,要更改測站代碼,直接以『 unit10-4-3.php?sid=46741 』之類的方式來更新即可! 這就是我們撰寫的中繼用小小 API 網頁了!
另外,你需要特別注意的是,上面 file_get_contents 函數內的網址列,不能有空格之類的特殊字元!
- 繼續處理氣象資料的更新功能
現在我們就已經有個 API 了,因此,使用自己網站的資料,應該就不至於會影響到 CORS 這種跨站的問題了! 現在,讓我們來將溫度與天氣狀態抓下來嵌入到我們的網站上吧!
- 將 unit10-4-2.php 另存新檔 unit10-4-4.php,並在 index.php 裡面加上相關的超連結,target 指向 js 視窗。
- 修改 getcwb() 函數,將原本的 cwburl 註解掉,然後:
- 新增一個區域變數 sid,填寫永康測站為 46742
- 將 cwburl 改為本地『 "unit10-4-3.php?sid=" + sid 』這樣的格式
- 修改 putcwb() 函數,大部分資料由 putmain() 複製過來,然後:
- xmldoc 為使用 responseXML 的樣式取得
- 設定 res 為 xmldoc 的 ClassName,名稱為『 tem-C is active 』若不確定時,請前往 API 的結果去查詢
- 設計 mytmp 區域變數內容為 res[0].innerHTML 的值,這個是氣溫
- 再次重新取得 xmldoc,同樣使用 XML 的樣式。
- 重點在取得 img 內的 title (或 alt 皆可),因此使用 TagName 取得 IMG 元素
- 設定 myweather 區域變數為 res[0].title。
- 最終讓 cwb.innerHTML 搭配溫度與天氣的狀態來展示即可。
- 修改 mymain() 函數:
- 保留一開始就執行一次 getcwb() 函數
- 新增一行,讓該函數使用 setInterval 的方式,每 10 分鐘,就是 600 秒,就是 600000 毫秒的樣式重複執行!
未來,如果你有任何的資料庫需要讀、寫資料時,又不想要整頁更新,只想部份區塊進行更新,就可以透過設計 API 來處理。 另外,透過 ajax 的功能,你也可以將內容與樣式分開,讓樣式歸前端設計師處理,後端資料交由後端工程師處理, 然後前後端設計師彼此透過某支 API 進行資料的進/出工作,就可以達到分工的效果了!當然,設計這隻 API 的介接工程師, 工作就很繁重!這也是目前很熱門的職業之一喔!
10.5: 課後作業
系上未來的專題,很多時候都需要用到公開的資料庫系統,將上面的資料拿來進行比對或處理。因此,自製重要的 API 與將資料抓下來處理,就顯得很重要了!
- 10-5-1、實做一個站外的 API 應用
選擇一個你有興趣的課題,例如鐵路時刻表、高鐵時刻表、環保署空氣品質、環保署水資源應用、水利署水庫目前水位、 新聞網站的最新新聞訊息等,反正就是你有興趣的課題,模仿本章課程中的中央氣象局資料取得的方式 (unit10-4-3.php), 製作一個簡易的中繼 API 網頁,用以取得你有興趣的部份網站資訊,而不是全部的資料。
收集資料的過程會比較繁複,請依據本課程的說明,慢慢收集好對方網站的 API 網頁,就可以逐步取得你想要知道的數據了。 沒有特定的主題,除了上面的主題之外,你也可以依據自己有興趣的主題來設計相關的跨站內容取得。
底下的範本,是根據環保署測站分析資料的結果來處理的!
- 10-5-2、將資料整合到網頁中
請將資料整合到整體網頁中,以本章 unit10-4-4.php 為範例,看看你要將資料結合到什麼地方都可以!不過,如果以上面的資料來看,輸出的格式其實是 JSON 的格式,要轉成我們可以應用的 javascript 元件,可能需要使用『 JSON.parse(output) 』來處理資料的輸出才可以喔!
10.6: 參考資料
- W3School 對 Ajax 的說明:https://www.w3schools.com/xml/ajax_intro.asp
- 追蹤瀏覽器市占率的網站:https://netmarketshare.com/