分类 资讯 下的文章

前段时间,和群友偶然聊到了GUI话题,期望可以使用GUI应用来替代前端工具,比如代码规范/校验等,但是查找了相关资料,关于代码管理类的GUI程序非常少(甚至说是一个空白),总结了以下大概有以下几点:

  1. 安全性问题,用户不期望将代码上传到服务端
  2. GUI设计不贴合程序员,提升不了工作效率
  3. 极难扩展,大多数基于代码管理的SASS服务,程序员不可以开发插件去扩展已有功能

如果你是Mac用户,相信你一定知道RayCast,RayCast本质上是一个代替Mac启动器的应用程序,但是由于其强大的插件机制,可以让前端开发者方便地开发各种插件,比如翻译/词典/截图等等,如下图是我安装的一款颜色选择器插件:

RayCast的最佳设计不仅限于此,它还提供了‘快捷键’功能,使其通过预定义好的快捷键,轻松的唤起用户装的指定扩展,例如在Mac中有一款大名鼎鼎的剪贴板软件叫作Paste,而在RayCast中可以直接免费的使用类似的功能,装好指定的插件之后,只需要快捷键就可以唤出剪贴板软件:

另外,有曾接触过运维的同学们应该都知道一个国产的面板应用“宝塔”,虽然这款软件面向的是初级运维人员,但是其免费易用的特性让不少非专业的开发人员也能管理服务器。比如本博客就在使用宝塔软件,是我在上高中时使用宝塔搭建的,已经运行有5,6年了。抛开安全问题,作为一个在当时不懂运维的同学也能深深产生依赖,一直沿用至今,这或许就是GUI的魅力。

读者们对于GUI程序并不陌生,我们在使用的Mac,iphone等操作系统都在时时刻刻的服务用户;同样我们开发者领域也有一些出色的GUI程序,比如Clashx,SourceTree帮助着我们日常开发。

但是有没有一款工具可以改变开发习惯,真正的提高效率,并且兼顾安全。拿前端领域举例,诸如Eslint等工具层出不穷,包管理工具也是五花八门,而且作为一名优秀的开发者无时无刻都在使用工具去减少重复劳动,我们需要有大量的额外工具,例如JSON校验,Mock服务,Local Server,甚至是代码优化,日报/日志工具。

很遗憾,我们目前并没有这样的软件去集成这些五花八门的工具,但是读者们会心里想,我可以唤出浏览器打开一个工具网站就可以解决所有问题,为什么还需要一个GUI软件去做这样的事情呢?这也是本篇文章的重点。

依赖管理

NPM对于前端开发者来说并不陌生,我们想要安装一个库其实要做很多调研,大多需要查看其更新时间,下载量以及issue数量等等;但是当你有一天想要开发Rust程序时,你并不知道你要编写的功能需要安装什么库,你也不知道库和库是如何依赖的,这势必要浪费很多时间,所以如何统一各个语言的依赖管理呢?

这里实现技术也比较简单,无非就是将用户所期望的依赖底层通过命令行的形式进行安装;GUI的重点是如何查找到开发者心仪的依赖,下图是使用NPM搜索PDF关键字的结果:

在默认搜索模式下,在搜索如此具体的关键字时,仍然找不到pdf.js的核心库,如果我是一个对前端领域比较陌生的开发者,我大概率会安装第一个依赖。虽然举得这个例子有点极端并且有一点杠精,但是作为一款跨平台的代码项目管理工具,我们要做的是多个流行语言的包管理工具,对于经常跨语言开发的开发者,你不再需要打开多个标签页并且你也不需要熟悉你安装依赖的命令,只需要2步:

  • 精确地搜索出依赖列表
  • 右键单击鼠标,你的依赖会自动下载

如果在依赖列表中看到了你已安装的依赖,你完全可以双击右键去卸载它,这在我们频繁查找与安装依赖的场景非常有用,并且节约时间。

另外依赖检索列表中,会借鉴NPM的搜索模式,并且在之上结合搜索引擎和ChatGPT,会给你一个智能的依赖推荐,尽管你搜索的关键字和实际的依赖完全不同,你也能快速安装它!并且智能推荐是联网的,你无需GPT4,软件内部会将搜索引擎的结果提供给GPT,让GPT提取可用的依赖信息。

