request(options)
描述
發送 XHR (又稱 AJAX) 請求,並回傳 promise。
m.request({
method: 'PUT',
url: '/api/v1/users/:id',
params: { id: 1 },
body: { name: 'test' },
}).then(function (result) {
console.log(result);
});
簽名
promise = m.request(options)
參數 | 類型 | 是否必要 | 描述 |
---|---|---|---|
options | Object | 是 | 要傳入請求的選項。 |
options.method | String | 否 | 要使用的 HTTP 方法。此值應為以下任一:GET 、POST 、PUT 、PATCH 、DELETE 、HEAD 或 OPTIONS 。預設值為 GET 。 |
options.url | String | 是 | 要將請求傳送到的 路徑名稱,並可使用 options.params 中的值進行插值。 |
options.params | Object | 否 | 要插值到 URL 中和/或序列化到查詢字串中的資料。 |
options.body | Object | 否 | 要序列化至請求 body 中的資料(適用於其他類型的請求)。 |
options.async | Boolean | 否 | 請求是否應為非同步處理。預設值為 true 。 |
options.user | String | 否 | 用於 HTTP 授權的使用者名稱。預設值為 undefined 。 |
options.password | String | 否 | 用於 HTTP 授權的密碼。預設值為 undefined 。提供此選項是為了與 XMLHttpRequest 相容,但應避免使用,因為它會以純文字形式在網路上傳輸密碼。 |
options.withCredentials | Boolean | 否 | 是否將 cookies 發送到第三方網域。預設值為 false 。 |
options.timeout | Number | 否 | 請求在自動 終止 前可花費的毫秒數。預設值為 undefined 。 |
options.responseType | String | 否 | 預期的回應類型。如果定義了 extract ,則預設值為 "" ,如果缺少則預設值為 "json" 。如果 responseType: "json" ,它會在內部執行 JSON.parse(responseText) 。 |
options.config | xhr = Function(xhr) | 否 | 暴露底層的 XMLHttpRequest 物件,用於底層配置和可選的替換(透過回傳新的 XHR)。 |
options.headers | Object | 否 | 在傳送請求之前附加到請求的標頭(在 options.config 之前應用)。 |
options.type | any = Function(any) | 否 | 要套用於回應中每個物件的建構函式。預設值為 恆等函數。 |
options.serialize | string = Function(any) | 否 | 要套用於 body 的序列化方法。預設值為 JSON.stringify ,或者如果 options.body 是 FormData 或 URLSearchParams 的實例,則預設值為 恆等函數(亦即 function(value) {return value} )。 |
options.deserialize | any = Function(any) | 否 | 要套用於 xhr.response 或標準化的 xhr.responseText 的反序列化方法。預設值為 恆等函數。如果定義了 extract ,則將跳過 deserialize 。 |
options.extract | any = Function(xhr, options) | 否 | 一個 Hook,用於指定應如何讀取 XMLHttpRequest 回應。適用於處理回應資料、讀取標頭和 Cookies。預設情況下,這是一個回傳 options.deserialize(parsedResponse) 的函式,當伺服器回應狀態碼指示錯誤或回應在語法上無效時,會拋出例外。如果提供了自訂 extract 回呼,則 xhr 參數是用於請求的 XMLHttpRequest 實例,而 options 是傳遞給 m.request 呼叫的物件。此外,將跳過 deserialize ,並且從 extract 回呼回傳的值將按原樣保留,直到 Promise 解決。 |
options.background | Boolean | 否 | 如果為 false ,則在請求完成後重新繪製已掛載的元件。如果為 true ,則不重新繪製。預設值為 false 。 |
returns | Promise | 一個 Promise,在透過 extract 、deserialize 和 type 方法傳輸後,解析為回應資料。如果回應狀態碼指示錯誤,則 Promise 會被拒絕,但可以透過設定 extract 選項來避免這種情況。 |
promise = m.request(url, options)
參數 | 類型 | 是否必要 | 描述 |
---|---|---|---|
url | String | 是 | 要將請求傳送到的 路徑名稱。如果存在 options.url ,則會覆蓋此值。 |
options | Object | 否 | 要傳入的請求選項。 |
returns | Promise | 一個 Promise,在透過 extract 、deserialize 和 type 方法傳輸後,解析為回應資料 |
第二種形式主要等效於 m.request(Object.assign({url: url}, options))
,只是它在內部不依賴於 ES6 全域 Object.assign
。
運作方式
m.request
實用工具是 XMLHttpRequest
的一個輕量級封裝,允許向遠端伺服器發送 HTTP 請求,以便從資料庫儲存和/或檢索資料。
m.request({
method: 'GET',
url: '/api/v1/users',
}).then(function (users) {
console.log(users);
});
呼叫 m.request
會回傳一個 promise,並在其 promise 鏈完成後觸發重新繪製。
預設情況下,m.request
假設回應為 JSON 格式,並將其剖析為 JavaScript 物件(或陣列)。
如果 HTTP 回應狀態碼指示錯誤,則返回的 Promise 將被拒絕。提供 extract 回呼將防止 promise 拒絕。
典型用法
以下是一個說明性範例,展示了一個使用 m.request
從伺服器檢索一些資料的元件。
var Data = {
todos: {
list: [],
fetch: function () {
m.request({
method: 'GET',
url: '/api/v1/todos',
}).then(function (items) {
Data.todos.list = items;
});
},
},
};
var Todos = {
oninit: Data.todos.fetch,
view: function (vnode) {
return Data.todos.list.map(function (item) {
return m('div', item.title);
});
},
};
m.route(document.body, '/', {
'/': Todos,
});
假設向伺服器 URL /api/items
發出請求會回傳 JSON 格式的物件陣列。
當在底部呼叫 m.route
時,會初始化 Todos
元件。呼叫 oninit
,它會呼叫 m.request
。這會從伺服器非同步檢索物件陣列。「非同步」表示 JavaScript 在等待伺服器的回應時會繼續執行其他程式碼。在這種情況下,這表示 fetch
返回,並且使用原始空陣列作為 Data.todos.list
來呈現元件。一旦對伺服器的請求完成,物件陣列 items
就會被指派給 Data.todos.list
,並且元件會再次呈現,產生一個包含每個 todo
標題的 <div>
清單。
錯誤處理
當非 file:
請求回傳的狀態不是 2xx 或 304 時,它會拒絕並顯示錯誤。此錯誤是一個普通的 Error 實例,並具有一些特殊屬性。
error.message
設定為原始回應文字。error.code
設定為狀態碼本身。error.response
設定為已剖析的回應,使用options.extract
和options.deserialize
,就像處理正常回應一樣。
這在許多情況下都很有用,因為錯誤本身是可以處理的。如果您想偵測會話是否已過期 - 您可以執行 if (error.code === 401) return promptForAuth().then(retry)
。如果您遇到 API 的節流機制,並且它返回了一個帶有 "timeout": 1000
的錯誤,您可以執行 setTimeout(retry, error.response.timeout)
。
載入圖示和錯誤訊息
以下是上述範例的擴充版本,它實作了載入指示器和錯誤訊息:
var Data = {
todos: {
list: null,
error: '',
fetch: function () {
m.request({
method: 'GET',
url: '/api/v1/todos',
})
.then(function (items) {
Data.todos.list = items;
})
.catch(function (e) {
Data.todos.error = e.message;
});
},
},
};
var Todos = {
oninit: Data.todos.fetch,
view: function (vnode) {
return Data.todos.error
? [m('.error', Data.todos.error)]
: Data.todos.list
? [
Data.todos.list.map(function (item) {
return m('div', item.title);
}),
]
: m('.loading-icon');
},
};
m.route(document.body, '/', {
'/': Todos,
});
此範例與之前的範例之間存在一些差異。在這裡,Data.todos.list
在一開始是 null
。此外,還有一個額外的欄位 error
用於保存錯誤訊息,並且修改了 Todos
元件的視圖,以便在存在錯誤訊息時顯示錯誤訊息,如果 Data.todos.list
不是陣列,則顯示載入圖示。
動態 URL
請求 URL 可能包含插值:
m.request({
method: 'GET',
url: '/api/v1/users/:id',
params: { id: 123 },
}).then(function (user) {
console.log(user.id); // logs 123
});
在上面的程式碼中,:id
會被 params
物件中的資料填入,請求會變成 GET /api/v1/users/123
。
如果 params
屬性中不存在相符的資料,則會忽略插值。
m.request({
method: 'GET',
url: '/api/v1/users/foo:bar',
params: { id: 123 },
});
在上面的程式碼中,請求會變成 GET /api/v1/users/foo:bar?id=123
中止請求
有時候,需要中止請求。例如,在自動完成/預先輸入元件中,您希望確保只有最後一個請求完成,因為通常自動完成器會在使用者輸入時觸發多個請求,並且由於網路的不可預測性,HTTP 請求可能會非順序完成。如果另一個請求在最後一個觸發的請求之後完成,則元件將顯示比最後一個觸發的請求最後完成時相關性較低(或可能錯誤)的資料。
m.request()
透過 options.config
參數暴露其底層的 XMLHttpRequest
物件,這允許您保存對該物件的參考,並在需要時呼叫其 abort
方法:
var searchXHR = null;
function search() {
abortPreviousSearch();
m.request({
method: 'GET',
url: '/api/v1/users',
params: { search: query },
config: function (xhr) {
searchXHR = xhr;
},
});
}
function abortPreviousSearch() {
if (searchXHR !== null) searchXHR.abort();
searchXHR = null;
}
檔案上傳
若要上傳檔案,首先您需要取得 File
物件的參考。最簡單的方法是從 <input type="file">
取得。
m.render(document.body, [m('input[type=file]', { onchange: upload })]);
function upload(e) {
var file = e.target.files[0];
}
上面的程式碼片段會呈現一個檔案輸入。如果使用者選擇一個檔案,則會觸發 onchange
事件,該事件會呼叫 upload
函數。e.target.files
是 File
物件的清單。
接下來,您需要建立一個 FormData
物件來建立一個 multipart request,這是一個特殊格式的 HTTP 請求,能夠在請求 body 中傳送檔案資料。
function upload(e) {
var file = e.target.files[0];
var body = new FormData();
body.append('myfile', file);
}
接下來,您需要呼叫 m.request
並將 options.method
設定為使用 body 的 HTTP 方法(例如 POST
、PUT
、PATCH
),並使用 FormData
物件作為 options.body
。
function upload(e) {
var file = e.target.files[0];
var body = new FormData();
body.append('myfile', file);
m.request({
method: 'POST',
url: '/api/v1/upload',
body: body,
});
}
假設伺服器已設定為接受 multipart 請求,則檔案資訊將與 myfile
鍵相關聯。
多個檔案上傳
可以在一個請求中上傳多個檔案。這樣做會使批次上傳具有原子性(atomic),即如果在上傳期間發生錯誤,則不會處理任何檔案,因此無法僅儲存部分檔案。如果您希望在網路發生故障時儲存盡可能多的檔案,則應考慮在單獨的請求中上傳每個檔案。
若要上傳多個檔案,只需將它們全部附加到 FormData
物件。使用檔案輸入時,您可以透過將 multiple
屬性新增到輸入來取得檔案清單:
m.render(document.body, [
m('input[type=file][multiple]', { onchange: upload }),
]);
function upload(e) {
var files = e.target.files;
var body = new FormData();
for (var i = 0; i < files.length; i++) {
body.append('file' + i, files[i]);
}
m.request({
method: 'POST',
url: '/api/v1/upload',
body: body,
});
}
監控進度
有時,如果請求本質上很慢(例如,大型檔案上傳),則需要向使用者顯示進度指示器,以表示應用程式仍在運作。
m.request()
透過 options.config
參數暴露其底層的 XMLHttpRequest
物件,這允許您將事件接聽程式附加到 XMLHttpRequest 物件:
var progress = 0;
m.mount(document.body, {
view: function () {
return [
m('input[type=file]', { onchange: upload }),
progress + '% completed',
];
},
});
function upload(e) {
var file = e.target.files[0];
var body = new FormData();
body.append('myfile', file);
m.request({
method: 'POST',
url: '/api/v1/upload',
body: body,
config: function (xhr) {
xhr.upload.addEventListener('progress', function (e) {
progress = e.loaded / e.total;
m.redraw(); // 告訴 Mithril.js 資料已變更,需要重新呈現
});
},
});
}
上面的範例中,會呈現一個檔案輸入。如果使用者選擇一個檔案,則會啟動上傳,並且在 config
回呼中,會註冊一個 progress
事件處理常式。每當 XMLHttpRequest 中有進度更新時,就會觸發此事件處理常式。由於 XMLHttpRequest 的進度事件不是由 Mithril.js 的虛擬 DOM 引擎直接處理的,因此必須呼叫 m.redraw()
以向 Mithril.js 發出訊號,表示資料已變更,需要重新繪製。
將回應轉換為類型
根據整體應用程式架構,可能需要將請求的回應資料轉換為特定的類別或類型(例如,統一剖析日期欄位或具有輔助方法)。
您可以將建構函式作為 options.type
參數傳遞,Mithril.js 將為 HTTP 回應中的每個物件實例化它。
function User(data) {
this.name = data.firstName + ' ' + data.lastName;
}
m.request({
method: 'GET',
url: '/api/v1/users',
type: User,
}).then(function (users) {
console.log(users[0].name); // logs a name
});
在上面的範例中,假設 /api/v1/users
返回一個物件陣列,則將為陣列中的每個物件實例化 User
建構函式(即,呼叫為 new User(data)
)。如果回應返回單個物件,則該物件將用作 body
參數。
非 JSON 回應
有時,伺服器端點不會回傳 JSON 回應:例如,您可能正在請求 HTML 檔案、SVG 檔案或 CSV 檔案。預設情況下,Mithril.js 會嘗試將回應剖析為 JSON。若要覆蓋該行為,請定義自訂 options.deserialize
函數:
m.request({
method: 'GET',
url: '/files/icon.svg',
deserialize: function (value) {
return value;
},
}).then(function (svg) {
m.render(document.body, m.trust(svg));
});
在上面的範例中,請求會檢索 SVG 檔案,不執行任何剖析(因為 deserialize
僅按原樣返回該值),然後將 SVG 字串呈現為受信任的 HTML。
當然,deserialize
函數可能會更複雜:
m.request({
method: 'GET',
url: '/files/data.csv',
deserialize: parseCSV,
}).then(function (data) {
console.log(data);
});
function parseCSV(data) {
// 為了保持範例簡單,採用簡單的實作方式
return data.split('\n').map(function (row) {
return row.split(',');
});
}
忽略上述 parseCSV 函數沒有處理正確的 CSV 剖析器會處理的許多情況,上面的程式碼會記錄一個陣列的陣列。
自訂標頭也可能在這方面有所幫助。例如,如果您正在請求 SVG,您可能需要相應地設定內容類型。若要覆寫預設的 JSON 請求類型,請將 options.headers
設定為與請求標頭名稱和值對應的鍵值對物件。
m.request({
method: 'GET',
url: '/files/image.svg',
headers: {
'Content-Type': 'image/svg+xml; charset=utf-8',
Accept: 'image/svg, text/*',
},
deserialize: function (value) {
return value;
},
});
檢索回應詳細資訊
預設情況下,Mithril.js 會嘗試將 xhr.responseText
剖析為 JSON,並回傳已剖析的物件。更詳細地檢查伺服器回應並手動處理它可能很有用。這可以透過傳遞自訂 options.extract
函數來完成:
m.request({
method: 'GET',
url: '/api/v1/users',
extract: function (xhr) {
return { status: xhr.status, body: xhr.responseText };
},
}).then(function (response) {
console.log(response.status, response.body);
});
options.extract
的參數是 XMLHttpRequest 物件,一旦其操作完成,但在將其傳遞到返回的 promise 鏈之前,因此如果處理擲回例外,promise 仍可能最終處於拒絕狀態。
向 IP 位址發出提取
由於 URL 中偵測參數的方式(非常簡單),IPv6 位址區段會被誤認為是路徑參數插值,並且由於路徑參數需要一些東西來分隔它們才能正確插值,因此會拋出例外。
// 這不起作用
m.request('http://[2001:db8::990a:cd27:4d9e:79]:8080/some/path', {
// ...
});
若要解決此問題,您應該將 IPv6 位址 + 連接埠對作為參數傳遞。
m.request('http://:host/some/path', {
params: { host: '[2001:db8::990a:cd27:4d9e:79]:8080' },
// ...
});
這不是 IPv4 位址的問題,您可以正常使用它們。
// 這將按照您的預期運作
m.request('http://192.0.2.15:8080/some/path', {
// ...
});
為什麼選擇 JSON 而不是 HTML
許多伺服器端框架提供一個視圖引擎,該引擎在提供 HTML 之前(在頁面載入時或透過 AJAX)將資料庫資料插入到範本中,然後使用 jQuery 來處理使用者互動。
相比之下,Mithril.js 是一個專為胖客戶端應用程式設計的框架,這些應用程式通常會分別下載範本和資料,並透過 JavaScript 在瀏覽器中將它們組合在一起。在瀏覽器中進行範本的繁重工作可以帶來一些好處,例如透過釋放伺服器資源來降低營運成本。將範本與資料分離還可以更有效地快取範本程式碼,並在不同類型的客戶端(例如,桌面、行動裝置)之間實現更好的程式碼可重用性。另一個好處是 Mithril.js 啟用了 保留模式 UI 開發範例,這大大簡化了複雜使用者互動的開發和維護。
預設情況下,m.request
期望回應資料為 JSON 格式。在典型的 Mithril.js 應用程式中,該 JSON 資料通常由視圖使用。
您應該避免嘗試使用 Mithril 呈現伺服器產生的動態 HTML。如果您有一個現有的應用程式使用伺服器端範本系統,並且您希望重新架構它,請首先確定從一開始就努力是否可行。從胖伺服器架構遷移到胖客戶端架構通常是一項相當大的工作,並且涉及將邏輯從範本重構為邏輯資料服務(以及隨之而來的測試)。
資料服務可以透過許多不同的方式組織,具體取決於應用程式的性質。RESTful 架構在 API 提供者中很受歡迎,並且在存在大量高度事務性工作流程的情況下,通常需要 面向服務的架構。
為什麼選擇 XHR 而不是 fetch
fetch()
是一個較新的 Web API,用於從伺服器提取資源,類似於 XMLHttpRequest
。
Mithril.js 的 m.request
使用 XMLHttpRequest
而不是 fetch()
的原因有很多:
fetch
尚未完全標準化,並且可能會受到規格變更的影響。- 可以在解析之前中止
XMLHttpRequest
呼叫(例如,為了避免即時搜尋 UI 中的競爭條件)。 XMLHttpRequest
為長時間執行的請求(例如,檔案上傳)提供進度接聽程式的 hook。- 所有瀏覽器都支援
XMLHttpRequest
,而 Internet Explorer 和較舊的 Android(5.0 Lollipop 之前)不支援fetch()
。
目前,由於缺乏瀏覽器支援,fetch()
通常需要一個 polyfill,它未壓縮時超過 11kb - 幾乎是 Mithril.js 的 XHR 模組的三倍大。
儘管 Mithril.js 的 XHR 模組小得多,但它支援許多重要且不易實作的功能,例如 URL 插值 和查詢字串序列化,此外它還能夠無縫整合到 Mithril.js 的自動重繪子系統。fetch
polyfill 不支援任何這些功能,並且需要額外的程式庫和模板程式碼才能達到相同的功能層級。
此外,Mithril.js 的 XHR 模組針對基於 JSON 的端點進行了最佳化,並使最常見的情況簡潔明瞭 - 即 m.request(url)
- 而 fetch
需要一個額外的明確步驟才能將回應資料剖析為 JSON:fetch(url).then(function(response) {return response.json()})
在一些不常見的情況下,fetch()
API 確實比 XMLHttpRequest
具有一些技術優勢:
- 它提供了一個串流處理 API(在「視訊串流」意義上,而不是在反應式程式設計意義上),這可以提高非常大的回應的延遲和記憶體消耗(以程式碼複雜性為代價)。
- 它整合到 Service Worker API,這提供了對網路請求發生方式和時間的額外控制層。此 API 還允許存取推播通知和背景同步功能。
在典型情況下,串流處理不會提供明顯的效能優勢,因為通常不建議從一開始就下載數 MB 的資料。此外,如果重複使用小緩衝區導致過多的瀏覽器重繪,則重複使用小緩衝區所獲得的記憶體收益可能會被抵消或無效。由於這些原因,僅建議對資源密集型應用程式選擇 fetch()
串流處理而不是 m.request
。
避免反模式
Promise 不是回應資料
m.request
方法回傳一個 Promise
,而不是回應資料本身。它無法直接回傳該資料,因為 HTTP 請求可能需要很長時間才能完成(由於網路延遲),並且如果 JavaScript 等待它,它將凍結應用程式,直到資料可用。
// 避免
var users = m.request('/api/v1/users');
console.log('list of users:', users);
// `users` 不是使用者清單,它是一個 promise
// 偏好
m.request('/api/v1/users').then(function (users) {
console.log('list of users:', users);
});