本篇文章是我在六角學院地下城活動5F關卡的實作紀錄,會描述如何架設簡單的Proxy Server去跟政府的公開API拉取資料回來給前端Vue.js介面做互動。
成果可參考這裏(heroku開機要一段時間如果拉不到資料請多重整幾次。)
使用技術及觀念:
- Vue.js (with vue-cli3)
- Node.js (express with express-generator)
- Heroku
- CORS Headers
- Jsonp (僅技術講解)
Outline
- 同源政策
- Node.js — express generator
- HTTP 的 CORS Request
- 修改 Response Header 允許跨域
- 部署到Heroku
- Vue-Cli dev 模式下的proxy
- 什麼是Jsonp
- 寫在最後
同源政策
一般開發網站的時候,如果是採前後端分離的架構,就會遇到跨域問題。就像今天這個專案,我的目標是抓取台灣空氣品質的開放API,回來做成介面給使用者搜尋,但是在利用axios直接打API的時候,遇到了下面的狀況:
這是因為網頁在傳遞資料的時候,不管是透過傳統XMLHttpRequest(常見的ajax方式) 或是Fetch,都會遵循同源政策(Same Origin Policy),「同源」指的是同個域名底下的資源,因為只能存取相同來源的資料,所以那些跨域的請求就會被阻擋掉。
這是瀏覽器的安全機制,當然並非如此一來就無法跨域存取了,我想到的解決有以下兩種:
- Jsonp (Json with padding)
- 使用Proxy 代理伺服器存取該目標API
因為jsonp的相關知識文章應該有不少,且有安全性疑慮(最後面會說明)。本篇會以Node實做Proxy為主來講解。
Node.js — express generator
跟前端有vue-cli 、 react-create-app 等方便又快速的手腳架一樣,許多後端框架也有類似的功能,express-generator就是其中一個。安裝方式非常簡單,如果對接下來流程有任何疑問,可以參考官方文件,有很詳細的使用說明。
在terminal輸入以下指令以安裝
npm install express-generator -g
- 輸入 express [你的專案名稱] 就可以產生出以express為基底的專案架構:
express proxy-server
接下來就可以進入專案去新增我們要的api了。
HTTP 的 CORS Request
就像前面說的,CORS Request並非完全不可行,只是在Server端必須要有一些設定。既然我們不可能修改OpenAPI的Server內容,又因為後端資料交互是不會碰到瀏覽器的(因此就不會因為跨域問題被阻擋),我們就自己架一個Proxy Server來修改Http相關的設定,讓前端可以順利拿到資料。
CORS Request / Response流程
你在 example.com.tw 送出request給某網站的時候,在request body裡面會夾帶一個Origin的header,內容是你網站的Domain名稱:
Origin: http://www.foo.com
而在後端伺服器收到request並且回傳resonse到client的時候,瀏覽器會去看response裡面的header–***「Access-Control-Allow-Origin」***是不是包含剛剛發出request寫的Origin 域名,如果有,資料才會允許被回傳。
先到剛剛創造的express專案底下。
先找到我們的Open API URL,等等會用到:
http://opendata.epa.gov.tw/webapi/api/rest/datastore/355000000I-000259
###安裝所需套件
因為我們要利用後端去跟遠端Open API互動,所以這邊我們使用node-rest-client來達成這部分功能,node-rest-client官方文件:
npm install node-rest-client
新增API路由
在剛剛創造的express專案底下的主程式routes資料夾新增 air.js :
air.js
var express = require('express');
var router = express.Router();
var Client = require('node-rest-client').Client;
var client = new Client();
/* GET users listing. */
router.get('/', function(req, res, next) {
let queries=req.query
console.log('query: ', queries);
let url='http://opendata.epa.gov.tw/webapi/api/rest/datastore/355000000I-000259'
var options = {
host: url,
method: 'GET'
};
client.get(url, function (data, response) {
res.json({...data})
});
});
module.exports = router;
router.get() 是express的router基本寫法,最後將這個包含自定義route的Router實體export出去,之後必須在主程式app.js引入才會有效,可以參考官方文件,可以看到在rest-client抓完資料的callback,我用json方式回傳了Open API 回傳的結果。
...
client.get(url, function (data, response) {
res.json({...data})
});
...
主程式app.js路由引入
接下來在app.js裡面將剛剛新增的router引入:
var airRouter = require('./routes/air');
app.use('/air', airRouter);
記得在var app=express()之後才做app.use()這件事,否則會讀不到app這個變數。
將設定用middle的方式寫入,Express內,如果你直接在app.use()裡面傳入一個
function(req, res, next) {
next()d
}
那是middleware的意思,即所有request都會經過你傳入的function,且直到呼叫next()之後才會繼續執行,我們在這裡面設定Response 的Header,如此一來所有的Request都會得到相同的Header設定值(也可以在單獨某隻router寫入設定,可視需求改變)。
let allowCrossDomain = function(req, res, next) {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
next();
}
app.use(allowCrossDomain)
我在Response Header裡面加入了
- "Access-Control-Allow-Methods"代表允許讓設定的Http Method通過。
- “Access-Control-Allow-Origin”,其值為’*’。
注意,剛剛說Response回傳到Client之前瀏覽器會先檢查這個Header有沒有跟來源Origin的值ㄧ樣,如果是一般前後端分離的sercer這部分的值一般會xxx.com.tw,不會公開。如果你這樣寫,代表大家都可以對你的Server做跨域請求,這邊因為是open API 且是示範性質所以才這樣做。
到這邊其實已經完成了,如果順利的話你的Server應該已經可以拿到遠端API的資料,你可以用
npm run start
// then go to http://localhost:3000/air
在本地架起來用瀏覽器或Postman去測試看看。
部署到Heroku
想要在Heroku 架起一台node Server非常簡單,流程大致上是註冊帳號->安裝heroku cli-> 登入-> 到你的專案輸入幾個指令-> 最後 git push heroku master,他自己就會幫你在heroku提供的空間下npm install安裝好需要的套件並且架起來。
這邊就不多做說明,可參考官方教學,相信我,不會花掉你太多時間的,可先用官方提供的專案做練習。需注意的是因為是免費的服務,所以只要半小時沒在使用,他會暫時關閉你的機器,所以有時候連線會比較慢,可不要以為壞掉了。
可參考我的實作成果。
Vue-Cli dev 模式下的proxy
Vue-cli因為是基於webpack,其實有proxy的設定,你可以參考官方文件。但因為這樣只解決開發環境的問題,所以後來我才打算自己架設Proxy。
另外,前端的部分因為比較基礎,就沒有太詳細解說,你可以看看我的Repo,如果還是有不懂的地方,隨時可以聯絡我。
什麼是JSONP
因為傳統 Ajax無法跨域,早期工程師們找到了替代解決方案—< script> tag 的src引用是可以跨域的,於是利用這個原理的jsonp就這麼誕生了(想想你在引入一些js函式庫如lodash的時候是不是可以用cdn方式直接引入執行?)
Jsonp會做什麼事情
- 創造一個 < script > tag
- 根據你指定的Jsonp位置,設定這個tag的src
- 把這個< script > 加到瀏覽器DOM的head
- 一但資料載入完畢,會將回傳的資料傳給設定的callback並執行這些內容
Jsonp的安全隱患
如果你使用JsonP,那你帶進來的這些內容,跟你自己寫的jsㄧ樣,可以去存取你的DOM等網頁內容,這種情況下如果無法保證server的安全性,將會是潛在的問題。
寫在最後
前端介面成果:http://underground-air.surge.sh/
Proxy Server成果: https://mu-air-proxy.herokuapp.com/air
到這邊,如果你可以很順的理解以上的內容,那你應該對CORS Request流程有一定的了解了。其實因為時間跟篇幅的關係,有些實作部分我並沒有講的很仔細,但是我都有提供相關文件在該段落的附近,因為我覺得寫程式常常需要的是閱讀文件的能力,希望各位可以學著找到自己所需的資源並補上。在「尋找資源-實作-找問題修Bug」的循環過程中其實也在訓練自己的理解跟整合能力,才會讓你慢慢進入學習的正向循環。
當然在這過程中有任何的不懂或是覺得有筆誤的話都可以隨時聯絡我,我很樂意跟你分享我所知道的知識。
參考文章
STOF上關於CORS的討論
CORS PROTOCAL
how jsonp works?
CORS流程圖