代码体检?咋有点像360?

代码体检的功能是一个仍然在探讨的feature,所以本章和最后完成的功能差距可能会非常大。在一些代码管理的SASS产品中,可能会提供这种功能,并且相对耗时以及昂贵。那么我们如何做这个功能呢?当代码项目被添加到软件中时,会经过一次极快的预处理,会在短时间内得出项目的技术栈和依赖关系,基础的模块数量。

在真正体检时,会得出一份报告,这个报告生成的过程不会产生联网的请求,一切都在本地完成,不依赖GPT。因为大额的代码段落让GPT分析会造成很多Token消耗,这一般人不太用得起(笑哭)。所以这块可能会使用开源的一些检测工具去扫描代码库中可能重复的函数,并且行数特别多的函数,并且注释的数量多少也会影响报告分数;

报告中的检查项可以手动关闭,因为开发者的习惯并不一样,有些人并不习惯编写注释,也有一些项目避免不了行数过多的函数。如果关闭对应的检查项,软件也会跳过这些步骤,势必会更快地生成报告。

工作流

软件会提供一个基础的工作流,将软件项目和快捷键绑定,甚至和其他软件绑定。你可以理解为迷你版的快捷指令,可以用代码编写工作流中的代码,比方说你有一个很重很大的项目,当你有一天重启了电脑或者应用崩溃闪退,导致你不得不重新打开这些应用。那么你就可以自定义一个工作流,当你使用快捷键唤出项目时,你的数据库,Docker,Redis就全部按照你的指示有顺序的跑起来了!

另外当你编写代码时,软件会每隔一段时间去检测你正在编写的模块,找到你经常更改的代码块,并且自动的给出你优化的建议(会有一个提示消息,和Copilot不同)。

利用到GPT的地方会有很多,比如当您把代码提到暂存区时,软件就已经分析其代码并且对代码做总结,生成好commit message,你只需要看到消息之后,点击确定,就可以应用这个提交。

除了显性的工作流之外,在软件内部,不管是体检的预处理分析文件还是你在编写代码时候产生的输出,都有可能会给软件其他功能提供支持。比如日志/日报插件,它会总结你一天的Commit message和具体的工作任务,你可以为其定义一个日报模板,只需要稍加修改,就可以一键发布;发布到哪儿呢?Github/Gitlab/钉钉,只要存在第三方接口的应用,自己编写逻辑,就可以直达!

插件

还记得一个成功的软件核心是什么吗?就是插件,软件可以允许用户使用HTML/JS定义界面,使用Rust/WASM去定义其实现,关于支持Nodejs?这个得看后续啦。

插件分为2类:

  • 核心官方插件(上述的几个功能都属于核心插件)
  • 第三方插件

这是借鉴了Obsidian,对第三方插件必须要提供源代码并且接受监管,如果对于非常流行的第三方插件会合并到官方插件中,但是会继续由社区开源爱好者(和官方开发者)一起推动维护。

有了第三方插件,你脑海中的各种工具,比如JSON校验,Mock服务,Format等等,把那些工具网站都删除了吧~,只需要一个快捷键就可以抵达的插件,无论是性能还是便捷性,本地的软件都会更胜一筹。

总结

对了,软件的名字叫作YourPanel,现已开源:

https://github.com/yourpanel/core (Next.js + Tauri)

文章中的GPT相关功能,都可以关闭,关闭这些GPT调用之后,任何有网络连接的插件在运行时,都会让你知晓;并且在安装插件时,你也可以清楚地看到插件是否基于网络连接!

作为一个本地的代码管理面板软件,我期望你的代码只允许你一个人访问,并且期望用较少的网络连接去完成任务。在初期使用中,我们没有计划引入用户管理,这意味着软件没有登录,我们也看不到你的代码和你的任务。

我们的初心就是:在一个安全环境下,提供开发者便捷服务。

往期回顾

2020年终总结

前言

每年都会写年终总结,目的就是为了3 5年后从博客中找出每一年的年终总结,可以一目了然看到成长,这种感觉是非常幸福的。今年真的收获巨大,因为完成了我职业生涯中很多第一次;或许前几年初入圈子有些许迷茫吧,虽然目前我对我今后几年的发展抱有很大期望,但是如果要达到我的最终目标,付出的时间和精力将会成倍上增。这个目标是什么,后面会有聊到。往年我写年终总结的时候主要是3个核心概念:疫情,心境成长,技术成长,今年我打算多增加几个板块,而且还会传一些图片上来,也算是一份宝贵经历。

