1.1.16 • Published 7 months ago

glede-server v1.1.16

Weekly downloads
-
License
MIT
Repository
-
Last release
7 months ago

GledeServer

打包

# 打开文件.npmrc注释掉下面这行, 以便使用镜像快速下载依赖
<!-- registry=https://registry.npmjs.org -->

# 全局下载Typescript打包依赖
npm install -g typescript tsc

# 下载GledeServer打包依赖
npm install

# 执行打包
npm run build

单例模式

目前单个项目中只能创建一个Server实例, 暂时无mono-repo的需求。

若后续使用mono-repo, 则考虑新增配置 basePath: '', 路由注册、日志记录、文档校验、数据库等操作基于basePath

当前文档支持不全, tests 目录下有一些基础示例, 另外d.ts有些描述, 若您有任何疑问可联系邮箱 1061393710@qq.com

基于配置启动

配置类型描述: GledeServerOpts

JSON配置案例: app.json

推荐!TS配置案例: app-config.ts

配置中的服务驱动配置以及环境环境请您自定义。

服务器日志信息 / 注册路由状态树 / api文档 (导入Apifox工具查看)

Benchmark

⚡️ 3~4x faster than Express

MacOS; Intel-i5 2.9GHz; Memory-DDR4 32 GB

压测数据

// types/index.d.ts 有详细的描述,手写配置会覆盖conf文件中的对应字段
// 优先级:代码中手写配置 > conf字段文件配置
interface GledeServerOpts {
    // ...
}

// app.ts
import { Server } from 'glede-server';

Server({ conf: 'configs/app.json' }, (err, address) => {
    if (err) {
        console.log(err);
    }
    else {
        console.log(`GledeServer is running at ${address}`);
    }
});

模版目录结构

  • 在您的项目目录下执行 npm install glede-server 后参考本项目的tests目录创建即可
  • 注意引包时使用 import {...} from 'glede-server';
  • 注意引用类型使用 import type {...} from 'glede-server';
├── app.ts                      // 服务器启动入口
├── configs                     // 服务器配置
│   ├── app-config.ts           // 服务器配置文件 支持ts和json格式, 可配置多个用于区分运行时环境
│   ├── app.json
│   └── lua                     // lua脚本目录
│       ├── index.ts            // lua脚本导出口
│       └── statList.lua        // 自定义redis lua脚本
├── tsconfig.json               // ts编译配置
├── types                       // ts类型描述
│   ├── server.d.ts             // 默认: /// <reference types="glede-server/types" />
│   └── redis-lua.ts            // 拓展redis指令类型描述
├── controllers                 // DAO, 数据操作对象
│   └── cat.ts
├── demos                       // 基础使用方式
├── crons                       // 定时事务
│   └── test.ts
├── logs                        // 日志目录
│   ├── apis.json               // 配置开启swagger, 在运行时执行生成覆盖接口文档
│   ├── error.log               // 必须存在, 初始化启动需要手动创建
│   └── routers.txt             // 最新的路由信息, 服务器的路由树
└── routers                     // 接口目录
    ├── api
    ├── common
    └── openapi

路由类

import { GledeRouter, Get, Post } from 'glede-server';

export class Router extends GledeRouter {
    // 注意方法不要使用箭头函数
    // 1. 依赖原型处理逻辑; 2. 注入依赖工具方便处理请求

    getAllUser(this: GledeThis, data: GledeReqData) {
        // doSomething.

        if (noPass) {
            return {
                // 1 客户端参数校验未通过, 业务无需关心
                // >= 2 自定义
                code: 2,
                data: null,
                msg: '描述错误原因'
            };
        }

        // 以下情景等价于返回 {code:0, data: null}
        // 1. 无return语句
        // 2. return null
        // 3. return;
        // 4. return undefined

        return {
            code: 0, // 0 处理成功
            data: {
                // ...
            }
        };
    }
}

通用方法

路由注册

注册不带前缀的路由

非index文件或目录会保持大小写被记录到路由中,例如示例中./api/user/index.tsuser会被注册到 /api/user/$subpath。一下示例中index是不会注册到路由中的,若注册/index则需装饰器完成需求:@Get('/index')。

