在現代的網頁開發中,跨來源資源共享(Cross-Origin Resource Sharing,簡稱 CORS)是一個經常被討論的議題。CORS 是一種瀏覽器的安全機制,用來防止來自不同來源的請求存取用戶敏感資訊。然而,這項安全性設計在實際應用中也為開發者帶來了不少挑戰,特別是在需要跨域整合第三方服務或 API 時。
kintone 提供了豐富的 API 功能,讓開發者能夠快速構建客製化解決方案。但在與外部服務整合時,CORS 問題往往成為一大障礙。為了解決這個問題,kintone 提供了 kintone.proxy()
的方法,讓開發者能夠透過 kintone 平台的代理功能來處理跨域請求,進一步提高開發靈活性和安全性。
本文將說明什麼是 CORS,以及如何透過 kintone.proxy()
解決跨域問題。
CORS 問題的核心在於瀏覽器的同源政策(Same-Origin Policy)。這是一種安全機制,用來限制網頁從一個來源(origin)請求另一個來源的資源,避免惡意網站竊取敏感資訊。
「同源」的定義是 URL 的協議、域名和埠號都必須一致。當一個網頁的來源不同於目標伺服器的來源時,瀏覽器會自動阻止請求,除非目標伺服器明確授權該跨域請求。
在 kintone 平台上撰寫客製化代碼時,這些代碼執行於使用者的瀏覽器前端。當我們需要從前端代碼直接發送 API 請求到第三方伺服器(如外部服務的 REST API)時,這些請求的來源是 kintone 的域名(例如 https://example.kintone.com
),而目標伺服器的域名可能是另一個完全不同的來源(例如 https://api.example.com
)。
由於這兩者的來源不同,瀏覽器會認為這是一個跨域請求,並且根據同源政策阻止請求的執行,從而產生 CORS 問題。如果目標伺服器未配置適當的 CORS 標頭來允許跨域請求,開發者在 kintone 的前端代碼中無法正常與該 API 進行通信。
舉例說明:
POST
請求到 https://api.analytics.com/upload
。https://example.kintone.com
和 https://api.analytics.com
是不同的來源,瀏覽器會阻止該請求,並在開發者工具中顯示 CORS 錯誤訊息。在 kintone 的客製化代碼中發送 API 請求時,請求是直接從使用者的瀏覽器前端發出的,因此自然會受到瀏覽器同源政策的影響。解決這個問題需要伺服器正確設置 CORS 標頭,或者使用 kintone 提供的 kintone.proxy()
功能來代理請求,繞過這些限制。
kintone JavaScript API 中提供了 kintone.proxy()
的方法,透過 kintone 的代理伺服器發送請求至外部伺服器,來避開 CORS 問題。
kintone.proxy(url, method, headers, data, successCallback, failureCallback)
引數 | 型別 | 必須 | 說明 |
---|---|---|---|
url | 字串 | 必須 | 欲執行之外部 API Url |
method | 字串 | 必須 | 執行 API 使用之 http 方法,可指定以下值:GET, POST, PUT, DELETE |
headers | 物件 | 必須 | 欲攜帶之請求標頭 (headers),不指定內容時請傳入空物件 {} |
data | 物件 | 必須 | 欲攜帶之請求主體 (body),不指定內容時請傳入空物件 {} |
successCallback | 函式 | 可省略 | 當請求完成時執行的回呼函數 |
failureCallback | 函式 | 可省略 | 當請求失敗時執行的回呼函數 |
successCallback 的引數會傳遞以下資訊:
省略 successCallback 與 failureCallback 時,將返回一個 Promise 物件,若請求完成,該物件會解決(resolve)為一個包含回應主體、狀態碼和回應標頭的陣列;若請求失敗,該物件會以代理 API 的回應主體(字串)作為拒絕理由而被拒絕(rejected)。
kintone.proxy(
'https://api.example.com',
'GET',
{},
{},
(body, status, headers) => {
// success
console.log(status, body, headers)
},
(error) => {
// error
console.log(error)
}
)
try {
const [body, status, headers] = await kintone.proxy(
'https://api.example.com',
'GET',
{},
{}
)
// success
console.log(status, body, headers)
} catch (error) {
// error
console.log(error)
}
💡 推薦使用 async/await 寫法,程式碼易讀性較高,也方便處理錯誤。
大部分 API 回傳的資料格式多為 JSON 物件,但在 kintone.proxy
中,回應主體會被轉換為字串,如果要進行物件的操作,就必須先透過 JSON.parse()
方法將其轉換回物件。
try {
const [body, status, headers] = await kintone.proxy(
'https://api.example.com',
'GET',
{},
{}
)
// success
const dataObject = JSON.parse(body)
console.log(dataObject)
} catch (error) {
// error
console.log(error)
}
更多的細節以及使用上的限制請參閱 kintone.proxy 官方文件(英文版)
上述提到的 kintone.proxy()
方法僅能使用於文字資料的處理,如果要夠過 kintone 代理傳送檔案至外部時,則需要使用 kintone.proxy.upload()
方式。
kintone.proxy.upload(url, method, headers, data, successCallback, failureCallback)
引數 | 型別 | 必須 | 說明 |
---|---|---|---|
url | 字串 | 必須 | 請求之 Url |
method | 字串 | 必須 | http 方法,可指定以下值:POST, PUT |
headers | 物件 | 必須 | 請求標頭 (headers),不指定內容時請傳入空物件 {} |
data | 物件 | 必須 | 請求主體 (body),規定格式與限制請見下方說明 |
successCallback | 函式 | 可省略 | 當請求完成時執行的回呼函數 |
failureCallback | 函式 | 可省略 | 當請求失敗時執行的回呼函數 |
successCallback
與 failureCallback
的運作方式與 kintone.proxy()
相同。
data
的格式與限制data
物件必須為以下格式:
{
format: 'RAW', // 上傳之格式,只能指定為 'RAW'
value: // 欲上傳之檔案
}
'RAW'
🔗 kintone.proxy.upload 官方文件(英文版)
這裡演示一個將 kintone 附件欄位內的圖檔上傳至 Imgur,並且將圖片網址更新回記錄欄位中的簡單範例。效果如下圖:
首先建立一個 kintone 應用程式,加入以下欄位:
先建立一個客製化上傳按鈕,放到空白欄位中。
(() => {
'use strict'
const SPACE_ELEMENT_ID = '<Space Element ID>'
const IMGUR_CLIENT_ID = '<Your Client ID>'
kintone.events.on('app.record.detail.show', event => {
// 將客製化按鈕插入至空白欄
const spaceEl = kintone.app.record.getSpaceElement(SPACE_ELEMENT_ID)
const uploadButton = createButton('Upload')
spaceEl.appendChild(uploadButton)
return event
})
function createButton(name) {
const button = document.createElement('button')
button.className = 'kintone-btn-primary'
button.textContent = name
return button
}
})()
接著附加一個點擊事件到按鈕上,讓使用者點擊該按鈕就可以觸發上傳功能。
在上傳到外部之前,首先要透過 kintone REST API 取得附件檔案。從 event.record
中可以取得附件欄位中的值,其值為一陣列,裡面包含代表上傳在欄位中的檔案物件,但它並不是真正的檔案,只是檔案的資訊,必須要拿 fileKey
的值透過 REST API 請求才能拿到檔案。
uploadButton.addEventListener('click', async () => {
try {
// 取得附件檔案
const file = event.record.file.value[0] // 取得附件欄位中的第一個檔案
const downloadRes = await fetch(
`/k/v1/file.json?fileKey=${file.fileKey}`,
{
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
}
)
const blob = await downloadRes.blob()
} catch (error) {
console.error(error)
window.alert('發生錯誤')
}
})
拿到圖檔的二進制資料後,就可以按照 kintone.proxy.upload
的寫法,將檔案上傳到外部伺服器。
const data = {
format: 'RAW',
value: blob
}
const [body, status, headers] = await kintone.proxy.upload(
'https://api.imgur.com/3/image',
'POST',
{
'Authorization': `Client-ID ${IMGUR_CLIENT_ID}`
},
data
)
const resp = {
body: JSON.parse(body),
status,
headers
}
請求成功後,可以從 body.data.link
取得圖片的網址。由於透過 kintone proxy 回傳的 response body 會被轉為字串,所以要先 parse 成物件,後續才方便取得資料。實作時可以先將 resp.body
印出來觀察回傳的資料結構。
最後再用 API 將網址更新回這筆記錄當中:
await kintone.api(
kintone.api.url('/k/v1/record.json'),
'PUT',
{
app: kintone.app.getId(),
id: event.record.$id.value,
record: {
imgur_link: { value: resp.body.data.link }
}
}
)
window.alert('上傳成功!')
window.location.reload()
使用 kintone proxy 是一個可以簡單解決 CORS 問題的方式,但因為它本身的功能有一些限制,操作起來會有一些不直觀、不方便的地方。如果真的要處理比較複雜的請求,可能直接架設一個代理伺服器會更加方便。