一個前端項目須要管理一堆前端數據請求,現代前端應用,幾乎沒見過將數據請求直接寫在業務代碼中,大部分時候,咱們都會將這些請求邏輯從業務代碼中抽出來,集中管理。但隨着業務開發的反覆進行,咱們會逐漸發現一些現象,咱們對後端吐給咱們的數據開始提出一些具體細節上的要求,就我我的而言,我總結出以下要求:前端
我在幾年前寫過一個庫databaxe,提出一種新型的數據源理念,這種理念讓咱們能夠寫同步代碼,把請求過程和數據進行分離,對前端而言,請求自己是不可見的。前端只須要從倉庫中讀取數據便可。但當時採用了具名方式規定每個數據源的名稱,獲取參數對應關係比較複雜,須要監聽,並且內置了axios做爲數據請求器,對開發者而言是不開放的。vue
爲了繼續實踐這種寫同步代碼的方式,同時使數據請求自己更開放,我寫了algeb這個庫react
gitee.com/frustigor/algebgitee.comios
它的源碼比databaxe少了n倍,使用方法簡單了n倍。讓咱們來看看,我是如何作到的。git
咱們大多狀況下是經過請求後端API獲取數據,但API並非惟一的數據源,在前端編程中,客戶端持久化數據(例如存在indexedDB中的數據),websocket推送的數據,都是重要的數據來源。所以,咱們要尋找一種編程方式,能夠兼容不一樣形式的數據源,將不一樣形式來源的數據,經過一套方式進行管理。github
Algeb的方式是,將數據源和數據使用進行隔離,如何從數據源獲取數據不在Algeb的管轄範圍內,可是開發者須要將一個函數託管給它,這個函數從數據源獲得該數據源的數據。也就是說,它不關心獲取的過程,只關心結果,也就是這個函數的返回值就是我須要的最終數據。web
import { source } from 'algeb' const Some = source(function() { // ... 獲取數據的函數,返回值即爲被管轄的數據源數據 }, { name: '', age: 0, })
可是有一個很是常見的問題,咱們管轄一個數據源,卻可能經過不一樣參數得到不一樣對象。例如:編程
async function getBook(id) { return fetch(`/api/v2/books/${id}`).then(res => res.json()).then(body => body.data) }
這是咱們常見的一個用於獲取一本書詳細信息的函數。咱們常常會傳入id來決定獲取哪一本書的信息。而面對這種狀況,咱們怎麼去用Algeb管理呢?難道要爲每一本書創建一個源?json
固然不須要,Algeb所認爲的數據源,並不是指單一數據,而是獲取形式相同數據的方法(也就是這個函數),而且以該函數的參數做爲標記記錄該源全部被使用到的具體數據顆粒。這個邏輯是內部實現的,開發者不須要關心,只須要記住一點,數據源函數參數最好越簡單越好,這樣有利於對參數進行計算,做爲識別具體數據的依據。axios
const Book = source(getBook, { title: '', price: 0, })
source
函數的第二個參數是該源的默認值,我所崇尚的同步代碼書寫方式要求代碼在執行一開始就是OK的,不報錯的,因此,這個默認值很是關鍵,同時,經過這個默認值,也能夠告訴團隊其餘成員瞭解一個數據源將獲取到的數據的基本格式。
你可能會問,websockt推送的數據怎麼辦呢?因爲algeb只關心獲取數據的結果,因此開發者怎麼從websockt獲取數據咱們並不關心。我本身想到一種方式是,用一個全局變量保管不一樣數據源來自websockt的數據,而後在數據源函數中,讀取該全局變量上的屬性返回。
一般狀況下,咱們現有的數據源管理器只是簡單的讀寫邏輯,並無規定數據緩存的邏輯。我但願經過更抽象的方式,讓開發者本身來規定數據再次請求的邏輯。經過Algeb的compose方法,能夠組合一個或多個數據源,並附增特殊邏輯進去。
import { compose, query, affect } from 'algeb' const Order = compose(function(bookId, photoId) { const [book, refetchBook] = query(Book, bookId) const [photo, refetchPhoto] = query(Photo, photoId) affect(function() { const timer = setInterval(() => { refetchBook() refetchPhoto() }, 5000) return () => clearInterval(timer) }, [book, photo]) const total = book.price + photo.price return { book, photo, total } })
這是compose
的一個例子。它經過組合book和photo兩個對象,並附加算出這個訂單的總價格,做爲一個新的數據源返回。從「數據源」的定義上,Book, Photo, Order都是數據源,本質相同,只是類型不一樣而已。
有一個約定,雖然compose的返回值能夠是任意的,可是它必定是同步執行完後返回,因此compose不接受async函數。
但凡是數據源,就能夠在環境中(compose/setup)使用query
讀取,query函數接收第一個參數爲一個數據源對象,後面的參數將做爲數據源函數的參數進行透傳。它的返回值是一個兩個元素的數組,第一個元素是數據源根據該參數返回的值,第二個參數是刷新數據源數據的觸發器(非請求器)。
在環境中,還可使用affect等hooks函數,這些函數在環境中執行,例如上面這段代碼中,經過affect規定了Order這個數據源一旦被查詢,就會每隔5秒鐘再查一次。這樣,咱們經過compose,實際上定義了一個不只能夠獲取值的數據源,還定義了該數據源刷新數據的方式。
compose
讓咱們能夠在獲取一個值的同時,還會觸發其餘源的更新。這在一些場景下極其好用。例如,咱們有A、B兩個源,當咱們提交對A的更新後,須要同時從新拉取A、B的新值。咱們能夠經過compose來處理。
const UpdateBook = compose(function(bookId, data, photoId) { const [book, refetchBook] = query(Book, bookId) const [_, refetchPhoto] = query(Photo, photoId) affect(function() { updateBook(bookId, data).then(() => { refetchBook() // 從新獲取該書信息 refetchPhoto() // 從新獲取圖像信息 }) }) })
這個組合源只用於發送數據到服務端,發送成功後會同時抓取兩個數據源的新數據。一旦新數據獲取成功,全部依賴於對應數據顆粒(Book:bookId, Photo:photoId)的環境,所有都會被更新。
響應式應用框架的特徵是自動將數值的變化反應爲界面的變化。但若是你仔細觀察我上述描述,就會發現,怎麼實現響應式呢?這涉及到咱們怎麼去設計當數據源發生變化時,將這一變化產生的反作用即時反饋。
和常見的「觀察者模式」不一樣,我借鑑的是react hooks的響應式方案,即基於代數效應的依賴響應。咱們看react的functional組件,你會發現,它的響應式反作用,是「再算一次」!
再算一次!也就是組件function再執行一次,每次state被更新時,組件function被再次執行,獲得新的組件樹。神奇的「再算一次」特效,理論上會消耗更多性能,卻讓咱們能夠像撰寫同步代碼同樣,從頂向底書寫邏輯,並經過useEffect來執行反作用。
在Algeb中,我也是基於這種思路,但因爲這是一個通用庫,它不依賴框架,要去適應不一樣框架的差別,所以,我提供了一個setup
提供執行上下文。
import { setup } from 'algeb' setup(function() { const [some, refetchSome] = query(Some) affect(function() { console.log(some.price) }, [some.price]) render(`<div>${some.price}</div>`) })
setup
是全部algeb應用的入口,在setup以外使用algeb定義的源沒有意義,甚至會報錯。它接收的函數被成爲執行宿主,這個宿主函數會被反覆執行,它內部必定是會有反作用的,例如,上面這段代碼,反作用就是render
。當被query
的數據顆粒得到新數據時,宿主函數會被再次執行,這樣,就會產生新的反作用,從而反饋到界面上。
數據顆粒是指基於query參數的數據源狀態之一,好比前面的Book這個源,每個bookId會對應一個數據顆粒,每一個數據顆粒保存着當前時刻該bookId的book的真實信息,一旦有任何一個地方觸發了數據更新,那麼就會讓源函數再次執行,去得到新的數據,新數據回來以後,經過內部對比發現數據發生了變化,宿主函數就會再次執行,從而反作用生效。
如此循環往復,就會給人一種響應式的編程的感受,而這種感受,和傳統的經過觀察者模式實現的響應式具備很是大的感官差別,而這個差別,就是react踐行的代數效應所帶來的。
爲了適應不一樣框架中更好的結合使用,我在庫中提供了不一樣框架的使用。
React中使用
import { useQuery } from 'algeb/react' function MyComponent(props) { const { id } = props const [some, fetchSome] = useQuery(SomeSource, id) // ... }
Vue中使用
import { useQuery } from 'algeb/vue' export default { setup(props) { const { id } = props const [someRef, fetchSome] = useQuery(SomeSource, id) const some = someRef.value // ... } }
Angularjs中使用
const { useQuery } = require('algeb/vue') module.exports = ['$scope', '$stateParams', function($scope, $stateParams) { const { id } = $stateParams const [someRef, fetchSome] = useQuery(SomeSource, id)($scope) $scope.some = someRef // { value } // ... }]
Angular中使用
import { Algeb } from 'algeb/angular' // ts @Component() class MyComponent { @Input() id constructor(private algeb:Algeb) { const [someRef, fetchSome] = this.algeb.useQuery(SomeSource, this.id) this.some = someRef // { value } } }
在前端應用層和後端、持久化存儲、websockt等原始數據交互時,對於前端而言,這種交互過程都是沒有必要的,是和業務自己無關的反作用。Algeb這個庫,試圖用代數效應,參考react hooks的使用方法,實現先後端中間服務層的抽象。經過對數據源的定義和組合,以setup提供宿主,實現另外一種風格的響應式。若是你認爲這種抽象能激發起你一點點興趣,不妨到倉庫中一塊兒討論,寫碼。