From f38249d220af98663b337917508e485a966b98bc Mon Sep 17 00:00:00 2001 From: NataliaTepluhina Date: Wed, 3 Mar 2021 17:18:18 +0100 Subject: [PATCH 01/64] feat: added universal code --- src/guide/ssr/universal.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/guide/ssr/universal.md diff --git a/src/guide/ssr/universal.md b/src/guide/ssr/universal.md new file mode 100644 index 0000000000..7cc390317a --- /dev/null +++ b/src/guide/ssr/universal.md @@ -0,0 +1,33 @@ +# Writing Universal Code + +Before going further, let's take a moment to discuss the constraints when writing "universal" code - that is, code that runs on both the server and the client. Due to use case and platform API differences, the behavior of our code will not be exactly the same when running in different environments. Here we will go over the key things you need to be aware of. + +## Data Reactivity on the Server + +In a client-only app, every user will be using a fresh instance of the app in their browser. For server-side rendering we want the same: each request should have a fresh, isolated app instance so that there is no cross-request state pollution. + +Because the actual rendering process needs to be deterministic, we will also be "pre-fetching" data on the server - this means our application state will be already resolved when we start rendering. This means data reactivity is unnecessary on the server, so it is disabled by default. Disabling data reactivity also avoids the performance cost of converting data into reactive objects. + +## Component Lifecycle Hooks + +Since there are no dynamic updates, of all the lifecycle hooks, only `beforeCreate` and `created` will be called during SSR. This means any code inside other lifecycle hooks such as `beforeMount` or `mounted` will only be executed on the client. + +Another thing to note is that you should avoid code that produces global side effects in `beforeCreate` and `created`, for example setting up timers with `setInterval`. In client-side only code we may setup a timer and then tear it down in `beforeUnmount` or `unmounted`. However, because the destroy hooks will not be called during SSR, the timers will stay around forever. To avoid this, move your side-effect code into `beforeMount` or `mounted` instead. + +## Access to Platform-Specific APIs + +Universal code cannot assume access to platform-specific APIs, so if your code directly uses browser-only globals like `window` or `document`, they will throw errors when executed in Node.js, and vice-versa. + +For tasks shared between server and client but using different platform APIs, it's recommended to wrap the platform-specific implementations inside a universal API, or use libraries that do this for you. For example, [axios](https://github.com/axios/axios) is an HTTP client that exposes the same API for both server and client. + +For browser-only APIs, the common approach is to lazily access them inside client-only lifecycle hooks. + +Note that if a 3rd party library is not written with universal usage in mind, it could be tricky to integrate it into an server-rendered app. You _might_ be able to get it working by mocking some of the globals, but it would be hacky and may interfere with the environment detection code of other libraries. + +## Custom Directives + +Most custom directives directly manipulate the DOM, and therefore will cause error during SSR. There are two ways to work around this: + +1. Prefer using components as the abstraction mechanism and work at the Virtual-DOM level (e.g. using render functions) instead. + +2. If you have a custom directive that cannot be easily replaced by components, you can provide a "server-side version" of it using the `directives`option when creating the server renderer. From 42f211ca1395e2d4aedff778bbf06d484fd3b631 Mon Sep 17 00:00:00 2001 From: NataliaTepluhina Date: Wed, 10 Mar 2021 13:39:34 +0100 Subject: [PATCH 02/64] Added a project structure entry --- src/guide/ssr/structure.md | 145 +++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 src/guide/ssr/structure.md diff --git a/src/guide/ssr/structure.md b/src/guide/ssr/structure.md new file mode 100644 index 0000000000..df87da73b2 --- /dev/null +++ b/src/guide/ssr/structure.md @@ -0,0 +1,145 @@ +# Source Code Structure + +## Avoid Stateful Singletons + +When writing client-only code, we are used to the fact that our code will be evaluated in a fresh context every time. However, a Node.js server is a long-running process. When our code is required into the process, it will be evaluated once and stays in memory. This means if you create a singleton object, it will be shared between every incoming request. + +As seen in the basic example, we are **creating a new root Vue instance for each request.** This is similar to how each user will be using a fresh instance of the app in their own browser. If we use a shared instance across multiple requests, it will easily lead to cross-request state pollution. + +So, instead of directly creating an app instance, we should expose a factory function that can be repeatedly executed to create fresh app instances for each request: + +```js +// app.js +const { createSSRApp } = require('vue') + +module.exports = function createApp() { + return createSSRApp({ + data() { + return { + user: 'John Doe' + } + }, + template: `
Current user is: {{ user }}
` + }) +} +``` + +And our server code now becomes: + +```js +// server.js +const createApp = require('./app') + +server.get('*', (req, res) => { + const app = createApp() + + const appContent = await renderToString(app) + const html = ` + + +

