一个前端项目须要管理一堆前端数据请求,现代前端应用,几乎没见过将数据请求直接写在业务代码中,大部分时候,咱们都会将这些请求逻辑从业务代码中抽出来,集中管理。但随着业务开发的反复进行,咱们会逐渐发现一些现象,咱们对后端吐给咱们的数据开始提出一些具体细节上的要求,就我我的而言,我总结出以下要求:前端
我在几年前写过一个库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提供宿主,实现另外一种风格的响应式。若是你认为这种抽象能激发起你一点点兴趣,不妨到仓库中一块儿讨论,写码。