UNPKG

vue-instantsearch-ssr

Version:

👀 Lightning-fast Algolia search for Vue apps

262 lines (225 loc) • 7.58 kB
import instantsearch from 'instantsearch.js/es'; import { isVue3, isVue2, Vue2, createSSRApp } from './vue-compat'; import { warn } from './warn'; function walkIndex(indexWidget, visit) { visit(indexWidget); return indexWidget.getWidgets().forEach(widget => { if (widget.$$type !== 'ais.index') return; visit(widget); walkIndex(widget, visit); }); } function searchOnlyWithDerivedHelpers(helper) { return new Promise((resolve, reject) => { helper.searchOnlyWithDerivedHelpers(); // we assume all derived helpers resolve at least in the same tick helper.derivedHelpers[0].on('result', () => { resolve(); }); helper.derivedHelpers.forEach(derivedHelper => derivedHelper.on('error', e => { reject(e); }) ); }); } function defaultCloneComponent(componentInstance, { mixins = [] } = {}) { const options = { serverPrefetch: undefined, fetch: undefined, _base: undefined, name: 'ais-ssr-root-component', }; let app; if (isVue3) { const appOptions = Object.assign({}, componentInstance.$options, options); appOptions.mixins = [...mixins, ...appOptions.mixins]; app = createSSRApp(appOptions); if (componentInstance.$router) { app.use(componentInstance.$router); } if (componentInstance.$store) { app.use(componentInstance.$store); } } else { // copy over global Vue APIs options.router = componentInstance.$router; options.store = componentInstance.$store; const Extended = componentInstance.$vnode ? componentInstance.$vnode.componentOptions.Ctor.extend(options) : Vue2.component( options.name, Object.assign({}, componentInstance.$options, options) ); app = new Extended({ propsData: componentInstance.$options.propsData, mixins: [...mixins], }); } // https://stackoverflow.com/a/48195006/3185307 app.$slots = componentInstance.$slots; app.$root = componentInstance.$root; if (isVue2) { app.$options.serverPrefetch = []; } return app; } function augmentInstantSearch(instantSearchOptions) { const { $cloneComponent: cloneComponent = defaultCloneComponent, } = instantSearchOptions; const search = instantsearch(instantSearchOptions); let initialResults; /** * main API for SSR, called in serverPrefetch of a root component which contains instantsearch * @param {Object} props the object including `component` and `renderToString` * @param {Object} props.component the calling component's `this` * @param {Function} props.renderToString the function to render componentInstance to string * @returns {Promise} result of the search, to save for .hydrate */ search.findResultsState = function({ component, renderToString }) { if (!renderToString) { throw new Error( 'findResultsState requires `renderToString: (component) => Promise<string>` in the first argument.' ); } let app; let instance; return Promise.resolve() .then(() => { app = cloneComponent(component, { mixins: [ { beforeCreate() { const descriptor = Object.getOwnPropertyDescriptor( this, '$nuxt' ); const isWritable = descriptor ? descriptor.writable || descriptor.set : false; if (component.$nuxt && isWritable) { // In case of Nuxt (3), we ensure the context is shared between // the real and cloned component this.$nuxt = component.$nuxt; } }, created() { instance = this.instantsearch; instance.start(); // although we use start for initializing the main index, // we don't want to send search requests yet instance.started = false; }, }, ], }); }) .then(() => renderToString(app)) .then(() => searchOnlyWithDerivedHelpers(instance.mainHelper)) .then(() => { initialResults = {}; walkIndex(instance.mainIndex, widget => { const { _state, _rawResults } = widget.getResults(); initialResults[widget.getIndexId()] = { // copy just the values of SearchParameters, not the functions state: Object.keys(_state).reduce((acc, key) => { // eslint-disable-next-line no-param-reassign acc[key] = _state[key]; return acc; }, {}), results: _rawResults, }; }); search.hydrate(initialResults); return search.getState(); }); }; /** * @returns {Promise} result state to serialize and enter into .hydrate */ search.getState = function() { if (!initialResults) { throw new Error('You need to wait for findResultsState to finish'); } return initialResults; }; /** * make sure correct data is available in each widget's state. * called in widget mixin with (this.widget, this) * * @param {object} widget The widget instance * @param {object} parent The local parent index * @returns {void} */ search.__forceRender = function(widget, parent) { const results = parent.getResults(); // this happens when a different InstantSearch gets rendered initially, // after the hydrate finished. There's thus no initial results available. if (results === null) { return; } const state = results._state; const localHelper = parent.getHelper(); // helper gets created in init, but that means it doesn't get the injected // parameters, because those are from the lastResults localHelper.state = state; widget.render({ helper: localHelper, results, scopedResults: parent.getScopedResults(), parent, state, templatesConfig: {}, createURL: parent.createURL, instantSearchInstance: search, searchMetadata: { isSearchStalled: false, }, }); }; /** * Called both in server * @param {object} results a map of indexId: SearchResults * @returns {void} */ search.hydrate = function(results) { if (!results) { warn( 'The result of `findResultsState()` needs to be passed to `hydrate()`.' ); return; } search._initialResults = results; search.start(); search.started = false; }; return search; } export function createServerRootMixin(instantSearchOptions = {}) { if (!instantSearchOptions.searchClient) { throw new Error(`The \`searchClient\` option is required. See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/`); } if (!instantSearchOptions.indexName) { throw new Error(`The \`indexName\` option is required. See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/`); } // put this in the user's root Vue instance // we can then reuse that InstantSearch instance seamlessly from `ais-instant-search-ssr` const rootMixin = { provide() { return { $_ais_ssrInstantSearchInstance: this.instantsearch, }; }, data() { return { // this is in data, so that the real & cloned render do not share // the same instantsearch instance. instantsearch: augmentInstantSearch(instantSearchOptions), }; }, }; return rootMixin; }