My First Heading

+ ${appContent} + + + ` + + res.end(html) +}) +``` + +The same rule applies to router and store instances as well. Instead of exporting it directly from a module and importing it across your app, you need to create a fresh instance in `createApp` and inject it from the root Vue instance. + +## Introducing a Build Step + +So far, we haven't discussed how to deliver the same Vue app to the client yet. To do that, we need to use webpack to bundle our Vue app. In fact, we probably want to use webpack to bundle the Vue app on the server as well, because: + +- Typical Vue apps are built with webpack and `vue-loader`, and many webpack-specific features such as importing files via `file-loader`, importing CSS via `css-loader` would not work directly in Node.js. + +- Although the latest version of Node.js fully supports ES2015 features, we still need to transpile client-side code to cater to older browsers. This again involves a build step. + +So the basic idea is we will be using webpack to bundle our app for both client and server - the server bundle will be required by the server and used for SSR, while the client bundle is sent to the browser to hydrate the static markup. + +![architecture](https://cloud.githubusercontent.com/assets/499550/17607895/786a415a-5fee-11e6-9c11-45a2cfdf085c.png) + +We will discuss the details of the setup in later sections - for now, let's just assume we've got the build setup figured out and we can write our Vue app code with webpack enabled. + +## Code Structure with webpack + +Now that we are using webpack to process the app for both server and client, the majority of our source code can be written in a universal fashion, with access to all the webpack-powered features. At the same time, there are [a number of things](./universal.md) you should keep in mind when writing universal code. + +A simple project would look like this: + +```bash +src +├── components +│ ├── MyUser.vue +│ └── MyTable.vue +├── App.vue +├── app.js # universal entry +├── entry-client.js # runs in browser only +└── entry-server.js # runs on server only +``` + +### `app.js` + +`app.js` is the universal entry to our app. In a client-only app, we would create the Vue application instance right in this file and mount directly to DOM. However, for SSR that responsibility is moved into the client-only entry file. `app.js` instead creates an application root component and exports it: + +```js +import { h } from 'vue' +import App from './App.vue' + +// export a factory function for creating a root component +export default function(args) { + return { + rootComponent: { + render: () => h(App), + components: { App }, + setup() { + // additional application logic here + } + } + } +} +``` + +### `entry-client.js`: + +The client entry creates the application using the root component factory and mounts it to the DOM: + +```js +import { createApp } from 'vue' +import createRootComponent from './app' + +// client-specific bootstrapping logic... + +const { rootComponent } = createRootComponent({ + // here we can pass arguments to root component + // for example, to be provided in the `setup()` +}) + +const app = createApp(rootComponent) + +// this assumes App.vue template root element has `id="app"` +app.mount('#app', true) +``` + +### `entry-server.js`: + +The server entry uses a default export which is a function that can be called repeatedly for each render. At this moment, it doesn't do much other than creating and returning the app instance with `createSSRApp` - but later we will perform server-side route matching and data pre-fetching logic here. + +```js +import { createSSRApp } from 'vue' +import createRootComponent from './app' + +export default function() { + const { rootComponent } = createRootComponent({ + /*...*/ + }) + + const app = createSSRApp(rootComponent) + + return { + app + } +} +``` From 3d6ec0d4745ee76eb160f0593e1151e090594979 Mon Sep 17 00:00:00 2001 From: NataliaTepluhina Date: Wed, 10 Mar 2021 14:07:07 +0100 Subject: [PATCH 03/64] feat: added a router guide --- src/guide/ssr/routing.md | 109 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 src/guide/ssr/routing.md diff --git a/src/guide/ssr/routing.md b/src/guide/ssr/routing.md new file mode 100644 index 0000000000..c52085d114 --- /dev/null +++ b/src/guide/ssr/routing.md @@ -0,0 +1,109 @@ +# Routing and Code-Splitting + +## Routing with `vue-router` + +You may have noticed that our server code uses a `*` handler which accepts arbitrary URLs. This allows us to pass the visited URL into our Vue app, and reuse the same routing config for both client and server! + +It is recommended to use the official [vue-router](https://github.com/vuejs/vue-router-next) library for this purpose. Let's first create a file where we create the router. Note that similar to `createRootComponent`, we also need a fresh router instance for each request, so the file exports a `createRouter` function: + +```js +// router.js +const { + createRouter, + createMemoryHistory, + createWebHistory +} = require('vue-router') + +const isServer = typeof window === 'undefined' + +let history = isServer ? createMemoryHistory() : createWebHistory() + +const routes = [ + /* ... */ +] + +export default function() { + return createRouter({ routes, history }) +} +``` + +And update our `app.js`, client and server entries: + +```js +// app.js +import { h } from 'vue' +import App from './App.vue' +import createRouter from './router' + +export default function(args) { + return { + rootComponent: { + render: () => h(App), + components: { App }, + setup() { + nativeStore.provideStore(args.nativeStore) + vuexStore.provideStore(args.vuexStore) + } + }, + // create a router instance + router: createRouter() + } +} +``` + +```js +// entry-client.js + +const { rootComponent, router } = createRootComponent({}) + +const app = createApp(rootComponent) +app.use(router) +``` + +```js +// entry-server.js +const { rootComponent, router } = createRootComponent({}) + +const app = createSSRApp(rootComponent) + +app.use(router) + +return { + app, + router +} +``` + +## Code-Splitting + +Code-splitting, or lazy-loading part of your app, helps reducing the amount of assets that need to be downloaded by the browser for the initial render, and can greatly improve TTI (time-to-interactive) for apps with large bundles. The key is "loading just what is needed" for the initial screen. + +Vue provides async components as a first-class concept, combining it with [webpack 2's support for using dynamic import as a code-split point](https://webpack.js.org/guides/code-splitting-async/), all you need to do is: + +```js +// changing this... +import User from './User.vue' + +// to this: +const User = () => import('./User.vue') +``` + +Note that it is still necessary to use `router.isReady` on both server and client before returning / mounting the app, because the router must resolve async route components ahead of time in order to properly invoke in-component hooks. We already did that in our server entry, and now we just need to update the client entry: + +```js +// entry-client.js + +import { createApp } from 'vue' +import createRootComponent from './app' + +const { rootComponent, router } = createRootComponent({}) + +const app = createApp(rootComponent) + +app.use(router) + +(async (r, a) => { + await r.isReady() + a.mount('#app', true) +})(router, app) +``` From 3d6097d0ce9413caa803928d670b0e1e1a50bdd7 Mon Sep 17 00:00:00 2001 From: NataliaTepluhina Date: Thu, 11 Mar 2021 08:43:37 +0100 Subject: [PATCH 04/64] fix: fixed entries and app --- src/guide/ssr/getting-started.md | 2 +- src/guide/ssr/structure.md | 54 ++++++++++++++++---------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/guide/ssr/getting-started.md b/src/guide/ssr/getting-started.md index 6aca2905f1..98a4b5cdec 100644 --- a/src/guide/ssr/getting-started.md +++ b/src/guide/ssr/getting-started.md @@ -83,7 +83,7 @@ server.get('*', async (req, res) => {

