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

@fatewa/kinnara

v1.0.6

Published

Declarative proxy http api request.

Downloads

26

Readme

Kinnara

Kinnara 是为了解决前端应用中接口路径难管理、代码重复率高等问题而实现的基于 JSProxy 的轻量级解决方案

Install

一般情况下,安装最新的版本即可

npm i @fatewa/kinnara

Quick Start

// 定义接口路径对象
const routes = {
    v1: {
        users: '/v1/users',
        favorites: '/v1/favorites'
    }
}
  
// 实例化一个对象
const kinnara = new Kinnara()
  
const _ = kinnara
        // 设置一个 http 请求适配器
        // kinnara 提供了一个 axios 的适配器
        // 构造器传递的参数为 AxiosInstance
        .setHttpAdapter(new AxiosHttpAdapter(axios))
        // 并返回代理对象
        .proxy(routes)
  
 (async () => {
  
    // 直接使用对象 key 路径对接口进行请求
    const response = await _.v1.users.get()
  
})()

直接重用路由结构,直接请求接口, 支持 自定义指令,默认提供一套操作指令, 使用 Kinnara,API路径难以管理即将成为过去。

操作指令

我们在路径中声明的URI通常很难满足所有情况, 我们无法避免 URI 的调整 因此,Kinnara 提供了一个通用的、可扩展的命令接口,通过这些命令可以轻松地操作URL、Headers 和 其他属性。

基础代码

下文中的命令介绍代码都有这一段通用的代码, 在此声明后下文介绍命令时不再重复编写

const kinnara = new Kinnara()
  
