基于 graphql + 实时数据引擎建设一个资源站——(零)规划篇


基于 graphql + 实时数据引擎建设一个资源站——(零)规划篇


这几天当插画师的老婆要求我为她找一些设计资源并且聚合起来,方便她去查阅和使用。本质上这种东西就是一个导航站是很简单的,有很多建站工具、静态页面生成工具,甚至只需要“手动”做个 html 页面也就可以满足。

但是职业习惯(造轮子),不想因为是个简单的需求就去做一个简单的东西。既然是个需求我觉得也是个方向,如果这个资源站可以是一个有价值和品质的产品呢?那就需要花些心思。

按我希望的资源站本身能拥有聚合功能,比如我们喜欢某些资源可以自己提交,也可以将站内的资源收为已用。有点像pinterest、花瓣网,只不过是它的资源的针对性是有差异的。并且也希望它在桌面端、移动端都有所表现,因为资源本身就要随时随时可以使用,也便于应用,但按正常这样下去半个月没了。

可是老婆催的紧,希望我半天完成“交付使用”。

那么这次我们就一起来打造一个基于实时数据库 + graphql 为技术核心的资源站产品。先用半天时间实现出有自适应能力的前端、支持 pwa、打包为 android、ios、桌面端、ssr、spa,后端支持实时数据查询。而这一切内容只需要两项核心技术的搭配。


主要技术栈:

  • hasura Graphql 基于 Postgres 的即时 GraphQL

作为第一家GraphQL-as-a-Service公司,Hasura推出了其开源GraphQL引擎,这是目前唯一可立即将GraphQL-as-a-Service添加到现有基于Postgres应用程序中的解决方案

  • Quasar (基于 vuejs 的一个真正框架)

今天主要的目标是将所有点先串起来,然后实现一个基础,先将最核心的资源内容展现出来。目标效果是这样的:


基于 graphql + 实时数据引擎建设一个资源站——(零)规划篇


那么我们开始实践:

1、Hasura GraphQL Engine 服务搭建

要使用Hasura GraphQL引擎,需要:

  1. 运行Hasura GraphQL引擎并访问Postgres数据库
  2. 使用连接到Hasura GraphQL引擎的Hasura控制台(一个管理UI)来帮助构建模式并运行GraphQL查询
基于 graphql + 实时数据引擎建设一个资源站——(零)规划篇

image.png

Hasura控制台用于查询和更新数据库,并生成对应的

query

mutation

delete

insert

update

subscription

基于 graphql + 实时数据引擎建设一个资源站——(零)规划篇


基于 docker-compose 编排文件,一个文件搞定全部数据服务环境

<code>version: '3.6'
services:
postgres:
image: postgres:12
restart: always
ports:
- "5432:5432"
volumes:
- db_data:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: postgrespassword
graphql-engine:
image: hasura/graphql-engine:v1.2.0-beta.2
ports:
- "8080:8080"
depends_on:
- "postgres"
restart: always
environment:
HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/postgres
HASURA_GRAPHQL_ENABLE_CONSOLE: "true" # set to "false" to disable console
HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log
## uncomment next line to set an admin secret
# HASURA_GRAPHQL_ADMIN_SECRET: myadminsecretkey
volumes:
db_data:
/<code>

运行 docker-compose

<code>docker-compose up -d
/<code>

