Skip to content

Commit 88c61cb

Browse files
NataliaTepluhinadanielroeskirtles-codeCyberAP
authored
Ssr webpack (#937)
* feat: added universal code * Added a project structure entry * feat: added a router guide * fix: fixed entries and app * fix: fixed routing * chore: formatted the code * fix: fixed structure and router * feat: added hydration guide * feat: added basic configuration * feat: added sidebar config * Update src/guide/ssr/build-config.md Co-authored-by: Daniel Roe <[email protected]> * Update src/guide/ssr/getting-started.md Co-authored-by: Daniel Roe <[email protected]> * Update src/guide/ssr/hydration.md Co-authored-by: Daniel Roe <[email protected]> * Update src/guide/ssr/hydration.md Co-authored-by: Daniel Roe <[email protected]> * Update src/guide/ssr/hydration.md Co-authored-by: Daniel Roe <[email protected]> * Update src/guide/ssr/routing.md Co-authored-by: Daniel Roe <[email protected]> * Update src/guide/ssr/structure.md Co-authored-by: Daniel Roe <[email protected]> * Update src/guide/ssr/routing.md Co-authored-by: Daniel Roe <[email protected]> * Update src/guide/ssr/routing.md Co-authored-by: Daniel Roe <[email protected]> * Update src/guide/ssr/routing.md Co-authored-by: Daniel Roe <[email protected]> * Update src/guide/ssr/routing.md Co-authored-by: Daniel Roe <[email protected]> * Update src/guide/ssr/structure.md Co-authored-by: Daniel Roe <[email protected]> * Update src/guide/ssr/structure.md Co-authored-by: Daniel Roe <[email protected]> * Update src/guide/ssr/structure.md Co-authored-by: Daniel Roe <[email protected]> * Update src/guide/ssr/structure.md Co-authored-by: Daniel Roe <[email protected]> * Update src/guide/ssr/build-config.md Co-authored-by: skirtle <[email protected]> * Update src/guide/ssr/build-config.md Co-authored-by: skirtle <[email protected]> * Update src/guide/ssr/routing.md Co-authored-by: skirtle <[email protected]> * Update src/guide/ssr/routing.md Co-authored-by: skirtle <[email protected]> * Update src/guide/ssr/universal.md Co-authored-by: skirtle <[email protected]> * Update src/guide/ssr/universal.md Co-authored-by: skirtle <[email protected]> * fix: removed rootComponent * fix: removed rootComponent from routing guide * chore: comment about cache loader * fix: removed Node version * fix: reworded client manifest * fix: removed unnecessary comment * fix: added defineAsyncComponent * fix: fixed links * fix: fixed a link * fix: explained key differences * Update src/guide/ssr/routing.md Co-authored-by: skirtle <[email protected]> * Update src/guide/ssr/build-config.md Co-authored-by: skirtle <[email protected]> * Update src/guide/ssr/build-config.md Co-authored-by: skirtle <[email protected]> * fix: fixed app path * fix: added links * feat: server code * fix: fixed router * fix: changes server chapter name * Update src/guide/ssr/build-config.md Co-authored-by: Stanislav Lashmanov <[email protected]> * fix: remove hmr * fix: simplified reading file * fix: rephrased caveats * fix: removed custom directives mention * Update src/guide/ssr/server.md Co-authored-by: skirtle <[email protected]> * Update src/guide/ssr/server.md Co-authored-by: skirtle <[email protected]> * Update src/guide/ssr/server.md Co-authored-by: skirtle <[email protected]> * Update src/guide/ssr/server.md Co-authored-by: skirtle <[email protected]> * Update src/guide/ssr/server.md Co-authored-by: skirtle <[email protected]> * fix: fixed server code * fix: fixed build script * fix: fixed router async * Update src/guide/ssr/routing.md Co-authored-by: skirtle <[email protected]> * Update src/guide/ssr/routing.md Co-authored-by: skirtle <[email protected]> Co-authored-by: Daniel Roe <[email protected]> Co-authored-by: skirtle <[email protected]> Co-authored-by: Stanislav Lashmanov <[email protected]>
1 parent 90688fa commit 88c61cb

File tree

8 files changed

+537
-3
lines changed

8 files changed

+537
-3
lines changed

src/.vuepress/config.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,13 @@ const sidebar = {
213213
],
214214
ssr: [
215215
['/guide/ssr/introduction', 'Introduction'],
216-
'/guide/ssr/getting-started'
216+
'/guide/ssr/getting-started',
217+
'/guide/ssr/universal',
218+
'/guide/ssr/structure',
219+
'/guide/ssr/build-config',
220+
'/guide/ssr/server',
221+
'/guide/ssr/routing',
222+
'/guide/ssr/hydration'
217223
],
218224
contributing: [
219225
{

src/guide/ssr/build-config.md

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Build Configuration
2+
3+
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).
4+
5+
## Key Differences with Client-Only Builds
6+
7+
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.
8+
9+
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).
10+
11+
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.
12+
13+
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`:
14+
15+
```json
16+
"scripts": {
17+
"build:client": "vue-cli-service build --dest dist/client",
18+
"build:server": "SSR=1 vue-cli-service build --dest dist/server",
19+
"build": "npm run build:client && npm run build:server",
20+
}
21+
```
22+
23+
## Example Configuration
24+
25+
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.
26+
27+
```js
28+
const { WebpackManifestPlugin } = require('webpack-manifest-plugin')
29+
const nodeExternals = require('webpack-node-externals')
30+
const webpack = require('webpack')
31+
32+
module.exports = {
33+
chainWebpack: webpackConfig => {
34+
// We need to disable cache loader, otherwise the client build
35+
// will used cached components from the server build
36+
webpackConfig.module.rule('vue').uses.delete('cache-loader')
37+
webpackConfig.module.rule('js').uses.delete('cache-loader')
38+
webpackConfig.module.rule('ts').uses.delete('cache-loader')
39+
webpackConfig.module.rule('tsx').uses.delete('cache-loader')
40+
41+
if (!process.env.SSR) {
42+
// Point entry to your app's client entry file
43+
webpackConfig
44+
.entry('app')
45+
.clear()
46+
.add('./src/entry-client.js')
47+
return
48+
}
49+
50+
// Point entry to your app's server entry file
51+
webpackConfig
52+
.entry('app')
53+
.clear()
54+
.add('./src/entry-server.js')
55+
56+
// This allows webpack to handle dynamic imports in a Node-appropriate
57+
// fashion, and also tells `vue-loader` to emit server-oriented code when
58+
// compiling Vue components.
59+
webpackConfig.target('node')
60+
// This tells the server bundle to use Node-style exports
61+
webpackConfig.output.libraryTarget('commonjs2')
62+
63+
webpackConfig
64+
.plugin('manifest')
65+
.use(new WebpackManifestPlugin({ fileName: 'ssr-manifest.json' }))
66+
67+
// https://webpack.js.org/configuration/externals/#function
68+
// https://github.com/liady/webpack-node-externals
69+
// Externalize app dependencies. This makes the server build much faster
70+
// and generates a smaller bundle file.
71+
72+
// Do not externalize dependencies that need to be processed by webpack.
73+
// You should also whitelist deps that modify `global` (e.g. polyfills)
74+
webpackConfig.externals(nodeExternals({ allowlist: /\.(css|vue)$/ }))
75+
76+
webpackConfig.optimization.splitChunks(false).minimize(false)
77+
78+
webpackConfig.plugins.delete('preload')
79+
webpackConfig.plugins.delete('prefetch')
80+
webpackConfig.plugins.delete('progress')
81+
webpackConfig.plugins.delete('friendly-errors')
82+
83+
webpackConfig.plugin('limit').use(
84+
new webpack.optimize.LimitChunkCountPlugin({
85+
maxChunks: 1
86+
})
87+
)
88+
}
89+
}
90+
```
91+
92+
## Externals Caveats
93+
94+
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.
95+
96+
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.
97+
98+
## Generating `clientManifest`
99+
100+
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.
101+
102+
The benefits are two-fold:
103+
104+
1. It can replace `html-webpack-plugin` for injecting the correct asset URLs when there are hashes in your generated filenames.
105+
106+
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).

src/guide/ssr/getting-started.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ yarn add express
6464
```
6565

6666
```js
67+
// server.js
68+
6769
const { createSSRApp } = require('vue')
6870
const { renderToString } = require('@vue/server-renderer')
6971
const server = require('express')()
@@ -83,7 +85,7 @@ server.get('*', async (req, res) => {
8385
<html>
8486
<body>
8587
<h1>My First Heading</h1>
86-
${appContent}
88+
<div id="app">${appContent}</div>
8789
</body>
8890
</html>
8991
`
@@ -94,4 +96,4 @@ server.get('*', async (req, res) => {
9496
server.listen(8080)
9597
```
9698

97-
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.
99+
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.

src/guide/ssr/hydration.md

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Client Side Hydration
2+
3+
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.
4+
5+
In `entry-client.js`, we are simply mounting the app with this line:
6+
7+
```js
8+
app.mount('#app')
9+
```
10+
11+
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.
12+
13+
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.
14+
15+
### Hydration Caveats
16+
17+
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.
18+
19+
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.
20+
21+
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:
22+
23+
```html
24+
<table>
25+
<tr>
26+
<td>hi</td>
27+
</tr>
28+
</table>
29+
```
30+
31+
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.
32+
33+
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.

src/guide/ssr/routing.md

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# Routing and Code-Splitting
2+
3+
## Routing with `vue-router`
4+
5+
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!
6+
7+
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:
8+
9+
```js
10+
// router.js
11+
import { createRouter, createMemoryHistory, createWebHistory } from 'vue-router'
12+
import MyUser from './components/MyUser.vue'
13+
14+
const isServer = typeof window === 'undefined'
15+
16+
const history = isServer ? createMemoryHistory() : createWebHistory()
17+
18+
const routes = [{ path: '/user', component: MyUser }]
19+
20+
export default function() {
21+
return createRouter({ routes, history })
22+
}
23+
```
24+
25+
And update our `app.js`, client and server entries:
26+
27+
```js
28+
// app.js
29+
import { createSSRApp } from 'vue'
30+
import App from './App.vue'
31+
import createRouter from './router'
32+
33+
export default function(args) {
34+
const app = createSSRApp(App)
35+
const router = createRouter()
36+
37+
app.use(router)
38+
39+
return {
40+
app,
41+
router
42+
}
43+
}
44+
```
45+
46+
```js
47+
// entry-client.js
48+
const { app, router } = createApp({
49+
/*...*/
50+
})
51+
```
52+
53+
```js
54+
// entry-server.js
55+
const { app, router } = createApp({
56+
/*...*/
57+
})
58+
```
59+
60+
## Code-Splitting
61+
62+
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.
63+
64+
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:
65+
66+
```js
67+
// change this...
68+
import MyUser from './components/MyUser.vue'
69+
const routes = [{ path: '/user', component: MyUser }]
70+
71+
// to this:
72+
const routes = [
73+
{ path: '/user', component: () => import('./components/MyUser.vue') }
74+
]
75+
```
76+
77+
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:
78+
79+
```js
80+
// entry-client.js
81+
import createApp from './app'
82+
83+
const { app, router } = createApp({
84+
/* ... */
85+
})
86+
87+
router.isReady().then(() => {
88+
app.mount('#app')
89+
})
90+
```
91+
92+
We also need to update our `server.js` script:
93+
94+
```js
95+
// server.js
96+
const path = require('path')
97+
98+
const appPath = path.join(__dirname, './dist', 'server', manifest['app.js'])
99+
const createApp = require(appPath).default
100+
101+
server.get('*', async (req, res) => {
102+
const { app, router } = createApp()
103+
104+
router.push(req.url)
105+
await router.isReady()
106+
107+
const appContent = await renderToString(app)
108+
109+
fs.readFile(path.join(__dirname, '/dist/client/index.html'), (err, html) => {
110+
if (err) {
111+
throw err
112+
}
113+
114+
html = html
115+
.toString()
116+
.replace('<div id="app">', `<div id="app">${appContent}`)
117+
res.setHeader('Content-Type', 'text/html')
118+
res.send(html)
119+
})
120+
})
121+
```

0 commit comments

Comments
 (0)