More recently, Vue.js has gained full support for server rendering. There is quite a bit of information on the Internet on how to prepare it correctly, so I decided to describe in detail the process of creating the necessary environment for developing an application with SSR on Vue.js.
Everything that will be discussed is implemented in the repository on github . I will often refer to its sources and, in fact, try to explain what is happening and why it is necessary :)
The article will describe approaches that are quite common for SSR ( if you just need something ready for use, then you can look in the direction of Nuxt.js ), so it’s quite likely that what’s said below can be partially or fully applied to other frameworks / libraries like Angular and React.
I did not set a goal to make a free retelling of official documentation , therefore for a full understanding of the process it is better to get acquainted at least superficially.
The main idea of any application with SSR is that it should generate the same HTML markup when running on the server and on the client.
Data that is substituted in HTML must be pulled out by an API located on the same or on a different server / domain. Setting up and developing an API server is beyond the scope of this article, but you can take axios or any other isomorphic http client as a client for it.
You also need to remember that there is no DOM on the server, so all manipulations with document, window and other navigators should either not be used at all, or should be run only on the client, that is, in hooks beforeMount, mounted, etc.
Below there will be a lot of letters where I try to clarify what happens in the code. Therefore, if the letters seem difficult to read to you, I recommend looking directly at the code :) Links to the relevant parts of the repository will be given in each section.
The assembly is divided into 3 main configurations of webpack - general, assembly for the server and assembly for the client. After the build, we should receive 2 independent bundles with a set of files for the client and only one js file for the server.
For each bundle, obviously, you will need to create a separate entry, but more on that later.
The general assembly (base.js) includes loaders for all statics, templates, JavaScript sources, and vue components. Styles can also be included here theoretically, but for obvious reasons they are not needed on the server, so they will be registered only for the client.
The client build (client.js) adds to the total what we need in the browser. In the rules are written loaders for css, stylus, sass, postcss, etc.
You can also add output to split the bundle into several files, extract css, uglify, etc. In general, everything is as usual :)
Here we also add the generation of a common HTML template using the html-webpack-plugin. On it I will separately stop a little lower.
The build for SSR (server.js) should create a single js-file for running on the server. We do not care about the file size, since no one will download it via http, so everything that is usually written in the configs for optimization does not make sense here.
You must also specify target: node
, null-loader for styles and externals. All packages from package.json are specified in externals so that the webpack does not include installed packages in the assembly, since they will be connected from the node_modules on the server.
{ target: 'node', externals: Object.keys(require('../../package.json').dependencies) }
A generic template is simply generic HTML markup into which rendered Vue application code will be inserted. It is important to understand here that a server without specially trained libraries knows nothing about the DOM. Therefore, in the template you need to enter a certain string, which will be a simple replacement for the substring replaced by the markup of the application. In the example, this is just <!--APP-->
(or //APP
in pug), but it can be any other.
With the scripts, styles and tags in the head a little easier - we will insert them using the same replacement in front of </body>
/ </head>
.
SSR requires a server (express in the example) on Node.js, which will also build the project on the fly during development. There is a lot of code, so it will be easier to see examples of server launch points and server configurations for development .
A few subtleties:
data-vue-meta-server-rendered
with no value in the <html>
. The attribute name is customizable , so in your project it may be different (for example, I decided to replace it with data-meta-ssr
, since this is shorter). // ... const { title, htmlAttrs, bodyAttrs, link, style, script, noscript, meta } = context.meta.inject() res.write(` <!doctype html> <html data-vue-meta-server-rendered ${htmlAttrs.text()}> <head> ${meta.text()} ${title.text()} ${link.text()} ${style.text()} ${script.text()} ${noscript.text()} </head> <body ${bodyAttrs.text()}> ... `) // ...
JSON.stringify
or, even better, using serialize-javascript . const serialize = require('serialize-javascript') // ... res.write(`<script> window.__INITIAL_VUEX_STATE__=${serialize(context.initialVuexState)} </script>`); res.write(`<script> window.__INITIAL_COMP_STATE__=${serialize(context.initialComponentStates)} </script>`);
In the case of starting the server in development mode, the server itself will work about the same. Only 2 things differ - in another way, errors that occurred during rendering are handled, and the renderer and the layout of the general template are replaced with new ones when the application code changes.
In addition to the server itself, you need to run webpack(clientConfig).watch
to generate the assembly on the fly as the source changes. Before this, the webpack is initialized with all the plug-ins of the HotModuleReplacementPlugin type necessary for development.
You also need to inform the customer about new builds of the bundle. This will require webpack-dev-middleware and webpack-hot-middleware. They are responsible for delivering the changed code to the client when new assemblies appear (that is, each time the source code of the application changes).
Separately, webpack(serverConfig).watch
and the server bundle is replaced with a new one when it changes. In my case, we report that it has changed using a simple callback (line 50 in build/setup-dev-server.js
, line 73 in index.js
).
As I mentioned above, you need to create 2 separate entry points (entry in the webpack) of the application for the SSR and for the client. Actually, here is the same as in the webpack configs - 3 files with common, server and client code.
The common code (app.js) includes a common initialization of the application, that is, it connects Vue-plugins, creates a vuex store, a router and a new root component. Global components, filters and directives are also registered here.
Here, the root component needs to mix the vue file with the template and logic of the application itself, so that the main component of the application and the root component become one.
It is important that for vue-server-renderer there is a runInNewContext option, which can be turned off, while obtaining a good performance boost. But to use it, you need to initialize the application each time, so in app.js I return the function that performs the initialization, not the finished object of the Vue component. The code that is executed directly in this file will be executed only once when the server is started, which must be remembered. Here you can register common moments that are not dependent on the data obtained in runtime - register components, filters, directives, retrieve environment variables, etc. etc.
Client entry point (client.js) . Here, an application is created using the function from app.js, then it loads and everything necessary for correct work in the browser is executed.
It also replaces the data object for the component that should be shown on this page and the status of the vuex store.
if (window.__INITIAL_VUEX_STATE__) { // state app.$store.replaceState(window.__INITIAL_VUEX_STATE__); delete window.__INITIAL_VUEX_STATE__; } if (window.__INITIAL_COMP_STATE__) { app.$router.onReady(() => { // , // ( , )... const comps = app.$router.getMatchedComponents() // ... , .filter(comp => typeof comp.prefetch === 'function'); for (let i in comps) if (window.__INITIAL_COMP_STATE__[i]) // , data // ( $data , ) comps[i].prefetchedData = window.__INITIAL_COMP_STATE__[i]; delete window.__INITIAL_COMP_STATE__; }); }
We end the code by taking the root component and calling $mount
from it in the root element of the application. This element will be automatically given the data-server-rendered
, so you can do this: app.$mount(document.body.querySelector('[data-server-rendered]'))
.
Entry point for SSR (server.js) . Here, a function is simply created that will accept the context of the request (that is, the request object from express) and initialize the application. The function should return a promise that will be executed at the moment when all the necessary data is loaded from the API, and the application is ready to be sent to the client.
The procedure for this function may be as follows ( code ):
app.$router.onReady(...)
), which will be executed when a match is found between the components and the URL:Promise.all
.app.$router.push(context.url)
).Further, all received data will be processed by the http server, the components will give up their markup, the data with the markup will be written to the template, the resulting HTML will be sent to the client.
Code for registering the router and components for it.
To develop an application with SSR, you need to assume that only the root component or components that are bound to routs have the ability to load data asynchronously before rendering. For these components, in a special way, you need to handle changes to the route and record the data that the server returned after rendering. For these purposes, a good solution would be to create mixin, which automatically connects to each component when the router is initialized. Sample code like mixin.
In prefetch-mixin you need to add something like the following:
this.$data
fields with the values from this.constructor.extendOptions.prefetchData
, but only before the application is fully initialized, what can we find out from the this.$root._isMounted
. function update(vm, next, route) { if (!route) route = vm.$route; const promise = vm.$options.prefetch({ store: vm.$store, props: route.params, route }); if (!promise) return next ? next() : undefined; promise .then(data => { Object.assign(vm.$data, data); if (next) next(); }) .catch(err => next && next(err)); } const mixin = { // created() { if (this.$root._isMounted || !this.constructor.extendOptions.prefetchedData) return; Object.assign(this.$data, this.constructor.extendOptions.prefetchedData); }, // prefetch ( , created) beforeMount() { if (this.$root._isMounted && this.$options.prefetch) update(this); } // prefetch, // , beforeMount beforeRouteUpdate(to, from, next) { if (this.$options.prefetch && to.path !== from.path) update(this, next, to); else next(); }, };
SSR imposes almost no restrictions on application development. It is enough just to remember that you cannot use the browser API where the code is executed on the server, in other cases it is necessary to make the code in the client hooks beforeMount / mounted.
Also, an application created for working with SSR will work correctly without SSR, so this approach can be used for regular SPAs so that when SEO requirements appear suddenly, do not rack your brains and write crutches to optimize your websites.
There may be problems with directives, whose role is often reduced to manipulating the DOM, but they can be easily solved by giving an alternative implementation (empty?) Instead of the directive on the server ( docs ).
In general, this is all that needs to be considered before starting the development of the application itself. Then you simply create components, connect the page components to the corresponding routes and, if everything is done correctly, you will receive a rendered page from the server and a correctly working application on the client.
Source: https://habr.com/ru/post/334952/
All Articles