npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

algeb

v5.4.0

Published

一个模拟代数效应的前端数据源管理工具。

Downloads

15

Readme

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 由sourcecompose创建的源。
  • params 传给sourcecompose第一个参数函数的参数。

下文会在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 在一切行动开始之后

具体生命周期的钩子如下:

  1. beforeAffect, afterAffect 是针对 runner 副作用而言,没有参数,表示副作用过程从开始到结束,一般用来作为渲染的某些处理。(开发者:只有 source 的刷新会带来这两个事件的触发,compund source 不会带来。)需要注意,它们在单一次周期中,只会触发一次,setup 内部在同一时刻可能会存在多个并行的数据请求,每次请求都会带来副作用,但是,为了便于更好的管理,beforeAffect, afterAffect 会合并这些并行请求,只会执行一次。(要警惕数据请求处于持续不断过程中,一旦出现这种情况,afterAffect 不会触发。)
  2. ready, success, fail, finish 是针对数据请求过程而言,它们提供了数据请求的结果状态,对于 compound source 而言,它本身是没有实际的请求过程的,compund source 的结果是计算出来的,因此本身没有这些事件,但是如果我们直接调用 compound source 的 renew 函数,则它的这些事件会被触发。
  3. 除了 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