diff --git a/README.md b/README.md
index 72af3c6..057345b 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ Integrates [apollo](http://www.apollostack.com/) in your [Vue](http://vuejs.org)
[
Apollo graphql server example](https://github.com/Akryum/apollo-server-example)
-[
Apollo "hello world" example app](https://github.com/Akryum/frontpage-vue-app)
+[
Apollo "hello world" example app](https://github.com/Akryum/frontpage-vue-app) (outdated)
[
Howto on Medium](https://dev-blog.apollodata.com/use-apollo-in-your-vuejs-app-89812429d8b2#.pdd4hmcrc)
@@ -1068,28 +1068,298 @@ tags: {
## Server-Side Rendering
-(Work in progress)
+### Prefetch components
-Use `apolloProvider.collect()` to being collect queries made against this provider. You get a function that returns a promise when all queries collected are ready. Note that the provider stops collecting queries when you call the function.
+On the queries you want to prefetch on the server, add the `prefetch` option. It can either be:
+ - a variables object,
+ - a function that gets the context object (which can contain the URL for example) and return a variables object,
+ - `true` (query's `variables` is reused).
+
+**Warning! You don't have access to the component instance when doing prefetching on the server. Don't use `this` in `prefetch`!**
+
+Example:
```javascript
-const ensureReady = apolloProvider.collect({
- // If set to false, may resolve when partial/cache result is emitted
- waitForLoaded: true, // default
-})
+export default {
+ apollo: {
+ allPosts: {
+ query: gql`query AllPosts {
+ allPosts {
+ id
+ imageUrl
+ description
+ }
+ }`,
+ prefetch: true,
+ }
+ }
+}
+```
-new Vue({
- el: '#app',
- apolloProvider,
- render: h => h(App),
-})
+Example 2:
-ensureReady().then(results => {
- console.log(results.length, 'queries ready')
+```javascript
+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,
+ }
+ },
+ }
+ }
+}
+```
+
+You can also tell vue-apollo that some components not used in a `router-view` (and thus not in vue-router `matchedComponents`) need to be prefetched, with the `willPrefetch` method:
+
+```javascript
+import { willPrefetch } from 'vue-apollo'
+
+export default willPrefetch({
+ apollo: {
+ allPosts: {
+ query: gql`query AllPosts {
+ allPosts {
+ id
+ imageUrl
+ description
+ }
+ }`,
+ prefetch: true, // Don't forget this
+ }
+ }
})
```
-[White paper](./ssr.js)
+### On the server
+
+To prefetch all the apollo queries you marked, use the `apolloProvider.prefetchAll` method. The first argument is the context object passed to the `prefetch` hooks (see above). It is recommended to pass the vue-router `currentRoute` object. The second argument is the array of component definition to include (e.g. from `router.getMatchedComponents` method). The third argument is an optional `options` object. It returns a promise resolved when all the apollo queries are loaded.
+
+Here is an example with vue-router and a Vuex store:
+
+```javascript
+return new Promise((resolve, reject) => {
+ const { app, router, store, apolloProvider } = CreateApp({
+ ssr: true,
+ })
+
+ // set router's location
+ router.push(context.url)
+
+ // wait until router has resolved possible async hooks
+ router.onReady(() => {
+ const matchedComponents = router.getMatchedComponents()
+
+ // no matched routes
+ if (!matchedComponents.length) {
+ reject({ code: 404 })
+ }
+
+ let js = ''
+
+ // Call preFetch hooks on components matched by the route.
+ // A preFetch hook dispatches a store action and returns a Promise,
+ // which is resolved when the action is complete and store state has been
+ // updated.
+ Promise.all([
+ // Vuex Store prefetch
+ ...matchedComponents.map(component => {
+ return component.preFetch && component.preFetch(store)
+ }),
+ // Apollo prefetch
+ apolloProvider.prefetchAll({
+ route: router.currentRoute,
+ }, matchedComponents),
+ ]).then(() => {
+ // Inject the Vuex state and the Apollo cache on the page.
+ // This will prevent unecessary queries.
+
+ // Vuex
+ js += `window.__INITIAL_STATE__=${JSON.stringify(store.state)};`
+
+ // Apollo
+ js += apolloProvider.exportStates()
+
+ resolve({
+ app,
+ js,
+ })
+ }).catch(reject)
+ })
+})
+```
+
+The `options` argument defaults to:
+
+```javascript
+{
+ // Include components outside of the routes
+ // that are registered with `willPrefetch`
+ includeGlobal: true,
+}
+```
+
+Use the `apolloProvider.exportStates` method to get the JavaScript code you need to inject into the generated page to pass the apollo cache data to the client.
+
+It takes an `options` argument which defaults to:
+
+```javascript
+{
+ // Global variable name
+ globalName: '__APOLLO_STATE__',
+ // Global object on which the variable is set
+ attachTo: 'window',
+ // Prefix for the keys of each apollo client state
+ exportNamespace: '',
+}
+```
+
+### Creating the Apollo Clients
+
+It is recommended to create the apollo clients inside a function with an `ssr` argument, which is `true` on the server and `false` on the client.
+
+Here is an example:
+
+```javascript
+// src/api/apollo.js
+
+import Vue from 'vue'
+import { ApolloClient, createNetworkInterface } from 'apollo-client'
+import VueApollo from 'vue-apollo'
+
+// Install the vue plugin
+Vue.use(VueApollo)
+
+// Create the apollo client
+export function createApolloClient (ssr = false) {
+ let initialState
+
+ // If on the client, recover the injected state
+ if (!ssr && typeof window !== 'undefined') {
+ const state = window.__APOLLO_STATE__
+ if (state) {
+ // If you have multiple clients, use `state.`
+ initialState = state.defaultClient
+ }
+ }
+
+ const apolloClient = new ApolloClient({
+ networkInterface: createNetworkInterface({
+ // You should use an absolute URL here
+ uri: 'https://api.graph.cool/simple/v1/cj1jvw20v3n310152sv0sirl7',
+ transportBatching: true,
+ }),
+ ...(ssr ? {
+ // Set this on the server to optimize queries when SSR
+ ssrMode: true,
+ } : {
+ // Inject the state on the client
+ initialState,
+ // This will temporary disable query force-fetching
+ ssrForceFetchDelay: 100,
+ }),
+ })
+
+ return apolloClient
+}
+```
+
+Example for common `CreateApp` method:
+
+```javascript
+import Vue from 'vue'
+
+import VueRouter from 'vue-router'
+Vue.use(VueRouter)
+
+import Vuex from 'vuex'
+Vue.use(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'
+
+function createApp (context) {
+ const router = new VueRouter({
+ mode: 'history',
+ routes,
+ })
+
+ const store = new Vuex.Store(storeOptions)
+
+ // sync the router with the vuex store.
+ // this registers `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
+```
+
+On the client:
+
+```javascript
+import CreateApp from './app'
+
+CreateApp({
+ ssr: false,
+})
+```
+
+On the server:
+
+```javascript
+import { CreateApp } from './app'
+
+const { app, router, store, apolloProvider } = CreateApp({
+ ssr: true,
+})
+
+// set router's location
+router.push(context.url)
+
+// wait until router has resolved possible async hooks
+router.onReady(() => {
+ // Prefetch, render HTML (see above)
+})
+```
---
diff --git a/src/apollo-provider.js b/src/apollo-provider.js
index 4bcafeb..bdcff66 100644
--- a/src/apollo-provider.js
+++ b/src/apollo-provider.js
@@ -1,3 +1,7 @@
+import omit from 'lodash.omit'
+import { VUE_APOLLO_QUERY_KEYWORDS } from './consts'
+import { getMergedDefinition } from './utils'
+
export class ApolloProvider {
constructor (options) {
if (!options) {
@@ -5,43 +9,146 @@ export class ApolloProvider {
}
this.clients = options.clients || {}
this.clients.defaultClient = this.defaultClient = options.defaultClient
- this._collecting = false
+
+ this.prefetchQueries = []
}
- collect (options) {
- const finalOptions = Object.assign({}, {
- waitForLoaded: true,
- }, options)
- this._ready = false
- this._promises = []
- this._collectingOptions = finalOptions
- this._isCollecting = true
- this._ensureReadyPromise = null
- return this.ensureReady.bind(this)
- }
-
- ensureReady () {
- if (this._ready) {
- return Promise.resolve()
- } else {
- if (!this._ensureReadyPromise) {
- this._ensureReadyPromise = this._ensureReady()
- }
- return this._ensureReadyPromise
- }
- }
-
- _waitFor (promise) {
- if (this._isCollecting) {
- this._promises.push(promise)
- }
- }
-
- _ensureReady () {
- this._isCollecting = false
- return Promise.all(this._promises).then((result) => {
- this._ready = true
- return result
+ willPrefetchQuery (queryOptions, client) {
+ this.prefetchQueries.push({
+ queryOptions,
+ client,
})
}
+
+ willPrefetch (component) {
+ component = getMergedDefinition(component)
+ const apolloOptions = component.apollo
+
+ if (!apolloOptions) {
+ return
+ }
+
+ const componentClient = apolloOptions.$client
+ for (let key in apolloOptions) {
+ const options = apolloOptions[key]
+ if (
+ !options.query || (
+ (typeof options.ssr === 'undefined' || options.ssr) &&
+ (typeof options.prefetch !== 'undefined' && options.prefetch)
+ )
+ ) {
+ this.willPrefetchQuery(options, options.client || componentClient)
+ }
+ }
+ }
+
+ willPrefetchComponents (definitions) {
+ for (const def of definitions) {
+ this.willPrefetch(def)
+ }
+ }
+
+ prefetchAll (context, components, options) {
+ // Optional components argument
+ if (!options && components && !Array.isArray(components)) {
+ options = components
+ components = undefined
+ }
+
+ const finalOptions = Object.assign({}, {
+ includeGlobal: true,
+ }, options)
+
+ if (components) {
+ this.willPrefetchComponents(components)
+ }
+
+ if (finalOptions.includeGlobal) {
+ this.willPrefetchComponents(globalPrefetchs)
+ }
+
+ return Promise.all(this.prefetchQueries.map(
+ o => this.prefetchQuery(o.queryOptions, context, o.client)
+ ))
+ }
+
+ prefetchQuery (queryOptions, context, client) {
+ let variables
+
+ // Client
+ if (!client) {
+ client = this.defaultClient
+ } else if (typeof client === 'string') {
+ client = this.clients[client]
+ if (!client) {
+ throw new Error(`[vue-apollo] Missing client '${client}' in 'apolloProvider'`)
+ }
+ }
+
+ // Simple query
+ if (!queryOptions.query) {
+ queryOptions = {
+ query: queryOptions,
+ }
+ } else {
+ const prefetch = queryOptions.prefetch
+ const prefetchType = typeof prefetch
+
+ // Resolve variables
+ if (prefetchType !== 'undefined') {
+ let result
+ if (prefetchType === 'function') {
+ result = prefetch(context)
+ } else {
+ result = prefetch
+ }
+
+ const optVariables = queryOptions.variables
+
+ if (!result) {
+ return Promise.resolve()
+ } else if (prefetchType === 'boolean' && typeof optVariables !== 'undefined') {
+ // Reuse `variables` option with `prefetch: true`
+ if (typeof optVariables === 'function') {
+ variables = optVariables.call(context)
+ } else {
+ variables = optVariables
+ }
+ } else {
+ variables = result
+ }
+ }
+ }
+
+ // Query
+ return new Promise((resolve, reject) => {
+ const options = omit(queryOptions, VUE_APOLLO_QUERY_KEYWORDS)
+ options.variables = variables
+ client.query(options).then(resolve, reject)
+ })
+ }
+
+ exportStates (options) {
+ const finalOptions = Object.assign({}, {
+ exportNamespace: '',
+ globalName: '__APOLLO_STATE__',
+ attachTo: 'window',
+ }, options)
+
+ let js = `${finalOptions.attachTo}.${finalOptions.globalName} = {`
+ for (const key in this.clients) {
+ const client = this.clients[key]
+ const state = { [client.reduxRootKey || 'apollo']: client.getInitialState() }
+ js += `['${finalOptions.exportNamespace}${key}']:${JSON.stringify(state)},`
+ }
+ js += `};`
+ return js
+ }
+}
+
+const globalPrefetchs = []
+
+export function willPrefetch (component) {
+ globalPrefetchs.push(component)
+ return component
}
diff --git a/src/consts.js b/src/consts.js
new file mode 100644
index 0000000..71f68e2
--- /dev/null
+++ b/src/consts.js
@@ -0,0 +1,12 @@
+export const VUE_APOLLO_QUERY_KEYWORDS = [
+ 'variables',
+ 'watch',
+ 'update',
+ 'result',
+ 'error',
+ 'loadingKey',
+ 'watchLoading',
+ 'skip',
+ 'throttle',
+ 'debounce',
+]
diff --git a/src/dollar-apollo.js b/src/dollar-apollo.js
index a15fa55..57a2915 100644
--- a/src/dollar-apollo.js
+++ b/src/dollar-apollo.js
@@ -15,33 +15,8 @@ export class DollarApollo {
return this._apolloProvider || this.vm.$root._apolloProvider
}
- _autoCollect (query) {
- if (this.provider._isCollecting) {
- let sub
- this.provider._waitFor(new Promise((resolve, reject) => {
- sub = query.subscribe({
- next: result => {
- if (!this.provider._collectingOptions.waitForLoaded || !result.loading) {
- resolve(result)
- }
- },
- error: error => {
- reject(error)
- },
- })
- }).then(result => {
- sub.unsubscribe()
- return result
- }, error => {
- sub.unsubscribe()
- return error
- }))
- }
- return query
- }
-
query (options) {
- return this._autoCollect(this.getClient(options).query(options))
+ return this.getClient(options).query(options)
}
getClient (options) {
@@ -77,7 +52,7 @@ export class DollarApollo {
this._apolloSubscriptions.push(sub)
return sub
}
- return this._autoCollect(observable)
+ return observable
}
mutate (options) {
diff --git a/src/index.js b/src/index.js
index f5c2eb0..b05fc50 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,8 +1,7 @@
import omit from 'lodash.omit'
import { DollarApollo } from './dollar-apollo'
import { ApolloProvider } from './apollo-provider'
-
-let Vue
+import { Globals } from './utils'
const keywords = [
'$subscribe',
@@ -46,7 +45,7 @@ const launch = function launch () {
let apollo = this.$options.apollo
if (apollo) {
if (apollo.subscribe) {
- Vue.util.warn('vue-apollo -> `subscribe` option is deprecated. Use the `$subscribe` option instead.')
+ Globals.Vue.util.warn('vue-apollo -> `subscribe` option is deprecated. Use the `$subscribe` option instead.')
}
if (apollo.$subscribe) {
@@ -72,11 +71,11 @@ function defineReactiveSetter ($apollo, key, value) {
}
}
-function install (pVue, options) {
+function install (Vue, options) {
if (install.installed) return
install.installed = true
- Vue = pVue
+ Globals.Vue = Vue
// Options merging
const merge = Vue.config.optionMergeStrategies.methods
@@ -133,3 +132,5 @@ function install (pVue, options) {
ApolloProvider.install = install
export default ApolloProvider
+
+export { willPrefetch } from './apollo-provider'
diff --git a/src/smart-apollo.js b/src/smart-apollo.js
index 92c7c78..3dca9c4 100644
--- a/src/smart-apollo.js
+++ b/src/smart-apollo.js
@@ -1,5 +1,6 @@
import omit from 'lodash.omit'
import { throttle, debounce } from './utils'
+import { VUE_APOLLO_QUERY_KEYWORDS } from './consts'
class SmartApollo {
type = null
@@ -137,18 +138,7 @@ class SmartApollo {
export class SmartQuery extends SmartApollo {
type = 'query'
- vueApolloSpecialKeys = [
- 'variables',
- 'watch',
- 'update',
- 'result',
- 'error',
- 'loadingKey',
- 'watchLoading',
- 'skip',
- 'throttle',
- 'debounce',
- ]
+ vueApolloSpecialKeys = VUE_APOLLO_QUERY_KEYWORDS
loading = false
constructor (vm, key, options, autostart = true) {
diff --git a/src/utils.js b/src/utils.js
index 3963f65..a5819e6 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -1,6 +1,8 @@
import loThrottle from 'lodash.throttle'
import loDebounce from 'lodash.debounce'
+export const Globals = {}
+
function factory (action) {
return (cb, options) => {
if (typeof options === 'number') {
@@ -14,3 +16,7 @@ function factory (action) {
export const throttle = factory(loThrottle)
export const debounce = factory(loDebounce)
+
+export function getMergedDefinition (def) {
+ return Globals.Vue.util.mergeOptions({}, def)
+}
diff --git a/ssr.js b/ssr.js
deleted file mode 100644
index e482bff..0000000
--- a/ssr.js
+++ /dev/null
@@ -1,112 +0,0 @@
-// app.js
-
-import VueApollo from 'vue-apollo'
-
-Vue.use(VueApollo)
-
-export default function createApp ({ createApolloClients }) {
- const router = new VueRouter({})
- const store = new Vuex.Store({})
- const { apolloClientA, apolloClientB } = createApolloClients()
- const apollo = new VueApollo({
- clients: {
- a: apolloClientA,
- b: apolloClientB,
- },
- defaultClient: apolloClientA,
- })
-
- const ensureReady = apollo.collect()
-
- const app = new Vue({
- el: '#app',
- router,
- store,
- apollo,
- ...App,
- })
-
- return { app, router, store, apollo, ensureReady }
-}
-
-// server.js
-
-import { ApolloClient, createNetworkInterface } from 'apollo-client'
-import { createLocalInterface } from 'apollo-local-query'
-import graphql from 'graphql'
-import { schema } from './graphql/schema'
-
-function createApolloClients() {
- const apolloClientA = new ApolloClient({
- ssrMode: true,
- networkInterface: createLocalInterface(graphql, schema)
- })
-
- const apolloClientB = new ApolloClient({
- ssrMode: true,
- networkInterface: createNetworkInterface({
- uri: 'http://my-awesome-api/graphql',
- transportBatching: true,
- }),
- })
-
- return {
- apolloClientA,
- apolloClientB,
- }
-}
-
-export default function render (context) {
- const { app, router, store, apollo, ensureReady } = createApp({ createApolloClients })
- router.push(context.url)
- router.onReady(async () => {
- // Wait for apollo queries to be loaded
- await ensureReady()
-
- // Inject initial apollo states
- let js = `window.__APOLLO_STATE__ = {`
- for (const key in apollo.clients) {
- const client = apollo.clients[key]
- const state = {[client.reduxRootKey]: client.getInitialState() }
- js += `['${key}']:${JSON.stringify(state)},`
- }
- js += `};`
-
- // TODO Render here
-
- // TODO Add the js to the response
- })
-}
-
-// client.js
-
-import { ApolloClient, createNetworkInterface } from 'apollo-client'
-
-function createApolloClients() {
- const state = window.__APOLLO_STATE__
-
- const apolloClientA = new ApolloClient({
- networkInterface: createNetworkInterface({
- uri: '/graphql',
- transportBatching: true,
- }),
- initialState: state.a,
- ssrForceFetchDelay: 100,
- })
-
- const apolloClientB = new ApolloClient({
- networkInterface: createNetworkInterface({
- uri: 'http://my-awesome-api/graphql',
- transportBatching: true,
- }),
- initialState: state.b,
- ssrForceFetchDelay: 100,
- })
-
- export {
- apolloClientA,
- apolloClientB,
- }
-}
-
-createApp({ createApolloClients })