// 定义接口路径对象
const routes = {
    v1: {
        users: {
          root: '/v1/users',
          single: '/v1/users/{id}'
        }
        favorites: '/v1/favorites',
    }
  
// 得到代理对象
const _: any = kinnara
                .setHttpAdapter(new AxiosHttpAdapter(axios))
                .proxy(routing)

Join

在 URL 的右边动态地添加字符串

// 动态得到的参数值
const configId = 1
  
const response = _.v1.users.root
    .join`/${configId}`
    .get()

得到的 URI 为 /v1/users/1

head

为单次请求添加请求头

const response = _.v1.users.root
  .head({
      ContentType:'application/json',
      Origin: 'https://xxx'
  })
  .get()

replace

替换 URI 中的占位符

const response = _.v1.users.single
  .replace({ id: 1 })
  .get()

得到的 URI 为 /v1/users/1

seq

指令序列,可以聚合多个指令执行, 用于构建复杂的 URI 操作

const response = _.v1.users.single
  .seq((chain) => {
    // chain provides all the instructions for the Kinnara library
    chain.replace({ id: 1 })
    chain.join`/issues`
    chain.head({ ContentType:'application/json' })
  })
  .get()

得到的 URI 为 /v1/users/1/issues, 并且请求时会附加 ContentType: application/json

map

操作原始请求对象,并进行映射,基本上可以处理任意复杂请求, 如果目前 Kinnara 所提供的命令无法满足需求,则可以使用 map

// API参数以执行自定义操作
const resposne = _.v1.users.single
  .map((it: RequestWrapper) => {
    // 调整请求url
    url: '/users',
    // 调整请求头
    headers: {},
    // 调整查询参数
    query: {},
    // 调整 body 参数
    body: {}
    // 调整是否可被观察(详情见下文)
    observable: true
  })
  .get()
  
  
// 或者你也可以直接将 map 中要返回的对象作为 get 的参数传入
const resposne = _.v1.users.single
  .get({
    // 调整请求url
    url: '/users',
    // 调整请求头
    headers: {},
    // 调整查询参数
    query: {},
    // 调整 body 参数
    body: {}
    // 调整是否可被观察(详情见下文)
    observable: true
  })

两种方式任选其一即可

observe

控制当前请求是否可被观察(见下文)

_.v1.users.root
  // 设置本次请求为禁止观察
  .observe(false)
  .get()

自定义操作命令

如果上文中所提到的命令都无法满足需求,则可以自定义命令, 我们以实现 random 命令为例, random 的作用为,在 URI 右边添加一个随机的整数

自定义指令的本质其实是对 RequestWrapper 对象的构建

export default class RandomCommand implements Command {
  
  /**
   * 当前已经处理完毕的结构
   */
  wrapper!: RequestWrapper
  
  /**
   * 命令的参数,这里我们设定一个系数
   */
  entrypoint (coefficient: number): RequestWrapper {
    return {
      url: `${this.wrapper.url}/${Math.random() * coefficient | 0}`
    }
  }
  
    /**
     * 命令在调用时的名称
     */
    name (): string {
      return 'random'
    }
  
}

使用

// 装载命令
Kinnara.use(RandomCommand)
  
// 使用自定义命令
_.v1.users.root.random(100).get()

Kinnara 中的观察者

在 URL 处于请求状态时提供订阅功能,因此您可以拦截或执行一些额外的响应处理

// 监听一个接口的请求
// 第一个参数的类型为 RegExp | string
// 当路径中存在占位符时,
// Kinnara 会自动将 形如 {any} 的占位符替换为 [\w-]+? 并将字符串转为正则
kinnara.subscribe(routes.v1.users.single, (request, response) => {
  // ... Do anything you want
  return {
    ...response,
    ext: '这是在订阅中新增的字段‘
  }
})
  
// After the listener
_.v1.users.single.replace({ id: 1 }).get((response) => {
  // 由于订阅时新增了此字段,在调用时,可以获取到
  const { ext } = response
})

如果在订阅中需要再次调用被订阅的 API,为了防止栈溢出,需在订阅的处理器中 禁用观察

kinnara.subscribe(routes.v1.users.single, (request, response) => {
  
  _.v1.users.single.seq(chain => {
    chain.replace({ id: 1 })
    // 标记禁用观察,避免栈溢出
    chain.observe(false)
  }).get()
  
  return {
    ...response,
    ext: '这是在订阅中新增的字段‘
  }
})
  
_.v1.users.single.replace({ id: 1 }).get((response) => {
  // 由于订阅时新增了此字段,在调用时,可以获取到
  const { ext } = response
})

取消订阅

调用 subscribe 方法后,会得到一个 key, 取消就是通过这个 key

const key = kinnara.subscribe(any, any)
  
// 取消订阅
kinnara.cancel(key)

有什么用 ?

有了针对单个路径的观察模型,我们可以很容易地实现一条完整的请求链路, 并且这个链路是全局有效的

我们假设现在拿到了一个这样的接口文档

获取当前登录用户的角色集合 /v1/users/roles

Response

code: 200,
// 角色 ID
data: [1, 2, 3, 4]

根据角色ID获取角色详情 /v1/roles/{id}

Response

code: 200,
// 角色 ID
data: {
  name: '管理员',
  code: 'admin'
}

很显然,这两个接口就是存在链路关系的, 我们使用 kinnara 对接这两个接口

我们推荐以下的文件夹组织结构进行管理

使用 index 进行导出, 各个模块的接口
放置在各自的文件夹中进行维护
  
project
├── api
│   ├── user
│   │   ├── index.ts
│   ├── biz
│   │   ├── index.ts
│   └── index.ts

user/index.ts

export default {
  users: {
    root: '/users',
    roles: '/users/roles'
  }
}

roles/index.ts

export default {
  roles: {
    root: 'roles',
    single: '/roles/{id}'
  }
}

index.ts

import users from './users'
import roles from './roles'
  
  
export default {
  // 外层组织 API 版本号
  v1: { users, roles }
}
  
import routes from '@/api'
  
const kinnara = new Kinnara()
  
// 得到代理对象
const _: any = kinnara
                .setHttpAdapter(new AxiosHttpAdapter(axios))
                .proxy(routing)
  
  
// 订阅请求用户角色的接口
kinnara.subscribe(routes.v1.users.roles, async (request, response) => {
  const { data } = response
  const roles = []
  for (const id of data) {
    const role = await _.v1.roles.single.seq(chain => {
      chain.replace({ id })
      chain.observe(false)
    }).get()
    roles.push(roles)
  }
  return {
    ...response,
    // 保留原始请求结构,复写 data 属性
    data: roles
  }
})

调用

  
_.v1.users.roles.get()
  
// 得到的结果 -> [{ ...role(完整的 Role 对象) }]
  

当然这样的请求方式,显然是不够合理的,很容易导致 Qps 过高, 所以我们需要对请求的结果进行缓存(如果应用是面向 C 端的,这样的处理方式仍旧不合理)

  
// 缓存请求到的结果
const cache = {
  counter: 0,
  data: null
}
  
// 订阅请求用户角色的接口
kinnara.subscribe(routes.v1.users.roles, async (request, response) => {
  const { data } = response
  const roles = []
  
  if (!cache.data) {
    for (const id of data) {
      const role = await _.v1.roles.single.seq(chain => {
        chain.replace({ id })
        chain.observe(false)
      }).get()
      roles.push(roles)
    }
    cache.data = roles
  } else {
    roles = cache.data
    // 缓存的数据保留 50 次,
    // 次数超过 50 后,则再次请求, 一定程度保障数据的实时性
    if (++cache.counter > 49) {
       cache.data = null
       cache.counter = 0
    }
  }
  
  return {
    ...response,
    // 保留原始请求结构,复写 data 属性
    data: roles
  }
})