My First Heading

- ${appContent} +
${appContent}
` diff --git a/src/guide/ssr/structure.md b/src/guide/ssr/structure.md index df87da73b2..a0ea2a0e70 100644 --- a/src/guide/ssr/structure.md +++ b/src/guide/ssr/structure.md @@ -38,7 +38,7 @@ server.get('*', (req, res) => {

My First Heading

- ${appContent} +
${appContent}
` @@ -82,23 +82,27 @@ src ### `app.js` -`app.js` is the universal entry to our app. In a client-only app, we would create the Vue application instance right in this file and mount directly to DOM. However, for SSR that responsibility is moved into the client-only entry file. `app.js` instead creates an application root component and exports it: +`app.js` is the universal entry to our app. In a client-only app, we would create the Vue application instance right in this file and mount directly to DOM. However, for SSR that responsibility is moved into the client-only entry file. `app.js` instead creates an application instance and exports it: ```js -import { h } from 'vue' +import { createSSRApp, h } from 'vue' import App from './App.vue' // export a factory function for creating a root component -export default function(args) { - return { - rootComponent: { - render: () => h(App), - components: { App }, - setup() { - // additional application logic here - } +export default function (args) { + const rootComponent = { + render: () => h(App), + components: { App }, + setup() { + // additional application logic here } } + + const app = createSSRApp(rootComponent); + + return { + app, + }; } ``` @@ -107,8 +111,7 @@ export default function(args) { The client entry creates the application using the root component factory and mounts it to the DOM: ```js -import { createApp } from 'vue' -import createRootComponent from './app' +import createApp from './app' // client-specific bootstrapping logic... @@ -117,29 +120,26 @@ const { rootComponent } = createRootComponent({ // for example, to be provided in the `setup()` }) -const app = createApp(rootComponent) +const { app, router } = createApp({ + // here we can pass additional arguments to app factory +}); // this assumes App.vue template root element has `id="app"` -app.mount('#app', true) +app.mount('#app') ``` ### `entry-server.js`: -The server entry uses a default export which is a function that can be called repeatedly for each render. At this moment, it doesn't do much other than creating and returning the app instance with `createSSRApp` - but later we will perform server-side route matching and data pre-fetching logic here. +The server entry uses a default export which is a function that can be called repeatedly for each render. At this moment, it doesn't do much other than returning the app instance - but later we will perform server-side route matching and data pre-fetching logic here. ```js -import { createSSRApp } from 'vue' -import createRootComponent from './app' - -export default function() { - const { rootComponent } = createRootComponent({ - /*...*/ - }) - - const app = createSSRApp(rootComponent) +export default function () { + const { + app, + } = createApp({/*...*/}); return { - app - } + app, + }; } ``` From d543e7de87119b0db40fb25828ad5a3651689495 Mon Sep 17 00:00:00 2001 From: NataliaTepluhina Date: Thu, 11 Mar 2021 08:56:36 +0100 Subject: [PATCH 05/64] fix: fixed routing --- src/guide/ssr/routing.md | 85 +++++++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 36 deletions(-) diff --git a/src/guide/ssr/routing.md b/src/guide/ssr/routing.md index c52085d114..c62e170fa4 100644 --- a/src/guide/ssr/routing.md +++ b/src/guide/ssr/routing.md @@ -31,47 +31,38 @@ And update our `app.js`, client and server entries: ```js // app.js -import { h } from 'vue' +import { createSSRApp,, h } from 'vue' import App from './App.vue' import createRouter from './router' -export default function(args) { - return { - rootComponent: { - render: () => h(App), - components: { App }, - setup() { - nativeStore.provideStore(args.nativeStore) - vuexStore.provideStore(args.vuexStore) - } - }, - // create a router instance - router: createRouter() +export default function (args) { + const rootComponent = { + render: () => h(App), + components: { App }, + setup() {/*...*/} } + + const app = createSSRApp(rootComponent); + const router = createRouter(); + + app.use(router); + + return { + app, + router, + }; } + ``` ```js // entry-client.js - -const { rootComponent, router } = createRootComponent({}) - -const app = createApp(rootComponent) -app.use(router) +const { app, router } = createApp({/*...*/}); ``` ```js // entry-server.js -const { rootComponent, router } = createRootComponent({}) - -const app = createSSRApp(rootComponent) - -app.use(router) - -return { - app, - router -} +const { app, router } = createApp({/*...*/}); ``` ## Code-Splitting @@ -88,7 +79,7 @@ import User from './User.vue' const User = () => import('./User.vue') ``` -Note that it is still necessary to use `router.isReady` on both server and client before returning / mounting the app, because the router must resolve async route components ahead of time in order to properly invoke in-component hooks. We already did that in our server entry, and now we just need to update the client entry: +Note that it is still necessary to use `router.isReady` on both server and client before returning / mounting the app, because the router must resolve async route components ahead of time in order to properly invoke in-component hooks. Let's update our client entry: ```js // entry-client.js @@ -96,14 +87,36 @@ Note that it is still necessary to use `router.isReady` on both server and clien import { createApp } from 'vue' import createRootComponent from './app' -const { rootComponent, router } = createRootComponent({}) +const { app, router } = createApp({/* ... */}); -const app = createApp(rootComponent) +router.isReady().then(() => { + app.mount('#app') +}) +``` -app.use(router) +For server part, we need to update our `server.js` script: -(async (r, a) => { - await r.isReady() - a.mount('#app', true) -})(router, app) +```js +// server.js +const createApp = require('./app') + +server.get('*', async (req, res) => { + const { app, router } = await createApp() + + router.push(req.url); + await router.isReady(); + + const appContent = await renderToString(app) + + const html = ` + + +

