博客和公众号

此文已同步到因卓诶博客,请大家关注同名公众号

[See How]全栈Node TS框架TSRPC实践教程(一)​www.yinzhuoei.com

前言

某个普通的一天的早晨,水友群的小姐姐和我聊前端架构,因为她们组最近要筹备一些新项目,在做架构的中途出现了很多问题,所以我拿到了她们的架构项目脚手架代码。拿到代码之后我发现深圳那边的前端团队普遍做的很好,有先进的架构思想,也把ts用的很纯粹,最后没帮人家解决问题,反倒是自己学到了不少。最后我们聊到了前后端全栈开发,如何动态校验协议参数等问题,因为熟悉我开源项目(剑指题解)的朋友都知道,我的后端代码尤其是动态校验那块写的是真差,为了ts而用ts,这也是目前很多用ts的小伙伴的通病,所以我一直打算重构我的一部分后端代码,这个时候见多识广的小姐姐就推荐给我了一个框架,这个框架也是[see how]系列第一篇教程的主角,这个框架就叫做TSRPC

关于专栏

关于see how是什么,说来很巧,这也是TSRPC作者王大大对我的seho这个名字的猜测,其实我的一个名字也没那么多深意,然后被大佬解读成了see how,所以我感觉这是一个不错的idea,那么本来就是想要出一个tsrpc的系列教程,和大家一起学习这个优秀的框架,就正好作为see how 专栏的第一篇文章吧。

关于TSRPC

在正文开始之前,我希望大家可以去自行先去简单快速的浏览相关知识,tsrpc是一个ts的开源rpc框架,它是为了全栈项目而生的,从我上手的第一天开始,我就对这个框架有了以下的第一印象:

  1. 天然二进制传输
  2. 纯粹的ts,规避了极大部分开发中的错误
  3. 强大的运行时复杂检测
  4. 这种前后端开发模式,我闻所未闻

官方文档
视频教程

前期准备

学习tsrpc需要你有一些前置知识和其他准备:

  1. 熟悉typescript基本语法
  2. 准备一个mongodb数据库

开发

使用tsrpc开发全栈应用简单到没朋友,可以从官方提供的cli快速创建前后端一体项目:

npx create-tsrpc-app@latest

按照指引选择浏览器应用,等待完成安装之后,你的目录中会出现2个目录:

- backend 后端
- frontend 前端

我们直接一睹为快,在前端项目根目录运行

官方的脚手架为我们准备了一个简单的todolist应用

整个前后端的目录结构(摘抄官网)

|- backend --------------------------- 后端项目|- src|- shared -------------------- 前后端共享代码(同步至前端)|- protocols ------------- 协议定义|- api ----------------------- API 实现index.ts|- frontend -------------------------- 前端项目|- src|- shared -------------------- 前后端共享代码(只读)|- protocols|- index.ts

诶,你可能会疑问了,为啥会有一个莫名其妙的shared目录,还要给前端项目去分享这个目录。是因为在shared这个目录我们要定义协议,啥玩意是协议呢?我们通过一个小小的接口来给大家解释什么是协议;


export interface ReqAddPost {newPost: {name: string;};
}export interface ResAddPost {insertedId: string;
}

我们可以在shared/protocols中新建了一个文件PtlAddPost.ts,我们必须以Ptl进行开头定义协议,协议是用来描述一个接口的请求和响应的结构体的文件,你可以这么理解。协议文件通过shared目录共享到前端,你知道会发生什么事情吗?造成了我们前端在对接口的时候,全程代码提示以及严格和请求和返回类型校验。

那么我们接着后端继续聊,协议定义之后该如何做呢?

npm run proto 每当协议更改后,需要重新运行这个命令

tsrpc的设计是协议和api分离,我们必须要清楚,api在我的认知里就是一个异步函数,tsrpc可以帮助我们根据我们刚刚写的协议生成api,比如刚刚我们实现的PtlAddPost.ts,我们运行