routers/open?api|common/index/index.ts

routers/open?api|common/index.ts

严格注册模式

  • 除 '/' 路由外,是否携带 / 需注册不同的 RouterHandler

@Get('')@Get('/')监听的是不同的路由,

localhost:3020/userlocalhost:3020/user/ 是不同的路由

// 目录: routers/api/post
import { Post } from '../controllers';
import { GledeUtil, Get, GledeRouter } from 'glede-server'

export default class extends GledeRouter {
    /**
     * 删除动态
     */
    @Get('/del/:id', { schema: schema.delPost })
    @NeedAuth('user')
    async delPost(this: GledeThis, data: GledeReqData) {
        // Token鉴权通过, 这里可以看到用户身份
        const { token, payload } = this.getToken();
        console.log(payload.role, payload.uid, payload.exp);

        // 指定身份 root 0 | super 1 | admin 2 可下架用户文章
        // const ROLE_USER = 3; 参考类型描述文件 getToken 方法
        if (payload.role < ROLE_USER) {
            Post.deleteOne({ postId: data.params.id });
        }

        // 非管理员, 只能删除自己的文章
        else {
            Post.deleteOne({ postId: data.params.id });
        }
    }
}

最佳实践

  • 为了您能便捷使用GledeServer的装饰器, 装饰器GledeRouter被挂在了全局变量GSD上。
  • 日志打印、数据库模型、高复用代码块儿等挂载到global上或统一导出在GledeServer初始化前或其他合适时机执行一次。具体操作参考tests/app.ts & tests/components/service
// ./routers/api/xx
export class Router extends GSD.GledeRouter {
  @GSD.Get('/test')
  test(this: GledeThis, data: GledeReqData) {
    // do sth
  }
}

装饰器介绍

方法装饰器

将Handler装载至路由

  • @Get(url: string, { schema?: GledeGetSchema, version?: string })
  • @Post(url: string, { schema?: GledePostSchema, version?: string })

跨域装饰器

设置需要跨域的域名、方法、是否允许携带cookie

  • @Cors(origin: string | string[], method: string, credential?: boolean)

鉴权装饰器

身份鉴权(noauth | user | admin | super | root), 是否允许Handler处理 Default: noauth

  • @NeedAuth(role: string)

验签装饰器

签名验证, 是否允许Handler处理

  • @NeedSign()
/**
 * 1. 客户端 摘要过程
 */

// 通过登陆等鉴权接口拿到 'MTcwMjE0MTE0Mzg5M183ODk4.BGZh4oyyHMWAWkiVSJptV5yNb7w'

// 切割取第二部分缓存
const signKey = 'BGZh4oyyHMWAWkiVSJptV5yNb7w';

// 切割取第一部分, 需要随请求报文发送到服务端
const content = 'MTcwMjE0MTE0Mzg5M183ODk4';

// 要发送的报文体
const payload = JSON.stringify({ name: 'Kitty' });

// 同服务端约定的本项目的key
const baseKey = '007';

// 请求方法 uppercase
const method = 'POST' as 'POST' | 'GET';

// /开头的url上的query
const query = '/?test=001';

// 一个空格分割method 和 query
const head = method + ' ' + query;

function stringify(content) {
  if (method === 'GET') {
    return '';
  }
  if (method === 'POST') {
    return typeof content === 'string' ? content : JSON.stringify(content);
  }

  return '';
}

function getSign(head, payload) {
  return content + '.' + sha1(signKey + baseKey + head + stringify(payload));
}

function sendRequest() {
  return fetch('http://localhost:3020/?test=001', {
    method: 'POST',
    headers: {
      signature: getSign(head, payload)
    },
    body: stringify(payload)
  }).then(res => res.json());
}

sendRequest().then(res => {
  console.log(res);
});

数据库操作

mongoose 操作文档

定义数据模型

📢 参考DEMO: ./tests/controllers/cat.ts cat 对应了数据库中的集合名称 cats, 起名字要使用单数!否则需要指定集合名字