My First Heading

+
${appContent}
+ + + ` + + res.end(html) +}) ``` From 6cfeac5c2133086a64aaccb7a2af07c93db6a745 Mon Sep 17 00:00:00 2001 From: NataliaTepluhina Date: Thu, 11 Mar 2021 08:57:14 +0100 Subject: [PATCH 06/64] chore: formatted the code --- src/guide/ssr/routing.md | 20 +++++++++++++------- src/guide/ssr/structure.md | 22 +++++++++++----------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/guide/ssr/routing.md b/src/guide/ssr/routing.md index c62e170fa4..e47e0ca891 100644 --- a/src/guide/ssr/routing.md +++ b/src/guide/ssr/routing.md @@ -57,12 +57,16 @@ export default function (args) { ```js // entry-client.js -const { app, router } = createApp({/*...*/}); +const { app, router } = createApp({ + /*...*/ +}) ``` ```js // entry-server.js -const { app, router } = createApp({/*...*/}); +const { app, router } = createApp({ + /*...*/ +}) ``` ## Code-Splitting @@ -87,7 +91,9 @@ Note that it is still necessary to use `router.isReady` on both server and clien import { createApp } from 'vue' import createRootComponent from './app' -const { app, router } = createApp({/* ... */}); +const { app, router } = createApp({ + /* ... */ +}) router.isReady().then(() => { app.mount('#app') @@ -102,12 +108,12 @@ const createApp = require('./app') server.get('*', async (req, res) => { const { app, router } = await createApp() - - router.push(req.url); - await router.isReady(); + + router.push(req.url) + await router.isReady() const appContent = await renderToString(app) - + const html = ` diff --git a/src/guide/ssr/structure.md b/src/guide/ssr/structure.md index a0ea2a0e70..9f2c2b8f26 100644 --- a/src/guide/ssr/structure.md +++ b/src/guide/ssr/structure.md @@ -89,7 +89,7 @@ import { createSSRApp, h } from 'vue' import App from './App.vue' // export a factory function for creating a root component -export default function (args) { +export default function(args) { const rootComponent = { render: () => h(App), components: { App }, @@ -98,11 +98,11 @@ export default function (args) { } } - const app = createSSRApp(rootComponent); + const app = createSSRApp(rootComponent) return { - app, - }; + app + } } ``` @@ -122,7 +122,7 @@ const { rootComponent } = createRootComponent({ const { app, router } = createApp({ // here we can pass additional arguments to app factory -}); +}) // this assumes App.vue template root element has `id="app"` app.mount('#app') @@ -133,13 +133,13 @@ app.mount('#app') The server entry uses a default export which is a function that can be called repeatedly for each render. At this moment, it doesn't do much other than returning the app instance - but later we will perform server-side route matching and data pre-fetching logic here. ```js -export default function () { - const { - app, - } = createApp({/*...*/}); +export default function() { + const { app } = createApp({ + /*...*/ + }) return { - app, - }; + app + } } ``` From 5646123afe5936fef5009940105d195dcd0deb7d Mon Sep 17 00:00:00 2001 From: NataliaTepluhina Date: Thu, 11 Mar 2021 09:03:39 +0100 Subject: [PATCH 07/64] fix: fixed structure and router --- src/guide/ssr/routing.md | 19 ++++++++++--------- src/guide/ssr/structure.md | 9 +++------ 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/guide/ssr/routing.md b/src/guide/ssr/routing.md index e47e0ca891..da9abf7782 100644 --- a/src/guide/ssr/routing.md +++ b/src/guide/ssr/routing.md @@ -31,28 +31,29 @@ And update our `app.js`, client and server entries: ```js // app.js -import { createSSRApp,, h } from 'vue' +import { createSSRApp, h } from 'vue' import App from './App.vue' import createRouter from './router' -export default function (args) { +export default function(args) { const rootComponent = { render: () => h(App), components: { App }, - setup() {/*...*/} + setup() { + /*...*/ + } } - const app = createSSRApp(rootComponent); - const router = createRouter(); + const app = createSSRApp(rootComponent) + const router = createRouter() - app.use(router); + app.use(router) return { app, - router, - }; + router + } } - ``` ```js diff --git a/src/guide/ssr/structure.md b/src/guide/ssr/structure.md index 9f2c2b8f26..78e4b14036 100644 --- a/src/guide/ssr/structure.md +++ b/src/guide/ssr/structure.md @@ -115,12 +115,7 @@ import createApp from './app' // client-specific bootstrapping logic... -const { rootComponent } = createRootComponent({ - // here we can pass arguments to root component - // for example, to be provided in the `setup()` -}) - -const { app, router } = createApp({ +const { app } = createApp({ // here we can pass additional arguments to app factory }) @@ -133,6 +128,8 @@ app.mount('#app') The server entry uses a default export which is a function that can be called repeatedly for each render. At this moment, it doesn't do much other than returning the app instance - but later we will perform server-side route matching and data pre-fetching logic here. ```js +import createApp from './app' + export default function() { const { app } = createApp({ /*...*/ From d12048e09548df6a253917568beb7205cb8c9cb7 Mon Sep 17 00:00:00 2001 From: NataliaTepluhina Date: Wed, 17 Mar 2021 19:23:55 +0100 Subject: [PATCH 08/64] feat: added hydration guide --- src/guide/ssr/getting-started.md | 2 +- src/guide/ssr/hydration.md | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 src/guide/ssr/hydration.md diff --git a/src/guide/ssr/getting-started.md b/src/guide/ssr/getting-started.md index 98a4b5cdec..17f740bc9d 100644 --- a/src/guide/ssr/getting-started.md +++ b/src/guide/ssr/getting-started.md @@ -94,4 +94,4 @@ server.get('*', async (req, res) => { server.listen(8080) ``` -Now, when running this Node.js script, we can see a static HTML page on `localhost:8080`. However, this code is not _hydrated_: Vue hasn't yet take over the static HTML sent by the server to turn it into dynamic DOM that can react to client-side data changes. This will be covered in the [Client Side Hydration](#) section. +Now, when running this Node.js script, we can see a static HTML page on `localhost:8080`. However, this code is not _hydrated_: Vue hasn't yet take over the static HTML sent by the server to turn it into dynamic DOM that can react to client-side data changes. This will be covered in the [Client Side Hydration](hydration.html) section. diff --git a/src/guide/ssr/hydration.md b/src/guide/ssr/hydration.md new file mode 100644 index 0000000000..465c69d666 --- /dev/null +++ b/src/guide/ssr/hydration.md @@ -0,0 +1,30 @@ +# Client Side Hydration + +Hydration refers to the client-side process during which Vue takes over the static HTML sent by the server and turns it into dynamic DOM that can react to client-side data changes. + +In `entry-client.js`, we are simply mounting the app with this line: + +```js +// this assumes App.vue template root element has `id="app"` +app.mount('#app') +``` + +Since the server has already rendered the markup, we obviously do not want to throw that away and re-create all the DOM elements. Instead, we want to "hydrate" the static markup and make it interactive. + +If application is created with `createSSRApp`, Vue will perform a hydration process. Otherwise, DOM elements will be re-created. That's why we used `createSSRApp` in the `entry-client.js` + +In development mode, Vue will assert the client-side generated virtual DOM tree matches the DOM structure rendered from the server. If there is a mismatch, it will bail hydration, discard existing DOM and render from scratch. **In production mode, this assertion is disabled for maximum performance.** + +### Hydration Caveats + +One thing to be aware of when using SSR + client hydration is some special HTML structures that may be altered by the browser. For example, when you write this in a Vue template: + +```html + + + + +
hi
+``` + +The browser will automatically inject `` inside ``, however, the virtual DOM generated by Vue does not contain ``, so it will cause a mismatch. To ensure correct matching, make sure to write valid HTML in your templates. From a50d49e56253b477eac35b356cbfa26d9565749f Mon Sep 17 00:00:00 2001 From: NataliaTepluhina Date: Wed, 17 Mar 2021 19:35:43 +0100 Subject: [PATCH 09/64] feat: added basic configuration --- src/guide/ssr/build-config.md | 83 +++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 src/guide/ssr/build-config.md diff --git a/src/guide/ssr/build-config.md b/src/guide/ssr/build-config.md new file mode 100644 index 0000000000..8616d19d5d --- /dev/null +++ b/src/guide/ssr/build-config.md @@ -0,0 +1,83 @@ +# Build Configuration + +We will assume you already know how to configure webpack for a client-only project. The config for an SSR project will be largely similar. This example shows a config for Vue CLI project but can be adapted to be used on any webpack build. + +```js +const ManifestPlugin = require('webpack-manifest-plugin') +const nodeExternals = require('webpack-node-externals') +const webpack = require('webpack') + +module.exports = { + chainWebpack: webpackConfig => { + // + webpackConfig.module.rule('vue').uses.delete('cache-loader') + webpackConfig.module.rule('js').uses.delete('cache-loader') + webpackConfig.module.rule('ts').uses.delete('cache-loader') + webpackConfig.module.rule('tsx').uses.delete('cache-loader') + + if (!process.env.SSR) { + // Point entry to your app's client entry file + webpackConfig + .entry('app') + .clear() + .add('./src/client-entry.js') + return + } + + // Point entry to your app's server entry file + webpackConfig + .entry('app') + .clear() + .add('./src/server-entry.js') + + // This allows webpack to handle dynamic imports in a Node-appropriate + // fashion, and also tells `vue-loader` to emit server-oriented code when + // compiling Vue components. + webpackConfig.target('node') + // This tells the server bundle to use Node-style exports + webpackConfig.output.libraryTarget('commonjs2') + + webpackConfig + .plugin('manifest') + .use(new ManifestPlugin({ fileName: 'ssr-manifest.json' })) + + // https://webpack.js.org/configuration/externals/#function + // https://github.com/liady/webpack-node-externals + // Externalize app dependencies. This makes the server build much faster + // and generates a smaller bundle file. + // do not externalize dependencies that need to be processed by webpack. + // you should also whitelist deps that modifies `global` (e.g. polyfills) + webpackConfig.externals(nodeExternals({ allowlist: /\.(css|vue)$/ })) + + webpackConfig.optimization.splitChunks(false).minimize(false) + + webpackConfig.plugins.delete('hmr') + webpackConfig.plugins.delete('preload') + webpackConfig.plugins.delete('prefetch') + webpackConfig.plugins.delete('progress') + webpackConfig.plugins.delete('friendly-errors') + + webpackConfig.plugin('limit').use( + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1 + }) + ) + } +} +``` + +### Externals Caveats + +Notice that in the `externals` option we are whitelisting CSS files. This is because CSS imported from dependencies should still be handled by webpack. If you are importing any other types of files that also rely on webpack (e.g. `*.vue`, `*.sass`), you should add them to the whitelist as well. + +If you are using `runInNewContext: 'once'` or `runInNewContext: true`, then you also need to whitelist polyfills that modify `global`, e.g. `babel-polyfill`. This is because when using the new context mode, **code inside a server bundle has its own `global` object.** Since you don't really need it on the server when using Node 7.6+, it's actually easier to just import it in the client entry. + +### Generating `clientManifest` + +In addition to the server bundle, we can also generate a client build manifest. With the client manifest and the server bundle, the renderer now has information of both the server _and_ client builds, so it can automatically infer and inject [preload / prefetch directives](https://css-tricks.com/prefetching-preloading-prebrowsing/) and css links / script tags into the rendered HTML. + +The benefits are two-fold: + +1. It can replace `html-webpack-plugin` for injecting the correct asset URLs when there are hashes in your generated filenames. + +2. When rendering a bundle that leverages webpack's on-demand code splitting features, we can ensure the optimal chunks are preloaded / prefetched, and also intelligently inject `