Basic concepts of server-side rendering
Client-side rendering
The most common SPA page that front-end developers do is typical client rendering. When sending a request, we get a simple HTML structure and then load static resources, resulting in a blank screen on the front page. When the resource load is executed, the page elements created by the client are displayed, which is called client rendering
Server side rendering
Server-side rendering has been around for a long time, such as in the days of typical PHP output pages, where the entire HTML content of the page was sent to the client at once on request, with good rendering speed and SEO friendly support. But for the current front-end framework server rendering, we are not only output HTML, but to play the framework to provide a variety of functions, that is, today to talk about VUE SSR
Problems that need to be solved
In order to implement a straight HTML structure while preserving the framework’s ability, we need to solve the following problems:
1. Problems with converting vUE instances to HTML strings
The output to the browser should preserve the vUE capability, that is, interaction
3. Engineering ability
The directory structure
├ ─ ─ index. The HTML ├ ─ ─ package. The json ├ ─ ─ the readme. Md ├ ─ ─ for server js # server runs the script ├ ─ ─ the SRC | ├ ─ ─ components | ├ ─ ─ the router | | └ ─ ─ Index. Js | └ ─ ─ views | └ ─ ─ Home. Vue | ├ ─ ─ App. Vue | ├ ─ ─ App. Js | ├ ─ ─ client_entry. Js | ├ ─ ─ server_entry. JsCopy the code
Start coding
The basic case
Install the base dependencies first
yarn add express vue vue-router vue-server-renderer -S
yarn add nodemon -D
Copy the code
The vue-server-renderer dependency is officially provided to help us handle server-side rendering. Express is the back-end framework for this experiment
Server codeserver.js
const express = require('express')
const Vue = require('vue')
const renderer = require('vue-server-renderer').createRenderer()
const app = express()
app.get(The '*'.async (req, res) => {
const vm = new Vue({
template: `<button @click="count++">{{count}}</button>`.data: {
count: 1
},
})
renderer.renderToStream(vm).pipe(res)
})
app.listen(3000)
Copy the code
Add a script to package.json to run the server-side code
"start": "nodemon server.js"
Copy the code
A blank button appears on the page, but clicking it has no effect. By looking at the source code found is very pure source code, even the standard HTML has no.
Write a template to enrich the page
CreateRenderer can pass some configuration parameters, you can refer to the documentation or click in to see how to configure
server.js
const express = require('express')
const Vue = require('vue')
const template = require('fs').readFileSync('./index.html'.'utf-8')
const renderer = require('vue-server-renderer').createRenderer({
template
})
const app = express()
app.get(The '*'.async (req, res) => {
const vm = new Vue({
template: `<button @click="count++">{{count}}</button>`.data: {
count: 1
},
})
renderer.renderToStream(vm, {
title: 'hello vue ssr'
}).pipe(res)
})
app.listen(3000)
Copy the code
index.html
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<title>{{title}}</title>
</head>
<body>
<! --vue-ssr-outlet-->
</body>
</html>
Copy the code
If you are careful, you will notice that I passed the second parameter to renderToStream and rendered it in HTML. So far, I have experienced SSR rendering, but it is far from my usual vUE – CLI project development. Can’t play with our single file components and routing and so on, let’s start to change
Engineering solution
To do it as a normal development project, there will be no dependency on webpack packaging, etc. Install the dependency before you start.
yarn add webpack webpack-cli vue-loader vue-template-compiler vue-style-loader css-loader babel-loader @babel/core @babel/preset-env webpack-merge -D
Copy the code
After installing the dependencies, we first created a build folder in the root directory to hold our WebPack build configuration files and created three files:
- build/base.config.js
- build/client.config.js
- build/server.config.js
The previous basic part obviously only solves part of the problem on the server side, how to ensure the back-end output and front-end interaction and code reuse and engineering construction have not been solved. Before we write the configuration of the packaged build, let’s write the entry file for the server and client respectively. Let’s look at the following figure:
Because the server process is resident in memory, we need to avoid singletons to avoid cross-request state contamination, so the general code app.js is as follows:
const Vue = require('vue')
module.exports = function () {
const app = new Vue({
data: {
count: 1
},
template: `
`,})return app
}
Copy the code
We haven’t used webpack yet, so now we’re writing the code directly in CommonJS, and the corresponding server.js will look like this:
const express = require('express')
const Vue = require('vue')
const template = require('fs').readFileSync('./index.html'.'utf-8')
const renderer = require('vue-server-renderer').createRenderer({
template
})
const app = express()
app.get(The '*'.async (req, res) => {
const createApp = require('./src/app.js')
renderer.renderToStream(createApp(), {
title: 'hello vue ssr'
}).pipe(res)
})
app.listen(3000)
Copy the code
But ah but, now there is no use of Webpack, so we can not play well ah, certainly not in line with the style of front-end son, right? Let’s write the client side and the server side entry file separately, incidentally also transformed into esModule app.vue
<template> <div id="app"> <h1>this is app vue</h1> <button @click="count++">{{ count }}</button> </div> </template> <script> export default { data() { return { count: 1, }; }}; </script> <style> </style>Copy the code
app.js
import Vue from 'vue'
import App from './App.vue'
export default function () {
const app = new Vue({
render: h= > h(App)
})
return app
}
Copy the code
client_entry.js
import createApp from './app.js'
createApp().$mount('#app')
Copy the code
server_entry.js
import createApp from './app.js'
export default() = > {const app = createApp()
return app
}
Copy the code
Js can not be used directly in the source file, so we need to write a package configuration to package the ability to enrich the project, we only need to package the resources needed by the ue-server-renderer, so that our server program can complete SSR and client isomers:
build/base.config.js
const path = require('path')
const { VueLoaderPlugin } = require('vue-loader')
module.exports = {
mode: 'development'.output: {
filename: '[name].bundle.js'
},
module: {
rules: [{test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'.options: {
presets: ['@babel/preset-env'],}},}, {test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.css$/,
use: [
'vue-style-loader',
{
loader: 'css-loader',}]}]},plugins: [
new VueLoaderPlugin()
]
}
Copy the code
build/client.config.js
const path = require('path')
const { merge } = require('webpack-merge')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
const base = require('./base.config.js')
module.exports = merge(base, {
entry: {
client: path.resolve(__dirname, '.. /src/client_entry.js')},plugins: [
new VueSSRClientPlugin()
]
})
Copy the code
build/server.config.js
const path = require('path')
const { merge } = require('webpack-merge')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
const base = require('./base.config.js')
module.exports = merge(base, {
entry: {
client: path.resolve(__dirname, '.. /src/server_entry.js')},target: 'node'.output: {
libraryTarget: 'commonjs2'
},
plugins: [
new VueSSRServerPlugin()
]
})
Copy the code
Next we add a few commands to package.json:
"scripts": {
"start": "nodemon server.js"."build": "npm run build:client && npm run build:server"."build:client": "webpack --config build/client.config.js"."build:server": "webpack --config build/server.config.js"
}
Copy the code
After the build is executed and the packaged file is generated, we will modify our server-side code server.js
const express = require('express')
const fs = require('fs')
const template = fs.readFileSync('./index.html'.'utf-8')
// const renderer = require('vue-server-renderer').createRenderer({
// template
// })
const app = express()
app.use(express.static('dist')) //
const serverBundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
const renderer = require('vue-server-renderer').createBundleRenderer(serverBundle, {
runInNewContext: false,
template,
clientManifest,
})
app.get(The '*'.async (req, res) => {
// const createApp = require('./src/app.js')
// renderer.renderToStream(createApp(), {
// title: 'hello vue ssr'
// }).pipe(res)
renderer.renderToString({
title:'hello vue ssr'
}, (err, html) = > {
// Handle exceptions......
res.end(html)
})
})
app.listen(3000)
Copy the code
Run yarn Build and YARN start again. After running yarn build and YARN start again, the console displays an error.
1. Specify publicPath in the basic configuration of WebPack
2, app. Use (express. Static (‘ dist ‘))
At this point, a basic server rendering is complete, the page is straight OUT of HTML, and the buttons are clickable!
Resolve routing problems
So far I have not used routing, first think about it, the service is from the background, and then route JS code here certainly do not know, certainly need to tell the server to pass to our application code! Okay, so let’s write some basic routing code, and then we’ll solve the routing problem
router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
import About from '.. /views/About.vue'
import Home from '.. /views/Home.vue'
function createRouter() {
return new VueRouter({
mode: 'history'.routes: [{path: '/about'.component: About
},
{
path: '/'.component: Home
}
]
})
}
export default createRouter
Copy the code
Change the corresponding app.js to:
import Vue from 'vue'
import App from './App.vue'
import createRouter from './router'
export default function () {
const router = createRouter()
const app = new Vue({
router,
render: h= > h(App)
})
return {
router,
app
}
}
Copy the code
The server and client entry files also need to be modified to deconstruct the app and route, especially the server entry needs to be modified to a promise form because there may be asynchronous components
server_entry.js
import createApp from './app.js'
export default (ctx) => {
return new Promise((resolve, reject) = > {
const { app, router } = createApp()
router.push(ctx.url)
router.onReady(function () {
resolve(app)
})
})
}
Copy the code
Finally, we modify server.js to pass the requested URL to our application code
const express = require('express')
const fs = require('fs')
const template = fs.readFileSync('./index.html'.'utf-8')
// const renderer = require('vue-server-renderer').createRenderer({
// template
// })
const app = express()
app.use(express.static('dist'))
const serverBundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
const renderer = require('vue-server-renderer').createBundleRenderer(serverBundle, {
runInNewContext: false,
template,
clientManifest,
})
app.get(The '*'.async (req, res) => {
renderer.renderToString({
title:'hello vue ssr'.url:req.url
}, (err, html) = > {
// Handle exceptions......
console.log(err);
res.end(html)
})
})
app.listen(3000)
Copy the code
At this point, we can run the build and start commands again and enjoy the routing perfectly.
The last
Finally, I would like to say that routing can be handled dynamically by referring to nuxt.js. Of course, you can also inject some server-side data fetching functions and some 404 scenarios. There are still unfinished parts of the project: code splitting, VUex, etc! Hopefully this article has given you a good understanding of how to render on the server side. Especially that picture must be carefully and repeatedly look at oh, the last project code repository is: SSr_demo, if there is any problem or error, please reply in the comment area ~ ~