Skip to content

Commit 3f57cf5

Browse files
authoredFeb 4, 2019
refactor(ssr): use ssrPrefetch (#469)
* refactor(ssr): use ssrPrefetch * fix: ssrPrefetch option was renamed to serverPrefetch * docs(ssr): vue version notice * fix: skip ssrPrefetch if apollo.$prefetch is false * docs: new SSR docs * chore: v3.0.0-beta.28
2 parents 05dd426 + 455a5e7 commit 3f57cf5

13 files changed

+306
-724
lines changed
 

‎dist/vue-apollo.esm.js

+70-18
Original file line numberDiff line numberDiff line change
@@ -413,10 +413,6 @@ function () {
413413
this._watchers = [];
414414
this._destroyed = false;
415415

416-
if (this.vm.$isServer) {
417-
this.options.fetchPolicy = 'cache-first';
418-
}
419-
420416
if (autostart) {
421417
this.autostart();
422418
}
@@ -724,14 +720,23 @@ function (_SmartApollo) {
724720
});
725721
}
726722

727-
_this = _possibleConstructorReturn(this, _getPrototypeOf(SmartQuery).call(this, vm, key, options, autostart));
723+
_this = _possibleConstructorReturn(this, _getPrototypeOf(SmartQuery).call(this, vm, key, options, false));
728724

729725
_defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), "type", 'query');
730726

731727
_defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), "vueApolloSpecialKeys", VUE_APOLLO_QUERY_KEYWORDS);
732728

733729
_defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), "_loading", false);
734730

731+
_this.firstRun = new Promise(function (resolve, reject) {
732+
_this._firstRunResolve = resolve;
733+
_this._firstRunReject = reject;
734+
});
735+
736+
if (_this.vm.$isServer) {
737+
_this.options.fetchPolicy = 'network-only';
738+
}
739+
735740
if (!options.manual) {
736741
_this.hasDataField = _this.vm.$data.hasOwnProperty(key);
737742

@@ -754,6 +759,10 @@ function (_SmartApollo) {
754759
}
755760
}
756761

762+
if (autostart) {
763+
_this.autostart();
764+
}
765+
757766
return _this;
758767
}
759768