先听首歌吧,边听边看

疫情

今年已经临近尾声,没想到西安疫情爆发,仅次于当年的武汉,不知道过年还能不能回家。我现在在家办公中,在家办公很舒服,但是有一点非常不好,我的生物钟全部被打乱了,每天11点睡,自然醒已经早上8点了;如果按照往常工作日,我应该是6点或者不到6点就起床了,但是这样也问题不大,省去了大量通勤时间,尽管我睡眠时间长,但是仍然有学习时间。西安疫情的事件上了很多次热搜,ZF的种种蜜汁操作,还有一码通小程序平均每周崩一次,应急预案也没有这次丢人真的丢大发了。

封城之后,时隔一周看到了政府的救济菜:白萝卜,大白菜,土豆,洋葱,但是不知道下一次送菜是什么时候...

疫情中不得不提的就是,我和几个小伙伴搞了一个核酸地图的小程序,可以清楚的看到自己身边有多少个核酸检测点,上线之后流量暴增,上了纸媒,也上了热搜,接受了采访,这一段经历真的是非常难忘。

IMG_0349.PNG

IMG_0350.PNG

小组开会的随手截图

WechatIMG3527.jpeg

随后当小区居家隔离,西安的临时检测点也就没了,这个程序在一段时间帮助了很多人,虽然现在它没有用处,但是它也曾出现过...

在家办公很爽,尤其是和姐姐们一起住,自己在屋里写代码,饭不用担心,只洗碗就够了哈哈,总之疫情居家生活还是蛮舒服的,舒服是相对的,相信大家也听说过西安有些小伙伴都饿晕了,也有出门买馒头被抓的,也有医院门口因为核酸流产的,也有老父亲因为送医不及时心脏病故,这样对比起来,我真的算幸运的了。


回顾老剧

跑男排面:

IMG_0090.JPG

薛仁贵传奇,真老剧了:

i45iqmbggfuik29.jpeg

怪侠一枝梅:

8c0f65f348ff435f91150b2a760608ee.png

庆余年:

v2-e3c1be7492a0a5b9b2eafc7504467820_1440w.jpeg

越狱1-5:

143892174951732900_a700x398.jpeg


推荐一些最近看的资源

不光有b站视频,还有最近一年写的比较好的文章,还有看的一些书

书籍

视频

文章


未来几年的打算

熟悉我的朋友们都知道,我毕业之后的工资很低很低,曾经只有1.2k只够养活自己,2年过去了,我的薪资差不多是翻了10倍多,听起来感觉很不错,但其实远远达不到我心中的高度,2022年将是我的第三年,在写这篇文章的前一天晚上,和朋友小马哥聊天,改变了一些原先计划:

WX20220105-152052.png

我决定还是2022年安稳过一年吧,因为2022年太多计划了,比如无止境的考试还有驾照,我希望安稳度过,而不是逞一时之快去拿所谓的15 16k的高薪offer。我希望2022年产出极高,水平提升极快,能够奠定相对扎实的基础去实现后面的事情。

我个人非常希望纸贵这个公司是我在中国近几年最后一家公司,这里的小伙伴很nice,等待疫情结束(几年后),如果我的本科以及雅思学习旅途顺利,我大概率将会申请国外的在职研究生,这也算圆了我的学习梦,也算是变相脱离内卷,也希望以后的职业发展将会在其他国家。

2022年非技术目标:

  • 一切考试顺利,英语学习顺利,驾照考试顺利

关于技术

今年说实话没什么开源的作品,大部分都是下半年开始做的,vite的流行意味着前端构建已经变天了,webpack纵然还有用武之地,但是中小新项目基本都会选择vite作为构建工具。vite的出现进一步的带动了esbuild等新兴工具的热度,使用go,rust等语言,借助语言特性可以使编译,检查等工作变得更快更靠谱。

在下半年,来到新团队,就开始搞模板搞新技术栈,开源了一套模板和项目管理工具

WX20220105-180205.png

也写了相关文章介绍了几个模板 基于vite的模板

同样的,写了一个较为完整的命令行工具

