服务端渲染

Vue CLI 插件

我为 vue-cli 制作了一个插件,因此仅用两分钟你就可以将你的 vue-apollo 应用转换为同构 SSR 应用!✨🚀

在你的 vue-cli 3 项目中:

vue add @akryum/ssr

更多信息

预取组件

在要在服务端预取的查询上,添加 prefetch 选项。它可以是:

  • 一个变量对象;
  • 一个获取上下文对象(例如可以包含 URL)并返回一个变量对象的函数;
  • false 禁用此查询的预取。

如果你在 prefetch 选项中返回一个变量对象,请确保它与 variables 选项的结果相匹配。如果它们不匹配,则在服务端渲染模板时,查询的数据属性将不会被填充。

WARNING

在服务端进行预取时,你无法访问组件实例。

示例:

export default {
  apollo: {
    allPosts: {
      // 此查询将被预取
      query: gql`query AllPosts {
        allPosts {
          id
          imageUrl
          description
        }
      }`,
      prefetch: true,
    }
  }
}

示例 2:

export default {
  apollo: {
    post: {
      query: gql`query Post($id: ID!) {
        post (id: $id) {
          id
          imageUrl
          description
        }
      }`,
      prefetch: ({ route }) => {
        return {
          id: route.params.id,
        }
      },
      variables () {
        return {
          id: this.id,
        }
      },
    }
  }
}

跳过预取

不预取查询的示例:

export default {
  apollo: {
    allPosts: {
      query: gql`query AllPosts {
        allPosts {}
          id
          imageUrl
          description
        }
      }`,
      // 不要预取
      prefetch: false,
    }
  }
}

如果要跳过特定组件的所有查询的预取,使用 $prefetch 选项:

export default {
  apollo: {
    // 不要预取任何查询
    $prefetch: false,
    allPosts: {
      query: gql`query AllPosts {
        allPosts {
          id
          imageUrl
          description
        }
      }`,
    }
  }
}

你也可以在任何组件上放置一个 no-prefetch 属性,以便在遍历树收集 Apollo 查询时忽略它:

<ApolloQuery no-prefetch>

在服务端

在服务端入口中,你需要在 Vue 中安装 ApolloSSR 插件:

import Vue from 'vue'
import ApolloSSR from 'vue-apollo/ssr'

Vue.use(ApolloSSR)

使用 ApolloSSR.prefetchAll 方法来预取你已标记的所有 apollo 查询。第一个参数是 apolloProvider。第二个参数是要包含的组件定义数组(例如来自 router.getMatchedComponents 方法)。第三个参数是传递给 prefetch 钩子的上下文对象(参见上文),建议传入 vue-router 的 currentRoute 对象。当所有的 apollo 查询都被加载时,它返回已解决的(resolved) promise。

以下是一个使用了 vue-router 和 Vuex store 的示例:

import Vue from 'vue'
import ApolloSSR from 'vue-apollo/ssr'
import App from './App.vue'

Vue.use(ApolloSSR)

export default () => new Promise((resolve, reject) => {
  const { app, router, store, apolloProvider } = CreateApp({
    ssr: true,
  })

  // 设置 router 的位置
  router.push(context.url)

  // 等待 router 解析完可能的异步钩子
  router.onReady(() => {
    const matchedComponents = router.getMatchedComponents()

    // 匹配不到的路由
    if (!matchedComponents.length) {
      reject({ code: 404 })
    }

    let js = ''

    // 调用匹配到路由的组件的预取钩子
    // 每个 preFetch 钩子分配到一个 store action 并返回一个 Promise
    // 当 action 操作完成且 store 状态已更新时解析这个 Promise

    // Vuex Store 预取
    Promise.all(matchedComponents.map(component => {
      return component.asyncData && component.asyncData({
        store,
        route: router.currentRoute,
      })
    }))
    // Apollo 预取
    // 这里将预取整个应用中的所有 Apollo 查询
    .then(() => ApolloSSR.prefetchAll(apolloProvider, [App, ...matchedComponents], {
      store,
      route: router.currentRoute,
    }))
    .then(() => {
      // 将 Vuex 状态和 Apollo 缓存注入到页面
      // 这将防止不必要的查询

      // Vuex
      js += `window.__INITIAL_STATE__=${JSON.stringify(store.state)};`

      // Apollo
      js += ApolloSSR.exportStates(apolloProvider)

      resolve({
        app,
        js,
      })
    }).catch(reject)
  })
})