import { Model } from '@/index';

// 模型数据结构
const CatSchema = {};

// 模型自定义
const CatOpts = {
    // 指定集合名, 此时集合链接到了cat, 默认是cats
    collection: 'cat',

    // 添加便捷方法, 注意不要使用箭头函数!
    // 可以这样使用:Cat.findByName('^cool').then(res => {});
    statics: {
        findByName(name: string) {
            return this.find({ name: new RegExp(name, 'i') });
        }
    }
};

export default Model('cat', CatSchema, CatOpts);

操作数据模型

import Cat from '@/tests/controllers/cat';

// 1. 在Cat表中插入一条数据, 后面Demo默认包裹在try-catch中
try {
    await Cat.create({
        // 插入数据格式必须是CatSchema中定义, 否则字段会被忽略
    });
}
catch (err) {/* Handle Error */}

// 2. 在Cat表中查找一条数据, 随便找一只名叫 cool_xx 且小于2岁的🐱
// 非常不推荐正则, 除非搜索过滤等场景。一般在任何语言中的实现都是最慢最耗性能的模式匹配。
// 不过有的语言实现了正则的缓存, 可能在某些场景下会快。尽量不用吧!
Cat.findOne({
    name: new RegExp('^cool_', 'i'),
    age: { $lt: 2 }
});

// 3. 在Cat表中找到一条匹配的数据,删除
Cat.deleteOne({});

// 4. 在Cat表中找到所有可以匹配删除的数据
Cat.deleteMany({});

// 5. 在Cat表中找到数据并更新, upsert默认为false, 设置为true不存在就插入
// 注意原子操作, filter, { $set: { name: '小小明' } }, options
Cat.updateOne({ name: '明' }, { $set: { name: '小明' } }, { upsert: true });
Cat.updateOne({ name: '小明' }, {}, { upsert: true });

// 所有男生, 分数 +1
Cat.updateMany({ sex: 'male' }, { $inc: { score: 1 } });

// 6. 多种操作, 一次通信。性能upup!
// [https://mongoosejs.com/docs/api/model.html#model_Model.bulkWrite]
Cat.bulkWrite([
  {
    insertOne: {
      document: {
        name: 'Eddard Stark',
        title: 'Warden of the North'
      }
    }
  },
  {
    updateOne: {
      filter: { name: 'Eddard Stark' },
      update: { title: 'Hand of the King' }
    }
  },
  {
    deleteOne: {
      filter: { name: 'Eddard Stark' }
    }
  }
]).then(({ insertedCount, modifiedCount, deletedCount }) => {
    // 1 1 1
    console.log(insertedCount, modifiedCount, deletedCount);
});

默认记录错误日志

  • 默认记录日志, 需要创建对应的目录路径
  • 根目录创建文件: logs/error.log

请求需通过Schema校验

// 新建或修改路由文件
// mkdir routers/${api | openapi}/${router | routerDir/index.ts}
// api|openapi目录下存放路由可以是ts文件或目录, 文件内和目录内的Schema定义可相互引用
// 示例 /routers/api/user/index.ts
import { getAllUsersSchema, getAllUsersSchemaV2 } from './schema';

export Router extends GledeRouter {
    // version是接口的版本用于线上并行, 可选:默认 '', 如果出现版本区分可填写 v1, v2, ...
    // schema是参数的拦截校验, 必选:1. 客户端字段安全拦截 2. 增加序列化的性能10%~15% 3. 生成接口文档协同开发
    // match: /api/v1/:id
    @Get('/:id', { version: 'v1', schema: getAllUsersSchema }) @Cors()
    getAllUsers(this: GledeThis, data: GledeReqData): GledeResData {
        return {
            code: 0,
            data: {
                // ...
            }
        }; 
    }

    @Get('/:id', { version: 'v2', schema: getAllUsersSchemaV2 })
    @Post('/:id', { schema})
    @NeedAuth('super') @Cors('https://philuo.com', 'GET,POST')
    getAllUsersV2(this: GledeThis, data: GledeReqData): GledeResData {
        return {
            code: 0,
            data: {
                // ...
            }
        };
    }
}

