In order to pursue better user experience, many companies will use SSR to render their business pages and return the rendered results to the browser. The browser can directly parse the HTML code display without instantiating Vue, which can reduce the first screen time and improve user experience.
0.Server Side Render
0.1 use SSR
Vue provides a NPM package called vue-server-renderer, which is packaged separately in the Server directory of the Vue source code as a dependency package for server rendering. The package’s name makes it clear what it does: it serves as a server renderer that renders everything that needs to be displayed in a Vue instance.
Take a very simple server-side rendering example:
const Vue = require('vue')
const server = require('express') ()const renderer = require('vue-server-renderer').createRenderer();
server.get(The '*', (req, res) => {
// Instantiate a Vue
const app = new Vue({
template: `<div>Server Render</div>`,})// Render the vue instance to generate HTML and return it as HTML code
renderer.renderToString(app, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
}
res.end(` <! DOCTYPE html> <html lang="en"> <head><title>Hello</title></head> <body>${html}</body>
</html>
`)
})
})
server.listen(8080)
Copy the code
Render a string on a normal instance using the renderer and return it.
SSR is that easy? Definitely not.
Remember that in a Server environment, a service receives many requests. The above example results in many requests sharing a Vue instance. Now the program is more complex, need to borrow the store to save the state:
const Vue = require('vue')
const server = require('express') ()const renderer = require('vue-server-renderer').createRenderer();
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment(state) {
state.count++;
}
}
})
server.get(The '*', (req, res) => {
// Instantiate a Vue
const app = new Vue({
store,
template: `<div>Server Render{{$store.state.count}}</div>`,
created() {
this.$store.commit('increment'); }})// Render the vue instance to generate HTML and return it as HTML code
renderer.renderToString(app, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
}
res.end(` <! DOCTYPE html> <html lang="en"> <head><title>Hello</title></head> <body>${html}</body>
</html>
`)
})
})
server.listen(8080)
Copy the code
Be careful. In this example, a store instance is created when the service is started, and the store value is updated when the service is requested. Since there is only one store instance and count is incremented each time a request is made, the same request will get different results (the same request will get different results twice).
So how to solve the above problems?
The problem is that there is only one store instance globally, so the solution is to create a new store each time a Vue is instantiated.
// The factory function returns a new store instance
function createStore() {
return new Vuex.Store({
state: {
count: 1
},
mutations: { increment(state) { state.count++; }}}); }function createApp() {
let store = createStore();
let vue = new Vue({
store,
template: `<div>Server Render{{$store.state.count}}</div>`,
created() {
this.$store.commit('increment'); }});return {
vue,
store
}
}
Copy the code
As a simple example, the main point is that writing SSR is not the same as writing a browser application. When writing SSR code, we need to consider whether the code will affect the environment and whether the generated Store and Router are singletons. If so, pay attention to whether they affect each other. Because you run your program for a long time on the server, you can’t avoid accessing the service multiple times. Each time the service is likely to change the data. If multiple different applications access the same shared memory, it can have unpredictable consequences.
1. The whole process of SSR from server to browser
A few questions first:
- Why does the documentation say that when the server renders, only the front is called
beforeCreate
andcreated
What about the lifecycle hook functions? - If SSR rendered pages need to be requested for data, when should data be requested? In what way is the data requested?
- After the server initializes the App and requests data, the state of the App changes. So how can we ensure that the state of the app created on the server is the same as the state of the app created on the client?
1.1 Why does server render only call beforeCreate and Created?
Here’s the official document:
SSR documentation states that only these two lifecycle functions are called in server-side rendering. Why not call later life cycles? Is it because the Vue object sets aside an interface to block calls or is SSR specialized?
All is not! Why is that? We look from the Vue source lifecycle!
Here’s how to answer the question:
-
Why are beforeCreate and Created lifecycle hook functions called? CallHook functions are functions that invoke the instance lifecycle. The above code executes directly when calling new Vue(), so of course the beforeCreate and Created lifecycle hook functions are called.
-
Why not call the later lifecycle hook function?
Look at the last piece of code in the diagram
if (vm.$options.el) { vm.$mount(vm.$options.el) } Copy the code
$mount () {$mount (); $mount () {$mount (); BeforeMount and mounted hook functions are called when the vm.$mount function is called. In Node and other environments, there is no document object, and el is not configured in the option, so this condition is false, meaning that the next two lifecycle hook functions will not be executed. The following code illustrates this:
The life-cycle hook functions beforeMount and Mounted are called in the $mount function. The VUE instance of SSR does not set the EL attribute, that is, it does not execute this function, so it naturally does not execute the subsequent lifecycle hook function.
1.2 How to conduct data request in SSR?
How and when to get the data:
-
How do I get data?
The configuration of all components of a Vue is contained in an object, and component configurations are available when instantiated into Vue instances, so you can add an option to the component configuration. This allows you to add an asyncData option to the configuration to request data after the route is matched to the component, and then initialize the route after the request is completed.
import { createApp } from './app.js';
// Here is the component configuration
export default {
asyncData({store}) {
/// code....}}// Here is the server entry file content
export default context => {
return new Promise((resolve, reject) = > {
const { app, store, router, App } = createApp();
router.push(context.url);
router.onReady((a)= > {
const matchedComponents = router.getMatchedComponents();
if(! matchedComponents.length) {return reject({ code: 404 });
}
Promise.all(matchedComponents.map(component= > {
if (component.asyncData) {
return component.asyncData({ store });
}
})).then((a)= > {
// When template is used, context.state is automatically embedded in the final HTML as the window.__initial_state__ state
context.state = store.state;
// Returns the root component
resolve(app);
});
}, reject);
});
}
Copy the code
The above example starts by defining asyncData in the routing hierarchy (that is, the component to which the routing configuration points directly) component. Of course you can define other properties as well. After fetching the component configuration based on the route, the data fetching can be performed by fetching the asyncData property of the configuration and executing the code.
-
So when does the server request data?
The request has just come in and the VUE instance has been initialized. According to the url of the request to match the route, after the match to get the matched component configuration for request data. The relevant code is as follows:
1.3 How to ensure that the state of the app created on the server is consistent with that of the app created on the client?
Some components need to obtain data before rendering, and after obtaining data, the state of the application will change. If the state of the browser is not consistent with the state rendered by the server when the browser initialization, then how to achieve the unity of the state of the server and the browser?
- The server gets the data, saves it to the server’s Store state for use in rendering, and also saves it to the state of the context, and the contents of the context are eventually saved to
window
; - In the renderer it’s added to the HTML code
<script>window.__INITIAL_STATE__ = context.state</script>
Global variables are set during page parsing. - When the browser initializes the Store, pass
window
Object to get the state of the data on the server and inject it intostore.state
State, this can achieve state unity. (The Window object serves as an intermediary for passing data)
You can see from the HTML code rendered by SSR:
The above is the process of SSR as a way to save the state, but we also need to initialize the store to execute the following code, data synchronization:
2.Server Side Render and Client Side Render
2.1 Advantages of SSR over CSR
To explain the difference between the two ends of rendering, it is necessary to start from why SSR is used: SSR can first request rendering on the server side, because the server side has a small delay in requesting data, it can quickly get data and return HTML code. The benefit of server-side rendering is that the data can be rendered directly on the client side without spending some of the time it takes to request the data.
2.2 From what is returned
Returning content SSR will result in more first render results in HTML code than normal SPA, so the page will be rendered directly at initialization without spending time requesting data to be rendered again.
2.3 In terms of times of rendering
SSR does not mean only render on the server side, but it means that SSR will render once more on the server side than normal client side. To the browser side, SSR still needs to initialize Vue again, and go through the beforeCreate, Created, beforeMount, mounted life cycle, but when the client VNode is performing patch, if it encounters the node rendered by the server, it will skip. So you can take some of the work out of rendering on the browser side and improve the page experience.
3.Server Side Render internal implementation notes
Vue SSR and browser side of the execution process has a lot of similarity, but the implementation of the time also need to have some key points:
3.1 Isolation Mechanism
When writing a VUE application, it is unavoidable to change environment variables (i.e., global variables) in your code. On the browser side, a new application is launched every time a page is refreshed, and global variables are rarely changed without explanation. On the Server side, there is not only the vUE application running, but also the Server receiving the request, so be careful not to change the global variable error:
The node environment on the server side has a shared global object. The Vue SSR Renderer can be configured to create a new execution environment sandbox global (i.e., global) each time the code is executed, so that a new state is created each time a Vue instance is created.
const renderer = createBundleRenderer(' ', {
runInNewContext: true // Enable sandbox creation every time
});
Copy the code
If your application relies heavily on global variables for communication, you are advised to enable this feature.
3.2 Compilation optimization
For component rendering that is not compiled (not coded by scaffolding), SSR optimizes compilation. The idea is as follows:
- For plain text, nodes with no content, and input tags that are not bound to the data source, mark them so that they will be converted directly into strings when later processed into rendering functions.
- For nodes with dynamically bound data sources, this is the same as normal template compilation.
This is because the essence of server rendering is to directly generate HTML strings. Patch operation will not be carried out after the render function is executed, but the rendering layer is rewritten and directly converted into strings. Therefore, some static content can be directly processed into strings by skipping the steps of converting into rendering functions during compilation. However, if the vue-template-Compiler is used in webpack for components that have already been processed as rendering functions, compilation optimizations will be skipped.
3.3 render layer
The Rendering layer on the Web side does DOM manipulation, while the rendering layer on the server side does string generation. Let’s take a look at what the render layer in SSR does, first finding the render layer entry:
export function createRenderFunction (
modules: Array<(node: VNode) => ?string>,
directives: Object.isUnaryTag: Function.cache: any
) {
// Render function
return function render (component: Component, write: (text: string, next: Function) = >void.userContext:?Object.done: Function
) {
warned = Object.create(null)
// Execution context, including active VM instances
const context = new RenderContext({
activeInstance: component,
userContext,
write, done, renderNode,
isUnaryTag, modules, directives,
cache
});
// Mix in some tools such as tag class injection and style injection
installSSRHelpers(component)
// Determine if there is a render function, if not, compile
normalizeRender(component)
const resolve = (a)= > {
// Execute the render function, which is the beginning of the render layer
renderNode(component._render(), true, context)
}
// It is stated in the documentation that serverPrefetch can be configured in each component. This hook function is called before rendering
waitForServerPrefetch(component, resolve, done)
}
}
Copy the code
Next, see the renderNode function:
/** * @desc; /** * @desc; /** * @desc Instead, focus on the type of node * @param node * @param isRoot * @param context */
function renderNode (node, isRoot, context) {
if (node.isString) {
renderStringNode(node, context)
} else if (isDef(node.componentOptions)) {
renderComponent(node, isRoot, context)
} else if (isDef(node.tag)) {
// Render components or real DOM nodes
renderElement(node, isRoot, context)
} else if (isTrue(node.isComment)) {
if (isDef(node.asyncFactory)) {
// async component
renderAsyncComponent(node, isRoot, context)
} else {
/ / comment
context.write(` <! --${node.text}-- > `, context.next)
}
} else {
// Perform the escape operation
context.write(
node.raw ? node.text : escape(String(node.text)),
context.next
)
}
}
Copy the code
Now that the rendering process is clear, I’m not going to expand it. But let’s clarify a few common questions:
-
How are event listening and bidirectional binding handled in SSR?
SSR is under the condition of a snapshot of the page, will not according to the change of data update operation (because didn’t create a function to be executed and _render in rendering function observer, changing the data does not update the view, so that a snapshot), and the service side apply colours to a drawing gives a string, there is no dom this concept, So there’s no way to talk about event monitoring. The event listener is mounted when the browser patch again.