@@ -827,7 +836,12 @@ function (_SmartApollo) {
827836
_get(_getPrototypeOf(SmartQuery.prototype), "nextResult", this).call(this, result);
828837

829838
var data = result.data,
830-
loading = result.loading;
839+
loading = result.loading,
840+
error = result.error;
841+
842+
if (error) {
843+
this.firstRunReject();
844+
}
831845

832846
if (!loading) {
833847
this.loadingDone();
@@ -861,7 +875,8 @@ function (_SmartApollo) {
861875
value: function catchError(error) {
862876
_get(_getPrototypeOf(SmartQuery.prototype), "catchError", this).call(this, error);
863877

864-
this.loadingDone();
878+
this.firstRunReject();
879+
this.loadingDone(error);
865880
this.nextResult(this.observer.currentResult()); // The observable closes the sub if an error occurs
866881

867882
this.resubscribeToQuery();
@@ -901,11 +916,17 @@ function (_SmartApollo) {
901916
}, {
902917
key: "loadingDone",
903918
value: function loadingDone() {
919+
var error = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
920+
904921
if (this.loading) {
905922
this.applyLoadingModifier(-1);
906923
}
907924

908925
this.loading = false;
926+
927+
if (!error) {
928+
this.firstRunResolve();
929+
}
909930
}
910931
}, {
911932
key: "fetchMore",
@@ -995,6 +1016,24 @@ function (_SmartApollo) {
9951016
return (_this$observer4 = this.observer).stopPolling.apply(_this$observer4, arguments);
9961017
}
9971018
}
1019+
}, {
1020+
key: "firstRunResolve",
1021+
value: function firstRunResolve() {
1022+
if (this._firstRunResolve) {
1023+
this._firstRunResolve();
1024+
1025+
this._firstRunResolve = null;
1026+
}
1027+
}
1028+
}, {
1029+
key: "firstRunReject",
1030+
value: function firstRunReject() {
1031+
if (this._firstRunReject) {
1032+
this._firstRunReject();
1033+
1034+
this._firstRunReject = null;
1035+
}
1036+
}
9981037
}, {
9991038
key: "destroy",
10001039
value: function destroy() {
@@ -1733,7 +1772,7 @@ function hasProperty(holder, key) {
17331772
return typeof holder !== 'undefined' && Object.prototype.hasOwnProperty.call(holder, key);
17341773
}
17351774

1736-
function initDollarApollo() {
1775+
function initProvider() {
17371776
var options = this.$options; // ApolloProvider injection
17381777

17391778
var optionValue = options.apolloProvider;
@@ -1797,6 +1836,8 @@ function launch() {
17971836
var apollo = this.$options.apollo;
17981837

17991838
if (apollo) {
1839+
this.$_apolloPromises = [];
1840+
18001841
if (!apollo.$init) {
18011842
apollo.$init = true; // Default options applied to `apollo` options
18021843

@@ -1824,7 +1865,11 @@ function launch() {
18241865
for (var key in apollo) {
18251866
if (key.charAt(0) !== '$') {
18261867
var options = apollo[key];
1827-
this.$apollo.addSmartQuery(key, options);
1868+
var smart = this.$apollo.addSmartQuery(key, options);
1869+
1870+
if (options.prefetch !== false && apollo.$prefetch !== false) {
1871+
this.$_apolloPromises.push(smart.firstRun);
1872+
}
18281873
}
18291874
}
18301875

@@ -1850,9 +1895,16 @@ function defineReactiveSetter($apollo, key, value, deep) {
18501895
}
18511896
}
18521897

1898+
function destroy() {
1899+
if (this.$_apollo) {
1900+
this.$_apollo.destroy();
1901+
this.$_apollo = null;
1902+
}
1903+
}
1904+
18531905
function installMixin(Vue, vueVersion) {
18541906
Vue.mixin(_objectSpread({}, vueVersion === '1' ? {
1855-
init: initDollarApollo
1907+
init: initProvider
18561908
} : {}, vueVersion === '2' ? {
18571909
data: function data() {
18581910
return {
@@ -1864,17 +1916,17 @@ function installMixin(Vue, vueVersion) {
18641916
};
18651917
},
18661918
beforeCreate: function beforeCreate() {
1867-
initDollarApollo.call(this);
1919+
initProvider.call(this);
18681920
proxyData.call(this);
1921+
},
1922+
serverPrefetch: function serverPrefetch() {
1923+
if (this.$_apolloPromises) {
1924+
return Promise.all(this.$_apolloPromises);
1925+
}
18691926
}
18701927
} : {}, {
18711928
created: launch,
1872-
destroyed: function destroyed() {
1873-
if (this.$_apollo) {
1874-
this.$_apollo.destroy();
1875-
this.$_apollo = null;
1876-
}
1877-
}
1929+
destroyed: destroy
18781930
}));
18791931
}
18801932

@@ -1925,7 +1977,7 @@ function install(Vue, options) {
19251977
}
19261978
ApolloProvider.install = install; // eslint-disable-next-line no-undef
19271979

1928-
ApolloProvider.version = "3.0.0-beta.27"; // Apollo provider
1980+
ApolloProvider.version = "3.0.0-beta.28"; // Apollo provider
19291981

19301982
var ApolloProvider$1 = ApolloProvider; // Components
19311983

‎dist/vue-apollo.min.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎dist/vue-apollo.umd.js

+70-18
Original file line numberDiff line numberDiff line change
@@ -419,10 +419,6 @@
419419
this._watchers = [];
420420
this._destroyed = false;
421421

422-
if (this.vm.$isServer) {
423-
this.options.fetchPolicy = 'cache-first';
424-
}
425-
426422
if (autostart) {
427423
this.autostart();
428424
}
@@ -730,14 +726,23 @@
730726
});
731727
}
732728

733-
_this = _possibleConstructorReturn(this, _getPrototypeOf(SmartQuery).call(this, vm, key, options, autostart));
729+
_this = _possibleConstructorReturn(this, _getPrototypeOf(SmartQuery).call(this, vm, key, options, false));
734730

735731
_defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), "type", 'query');
736732

737733
_defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), "vueApolloSpecialKeys", VUE_APOLLO_QUERY_KEYWORDS);
738734

739735
_defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), "_loading", false);
740736

737+
_this.firstRun = new Promise(function (resolve, reject) {
738+
_this._firstRunResolve = resolve;
739+
_this._firstRunReject = reject;
740+
});
741+
742+
if (_this.vm.$isServer) {
743+
_this.options.fetchPolicy = 'network-only';
744+
}
745+
741746
if (!options.manual) {
742747
_this.hasDataField = _this.vm.$data.hasOwnProperty(key);
743748

@@ -760,6 +765,10 @@
760765
}
761766
}
762767

768+
if (autostart) {
769+
_this.autostart();
770+
}
771+
763772
return _this;
764773
}
765774

@@ -833,7 +842,12 @@
833842
_get(_getPrototypeOf(SmartQuery.prototype), "nextResult", this).call(this, result);
834843

835844
var data = result.data,
836-
loading = result.loading;
845+
loading = result.loading,
846+
error = result.error;
847+
848+
if (error) {
849+
this.firstRunReject();
850+
}
837851