使用 ApolloSSR.exportStates(apolloProvider, options) 方法来获取你需要注入到生成出来页面的 JavaScript 代码,这些代码用于将 apollo 缓存数据传递给客户端。

它需要一个 options 参数,默认为:

{
  // 全局变量名
  globalName: '__APOLLO_STATE__',
  // 变量设置到的全局对象
  attachTo: 'window',
  // 每个 apollo 客户端状态的 key 的前缀
  exportNamespace: '',
}

你也可以使用 ApolloSSR.getStates(apolloProvider, options) 方法来获取 JS 对象而不是脚本字符串。

它需要一个 options 参数,默认为:

{
  // 每个 apollo 客户端状态的 key 的前缀
  exportNamespace: '',
}

创建 Apollo Client

建议在一个带有 ssr 参数的函数内部创建 apollo 客户端,参数在服务端为 true,在客户端为 false

这里是一个示例:

// src/api/apollo.js

import Vue from 'vue'
import { ApolloClient } from 'apollo-client'
import { HttpLink } from 'apollo-link-http'
import { InMemoryCache } from 'apollo-cache-inmemory'
import VueApollo from 'vue-apollo'

// 安装 vue 插件
Vue.use(VueApollo)

// 创建 apollo 客户端
export function createApolloClient (ssr = false) {
  const httpLink = new HttpLink({
    // 你需要在这里使用绝对路径
    uri: ENDPOINT + '/graphql',
  })

  const cache = new InMemoryCache()

  // 如果在客户端则恢复注入状态
  if (!ssr) {
    if (typeof window !== 'undefined') {
      const state = window.__APOLLO_STATE__
      if (state) {
        // 如果你有多个客户端,使用 `state.<client_id>`
        cache.restore(state.defaultClient)
      }
    }
  }

  const apolloClient = new ApolloClient({
    link: httpLink,
    cache,
    ...(ssr ? {
      // 在服务端设置此选项以优化 SSR 时的查询
      ssrMode: true,
    } : {
      // 这将暂时禁用查询强制获取
      ssrForceFetchDelay: 100,
    }),
  })

  return apolloClient
}

常见的 CreateApp 方法示例:

import Vue from 'vue'
import VueRouter from 'vue-router'
import Vuex from 'vuex'
import { sync } from 'vuex-router-sync'

import VueApollo from 'vue-apollo'
import { createApolloClient } from './api/apollo'

import App from './ui/App.vue'
import routes from './routes'
import storeOptions from './store'

Vue.use(VueRouter)
Vue.use(Vuex)

function createApp (context) {
  const router = new VueRouter({
    mode: 'history',
    routes,
  })

  const store = new Vuex.Store(storeOptions)

  // 同步路由到 vuex store
  // 将注册 `store.state.route`
  sync(store, router)

  // Apollo
  const apolloClient = createApolloClient(context.ssr)
  const apolloProvider = new VueApollo({
    defaultClient: apolloClient,
  })

  return {
    app: new Vue({
      el: '#app',
      router,
      store,
      apolloProvider,
      ...App,
    }),
    router,
    store,
    apolloProvider,
  }
}

export default createApp

在客户端:

import CreateApp from './app'

CreateApp({
  ssr: false,
})

在服务端:

import CreateApp from './app'

export default () => new Promise((resolve, reject) => {
  const { app, router, store, apolloProvider } = CreateApp({
    ssr: true,
  })

  // 设置 router 的位置
  router.push(context.url)

  // 等待 router 解析完可能的异步钩子
  router.onReady(() => {
    // 预取,渲染 HTML(参见上文)
  })
})
上次更新时间: 11/8/2018, 5:22:09 PM