在api目录中会多出一个ApiAddPost.ts

import { ApiCall } from "tsrpc";
import { ReqAddPost, ResAddPost } from "../shared/protocols/PtlAddPost";export async function ApiAddPost(call: ApiCall<ReqAddPost, ResAddPost>) {}

我们通过call这个方法获取请求参数以及响应给客户端一些信息,我们来一个简单的例子:

export async function ApiAddPost(call: ApiCall<ReqAddPost, ResAddPost>) {if(call.req.newpost.name){call.success({msg: "hello," + call.req.newpost.name})}else{call.error('Invalid name');}
}

我们的第一个api已经写完了,我们需要正常的过一次test,然后我们在让前端去调用。

tsrpc使用的是mocha这个测试框架。


import { HttpClient } from "tsrpc";
import { serviceProto } from "../../src/shared/protocols/serviceProto";describe("api 测试", async function () {let client = new HttpClient(serviceProto, {server: "http://127.0.0.1:3000",logger: console,});let ret = await client.callApi("AddTest", {newPost: {name: "seho"},});
});

这是我们后端的一个简单的测试用例,在运行这个测试用例之前,您必须要开启后端的服务:

然后可以再开启一个窗口运行npm run test,如果一切正常,你可以看到下面的控制台输出:

粗略计算了一下,我们从开始定义协议到api测试完成,一个简单的接口不到5分钟就已经完成。

这个时候我们可以把这个接口放到前端再继续测试一下。

当然在此之前,我们需要运行以下命令:

我们之前提到过,前后端有一个共享的目录,运行此命令我们就可以把协议等信息同步过来,这个时候我们可以在前端的index.ts文件中,可以获得非常完善的代码提示。


import { HttpClient } from "tsrpc-browser";
import { serviceProto } from "./shared/protocols/serviceProto";let client = new HttpClient(serviceProto, {server: "http://127.0.0.1:3000",logger: console,
});client.callApi("AddTest", {newPost: {name: "hello, seho"},
});

当我们回到浏览器前端页面上时,这个请求就会发出,如果你仔细观察控制台,会看到以下的场景:

我们的请求体被二进制序列化了,这也是tsrpc的特点之一,我们会在稍后的段落中对tsrpc各个特性做介绍,但是此时此刻我们已经完成了一个api的后端开发->test测试->前端调用。

完善我们的程序

上一个部分相信大家已经学会了如何使用tsrpc开发第一个api,这个部分结合了tsrpc的视频教程中的案例,我们需要做一个简单的CRUD,使用mongoDB。

我们需要在本地启动我们的mongoDB服务,然后我们需要添加一些代码到后端backend项目中。

代码开始之前,我们需要安装mongoDB的依赖,我们可以更方便的引入类型定义以及各种数据库方法。

为了和视频教程统一,我们的工具类/架构方式,将直接挪用视频教程中的代码:

  • 写一个数据库的表模型,名为Post.ts

export interface Post {_id: string;author: string;title: string;content: string;visitedNum: number;create: {uid: string;time: Date;};update?: {uid: string;time: Date;};
}

我们的数据库模型是需要共享到前端的,方便前端工程能够复用,但是为了确保后端的类型安全,我们需要在模型上多做一层处理。mongodb的id属性不是string,而是ObjectID,所以我们需要在后端对模型进行类型重写(只重写id字段)。

关于为什么要在后端多做一层封装是因为不可能在前端引入mongodb中的objectID


import { ObjectID } from "mongodb";
import { Overwrite } from "tsrpc";
import { Post } from "../Post";export type DbPost = Overwrite<Post, {_id: ObjectID
}>

我们使用tsrpc提供的Overwrite泛型对刚刚写的Post类型进行改写,将mongodb中的objectID类型引入进来进行替换,然后我们后端工程就要使用这个Dbpost类型,而不是刚刚我们写的Post类型。

  • 数据库相关配置

