Skip to content

Ssr webpack #937

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 67 commits into from
Apr 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
f38249d
feat: added universal code
NataliaTepluhina Mar 3, 2021
42f211c
Added a project structure entry
NataliaTepluhina Mar 10, 2021
3d6ec0d
feat: added a router guide
NataliaTepluhina Mar 10, 2021
3d6097d
fix: fixed entries and app
NataliaTepluhina Mar 11, 2021
d543e7d
fix: fixed routing
NataliaTepluhina Mar 11, 2021
6cfeac5
chore: formatted the code
NataliaTepluhina Mar 11, 2021
5646123
fix: fixed structure and router
NataliaTepluhina Mar 11, 2021
d12048e
feat: added hydration guide
NataliaTepluhina Mar 17, 2021
a50d49e
feat: added basic configuration
NataliaTepluhina Mar 17, 2021
59346d4
feat: added sidebar config
NataliaTepluhina Mar 18, 2021
053eb04
Update src/guide/ssr/build-config.md
NataliaTepluhina Mar 22, 2021
3fde0f8
Update src/guide/ssr/getting-started.md
NataliaTepluhina Mar 22, 2021
8c19e79
Update src/guide/ssr/hydration.md
NataliaTepluhina Mar 22, 2021
e20873c
Update src/guide/ssr/hydration.md
NataliaTepluhina Mar 22, 2021
0942c46
Update src/guide/ssr/hydration.md
NataliaTepluhina Mar 22, 2021
e0d5218
Update src/guide/ssr/routing.md
NataliaTepluhina Mar 22, 2021
32a16fd
Update src/guide/ssr/structure.md
NataliaTepluhina Mar 22, 2021
dfe68d1
Update src/guide/ssr/routing.md
NataliaTepluhina Mar 22, 2021
d58ff69
Update src/guide/ssr/routing.md
NataliaTepluhina Mar 22, 2021
d984a3e
Update src/guide/ssr/routing.md
NataliaTepluhina Mar 22, 2021
1667317
Update src/guide/ssr/routing.md
NataliaTepluhina Mar 22, 2021
2ffe6d5
Update src/guide/ssr/structure.md
NataliaTepluhina Mar 22, 2021
b96d6b4
Update src/guide/ssr/structure.md
NataliaTepluhina Mar 22, 2021
60d284d
Update src/guide/ssr/structure.md
NataliaTepluhina Mar 22, 2021
764506a
Update src/guide/ssr/structure.md
NataliaTepluhina Mar 22, 2021
494fb49
Update src/guide/ssr/build-config.md
NataliaTepluhina Mar 22, 2021
07c87a4
Update src/guide/ssr/build-config.md
NataliaTepluhina Mar 22, 2021
72b37e8
Update src/guide/ssr/routing.md
NataliaTepluhina Mar 22, 2021
d6be1e2
Update src/guide/ssr/routing.md
NataliaTepluhina Mar 22, 2021
a615136
Update src/guide/ssr/universal.md
NataliaTepluhina Mar 22, 2021
de4f557
Update src/guide/ssr/universal.md
NataliaTepluhina Mar 22, 2021
423f28f
fix: removed rootComponent
NataliaTepluhina Mar 23, 2021
c781688
fix: removed rootComponent from routing guide
NataliaTepluhina Mar 23, 2021
e383a7d
chore: comment about cache loader
NataliaTepluhina Mar 23, 2021
708a0e3
fix: removed Node version
NataliaTepluhina Mar 23, 2021
ea51bdd
fix: reworded client manifest
NataliaTepluhina Mar 23, 2021
6c9b6f6
fix: removed unnecessary comment
NataliaTepluhina Mar 23, 2021
14963cb
fix: added defineAsyncComponent
NataliaTepluhina Mar 23, 2021
e0b21d5
fix: fixed links
NataliaTepluhina Mar 23, 2021
e3db9c9
fix: fixed a link
NataliaTepluhina Mar 23, 2021
645069e
fix: explained key differences
NataliaTepluhina Mar 25, 2021
fa95156
Update src/guide/ssr/routing.md
NataliaTepluhina Mar 25, 2021
886d486
Update src/guide/ssr/build-config.md
NataliaTepluhina Mar 25, 2021
89c0ea8
Update src/guide/ssr/build-config.md
NataliaTepluhina Mar 25, 2021
efb0b97
fix: fixed app path
NataliaTepluhina Mar 25, 2021
140c2e4
Merge branch 'ssr-webpack' of github.com:vuejs/docs-next into ssr-web…
NataliaTepluhina Mar 25, 2021
e5cea72
fix: added links
NataliaTepluhina Mar 25, 2021
026d2ca
feat: server code
NataliaTepluhina Mar 27, 2021
b7d08c8
fix: fixed router
NataliaTepluhina Mar 27, 2021
8aa3e9a
fix: changes server chapter name
NataliaTepluhina Mar 27, 2021
feebe1b
Update src/guide/ssr/build-config.md
NataliaTepluhina Mar 27, 2021
d395ce1
fix: remove hmr
NataliaTepluhina Mar 28, 2021
ad13a9d
Merge branch 'ssr-webpack' of github.com:vuejs/docs-next into ssr-web…
NataliaTepluhina Mar 28, 2021
fb69cfd
fix: simplified reading file
NataliaTepluhina Mar 28, 2021
1de777e
fix: rephrased caveats
NataliaTepluhina Mar 28, 2021
24a1646
fix: removed custom directives mention
NataliaTepluhina Mar 28, 2021
b306b9e
Update src/guide/ssr/server.md
NataliaTepluhina Mar 31, 2021
609a346
Update src/guide/ssr/server.md
NataliaTepluhina Mar 31, 2021
4a8c921
Update src/guide/ssr/server.md
NataliaTepluhina Mar 31, 2021
2c54e8a
Update src/guide/ssr/server.md
NataliaTepluhina Mar 31, 2021
7deaa4c
Update src/guide/ssr/server.md
NataliaTepluhina Mar 31, 2021
5668010
fix: fixed server code
NataliaTepluhina Mar 31, 2021
32b1b4c
Merge branch 'ssr-webpack' of github.com:vuejs/docs-next into ssr-web…
NataliaTepluhina Mar 31, 2021
999f518
fix: fixed build script
NataliaTepluhina Mar 31, 2021
ff5420e
fix: fixed router async
NataliaTepluhina Mar 31, 2021
c0361c4
Update src/guide/ssr/routing.md
NataliaTepluhina Apr 5, 2021
1fdfa5e
Update src/guide/ssr/routing.md
NataliaTepluhina Apr 5, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,13 @@ const sidebar = {
],
ssr: [
['/guide/ssr/introduction', 'Introduction'],
'/guide/ssr/getting-started'
'/guide/ssr/getting-started',
'/guide/ssr/universal',
'/guide/ssr/structure',
'/guide/ssr/build-config',
'/guide/ssr/server',
'/guide/ssr/routing',
'/guide/ssr/hydration'
],
contributing: [
{
Expand Down
106 changes: 106 additions & 0 deletions src/guide/ssr/build-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Build Configuration

The webpack config for an SSR project will be similar to a client-only project. If you're not familiar with configuring webpack, you can find more information in the documentation for [Vue CLI](https://cli.vuejs.org/guide/webpack.html#working-with-webpack) or [configuring Vue Loader manually](https://vue-loader.vuejs.org/guide/#manual-setup).

## Key Differences with Client-Only Builds

1. We need to create a [webpack manifest](https://webpack.js.org/concepts/manifest/) for our server-side code. This is a JSON file that webpack keeps to track how all the modules map to the output bundles.

2. We should [externalize application dependencies](https://webpack.js.org/configuration/externals/). This makes the server build much faster and generates a smaller bundle file. When doing this, we have to exclude dependencies that need to be processed by webpack (like `.css`. or `.vue` files).

3. We need to change webpack [target](https://webpack.js.org/concepts/targets/) to Node.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.

4. When building a server entry, we would need to define an environment variable to indicate we are working with SSR. It might be helpful to add a few `scripts` to the project's `package.json`:

```json
"scripts": {
"build:client": "vue-cli-service build --dest dist/client",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's worth exploring into the vue-cli agnostic SSR idea? Because right now if you're not using vue-cli your first migration step would be to migrate your current build config to vue-cli and only then apply all the SSR practies in the guide.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up: #969

"build:server": "SSR=1 vue-cli-service build --dest dist/server",
"build": "npm run build:client && npm run build:server",
}
```

## Example Configuration

Below is a sample `vue.config.js` that adds SSR rendering to a Vue CLI project, but it can be adapted for any webpack build.

```js
const { WebpackManifestPlugin } = require('webpack-manifest-plugin')
const nodeExternals = require('webpack-node-externals')
const webpack = require('webpack')

module.exports = {
chainWebpack: webpackConfig => {
Copy link
Contributor

@CyberAP CyberAP Mar 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking whether it is a good idea or not to have 3 configs here?
One for common rules likes aliases, one for client only and one for server only. Both of them deriving from the base config (using webpackMerge).

The reasoning behind this is that using chainWebpack for re-configuration is hardly maintainable. It does get very complex extremely fast so noone wants to touch this config because noone understands what happens here. I have the same setup on my project and it overwrites a ton of rules exactly like shown here: replacements\deletions inside if statements which depend on environment. I think a static webpack config is way more readable because you can understand what it does at the first glance just by the structure of that config (given you have an experience with webpack). This programmatic config is hard to read because you have to parse it line by line working as an interpreter. It is also unclear what the base vue-cli config looks like, so all the delete calls look like magic to me.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with the point but I have a few questions regarding it:

  • should we move away from Vue CLI in this case? I am not sure how it's done correctly in the CLI environment as I usually always went with configureWebpack / chainWebpack in the `vue.config.js

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a follow-up to refactor to CLI agnostic build with a base, server and client configs: #969

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is probably not feasible with current vue-cli API, so it should be a part of vue-cli agnostic build in that case.

// We need to disable cache loader, otherwise the client build
// will used cached components from the server build
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/entry-client.js')
return
}

// Point entry to your app's server entry file
webpackConfig
.entry('app')
.clear()
.add('./src/entry-server.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 WebpackManifestPlugin({ 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 modify `global` (e.g. polyfills)
webpackConfig.externals(nodeExternals({ allowlist: /\.(css|vue)$/ }))

webpackConfig.optimization.splitChunks(false).minimize(false)

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, 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. This way it can automatically infer and inject [preload / prefetch directives](https://css-tricks.com/prefetching-preloading-prebrowsing/), `<link>` and `<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 `<script>` tags for needed async chunks to avoid waterfall requests on the client, thus improving TTI (time-to-interactive).
6 changes: 4 additions & 2 deletions src/guide/ssr/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ yarn add express
```

```js
// server.js
Copy link
Contributor

@CyberAP CyberAP Mar 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since most of the people who want to migrate from CSR to SSR would be having some kind of main.js entry file maybe it would make sense to show this migration path as a step-by-step guide how we extract our current code from main.js and split it into three different files: app.js, entry-client.js and entry-server.js. The current approach is also very good, but I think it's more focused on writing an SSR app from scracth. Maybe there should be a separate guide for migrating from CSR to SSR? What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, it would be a great improvement! Created a follow-up: #970


const { createSSRApp } = require('vue')
const { renderToString } = require('@vue/server-renderer')
const server = require('express')()
Expand All @@ -83,7 +85,7 @@ server.get('*', async (req, res) => {
<html>
<body>
<h1>My First Heading</h1>
${appContent}
<div id="app">${appContent}</div>
</body>
</html>
`
Expand All @@ -94,4 +96,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 taken 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.
33 changes: 33 additions & 0 deletions src/guide/ssr/hydration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# 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
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.

Vue provides a `createSSRApp` method for use in client-side code (in this case, in our `entry-client.js`) to tell Vue to hydrate the existing static HTML instead of re-creating all the DOM elements.

### Hydration Caveats

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. There will be a warning in the browser console but your site will still work.

The first key way to ensure that SSR is working to ensuring your application state is the same on client and server. Take special care not to depend on APIs specific to the browser (like window width, device capability or localStorage) or server (such as Node built-ins), and take care where the same code will give different results when run in different places (such as when using timezones, timestamps, normalizing URLs or generating random numbers). See [Writing Universal Code](./universal.md) for more details.

A second key thing to be aware of when using SSR + client hydration is that invalid HTML may be altered by the browser. For example, when you write this in a Vue template:

```html
<table>
<tr>
<td>hi</td>
</tr>
</table>
```

The browser will automatically inject `<tbody>` inside `<table>`, however, the virtual DOM generated by Vue does not contain `<tbody>`, so it will cause a mismatch. To ensure correct matching, make sure to write valid HTML in your templates.

You might consider using a HTML validator like [the W3C Markup Validation Service](https://validator.w3.org/) or [HTML-validate](https://html-validate.org/) to check your templates in development.
121 changes: 121 additions & 0 deletions src/guide/ssr/routing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Routing and Code-Splitting
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an SSR app developer I am also curious how Critical CSS should be implemeneted correctly with this approach.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree! However, this guide is not full and there are a few missing parts here. Here is an issue with 'what's currently missing': #971


## 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 application instance, we also need a fresh router instance for each request, so the file exports a `createRouter` function:

```js
// router.js
import { createRouter, createMemoryHistory, createWebHistory } from 'vue-router'
import MyUser from './components/MyUser.vue'

const isServer = typeof window === 'undefined'

const history = isServer ? createMemoryHistory() : createWebHistory()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a slightly better approach here would be to pass the history instance on an entry-level, so we have better code-splitting here and won't have to check for isServer flag.


const routes = [{ path: '/user', component: MyUser }]

export default function() {
return createRouter({ routes, history })
}
```

And update our `app.js`, client and server entries:

```js
// app.js
import { createSSRApp } from 'vue'
import App from './App.vue'
import createRouter from './router'

export default function(args) {
const app = createSSRApp(App)
const router = createRouter()

app.use(router)

return {
app,
router
}
}
```

```js
// entry-client.js
const { app, router } = createApp({
/*...*/
})
```

```js
// entry-server.js
const { app, router } = createApp({
/*...*/
})
```

## Code-Splitting

Code-splitting, or lazy-loading part of your app, helps reduce the size 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 Router provides [lazy-loading support](https://next.router.vuejs.org/guide/advanced/lazy-loading.html), allowing [webpack to code-split at that point](https://webpack.js.org/guides/code-splitting-async/). All you need to do is:

```js
// change this...
import MyUser from './components/MyUser.vue'
const routes = [{ path: '/user', component: MyUser }]

// to this:
const routes = [
{ path: '/user', component: () => import('./components/MyUser.vue') }
]
```

On both client and server we need to wait for router to resolve async route components ahead of time in order to properly invoke in-component hooks. For this we will be using [router.isReady](https://next.router.vuejs.org/api/#isready) method Let's update our client entry:

```js
// entry-client.js
import createApp from './app'

const { app, router } = createApp({
/* ... */
})

router.isReady().then(() => {
app.mount('#app')
})
```

We also need to update our `server.js` script:

```js
// server.js
const path = require('path')

const appPath = path.join(__dirname, './dist', 'server', manifest['app.js'])
const createApp = require(appPath).default

server.get('*', async (req, res) => {
const { app, router } = createApp()

router.push(req.url)
await router.isReady()

const appContent = await renderToString(app)

fs.readFile(path.join(__dirname, '/dist/client/index.html'), (err, html) => {
if (err) {
throw err
}

html = html
.toString()
.replace('<div id="app">', `<div id="app">${appContent}`)
res.setHeader('Content-Type', 'text/html')
res.send(html)
})
})
```
Loading