838852
if (!loading) {
839853
this.loadingDone();
@@ -867,7 +881,8 @@
867881
value: function catchError(error) {
868882
_get(_getPrototypeOf(SmartQuery.prototype), "catchError", this).call(this, error);
869883

870-
this.loadingDone();
884+
this.firstRunReject();
885+
this.loadingDone(error);
871886
this.nextResult(this.observer.currentResult()); // The observable closes the sub if an error occurs
872887

873888
this.resubscribeToQuery();
@@ -907,11 +922,17 @@
907922
}, {
908923
key: "loadingDone",
909924
value: function loadingDone() {
925+
var error = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
926+
910927
if (this.loading) {
911928
this.applyLoadingModifier(-1);
912929
}
913930

914931
this.loading = false;
932+
933+
if (!error) {
934+
this.firstRunResolve();
935+
}
915936
}
916937
}, {
917938
key: "fetchMore",
@@ -1001,6 +1022,24 @@
10011022
return (_this$observer4 = this.observer).stopPolling.apply(_this$observer4, arguments);
10021023
}
10031024
}
1025+
}, {
1026+
key: "firstRunResolve",
1027+
value: function firstRunResolve() {
1028+
if (this._firstRunResolve) {
1029+
this._firstRunResolve();
1030+
1031+
this._firstRunResolve = null;
1032+
}
1033+
}
1034+
}, {
1035+
key: "firstRunReject",
1036+
value: function firstRunReject() {
1037+
if (this._firstRunReject) {
1038+
this._firstRunReject();
1039+
1040+
this._firstRunReject = null;
1041+
}
1042+
}
10041043
}, {
10051044
key: "destroy",
10061045
value: function destroy() {
@@ -1739,7 +1778,7 @@
17391778
return typeof holder !== 'undefined' && Object.prototype.hasOwnProperty.call(holder, key);
17401779
}
17411780

1742-
function initDollarApollo() {
1781+
function initProvider() {
17431782
var options = this.$options; // ApolloProvider injection
17441783

17451784
var optionValue = options.apolloProvider;
@@ -1803,6 +1842,8 @@
18031842
var apollo = this.$options.apollo;
18041843

18051844
if (apollo) {
1845+
this.$_apolloPromises = [];
1846+
18061847
if (!apollo.$init) {
18071848
apollo.$init = true; // Default options applied to `apollo` options
18081849

@@ -1830,7 +1871,11 @@
18301871
for (var key in apollo) {
18311872
if (key.charAt(0) !== '$') {
18321873
var options = apollo[key];
1833-
this.$apollo.addSmartQuery(key, options);
1874+
var smart = this.$apollo.addSmartQuery(key, options);
1875+
1876+
if (options.prefetch !== false && apollo.$prefetch !== false) {
1877+
this.$_apolloPromises.push(smart.firstRun);
1878+
}
18341879
}
18351880
}
18361881

@@ -1856,9 +1901,16 @@
18561901
}
18571902
}
18581903

1904+
function destroy() {
1905+
if (this.$_apollo) {
1906+
this.$_apollo.destroy();
1907+
this.$_apollo = null;
1908+
}
1909+
}
1910+
18591911
function installMixin(Vue, vueVersion) {
18601912
Vue.mixin(_objectSpread({}, vueVersion === '1' ? {
1861-
init: initDollarApollo
1913+
init: initProvider
18621914
} : {}, vueVersion === '2' ? {
18631915
data: function data() {
18641916
return {
@@ -1870,17 +1922,17 @@
18701922
};
18711923
},
18721924
beforeCreate: function beforeCreate() {
1873-
initDollarApollo.call(this);
1925+
initProvider.call(this);
18741926
proxyData.call(this);
1927+
},
1928+
serverPrefetch: function serverPrefetch() {
1929+
if (this.$_apolloPromises) {
1930+
return Promise.all(this.$_apolloPromises);
1931+
}
18751932
}
18761933
} : {}, {
18771934
created: launch,
1878-
destroyed: function destroyed() {
1879-
if (this.$_apollo) {
1880-
this.$_apollo.destroy();
1881-
this.$_apollo = null;
1882-
}
1883-
}
1935+
destroyed: destroy
18841936
}));
18851937
}
18861938

@@ -1931,7 +1983,7 @@
19311983
}
19321984
ApolloProvider.install = install; // eslint-disable-next-line no-undef
19331985

1934-
ApolloProvider.version = "3.0.0-beta.27"; // Apollo provider
1986+
ApolloProvider.version = "3.0.0-beta.28"; // Apollo provider
19351987

19361988
var ApolloProvider$1 = ApolloProvider; // Components
19371989

‎docs/.vuepress/config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ module.exports = {
5050
link: 'https://www.patreon.com/akryum',
5151
},
5252
],
53-
sidebarDepth: 3,
53+
sidebarDepth: 2,
5454
sidebar: {
5555
'/guide/': [
5656
'',

‎docs/api/ssr.md

-76
Original file line numberDiff line numberDiff line change
@@ -6,45 +6,6 @@ See [SSR guide](../guide/ssr.md).
66

77
## Methods
88

9-
### install
10-
11-
Install the SSR plugin only on the server with:
12-
13-
```js
14-
Vue.use(ApolloSSR)
15-
```
16-
17-
You can pass additional options like this:
18-
19-
```js
20-
Vue.use(ApolloSSR, {
21-
fetchPolicy: 'network-only',
22-
suppressRenderErrors: false,
23-
})
24-
```
25-
26-
#### fetchPolicy
27-
28-
When an Apollo query is prefetched, it's recommended to override `fetchPolicy` to force the queries to happen.
29-
30-
Default value: `'network-only'`.
31-
32-
#### suppressRenderErrors
33-
34-
Silent the fake render errors.
35-
36-
Default value: `false`.
37-
38-
### prefetchAll
39-
40-
Prefetches all queued component definitions and returns a promise resolved when all corresponding apollo data is ready.
41-
42-
```js
43-
await ApolloSSR.prefetchAll (apolloProvider, componentDefs, context)
44-
```
45-
46-
`context` is passed as the argument to the `prefetch` options inside the smart queries. It may contain the route and the store.
47-
489
### getStates
4910

5011
Returns the apollo stores states as JavaScript objects.
@@ -82,40 +43,3 @@ const js = ApolloSSR.exportStates(apolloProvider, options)
8243
exportNamespace: '',
8344
}
8445
```
85-
86-
### globalPrefetch
87-
88-
Allow you to register a component to be prefetched explicitly.
89-
90-
Simple example:
91-
92-
```js
93-
import MyComponent from '@/components/MyComponent.vue'
94-
95-
ApolloSSR.globalPrefetch(() => MyComponent)
96-
```
97-
98-
You can disable prefetching depending on context:
99-
100-
```js
101-
ApolloSSR.globalPrefetch(context => {
102-
if (context.route.name === 'foo'){
103-
return MyComponent
104-
}
105-
})
106-
```
107-
108-
### mockInstance
109-
110-
During `prefetchAll`, the app components tree is re-created with fake instances so the process is faster. You can apply plugins to modify the fake instances to prevent their render functions to crash if you have helpers like `this.$http` that is accessed in the template or render function (typically `Undefined error`). It's recommended to mock those helpers to improve performance.
111-
112-
```js
113-
const noop = () => {}
114-
115-
ApolloSSR.mockInstance({
116-
apply: vm => {
117-
// Mock $http
118-
vm.$http = noop
119-
},
120-
})
121-
```

‎docs/guide/ssr.md

+100-147
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# Server-Side Rendering
22

3-
## Vue CLI Plugin
3+
::: warning
4+
**Requires Vue 2.6+ with `serverPrefetch` support**
5+
:::
6+
7+
## Vue CLI plugin
48

59
I made a plugin for [vue-cli](http://cli.vuejs.org) so you can transform your vue-apollo app into an isomorphic SSR app in literary two minutes! ✨🚀
610

@@ -12,17 +16,16 @@ vue add @akryum/ssr
1216

1317
[More info](https://github.com/Akryum/vue-cli-plugin-ssr)
1418

15-
## Prefetch components
19+
## Component prefetching
1620

17-
On the queries you want to prefetch on the server, add the `prefetch` option. It can either be:
18-
- a variables object,
19-
- a function that gets the context object (which can contain the URL for example) and return a variables object,
20-
- `false` to disable prefetching for this query.
21+
::: tip
22+
Follow the [offical SSR guide](https://ssr.vuejs.org) to learn more about Server-Side Rendering with Vue.
23+
:::
2124

22-
If you are returning a variables object in the `prefetch` option, make sure it matches the result of the `variables` option. If they do not match, the query's data property will not be populated while rendering the template server-side.
25+
By default with `vue-server-renderer`, all the GraphQL queries in your server-side rendered components will be prefetched automatically.
2326

24-
::: danger
25-
You don't have access to the component instance when doing prefetching on the server.
27+
::: tip
28+
You have access to `this` in options like `variables`, even on the server!
2629
:::
2730

2831
Example:
@@ -31,7 +34,6 @@ Example:
3134
export default {
3235
apollo: {
3336
allPosts: {
34-
// This will be prefetched
3537
query: gql`query AllPosts {
3638
allPosts {
3739
id
@@ -57,11 +59,6 @@ export default {
5759
description
5860
}
5961
}`,
60-
prefetch: ({ route }) => {
61-
return {
62-
id: route.params.id,
63-
}
64-
},
6562
variables () {
6663
return {
6764
id: this.id,
@@ -72,11 +69,13 @@ export default {
7269
}
7370
```
7471

75-
### Skip prefetching
72+
## Skip prefetching
73+
74+
You can skip server-side prefetching on a query with the `prefetch` option set to `false`.
7675

7776
Example that doesn't prefetch the query:
7877

79-
```js
78+
```js{12}
8079
export default {
8180
apollo: {
8281
allPosts: {
@@ -96,7 +95,7 @@ export default {
9695

9796
If you want to skip prefetching all the queries for a specific component, use the `$prefetch` option:
9897

99-
```js
98+
```js{4}
10099
export default {
101100
apollo: {
102101
// Don't prefetch any query
@@ -114,128 +113,16 @@ export default {
114113
}
115114
```
116115

117-
You can also put a `no-prefetch` attribute on any component so it will be ignored while walking the tree to gather the Apollo queries:
118-
119-
```vue
120-
<ApolloQuery no-prefetch>
121-
```
122-
123-
## On the server
124-
125-
In the server entry, you need to install `ApolloSSR` plugin into Vue:
126-
127-
```js
128-
import Vue from 'vue'
129-
import ApolloSSR from 'vue-apollo/ssr'
130-
131-
Vue.use(ApolloSSR)
132-
```
133-
134-
To prefetch all the apollo queries you marked, use the `ApolloSSR.prefetchAll` method. The first argument is the `apolloProvider`. The second argument is the array of component definition to include (e.g. from `router.getMatchedComponents` method). The third argument is the context object passed to the `prefetch` hooks (see above). It is recommended to pass the vue-router `currentRoute` object. It returns a promise resolved when all the apollo queries are loaded.
135-
136-
Here is an example with vue-router and a Vuex store:
137-
138-
```js
139-
import Vue from 'vue'
140-
import ApolloSSR from 'vue-apollo/ssr'
141-
import App from './App.vue'
142-
143-
Vue.use(ApolloSSR, {
144-
// SSR config
145-
fetchPolicy: 'network-only',
146-
suppressRenderErrors: false,
147-
})
148-
149-
export default () => new Promise((resolve, reject) => {
150-
const { app, router, store, apolloProvider } = CreateApp({
151-
ssr: true,
152-
})
153-
154-
// set router's location
155-
router.push(context.url)
156-
157-
// wait until router has resolved possible async hooks
158-
router.onReady(() => {
159-
const matchedComponents = router.getMatchedComponents()
160-
161-
// no matched routes
162-
if (!matchedComponents.length) {
163-
reject({ code: 404 })
164-
}
165-
166-
let js = ''
167-
168-
// Call preFetch hooks on components matched by the route.
169-
// A preFetch hook dispatches a store action and returns a Promise,
170-
// which is resolved when the action is complete and store state has been
171-
// updated.
172-
173-
// Vuex Store prefetch
174-
Promise.all(matchedComponents.map(component => {
175-
return component.asyncData && component.asyncData({
176-
store,
177-
route: router.currentRoute,
178-
})
179-
}))
180-
// Apollo prefetch
181-
// This will prefetch all the Apollo queries in the whole app
182-
.then(() => ApolloSSR.prefetchAll(apolloProvider, [App, ...matchedComponents], {
183-
store,
184-
route: router.currentRoute,
185-
}))
186-
.then(() => {
187-
// Inject the Vuex state and the Apollo cache on the page.
188-
// This will prevent unnecessary queries.
189-
190-
// Vuex
191-
js += `window.__INITIAL_STATE__=${JSON.stringify(store.state)};`
192-
193-
// Apollo
194-
js += ApolloSSR.exportStates(apolloProvider)
195-
196-
resolve({
197-
app,
198-
js,
199-
})
200-
}).catch(reject)
201-
})
202-
})
203-
```
204-
205-
Use the `ApolloSSR.exportStates(apolloProvider, options)` method to get the JavaScript code you need to inject into the generated page to pass the apollo cache data to the client.
206-
207-
It takes an `options` argument which defaults to:
208-
209-
```js
210-
{
211-
// Global variable name
212-
globalName: '__APOLLO_STATE__',
213-
// Global object on which the variable is set
214-
attachTo: 'window',
215-
// Prefix for the keys of each apollo client state
216-
exportNamespace: '',
217-
}
218-
```
219-
220-
You can also use the `ApolloSSR.getStates(apolloProvider, options)` method to get the JS object instead of the script string.
221-
222-
It takes an `options` argument which defaults to:
223-
224-
```js
225-
{
226-
// Prefix for the keys of each apollo client state
227-
exportNamespace: '',
228-
}
229-
```
230-
231-
### Creating the Apollo Clients
116+
## Create Apollo client
232117

233118
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.
234119

120+
If `ssr` is false, we try to restore the state of the Apollo cache with `cache.restore`, by getting the `window.__APOLLO_STATE__` variable that we will inject in the HTML page on the server during SSR.
121+
235122
Here is an example:
236123

237-
```js
238-
// src/api/apollo.js
124+
```js{21-31}
125+
// apollo.js
239126
240127
import Vue from 'vue'
241128
import { ApolloClient } from 'apollo-client'
@@ -283,16 +170,24 @@ export function createApolloClient (ssr = false) {
283170
}
284171
```
285172

286-
Example for common `CreateApp` method:
173+
## Create app
174+
175+
Instead of creating our root Vue instance right away, we use a `createApp` function that accept a `context` parameter.
176+
177+
This function will be used both on the client and server entries with a different `ssr` value in the context. We use this value in the `createApolloClient` method we wrote previously.
178+
179+
Example for common `createApp` method:
180+
181+
```js{9,37}
182+
// app.js
287183
288-
```js
289184
import Vue from 'vue'
290185
import VueRouter from 'vue-router'
291186
import Vuex from 'vuex'
292187
import { sync } from 'vuex-router-sync'
293188
294189
import VueApollo from 'vue-apollo'
295-
import { createApolloClient } from './api/apollo'
190+
import { createApolloClient } from './apollo'
296191
297192
import App from './ui/App.vue'
298193
import routes from './routes'
@@ -313,6 +208,12 @@ function createApp (context) {
313208
// this registers `store.state.route`
314209
sync(store, router)
315210
211+
// Vuex state restoration
212+
if (!context.ssr && window.__INITIAL_STATE__) {
213+
// We initialize the store state with the data injected from the server
214+
store.replaceState(window.__INITIAL_STATE__)
215+
}
216+
316217
// Apollo
317218
const apolloClient = createApolloClient(context.ssr)
318219
const apolloProvider = new VueApollo({
@@ -336,23 +237,34 @@ function createApp (context) {
336237
export default createApp
337238
```
338239

339-
On the client:
240+
## Client entry
241+
242+
The client entry is very simple -- we just call `createApp` with `ssr` being `false`:
340243

341244
```js
342-
import CreateApp from './app'
245+
// client-entry.js
246+
247+
import createApp from './app'
343248

344-
CreateApp({
249+
createApp({
345250
ssr: false,
346251
})
347252
```
348253

349-
On the server:
254+
## Server entry
350255

351-
```js
352-
import CreateApp from './app'
256+
Nothing special is required apart from storing the Apollo cache to inject it in the client HTML. Learn more about [server entry with routing](https://ssr.vuejs.org/guide/routing.html#routing-with-vue-router) and [data prefetching](https://ssr.vuejs.org/guide/data.html#data-store) in the official SSR guide.
257+
258+
Here is an example with vue-router and a Vuex store:
259+
260+
```js{3,26}
261+
// server-entry.js
262+
263+
import ApolloSSR from 'vue-apollo/ssr'
264+
import createApp from './app'
353265
354266
export default () => new Promise((resolve, reject) => {
355-
const { app, router, store, apolloProvider } = CreateApp({
267+
const { app, router, store, apolloProvider } = createApp({
356268
ssr: true,
357269
})
358270
@@ -361,9 +273,50 @@ export default () => new Promise((resolve, reject) => {
361273
362274
// wait until router has resolved possible async hooks
363275
router.onReady(() => {
364-
// Prefetch, render HTML (see above)
276+
// This `rendered` hook is called when the app has finished rendering
277+
context.rendered = () => {
278+
// After the app is rendered, our store is now
279+
// filled with the state from our components.
280+
// When we attach the state to the context, and the `template` option
281+
// is used for the renderer, the state will automatically be
282+
// serialized and injected into the HTML as `window.__INITIAL_STATE__`.
283+
context.state = store.state
284+
285+
// ALso inject the apollo cache state
286+
context.apolloState = ApolloSSR.getStates(apolloProvider)
287+
}
288+
resolve(app)
365289
})
366290
})
367291
```
368292

369-
See the [SSR API](../api/ssr.md) for more details and other features.
293+
Use the [ApolloSSR.getStates](../api/ssr.md#getstates) method to get the JavaScript code you need to inject into the generated page to pass the apollo cache data to the client.
294+
295+
In the [page template](https://ssr.vuejs.org/guide/#using-a-page-template), use the `renderState` helper:
296+
297+
```html
298+
{{{ renderState({ contextKey: 'apolloState', windowKey: '__APOLLO_STATE__' }) }}}
299+
```
300+
301+
Here is a full example:
302+
303+
```html{15}
304+
<!DOCTYPE html>
305+
<html>
306+
<head>
307+
<meta charset="utf-8">
308+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
309+
<meta name="viewport" content="width=device-width,initial-scale=1.0">
310+
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
311+
<title>{{ title }}</title>
312+
{{{ renderResourceHints() }}}
313+
{{{ renderStyles() }}}
314+
</head>
315+
<body>
316+
<!--vue-ssr-outlet-->
317+
{{{ renderState() }}}
318+
{{{ renderState({ contextKey: 'apolloState', windowKey: '__APOLLO_STATE__' }) }}}
319+
{{{ renderScripts() }}}
320+
</body>
321+
</html>
322+
```

‎package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "vue-apollo",
3-
"version": "3.0.0-beta.27",
3+
"version": "3.0.0-beta.28",
44
"description": "Use Apollo and GraphQL with Vue.js",
55
"main": "dist/vue-apollo.umd.js",
66
"module": "dist/vue-apollo.esm.js",

‎src/mixin.js

+23-10
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ function hasProperty (holder, key) {
44
return typeof holder !== 'undefined' && Object.prototype.hasOwnProperty.call(holder, key)
55
}
66

7-
function initDollarApollo () {
7+
function initProvider () {
88
const options = this.$options
99
// ApolloProvider injection
1010
const optionValue = options.apolloProvider
@@ -60,6 +60,8 @@ function launch () {
6060
let apollo = this.$options.apollo
6161

6262
if (apollo) {
63+
this.$_apolloPromises = []
64+
6365
if (!apollo.$init) {
6466
apollo.$init = true
6567

@@ -88,7 +90,10 @@ function launch () {
8890
for (let key in apollo) {
8991
if (key.charAt(0) !== '$') {
9092
let options = apollo[key]
91-
this.$apollo.addSmartQuery(key, options)
93+
const smart = this.$apollo.addSmartQuery(key, options)
94+
if (options.prefetch !== false && apollo.$prefetch !== false) {
95+
this.$_apolloPromises.push(smart.firstRun)
96+
}
9297
}
9398
}
9499

@@ -114,10 +119,17 @@ function defineReactiveSetter ($apollo, key, value, deep) {
114119
}
115120
}
116121

122+
function destroy () {
123+
if (this.$_apollo) {
124+
this.$_apollo.destroy()
125+
this.$_apollo = null
126+
}
127+
}
128+
117129
export function installMixin (Vue, vueVersion) {
118130
Vue.mixin({
119131
...vueVersion === '1' ? {
120-
init: initDollarApollo,
132+
init: initProvider,
121133
} : {},
122134

123135
...vueVersion === '2' ? {
@@ -132,18 +144,19 @@ export function installMixin (Vue, vueVersion) {
132144
},
133145

134146
beforeCreate () {
135-
initDollarApollo.call(this)
147+
initProvider.call(this)
136148
proxyData.call(this)
137149
},
150+
151+
serverPrefetch () {
152+
if (this.$_apolloPromises) {
153+
return Promise.all(this.$_apolloPromises)
154+
}
155+
},
138156
} : {},
139157

140158
created: launch,
141159

142-
destroyed: function () {
143-
if (this.$_apollo) {
144-
this.$_apollo.destroy()
145-
this.$_apollo = null
146-
}
147-
},
160+
destroyed: destroy,
148161
})
149162
}

‎src/smart-apollo.js

-4
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,6 @@ export default class SmartApollo {
1313
this._watchers = []
1414
this._destroyed = false
1515

16-
if (this.vm.$isServer) {
17-
this.options.fetchPolicy = 'cache-first'
18-
}
19-
2016
if (autostart) {
2117
this.autostart()
2218
}

‎src/smart-query.js

+40-4
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,16 @@ export default class SmartQuery extends SmartApollo {
2222
})
2323
}
2424

25-
super(vm, key, options, autostart)
25+
super(vm, key, options, false)
26+
27+
this.firstRun = new Promise((resolve, reject) => {
28+
this._firstRunResolve = resolve
29+
this._firstRunReject = reject
30+
})
31+
32+
if (this.vm.$isServer) {
33+
this.options.fetchPolicy = 'network-only'
34+
}
2635

2736
if (!options.manual) {
2837
this.hasDataField = this.vm.$data.hasOwnProperty(key)
@@ -40,6 +49,10 @@ export default class SmartQuery extends SmartApollo {
4049
})
4150
}
4251
}
52+
53+
if (autostart) {
54+
this.autostart()
55+
}
4356
}
4457

4558
get client () {
@@ -121,7 +134,11 @@ export default class SmartQuery extends SmartApollo {
121134
nextResult (result) {
122135
super.nextResult(result)
123136

124-
const { data, loading } = result
137+
const { data, loading, error } = result
138+
139+
if (error) {
140+
this.firstRunReject()
141+
}
125142

126143
if (!loading) {
127144
this.loadingDone()
@@ -154,7 +171,8 @@ export default class SmartQuery extends SmartApollo {
154171

155172
catchError (error) {
156173
super.catchError(error)
157-
this.loadingDone()
174+
this.firstRunReject()
175+
this.loadingDone(error)
158176
this.nextResult(this.observer.currentResult())
159177
// The observable closes the sub if an error occurs
160178
this.resubscribeToQuery()
@@ -189,11 +207,15 @@ export default class SmartQuery extends SmartApollo {
189207
this.watchLoading(value === 1, value)
190208
}
191209

192-
loadingDone () {
210+
loadingDone (error = null) {
193211
if (this.loading) {
194212
this.applyLoadingModifier(-1)
195213
}
196214
this.loading = false
215+
216+
if (!error) {
217+
this.firstRunResolve()
218+
}
197219
}
198220

199221
fetchMore (...args) {
@@ -260,6 +282,20 @@ export default class SmartQuery extends SmartApollo {
260282
}
261283
}
262284

285+
firstRunResolve () {
286+
if (this._firstRunResolve) {
287+
this._firstRunResolve()
288+
this._firstRunResolve = null
289+
}
290+
}
291+
292+
firstRunReject () {
293+
if (this._firstRunReject) {
294+
this._firstRunReject()
295+
this._firstRunReject = null
296+
}
297+
}
298+
263299
destroy () {
264300
super.destroy()
265301

‎ssr/consts.js

-36
This file was deleted.

‎ssr/index.js

-211
Original file line numberDiff line numberDiff line change
@@ -1,214 +1,3 @@
1-
const chalk = require('chalk')
2-
const { VUE_APOLLO_QUERY_KEYWORDS } = require('../lib/consts')
3-
const { createFakeInstance, resolveComponent } = require('./utils')
4-
const { Globals, getMergedDefinition, omit } = require('../lib/utils')
5-
6-
const config = exports.config = {
7-
globalPrefetchs: [],
8-
fakeInstanceMocks: [],
9-
fetchPolicy: 'network-only',
10-
suppressRenderErrors: false,
11-
}
12-
13-
exports.install = function (Vue, options = {}) {
14-
Globals.Vue = Vue
15-
Object.assign(config, options)
16-
}
17-
18-
exports.globalPrefetch = function (handler) {
19-
config.globalPrefetchs.push(handler)
20-
}
21-
22-
exports.mockInstance = function (plugin) {
23-
config.fakeInstanceMocks.push(plugin)
24-
}
25-
26-
exports.prefetchAll = function (apolloProvider, components = [], context = {}) {
27-
const globalPrefetchs = config.globalPrefetchs.map(handler => handler(context)).filter(Boolean)
28-
return exports.getQueriesFromTree(components.concat(globalPrefetchs), context)
29-
.then(queries => Promise.all(queries.map(
30-
query => prefetchQuery(apolloProvider, query, context)
31-
)))
32-
}
33-
34-
exports.getQueriesFromTree = function (components, context) {
35-
const queries = []
36-
return Promise.all(
37-
components.map(component => walkTree(component, {}, undefined, [], context, queries, components))
38-
).then(() => queries)
39-
}
40-
41-
function walkTree (component, data, parent, children, context, queries, components) {
42-
component = getMergedDefinition(component)
43-
return new Promise((resolve, reject) => {
44-
const queue = []
45-
data = data || {}
46-
const vm = createFakeInstance(component, data, parent, children, context)
47-
48-
// Mocks
49-
for (const mock of config.fakeInstanceMocks) {
50-
mock.apply(mock)
51-
}
52-
53-
// Render h function
54-
vm.$createElement = (el, data, children) => {
55-
if (typeof data === 'string' || Array.isArray(data)) {
56-
children = data
57-
data = {}
58-
}
59-
60-
// No Prefetch flag
61-
if (data && data.attrs &&
62-
data.attrs['no-prefetch'] !== undefined &&
63-
data.attrs['no-prefetch'] !== false) {
64-
return
65-
}
66-
67-
queue.push(resolveComponent(el, component).then(resolvedComponent => {
68-
let child
69-
if (resolvedComponent && !components.includes(resolvedComponent)) {
70-
child = {
71-
component: resolvedComponent,
72-
data,
73-
children,
74-
}
75-
}
76-
return child
77-
}))
78-
}
79-
80-
prefetchComponent(component, vm, queries)
81-
82-
try {
83-
component.render.call(vm, vm.$createElement)
84-
} catch (e) {
85-
if (!config.suppressRenderErrors) {
86-
console.log(chalk.red(`Error while rendering ${component.name || component.__file}`))
87-
console.log(e.stack)
88-
}
89-
}
90-
91-
Promise.all(queue).then(queue => queue.filter(child => !!child).map(
92-
child => walkTree(child.component, child.data, vm, child.children, context, queries, components)
93-
)).then(() => resolve())
94-
})
95-
}
96-
97-
function prefetchComponent (component, vm, queries) {
98-
const apolloOptions = component.apollo
99-
100-
if (!apolloOptions) return
101-
if (apolloOptions.$prefetch === false) return
102-
103-
const componentClient = apolloOptions.$client
104-
for (let key in apolloOptions) {
105-
const options = apolloOptions[key]
106-
if (
107-
key.charAt(0) !== '$' && (
108-
!options.query || (
109-
(typeof options.ssr === 'undefined' || options.ssr) &&
110-
options.prefetch !== false
111-
)
112-
)
113-
) {
114-
queries.push({
115-
queryOptions: options,
116-
client: options.client || componentClient,
117-
vm,
118-
})
119-
}
120-
}
121-
}
122-
123-
function prefetchQuery (apolloProvider, query, context) {
124-
try {
125-
let variables
126-
127-
let { queryOptions, client, vm } = query
128-
129-
// Client
130-
if (typeof client === 'function') {
131-
client = client.call(vm)
132-
}
133-
if (!client) {
134-
client = apolloProvider.defaultClient
135-
} else if (typeof client === 'string') {
136-
client = apolloProvider.clients[client]
137-
if (!client) {
138-
throw new Error(`[vue-apollo] Missing client '${client}' in 'apolloProvider'`)
139-
}
140-
}
141-
142-
// Function query
143-
if (typeof queryOptions === 'function') {
144-
queryOptions = queryOptions.call(vm)
145-
}
146-
147-
// Simple query
148-
if (!queryOptions.query) {
149-
queryOptions = {
150-
query: queryOptions,
151-
}
152-
} else {
153-
const prefetch = queryOptions.prefetch
154-
const prefetchType = typeof prefetch
155-
156-
// Resolve variables
157-
let prefetchResult
158-
if (prefetchType !== 'undefined') {
159-
if (prefetchType === 'function') {
160-
prefetchResult = prefetch.call(vm, context)
161-
} else if (prefetchType === 'boolean') {
162-
if (prefetchResult === false) {
163-
return Promise.resolve()
164-
}
165-
} else {
166-
prefetchResult = prefetch
167-
}
168-
}
169-
170-
if (prefetchResult) {
171-
variables = prefetchResult
172-
} else {
173-
const optVariables = queryOptions.variables
174-
if (typeof optVariables !== 'undefined') {
175-
// Reuse `variables` option with `prefetch: true`
176-
if (typeof optVariables === 'function') {
177-
variables = optVariables.call(vm)
178-
} else {
179-
variables = optVariables
180-
}
181-
} else {
182-
variables = undefined
183-
}
184-
}
185-
}
186-
187-
// Query
188-
if (typeof queryOptions.query === 'function') {
189-
queryOptions.query = queryOptions.query.call(vm)
190-
}
191-
192-
// Default query options from apollo provider
193-
if (apolloProvider.defaultOptions && apolloProvider.defaultOptions.$query) {
194-
queryOptions = Object.assign({}, apolloProvider.defaultOptions.$query, queryOptions)
195-
}
196-
197-
// Remove vue-apollo specific options
198-
const options = omit(queryOptions, VUE_APOLLO_QUERY_KEYWORDS)
199-
options.variables = variables
200-
// Override fetchPolicy
201-
if (config.fetchPolicy != null) {
202-
options.fetchPolicy = config.fetchPolicy
203-
}
204-
205-
return client.query(options)
206-
} catch (e) {
207-
console.log(chalk.red(`[ERROR] While prefetching query`), query, chalk.grey(`Error stack trace:`))
208-
console.log(e.stack)
209-
}
210-
}
211-
2121
exports.getStates = function (apolloProvider, options) {
2132
const finalOptions = Object.assign({}, {
2143
exportNamespace: '',

‎ssr/utils.js

-197
This file was deleted.

0 commit comments

Comments
 (0)
Please sign in to comment.