JimmyLv / jimmylv.github.io

:bowtie: Agile Learning based on GitHub issues, KEEP Retrospection and Introspection! Thanks to @GitHub https://jimmylv.github.io/issues/
https://blog.jimmylv.info
MIT License
704 stars 114 forks source link

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

Open JimmyLv opened 5 years ago

JimmyLv commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

但其实,为了不必要的 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 commented 5 years ago

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

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

JimmyLv commented 5 years ago

关于 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