即使還沒有讀過我的文章《在處理網路資料的 JavaScript 抽象的重要性》,你也很有可能已經意識到程式碼的可維護性和可擴充套件性很重要,這也是介紹 JavaScript
抽象的目的。
為了更加清楚的說明,我們假設在 JavaScript
中抽象是一個模組。
一個模組的最初實現只是它們漫長(也許是持久的)的生命週期過程的開始。我將一個模組的生命週期分成 3 個重要階段。
- 引入模組。在專案中編寫該模組或複用該模組;
- 調整模組。隨時調整模組;
- 移除模組。
在我先前的文章中,重心放在了第一點上。而在這篇文章中,我將把重點放在第二點上。
模組更改是我經常碰到的一個難題。與引入模組相比,開發者維護和更改模組的方式對保證專案的可維護性和可擴充性是同等重要甚至是更加重要。我看過一個寫得很好、抽象得很好的模組隨著時間推移歷經多次更改後被徹底毀了。我自己也經常是造成那種破壞性更改的其中一個。
當我說破壞性,我指的是對可維護性和可擴充套件性方面的破壞。我也明白,當面臨專案最後交付期限的壓力時,放慢速度以進行更好的修改設計並不是優先選擇。
開發者做出非最優修改的原因可能有很多種,我在這裡想特別強調一個:
以可維護的方式進行修改的技巧
這種方法讓你的修改顯得更專業。
讓我們從一個 API
模組的程式碼示例開始。之所以選擇這個示例,是因為與外部 API
通訊是我在開始專案時定義的最基本的抽象之一。這裡的想法是將所有與 API
相關的配置和設定(如基本 URL
,錯誤處理邏輯等)儲存在這個模組中.
我將編寫一個設定 API.url
、一個私有方法 API._handleError()
和一個公共方法 API.get()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class API { constructor() { this.url = 'http://whatever.api/v1/'; } /** * API 資料獲取的特有方法 * 檢查一個 HTTP 返回的狀態碼是否在成功的範圍內 */ _handleError(_res) { return _res.ok ? _res : Promise.reject(_res.statusText); } /** * 獲取資料 * <a href='http://www.jobbole.com/members/wx1409399284'>@return</a> {Promise} */ get(_endpoint) { return window.fetch(this.url + _endpoint, { method: 'GET' }) .then(this._handleError) .then( res => res.json()) .catch( error => { alert('So sad. There was an error.'); throw new Error(error); }); } }; |
在這個模組中,公共方法 API.get()
返回一個 Promise
。我們使用我們抽象出來的 API
模組,而不是通過 window.fetch()
直接呼叫 Fetch API
。例如,獲取使用者資訊 API.get('user')
或當前天氣預報 API.get('weather')
。實現這個功能的重要意義在於Fetch API與我們的程式碼沒有緊密耦合。
現在,我們面臨一個修改!技術主管要求我們把獲取遠端資料的方式切換到Axios上。我們該如何應對呢?
在我們開始討論方法之前,我們先來總結一下什麼是不變的,什麼是需要修改的:
- 更改:在公共
API.get()
方法中
- 需要修改
axios()
的window.fetch()
呼叫;需要再次返回一個Promise
, 以保持介面的一致, 好在Axios
是基於Promise
的,太棒了! - 伺服器的響應的是
JSON
。通過Fetch API
並通過鏈式呼叫.then( res => res.json())
語句來解析響應的資料。使用Axios
,伺服器響應是在data
屬性中,我們不需要解析它。因此,我們需要將.then
語句改為.then(res => res.data)
。
- 更改:在私有
API._handleError
方法中:
- 在響應物件中缺少
ok
布林標誌,但是,還有statusText
屬性。我們可以通過它來串起來,如果它的值是OK
,那麼一切將沒什麼問題(附註:在Fetch API
中OK
為true
與在Axios
中的statusText
為OK
是不一樣的。但為了便於理解,為了不過於寬泛,不再引入任何高階錯誤處理。)
- 不變之處:
API.url
保持不變,我們會發現錯誤並以愉快的方式提醒他們。
講解完畢!現在讓我們深入應用這些修改的實際方法。
方法一:刪除程式碼。編寫程式碼。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class API { constructor() { this.url = 'http://whatever.api/v1/'; // 一模一樣的 } _handleError(_res) { // DELETE: return _res.ok ? _res : Promise.reject(_res.statusText); return _res.statusText === 'OK' ? _res : Promise.reject(_res.statusText); } get(_endpoint) { // DELETE: return window.fetch(this.url + _endpoint, { method: 'GET' }) return axios.get(this.url + _endpoint) .then(this._handleError) // DELETE: .then( res => res.json()) .then( res => res.data) .catch( error => { alert('So sad. There was an error.'); throw new Error(error); }); } }; |
聽起來很合理。 提交、上傳、合併、完成。
不過,在某些情況下,這可能不是一個好主意。想象以下情景:在切換到 Axios
之後,你會發現有一個功能並不適用於 XMLHttpRequests( Axios
的獲取資料的方法),但之前使用 Fetch API
的新型瀏覽器工作得很好。我們現在該怎麼辦?
我們的技術負責人說,讓我們使用舊的 API
實現這個特定的用例,並繼續在其他地方使用 Axios
。你該做什麼?在原始碼管理歷史記錄中找到舊的 API
模組。還原。在這裡和那裡新增 if
語句。這樣聽起來並不太友好。
必須有一個更容易,更易於維護和可擴充套件的方式來進行更改!那麼,下面的就是。
方法二:重構程式碼,做適配!
重構的需求馬上來了!讓我們重新開始,我們不再刪除程式碼,而是讓我們在另一個抽象中移動 Fetch
的特定邏輯,這將作為所有 Fetch
特定的介面卡(或包裝器)。
HEY!???對於那些熟悉介面卡模式(也被稱為包裝模式)的人來說,是的,那正是我們前進的方向!如果您對所有的細節感興趣,請參閱這裡我的介紹。
如下所示:
步驟1
將跟 Fetch
相關的幾行程式碼拿出來,單獨抽象為一個新的方法 FetchAdapter
。
1 2 3 4 5 6 7 8 9 10 11 |
class FetchAdapter { _handleError(_res) { return _res.ok ? _res : Promise.reject(_res.statusText); } get(_endpoint) { return window.fetch(_endpoint, { method: 'GET' }) .then(this._handleError) .then( res => res.json()); } }; |
步驟2
重構API模組,刪除 Fetch
相關程式碼,其餘程式碼保持不變。新增 FetchAdapter
作為依賴(以某種方式):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class API { constructor(_adapter = new FetchAdapter()) { this.adapter = _adapter; this.url = 'http://whatever.api/v1/'; } get(_endpoint) { return this.adapter.get(_endpoint) .catch( error => { alert('So sad. There was an error.'); throw new Error(error); }); } }; |
現在情況不一樣了!這種結構能讓你處理各種不同的獲取資料的場景(介面卡)改。最後一步,你猜對了!寫一個 AxiosAdapter
!
1 2 3 4 5 6 7 8 9 10 11 |
const AxiosAdapter = { _handleError(_res) { return _res.statusText === 'OK' ? _res : Promise.reject(_res.statusText); }, get(_endpoint) { return axios.get(_endpoint) then(this._handleError) .then( res => res.data); } }; |
在 API
模組中,將預設介面卡改為 AxiosAdapter
:
1 2 3 4 5 6 7 8 |
class API { constructor(_adapter = new /*FetchAdapter()*/ AxiosAdapter()) { this.adapter = _adapter; /* ... */ } /* ... */ }; |
真棒!如果我們需要在這個特定的用例中使用舊的 API
實現,並且在其他地方繼續使用Axios
?沒問題!
1 2 3 4 5 6 7 8 9 10 11 12 |
//不管你喜歡與否,將其匯入你的模組,因為這只是一個例子。 import API from './API'; import FetchAdapter from './FetchAdapter'; //使用 AxiosAdapter(預設的) const API = new API(); API.get('user'); // 使用FetchAdapter const legacyAPI = new API(new FetchAdapter()); legacyAPI.get('user'); |
所以下次你需要改變你的專案時,評估下面哪種方法更有意義:
- 刪除程式碼,編寫程式碼。
- 重構程式碼,寫介面卡。
總結請根據你的場景選擇性使用。如果你的程式碼庫濫用介面卡和引入太多的抽象可能會導致複雜性增加,這也是不好的。
愉快的去使用介面卡吧!