为什么我要写这样的一个模板(网站)
我曾经是做 API 请求的,经常要模拟某些请求(协议复现)。所以写过比较多的 api 请求代码,在此期间尝试编写过许多代码方式和软件形态。但都不令我满意,不是过于繁琐,就是开发太慢,都达不到我想要的预期。尤其是开发体验,可以说是苦不堪言。
就在前段时间接触了 SSR 框架(Nuxt3)与 Serverless Function,并用其写了一些项目,如 api-service 。而某了个羊刷次数的网站的实现,则让我意外发现 这貌似就是我理想中的的协议复现最佳实现?于是我立马开启了 VSCode,将我的这一想法用代码的方式来实现出来,在经历了两周几乎不间断的开发,最终达到了我的预期效果!
在 模拟请求|协议复现方案 这篇文章中我对协议复现的一些方案总结,而这篇就是对 SSR 框架方案的一个具体实践。
技术栈
这个模板基于Nuxt3开发的,该框架拥有全栈开发能力(即全栈框架),并有诸多模块,即装即用。同时由于采用Serverless Function
方式来定义 api 接口,可以轻易地部署在自有服务器或Vercel, Netlify这样的平台上。由于要做到敏捷开发,该模板集成了Naive UI 组件库,组件库的质量足够胜任常规前端业务开发。此外还封装了一些我个人的所用到的工具库以提高开发效率。
为此我给这个模板起名 Protocol,即协议,也可以认为是礼仪。一个用于快速复现请求协议的 Web 开发模板。
废话不多数,就正式来介绍下 Protocol。
目录结构
protocol
├── assets # 前端静态资源文件
├── components # 组件
├── composables # 组合式API
├── content # content 模块
│ ├── changelog.md # 更新日志
│ └── help.md # 帮助说明
├── data # 持久化数据
│ └── db
├── layouts # 布局
├── nuxt.config.ts # nuxt 配置文件
├── package.json # 依赖包
├── pages # 页面
├── plugins # 插件
├── public # 服务端静态资源文件
│ └── logo.svg
├── server # 服务端文件
│ ├── api # 后端接口
│ └── protocol # 协议请求逻辑代理
├── stores # pinia 状态管理
│ └── user.ts # 用户状态
├── types # 类型定义
│ └── user.d.ts # 用户类型声明文件
├── ecosystem.config.js # pm2 配置文件
├── nitro.config.ts # nitro 配置文件
├── app.config.ts # 前端配置文件
└── app.vue # 入口文件
从这个项目的目录结构中其实就可以看出,本项目是集成了全栈能力,并且使用 Vue 与 Node 来编写前端与后端,并不会产生前后端分离的分割感,只需要打开一个项目即可开始工作。这得益于Nuxt3 与 Nitro。
由于是基于 Nuxt3 开发的,所以使用该项目是需要一些 SSR 开发经验。如果你还没有接触 SSR,可以根据你熟悉的前端框架选择对应的 SSR 框架来尝试体验 一番。都要 2023 年了,不会还有前端程序员没用过 SSR 框架吧。
基本功能
全栈开发
这里我不想过多介绍 Nuxt3 的基本功能与使用,在我的一个 基于 Nuxt3 的 API 接口服务网站 的项目中,有简单介绍过 Nuxt3,有兴趣可以去看看。
这里你只需要知道 Nuxt3 具有全栈开发的能力,如果你想,完成可以基于 Nuxt3 这个技术栈来实现 Web 开发的前端后端工作。
类型提示
首先,最重要的就是类型提示,对于大多数 api 请求而言,类型往往常被忽略。这就导致不知道这个请求的提交参数、响应结果有什么数据字段。举个例子
这是一个 post 请求用于实现登录的,但是这个响应数据 data 没有任何具体提示(这里的提示是 vscode 记录用户常输入的提示),这时候如果一旦拼接错误,就会导致某个数据没拿到,从而诱发 bug。同理提交的请求体 body 不做约束,万一这个请求还有验证码 code 参数, 但是我没写上,那请求就会失败,这是就需要通过调试输出,甚至需要抓包比对原始数据包才能得知。
最主要的是没有类型约束的情况下,非常容易出现出现访问的对象属性不存在,做 js 开发的肯定经常遇到如下错误提示。
Uncaught TypeError: Cannot read properties of undefined (reading 'data')
有太多很多时候就是因为没有类型,无形间诱发 bug。就极易造成开发疲惫,不愿维护代码,这也是很多做 api 接口都常常忽视的一点。包括我之前也是同样如此。
对于 js 而言,上述情况自然是解决不了,但这种场景对于 ts 来说在适合不过了。所以 Protocol 自然是集成了 ts,并且有良好的类型提示。下面展示几张开发时的截图就能体会到,当然你前提是得会 ts 或者看的懂 ts。
上面的类型提示演示代码仅仅作为体现类型的好处,将类型定义(interface,type 等)和核心逻辑都在同一个文件自然不好,容易造成代码冗余。实际开发中,更多使用命名空间,将类型写到 ts 声明文件.d.ts 中。比如将上面的改写后如下
就在我写这篇文章做代码演示的时候,又发生了拼写错误,如下图。由于使用 ts 类型与 eslint,所以在开发时的问题我就能立马发现,而不是到了运行时才提示错误。
有了类型提示能非常有效的避免上述问题。同时 ts 并不像 java 那样的强类型语言,你完全可以选择是否编写 ts 的类型(type 或 interfere),这由你决定,你乐意都可以将 typescript 写成 anyscript,因为确实有些人确实不喜欢写类型。
ts 的类型提示仅是其次,此外还配置了 eslint 对代码检查,让代码在 2 个空格缩进,无分号,单引号等代码规范下。保证代码质量,而不会出现这边一个分号,那边来个双引号的情况。
工具库
要想在实际项目中使用,还需要做很多功课,例如数据格式转换,编码,加解密,cookie 存储,IP 代理等等。这段时间也特此对常用工具封装成 npm 包,也就是 @kuizuo/http 与 @kuizuo/utils。
大部分的代码我都会采用最新的 ECMAScript 标准来编写,目的也是为了简化代码,减少不必要的负担。
数据库
既然是全栈框架,那么必然少不了数据库的存取,nitro 自然是提供了数据存储选择,即 unjs/unstorage。使用特别简单:
await useStorage().setItem('test:foo', { hello: 'world' })
await useStorage().getItem('test:foo')
不指定则使用内存,当然了想要持久化配置,nitro 也提供了相关配置
// nitro.config.ts
import { defineNitroConfig } from 'nitropack'
export default defineNitroConfig({
storage: {
redis: {
driver: 'redis',
/* redis connector options */
},
db: {
driver: 'fs',
base: './data/db',
},
},
})
并根据不同前缀(根据 nitro.config.ts 中的 storage 对象的属性)存储在不同存储位置,如
// 存内存缓存中
await useStorage().setItem('cache:foo', { hello: 'world' })
await useStorage().getItem('cache:foo')
// 存db中
await useStorage().setItem('db:foo', { hello: 'world' })
await useStorage().getItem('db:foo')
// 存redis中
await useStorage().setItem('redis:foo', { hello: 'world' })
await useStorage().getItem('redis:foo')
从目前来看,unjs/unstorage并没有提供 sql 数据库的方案。不过对于这类项目而言,似乎也没有上 sql 数据库的必要,文件和 redis 就足以了。如果需要也可以自定义 drivers。
由于 Vercel 是不支持文件读写的,所以想要文件方式数据存储功能就行不通,需要更换存储方案,比如远程 redis 数据库。
如果是部署到自由的服务器(通常是 Linux 系统),则还需要分配相应的读写权限。