集成功能

自定义日志输出

// @/utils/log.ts
import { GledeStaticUtil } from 'glede-server';
import { join } from 'path';

export const logger = new GledeStaticUtil.Logger({
    // 输出位置, 默认[1]输出到日志文件; [0]输出到控制台, [0, 1]输出到控制台和文件
    target: [1],
    // 日志输出的目录, 默认存储在运行node的路径下的logs路径下
    // !import 注意服务运行中不可以删除 dir目录
    dir: join(__dirname, 'logs'),
    // 日志文件名 默认 glede-server.log 如果开启轮转会自动补充后缀
    // !import 注意服务运行中不可以删除 filename文件, 其他轮转生成的文件可以移动或删除
    filename: 'glede-server.log',
    // 日志轮转, 到期生成新的日志文件格式如下 20231210-1411-03-glede-server.log
    interval: '30d',
    // 日志大小, 超限生成新的日志文件格式如下 20231210-1411-03-glede-server.log
    size: '10M',
    // 控制单个文件大小, 注意开启压缩再使用 超过限制后旧文件会被压缩
    // maxSize: '10M',
    // 是否开启压缩, 默认关闭 不允许设置false, 关闭不设置该属性即可
    // compress: true
    // 最多保留的最近的日志文件和压缩包数量, 默认全部保留不设置即可
    // maxFiles: 30
});

logger.error('123'); // level === 0
logger.warn('123');  // level === 1
logger.info('123');  // level === 2
logger.log('123', 2);   // 仅输出到控制台, 不干扰日志文件(level可选默认2 INFO级别)

Token签发与验证

  • 实现分发(sign, unsign)

  • 实现校验(verify)

if not 过期 -> if not 快过期 -> if match 身份 -> if not 是否篡改 -> if not blklist -> ok
else fail -> else -> else fail -> else fail -> else fail
             if ok then blklist and return data with new token
             if not ok then fail

数据库驱动

  • mongoose

  • ioredis

区域检测

@yuo/ip2region

SMTP邮件发送

nodemailer

黑名单

  • ip blklist

判黑条件:超管手动添加 / 时间段频率 / 单日访问次数

  • token blklist

判黑条件:超管手动添加 / 即将过期且验证通过的Token

定时任务

@yuo/node-cron

定时任务

  • 黑名单持久化

前期直接覆盖磁盘文件 后面要开线程做去重, 并且可能需要分片

  • 邮件通知警告

触发条件: 程序判定新增IP黑名单

1.1.16

7 months ago

1.1.15

8 months ago

1.1.13-doc.3

8 months ago

1.1.13-doc.1

8 months ago

1.1.13-doc.2

8 months ago

1.1.14

8 months ago

1.1.13

8 months ago

1.1.12

8 months ago

1.1.11

8 months ago

1.1.10

8 months ago

1.1.11-doc.1

8 months ago

1.1.11-doc.2

8 months ago

1.1.12-doc.1

8 months ago

1.1.9

8 months ago

1.1.8

8 months ago

1.1.7

1 year ago

1.1.6

1 year ago

1.1.5

1 year ago

1.1.4

1 year ago

1.1.5-doc.1

1 year ago

1.1.3

1 year ago

1.1.2

1 year ago

1.1.3-docs.1

1 year ago

1.1.3-docs.2

1 year ago

1.1.1

1 year ago

1.1.0

1 year ago

1.0.18

2 years ago

1.0.17

2 years ago

1.0.16

2 years ago

1.0.11

2 years ago

1.0.15

2 years ago

1.0.14

2 years ago

1.0.13

2 years ago

1.0.12

2 years ago

1.0.10

2 years ago

1.0.9

2 years ago

1.0.8

2 years ago

1.0.7

2 years ago

1.0.6

2 years ago

1.0.5

2 years ago

1.0.4

2 years ago

1.0.3

2 years ago

1.0.2

2 years ago

1.0.1

2 years ago

1.0.0

2 years ago