最後更新於 2021 年 10 月 17 日
因為本篇以 POST請求 為主旨,GET及其他HTTP操作的部分不會提到太多。我之前發過一篇關於Flutter發出HTTP請求獲取數據的文章,有興趣可以看一下:[Flutter]發出HTTP請求獲取數據
在開始之前,先補充一些知識點。
HTTP
HyperText Transfer Protocol(HTTP)是一種用戶端瀏覽器和伺服端伺服器之間溝通的標準協定,他是屬於OSI七層模型中的應用層。
網路資料的傳輸是建構在 HTTP 協定之上,如下圖 Request、Response之間的交換機制都是基於HTTP的基本規範。
- Request:使用者透過瀏覽器或程式發送的 HTTP 請求 ,一般來說分成 GET 和 POST 兩種方法。
- Response:網頁伺服器收到 Request 後,回傳給使用者的 HTTP Response,通常會有兩種形式。一種是僅有特定資料格式所組成的字串,稱為是 API;另一種是包含 HTML 的原始碼,稱為 HTML Response。
如果查看使用POST請求方法的資源並在請求頭的地方點擊「View Source」可以查看HTTP的訊息格式。不管是 Request 或是 Response 的封包都由三種部份所組成:State、Headers、Body,以下有兩種例子作參考:
HTTP請求方法
常見如GET、POST,但其實不只這兩種,只不過其他很少會用到就不多提。
GET vs. POST
GET的資料是包含在網址當中,而POST的資料是包含在封包之內,看一個例子就能理解:
GET
你在 Twitter 搜索 COVID-19 後觀察網址可以得到:
https://twitter.com/search?q=COVID-19&src=typed_query
網址後面多了一串 search?q=COVID-19&src=typed_query
是為什麼呢?
原因是使用GET方式請求瀏覽器會將表單內容轉為Query String加在URL裡進行連線,網址 ?
後面的為參數,每個參數值以 &
隔開,因此可以得知總共有兩個參數:q(搜索關鍵字)、src。
使用開發者工具觀察可以看到Request Method確實是GET。
POST
但是如果是以POST方式請求的話,可以觀察送出前後網址是不會有變化的,得開啟開發者工具查看詳細的請求資訊以及輸入的表單資料:
送出的表單資料可以在headers / Form Data得到
Flutter的網路請求
大致分為三種方式:
HttpClient
Dart原生網路請求方式
import 'dart:io'; var httpClient = new HttpClient();
該 client 支持常用的HTTP操作,如:GET, POST, PUT, DELETE. 但此方法對POST貌似比較不友好,所以建議使用http庫或dio庫
http庫
POST請求格式
- url:請求地址(必要)
- headers:請求頭(可選)
- body:參數(可選)
- Encoding:編碼(編碼)
post(dynamic url, { Map<String, String> headers, dynamic body, Encoding encoding }) → Future<Response>
Example:
import 'package:http/http.dart' as http; await http.post('https://www.coa.gov.tw/theme_list.php?theme=news&sub_theme=agri', headers: headersMap,body: formdata,encoding: Utf8Codec()) .then((http.Response response) { var responseBody = response.body; // 處理響應數據 }).catchError((error) { print('$error'); });
dio庫
Example:
import 'package:dio/dio.dart'; void getHttp() async { try { var response = await Dio().get('http://www.google.com'); print(response); } catch (e) { print(e); } }
至於選擇哪一種方式全憑個人喜好和習慣,我個人是比較常用http庫。
資源Headers
先叫出開發者工具 找到 Network,可以得到頁面的資源,至於開發者工具的相關說明這邊不多加篇幅贅述,請自行google。
Network:從發起網頁頁面請求Request後,分析HTTP請求後得到的各個資源請求資訊(包括狀態、資源型別、大小、所用時間等)
試著修改表單的一些選項,然後刷新取得最新的資源請求(與網址同名)
點開後能查看該資源的詳細訊息,分為Headers、Preview、Response、Initiator、Timing、Cookies
Headers
會列出資源的請求url、HTTP方法、狀態碼、請求頭和響應頭及它們各自的值、請求參數等等。如圖所示
General
Request URL代表請求路徑,也是API路徑。
Status Code 為 HTTP狀態碼
- 資訊回應(Informational responses) 100-199
- 成功回應(Successful responses) 200-299
- 重定向(Redirects) 300-399
- 用戶端錯誤 (Client errors) 400-499
- 伺服器端錯誤 (Server errors) 500-599
這邊可以看到狀態碼是200 OK 代表請求成功,而Request方法為 POST。
Request Headers
為請求頭資訊
- accept:提供給後端了解前端所能接受的資料類型,就像是 txt 副檔名一樣,如果格式不正確會難以解析。
- user-agent:發出請求的瀏覽器資訊,後端也常會透過此資訊來判斷瀏覽器的裝置為何(行動版、桌面版),藉此給予不同的回應。
- cookie:瀏覽器紀錄的資訊,大多用來儲存具有時限的個人資訊,如驗證資料、網頁瀏覽紀錄。
- authorization:驗證資訊,當發出的請求需要另外進行驗證(如後端資料)時,則可透過此參數夾帶驗證資料。
- Content-Type:用於通知客戶端實際返回的內容的類型;如果表單method屬性的值為
POST
,則Content-Type可能的值有以下三種:- application/x-www-form-urlencoded:未指定屬性時的默認值。
- multipart/form-data:當表單包含
type=file
的<input>
元素時使用此值。 - text/plain:出現於 HTML5,用於調試。
詳細文檔:https://developer.mozilla.org/zh-TW/docs/Web/HTTP/Methods/POST
Form Data
送出的表單資料。
Flutter HTTP POST Form Data實作
最近在學習抓取農委會發布的農業新聞資料,順便把我自己的作法分享出來。
Form Data各個參數的涵義:
- keyword 關鍵字搜尋
- division_lv1 發布機關
- year 起始發布年
- month 起始發布月
- end_year 結束發布年
- end_month 結束發布月
- search_Submit 查詢
- is_search 是否送出查詢
division_lv1
這個參數比較特別,他的發布機關 value 並不是純數字代號、中文名稱,而是使用英文縮寫。
開啟開發工具觀察它的下拉選單 html 就能獲取所有機關的 value 。
發起請求
建立Map來存 Headers 與 Form Data,由於我最後想實現的效果是透過下拉選單切換發布機關來獲取不同機關所發布的新聞內容,因此我的 division_lv1
必須使用變數(selectDivision)。
String selectDivision = '*'; var response; Future<http.Response> requestData(String division) async{ selectDivision = division; var url_post = 'https://www.coa.gov.tw/theme_list.php?theme=news&sub_theme=agri'; Map<String, String> headersMap = { "content-type":"application/x-www-form-urlencoded", "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36", }; Map<String, String> formdata = { "division_lv1":selectDivision, "year":"110", "month":"1", "end_year":"110", "end_month":"5", "search_Submit":"查詢", "is_search":"y", }; return response = await http.post( url_post, headers: headersMap, body: formdata, encoding: Utf8Codec() //注:Utf8Codec()需要 import 'dart:convert'; ); }
獲取新聞內容
HTML Selector 和 Matches 的部分就不多提了
var title = new List(); var href = new List(); var date = new List(); var division = new List(); var date_reg = new RegExp(r'..-..-..'); //日期正則 var division_reg = new RegExp(r'(<td align="center">)(.+)(</td>)'); //發布機關正則 void getNews() async { var data = await requestData(selectDivision); setState(() { response = data; }); if (response.statusCode == 200) { title.clear(); href.clear(); date.clear(); division.clear(); var document = parse(response.body); //標題和連結 var titleElement = document.querySelectorAll('.main-c9-index'); for (final word in titleElement) { title.add(word.attributes['title']); href.add('https://www.coa.gov.tw/' + word.attributes['href']); } //日期和發布機關 var tableElement = document.querySelectorAll('.table > tbody > tr'); for (final word in tableElement) { Iterable date_allMatches = date_reg.allMatches(word.innerHtml); //抓取符合日期正則的資料 Iterable division_allMatches = division_reg.allMatches(word.innerHtml); //抓取符合發布機關正則的資料 //日期 date_allMatches.forEach((match) { date.add(word.innerHtml.substring(match.start, match.end)); }); //發布機關 division_allMatches.forEach((match) { division.add(match.group(2)); }); } setState(() { title = title; href = href; date = date; division = division; }); } }
初始化呼叫 getNews()
@override void initState() { getNews(); super.initState(); }
新聞列表
關於listview.separated 可以查看官方文檔:https://api.flutter.dev/flutter/widgets/ListView/ListView.separated.html
Widget newsList(){ return ListView.separated( itemCount: title.length, itemBuilder: (context, index) { return new ListTile( leading: new Icon(Icons.new_releases), title: new Text("${title[index]}"), subtitle: new Text("${device[index]} ${day[index]}"), contentPadding: EdgeInsets.symmetric(horizontal: 20.0), enabled: true, onTap: () => { //頁面傳值 Navigator.of(context) .push(new MaterialPageRoute(builder: (_) { return new newsContent( title: "${title[index]}", url: "${href[index]}", day: "${date[index]}", division: "${division[index]}", ); })), }, ); }, separatorBuilder: (BuildContext context, int index) { return Divider(); }, ); }
選取發布機關
既然要能夠選取,那就必須要建立一個下拉選單。
建立 ListItem
物件
class ListItem { String id; String name; ListItem(this.id, this.name); }
使用 ListItem物件 創建 _dropdownItems
ItemList 將所有機關及代號存入
List<ListItem> _dropdownItems = [ ListItem('*', "所有機關"), ListItem('coa', "農委會"), ListItem('afa', "農糧署"), ListItem('boaf', "農業金融局"), ListItem('forest', "林務局"), ListItem('swcb', "水土保持局"), ListItem('irrigation2', "農田水利署"), ListItem('tari', "農業試驗所"), ListItem('tfri', "林業試驗所"), ListItem('tactri', "農業藥物毒物試驗所"), ListItem('tydares', "桃園區農業改良場"), ListItem('mdares', "苗栗區農業改良場"), ListItem('tdares', "臺中區農業改良場"), ListItem('tndais', "臺南區農業改良場"), ListItem('kdais', "高雄區農業改良場"), ListItem('hdais', "花蓮區農業改良場"), ListItem('ttdares', "臺東區農業改良場"), ListItem('teais', "茶葉改良場"), ListItem('tss', "種苗改良繁殖場"), ListItem('pabp', "屏東農業生物技術園區"), ];
然後創建 DropdownMenuItem List 的 buildDropDownMenuItems
函數
List<DropdownMenuItem<ListItem>> _dropdownMenuItems; ListItem _selectedItem; List<DropdownMenuItem<ListItem>> buildDropDownMenuItems(List listItems) { List<DropdownMenuItem<ListItem>> items = List(); for (ListItem listItem in listItems) { items.add( DropdownMenuItem( child: SizedBox( width: 200, child: Text( listItem.name, textAlign: TextAlign.center, ), ), value: listItem, ), ); } return items; }
初始化宣告 _dropdownMenuItems
與 _selectedItem
- _dropdownMenuItems:使用 buildDropDownMenuItems 函數建立下拉列表的所有選項。
- _selectedItem:當使用者使用 DropdownButton 改變選擇的值時,會將改變的值傳給 _selectedItem,預設為第一個選選項的value。
@override void initState() { getNews(); _dropdownMenuItems = buildDropDownMenuItems(_dropdownItems); _selectedItem = _dropdownMenuItems[0].value; super.initState(); }
建立 DropdownButton
DropdownButton<ListItem>( style: TextStyle(fontSize: 17), value: _selectedItem, items: _dropdownMenuItems, onChanged: (value) { setState((){ _selectedItem = value; selectDivision = value.id; getNews(); }); } ),
大功告成!
- Expo 使用 EAS build 時遇到的坑及解決方式 - 2022 年 5 月 19 日
- Typora + PicGo-Core 使用 Github 作為筆記免費圖床的詳細圖文教學 - 2022 年 5 月 12 日
- [JS學習筆記] JS 中的傳值、傳址與淺拷貝、深拷貝 - 2022 年 5 月 8 日