1.0.6 • Published 2 years ago

@fatewa/kinnara v1.0.6

Weekly downloads
-
License
MIT
Repository
github
Last release
2 years ago

npm.io npm.io npm.io

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
  }
})
1.0.6

2 years ago

1.0.5

2 years ago

1.0.4

3 years ago

1.0.3

3 years ago

1.0.2

3 years ago

1.0.0

3 years ago