1641377080467.jpg

在来新团队时候,也做了一个试验性的东西就是低代码雏形,解决了模板代码生成的问题

WX20220105-180602.png

尽管这个项目不维护了哈哈: https://github.com/seho-code-life/code-template-generation-web

同样的,今年还给antdv以及esbuild-node-tsc(基于esbuild的tsc编译工具)贡献了一部分代码,而且还出了一款教程关于tsrpc(typescript的rpc框架)。

临近年底,也写了一个关于状态管理持久化的库,它是一个支持多平台的持久化插件,马上就发布了

WX20220105-181006.png

https://github.com/1018715564/store-persistedstate-killer

而且在公司内部做了2次分享,第一次是关于前端基建,第二次是关于全栈开发,第二次的经历录制成视频了换一个方式构建全栈应用

顺便说一句,slidev是真好用,用熟悉的语法构建ppt,真的省心不少,建议开发者以后使用slidev写ppt

今年技术这块没有取的突破进展,只是对rust和ts有了新的领悟,以后会在文章体现。明年团队有flutter的工作,到时候我也会分享一些flutter的小文。

2022年技术相关目标:

  • 学习rust和wasm
  • 学习flutter
  • vue3全家桶源码分析
  • 算法
  • ts水平提高
  • js基础和http基础

拜拜咯,2022年终总结见~

前言

某个普通的一天的早晨,水友群的小姐姐和我聊前端架构,因为她们组最近要筹备一些新项目,在做架构的中途出现了很多问题,所以我拿到了她们的架构项目脚手架代码。拿到代码之后我发现深圳那边的前端团队普遍做的很好,有先进的架构思想,也把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 前端

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

npm run dev

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

WX20210706-072702@2x.png

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

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

|- frontend -------------------------- 前端项目
    |- src
        |- shared -------------------- 前后端共享代码(只读)
            |- protocols
        |- index.ts

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

// PtlAddPost.ts

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,我们运行

npm run api 新协议生成一个新的api

在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这个测试框架。
// /test/api/**.test.ts

import { HttpClient } from "tsrpc";
import { serviceProto } from "../../src/shared/protocols/serviceProto";

// 1. EXECUTE `npm run dev` TO START A LOCAL DEV SERVER
// 2. EXECUTE `npm test` TO START UNIT TEST

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 dev

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

1625806333932.jpg

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

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

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

npm run sync

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

// frontend/src/index.ts

import { HttpClient } from "tsrpc-browser";
import { serviceProto } from "./shared/protocols/serviceProto";

// This is a demo code file
// Feel free to modify or clear it

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

client.callApi("AddTest", {
  newPost: {
    name: "hello, seho"
  },
});

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

1626042788250.jpg

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

完善我们的程序

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

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

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

npm install mongodb -s

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

  • 写一个数据库的表模型,名为Post.ts
// shared/protocols/models/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
// shared/protocols/models/dbItems/DbPost.ts

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类型。

  • 数据库相关配置
// 配置文件
// shared/protocols/models/BackConfig.ts (models文件夹是新建的)
export const BackConfig = {
  // 替换数据库url,数据库名test
  mongoDb: "mongodb://localhost:27017/test",
};
  • 定义数据库初始化类
// 全局一个类,里面写了初始化数据库的内容
// shared/protocols/models/Global.ts

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
// backend/index.ts 添加如下内容

import { Global } from "../src/shared/protocols/models/Global";

async function main() {
    // Auto implement APIs
    await server.autoImplementApi(path.resolve(__dirname, 'api'));
    // TODO:在这里初始化了数据库
    await Global.init(server.logger);
    await server.start();
};

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

npm run dev

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

WX20210719-223641@2x.png

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

// shared/protocols/PtlAddPost.ts

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 api
// api/ApiAddPost.ts

import { 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的全栈应用在应对上传图片这种业务的时候简直就像是小儿科,我们可以用一个例子来证明。

// 定义一个协议在后端,PtlUpload.ts
export interface ReqUpload {
    fileName: string,
    fileData: Uint8Array
}

export interface ResUpload {
    url: string;
}

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

npm run proto
npm run api
// api实现, 先提前把uploads文件夹建立好,或者使用mkdir方法
import { 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下的协议

npm run sync

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

<input type="file" id="fileInput">
// index.ts
import { HttpClient } from "tsrpc-browser";
import { serviceProto } from "./shared/protocols/serviceProto";

// Create Client
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);
  });
}
// 同步到前端
npm run sync

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

