Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue 数据流最佳实践:nuxt vue graphql apollo (maybe no vuex) #356

Open
JimmyLv opened this issue May 22, 2019 · 5 comments
Open

Vue 数据流最佳实践:nuxt vue graphql apollo (maybe no vuex) #356

JimmyLv opened this issue May 22, 2019 · 5 comments

Comments

@JimmyLv
Copy link
Owner

JimmyLv commented May 22, 2019

graphql mutation 要以input为入参
pages/xxx apollo.js components.js
query就该离得最近, 子组件也由自己去取
或者就使用fragememts吧,如果想合并请求,或者是考虑page route 入参
SSR渲染,只可能发生在fetch或者async data,但是这样的话 apollo怎么到data呢?

::: tip You have access to this in options like variables, even on the server! :::

import gql from 'fraql'

export default {
  name: 'PDP',
  components: {
    productDetail,
    pdpAppealTo,
    swiper,
    swiperSlide,
    recommendProduct,
    MoreBrandProduct,
    GuestYouLink,
    RecommendPopup,
    lazyImg,
    LittleRedBook,
  },
  data() {
    return {
      sku: null,
      selectedOptions: {},
      lowStock: false,
      sellOut: false,
      offSheld: false,
      skuOff: false,
      show: true,
      brand: '',
      lodingImage: true,
      imageList: [],
    }
  },
  apollo: {
    name: gql`{
    shop {
      name
    }
  }
  `,
    detail: {
      query: gql`
      query productDetail($codes: [String!]!) {
        shop {
          bffSpusByCodes(codes: $codes) {
            type
            preSaleAttribute {
              startTime
              endTime
              scheduledDeliveryTime
            }
            spu {
              isPreSale
              id
              seo {
                seoTitle
                seoKeywords
                seoDescription
              }
              code
              promotions {
                name
                label
                description
                ruleDesc
                id
                beginTime
                endTime
                gifts{
                  name
                  number
                }
              }
              title
              onShelves
              productType
              brand {
                imageUrl
                code
                name
                description
              }
              categories {
                name
                code
                path
              }
              images {
                url
              }
              subTitle
              description
              inventory
              sales
              skus {
                id
                code
                isEnabled
                inventory
                featuredType
                activityPrice {
                  promotionPrice {
                    amount
                    currencyCode
                  }
                }
                promotions {
                  name
                  label
                  activityType
                  promotionScope
                  description
                  ruleDesc
                  id
                  beginTime
                  endTime
                  gifts{
                    name
                    number
                  }
                }
                salePrice {
                  amount
                  currencyCode
                }
                listPrice {
                  amount
                  currencyCode
                }
                options {
                  code
                  frontName
                  value {
                    code
                    frontName
                    displayName
                    images {
                      url
                    }
                    thumbnails {
                      url
                    }
                  }
                }
                preDays
              }
              bffSkus {
                isWish
                sku {
                  id
                  code
                  isEnabled
                  inventory
                  featuredType
                  activityPrice {
                    promotionPrice {
                      amount
                      currencyCode
                    }
                  }
                  promotions {
                    name
                    label
                    activityType
                    promotionScope
                    description
                    id
                    beginTime
                    endTime
                    gifts{
                      name
                      number
                    }
                  }
                  salePrice {
                    amount
                    currencyCode
                  }
                  listPrice {
                    amount
                    currencyCode
                  }
                  options {
                    code
                    frontName
                    value {
                      code
                      frontName
                      displayName
                      images {
                        url
                      }
                    }
                  }
                  preDays
                }
              }
              listPrice {
                currencyCode
                amount
              }
              salePrice {
                currencyCode
                amount
              }
              options {
                code
                frontName
                values {
                  code
                  url
                  displayName
                  images {
                    url
                  }
                  thumbnails{
                    url
                  }
                }
              }
              attributes {
                code
                frontName
                values {
                  url
                  displayName
                }
              }
            }
          }
        }
      }
    `,
      variables() {
        return { codes: [this.productId] }
      },
    },
  },
@JimmyLv
Copy link
Owner Author

JimmyLv commented Jul 8, 2019

variables 中的 productId 来自 $route.param ,使用 Nuxt 但 SSR 也有效。

query 部分完全可以由各依赖的子组件们来提供对应数据片段,即 fragments。

之前的整个 query 内容实在太大了!一个页面根本不需要这么多冗余数据,只取UI需要的数据,按需查询。

fragment PreSale on BffSpu {
  preSaleAttribute {
    startTime
    endTime
    scheduledDeliveryTime
  }
  spu {
    isPreSale
  }
}

fragment SEO on Product {
  seo {
    seoTitle
    seoKeywords
    seoDescription
  }
}

fragment Promotion on PromotionActivity {
  name
  label
  description
  ruleDesc
  id
  beginTime
  endTime
  gifts {
    name
    number
  }
}

fragment Brand on Product {
  brand {
    imageUrl
    code
    name
    description
  }
}

fragment Category on ProductCategory {
  name
  code
  path
}
query productDetail($codes: [String!]!) {
  shop {
    bffSpusByCodes(codes: $codes) {
      type
      ...PreSale
      spu {
        id
        code
        title
        onShelves
        productType
        ...SEO
        promotions {
          ...Promotion
        }
        ...Brand
        categories {
          ...Category
        }
        images {
          url
        }
        subTitle
        description
        inventory
        sales
        skus {}
        bffSkus {}
    }
  }
}

image

@JimmyLv
Copy link
Owner Author

JimmyLv commented Jul 8, 2019

ref: Using fragments - ApolloQuery | Vue Apollo

<!-- MessageList.vue -->
export default {
  fragments: {
    message: gql`
      fragment message on Message {
        id
        user {
          id
          name
        }
        text
        created
      }
    `
  }
}
apollo: {
    messages: gql`
	  query GetMessages {
	    messages {
	      ...message
	    }
	  }
	  ${this.$options.fragments.message}
	`
  }

@JimmyLv
Copy link
Owner Author

JimmyLv commented Jul 8, 2019

但其实,为了不必要的 fragment 层级考虑,还有两种额外的处理方式,方便管理&解除耦合关系,同时也符合 vuex 中央集权式的理念。

  1. 完全自查询:即每个组件只查询自己的数据,且是一个完整的独立 graphql 查询,然后从 apollo link 层进行合并:

image

当然,这也需要后端的支持,因为此时请求的是 /bff/graphqls 的复数 API url,而不再是 /bff/graphql,客户端需要使用 BatchHttpLink:

import { BatchHttpLink } from 'apollo-link-batch-http'

  const httpLinkOptions = {
    uri,
    fetch,
    credentials: 'same-origin',
    headers: {},
  }

  const httpLink = createHttpLink(httpLinkOptions)

  const batchLink = new BatchHttpLink({
    ...httpLinkOptions,
    uri: `${uri}s`,
  })
  1. 根部fragment:即每个组件的fragment有着从根部 Query 开始的 fragment,然后再用统一的 merge 操作,将所有子组件的片段合并:
query fetchDynamicOrganismData($codes: ProductCodeInput) {
  ... on Query {
    ... on Query {
      Product: shop {
        currency
        decimalPlaces
        productByCode(codes: $codes) {
          id
          code
          title
          onShelves
          images {
            url
            __typename
          }
          salePrice {
            amount
            __typename
          }
          skus {
            id
            code
            isEnabled
            __typename
          }
          __typename
        }
        __typename
      }
      __typename
    }
    ... on Query {
      ProductCarousel: shop {
        currency
        decimalPlaces
        productByCode(codes: $codes) {
          id
          code
          title
          onShelves
          images {
            url
            __typename
          }
          salePrice {
            amount
            __typename
          }
          listPrice {
            amount
            __typename
          }
          skus {
            id
            code
            isEnabled
            __typename
          }
          __typename
        }
        __typename
      }
      __typename
    }
    __typename
  }
}

用于合并时的代码(有待改进,现在过多 ... on Query 层级的重复):

import gql from 'fraql'

const getDynamicFragments = organismsList =>
  Object.values(organisms)
    .filter(comp =>
      comp.fragment &&
        !!comp.fragment.dynamic &&
        organismsList.find(type => type === comp.name))
    .map(comp => comp.fragment.dynamic)
    .reduce((a, b) => gql`
      fragment _ on Query {
        ${a}
        ${b}
      }
  `)

而且需要通过 this.$root.$children.filter(child => !!child.$options.fragment); 获取每个子组件的fragment片段。

ref: smooth-code/fraql: GraphQL fragments made simple ⚡️

@JimmyLv
Copy link
Owner Author

JimmyLv commented Jul 8, 2019

两者对比可以看出,option 1 明显要简单直接一些:

潜在的优化包括:option 1 能够做到 query 的分时,从而让组件自由选择在 SSR 端或 client 端进行数据请求,从而更精确控制请求性能或是 SEO 数据。
反而 option 2 不能做到这一点,因为子组件只提供 fragment 而无法主动发送请求,这个 fragment 是static的等着被人来使用,而且父组件(一般是 route 层级,即 Nuxt 的 pages/xxx )使用的时候也只能选择一种使用方式。

@JimmyLv
Copy link
Owner Author

JimmyLv commented Jul 8, 2019

关于 GraphQL 作为后端 BFF 的定位和实践,可能面临的问题:

  1. 团队配置:这一点主要是考虑到 HC,每个团队都配比了后端开发(一般技术栈为 Java),那么其职责无外乎数据的合并或筛选,即业务逻辑组合。所以 GraphQL 的后端选择是否需要切换至 Apollo Server 即 Node 技术栈呢?而且 BFF 的概念本身就属于(大)前端,由前端来写会更加舒适,况且为了 SEO和性能,一般都已经有了一台 Node Server 用于 SSR,与其并存的 GraphQL Apollo Server 也是非常合适并且节省资源和运维成本的。

  2. (中台)服务依赖:如果中台团队足够出色,API设计丰富且符合 REST 规范(90%做不到),那么就不需要所谓的洗数据这一说法了吧。反之来说,团队配置当中的 Java 后端开发,一般就是在洗中台服务所提供的"脏"数据,梳理数据流程使其符合前端业务需要。工作量主要浪费的就在于这里,也就是说(领导理想情况下)让中台想省下来的功夫,最终又落到了前台后端开发这里。中台服务本身由于层层封装,或者是压根儿没有封装完全是数据表格映射/透传而已(俗称“捅”),实际上调用中台服务比直接操作数据库还痛苦。

  3. Apollo Server 本身的定位:

diagram

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant