美文网首页从零到一
从零开始:基于 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