export const BackConfig = {mongoDb: "mongodb://localhost:27017/test",
};
  • 定义数据库初始化类
import { Collection, Db, MongoClient } from "mongodb";
import { Logger } from "tsrpc";
import { BackConfig } from "./BackConfig";
import { DbPost } from "./dbItems/DbPost";export class Global {static db: Db;static async init(logger?: Logger) {logger?.log(`Start connecting db...`);const client = await new MongoClient(BackConfig.mongoDb).connect();logger?.log(`Db connected successfully...`);this.db = client.db();}static collection<T extends keyof DbCollectionType>(col: T): Collection<DbCollectionType[T]> {return this.db.collection(col);}
}export interface DbCollectionType {Post: DbPost;
}
  • 改写后端index.ts

import { Global } from "../src/shared/protocols/models/Global";async function main() {await server.autoImplementApi(path.resolve(__dirname, 'api'));await Global.init(server.logger);await server.start();
};

ok,截止到目前,我们把第一张表的相关配置已经搞定了,请确保数据库已打开且配置正确,然后我们直接运行一下服务器:

如果你运气好(狗头),那么你应该是成功开启这个服务器,并且控制台能看到连接成功的信息:

然后我们快速开发一下新增API,其他的更新和删除API,希望能大家举一反三,自行开发。


import { Post } from "./models/Post";export interface ReqAddPost {newPost: Omit<Post, "_id" | "create" | "update" | "visitedNum">;
}export interface ResAddPost {insertedId: string;
}

我们规定的请求类型是只能让客户端传递除了id,create,update,visitedNum的Post类型。然后我们还是运行那几个熟悉的命令:

npm run proto
npm run apiimport { ApiCall } from "tsrpc";
import { Global } from "../shared/protocols/models/Global";
import { ReqAddPost, ResAddPost } from "../shared/protocols/PtlAddPost";export async function ApiAddPost(call: ApiCall<ReqAddPost, ResAddPost>) {let op = await Global.collection("Post").insertOne({...call.req.newPost,create: {uid: "xxx",time: new Date(),},visitedNum: 0,});call.succ({insertedId: op.insertedId.toHexString(),});
}

这一part完成~

如何做到动态类型校验

之前我们就提到过,前端在调用后端的api时候,会给出完整的代码提示,从api名称到api的请求体类型等等,那么这一定程度上杜绝了开发中常见的接口联调不细心的问题。在传统的前后端开发中,尤其是分离模式,有一个非常常见的问题就是动态类型校验。每个语言/框架都有自己类型校验的手段,比如springmvc我们可以通过注解的方式来校验(下面展示了控制器中的校验,还有其他校验手段):

@Controller
@RequestMapping("valid")
@Slf4j
public class ValidateController {private static final String BASE_PATH = "/valid/";@RequestMapping("index")public String index(@Validated() Student student,BindingResult result){ return BASE_PATH + "index";}
}

那么tsrpc是如何保证数据传输的正确性的呢,首先我们如果在前端使用tsrpc的浏览器请求包,我们调用api时候不仅会在开发中提示开发者这个字段是错误的,而且会在请求发出之前做前端方面的遏制。在后端请求到达异步函数之前,也会去做第三次校验;所以我们在后端异步函数中使用到的参数一定是类型安全,完全不需要担心安全问题。

市面上有很多js领域解决动态校验的方案;最常见应该就是json schema,可以基于json自己实现一套校验方法可以在运行时来做校验。但是仍然有很多缺点,比如不能在前端进行运行时提示且可能重复写很多类型定义。那么tsrpc核心中使用到了一个库(这个库也是同个作者开发的):

tsrpc-buffer

为了实现ts动态类型校验,不可能把整个ts加进去,因为那有足足60m多,这是不现实的。所以作者开发了这个库。tsrpc依赖了这个库,它对ts的语法进行了兼容,目前支持了大部分的ts的写法,包括我们常用的string,number等,还支持一些复杂的泛形。