1626218954109.jpg

1626218957442.jpg

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

向后兼容http(json)和WebSocket

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

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

1626391401179.jpg

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

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

websocket实时服务-tsrpc

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

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

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

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

// shared/protocols/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 () => {
  // 与后端webscoket服务建立连接
  let result = await ws.connect();
};

init();

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

// shared/protocols/PtlSend.ts

export interface ReqSend {
  content: string;
}

export interface ResSend {
  time: Date;
}

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

npm run proto
npm run api

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

import { ApiCall } from "tsrpc";
// 这里引入banckend/index.ts 导出的server
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,
  });
}

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

npm run sync

我们进一步改写前端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.callApi
    ws.callApi("Send", {
      content: "hello websocket",
    });
  }
};

init();

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

WX20210719-073153@2x.png

WX20210719-073210@2x.png

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

多平台

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

结语

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

剑指题解


互联网人题解神器,神器在手,offer我有

随手刷题 | 无广告绿色免费 | 个性推荐 | 每日自动推送 | 多维度丰富领域知识

项目的起源是一个 github 仓库,名为前端知识每日 3+1,正是这个优秀的题库项目才有了我们团队开发聚合题库的想法。在我们国内程序员圈子,大多数小伙伴为了更好的技术发展和 offer,都通过训练题来提高自己的眼界,其中不乏包括算法/业务/软技能的题目,但是因为我们的环境和侧重点不一样,所以我们接触的项目和题目都不一样,而市面上很少有一个项目能做到,涵盖不同圈子的聚合题库。 《剑指题解》项目就是一个优秀的互联网行业聚合题库神器。


开源免费不易,请关注我们的公众号以及开源仓库

沈昊Seho/InternetQuestionBank<br/>
Github 点 star, 拉取代码时请选择最新分支
Dcloud 插件市场支持我们

目录

特点

  1. 汇聚优秀的开源题目,以技术社区为主线展开的刷题 APP
  2. 根据用户的喜好,去推送相关的最新题目,不会再刷到 “牛头不对马嘴”无用的题目
  3. 我们绑定了服务号,每日定时推送给用户最新的题目,让你能在每天清晨就开启活力新一天
  4. 刷题 APP 无广告,无硬性推广,一切盈利皆在赞助/官网流量
  5. 我们还有很多有意思的待开发 feature,比如大佬带萌新模式,还将开启校园/企业的通道,让每一个学生/员工都能每时每刻提高自己
  6. 剑指题解开源项目,从原型图到小程序,UI 图,UML,API 文档,第三方 API 对接说明书,使用说明都将免费开源,欢迎各位同僚二开,让这个行业变得更好,我们将用心地辅助你们解决在技术上的难题。

技术栈

关于技术栈的选型可以参考我们的语雀文档
周边物料的开源,还需要感谢 mockPlus 以及语雀:

<img class="imgIcon" src="http://static.yinzhuoei.com/typecho/2021/01/17/69752764374919.png" width="10%">


<img class="imgIcon" src="http://static.yinzhuoei.com/typecho/2021/01/17/697832282748067.jpg" width="20%"></img>

UML

由于此部分在开发新 feature 的工作中可能会进行变更或者本就有实质性的错误,欢迎向我们反馈错误,我们将感激不尽。

类图

拉取任务

更多

更多相关资料请移步
out/doc或者语雀-UML

设计概览



设计源文件请移步:点击查看-为了您的体验,请务必下载最新版本的设计图
原型图在线预览: 点击查看, 如果失效请联系我们

API

《剑指题解》团队在 API 层面,开源的内容非常有限制,因为涉及到多位合作伙伴的题库授权,所以如果您要使用聚合题库 API 请联系我们商议,如果您是学习者,那么在语雀中的 API 文档会帮助到您,在这里我们将简单介绍。