进入Hasura控制台访问 [http://localhost:8080/console/api-explorer](http://localhost:8080/console/api-explorer)

基于 graphql + 实时数据引擎建设一个资源站——(零)规划篇

image.png

因为数据库已经建立表过一些表,Hasura 服务是可以自动识别和追踪的,识别之后会生成如上图所见各表的 graphql 查询 schema,这样所有的基于 graphql 的增删改查、级联查询就都已经就绪,相当于你的 api 服务基本已经完成,如果业务层面主要是数据形式操作的话,服务系统就完成了。

2、建立客户端由于我们的目标最终是形成一个可供多端使用的资源站系统,需要 移动端 app、桌面端、浏览器端都能够有对应的终端,这里选用了一款框架 https://quasar.dev/ ,它可以一套 vuejs 代码生成所有终端,并且它有独立的 cli 环境、完善的UI组件,非常实用。

基于 graphql + 实时数据引擎建设一个资源站——(零)规划篇

image.png

好,现在开始建立客户端开发环境和创建项目工程

<code># Node.js >= 8.9.0 is required.

$ yarn global add @quasar/cli

quasar create picker-client
/<code>

完成后,修改 package 的>

<code>// package.json
"scripts": {
"dev": "quasar dev",
"build": "quasar build",
"build:pwa": "quasar build -m pwa"
}
/<code>

quasar 和普通的 vuecli 建立的项目有些不同,它提供了一个套整体解决方案,插件以及它的相关环境是在一个 /quasar.conf.js 文件中进行配置,它的结构如下:

<code>module.exports = function (ctx) {
console.log(ctx)

// Example output on console:
{
dev: true,
prod: false,
mode: { spa: true },
modeName: 'spa',
target: {},
targetName: undefined,
arch: {},
archName: undefined,
debug: undefined
}

// context gets generated based on the parameters
// with which you run "quasar dev" or "quasar build"

}
/<code>

更具体的配置可以参考 https://quasar.dev/quasar-cli/quasar-conf-js

运行

<code>npx quasar dev
/<code>

其它发布模式可参考:

基于 graphql + 实时数据引擎建设一个资源站——(零)规划篇

image.png

上述按 cli 指引就已经建立好基础的前端工程,但由于我们的需求是基于 graphql 的客户端,并且需要前端工程是基于 Typescript编写, 所以在结构上面还有一些调整。下面详细介绍:

Typescript 支持

1、首先增加 typescript 支持,在quasar.config.js 中 增加 supoortTS: true2、创建 tsconfig.json

<code>{
"compilerOptions": {
"allowJs": true,
"sourceMap": true,
"target": "es6",
"strict": true,
"experimentalDecorators": true,
"module": "esnext",
"moduleResolution": "node",
"baseUrl": ".",
"types": [
"quasar"
]
},
"exclude": ["node_modules"]
}

/<code>

graphql 支持

1、 创建 .graphqlconfig 文件

<code>{
"name": "Untitled GraphQL Schema",
"schemaPath": "schema.gql",
"extensions": {
"endpoints": {
"Default GraphQL Endpoint": {
"url": "http://localhost:8080/v1/graphql",
"headers": {
"user-agent": "JS GraphQL"
},
"introspect": false
}
}
}
}

/<code>

2、添加 graphql 支持

<code>yaran add graphql apollo-client apollo-link-http applo-link-context  graphql-tag

/<code>

封装 apolloClient

quasar 这个框架有 graphql 模块,但对 typescript 支持不太好,所以我自己封装了下,并增加了依赖注入的支持,采用 inversify 这个 lib先添加支持

<code>yarn add inversify inversify-props
/<code>

创建 apolloClient.service.interface.ts

<code>import ApolloClient from 'apollo-client'

export default interface ApolloClientServiceInterface {
client: ApolloClient
}

/<code>

创建 apolloClient.service.ts

<code>import fetch from 'node-fetch'
import { ApolloClient } from 'apollo-client'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { createHttpLink } from 'apollo-link-http'
import { ApolloLink } from 'apollo-link'
import { onError } from 'apollo-link-error'
import { injectable } from 'inversify-props'
import { uid } from 'quasar'

import ApolloClientServiceInterface from './apolloClient.service.interface'

@injectable()
class ApolloClientService implements ApolloClientServiceInterface {
public client: ApolloClient

public readonly uid: string = uid()

public constructor (

) {
const httpLink = createHttpLink({
uri: 'http://localhost:8080/v1/graphql',
fetch: fetch as any
})

const logoutLink = onError((error) => {
const errorRes = error.response
if (errorRes) {
const errors = errorRes.errors
if (errors && errors.length) {
const error = errors[0]
}
}
})

const apolloClient = new ApolloClient({
link: ApolloLink.from([
logoutLink,
httpLink
]),
cache: new InMemoryCache(),
connectToDevTools: true,
defaultOptions: {
query: {
fetchPolicy: 'network-only',
errorPolicy: 'all'
},
mutate: {
errorPolicy: 'all'
}
}
})

this.client = apolloClient
}
}

export default ApolloClientService
/<code>

以上两个文件是做为 apollo 服务的客户端的一个简单封装,然后再增加一个基础服务类,用于 CRUD

创建 baseApolloCrud.service.ts

<code>import BaseCrudServiceInterface from '../baseCrud.service.interface'
import ApolloClientService from './apolloClient.service'
import { injectSingleton } from '../../diContainer'
import { injectable } from 'inversify-props'
import {PaginatedList} from '../../../interfaces/page.interface';

@injectable()
export default abstract class BaseApolloCrudService implements BaseCrudServiceInterface {
/**
* Our Apollo Client instance.
* Note: Will (and should) be a singleton.
*/
@injectSingleton(ApolloClientService)
public readonly apolloClientService!: ApolloClientService

abstract create (dto: DTO): Promise

abstract delete (id: string): Promise

abstract get (params?: any): Promise<paginatedlist>>

abstract getById (id: string): Promise

abstract update (dto: DTO): Promise
}
/<paginatedlist>
/<code>

这个类用到了反射机制,注入了 apolloClientService 实例,供子服务直接调用查询,熟悉面向对象语言的朋友应该比较熟悉这种感觉,但 ts 更加灵活些。

创建 baseCrud.service.interface.ts 进一步解耦

<code>import {PaginatedList} from '../../interfaces/page.interface'; 


export default interface BaseCrudServiceInterface {
get (params?: any): Promise<paginatedlist>> // TODO: Type this PaginatedList
getById (id: string): Promise
create (dto: DTO): Promise
update (dto: DTO): Promise
delete (id: number): Promise
}
/<paginatedlist>
/<code>

创建 baseCrud.service.ts 接口实现类

<code>import { injectable } from 'inversify-props'

import BaseApolloCrudService from './apollo/baseApolloCrud.service'

@injectable()
export default abstract class BaseCrudService extends BaseApolloCrudService {
}

/<code>

这个类继承 BaseApolloCrudService 并且是个抽象类,做为服务基类进行扩展

然后创建 diContainer.ts

<code>import 'reflect-metadata'
import { container } from 'inversify-props'
import { Cookies, QSsrContext } from 'quasar'
import { Store } from 'vuex'
import { RootState } from '../store/types'
import ApolloClientService from './_base/apollo/apolloClient.service';
import PostService from './posts/post.service';

import PostServiceInterface from './posts/post.service.interface';
import PostCurdDto from './posts/dto/postCurd.dto';
import Post from './posts/post.model';
import StoreService from "./_base/store.service";

export const buildDependencyContainer = (ssrContext: QSsrContext, store: Store<rootstate>) => {
// console.log('Binding dependencies: ', ssrContext, store)
container.unbindAll()

const cookies = process.env.SERVER
? Cookies.parseSSR(ssrContext)
: Cookies

// Singletons
container.bind<apolloclientservice>(ApolloClientService).toSelf().inSingletonScope()
container.bind<storeservice>(StoreService).toSelf().inSingletonScope()

// Transient (instance per)
container.addTransient<postserviceinterface>>(PostService)

return container
}

export { container }

export function injectSingleton (type: any): any {
return function (target: any, targetKey: string, index?: number): any {
Reflect.deleteProperty(target, targetKey)
Reflect.defineProperty(target, targetKey, {
get () {
return container.get(type)
},
set (value) {
return value
}
})
}
}

/<postserviceinterface>/<storeservice>/<apolloclientservice>/<rootstate>/<code>

这个是做为全局依赖注入的容器类,把它引入到 store/index.ts 中

<code>import Vue from 'vue'
import Vuex, { Store } from 'vuex'
import { buildDependencyContainer } from '../modules/diContainer'
import { QSsrContext } from 'quasar'
import {RootState} from "./types";

import {ui} from "./modules/ui";
Vue.use(Vuex)

let store: Store<rootstate>

export default function ({ ssrContext }: { ssrContext: QSsrContext }) {
store = new Vuex.Store( {
modules: {
// book,
// auth,
ui,
// user
},

// enable strict mode (adds overhead!)
// for dev mode only
strict: !!process.env.DEV
})

buildDependencyContainer(ssrContext, store)

return store
}

export { store }
/<rootstate>/<code>

这样在应用启动时会执行容器内部的依赖实例化操作,达到控制反转的目的。

这样基础的配制工作都已完成,看配置过程可能稍显复杂,但是事实上没有过多的逻辑,当这些配置完成后,我们的系统已经完成了大半,下一步就是具体的前端实现,因为前端过于简单,可参考文尾的源码。


总结:

这篇文章中主要是实践了Hasura GraphQL Engine 服务,因为有了这种实时数据服务的存在,我们可以省去大量的服务端开发,可以轻松定制各种垮平台的业务。而客户端虽然 quasar 非常好,但事实上不是仅有它才可以做到,还有很多的解决方案,比如普通的 vuejs 程序、react、gatsbyjs、gridsome 等等。

后面我还会持续完善这个小产品,让它成为一个非常强大的资源产品。

源码与访问地址:

https://github.com/baisheng/picker-client

https://design.picker.cc

也可以直接点我个人页面底部的导航查看效果。


分享到:


相關文章: