Skip to content

Commit

Permalink
feat: support raw Vue SSR (#90)
Browse files Browse the repository at this point in the history
* feat: support raw Vue SSR

* test: warn clients on ssrPlugin install

* refactor: remove unnecessary branch

* test: ignore coverage on path not reachable without modifying serverPrefetch option merge

* refactor: simulate node-environment with jest

* refactor: adjust warn message for wrong environment
  • Loading branch information
JohannesLamberts committed Apr 6, 2020
1 parent 6f207ce commit 91d7b38
Show file tree
Hide file tree
Showing 9 changed files with 129 additions and 60 deletions.
35 changes: 34 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,40 @@ It may look like things are working even if you don't pass `req` to `useStore` *

#### Raw Vue SSR

TODO: this part isn't built yet. You need to call `setActiveReq` with the _Request_ object before `useStore` is called
In a Raw Vue SSR application you have to modify a few files to enable hydration and to tell requests apart.

```js
// entry-server.js
import { getRootState, PiniaSsr } from "pinia";

// install plugin to automatically use correct context in setup and onServerPrefetch
Vue.use(PiniaSsr);

export default context => {
/* ... */
context.rendered = () => {
// pass state to context
context.piniaState = getRootState(context.req);
};
/* ... */
};
```

```html
<!-- index.html -->
<body>
<!-- pass state from context to client -->
{{{ renderState({ contextKey: 'piniaState', windowKey: '__PINIA_STATE__' }) }}}
</body>
```

```js
// entry-client.js
import { setStateProvider } from "pinia";

// inject ssr-state
setStateProvider(() => window.__PINIA_STATE__);
```

### Accessing other Stores

Expand Down
28 changes: 18 additions & 10 deletions __tests__/ssr/app.spec.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,47 @@
/**
* @jest-environment node
*/

import renderApp from './app/entry-server'
import { createRenderer } from 'vue-server-renderer'

const renderer = createRenderer()

function createContext() {
return {
rendered: () => {},
req: {},
}
}

describe('classic vue app', () => {
it('renders using the store', async () => {
const context = {
rendered: () => {},
}
const context = createContext()
const app = await renderApp(context)

// @ts-ignore
const html = await renderer.renderToString(app)
const html = await renderer.renderToString(app, context)
expect(html).toMatchInlineSnapshot(
`"<div data-server-rendered=\\"true\\"><h2>Hi anon</h2> <p>Count: 1 x 2 = 2</p> <button>Increment</button></div>"`
)
})

it('resets the store', async () => {
const context = {
rendered: () => {},
}
let context = createContext()
let app = await renderApp(context)

// @ts-ignore
let html = await renderer.renderToString(app)
let html = await renderer.renderToString(app, context)
expect(html).toMatchInlineSnapshot(
`"<div data-server-rendered=\\"true\\"><h2>Hi anon</h2> <p>Count: 1 x 2 = 2</p> <button>Increment</button></div>"`
)

// render again
// render again with new request context
context = createContext()
app = await renderApp(context)

// @ts-ignore
html = await renderer.renderToString(app)
html = await renderer.renderToString(app, context)
expect(html).toMatchInlineSnapshot(
`"<div data-server-rendered=\\"true\\"><h2>Hi anon</h2> <p>Count: 1 x 2 = 2</p> <button>Increment</button></div>"`
)
Expand Down
5 changes: 5 additions & 0 deletions __tests__/ssr/app/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { defineComponent, computed } from '@vue/composition-api'
import { useStore } from './store'

export default defineComponent({
async serverPrefetch() {
const store = useStore()
store.state.counter++
},

setup() {
const store = useStore()

Expand Down
8 changes: 6 additions & 2 deletions __tests__/ssr/app/entry-server.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import Vue from 'vue'
import { createApp } from './main'
import { PiniaSsr, getRootState } from '../../../src'

Vue.use(PiniaSsr)

export default function(context: any) {
return new Promise(resolve => {
const { app, store } = createApp()
const { app } = createApp()

// This `rendered` hook is called when the app has finished rendering
context.rendered = () => {
Expand All @@ -11,7 +15,7 @@ export default function(context: any) {
// 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
context.state = getRootState(context.req)
}

resolve(app)
Expand Down
10 changes: 1 addition & 9 deletions __tests__/ssr/app/main.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,16 @@
import Vue from 'vue'
// import VueCompositionApi from '@vue/composition-api'
import App from './App'
import { useStore } from './store'
import { setActiveReq } from '../../../src'

// Done in setup.ts
// Vue.use(VueCompositionApi)

export function createApp() {
// create router and store instances
setActiveReq({})
const store = useStore()

store.state.counter++

// create the app instance, injecting both the router and the store
const app = new Vue({
render: h => h(App),
})

// expose the app, the router and the store.
return { app, store }
return { app }
}
12 changes: 12 additions & 0 deletions __tests__/ssr/ssrPlugin.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Vue from 'vue'
import { PiniaSsr } from '../../src'

it('should warn when installed in the browser', () => {
const mixinSpy = jest.spyOn(Vue, 'mixin')
const warnSpy = jest.spyOn(console, 'warn')
Vue.use(PiniaSsr)
expect(warnSpy).toHaveBeenCalledWith(
expect.stringMatching(/seems to be used in the browser bundle/i)
)
expect(mixinSpy).not.toHaveBeenCalled()
})
42 changes: 4 additions & 38 deletions nuxt/plugin.js
Original file line number Diff line number Diff line change
@@ -1,45 +1,11 @@
// @ts-check
import Vue from 'vue'
// @ts-ignore: this must be pinia to load the local module
import { setActiveReq, setStateProvider, getRootState } from 'pinia'
import { setActiveReq, PiniaSsr, setStateProvider, getRootState } from 'pinia'

Vue.mixin({
beforeCreate() {
// @ts-ignore
const { setup, serverPrefetch } = this.$options
if (setup) {
// @ts-ignore
this.$options.setup = (props, context) => {
if (context.ssrContext && context.ssrContext.req) {
setActiveReq(context.ssrContext.req)
}

return setup(props, context)
}
}

if (process.server && serverPrefetch) {
const patchedServerPrefetch = Array.isArray(serverPrefetch)
? serverPrefetch.slice()
: [serverPrefetch]

for (let i = 0; i < patchedServerPrefetch.length; i++) {
const original = patchedServerPrefetch[i]
/**
* @type {(this: import('vue').default) => any}
*/
patchedServerPrefetch[i] = function() {
setActiveReq(this.$ssrContext.req)

return original.call(this)
}
}

// @ts-ignore
this.$options.serverPrefetch = patchedServerPrefetch
}
},
})
if (process.server) {
Vue.use(PiniaSsr)
}

/** @type {import('@nuxt/types').Plugin} */
const myPlugin = context => {
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { createStore } from './store'
export { setActiveReq, setStateProvider, getRootState } from './rootStore'
export { StateTree, StoreGetter, Store } from './types'
export { PiniaSsr } from './ssrPlugin'
48 changes: 48 additions & 0 deletions src/ssrPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { VueConstructor } from 'vue/types'
import { setActiveReq } from './rootStore'

export const PiniaSsr = (vue: VueConstructor) => {
const isServer = typeof window === 'undefined'

if (!isServer) {
console.warn(
'`PiniaSsrPlugin` seems to be used in the browser bundle. You should only call it on the server entry: https://github.com/posva/pinia#raw-vue-ssr'
)
return
}

vue.mixin({
beforeCreate() {
const { setup, serverPrefetch } = this.$options
if (setup) {
this.$options.setup = (props, context) => {
// @ts-ignore
setActiveReq(context.ssrContext.req)
return setup(props, context)
}
}

if (serverPrefetch) {
const patchedServerPrefetch = Array.isArray(serverPrefetch)
? serverPrefetch.slice()
: // serverPrefetch not being an array cannot be triggered due tue options merge
// https://github.com/vuejs/vue/blob/7912f75c5eb09e0aef3e4bfd8a3bb78cad7540d7/src/core/util/options.js#L149
/* istanbul ignore next */
[serverPrefetch]

for (let i = 0; i < patchedServerPrefetch.length; i++) {
const original = patchedServerPrefetch[i]
patchedServerPrefetch[i] = function() {
// @ts-ignore
setActiveReq(this.$ssrContext.req)

return original.call(this)
}
}

// @ts-ignore
this.$options.serverPrefetch = patchedServerPrefetch
}
},
})
}

0 comments on commit 91d7b38

Please sign in to comment.