Koishi学习笔记

爷青回!

顺便这个修了

5 个赞

daemon.ts:

import { Context, Schema } from '@koishijs/core'

export interface Config {
  autoRestart?: boolean
  heartbeatInterval?: number
  heartbeatTimeout?: number
}

export const Config: Schema<Config> = Schema.object({
  autoRestart: Schema.boolean().description('在运行时崩溃自动重启。').default(true),
  heartbeatInterval: Schema.number().description('心跳发送间隔。').default(0),
  heartbeatTimeout: Schema.number().description('心跳超时时间。').default(0),
}).description('守护设置').hidden()

Context.Config.list.push(Schema.object({
  daemon: Config,
}))

export const name = 'daemon'

export function apply(ctx: Context, config: Config = {}) {
  function handleSignal(signal: NodeJS.Signals) {
    // prevent restarting when child process is exiting
    if (config.autoRestart) {
      process.send({ type: 'exit' })
    }
    ctx.logger('app').info(`terminated by ${signal}`)
    ctx.parallel('exit', signal).finally(() => process.exit())
  }

  ctx.on('ready', () => {
    process.send({ type: 'start', body: config })
    process.on('SIGINT', handleSignal)
    process.on('SIGTERM', handleSignal)

    config.heartbeatInterval && setInterval(() => {
      process.send({ type: 'heartbeat' })
    }, config.heartbeatInterval)
  })
}

看到这个apply了吗?

export function apply(ctx: Context, config: Config = {}) {
// ...
}

我们终于看到第一个插件了,看哭了。
从官方文件了解到,三种形式可以被认定是一个插件。
认识插件 | Koishi
对于一个模块,可以是类或者对象。
翻到对应的定义:

export type Plugin<C extends Context = Context, T = any> =
  | Plugin.Function<C, T>
  | Plugin.Constructor<C, T>
  | Plugin.Object<C, T>

export namespace Plugin {
    interface Base {
        name?: string;
        reactive?: boolean;
        reusable?: boolean;
        Config?: (config?: any) => any;
        inject?: string[] | Inject;
        /** @deprecated use `inject` instead */
        using?: string[] | Inject;
    }
    interface Function<C extends Context = Context, T = any> extends Base {
        (ctx: C, options: T): void;
    }
    interface Constructor<C extends Context = Context, T = any> extends Base {
        new (ctx: C, options: T): void;
    }
    interface Object<C extends Context = Context, T = any> extends Base {
        apply: (ctx: C, options: T) => void;
    }
}

可以看到,base中的是预留的属性,作特殊用途,但全都都是可选的。所以这个模块毫无疑问符合插件的定义。

而运行时是js,对于函数参数的限制仅被ts限制了,所以其实这个函数/方法可以是任意参数的,因此:
只要有个apply方法的对象就是koishi插件!
只要是个类就是koishi插件!
只要是个函数就是koishi插件!
(好孩子不要学)

这个插件和我们的插件写的插件是一样的,都是koishi(cordis)插件围绕ctx做一些事情。
我们看看apply做了什么事情,整体来看,它是一个一个一个……保姆?
插在koishi身上负责照顾孩子。每天上班就给雇主发消息:“孩子她妈,娃醒了。”
然后时不时还要再给雇主发消息安顿雇主:“孩子她妈,娃在撒尿”,“孩子她妈,娃在大叫”,“孩子她妈,娃在玩眼球”。
雇主就放心了。
假如不这么做——

雇主就会发癫鲨掉娃儿!然后再生一个!

雇主就会着急跑回家,照顾好孩子,让她能睡个好觉。

除此之外呢,她还要负责接收雇主的短信当雇主说,“孩子该休息了”她就会让孩子料理后事刷牙洗脸,然后睡个好觉。

6 个赞

终于我们要解读koishi包下的最后一个文件了,
worker/index.ts:

import { Context, Dict, Logger, Schema, Time } from '@koishijs/core'
import Loader from '@koishijs/loader'
import * as daemon from './daemon'
import * as logger from './logger'

export * from 'koishi'

declare module '@koishijs/core' {
  namespace Context {
    interface Config {
      plugins?: Dict
      timezoneOffset?: number
      stackTraceLimit?: number
      logger?: logger.Config
      daemon?: daemon.Config
    }
  }
}

Object.assign(Context.Config.Advanced.dict, {
  timezoneOffset: Schema.number().description('时区偏移量 (分钟)。').default(new Date().getTimezoneOffset()),
  stackTraceLimit: Schema.natural().description('报错的调用堆栈深度。').default(10),
  plugins: Schema.any().hidden(),
})

function handleException(error: any) {
  new Logger('app').error(error)
  process.exit(1)
}

process.on('uncaughtException', handleException)

process.on('unhandledRejection', (error) => {
  new Logger('app').warn(error)
})

async function start() {
  const loader = new Loader()
  await loader.init(process.env.KOISHI_CONFIG_FILE)
  const config = await loader.readConfig(true)
  logger.prepare(config.logger)

  if (config.timezoneOffset !== undefined) {
    Time.setTimezoneOffset(config.timezoneOffset)
  }

  if (config.stackTraceLimit !== undefined) {
    Error.stackTraceLimit = config.stackTraceLimit
  }

  const app = await loader.createApp()
  app.plugin(daemon, config.daemon)
  await app.start()
}

start().catch(handleException)

可以看到,我们又往config里塞了一些东西:

declare module '@koishijs/core' {
  namespace Context {
    interface Config {
      plugins?: Dict
      timezoneOffset?: number
      stackTraceLimit?: number
      logger?: logger.Config
      daemon?: daemon.Config
    }
  }
}

怎么一会儿用assign一会用defineProperty

定义了高级配置和plugins
这个plugins包含了所有插件相关的设定,但这个东西应该用独立的方式处理,而不能简单用Schema显示。
然后做了最基本异常处理处理方式仅仅是忽略异常并显示通用的异常信息,避免出现异常导致整个崩溃。
对于插件的作者,即使koishi已经有了最基本的异常处理,你也应当自己处理插件内部的异常,避免异常影响的扩散。
接下来,定义了start函数后立即调用,在这个进程中启动koishi。
start函数中,除了初始化高级配置中的参数,有一个叫loader的东西在活跃着。它从线程中获取了配置文件的地址,解析后读入,然后启动了一个app并且加载了第一个插件——daemon。
具体做的事情让我们接下来深入了解这个包吧。

4 个赞

Object.assign is used to assign properties to a specific object from another one, which only clone the properties mentioned. But Object.defineProperty not only create a new property but it also sets the parameters to this property such as getter, setter, writable, et cetera.

Object.assign 用于将属性从另一个对象分配给特定对象,它仅克隆提到的属性。 但是Object.defineProperty不仅创建一个新属性,而且还设置该属性的参数,例如getter、setter、writable等。

4 个赞

迷子好厉害,学习了!

3 个赞