diff --git a/dist/vue-apollo.esm.js b/dist/vue-apollo.esm.js index eb9f6643..8a144950 100644 --- a/dist/vue-apollo.esm.js +++ b/dist/vue-apollo.esm.js @@ -413,10 +413,6 @@ function () { this._watchers = []; this._destroyed = false; - if (this.vm.$isServer) { - this.options.fetchPolicy = 'cache-first'; - } - if (autostart) { this.autostart(); } @@ -724,7 +720,7 @@ function (_SmartApollo) { }); } - _this = _possibleConstructorReturn(this, _getPrototypeOf(SmartQuery).call(this, vm, key, options, autostart)); + _this = _possibleConstructorReturn(this, _getPrototypeOf(SmartQuery).call(this, vm, key, options, false)); _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), "type", 'query'); @@ -732,6 +728,15 @@ function (_SmartApollo) { _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), "_loading", false); + _this.firstRun = new Promise(function (resolve, reject) { + _this._firstRunResolve = resolve; + _this._firstRunReject = reject; + }); + + if (_this.vm.$isServer) { + _this.options.fetchPolicy = 'network-only'; + } + if (!options.manual) { _this.hasDataField = _this.vm.$data.hasOwnProperty(key); @@ -754,6 +759,10 @@ function (_SmartApollo) { } } + if (autostart) { + _this.autostart(); + } + return _this; } @@ -827,7 +836,12 @@ function (_SmartApollo) { _get(_getPrototypeOf(SmartQuery.prototype), "nextResult", this).call(this, result); var data = result.data, - loading = result.loading; + loading = result.loading, + error = result.error; + + if (error) { + this.firstRunReject(); + } if (!loading) { this.loadingDone(); @@ -861,7 +875,8 @@ function (_SmartApollo) { value: function catchError(error) { _get(_getPrototypeOf(SmartQuery.prototype), "catchError", this).call(this, error); - this.loadingDone(); + this.firstRunReject(); + this.loadingDone(error); this.nextResult(this.observer.currentResult()); // The observable closes the sub if an error occurs this.resubscribeToQuery(); @@ -901,11 +916,17 @@ function (_SmartApollo) { }, { key: "loadingDone", value: function loadingDone() { + var error = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; + if (this.loading) { this.applyLoadingModifier(-1); } this.loading = false; + + if (!error) { + this.firstRunResolve(); + } } }, { key: "fetchMore", @@ -995,6 +1016,24 @@ function (_SmartApollo) { return (_this$observer4 = this.observer).stopPolling.apply(_this$observer4, arguments); } } + }, { + key: "firstRunResolve", + value: function firstRunResolve() { + if (this._firstRunResolve) { + this._firstRunResolve(); + + this._firstRunResolve = null; + } + } + }, { + key: "firstRunReject", + value: function firstRunReject() { + if (this._firstRunReject) { + this._firstRunReject(); + + this._firstRunReject = null; + } + } }, { key: "destroy", value: function destroy() { @@ -1733,7 +1772,7 @@ function hasProperty(holder, key) { return typeof holder !== 'undefined' && Object.prototype.hasOwnProperty.call(holder, key); } -function initDollarApollo() { +function initProvider() { var options = this.$options; // ApolloProvider injection var optionValue = options.apolloProvider; @@ -1797,6 +1836,8 @@ function launch() { var apollo = this.$options.apollo; if (apollo) { + this.$_apolloPromises = []; + if (!apollo.$init) { apollo.$init = true; // Default options applied to `apollo` options @@ -1824,7 +1865,11 @@ function launch() { for (var key in apollo) { if (key.charAt(0) !== '$') { var options = apollo[key]; - this.$apollo.addSmartQuery(key, options); + var smart = this.$apollo.addSmartQuery(key, options); + + if (options.prefetch !== false && apollo.$prefetch !== false) { + this.$_apolloPromises.push(smart.firstRun); + } } } @@ -1850,9 +1895,16 @@ function defineReactiveSetter($apollo, key, value, deep) { } } +function destroy() { + if (this.$_apollo) { + this.$_apollo.destroy(); + this.$_apollo = null; + } +} + function installMixin(Vue, vueVersion) { Vue.mixin(_objectSpread({}, vueVersion === '1' ? { - init: initDollarApollo + init: initProvider } : {}, vueVersion === '2' ? { data: function data() { return { @@ -1864,17 +1916,17 @@ function installMixin(Vue, vueVersion) { }; }, beforeCreate: function beforeCreate() { - initDollarApollo.call(this); + initProvider.call(this); proxyData.call(this); + }, + serverPrefetch: function serverPrefetch() { + if (this.$_apolloPromises) { + return Promise.all(this.$_apolloPromises); + } } } : {}, { created: launch, - destroyed: function destroyed() { - if (this.$_apollo) { - this.$_apollo.destroy(); - this.$_apollo = null; - } - } + destroyed: destroy })); } @@ -1925,7 +1977,7 @@ function install(Vue, options) { } ApolloProvider.install = install; // eslint-disable-next-line no-undef -ApolloProvider.version = "3.0.0-beta.27"; // Apollo provider +ApolloProvider.version = "3.0.0-beta.28"; // Apollo provider var ApolloProvider$1 = ApolloProvider; // Components diff --git a/dist/vue-apollo.min.js b/dist/vue-apollo.min.js index 836cfd36..e99129ec 100644 --- a/dist/vue-apollo.min.js +++ b/dist/vue-apollo.min.js @@ -1 +1 @@ -var VueApollo=function(t){"use strict";function o(t){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function s(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function r(t,e){for(var i=0;i.provide() is deprecated. Use the 'apolloProvider' option instead with the provider object directly."),a({},t,this)}}]),e}();var P={name:"ApolloQuery",provide:function(){return{getDollarApollo:this.getDollarApollo,getApolloQuery:this.getApolloQuery}},props:{query:{type:Object,required:!0},variables:{type:Object,default:void 0},fetchPolicy:{type:String,default:void 0},pollInterval:{type:Number,default:void 0},notifyOnNetworkStatusChange:{type:Boolean,default:void 0},context:{type:Object,default:void 0},skip:{type:Boolean,default:!1},debounce:{type:Number,default:0},throttle:{type:Number,default:0},clientId:{type:String,default:void 0},deep:{type:Boolean,default:void 0},tag:{type:String,default:"div"}},data:function(){return{result:{data:null,loading:!1,networkStatus:7,error:null},times:0}},watch:{fetchPolicy:function(t){this.$apollo.queries.query.setOptions({fetchPolicy:t})},pollInterval:function(t){this.$apollo.queries.query.setOptions({pollInterval:t})},notifyOnNetworkStatusChange:function(t){this.$apollo.queries.query.setOptions({notifyOnNetworkStatusChange:t})}},apollo:{$client:function(){return this.clientId},query:function(){return{query:function(){return this.query},variables:function(){return this.variables},fetchPolicy:this.fetchPolicy,pollInterval:this.pollInterval,debounce:this.debounce,throttle:this.throttle,notifyOnNetworkStatusChange:this.notifyOnNetworkStatusChange,context:function(){return this.context},skip:function(){return this.skip},deep:this.deep,manual:!0,result:function(t){var e=t,i=e.errors,o=e.loading,r=e.networkStatus,n=t.error;t=Object.assign({},t),i&&i.length&&((n=new Error("Apollo errors occured (".concat(i.length,")"))).graphQLErrors=i);var s,a={};o?Object.assign(a,this.$_previousData,t.data):n?Object.assign(a,this.$apollo.queries.query.observer.getLastResult()||{},t.data):(a=t.data,this.$_previousData=t.data),this.result={data:(s=a,0 `subscribe` option is deprecated. Use the `$subscribe` option instead."),i.$subscribe)for(var n in i.$subscribe)this.$apollo.addSmartSubscription(n,i.$subscribe[n])}}}function T(t,e,i,o){void 0!==i&&("function"==typeof i?t.defineReactiveSetter(e,i,o):t[e]=i)}function I(t,e){t.mixin(h({},"1"===e?{init:M}:{},"2"===e?{data:function(){return{$apolloData:{queries:{},loading:0,data:this.$_apolloInitData}}},beforeCreate:function(){M.call(this),function(){var i=this;this.$_apolloInitData={};var t=this.$options.apollo;if(t){var e=function(e){"$"!==e.charAt(0)&&(t[e].manual||R(i.$options.props,e)||R(i.$options.computed,e)||R(i.$options.methods,e)||Object.defineProperty(i,e,{get:function(){return i.$data.$apolloData.data[e]},set:function(t){return i.$_apolloInitData[e]=t},enumerable:!0,configurable:!0}))};for(var o in t)e(o)}}.call(this)}}:{},{created:C,destroyed:function(){this.$_apollo&&(this.$_apollo.destroy(),this.$_apollo=null)}}))}var K=["$subscribe"];function N(t,e){if(!N.installed){N.installed=!0;var i=(m.Vue=t).version.substr(0,t.version.indexOf(".")),l=t.config.optionMergeStrategies.methods;t.config.optionMergeStrategies.apollo=function(t,e,i){if(!t)return e;if(!e)return t;for(var o=Object.assign({},O(t,K),t.data),r=Object.assign({},O(e,K),e.data),n={},s=0;s.provide() is deprecated. Use the 'apolloProvider' option instead with the provider object directly."),a({},t,this)}}]),e}();var Q={name:"ApolloQuery",provide:function(){return{getDollarApollo:this.getDollarApollo,getApolloQuery:this.getApolloQuery}},props:{query:{type:Object,required:!0},variables:{type:Object,default:void 0},fetchPolicy:{type:String,default:void 0},pollInterval:{type:Number,default:void 0},notifyOnNetworkStatusChange:{type:Boolean,default:void 0},context:{type:Object,default:void 0},skip:{type:Boolean,default:!1},debounce:{type:Number,default:0},throttle:{type:Number,default:0},clientId:{type:String,default:void 0},deep:{type:Boolean,default:void 0},tag:{type:String,default:"div"}},data:function(){return{result:{data:null,loading:!1,networkStatus:7,error:null},times:0}},watch:{fetchPolicy:function(t){this.$apollo.queries.query.setOptions({fetchPolicy:t})},pollInterval:function(t){this.$apollo.queries.query.setOptions({pollInterval:t})},notifyOnNetworkStatusChange:function(t){this.$apollo.queries.query.setOptions({notifyOnNetworkStatusChange:t})}},apollo:{$client:function(){return this.clientId},query:function(){return{query:function(){return this.query},variables:function(){return this.variables},fetchPolicy:this.fetchPolicy,pollInterval:this.pollInterval,debounce:this.debounce,throttle:this.throttle,notifyOnNetworkStatusChange:this.notifyOnNetworkStatusChange,context:function(){return this.context},skip:function(){return this.skip},deep:this.deep,manual:!0,result:function(t){var e=t,i=e.errors,o=e.loading,r=e.networkStatus,n=t.error;t=Object.assign({},t),i&&i.length&&((n=new Error("Apollo errors occured (".concat(i.length,")"))).graphQLErrors=i);var s,a={};o?Object.assign(a,this.$_previousData,t.data):n?Object.assign(a,this.$apollo.queries.query.observer.getLastResult()||{},t.data):(a=t.data,this.$_previousData=t.data),this.result={data:(s=a,0 `subscribe` option is deprecated. Use the `$subscribe` option instead."),i.$subscribe)for(var s in i.$subscribe)this.$apollo.addSmartSubscription(s,i.$subscribe[s])}}}function T(t,e,i,o){void 0!==i&&("function"==typeof i?t.defineReactiveSetter(e,i,o):t[e]=i)}function I(){this.$_apollo&&(this.$_apollo.destroy(),this.$_apollo=null)}function K(t,e){t.mixin(h({},"1"===e?{init:M}:{},"2"===e?{data:function(){return{$apolloData:{queries:{},loading:0,data:this.$_apolloInitData}}},beforeCreate:function(){M.call(this),function(){var i=this;this.$_apolloInitData={};var t=this.$options.apollo;if(t){var e=function(e){"$"!==e.charAt(0)&&(t[e].manual||L(i.$options.props,e)||L(i.$options.computed,e)||L(i.$options.methods,e)||Object.defineProperty(i,e,{get:function(){return i.$data.$apolloData.data[e]},set:function(t){return i.$_apolloInitData[e]=t},enumerable:!0,configurable:!0}))};for(var o in t)e(o)}}.call(this)},serverPrefetch:function(){if(this.$_apolloPromises)return Promise.all(this.$_apolloPromises)}}:{},{created:C,destroyed:I}))}var N=["$subscribe"];function V(t,e){if(!V.installed){V.installed=!0;var i=(m.Vue=t).version.substr(0,t.version.indexOf(".")),l=t.config.optionMergeStrategies.methods;t.config.optionMergeStrategies.apollo=function(t,e,i){if(!t)return e;if(!e)return t;for(var o=Object.assign({},O(t,N),t.data),r=Object.assign({},O(e,N),e.data),n={},s=0;s 0 && arguments[0] !== undefined ? arguments[0] : null; + if (this.loading) { this.applyLoadingModifier(-1); } this.loading = false; + + if (!error) { + this.firstRunResolve(); + } } }, { key: "fetchMore", @@ -1001,6 +1022,24 @@ return (_this$observer4 = this.observer).stopPolling.apply(_this$observer4, arguments); } } + }, { + key: "firstRunResolve", + value: function firstRunResolve() { + if (this._firstRunResolve) { + this._firstRunResolve(); + + this._firstRunResolve = null; + } + } + }, { + key: "firstRunReject", + value: function firstRunReject() { + if (this._firstRunReject) { + this._firstRunReject(); + + this._firstRunReject = null; + } + } }, { key: "destroy", value: function destroy() { @@ -1739,7 +1778,7 @@ return typeof holder !== 'undefined' && Object.prototype.hasOwnProperty.call(holder, key); } - function initDollarApollo() { + function initProvider() { var options = this.$options; // ApolloProvider injection var optionValue = options.apolloProvider; @@ -1803,6 +1842,8 @@ var apollo = this.$options.apollo; if (apollo) { + this.$_apolloPromises = []; + if (!apollo.$init) { apollo.$init = true; // Default options applied to `apollo` options @@ -1830,7 +1871,11 @@ for (var key in apollo) { if (key.charAt(0) !== '$') { var options = apollo[key]; - this.$apollo.addSmartQuery(key, options); + var smart = this.$apollo.addSmartQuery(key, options); + + if (options.prefetch !== false && apollo.$prefetch !== false) { + this.$_apolloPromises.push(smart.firstRun); + } } } @@ -1856,9 +1901,16 @@ } } + function destroy() { + if (this.$_apollo) { + this.$_apollo.destroy(); + this.$_apollo = null; + } + } + function installMixin(Vue, vueVersion) { Vue.mixin(_objectSpread({}, vueVersion === '1' ? { - init: initDollarApollo + init: initProvider } : {}, vueVersion === '2' ? { data: function data() { return { @@ -1870,17 +1922,17 @@ }; }, beforeCreate: function beforeCreate() { - initDollarApollo.call(this); + initProvider.call(this); proxyData.call(this); + }, + serverPrefetch: function serverPrefetch() { + if (this.$_apolloPromises) { + return Promise.all(this.$_apolloPromises); + } } } : {}, { created: launch, - destroyed: function destroyed() { - if (this.$_apollo) { - this.$_apollo.destroy(); - this.$_apollo = null; - } - } + destroyed: destroy })); } @@ -1931,7 +1983,7 @@ } ApolloProvider.install = install; // eslint-disable-next-line no-undef - ApolloProvider.version = "3.0.0-beta.27"; // Apollo provider + ApolloProvider.version = "3.0.0-beta.28"; // Apollo provider var ApolloProvider$1 = ApolloProvider; // Components diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 2ebfd1fe..3821e4db 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -50,7 +50,7 @@ module.exports = { link: 'https://www.patreon.com/akryum', }, ], - sidebarDepth: 3, + sidebarDepth: 2, sidebar: { '/guide/': [ '', diff --git a/docs/api/ssr.md b/docs/api/ssr.md index a0471dca..5518988c 100644 --- a/docs/api/ssr.md +++ b/docs/api/ssr.md @@ -6,45 +6,6 @@ See [SSR guide](../guide/ssr.md). ## Methods -### install - -Install the SSR plugin only on the server with: - -```js -Vue.use(ApolloSSR) -``` - -You can pass additional options like this: - -```js -Vue.use(ApolloSSR, { - fetchPolicy: 'network-only', - suppressRenderErrors: false, -}) -``` - -#### fetchPolicy - -When an Apollo query is prefetched, it's recommended to override `fetchPolicy` to force the queries to happen. - -Default value: `'network-only'`. - -#### suppressRenderErrors - -Silent the fake render errors. - -Default value: `false`. - -### prefetchAll - -Prefetches all queued component definitions and returns a promise resolved when all corresponding apollo data is ready. - -```js -await ApolloSSR.prefetchAll (apolloProvider, componentDefs, context) -``` - -`context` is passed as the argument to the `prefetch` options inside the smart queries. It may contain the route and the store. - ### getStates Returns the apollo stores states as JavaScript objects. @@ -82,40 +43,3 @@ const js = ApolloSSR.exportStates(apolloProvider, options) exportNamespace: '', } ``` - -### globalPrefetch - -Allow you to register a component to be prefetched explicitly. - -Simple example: - -```js -import MyComponent from '@/components/MyComponent.vue' - -ApolloSSR.globalPrefetch(() => MyComponent) -``` - -You can disable prefetching depending on context: - -```js -ApolloSSR.globalPrefetch(context => { - if (context.route.name === 'foo'){ - return MyComponent - } -}) -``` - -### mockInstance - -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. - -```js -const noop = () => {} - -ApolloSSR.mockInstance({ - apply: vm => { - // Mock $http - vm.$http = noop - }, -}) -``` diff --git a/docs/guide/ssr.md b/docs/guide/ssr.md index 80a1145f..05c236fc 100644 --- a/docs/guide/ssr.md +++ b/docs/guide/ssr.md @@ -1,6 +1,10 @@ # Server-Side Rendering -## Vue CLI Plugin +::: warning +**Requires Vue 2.6+ with `serverPrefetch` support** +::: + +## Vue CLI plugin 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! ✨🚀 @@ -12,17 +16,16 @@ vue add @akryum/ssr [More info](https://github.com/Akryum/vue-cli-plugin-ssr) -## Prefetch components +## Component prefetching -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, - - `false` to disable prefetching for this query. +::: tip +Follow the [offical SSR guide](https://ssr.vuejs.org) to learn more about Server-Side Rendering with Vue. +::: -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. +By default with `vue-server-renderer`, all the GraphQL queries in your server-side rendered components will be prefetched automatically. -::: danger -You don't have access to the component instance when doing prefetching on the server. +::: tip +You have access to `this` in options like `variables`, even on the server! ::: Example: @@ -31,7 +34,6 @@ Example: export default { apollo: { allPosts: { - // This will be prefetched query: gql`query AllPosts { allPosts { id @@ -57,11 +59,6 @@ export default { description } }`, - prefetch: ({ route }) => { - return { - id: route.params.id, - } - }, variables () { return { id: this.id, @@ -72,11 +69,13 @@ export default { } ``` -### Skip prefetching +## Skip prefetching + +You can skip server-side prefetching on a query with the `prefetch` option set to `false`. Example that doesn't prefetch the query: -```js +```js{12} export default { apollo: { allPosts: { @@ -96,7 +95,7 @@ export default { If you want to skip prefetching all the queries for a specific component, use the `$prefetch` option: -```js +```js{4} export default { apollo: { // Don't prefetch any query @@ -114,128 +113,16 @@ export default { } ``` -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: - -```vue - -``` - -## On the server - -In the server entry, you need to install `ApolloSSR` plugin into Vue: - -```js -import Vue from 'vue' -import ApolloSSR from 'vue-apollo/ssr' - -Vue.use(ApolloSSR) -``` - -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. - -Here is an example with vue-router and a Vuex store: - -```js -import Vue from 'vue' -import ApolloSSR from 'vue-apollo/ssr' -import App from './App.vue' - -Vue.use(ApolloSSR, { - // SSR config - fetchPolicy: 'network-only', - suppressRenderErrors: false, -}) - -export default () => 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. - - // Vuex Store prefetch - Promise.all(matchedComponents.map(component => { - return component.asyncData && component.asyncData({ - store, - route: router.currentRoute, - }) - })) - // Apollo prefetch - // This will prefetch all the Apollo queries in the whole app - .then(() => ApolloSSR.prefetchAll(apolloProvider, [App, ...matchedComponents], { - store, - route: router.currentRoute, - })) - .then(() => { - // Inject the Vuex state and the Apollo cache on the page. - // This will prevent unnecessary queries. - - // Vuex - js += `window.__INITIAL_STATE__=${JSON.stringify(store.state)};` - - // Apollo - js += ApolloSSR.exportStates(apolloProvider) - - resolve({ - app, - js, - }) - }).catch(reject) - }) -}) -``` - -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. - -It takes an `options` argument which defaults to: - -```js -{ - // 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: '', -} -``` - -You can also use the `ApolloSSR.getStates(apolloProvider, options)` method to get the JS object instead of the script string. - -It takes an `options` argument which defaults to: - -```js -{ - // Prefix for the keys of each apollo client state - exportNamespace: '', -} -``` - -### Creating the Apollo Clients +## Create Apollo client 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. +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. + Here is an example: -```js -// src/api/apollo.js +```js{21-31} +// apollo.js import Vue from 'vue' import { ApolloClient } from 'apollo-client' @@ -283,16 +170,24 @@ export function createApolloClient (ssr = false) { } ``` -Example for common `CreateApp` method: +## Create app + +Instead of creating our root Vue instance right away, we use a `createApp` function that accept a `context` parameter. + +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. + +Example for common `createApp` method: + +```js{9,37} +// app.js -```js 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 { createApolloClient } from './apollo' import App from './ui/App.vue' import routes from './routes' @@ -313,6 +208,12 @@ function createApp (context) { // this registers `store.state.route` sync(store, router) + // Vuex state restoration + if (!context.ssr && window.__INITIAL_STATE__) { + // We initialize the store state with the data injected from the server + store.replaceState(window.__INITIAL_STATE__) + } + // Apollo const apolloClient = createApolloClient(context.ssr) const apolloProvider = new VueApollo({ @@ -336,23 +237,34 @@ function createApp (context) { export default createApp ``` -On the client: +## Client entry + +The client entry is very simple -- we just call `createApp` with `ssr` being `false`: ```js -import CreateApp from './app' +// client-entry.js + +import createApp from './app' -CreateApp({ +createApp({ ssr: false, }) ``` -On the server: +## Server entry -```js -import CreateApp from './app' +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. + +Here is an example with vue-router and a Vuex store: + +```js{3,26} +// server-entry.js + +import ApolloSSR from 'vue-apollo/ssr' +import createApp from './app' export default () => new Promise((resolve, reject) => { - const { app, router, store, apolloProvider } = CreateApp({ + const { app, router, store, apolloProvider } = createApp({ ssr: true, }) @@ -361,9 +273,50 @@ export default () => new Promise((resolve, reject) => { // wait until router has resolved possible async hooks router.onReady(() => { - // Prefetch, render HTML (see above) + // This `rendered` hook is called when the app has finished rendering + context.rendered = () => { + // After the app is rendered, our store is now + // filled with the state from our components. + // When we attach the state to the context, and the `template` option + // is used for the renderer, the state will automatically be + // serialized and injected into the HTML as `window.__INITIAL_STATE__`. + context.state = store.state + + // ALso inject the apollo cache state + context.apolloState = ApolloSSR.getStates(apolloProvider) + } + resolve(app) }) }) ``` -See the [SSR API](../api/ssr.md) for more details and other features. +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. + +In the [page template](https://ssr.vuejs.org/guide/#using-a-page-template), use the `renderState` helper: + +```html +{{{ renderState({ contextKey: 'apolloState', windowKey: '__APOLLO_STATE__' }) }}} +``` + +Here is a full example: + +```html{15} + + + + + + + + {{ title }} + {{{ renderResourceHints() }}} + {{{ renderStyles() }}} + + + + {{{ renderState() }}} + {{{ renderState({ contextKey: 'apolloState', windowKey: '__APOLLO_STATE__' }) }}} + {{{ renderScripts() }}} + + +``` diff --git a/package.json b/package.json index 2ee099d9..d0de4e05 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vue-apollo", - "version": "3.0.0-beta.27", + "version": "3.0.0-beta.28", "description": "Use Apollo and GraphQL with Vue.js", "main": "dist/vue-apollo.umd.js", "module": "dist/vue-apollo.esm.js", diff --git a/src/mixin.js b/src/mixin.js index f7b83963..9856669d 100644 --- a/src/mixin.js +++ b/src/mixin.js @@ -4,7 +4,7 @@ function hasProperty (holder, key) { return typeof holder !== 'undefined' && Object.prototype.hasOwnProperty.call(holder, key) } -function initDollarApollo () { +function initProvider () { const options = this.$options // ApolloProvider injection const optionValue = options.apolloProvider @@ -60,6 +60,8 @@ function launch () { let apollo = this.$options.apollo if (apollo) { + this.$_apolloPromises = [] + if (!apollo.$init) { apollo.$init = true @@ -88,7 +90,10 @@ function launch () { for (let key in apollo) { if (key.charAt(0) !== '$') { let options = apollo[key] - this.$apollo.addSmartQuery(key, options) + const smart = this.$apollo.addSmartQuery(key, options) + if (options.prefetch !== false && apollo.$prefetch !== false) { + this.$_apolloPromises.push(smart.firstRun) + } } } @@ -114,10 +119,17 @@ function defineReactiveSetter ($apollo, key, value, deep) { } } +function destroy () { + if (this.$_apollo) { + this.$_apollo.destroy() + this.$_apollo = null + } +} + export function installMixin (Vue, vueVersion) { Vue.mixin({ ...vueVersion === '1' ? { - init: initDollarApollo, + init: initProvider, } : {}, ...vueVersion === '2' ? { @@ -132,18 +144,19 @@ export function installMixin (Vue, vueVersion) { }, beforeCreate () { - initDollarApollo.call(this) + initProvider.call(this) proxyData.call(this) }, + + serverPrefetch () { + if (this.$_apolloPromises) { + return Promise.all(this.$_apolloPromises) + } + }, } : {}, created: launch, - destroyed: function () { - if (this.$_apollo) { - this.$_apollo.destroy() - this.$_apollo = null - } - }, + destroyed: destroy, }) } diff --git a/src/smart-apollo.js b/src/smart-apollo.js index fe58ff9f..23cffc01 100644 --- a/src/smart-apollo.js +++ b/src/smart-apollo.js @@ -13,10 +13,6 @@ export default class SmartApollo { this._watchers = [] this._destroyed = false - if (this.vm.$isServer) { - this.options.fetchPolicy = 'cache-first' - } - if (autostart) { this.autostart() } diff --git a/src/smart-query.js b/src/smart-query.js index dda3b398..22fa9557 100644 --- a/src/smart-query.js +++ b/src/smart-query.js @@ -22,7 +22,16 @@ export default class SmartQuery extends SmartApollo { }) } - super(vm, key, options, autostart) + super(vm, key, options, false) + + this.firstRun = new Promise((resolve, reject) => { + this._firstRunResolve = resolve + this._firstRunReject = reject + }) + + if (this.vm.$isServer) { + this.options.fetchPolicy = 'network-only' + } if (!options.manual) { this.hasDataField = this.vm.$data.hasOwnProperty(key) @@ -40,6 +49,10 @@ export default class SmartQuery extends SmartApollo { }) } } + + if (autostart) { + this.autostart() + } } get client () { @@ -121,7 +134,11 @@ export default class SmartQuery extends SmartApollo { nextResult (result) { super.nextResult(result) - const { data, loading } = result + const { data, loading, error } = result + + if (error) { + this.firstRunReject() + } if (!loading) { this.loadingDone() @@ -154,7 +171,8 @@ export default class SmartQuery extends SmartApollo { catchError (error) { super.catchError(error) - this.loadingDone() + this.firstRunReject() + this.loadingDone(error) this.nextResult(this.observer.currentResult()) // The observable closes the sub if an error occurs this.resubscribeToQuery() @@ -189,11 +207,15 @@ export default class SmartQuery extends SmartApollo { this.watchLoading(value === 1, value) } - loadingDone () { + loadingDone (error = null) { if (this.loading) { this.applyLoadingModifier(-1) } this.loading = false + + if (!error) { + this.firstRunResolve() + } } fetchMore (...args) { @@ -260,6 +282,20 @@ export default class SmartQuery extends SmartApollo { } } + firstRunResolve () { + if (this._firstRunResolve) { + this._firstRunResolve() + this._firstRunResolve = null + } + } + + firstRunReject () { + if (this._firstRunReject) { + this._firstRunReject() + this._firstRunReject = null + } + } + destroy () { super.destroy() diff --git a/ssr/consts.js b/ssr/consts.js deleted file mode 100644 index 7bf54fe2..00000000 --- a/ssr/consts.js +++ /dev/null @@ -1,36 +0,0 @@ -exports.VM_HELPERS = [ - '_o', - '_n', - '_s', - '_l', - '_t', - '_q', - '_i', - '_m', - '_f', - '_k', - '_b', - '_v', - '_e', - '_g', -] - -exports.SSR_HELPERS = [ - '_ssrNode', - '_ssrEscape', - '_ssrList', - '_ssrStyle', - '_ssrClass', - '_ssrAttr', - '_ssrAttrs', - '_ssrDOMProps', -] - -exports.COMPONENT_BLACKLIST = [ - 'router-link', - 'router-view', - 'router-multi-view', - 'apollo-mutation', - 'apollo-subscribe', - 'apollo-subscribe-to-more', -] diff --git a/ssr/index.js b/ssr/index.js index 5ced0df1..2f95169c 100644 --- a/ssr/index.js +++ b/ssr/index.js @@ -1,214 +1,3 @@ -const chalk = require('chalk') -const { VUE_APOLLO_QUERY_KEYWORDS } = require('../lib/consts') -const { createFakeInstance, resolveComponent } = require('./utils') -const { Globals, getMergedDefinition, omit } = require('../lib/utils') - -const config = exports.config = { - globalPrefetchs: [], - fakeInstanceMocks: [], - fetchPolicy: 'network-only', - suppressRenderErrors: false, -} - -exports.install = function (Vue, options = {}) { - Globals.Vue = Vue - Object.assign(config, options) -} - -exports.globalPrefetch = function (handler) { - config.globalPrefetchs.push(handler) -} - -exports.mockInstance = function (plugin) { - config.fakeInstanceMocks.push(plugin) -} - -exports.prefetchAll = function (apolloProvider, components = [], context = {}) { - const globalPrefetchs = config.globalPrefetchs.map(handler => handler(context)).filter(Boolean) - return exports.getQueriesFromTree(components.concat(globalPrefetchs), context) - .then(queries => Promise.all(queries.map( - query => prefetchQuery(apolloProvider, query, context) - ))) -} - -exports.getQueriesFromTree = function (components, context) { - const queries = [] - return Promise.all( - components.map(component => walkTree(component, {}, undefined, [], context, queries, components)) - ).then(() => queries) -} - -function walkTree (component, data, parent, children, context, queries, components) { - component = getMergedDefinition(component) - return new Promise((resolve, reject) => { - const queue = [] - data = data || {} - const vm = createFakeInstance(component, data, parent, children, context) - - // Mocks - for (const mock of config.fakeInstanceMocks) { - mock.apply(mock) - } - - // Render h function - vm.$createElement = (el, data, children) => { - if (typeof data === 'string' || Array.isArray(data)) { - children = data - data = {} - } - - // No Prefetch flag - if (data && data.attrs && - data.attrs['no-prefetch'] !== undefined && - data.attrs['no-prefetch'] !== false) { - return - } - - queue.push(resolveComponent(el, component).then(resolvedComponent => { - let child - if (resolvedComponent && !components.includes(resolvedComponent)) { - child = { - component: resolvedComponent, - data, - children, - } - } - return child - })) - } - - prefetchComponent(component, vm, queries) - - try { - component.render.call(vm, vm.$createElement) - } catch (e) { - if (!config.suppressRenderErrors) { - console.log(chalk.red(`Error while rendering ${component.name || component.__file}`)) - console.log(e.stack) - } - } - - Promise.all(queue).then(queue => queue.filter(child => !!child).map( - child => walkTree(child.component, child.data, vm, child.children, context, queries, components) - )).then(() => resolve()) - }) -} - -function prefetchComponent (component, vm, queries) { - const apolloOptions = component.apollo - - if (!apolloOptions) return - if (apolloOptions.$prefetch === false) return - - const componentClient = apolloOptions.$client - for (let key in apolloOptions) { - const options = apolloOptions[key] - if ( - key.charAt(0) !== '$' && ( - !options.query || ( - (typeof options.ssr === 'undefined' || options.ssr) && - options.prefetch !== false - ) - ) - ) { - queries.push({ - queryOptions: options, - client: options.client || componentClient, - vm, - }) - } - } -} - -function prefetchQuery (apolloProvider, query, context) { - try { - let variables - - let { queryOptions, client, vm } = query - - // Client - if (typeof client === 'function') { - client = client.call(vm) - } - if (!client) { - client = apolloProvider.defaultClient - } else if (typeof client === 'string') { - client = apolloProvider.clients[client] - if (!client) { - throw new Error(`[vue-apollo] Missing client '${client}' in 'apolloProvider'`) - } - } - - // Function query - if (typeof queryOptions === 'function') { - queryOptions = queryOptions.call(vm) - } - - // Simple query - if (!queryOptions.query) { - queryOptions = { - query: queryOptions, - } - } else { - const prefetch = queryOptions.prefetch - const prefetchType = typeof prefetch - - // Resolve variables - let prefetchResult - if (prefetchType !== 'undefined') { - if (prefetchType === 'function') { - prefetchResult = prefetch.call(vm, context) - } else if (prefetchType === 'boolean') { - if (prefetchResult === false) { - return Promise.resolve() - } - } else { - prefetchResult = prefetch - } - } - - if (prefetchResult) { - variables = prefetchResult - } else { - const optVariables = queryOptions.variables - if (typeof optVariables !== 'undefined') { - // Reuse `variables` option with `prefetch: true` - if (typeof optVariables === 'function') { - variables = optVariables.call(vm) - } else { - variables = optVariables - } - } else { - variables = undefined - } - } - } - - // Query - if (typeof queryOptions.query === 'function') { - queryOptions.query = queryOptions.query.call(vm) - } - - // Default query options from apollo provider - if (apolloProvider.defaultOptions && apolloProvider.defaultOptions.$query) { - queryOptions = Object.assign({}, apolloProvider.defaultOptions.$query, queryOptions) - } - - // Remove vue-apollo specific options - const options = omit(queryOptions, VUE_APOLLO_QUERY_KEYWORDS) - options.variables = variables - // Override fetchPolicy - if (config.fetchPolicy != null) { - options.fetchPolicy = config.fetchPolicy - } - - return client.query(options) - } catch (e) { - console.log(chalk.red(`[ERROR] While prefetching query`), query, chalk.grey(`Error stack trace:`)) - console.log(e.stack) - } -} - exports.getStates = function (apolloProvider, options) { const finalOptions = Object.assign({}, { exportNamespace: '', diff --git a/ssr/utils.js b/ssr/utils.js deleted file mode 100644 index 8a56dcf0..00000000 --- a/ssr/utils.js +++ /dev/null @@ -1,197 +0,0 @@ -const { VM_HELPERS, SSR_HELPERS, COMPONENT_BLACKLIST } = require('./consts') -const { Globals, getMergedDefinition, noop } = require('../lib/utils') - -/* Fake instance creation */ - -function emptyString () { - return '' -} - -const computedPropDef = { - enumerable: true, - configurable: true, - get: noop, - set: noop, -} - -function defineComputed (target, key, userDef) { - if (typeof userDef === 'function') { - computedPropDef.get = userDef - computedPropDef.set = noop - } else { - computedPropDef.get = userDef.get || noop - computedPropDef.set = userDef.set || noop - } - Object.defineProperty(target, key, computedPropDef) -} - -function resolveScopedSlots (fns, res) { - res = res || {} - for (var i = 0; i < fns.length; i++) { - if (Array.isArray(fns[i])) { - resolveScopedSlots(fns[i], res) - } else { - res[fns[i].key] = fns[i].fn - } - } - return res -} - -function findRouteMatch (component, route) { - for (const r of route.matched) { - for (const key in r.components) { - if (r.components[key] === component) { - return r - } - } - } -} - -exports.createFakeInstance = function (options, data, parent, children, context) { - const vm = Object.assign( - {}, - data.attrs, - data.props, - { - $prefetch: true, - $options: options, - $parent: parent, - $children: children, - $attrs: data.attrs, - $props: data.props, - $slots: {}, - $scopedSlots: {}, - $set: Globals.Vue.set, - $delete: Globals.Vue.delete, - $route: context.route, - $store: context.store, - $apollo: { - queries: {}, - loading: false, - }, - $apolloData: { - loading: false, - }, - _self: {}, - _staticTrees: [], - _u: resolveScopedSlots, - } - ) - - // Render and other helpers - VM_HELPERS.forEach(helper => vm[helper] = noop) - SSR_HELPERS.forEach(helper => vm[helper] = emptyString) - - // Scoped slots - if (data.scopedSlots) { - vm.$scopedSlots = data.scopedSlots - } - - // Route props - if (context && context.route) { - const { route } = context - const matchedRoute = findRouteMatch(options, route) - if (matchedRoute && matchedRoute.props) { - const { props } = matchedRoute - if (props === true) { - Object.assign(vm, matchedRoute.params) - } else if (typeof props === 'function') { - Object.assign(vm, props(matchedRoute)) - } else if (typeof props === 'object') { - Object.assign(vm, props) - } - } - } - - // Methods - const methods = options.methods - for (const key in methods) { - vm[key] = methods[key].bind(vm) - } - - // Computed - const computed = options.computed - for (const key in computed) { - defineComputed(vm, key, computed[key]) - } - - // Data - const localData = options.data - if (typeof localData === 'function') { - vm._data = localData.call(vm, vm) - } else if (typeof localData === 'object') { - vm._data = localData - } else { - vm._data = {} - } - vm.$data = vm._data - Object.assign(vm, vm._data) - - // Prefetch state - const prefetch = options.prefetch - if (typeof prefetch === 'function') { - Object.assign(vm, prefetch(context)) - } else if (typeof prefetch === 'object') { - Object.assign(vm, prefetch) - } - - return vm -} - -/* Component resolution */ - -function cached (fn) { - const cache = Object.create(null) - return function cachedFn (str) { - const hit = cache[str] - return hit || (cache[str] = fn(str)) - } -} - -const camelizeRE = /-(\w)/g -const camelize = cached(function (str) { - return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : '' }) -}) - -const capitalize = cached(function (str) { - return str.charAt(0).toUpperCase() + str.slice(1) -}) - -const hasOwnProperty = Object.prototype.hasOwnProperty -function hasOwn (obj, key) { - return hasOwnProperty.call(obj, key) -} - -function resolveAsset (assets, id) { - if (typeof id !== 'string') return - - if (hasOwn(assets, id)) return assets[id] - - const camelCaseId = camelize(id) - if (hasOwn(assets, camelCaseId)) return assets[camelCaseId] - - const pascalCaseId = capitalize(camelCaseId) - if (hasOwn(assets, pascalCaseId)) return assets[pascalCaseId] -} - -exports.resolveComponent = function (name, options) { - return new Promise((resolve) => { - if (options.components) { - const component = resolveAsset(options.components, name) - if (component !== undefined) { - resolve(component) - } - } - return resolve(Globals.Vue.options.components[name]) - }).then(component => { - if (component) { - component = getMergedDefinition(component) - if (!component.functional && ( - !component.name || - !COMPONENT_BLACKLIST.includes(component.name) - )) { - return component - } - } - }) -}