如果你想细细了解这方面,可以看一下文档支持的ts类型有哪些

当然,随着ts的更新,这个buffer也会支持更多的ts类型,可以做更完善的全栈应用。而且我们可以使用tsrpc进行原汁原味的ts开发,市面上的第三方工具/框架需要借助另外编程语言/DSL,tsrpc-buffer完全让你使用ts,你不会感觉到一丝违和感。

二进制序列化

tsrpc的二进制序列化机制是由我们上文中提到的tsrpc-buffer中实现的,那么这个特性带给我们的是比json更小的传输体积且支持更多的数据类型,ArrayBuffer, Date等。这意味着使用tsrpc的全栈应用在应对上传图片这种业务的时候简直就像是小儿科,我们可以用一个例子来证明。

export interface ReqUpload {fileName: string,fileData: Uint8Array
}export interface ResUpload {url: string;
}

我们通过刚刚学到的一些命令,来生成协议以及api

npm run proto
npm run apiimport { ApiCall } from "tsrpc";
import { ReqUpload, ResUpload } from "../shared/protocols/PtlUpload";
import fs from "fs/promises";export async function ApiUpload(call: ApiCall<ReqUpload, ResUpload>) {await fs.writeFile("uploads/" + call.req.fileName, call.req.fileData);call.succ({url: "http://127.0.0.1:3000/uploads/" + call.req.fileName,});
}

为了让前端调用,同步shared下的协议

写一个简单的file选择器在index.html中

<input type="file" id="fileInput">import { HttpClient } from "tsrpc-browser";
import { serviceProto } from "./shared/protocols/serviceProto";let client = new HttpClient(serviceProto, {server: "http://127.0.0.1:3000",logger: console,
});const input = document.getElementById("fileInput") as HTMLInputElement;input.addEventListener("change", async () => {if (input.files) {const fileData = await loadFile(input.files?.[0]);upload(fileData, input.files?.[0].name);}
});const upload = async (fileData: Uint8Array, fileName: string) => {const fr = new FileReader();client.callApi("Upload", {fileData,fileName,});
};function loadFile(file: File): Promise<Uint8Array> {return new Promise((rs) => {let reader = new FileReader();reader.onload = (e) => {rs(new Uint8Array(e.target!.result as ArrayBuffer));};reader.readAsArrayBuffer(file);});
}

开发完毕,我们可以仔细看一下控制台:

尽管我们在日常开发中会用到一些组件库,组件库帮助我们做了上传的大部分工作,所以我们写原生的上传可能在代码量上更多,但是省去了前后端转换Formdata的时间。

向后兼容http(json)和WebSocket

tsrpc也向后支持json,我们可以在客户端进行一个简单的配置,发送的请求就是json啦:

let client = new HttpClient(serviceProto, {server: "http://127.0.0.1:3000",json: true,logger: console,
});

我其实暂时没有想到非要使用json的场景,使用二进制序列化比json体积更小传输更快,本地开发的日志也在控制台随时打印,所以我还是建议大家使用默认的二进制序列化的传输模式。

tsrpc设计之初是为了游戏,因为传输特性能让websocket更高效,我们可以用tsrpc简单做一个websocket-demo,具体实现我参考了官网的实现,如果你想直接了解官网的这一part的内容,直接移步:

websocket实时服务-tsrpc

tsrpc的实现和协议无关,意味着咱们之前写的代码都可以用,仅仅做一个简单的调整替换即可。

websocket的消息是tsrpc传输中最小单元,我们需要用另外一个方法去定义协议,我们的websocket例子如下:

客户端发起一个请求,服务端接收并且向所有客户端发送一个消息

首先我们需要定义一个MsgHello.ts这样的协议:


export interface MsgHello {time: Date;content: string;
}

这个协议规定了前后端通讯的请求体。

我们需要改写后端backend中的index.ts,将原先的HTTP服务,改成Websocket服务

import { HttpServer, WsServer } from "tsrpc";export const server = new WsServer(serviceProto, {port: 3000,logMsg: true
});

这里导出server是有用意的,我们将在之后的代码中会用到这个server。

改写frontend前端中的index.ts

import { HttpClient, WsClient } from "tsrpc-browser";let ws = new WsClient(serviceProto, {server: "ws://127.0.0.1:3000",logger: console,
});const init = async () => {let result = await ws.connect();
};init();

我们需要一个api来触发后端给client发送websocket消息:


export interface ReqSend {content: string;
}export interface ResSend {time: Date;
}

定义成功后,我们运行以下几个命令:

npm run proto
npm run api

运行成功,我们可以在api文件夹下的ApiSend.ts中写入以下内容:

import { ApiCall } from "tsrpc";import { server } from "..";
import { ReqSend, ResSend } from "../shared/protocols/PtlSend";export async function ApiSend(call: ApiCall<ReqSend, ResSend>) {const time = new Date();call.succ({time,});server.broadcastMsg("Hello", {content: call.req.content,time,});
}

我们的后端逻辑写完了,我们运行以下命令,将协议同步到前端

我们进一步改写前端frontend/src/index.ts:

import { HttpClient, WsClient } from "tsrpc-browser";
import { serviceProto } from "./shared/protocols/serviceProto";let ws = new WsClient(serviceProto, {server: "ws://127.0.0.1:3000",logger: console,
});const init = async () => {let result = await ws.connect();console.log(result)if (result.isSucc) {// ws.callApiws.callApi("Send", {content: "hello websocket",});}
};init();

我们在页面初始化的时候,向后端发送刚刚写好的SendApi,这个时候我们既能收到api的返回,也能收到websocket的消息推送。

可以看到websocket传输也是二进制的,我们在开发中,也能发现,无论是callApi和发送websocket通知,从始至终都有类型推导,永远不会在传输中出现类型上的错误,这就是tsrpc的强大之处。

多平台

tsrpc支持多个平台,支持浏览器/小程序/原生ios 安卓/nodejs,甚至它还支持serverless,可以使用tsrpc开发基于阿里云/腾讯云的云函数;在后续我也会对tsrpc生态开发更多插件,使其兼容uniapp&unicloud,让他严格严格意义上跨多端,我相信tsrpc可以改变unicloud的开发习惯,让全栈应用更简单。

结语

本篇文章所有的知识点均在官网&视频教程有体现,视频教程在文章开始之前就有链接,非常希望大家能够先去看一下那个视频。tsrpc的教程还会出,下一篇关于tsrpc文章主要还是讲一下如何和serverless(unicloud)融合。这篇文章正在写大纲,相信也会在这个月之内能和大家见到。

本文使用 文章同步助手 同步

[See How]全栈Node TS框架TSRPC实践教程(一)相关推荐

  1. 【Rust 日报】2022-10-16 全栈同构Web框架leptos

    leptos:全栈同构Web框架 Leptos 是一个全栈.同构的 Rust Web 框架,利用细粒度的响应式来构建声明性用户界面. 全栈:可用于构建在浏览器.服务器或通过在服务器上渲染 HTML 然 ...

  2. python全栈开发实战pdf老男孩_Python教程:2017年老男孩最新全栈python第2期视频教程全套完整版...

    教程名称:2017年老男孩最新全栈python第2期视频教程全套完整版 教程目录: day01-python 全栈开发–基础篇 day02-python 全栈开发-基础篇 day03-python 全 ...

  3. 【新书上架】 | 《全栈应用开发:精益实践》——历时两年二十万余字

    两年前,从 RePractise 的一篇文章里,我开始了 Growth 应用及电子书的编写.Growth 整个系列的内容在 GitHub 上已经有近万个 star.今天我们带来了 Growth 的纸质 ...

  4. 全栈工程师修炼指南 - 学习/实践

    1.介绍 TBD 2.应用背景 TBD 3.学习 参考: https://xueyuanjun.com/books/php-fullstack 后续补充 ... 4.推荐书籍 TBD 5.学习体会 T ...

  5. JDG人脸识别课堂管理系统全栈开发流程报告-软件工程实践报告

    JDG人脸识别课堂管理系统全栈开发流程报告-软件工程 ⭐️ 本报告的代码部分和程序设计参考了 计算机18-1班张宇哲(学号181002406)同学 在Gitee仓库发布的AI-Attendance,本 ...

  6. python flask框架优点_python之全栈(Flask框架)

    虚拟环境 虚拟环境顾名思义就是虚拟的,在这里面装任何软件都不会影响到其他的程序,类似与一个抽屉. 使用虚拟环境的好处是:让电脑中安装很多种解释器,并且互不影响 virtualenv的使用 安装virt ...

  7. 2022年值得使用的 Node.js 框架

    大家好,我是若川.持续组织了8个月源码共读活动,感兴趣的可以 点此加我微信ruochuan12 参与,每周大家一起学习200行左右的源码,共同进步.同时极力推荐订阅我写的<学习源码整体架构系列& ...

  8. 拿来就用:11款不容错过的 Node.js 框架

    整理 | 章雨铭 责编 | 屠敏 出品 | CSDN(ID:CSDNnews) Node.JS是最流行的开源JavaScript运行时框架之一,并具有在浏览器之外建立代码的跨平台能力.知名开发者Ale ...

  9. 前端到全栈 -- js连接MYSQL数据库

    前端到全栈–node.js连接MYSQL数据库 前置条件: 安装node环境 安装mysql数据库 这里建议使用webstorm来写js 1.创建一个文件夹(这里以server为文件夹名举例),在命令 ...

最新文章

  1. 参与开源项目,结识技术大牛!CSDN “开源加速器计划”招募志愿者啦!
  2. Lifecycle Activity和Fragment生命周期感知组件 LifecycleObserver MD
  3. linux下使用cmake构建C/C++项目
  4. linux 字符串加入中括号,Linux Shell 基础 -- 总结几种括号、引号的用法
  5. Linux——SSH服务器
  6. 小酌一下:Pycharm 2019.1.3 64位版本破解
  7. STM32工作笔记0083---UCOSIII中断和时间管理
  8. 洛谷 P2515 [HAOI2010]软件安装 解题报告
  9. android.content.res.Resources$NotFoundException: String resource ID #0x0
  10. 电脑怎么打出冒号符号_标点符号的用法,资深老师带你学习,提高学生学习效率...
  11. 洛谷—— P1018 乘积最大
  12. 经典SQL面试10题(附答案)
  13. Semantic Nets
  14. JavaWeb复习题(含答案)
  15. 计算机软件退税公式,软件产品增值税即征即退税额的计算方法 会计看过来!...
  16. 基于SpringBoot实现单点登录系统
  17. Matlab中插值函数汇总和使用说明
  18. 分享一个AUTO uninstaller|AUTOCAD 安装失败解决方案
  19. 大数据Hive搭建部署常见报错信息原因
  20. Guacamole会话记录屏幕录像

热门文章

  1. ZEMAX | 照明设计的理论背景与概念
  2. 基于二轴云台目标跟踪系统设计
  3. Linux命令+shell脚本大全:查看文件内容
  4. 运维安全-网络与基础架构图
  5. 2017年山东省CIO智库周年庆暨信息化与工业化融合发展峰会成功召开!
  6. 形式语义学-chapter 3 Attribute Grammars
  7. 颜色科学与计算机测配色 百度云,2004_01颜色科学与计算机测色配色实用技术_11196950.pdf...
  8. 如何用VS2022写C语言(新手入门)
  9. linux input系统的分析笔记(一)
  10. FLIR Systems宣布推出针对皮肤温度升高筛查的改良热像仪