algeb v5.4.0
ALGEB
一个模拟代数效应的前端数据源管理工具。
理念介绍
这是一个比较抽象的库,一开始可能比较难理解。我写它的初衷,是创建可响应的数据请求管理。在传统数据请求中,我们只是把携带ajax代码的一堆函数放在一起,这样就可以调用接口。但是这种方案不是很灵活,无法解决共享数据源,数据没回来时怎么办等等问题。我以前写过一个库databaxe,这个库抽象出了“数据源”这一概念,但是由于内置请求,导致无法灵活的适应各种框架。能否更底层更灵活一些?在研究react hooks之后,我决定做这个尝试,于是写出了这个库。
Algeb的核心理念和hooks一脉相承,简单的说,就是希望开发者可以在应用中以同步代码的形式进行写作,不用担心数据是否存在,只需要按照命令式的语句进行书写,就可以完成操作,而无需考虑数据本身。
安装
npm i algeb
API
import { source, query, setup } from 'algeb'
source(fn, default)
创建一个数据源获取对象获取器。
const Book = source(async function(bookId) {
const res = await fetch(some_url).then(res => res.json())
const { data } = res
return data
}, {
title: 'Book',
price: 0,
})
我们得到的Book
被成为“源”(Source),也就是获取数据的地方,在第一个函数中,你可以做任何操作,只要最终返回数据给我们即可。
- fn 可以是同步函数,也可以是异步函数,但最终都会被当作异步函数来使用。
- default 默认值,当fn还处于异步状态时,使用该值作为第一次计算的值进行计算。
query(Source, ...params)
获取源数据。
const [book, refetch, deferer] = query(Book, bookId)
我们得到一个只有4个值的数组,第一个值是当前Book的真实数据,第二个值是重新获取最新的数据的触发函数(该触发函数只触发请求,不返回结果),第三个值是对应数据的获取Promise。
- Source 由
source
或compose
创建的源。 - params 传给
source
或compose
第一个参数函数的参数。
下文会在setup
部分详细讲refetch
的运行机制。
setup(runner)
执行基于源的副作用运算。
setup(function() {
const [book, refetch] = query(Book, bookId)
render`
<div>
<span>${book.title}</span>
<span>${book.price}</span>
<button onclick="${refetch}">refresh</button>
</div>
`
}, options)
当执行该语句之后,setup中的函数会被执行。当refetch函数被调用,触发数据请求,当数据请求完成后,setup中断函数会被再次执行。
setup
的fn必须是同步函数,在第一次执行query时,由于请求刚刚发出,还没有真实值,因此会使用default作为默认值返回。
这就是 Algeb 的执行机制:通过触发数据源的重新请求,在得到新数据之后,重新执行setup中的函数,从而实现副作用的反复执行。'
setup返回stop函数,同时,它包含3个静态属性(以及一个可能的属性):
{
stop(): 停止setup再次执行的机制
next(): 如果执行完stop后,你又想再次运行这个机制,可以再调用next重新开始,如果没有执行过stop,调用next没有任何效果
value: fn的返回值,在执行机制中,fn会被反复执行,每次执行后,value都会被修改
start?(): 当 options.lazy 为 true 时存在,且只能被调用一次。
}
例子:
const ctx = setup(() => {
const [book, refetch] = query(Book, bookId)
render`
<div>
<span>${book.title}</span>
<span>${book.price}</span>
<button onclick="${refetch}">refresh</button>
</div>
`
return book
})
setInterval(() => {
console.log(ctx.value) // 每次都可能不一样
}, 1000)
options
目前支持 lifecycle 和 lazy 两个选项,lifecycle 阅读下文。当 lazy 为 true 时,setup 不会立即启动,而是需要你手动调用返回结果中的 start 方法。
高级用法
import { compose, affect, select, get } from 'algeb'
compose(fn)
创建一个基于源的组合获取器,它的作用是在源的基础上封装对该源的更多定义,一般是结合query
一起使用。
const Mix = compose(function(bookId, photoId) {
const [book, fetchBook] = query(Book, bookId)
const [photo, fetchPhoto] = query(Photo, photoId)
const total = book.price + photo.price
affect(() => {
const timer = setInterval(() => {
fetchBook()
fetchPhoto()
}, 5000)
return () => {
clearInterval(timer)
}
}, [book, photo])
return { book, photo, total }
})
我们可以同时组合多个源,获得一个“复合源”(Compound Source),组合函数必须是同步函数。组合函数返回组合后的复杂对象,还可以在内部提供一些特殊逻辑,比如上面的代码中,规定了每5秒钟更新数据源。
在compose组合函数中,你可以使用hooks(下方详解,source中不可以使用hooks),也可以query其他Compound Source。总之,compose组合其他源,同时可以使用hooks对不同源之间的重新计算逻辑进行逻辑处理。
它返回最终生成的“复合源”(Compound Source),它和source生产的源一样,可以被query使用,不同的是,query它返回的第二个值(函数)将触发组合内所有被依赖源全部重新请求新数据。
const [mix, updateMix] = query(Mix)
当调用updateMix()
时,Book和Photo这两个源的数据都会被重新请求。你也可以传入参数来决定只重新请求哪些源
updateMix(Book) // 只重新请求Book源
通过compose我们可以组合不同数据源,组合数据源的数据拉取规则,有利于复用一些特定规则。
stream(executor: ({ initiate, suspend, resolve, reject, complete }) => (...params) => void)
创建一个流类型的数据源(Stream Source),在某些场景下,数据是以流的形式,持续的输出数据,此时,我们使用stream数据源。
const streamSource = stream(({ initiate, resolve, reject }) => (projectId) => {
initiate()
const myDataStream = request(projectId)
const data = {}
myDataStream.on('data', (chunk) => {
Object.assign(data, chunk)
})
myDataStream.on('end', () => {
resolve(data)
})
myDataStream.on('error', (error) => {
reject(error)
})
})
其中,它的参数:
- initiate() 准备发起请求,此时会触发 beforeAffect和ready
- suspend(data) 刷新数据,但请求处于过程中,并未结束,会触发 success,同时,还会让依赖本source的compound source的数据进行刷新(但没有事件抛出),可以在finish之前被多次调用
- resolve(data) 刷新数据,此时会触发 success, finish, afterAffect,同时,还会让依赖本source的compound source进行刷新
- reject(error) 报错,此时会触发 fail, finish, afterAffect
- complete(unsubscribe) 可选,订阅终止时要执行的函数,当环境被销毁时,unsubscribe函数被执行,从而起到释放内存的作用
在调用这些方法时,你一定要安排好它们的调用时机,避免生命周期混乱导致表现不符合预期。例如,我们在做一些轮训时可以如此操作:
const streamSource = stream(({ initiate, suspend, resolve, reject, complete }) => (projectId) => {
let timer = null
const request = () => {
return fetchData(projectId, resolve, (res) => {
if (res.status === 206) {
suspend(res.data)
timer = setTimeout(() => {
request()
}, 2000)
}
else {
reject(res.error)
}
})
}
initiate()
request()
complete(() => clearTimeout(timer))
})
上面代码中,我们创建了一个 request 函数,它内部请求了某个数据,但是在请求过程中,由于后端的处理,返回了206状态码,那么我们需要等待2秒钟之后再次发起请求。但是,为了让界面上呈现出部分数据,我们此刻调用了suspend,把已经获得的数据提供给使用方(呈现在界面上)。之后,之后等了2秒,再运行request函数,最终获得数据之后,就可以在前端完整展示所有数据。
注意:当我们使用query查询stream时,其返回的renew没有实际的作用,无法触发具体的请求逻辑,因为请求的逻辑由 initiate, resolve 等控制。
get(source, ...params)
直接获取当前数据。
const data = get(Some, 123)
// 等价于
const [data] = query(Some, 123)
fetch(source, ...params)
通过Promise获取当前数据(缓存),当前如果没有则从后端拉取过数据。
const data = await fetch(Some, 123)
// 等价于
const [, , deferer] = query(Some, 123)
const data = await deferer
renew(source, ...params)
你可以使用renew来更新一个数据且缓存它。
renew(Some, 123)
// 等价于
const [, renew] = query(Some, 123)
renew()
请求完成时,对应参数的结构将会被放入仓库中,并触发对应的setup。
注意,Action
不能用于renew。
Hooks
下面的是hooks函数,它只能在compose或setup内部被使用,否则会导致错误。hooks的使用规则遵循react的规则,不允许在if..else中使用,必须在代码最前面撰写。
affect(fn, deps)
第一个hooks函数,它用于在compose或setup函数中执行一个副作用,它的使用方法和react hooks的useEffect基本一致,但在第二个参数上稍有不同。
- 如果不传deps,那么affect函数仅在compose函数第一次被执行时会执行
- 如果传入数组,那么每次执行会进行deps对比(深对比,对比内部对象每个节点上的值),有差异时执行
select(calc, deps)
它用于在compose或setup函数中,采用缓存计算技术得到一个值,和useMemo类似,它是否要重新计算值,取决于第二个参数deps是否发生变化。
- 如果不传deps,那么select仅在第一次进行计算,之后永远使用缓存
- 如果传入数组,那么每次执行会进行deps对比(深对比,对比内部对象每个节点上的值),有差异时才重新计算并缓存新值
apply(get, default)
某些情况下,你不想单独创建一个source,而是直接在compose中申请一个source,这样可以方便一些特定的source管理。此时,你可以使用apply。
const Mix = compose(function(bookId) {
const queryBook = apply((bookId) => ..., { name, price })
// 类似于做了两个步骤
// const Book = source((bookId) => ...);
// const queryBook = (...params) => query(Book, ...params);
// 不过这里的Book只在当前域内可用
const [book, updateBook] = queryBook(bookId)
...
})
apply本质上就是在compose内部的source函数。这样,你不需要在最外层通过source创建一个源,可以让代码分块更加一目了然。
ref(value)
有时你需要保持一个不变的量,此时使用ref。
const Mix = compose(function() {
const some = ref(0)
affect(() => {
setInterval(() => {
some.value ++
}, 1000)
}, [])
const any = select(() => some.value % 2, [some.value])
...
})
它和react的useRef很像,修改.value不会带来重新请求。
非代数效应用法
以下方法都不必在setup内部被调用,或与setup建立起来的体系无关。你可以理解为这些方法是algeb提供的扩展函数。
在algeb内部,会把一个数据源的具体数据进行缓存,当第二次传入相同参数时,不需要再次去远端请求,直接使用该缓存即可。 Algeb中的大部分方法都是基于这一设计来完成的。 但这里有一个问题,如果用户进行了更新操作,那么该数据理论上应该是最新的数据,但是由于我们读取了缓存,因此,就会导致读取出来的是不对的数据,因此,我们需要建立一套机制,在用户提交数据成功之后,立即更新与之关联的缓存。大致做法如下:
const SourceA = source(async (id) => ..., {})
const ActionA = action(async (id) => {
// postData...
await renew(SourceA, id) // 这里将更新SourceA中的数据(缓存),这样下次从SourceA中读取数据时,将获得最新的数据
})
await request(ActionA, id)
上面这一套机制,就可以保证我们的数据是实时最新的。
除了单用户本地更新外,我们还可以基于websocket来调用renew(SourceA, id)
,这样,即使有用户在另外一台电脑上进行了更新,我们也能知道这个更新动作,并更新SourceA中的数据。
注意:get
, fetch
, renew
也可以在 setup 之外使用,甚至 query
也可以,但是,在使用过程中,如果存在上下文中对 setup 有要求,则可能无法得到你预期的结果。
action(act)
创建一个仅用于处理副作用的source,该source只能被request使用。
const Update = action(async (bookId, data) => {
await patch('/api/books/' + bookId, data) // 提交数据到后台
request(Book, bookId) // 强制刷新数据
})
request(source, ...params)
读取数据:你可以用request,把source转化为类似一个普通的ajax请求来使用(类似于fetch,但不会使用缓存)。
发送数据:基于source发起请求,返回一个基于新请求的Promise,该请求将绕过algeb的运行机制,让你可以使用它作为纯粹的ajax数据请求。作用于ACTION类型的source。
const data = await request(source, { id })
注意,Compound Source
不能用于request。
isSource(value)
用于判断一个对象是否为source,返回boolean。
read(source, ...params)
读取当前值,不会触发内容任何机制,仅仅是一个读值过程。当值不存在时,返回 source 上的默认值。
release(source, ...params)
释放之前被请求过的源的保持数据,恢复到该源的初始状态。
也可以传入一个数组,此时表示将情况该数组内全部source的全部信息。
release([Book, Photo])
注意:基于不同参数得到的不同数据,将被全部释放,新的query都会重新请求数据。
subscribe()
创建一个 lifecycle 对象,用于传给 setup,当程序在运行时,在对应生命周期节点上,将会触发给定的函数。
来看下lifecycle的用法:
// 第一步,创建 lifecycle 对象
const lifecycle = subscribe()
// 第二步,订阅 lifecycle 事件
const print = ({ args }) => console.log('ready', args)
// 监听ready,并执行print函数
lifecycle.on('ready', print)
// 第三步,传入 setup
setup(runner, { lifecycle })
// 第四步:取消订阅
lifecycle.off('ready', print)
目前支持的生命周期钩子:
- beforeAffect 在一切行动开始之前
- ready 在准备开始刷新源数据之时(数据请求发出之前)
- success 执行source get函数(请求数据)成功时
- fail 执行source get函数(请求数据)失败时,可用于捕获ajax请求的错误信息或在get函数中主动/被动reject的错误信息
- finish 单次执行数据获取结束,即使fail被触发,也会进入finish生命周期
- afterAffect 在一切行动开始之后
具体生命周期的钩子如下:
- beforeAffect, afterAffect 是针对 runner 副作用而言,没有参数,表示副作用过程从开始到结束,一般用来作为渲染的某些处理。(开发者:只有 source 的刷新会带来这两个事件的触发,compund source 不会带来。)需要注意,它们在单一次周期中,只会触发一次,setup 内部在同一时刻可能会存在多个并行的数据请求,每次请求都会带来副作用,但是,为了便于更好的管理,beforeAffect, afterAffect 会合并这些并行请求,只会执行一次。(要警惕数据请求处于持续不断过程中,一旦出现这种情况,afterAffect 不会触发。)
- ready, success, fail, finish 是针对数据请求过程而言,它们提供了数据请求的结果状态,对于 compound source 而言,它本身是没有实际的请求过程的,compund source 的结果是计算出来的,因此本身没有这些事件,但是如果我们直接调用 compound source 的 renew 函数,则它的这些事件会被触发。
- 除了 beforeAffect, afterAffect 其他事件都能接收到具体参数,通过参数判断,你可以知道该事件是由哪一个 source 触发
React中使用
import { useSource, useLazySource } from 'algeb/react'
function MyComponent(props) {
const { id } = props
const [data, renew, pending, error] = useSource(SomeSource, id)
// ...
}
- data: 得到的数据
- renew: 重新拉取的函数
- pending: boolean 是否处于请求过程中
- error??: Error 出错时抛出的错误
useLazySource
不会在一开始就发起请求,而是会在调用renew时才发起,这有利于我们控制和减少首屏打开时就发出请求。
function MyComponent(props) {
const { id } = props
const [data, request, pending, error] = useLazySource(SomeSource, id)
// ...
// 点击某个按钮后才开始发出请求
const handleStart = () => {
request()
}
}
Vue中使用
仅支持vue3.0以上。
import { useSource } from 'algeb/vue'
export default {
setup(props) {
const { id } = props
const [data, renew, pending, error] = useSource(SomeSource, id)
// 其中除了renew之外,其他3个都是computed对象
}
}
Angularjs中使用
const { useSource } = require('algeb/vue')
module.exports = ['$scope', '$stateParams', function($scope, $stateParams) {
const { id } = $stateParams
const [data, renew] = useSource(SomeSource, id)($scope)
// data.value
// data.pending
// data.error
}]
Angular中使用
import { Algeb } from 'algeb/angular'
@Component()
class MyComponent {
@Input() id
private some:any
constructor(private algeb:Algeb) {
const [data, renew] = this.algeb.useSource(SomeSource, this.id)
// data.value
// data.pending
// data.error
this.data = data
this.renew = renew
}
}
Lisence
MIT
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
3 years ago
3 years ago
3 years ago
4 years ago
4 years ago
4 years ago