preface
SSR is certainly no stranger to everyone. Through server rendering, YOU can optimize SEO crawl and improve the loading speed of the home page. When I was learning SSR, I read a lot of articles, some of which were very inspiring to me, and some just copied the official website documents. Through a few days of learning, I have some understanding of SSR, but also from the beginning of a complete configuration of the SSR development environment, so I want to through this article, sum up some experience, and hope to learn SSR friends play a little help.
I will walk you through the SSR configuration step by step in five steps:
- Browser-only rendering
- Server render, not included
Ajax
Initialize data - Server render, including
Ajax
Initialize data - Server render, using
serverBundle
andclientManifest
To optimize - A complete based on
Vue + VueRouter + Vuex
The SSR engineering
If you are not familiar with what I said above, it doesn’t matter, follow me step by step down, eventually you can also configure an INDEPENDENT SSR development project, all the source code I will put on Github, we can refer to.
The body of the
1. Browser-only rendering
This configuration is a general development configuration based on Weback + vue. I will put some key code here. The complete code can be viewed on Github.
The directory structure
- node_modules
- components
- Bar.vue
- Foo.vue
- App.vue
- app.js
- index.html
- webpack.config.js
- package.json
- yarn.lock
- postcss.config.js
- .babelrc
- .gitignore
Copy the code
app.js
import Vue from 'vue';
import App from './App.vue';
let app = new Vue({
el: '#app',
render: h => h(App)
});
Copy the code
App.vue
<template>
<div>
<Foo></Foo>
<Bar></Bar>
</div>
</template>
<script>
import Foo from './components/Foo.vue';
import Bar from './components/Bar.vue';
export default {
components: {
Foo, Bar
}
}
</script>
Copy the code
index.html
<! DOCTYPE html> <html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge"> <title> Pure browser render </title> </head> <body> <div id="app"></div>
</body>
</html>
Copy the code
components/Foo.vue
<template>
<div class="foo">
<h1>Foo Component</h1>
</div>
</template>
<style>
.foo {
background: yellowgreen;
}
</style>
Copy the code
components/Bar.vue
<template>
<div class="bar">
<h1>Bar Component</h1>
</div>
</template>
<style>
.bar {
background: bisque;
}
</style>
Copy the code
webpack.config.js
const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
mode: 'development',
entry: './app.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
},
{
test: /\.css$/,
use: ['vue-style-loader'.'css-loader'.'postcss-loader'] / / if you need to separate out the CSS file, use the following configuration / / use: ExtractTextPlugin. Extract ({/ / fallback:'vue-style-loader',
// use: [
// 'css-loader', / /'postcss-loader'//})}, {test: /\.(jpg|jpeg|png|gif|svg)$/,
use: {
loader: 'url-loader',
options: {
limit: 10000 // 10Kb
}
}
},
{
test: /\.vue$/,
use: 'vue-loader'
}
]
},
plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
template: './index.html'}), // If you need to extract the CSS file separately, use the following configuration // new ExtractTextPlugin("styles.css")]};Copy the code
postcss.config.js
module.exports = {
plugins: [
require('autoprefixer')]};Copy the code
.babelrc
{
"presets": [
"@babel/preset-env"]."plugins"Const Foo = () => import()'.. /components/Foo.vue')
"dynamic-import-webpack"]}Copy the code
package.json
{
"name": "01"."version": "1.0.0"."main": "index.js"."license": "MIT"."scripts": {
"start": "yarn run dev"."dev": "webpack-dev-server"."build": "webpack"
},
"dependencies": {
"vue": "^ 2.5.17"
},
"devDependencies": {
"@babel/core": "^ 7.1.2." "."@babel/preset-env": "^ 7.1.0"."babel-plugin-dynamic-import-webpack": "^ 1.1.0." "."autoprefixer": "^ 9.1.5." "."babel-loader": "^ 8.0.4"."css-loader": "^ 1.0.0"."extract-text-webpack-plugin": "^ 4.0.0 - beta."."file-loader": "^ 2.0.0." "."html-webpack-plugin": "^ 3.2.0"."postcss": "^ 7.0.5"."postcss-loader": "^ 3.0.0"."url-loader": "^ 1.1.1"."vue-loader": "^ 15.4.2." "."vue-style-loader": "^ 4.1.2." "."vue-template-compiler": "^ 2.5.17"."webpack": "^ 4.20.2"."webpack-cli": "^ 3.1.2." "."webpack-dev-server": "^ 3.1.9." "}}Copy the code
The command
Start the development environment
yarn start
Copy the code
Build the production environment
yarn run build
Copy the code
Final effect screenshot:
See Github for the full code
2. Server rendering, not includedAjax
Initialize data
Server rendering SSR, similar to isomorphism, is ultimately about making a piece of code that can run on both the server and the client. If something goes wrong with SSR, you can roll back to browser-only rendering to make sure the user sees the page normally.
There must be two webpack entry files, one for rendering weboack.client.config.js on the browser side and one for rendering webpack.server.config.js on the server side. Extract their public parts as webpack.base.cofig.js and then merge them via webpack-merge. You also need a server to provide HTTP services, and I’m using KOA here.
Let’s take a look at the new directory structure:
-node_modules-config // Added -webpack.base.config. js -webpack.client.config. js -webpack.server.config. js-src - Components - bar.vue-foo.vue-app.vue-app.js - entry-client.js // added - entry-server.js // added - index.html - Index.ssr. HTML // Added -package. json -yarn.lock-postcss.config. js -.babelrc -.gitignoreCopy the code
In a client-only app, each user uses the new application instance in their respective browser. This is also expected for server-side rendering: each request should be a new, separate application instance so that there is no cross-request state pollution.
So, we’ll change app.js to wrap it as a factory function that generates a new root component every time it’s called.
app.js
import Vue from 'vue';
import App from './App.vue';
export function createApp() {
const app = new Vue({
render: h => h(App)
});
return { app };
}
Copy the code
On the browser side, we simply create a new root component and mount it.
entry-client.js
import { createApp } from './app.js';
const { app } = createApp();
app.$mount('#app');
Copy the code
On the server side, we return a function that takes a context argument and returns a new root component each time. This context is not going to be used here, it’s going to be used in the next steps.
entry-server.js
import { createApp } from './app.js';
export default context => {
const { app } = createApp();
return app;
}
Copy the code
Then take a look at index.ssr. HTML
index.ssr.html
<! DOCTYPE html> <html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge"</title> </head> <body> <! --vue-ssr-outlet--> <scripttype="text/javascript" src="<%= htmlWebpackPlugin.options.files.js %>"></script>
</body>
</html>
Copy the code
serves as a placeholder for the subsequent insertion of the component’S HTML string parsed by the server via vue-server-renderer.
is to put the file that webpack packaged with webpack.client.config.js here (this is for a simple demonstration, there will be other ways to do this later).
Since all the server spit out is an HTML string, the subsequent Vue responses, event responses, etc., need to be taken over by the browser, so the browser-side rendering package needs to be introduced here.
Officially, it is called client-side hydration.
Client-side activation refers to the process by which the Vue takes over static HTML sent by the server at the browser side and turns it into a dynamic DOM managed by the Vue.
In entry-client.js, we mount the application with the following line:
// This assumes the app. vue template root element's 'id="app"`
app.$mount('#app')
Copy the code
Since the server has rendered the HTML, we obviously don’t have to throw it away and recreate all the DOM elements. Instead, we need to “activate” the static HTML and then make it dynamic (able to respond to subsequent data changes).
If you examine the server rendered output, you’ll notice that a special attribute has been added to the root element of the application:
<div id="app" data-server-rendered="true">
Copy the code
Vue relies on this attribute to activate HTML from the server on the browser side, as we’ll see in a moment.
Let’s take a look at the webPack configuration:
webpack.base.config.js
const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
module.exports = {
mode: 'development',
resolve: {
extensions: ['.js'.'.vue']
},
output: {
path: path.resolve(__dirname, '.. /dist'),
filename: '[name].bundle.js'
},
module: {
rules: [
{
test: /\.vue$/,
use: 'vue-loader'
},
{
test: /\.js$/,
use: 'babel-loader'
},
{
test: /\.css$/,
use: ['vue-style-loader'.'css-loader'.'postcss-loader'] {},test: /\.(jpg|jpeg|png|gif|svg)$/,
use: {
loader: 'url-loader',
options: {
limit: 10000 // 10Kb
}
}
}
]
},
plugins: [
new VueLoaderPlugin()
]
};
Copy the code
webpack.client.config.js
const path = require('path');
const merge = require('webpack-merge');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const base = require('./webpack.base.config');
module.exports = merge(base, {
entry: {
client: path.resolve(__dirname, '.. /src/entry-client.js')
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, '.. /src/index.html'),
filename: 'index.html']}}));Copy the code
Notice that the entry file becomes entry-client.js, and insert its packaged client.bundle.js into index.html.
webpack.server.config.js
const path = require('path');
const merge = require('webpack-merge');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const base = require('./webpack.base.config');
module.exports = merge(base, {
target: 'node',
entry: {
server: path.resolve(__dirname, '.. /src/entry-server.js')
},
output: {
libraryTarget: 'commonjs2'
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, '.. /src/index.ssr.html'),
filename: 'index.ssr.html',
files: {
js: 'client.bundle.js'
},
excludeChunks: ['server']]}}));Copy the code
There are a few points to note here:
- The entry file is
entry-server.js
- Because it is packaged server-side dependent code, so
target
To set anode
And at the same time,output
thelibraryTarget
To set acommonjs2
The HtmlWebpackPlugin plugin is not a package of server.bundle.js in index.ssr. HTML, but a package of client. This is so that Vue can activate the HTML spit out by the server and take over the subsequent response.
So where does the packaged server.bundle.js work? Read on to find out
package.json
{
"name": "01"."version": "1.0.0"."main": "index.js"."license": "MIT"."scripts": {
"start": "yarn run dev"."dev": "webpack-dev-server"."build:client": "webpack --config config/webpack.client.config.js"."build:server": "webpack --config config/webpack.server.config.js"
},
"dependencies": {
"koa": "^ 2.5.3." "."koa-router": "^ 7.4.0"."koa-static": "^ 5.0.0"."vue": "^ 2.5.17"."vue-server-renderer": "^ 2.5.17"
},
"devDependencies": {
"@babel/core": "^ 7.1.2." "."@babel/preset-env": "^ 7.1.0"."autoprefixer": "^ 9.1.5." "."babel-loader": "^ 8.0.4"."css-loader": "^ 1.0.0"."extract-text-webpack-plugin": "^ 4.0.0 - beta."."file-loader": "^ 2.0.0." "."html-webpack-plugin": "^ 3.2.0"."postcss": "^ 7.0.5"."postcss-loader": "^ 3.0.0"."style-loader": "^ 0.23.0"."url-loader": "^ 1.1.1"."vue-loader": "^ 15.4.2." "."vue-style-loader": "^ 4.1.2." "."vue-template-compiler": "^ 2.5.17"."webpack": "^ 4.20.2"."webpack-cli": "^ 3.1.2." "."webpack-dev-server": "^ 3.1.9." "."webpack-merge": "^ 4.1.4." "}}Copy the code
Let’s look at the HTTP server code:
server/server.js
const Koa = require('koa');
const Router = require('koa-router');
const serve = require('koa-static');
const path = require('path');
const fs = require('fs');
const backendApp = new Koa();
const frontendApp = new Koa();
const backendRouter = new Router();
const frontendRouter = new Router();
const bundle = fs.readFileSync(path.resolve(__dirname, '.. /dist/server.js'), 'utf-8');
const renderer = require('vue-server-renderer').createBundleRenderer(bundle, {
template: fs.readFileSync(path.resolve(__dirname, '.. /dist/index.ssr.html'), 'utf-8')}); // Backendrouter.get (backendRouter)'/index'Renderer. renderToString((err, HTML) => {// There is a problem with the HTML returned with renderToString promise.if (err) {
console.error(err);
ctx.status = 500;
ctx.body = 'Server internal error';
} else{ console.log(html); ctx.status = 200; ctx.body = html; }}); }); backendApp.use(serve(path.resolve(__dirname,'.. /dist')));
backendApp
.use(backendRouter.routes())
.use(backendRouter.allowedMethods());
backendApp.listen(3000, () => {
console.log('Server side render address: http://localhost:3000'); }); // Frontendrouter.get ('/index', (ctx, next) => {
let html = fs.readFileSync(path.resolve(__dirname, '.. /dist/index.html'), 'utf-8');
ctx.type = 'html';
ctx.status = 200;
ctx.body = html;
});
frontendApp.use(serve(path.resolve(__dirname, '.. /dist')));
frontendApp
.use(frontendRouter.routes())
.use(frontendRouter.allowedMethods());
frontendApp.listen(3001, () => {
console.log('Browser rendering address: http://localhost:3001');
});
Copy the code
Port 3000 is used for server rendering and port 3001 is used for direct output of index.html. Then Vue will be used in the browser, mainly for comparison with server rendering.
The key code here is how to output the HTML string on the server side.
const bundle = fs.readFileSync(path.resolve(__dirname, '.. /dist/server.bundle.js'), 'utf-8');
const renderer = require('vue-server-renderer').createBundleRenderer(bundle, {
template: fs.readFileSync(path.resolve(__dirname, '.. /dist/index.ssr.html'), 'utf-8')});Copy the code
As you can see, server.bundle.js is used here because its entry is a function that takes context as an argument (not mandatory) and outputs a root component app.
Here we use the jue-server-renderer plugin, which has two methods for rendering: createRenderer and createBundleRenderer.
const { createRenderer } = require('vue-server-renderer') const renderer = createRenderer({/* options */})Copy the code
const { createBundleRenderer } = require('vue-server-renderer') const renderer = createBundleRenderer(serverBundle, {/* options */})Copy the code
CreateRenderer cannot receive the server.bundle.js file packaged for the server, so createBundleRenderer is used instead.
The serverBundle parameter can be one of the following:
- Absolute path, pointing to an already built one
bundle
File (.js
或.json
). Have to be/
The beginning is recognized as the file path. - by
webpack + vue-server-renderer/server-plugin
The generatedbundle
Object. JavaScript
Code strings (not recommended).
Here we’re going to introduce.js files, and we’ll show you how to use.json files and what the benefits are.
renderer.renderToString((err, html) => {
if (err) {
console.error(err);
ctx.status = 500;
ctx.body = 'Server internal error';
} else{ console.log(html); ctx.status = 200; ctx.body = html; }});Copy the code
The renderer function returned by createRenderer and createBundleRenderer contains two methods, renderToString and renderToStream, RenderToString returns a complete string on success, and renderToStream returns a Node stream.
RenderToString supports promises, but when I use the Prmoise form, the style will not render. I don’t know why yet, but please leave a comment if you know.
The configuration is almost complete. Let’s see how it works.
Yarn run build:client yarn run build:client yarn run build:server Providing HTTP servicesCopy the code
Final effect display:
Go to http://localhost:3000/index
We see the data-server-Rendered =”true” attribute mentioned earlier, and the client-bundle. js file will be loaded so that Vue can take over on the browser side.
http://localhost:3001/index is the first step and the effect of the implementation, pure browser rendering, here don’t put the screenshots.
See Github for the full code
3. Server-side rendering, including Ajax initialization data
If SSR needs to initialize some asynchronous data, the process becomes a little more complicated.
Let’s start with a few questions:
- Where does the server take asynchronous data?
- How do I determine which components need to fetch asynchronous data?
- Do I get asynchronous data back into the component like jose?
Let’s move on with the questions, and hopefully by the end of this article you’ll have answers to all of them.
Server-side rendering and browser-side rendering components go through different life cycles. On the server side, only beforeCreate and CREATED life cycles are used. There is no beforeMount and Mounted, there is no updating, there is no beforeUpdate and updated, because the SSR server simply spits out HTML strings and does not render DOM structures.
Let’s start by thinking about how, in a browser-only rendered Vue project, we get asynchronous data and render it into a component. A created or Mounted life cycle initiates an asynchronous request, and then executes this.data = XXX in the successful callback. Vue executes a Dom Diff to update the created data.
Can we do the same for server rendering? The answer is no.
- in
mounted
I’m not gonna be able to becauseSSR
All have nomounted
Life cycle, so definitely not here. - in
beforeCreate
Is it ok to make an asynchronous request? Because the request is asynchronous, the server may send the request before the interface returnshtml
The string is concatenated.
So, take a look at the official documentation to get the following ideas:
- Before rendering, all asynchronous data is pre-fetched and stored in
Vuex
thestore
In the. - Back end when rendering through
Vuex
The obtained data is injected into the corresponding component. - the
store
Is set towindow.__INITIAL_STATE__
Attribute. - In the browser environment, pass the
Vuex
willwindow.__INITIAL_STATE__
The data inside is injected into the corresponding component.
Normally, through these steps, the server spit out the HTML string corresponding to the component data is up to date, so step 4 does not cause DOM updates. However, if something goes wrong and the spit out HTML string does not have corresponding data, Vue can also inject data in the browser via ““Vuex. Update the DOM.
Updated directory structure:
- node_modules - config - webpack.base.config.js - webpack.client.config.js - webpack.server.config.js - src - Component-bar.vue-foo.vue-store // Add store.js-app.vue-app.js-entry-client.js-entry-server.js - index.html - index.ssr.html - package.json - yarn.lock - postcss.config.js - .babelrc - .gitignoreCopy the code
Take a look at store.js:
store/store.js
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const fetchBar = function() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Bar component returns Ajax data');
}, 1000);
});
};
function createStore() {
const store = new Vuex.Store({
state: {
bar: ' '
},
mutations: {
'SET_BAR'(state, data) {
state.bar = data;
}
},
actions: {
fetchBar({ commit }) {
return fetchBar().then((data) => {
commit('SET_BAR', data); }).catch((err) => { console.error(err); }}}}));if(typeof window ! = ='undefined' && window.__INITIAL_STATE__) {
console.log('window.__INITIAL_STATE__', window.__INITIAL_STATE__);
store.replaceState(window.__INITIAL_STATE__);
}
return store;
}
export default createStore;
typeof window
Copy the code
If you don’t know much about Vuex, go to the Vuex website to see some basic concepts.
Here the fetchBar can be thought of as an asynchronous request, which is simulated with setTimeout. Commit corresponding mutation changes the state in a successful callback.
Here’s a key piece of code:
if(typeof window ! = ='undefined' && window.__INITIAL_STATE__) {
console.log('window.__INITIAL_STATE__', window.__INITIAL_STATE__);
store.replaceState(window.__INITIAL_STATE__);
}
Copy the code
Since store.js is also packaged into server.bundle.js, the runtime environment does not have to be a browser. Note The server has obtained all the asynchronous data required for initialization. You need to replace the status in the Store to ensure consistency.
components/Bar.vue
<template>
<div class="bar">
<h1 @click="onHandleClick">Bar Component</h1> <h2> </h2> <span>{{ msg }}</span> </div> </template> <script> const fetchInitialData = ({ store }) => { store.dispatch('fetchBar');
};
export default {
asyncData: fetchInitialData,
methods: {
onHandleClick() {
alert('bar'); }},mounted() {// Since the server only has a beforeCreate and created life cycle, it will not go here. // So the Ajax initialization data is also written herelet store = this.$store;
fetchInitialData({ store });
},
computed: {
msg() {
return this.$store.state.bar;
}
}
}
</script>
<style>
.bar {
background: bisque;
}
</style>
Copy the code
Here we add a method asyncData to the default export object of the Bar component, which dispatches the corresponding action for asynchronous data retrieval.
In Mounted, I also write code to retrieve data. Why? Since the server does not have mounted life cycle, so I write here can solve the problem of using in the browser environment can also initiate the same asynchronous request to initialize data.
components/Foo.vue
<template>
<div class="foo">
<h1 @click="onHandleClick">Foo Component</h1>
</div>
</template>
<script>
export default {
methods: {
onHandleClick() {
alert('foo');
}
},
}
</script>
<style>
.foo {
background: yellowgreen;
}
</style>
Copy the code
Here I add a click event to both components to prove that after the server spits out the home page HTML, the subsequent steps are taken over by the browser-side Vue and can do the rest.
app.js
import Vue from 'vue';
import createStore from './store/store.js';
import App from './App.vue';
export function createApp() {
const store = createStore();
const app = new Vue({
store,
render: h => h(App)
});
return { app, store, App };
}
Copy the code
When creating the root component, pass in the Vuex store and return it, which will be used later.
Finally, take a look at entry-server.js. Here are the key steps:
entry-server.js
import { createApp } from './app.js';
export default context => {
return new Promise((resolve, reject) => {
const { app, store, App } = createApp();
let components = App.components;
let asyncDataPromiseFns = [];
Object.values(components).forEach(component => {
if(component.asyncData) { asyncDataPromiseFns.push(component.asyncData({ store })); }}); Promise.all(asyncDataPromiseFns).then((result) => {// When template is used, context.state will be the window.__initial_state__ state, Context. state = store.state; console.log(222); console.log(store.state); console.log(context.state); console.log(context); resolve(app); }, reject); }); }Copy the code
We use the exported App to retrieve all the components below it, and then iterate to find out which components have asyncData methods. If so, we call them and pass them to store. This method returns a Promise. To resolve (app).
Context. state = store.state when createBundleRenderer is used, if template is set, The value of context.state is automatically inserted into the template HTML as window.__initial_state__.
This is where you need to think a little bit more and figure out the whole server-side rendering logic.
How it works:
yarn run build:client
yarn run build:server
yarn start
Copy the code
Final effect screenshot:
Server render: Openhttp://localhost:3000/index
You can see that window.__initial_state__ is automatically inserted.
Let’s compare the effect of SSR on loading performance.
Server rendering performance screenshot:
Pure browser rendering performance screenshot:
Also in fast 3G network mode, pure browser rendering first screen loading takes 2.9s, because client.js loading takes 2.27s, because without client.js there is no Vue, there is no behind things.
The server side takes 0.8s to render the first screen. Although the client.js loading and throwing cost 2.27s, it is no longer needed for the first screen. It is in order for Vue to take over from the browser side.
From this we can really see that server-side rendering is very useful for improving the response speed of the first screen.
Of course, some students may ask, we also delayed the server rendering to get the initial Ajax data for 1s, during which time the user can not see the page. Yes, the interface time we can not avoid, even pure browser rendering, the home page of the interface still have to adjust, if the interface is slow, then pure browser rendering to see the full page will be slower.
See Github for the full code
4. Optimize with serverBundle and clientManifest
We created the server-side renderer as follows:
const bundle = fs.readFileSync(path.resolve(__dirname, '.. /dist/server.js'), 'utf-8');
const renderer = require('vue-server-renderer').createBundleRenderer(bundle, {
template: fs.readFileSync(path.resolve(__dirname, '.. /dist/index.ssr.html'), 'utf-8')});Copy the code
ServerBundle We use a packaged server.bundle.js file. To do so, the service must be stopped and restarted each time the application source code is edited. This can affect development efficiency during development. In addition, Node.js itself does not support source maps.
Vue-server-renderer provides an API called createBundleRenderer to handle this problem, using a custom plugin for WebPack, The Server bundle will generate special JSON files that can be passed to the Bundle Renderer. The created Bundle renderer is used in the same way as the normal renderer, but the bundle renderer provides the following advantages:
- The built-in
source map
Support (inwebpack
Use in configurationdevtool: 'source-map'
) - Hot reloading in the development environment or even during deployment (by reading the updated
bundle
And then recreate itrenderer
Instance) - The key
CSS(critical CSS)
Injection (in use*.vue
File time) : automatically inlines the components needed during renderingCSS
. Check out more detailsCSS
Chapter. - use
clientManifest
Perform resource injection: automatically infer the best preload (preload
) and prefetch (prefetch
) instructions, and the code segmentation required for initial renderingchunk
.
Preload and prefetch are useful if you don’t know about them.
So let’s modify the WebPack configuration:
webpack.client.config.js
const path = require('path');
const merge = require('webpack-merge');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin');
const base = require('./webpack.base.config');
module.exports = merge(base, {
entry: {
client: path.resolve(__dirname, '.. /src/entry-client.js'}, plugins: [new VueSSRClientPlugin(), // add new HtmlWebpackPlugin({template: path.resolve(__dirname,'.. /src/index.html'),
filename: 'index.html']}}));Copy the code
webpack.server.config.js
const path = require('path');
const merge = require('webpack-merge');
const nodeExternals = require('webpack-node-externals');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin');
const base = require('./webpack.base.config');
module.exports = merge(base, {
target: 'node'// The bundle renderer is providedsourceThe map support devtool:'#source-map',
entry: {
server: path.resolve(__dirname, '.. /src/entry-server.js'}, externals: [nodeExternals()], // Add output: {libraryTarget:'commonjs2'}, plugins: [new VueSSRServerPlugin(), // This should be the first one to write, or else CopyWebpackPlugin will not work. New HtmlWebpackPlugin({template: path.resolve(__dirname,'.. /src/index.ssr.html'),
filename: 'index.ssr.html',
files: {
js: 'client.bundle.js'
},
excludeChunks: ['server']]}}));Copy the code
Externals: [nodeExternals()] externals: [nodeExternals()]
The two configuration files generate vue-ssR-client-manifest. json and vue-SSR-server-bundle. json, respectively. As a parameter to createBundleRenderer.
Look at the server. Js
server.js
const serverBundle = require(path.resolve(__dirname, '.. /dist/vue-ssr-server-bundle.json'));
const clientManifest = require(path.resolve(__dirname, '.. /dist/vue-ssr-client-manifest.json'));
const template = fs.readFileSync(path.resolve(__dirname, '.. /dist/index.ssr.html'), 'utf-8');
const renderer = createBundleRenderer(serverBundle, {
runInNewContext: false,
template: template,
clientManifest: clientManifest
});
Copy the code
The effect and the third step is the same, not screenshots, the complete code to see github.
5. Configure a complete SSR based on Vue + VueRouter + Vuex
Unlike step 4, vue-Router is introduced, which is closer to the actual development project.
Add the router directory under SRC.
router/index.js
import Vue from 'vue';
import Router from 'vue-router';
import Bar from '.. /components/Bar.vue';
Vue.use(Router);
function createRouter() {
const routes = [
{
path: '/bar',
component: Bar
},
{
path: '/foo',
component: () => import('.. /components/Foo.vue'}]; const router = new Router({ mode:'history',
routes
});
return router;
}
export default createRouter;
Copy the code
Here we introduce the Foo component as an asynchronous component and make it load on demand.
Add router to app.js and export:
app.js
import Vue from 'vue';
import createStore from './store/store.js';
import createRouter from './router';
import App from './App.vue';
export function createApp() {
const store = createStore();
const router = createRouter();
const app = new Vue({
router,
store,
render: h => h(App)
});
return { app, store, router, App };
}
Copy the code
Modify app.vue import route component:
App.vue
<template>
<div id="app">
<router-link to="/bar">Goto Bar</router-link>
<router-link to="/foo">Goto Foo</router-link>
<router-view></router-view>
</div>
</template>
<script>
export default {
beforeCreate() {
console.log('App.vue beforeCreate');
},
created() {
console.log('App.vue created');
},
beforeMount() {
console.log('App.vue beforeMount');
},
mounted() {
console.log('App.vue mounted');
}
}
</script>
Copy the code
The most important changes are in entry-server.js,
entry-server.js
import { createApp } from './app.js';
export default context => {
return new Promise((resolve, reject) => {
const { app, store, router, App } = createApp();
router.push(context.url);
router.onReady(() => {
const matchedComponents = router.getMatchedComponents();
console.log(context.url)
console.log(matchedComponents)
if(! matchedComponents.length) {return reject({ code: 404 });
}
Promise.all(matchedComponents.map(component => {
if (component.asyncData) {
returncomponent.asyncData({ store }); }})).then(() => {// When template is used, context.state will be the window.__initial_state__ state, Context. state = store.state; // Return root resolve(app); }); }, reject); }); }Copy the code
The context mentioned above is useful here, passing in the URL that the user accesses for use by vue-Router. Since there are asynchronous components, the router.onReady success callback looks for the component that the URL route matches and gets asynchronous data is the same as before.
So, we have a basically complete configuration based on Vue + VueRouter + VuexSSR, complete the code and look at Github.
Final effect demonstration:
Go to http://localhost:3000/bar:
See Github for the full code
subsequent
Above, we completed the isomorphism from pure browser rendering to full server rendering in five steps. The code can be run on both the browser side and the server side. So, let’s go back and see if there’s room for improvement, or expansion thinking.
Optimization:
- We are currently using
renderToString
Method, full generationhtml
After that, it will be returned to the client if usedrenderToStream
Application,bigpipe
Technology can continually return a stream to the browser so that the file loading browser can display something as early as possible.
const stream = renderer.renderToStream(context)
Copy the code
The value returned is Node.js stream:
let html = ' '
stream.on('data', data => {
html += data.toString()
})
stream.on('end', () => {console.log(HTML) // Render complete}) stream.on()'error', err => {
// handle error...
})
Copy the code
In streaming rendering mode, the renderer sends data as quickly as possible as it traverses the virtual DOM tree. This means we can get the “first chunk” as quickly as possible and start sending it to clients faster.
However, when the first chunk of data is issued, the child components may not even be instantiated and their lifecycle hooks may not be called. This means that if a child component needs to append data to the render context in its lifecycle hook function, that data will not be available when the stream starts. This is because a lot of context information (such as head information or inline critical CSS) needs to appear before application markup, We basically have to wait for the stream to complete before we can start using the context data.
Therefore, if you rely on context data populated by component lifecycle hook functions, streaming mode is not recommended.
webpack
To optimize the
Webpack optimization is a big topic, so I don’t want to discuss it here. If you are interested, you can find some information by yourself. I may write an article on Webpack optimization later.
thinking
- Is it mandatory to use
vuex
?
The answer is no. Vuex just to help you to achieve a set of data storage, updating, access mechanism, if you don’t have to Vuex, then you have to think a plan can to save asynchronous access to data, and put it into components at the right time, there are some article put forward some solutions, I will be on the reference articles, everyone can read.
- Whether to use
SSR
Must be good?
This is also not certain, any technology has usage scenarios. SSR can help you improve the home page loading speed and optimize search engine SEO, but at the same time, because it needs to render the entire Vue template in node, it will take up server load, and only execute beforeCreate and created two life cycles. Some external extension libraries need to do some processing before they can run in SSR and so on.
conclusion
This article through five steps, from the pure browser side rendering, to configure a complete SSR environment based on Vue + VUE-Router + Vuex, introduced a lot of new concepts, perhaps you read a time do not understand, then combined with the source code, to their own hand several times, and then look at the article several times, I believe you can master SSR.
Finally, all the source code of this article is on my Github. If it helps you, please give it a like
Welcome to follow my public number
Refer to the link
- ssr.vuejs.org/zh/
- zhuanlan.zhihu.com/p/35871344
- www.cnblogs.com/qingmingsan…
- Juejin. Cn/post / 684490…
- Github.com/youngwind/b…
My blog is synchronized to tencent cloud + community, invite everyone to come together: cloud.tencent.com/developer/s…