用Graphql+Typescript+Hasura+Vue3+TSRPC构建全栈应用
- 从RestApi到GraphQL
- 如何构建GraphQL
- Hasura是什么
- CQRS
- WEB全栈框架的众神崛起:Nuxt/Next/TSRPC
- 演示在Vue3下与TSRPC,Hasura的畅快开发体验
- 反思: 为现有架构带来的不是新技术栈,而是新思想
- 小结
博客真的是个好东西,自己的博客中就是一方自己的小天地,这里都是技术圈子的朋友,虽然建立以来可能只有几十万次访问量,但是平均下来真的没多少人看,所以我经常在博客中说一些心里话。
事实上从我上初中家里的迎来第一台电脑开始,接触游戏很多,我承认游戏确实影响到了我,而且也间接导致了我中考失败。但是初中生涯中除了游戏还有编程,我建立了第一个属于自己的网站,也算是正式开启了我的创业小路,结交到了很多比我年长几岁的技术圈的人,可能现在他们都不从事这个行业了。由于网站业务繁杂加上本身酷爱游戏,学习自然是跟不上去了,导致我考了一个很差的高中。但是我意外迎来了事业第二春(哈哈哈哈)
所以我总说,人脉是最重要的,我利用了之前创建的人脉,高中期间涉及了3个创业项目,而且也真正的对好代码更加神往,想要往更高层次努力进军。然后提前毕业就来西安正式开始求学之旅,这2年时间非常感谢学校的何老师和乐老师教我后端技术,而且还遇到了一群好基友,我的好舍友们现在仍然活跃在各大互联网公司的不同岗位或者作为独立开发者,真的都特别厉害。
21岁,明年就迎来了我的第三个年头,可能是我眼光狭隘,照我目前的这个程度每天卷,应该在三年经验开发中算是还不错的。但是干这一行真的不能掉以轻心,你真的需要每天去死学,把每一天当成考研末日来过,你一年才会有所收获,才能弯道超车。在今年有一段时间内,我是真的学的昏天黑地。有一段时间我妈来西安,我早上5点多就起床准备开始学了,但是我妈见到就勒令我回去睡觉...
不管是80,90,00后只要你在互联网行业,你就要把每一天当成你被裁的最后一天度过,你要去学习,你要去提升,不管是技术维度,还是产品维度,运营维度,甚至是沟通维度,你要每一天有所收获,而不是每天一回家(没孩子的情况下)就躺在床上开始TIMI,久而久之你真的会废的。
这不是 “内卷”,很多人一看到我的状态就可能会说:“你别学了,躺平不好么,每天看代码,你不会吐么?”
我其实想说,你要是真热爱这一行,你不会吐的;换句话说,我也见过30多岁的资历非常丰富的工程师,对新技术的渴望,我就很佩服这样的人,因为他的斗志没有被一天的工作消磨掉,纵使他有孩子家庭,他也要去学习,这是态度,这是热爱。
说到谈恋爱,哈哈哈哈哈,今年我奶奶催过我,找对象这个事情,我真的不想找,但是又很渴望,哈哈。因为我可能不适合谈恋爱,我有自己的每天计划,我其实挺害怕别人突然打乱我的计划,可能是恐恋吧。我的二姐今年和我10年好朋友在一起了,说实话啊,真的挺甜的,说到这里我又留下了单身的泪水。一切都顺其自然吧,插一句,我大姐还单身,我们家都不是伏弟魔,兄弟们可放心,我是著名伏姐魔嘿嘿。
反正快过年了,每一年的年终总结都会有,技术方面的事情,咱们年终再聊。
放几张我二姐拍的照片吧,我个人不喜欢发照片
简历下载: 左晓倩2021.11-9,.产品.doc
主要优势就是善于沟通,高保真原型,而且很全能,技术出身前后端都懂,运营也懂做过游戏的策划,测试也曾经在中国移动还有在线途游做过(北京),产品岗位虽然经验不多,但是也在西安本地赛格做过,并且主导了赛格的一个大型活动,由于业务发展方向不符所以离职现在出来继续找产品的工作。
部分原型我发一下,有兴趣可以看看:
联系方式微信号: npm_install_s (我) heywego999 (她)
最近的工作有涉及到ssr,所以这篇文章算是一个总结,并且对还在beta阶段的nuxt3做一个浅析。前段时间有一个蛮火的视频,关于rollup作者rich的一段演讲,在演讲里面rich梳理了ssr和csr,并且讲述了痛点,和提出新的概念“transition app”,如果你有兴趣可以看看这个视频
在文章开始前,我来简单介绍一下"spa", "mpa", "ssr", "csr"......这些个名词的意义。如果你是做web前端开发的,这几个词可能伴随着你的工作生涯很久很久了,相关文章互联网上多如牛毛,如果你对这些概念比较模糊甚至压根不知道,那么别关闭网页,我希望这篇文章能够拯救你。
MPA称之为“多页应用”, 那么什么是多页应用呢?字面意思其实就是有多个页面的应用就是多页应用。从技术手段上来讲,你可以这么粗略地理解。SPA,MPA不同点太多了,而且各有利弊。
MPA应用你需要单独维护多个html页面,而且我们每加载/切换一次页面,都需要加载一整个页面。但是它对于seo特别友好,因为我们可以给每一个html页面设置不同的meta等信息,从而达到更好的收录效果;所以MPA多出现在大型的电商/新闻网站等。
不同于MPA,SPA可以使得我们通过ajax或者其他技术动态的更改某一个区域的内容而不需要重新加载页面,包括切换页面也不会重新加载整个html,它对状态的留存做的很好,而且在移动端表现特别优异(因为在以前流量是很珍贵的,可以以最小的损失切换页面,无论是用户体验还是成本相较于MPA都是极大的改善)
在我们web较早的时候,开发者喜欢使用jsp或者其他模板渲染引擎来构造一个应用。我们一般称之为SSR(服务端渲染) 它的大致架构是如下这个样子
用户发起一个请求抵达后端服务器后:
你可能也发现了,在SSR服务端渲染中,前端负责的东西太过单薄,说得好听叫交互,难听点就是“点击事件工程师”。所以老一辈的后端基本人人都会前端,js的水平高的一抓一大把。随着使用SSR渲染页面的应用越来越多,弊端也出现了:
CSR(客户端渲染)大致是以下的架构:
CSR架构更贴近我们的现代前端开发,我们一般使用VUE, REACT这一类的前端视图框架时,都是默认CSR体系的。大致的流程是下面这样子的:
可以发现,使用CSR进行开发,会有几个明显的缺点
因为从前端服务器获取的html最开始是空html,这非常不利于seo,很多搜索引擎的老版本蜘蛛会直接爬页面,不会等待js加载完,所以会直接爬出来一个空页面。尽管现在的百度,谷歌等搜索引擎的爬虫能力很强,能够部分支持CSR SPA页面,SEO效果虽然可以其他方式弥补 (比如加入meta标签等等); 但是我们使用SSR完全不用担心,因为获得的html页面是一个完整的,可以直接渲染的。
关于白屏,由于CSR从HTML构建完成到JS渲染页面完成(但还没呈现页面)这一段过程中,是处于一个白屏的时间,用户体验很不好,反之使用SSR获得HTML之后只需要直接构建DOM就可以了。
同样的,我们使用SSR还有不一样的缺点:
Vite SSR虽然现在是一个实验性质,不能用于生产环境。但是我们可以使用Vite做一个ssr的demo,帮助我们理解SSR的构建,理解之后我们再来引入"Nuxt", "同构"等概念。Vite里面为SSR提供了很多支持,所以我们要开发一个demo,会非常非常简单,你也可以参考这篇官网文档
我们首先需要更改index.html的内容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"><!--ssr-outlet--></div>
<script type="module" src="/src/entry-client"></script>
</body>
</html>
可以看到我们在app的div里写了一段注释,到时候我们渲染完之后的html将会replace这个注释。
然后需要在根目录新建一个server.mjs,作为我们的服务入口,用express作为一个例子:
// server.mjs
import { readFileSync } from 'fs'
import { resolve } from 'path'
import express from 'express'
import { createServer as createViteServer } from 'vite'
const createServer = async () => {
const app = express()
const vite = await createViteServer({
server: { middlewareMode: 'ssr' }
})
// 使用vite这个中间件
app.use(vite.middlewares)
app.use('*', async (req, res) => {
try {
// 服务 index.html - 下面我们来处理这个问题
const url = req.originalUrl
// 读取根目录的模板
let template = readFileSync(resolve('index.html'), 'utf-8')
// 转换index.html 使其hmr有效
template = await vite.transformIndexHtml(url, template)
// 加载entry-server这个文件中的render方法
const { render } = await vite.ssrLoadModule('./src/entry-server.js')
// 根据url进行渲染
const appHtml = await render(url)
// 替换注释为准备好的html
const html = template.replace(`<!--ssr-outlet-->`, appHtml)
res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
} catch (error) {
vite.ssrFixStacktrace(e)
console.error(e)
res.status(500).end(e.message)
}
})
app.listen(3000)
}
createServer()
我们的main.js也需要更改
// src/main.js
import App from './App.vue'
import Router from './router'
import { createSSRApp } from 'vue'
export function createApp() {
const app = createSSRApp(App)
app.use(Router)
return { app, router: Router }
}
我们在main.js中,从vue导出createSSRApp函数,并且使用router,并且返回一个对象,这个对象之后将会被entry-server引用。
那么router也和我们传统的csr应用不太一样,我们根据env判断,传入了不同的路由类型:
// src/router/router.js
import { createRouter, createWebHistory, createMemoryHistory } from 'vue-router'
const Router = createRouter({
history: import.meta.env.SSR ? createMemoryHistory() : createWebHistory(),
routes: [
{
name: 'index',
path: '/index',
component: () => import('../pages/index.vue')
}
]
})
export default Router
然后我们需要在src中新建 entry-client.js(会被index.html引入) 以及 entry-server.js
// src/entry-client.js
import { createApp } from './main'
const { app, router } = createApp()
router.isReady().then(() => {
app.mount('#app')
})
// src/entry-server.js
import { createApp } from './main'
import { renderToString } from 'vue/server-renderer'
export const render = async (url) => {
try {
const { app, router } = createApp()
// url跳转一下路径
router.push(url)
// 路由准备好
await router.isReady()
const ctx = {}
// 返回一个html
const html = await renderToString(app, ctx)
return html
} catch (error) {
// console.log(error)
}
}
到此为止我们可以在本地启动一个服务器,并且可以将我们的页面以ssr的形式渲染到浏览器中了,由于我们的demo代码都是esm,所以我们使用node执行,必须要写成mjs的后缀。
node server.mjs
启动服务器之后,访问/index这个路由,你就能看到我们的页面了
如果你的node版本不支持mjs,请先升级...
ssr示例项目:
读到这里,你或许已经对ssr的流程有一个粗略的了解了;那么这一part的三个例子会加深你对ssr的理解,就是ssr常常说的喝水,脱水,注水
。
我们ssr在服务端构造页面时,数据是从数据源流下
,使得我们页面数据得到填充,这个过程就叫做喝水
(render & beforeRender)
喝水的过程就是在服务端渲染页面做的事情,就好比下面这个图:
饱满的水气球代表了一个健壮的网页
我们实现ssr需要直出html,所以需要把结构以及数据进行脱水
(如图)
然后到了客户端,我们需要ssr应用重新焕活,就要让原本脱水了的state,prop等等数据恢复到原来的生机,并且重新render组件,这个过程就叫做注水
SSG这种渲染模式采取了CSR和SSR的共同优点,它不需要开发者介入服务器操作,开发者只需要准备cdn或者其他静态网页托管服务器,prerender出静态资源这一步将在构建时就已经做了,呈现在用户眼前的虽然不是实时变更的,但是也保留了CSR和SSR的精髓,一定程度上有了平衡。但是因为prerender的缘故,它和SSR的大致工作方式会相似一点。
同构说白了,就是将我们的前端代码,既能在客户端运行,也能在服务端运行,而且还能保持上下文的状态,我们在上面的改造例子已经实现了同一份代码在2个端的运行,但是并没有实现状态的同步,比如我们在nuxt中,使用asyncData
这类钩子一样,能在服务端运行而且返回的data可以和客户端共享。
// 在nuxt2中我们可以这样
async asyncData({ store, $axios, $oss }) {
return {
hello: "world"
}
}
<div>{{hello}}</div>
我们现在需要改造我们的demo:
// index.vue
// 新增一个option
asyncData() {
return {
hello: 'message'
}
}
其次在server端将asyncData返回的对象和其他页面html一起进行脱水:
// entry-
import { createApp } from './main'
import { renderToString } from 'vue/server-renderer'
export const render = async (url) => {
try {
const { app, router } = createApp()
router.push(url)
await router.isReady()
let data = {}
// 命中路由组件,且执行asyncData这个函数
if (router.currentRoute.value.matched[0].components.default.asyncData) {
const asyncFunc = router.currentRoute.value.matched[0].components.default.asyncData
data = asyncFunc.call()
}
const html = await renderToString(app)
return { html, data }
} catch (error) {
// console.log(error)
}
}
// 我们的server.mjs也需要变更一下
// server.mjs
app.use('*', async (req, res) => {
try {
// 服务 index.html - 下面我们来处理这个问题
const url = req.originalUrl
let template = readFileSync(resolve('index.html'), 'utf-8')
template = await vite.transformIndexHtml(url, template)
const { render } = await vite.ssrLoadModule('./src/entry-server.js')
const { html: appHtml, data } = await render(url)
// 拼接标签,把data序列化插入到文档中
const html = template.replace(`<!--ssr-outlet-->`, `${appHtml}<script>window.__data__=${JSON.stringify(data)}</script>`)
res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
} catch (error) {
vite.ssrFixStacktrace(e)
console.error(e)
res.status(500).end(e.message)
}
})
可以看到我们将data序列化到了window对象中了,接下来我们需要在client端注水的时候,把新data进行替换
// entry-client.js
router.isReady().then(() => {
const component = router.currentRoute.value.matched[0].components.default
let _data = {}
// 判断是否是函数
if (typeof component.data === 'function') {
_data = component.data.call()
}
// 判断是否有脱水的data
if (window.__data__) {
_data = {
..._data,
...window.__data__
}
}
component.data = () => _data
app.mount('#app')
})
这个时候我们已经成功的看到index.vue中能够正确的在template中打印hello
这个字段了
到这里,你就可以举一反三,使用vuex也可以进行同步数据,都是把data序列化到window中保存,然后在client挂载前重新commit到store里面就可以了。
是时候引入nuxt了,我们如果使用nuxt将会更容易的完成ssr需求,这一部分不会教大家怎么写nuxt,毕竟都是框架,都很简单。我会和大家梳理一下nuxt2和nuxt3的变化,如果你用过nuxt2,那么这一部分内容你可能会非常感兴趣。写这篇文章的时候,nuxt3并没有release,所以到时候release后会考虑再出一篇总结。
简单翻阅了一下文档,和大家分享一下,在nuxt3中的新服务端引擎 Nitro Engine
, nuxt2中服务端核心使用的是connect.js,而nuxt3使用的是nuxt团队自研的h3框架,特点就是具有很强的可移植性,而且非常轻量级,并且还支持connect编写的中间件。也就是说nuxt3基于h3编写的server端,可以无缝地移植到支持js运行环境的地方,比如说woker,serverless...
我们先试试,开发一个在nuxt3中使用的api
// server/api/hello.ts
export default (req, res) => {
return 'Hello World'
}
同样,支持异步,也支持nodejs风格的调用
export default async (req, res) => {
res.statusCode = 200
res.end('hello world')
}
nuxt3也支持在同一个server文件夹中编写middleware,而且是自动导入的。nuxt3这次的更新,属于是把文件系统玩出花了,不光plugins不需要重复声明了(nuxt2要在config重复声明),而且components,composables(nuxt3新增的文件夹,可以存放公共hook)... 都可以支持自动导入。
试想一下,如今写nuxt3应用,搭配vue3 composition api,将会使开发体验上升好几个台阶。
文末,我们可以试试打包一个nuxt应用到cloudflare 作为woker运行是什么效果?我们在build之后会发现output文件夹很简洁(不像nuxt2迁移部署都很令人头疼)
我们不仅可以在最后的demo中看到页面,也可以访问 api/hello 这个路由查看刚刚我们在nuxt中定义的api
又是水文一篇,希望以后可以出一些高质量的总结文章,希望这篇文章所讲述的前端常见的渲染模式,你能够知道,并且知道原理,这也就是本文最终的目标。框架会不会都没关系,我们要洞悉一切技术背后的真相,再去研究框架不是手到擒来么?
我们在开发中可能会经常遇到以下几个事情:
社区中有大量的api文档工具,支持mock,单测的数不胜数。但是有生态,而且有开放api的开源文档工具其实寥寥无几。yapi是开源的国产文档管理工具,我们可以用yapi的开放api去做一系列拓展,在市面上就有许多浏览器插件/vscode插件,而且它支持私有部署。我们今天就使用我们之前文档提到过的工程架构模板,去构建一个todolist应用,我们的api则是使用midway和serverless快速开发的。
如果你还不太了解我们之前使用的“工程架构模板”,那你可以看一下这篇文章
准备了4个api供我们测试,它们都是隶属于List模块,api的源码在这里,我们需要在yapi平台上把这些api进行登记。
我们新建了一个测试项目,并且新建了一个list分类,同时新增了4个api:
1. /v1/list get
2. /v1/list post
3. /v1/list delete
4. /v1/list put
我们list模块类型声明如下:
export interface List {
id: string;
title: string;
content: string;
}
export type AddItem = Omit<List, 'id'>;
export type UpdateItem = List;
比如说新增的api,在post方法下,我们需要传递title,content,返回一个新增成功的id,这是一个很常见的业务场景,我们在yapi定义一下。
这个时候就需要体现出社区的强大了,在社区中有类似yapi2typescript的浏览器插件,所以我就fork一份,再插件之上重新修改了interface名等&新增了controller,modle层代码片段,如果你需要安装这个插件,你可以到这个仓库查看源码
我们就可以在页面下面看到,如下的类型提示:
然后我们就可以copy到我们的工程中进行使用,这个时候你可能会问,yapi社区里有很多to typescript的方案,比如说vscode或者命令行工具,为什么要使用浏览器插件?出于以下几点原因,我使用浏览器插件:
我们通过vscode命令model-init-type
去生成一个list.d.ts文件内容, 然后把yapi的类型提示进行粘贴:
namespace TListApiModel {
interface ReqAddList {
/** 列表标题 */
title: string;
/** 列表内容 */
content: string;
}
interface ResAddList {
/** 数据id */
id: string;
}
}
export default TListApiModel;
然后我们就使用命令快速实现一下,list模块的model层和controller层:
// model api
import useRequest from '../../hook/useRequest';
import TListApiModel from '../../../typings/model/api/list';
export default class ListApiModel {
addList(params: TListApiModel.ReqAddList): Promise<TListApiModel.ResAddList> {
return useRequest({
url: '/v1/list',
method: 'POST',
data: params
});
}
}
// controller
import TListApiModel from '../../typings/model/api/list';
import ListApiModel from '../model/api/list';
export default class ListController {
private apiModel: ListApiModel;
constructor() {
this.apiModel = new ListApiModel();
}
addList(params: TListApiModel.ReqAddList) {
return this.apiModel.addList(params);
}
}
enjoy模板新增了jest,我们可以直接使用jest对功能进行测试,如下:
// tests/model/api/list.test.ts
import ListController from '../../../src/controller/list';
describe('addList', () => {
const controller = new ListController();
it('添加参数测试-1', async () => {
await controller.addList({
title: 'test',
content: 'nihao'
});
});
it('添加参数测试-2', async () => {
const result = await controller.addList({
title: 'test2',
content: 'hello?'
});
console.log(result.id);
});
});
运行npm run test tests/model/api/list.test.ts