美文网首页从零到一
从零开始:基于 graphql + 实时数据引擎建设一个资源站—

从零开始:基于 graphql + 实时数据引擎建设一个资源站—

作者: 思考蛙 | 来源:发表于2020-03-17 20:36 被阅读0次
image.png

这几天当插画师的老婆要求我为她找一些设计资源并且聚合起来,方便她去查阅和使用。本质上这种东西就是一个导航站是很简单的,有很多建站工具、静态页面生成工具,甚至只需要“手动”做个 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 的一个真正框架)
今天主要的目标是将所有点先串起来,然后实现一个基础,先将最核心的资源内容展现出来。目标效果是这样的:

这个产品将会用到的技术栈

  • hasura Graphql 基于 Postgres 的即时 GraphQL

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

  • nestjs 服务端,用于一些必要的逻辑、权限处理
  • graphql-client
  • postgres 数据库
  • vuejs(quasar)
  • docker 服务容器

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


image.png

那么我们开始实践:

1、Hasura GraphQL Engine 服务搭建

要使用Hasura GraphQL引擎,需要:

  1. 运行Hasura GraphQL引擎并访问Postgres数据库
  2. 使用连接到Hasura GraphQL引擎的Hasura控制台(一个管理UI)来帮助构建模式并运行GraphQL查询
image.png

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

query
mutation
delete
insert
update
subscription

image.png

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

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:

运行 docker-compose

docker-compose up -d

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

image.png

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

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

image.png

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

# Node.js >= 8.9.0 is required.

$ yarn global add @quasar/cli

 quasar create picker-client

完成后,修改 package 的 scripts 如下

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

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

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"
}

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

运行

npx quasar dev

其它发布模式可参考:

image.png

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

Typescript 支持

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

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

graphql 支持

1、 创建 .graphqlconfig 文件

{
  "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
      }
    }
  }
}

2、添加 graphql 支持

yaran add graphql apollo-client apollo-link-http applo-link-context  graphql-tag

封装 apolloClient

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

yarn add inversify inversify-props

创建 apolloClient.service.interface.ts

import ApolloClient from 'apollo-client'

export default interface ApolloClientServiceInterface {
  client: ApolloClient<any>
}

创建 apolloClient.service.ts

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<any>

  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

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

创建 baseApolloCrud.service.ts

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<DTO, T> implements BaseCrudServiceInterface<DTO, T> {
  /**
   * Our Apollo Client instance.
   * Note: Will (and should) be a singleton.
   */
  @injectSingleton(ApolloClientService)
  public readonly apolloClientService!: ApolloClientService

  abstract create (dto: DTO): Promise<T>

  abstract delete (id: string): Promise<any>

  abstract get (params?: any): Promise<PaginatedList<T>>

  abstract getById (id: string): Promise<T>

  abstract update (dto: DTO): Promise<T>
}

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

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

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

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

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

import { injectable } from 'inversify-props'

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

@injectable()
export default abstract class BaseCrudService<DTO, T> extends BaseApolloCrudService<DTO, T> {
}

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

然后创建 diContainer.ts

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<PostCurdDto, Post>>(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
      }
    })
  }
}

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

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 }

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

总结:

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

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

源码与访问地址:

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

https://design.picker.cc

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

相关文章

网友评论

    本文标题:从零开始:基于 graphql + 实时数据引擎建设一个资源站—

    本文链接:https://www.haomeiwen.com/subject/iaftyhtx.html