uniapp 的云函数开发,我们是第一次接触,所以在此之前我们做了很多调研,在下文也提到了关于 explain.js 这个框架的作用,我们也是非常感谢 uniapp 开源社区有这样创作质量的第一批的 unicloud 框架开发者,在咨询了官方人员之后,我们了解到如果要使用 ts 来开发,需要把 ts 编译成 js 来放在云函数目录,所以我们选择了以下的技术栈,开发者可以很爽的使用 ts 来开发 unicloud 云函数:

  • esbuild-node-tsc
  • nodemon

esbuild-node-tsc 这个工具是基于 esbuild 的,利用其特性,我们可以比 ts 官方提出的 tsc 编译工具更快,快几十倍到上百倍,这也是得益于 esbuild 的使用 go 语言直接编译的特性。

nodemon 可以帮助我们监听目录文件,一旦改变就执行打包 ts 命令直接将编译好的 js 放在指定目录
所以我们把 explain.js 的配置也相应的改变,我们在 services 目录中存储的是我们 ts 文件,在 dist 中存储的是我们编译好的 js 文件,而 dist 目录就是 explain 需要的:

config.init({
  baseDir: __dirname,
  serviceDir: "/dist/",
});

需要注意的事情是:
由于云函数大小限制,我们需要把相关依赖全部安装到全局,包括 typescript

读写职责分离

读写职责分离模式(CQRS)是一种把查询(Queries) 数据和和更新(Commands) 数据通过使用各自独立的接口分开的模式。
Uniapp 的 Unicloud 很好,其中的 jql 也是大开眼界,确实统一了前端开发操作数据库的体验,让我们前端开发操作数据库非常友好,但是我们使用 CQRS 将这些原本封装的很好的 API 不予以使用。

  • 前端使用 jql 去读取想要的数据
  • 写入操作就交给云函数去执行

如图,前端读取一个列表数据

const db = uniCloud.database();
const databaseName = "testInit";
// 获取测试数据列表
export function getTestList() {
  return new Promise((resolve) => {
    db.collection(databaseName)
      .get()
      .then((res) => {
        // res 为数据库查询结果
        resolve(res);
      })
      .catch((err) => {
        // err.message 错误信息
        // err.code 错误码
      });
  });
}

我们将这样的文件抽出了一个一个查询模块,每一个模块对应了一个数据表(这里是 testInit)封装在了 API 这个文件夹中。

Vue 页面将这样去调用,这样我们在 API 查询层将做好数据的处理,确保 Vue 拿到的是可以直接渲染的干净数据。

import { getTestList } from "../../api/test";
const data = getTestList();

我们写入操作将通过云函数直接调用,唯一设计不同的地方在于,我们将云函数这个概念改变了,应该是模块化,我们把一个一个函数变成了模块。

在我们的初步技术调研过程中,由于 Uni 官方并没有提供给用户云函数开发框架,但是我们在插件市场中找到了一款名为 explain 的开发框架,它可以迅速的帮助我们实现 restapi 风格的单路由云函数,这款框架我们不多做介绍,文档在这里:explain.js 快速开发 uni 云函数的框架

// 注册用户根据手机号
  addUserByPhone() {
    return handleMustRequireParam(
        [{
            key: "username",
            value: "用户名",
          },
          {
            key: "password",
            value: "密码",
          },
        ],
        this.event.params
      )
      .then(async () => {
        const {
          username,
          password
        } = this.event.params;
        if (!/^1[3456789]\d{9}$/.test(username)) {
          return appErrorMessage("用户名格式不正确");
        } else if (password === "" || password.length < 6) {
          return appErrorMessage("密码格式不正确");
        } else {
          // 校验手机号
          return await uniID.register({
            username,
            password,
          });
        }
      })
      .catch((err) => {
        return err;
      });
  }

那么我们如果要调用 testPrint 这个模块中的增加操作

uniCloud.callFunction({
  name: "application",
  data: {
    route: "api/user",
    method: "POST",
    params: {
      username: "18291563764",
      password: "sas",
    },
  },
});

贡献者

感谢为《剑指题解》这个优秀项目贡献自己一份力的小伙伴们:

如果您想加入到我们的贡献者队列中,请联系我们,这里还有相关贡献者的介绍,希望对您有帮助

核心维护者

相关开源物料

题库合作伙伴

QQ 群和钉钉群欢迎大家加入


许可

Apache License © 剑指题解
如果您在其他有关我们的文档见到了与之不符合的协议内容,请联系我们,这可能是我们的历史遗留的代码问题。