Preface, different SSR analysis

We’re halfway through 2020, and it’s only the first article of the year. Shame. Since it is a masterpiece, at the same time is such a platitude of the topic, this article of course will not follow the trend. I promise I will not post a single line of code on SSR configuration, and I will not analyze the basic questions about SSR. If you are here to learn how to configure SSR, I am sorry, Vue SSR guide is here.

This article continues the vue + Webpack expansion, except that SSR is added. With the help of the previously built project scaffolding, it was successfully upgraded to Vue SSR scaffolding. The purpose of this article is to record the problems encountered in the scaffolding construction process and the project and some thoughts, hoping to help you.

So let’s start with a picture, and maybe by the end of this picture, you’ll have a different understanding of SSR

What questions do you have about SSR?

  1. Why Node?
  2. What role does Node play in SSR? What does it do?
  3. What are the roles of client and server Entries in the official tutorial?
  4. After SSR, must all interfaces be forwarded by Node?
  5. How does Node render the original page and the page after the vue-router route?

Get to the point. Listed these questions, is the author before and after the development of the problem, but also novice when involving SSR development is easy to confuse do not understand several places. I would like to share my thoughts with you (if you understand the picture above, you will understand the questions).

Why Node?

Any backend language will work, as long as vue-server-renderer 2.5+ is used.

However, the emergence of MVVM framework has realized the separation of the front and back ends, and the back end has been liberated from the front-end business layer. If Vue SSR is done in a non-node environment, the students of the back end also have to participate in the construction work, and the front and back ends are closely combined. Therefore, it is recommended to use Node.js for SSR.

What role does Node play in SSR? What does it do?

In a nutshell, the Node layer basically requests for data, generates a “snapshot” of the page (what is a snapshot, will be mentioned below) and discards the page. It can be understood that its role as middleware has two main functions:

  1. Compile webPack (the server configuration file) to generate the server bundle
  2. According to the front-end route matching, pre-obtain data, rendering the corresponding HTML

Server – entry and client – entry

If you add the factory function create-entry that generates vUE instances, the entire project should have three entry files. Why there are three is not explained here. See the VUE SSR guide for a description.

It is not difficult to understand why there are two entries for server and client. In the simplest terms, the ultimate meaning of SSR is to let the server render the Vue page

However, Vue applications are built and compiled by Webpack and Vue-Loader. Node cannot run and render the exported page, so you need an entry file only for the server side and a webPack configuration file dedicated to packaging the server side bundle

Use the Vue-server-renderer /server-plugin in WebPack to package a JSON file that can be passed to createBundleRenderer (the API provided by Vue-server-renderer). In this JSON file, there are pages and static file resource paths corresponding to all the routes registered in the VUe-router, because each page registered in the routing table needs SSR to render.

Therefore, in summary, server-entry is used to package the Server bundle, and the Server bundle is used to render the server side, namely SSR

What about the client entry? In the same way, the webpack is used to package the client bundle on the client side. In combination with vue-server-renderer/client-plugin, when compiling and packaging front-end code, Json, officially called client build manifest, is passed as ops to createBundleRenderer mentioned above to mix resources, i.e. CSS, Js is inserted into HTML and returned to the browser to complete server-side rendering.

About Node interface forwarding and front-end page rendering

These two questions can be explained together.

First of all, if it is a page that needs to be rendered by the server, and the page has the operation of the request backend, and the page is the initial page, it needs to be forwarded by the Node, otherwise the request can be sent directly asynchronously.

How do you define the initial page? The first page rendered after the browser refreshes the page is the initial page. After that, all operations, requests, interactions, and redirects (if your redirects are in the form of router-links, not pure A-tags) are no longer Node related and are controlled by the client Vue. This explains the idea that SSR is just a “snapshot” of the page.

I do not know whether you have some inspiration after reading the analysis of the problem. Of course, if it doesn’t cover something you don’t understand, leave a question in the comments section and let us discuss it.

Have you encountered any of the following pitfalls regarding SSR?

Let’s move on to the next topic, which is the summary of the project. In fact, most of the pits have correct answers, such as the classic SSR question:

Cookies pass through me? Element – UI table component does not support SSR? Echarts server render? The window object? And so on…

For these classical problems, many Daniel also aligned for the explanation, this article is no longer scripted, the number of words in the article. Here IS a summary of my most outstanding problems, if you have the misfortune to encounter, then congratulations. If you have a better way to handle it, please don’t hesitate to comment.

The table component in element-UI does not support SSR

“Yi, this is not the above you said the classic question, to hit the face cough up.” Wait, wait, wait. Let’s see how we figure this out.

As with the solutions proposed in several articles, find the github issue, Portal, to solve this problem

Note: To use this solution, you need to change the Elemental-UI to load on demand; otherwise, the previous version is incompatible with the issue version

In this way? All forums and blogs say that. Is it unique? Do you think it’s over? Of course not:

The big hole is coming!! In the solution provided by the issue,table.js has the following code

if (array.includes(column)) {
  return;
}
Copy the code

What’s the problem? Of course, high version of the browser is no problem, but IE is not good, includes is what ghost?

Babel /preset-env {“useBuiltIns”: “usage”,”corejs”: 3}, using Corejs to convert higher version APIS, you can also configure Corejs: 3 using @babel/ plugin-transform-Runtime.

Is it so easy to solve that it’s called a pit? Let’s consider how WebPack configulates Babel-Loader as follows:

{
    test: /\.js$/.loader: 'babel-loader'.exclude: /node_modules/
}
Copy the code

Important: By ignoring all js files under node_modules, Babel will definitely not handle table.js as mentioned above.

What to do? Here’s the way:

  1. Also configure include, you ignore me, I will include in
  2. The babelrc file is configured only@babel/preset-envTo transform the syntax, do not use core-js to compile the higher version of the API. Introduce it in create-app.js, the entry file'core-js/stable'(the premise is the babel7 version, previous@babel/polyfillIt will import all gaskets and cause global contamination problems, so it is not recommended to usecore-js/stableandregenerator-runtime/runtimeInstead, explained in detail below). This will compile all the higher version apis when the entry file is compiled, but unlike the previous two methods, it will convert all the apis, rather than importing them as needed.

Vue: document is not defined

The reason is that the mini-css-extract-plugin used in the Webpack server configuration file (webpack.server.config.js) is used to process the CSS, extract the CSS and insert it into the HTML, while the server does not have a document.

How to handle this: Do not handle the CSS extraction operation in the server configuration file. This is also officially recommended by Vue SSR.

AsyncData cannot be usedthisThe keyword

I didn’t really think this was a problem until one day someone asked me on the blog if he wanted to retrieve the data value of the current instance object in the asyncData method. Is there any way…

First of all, the asyncData method is only called before the component is loaded or when the route is updated, whether for client or server prefetch, depending on where your client entry sets the asyncData function to be called.

Generally, the client entry uses the vue-Router navigation guard beforeResolve to match routes and invoke the asyncData operation in the component. At this time, there is no way to reference the instance object of the component in the asyncData method.

There’s no need to worry about how to get the data of the current instance in asyncData, the asyncData method is just used to get the data, according to what? For example, article details need to have an ID, why not get it from the data of the current instance? Get (‘/ API /detail/${this.id}’), wrong.

In both the server entry and the client entry, the store and router are passed in as context objects for the current page. The required parameters can be obtained directly from the Router or store.

How do I handle 404 500

This is mainly about how the server handles errors such as 404 500, because in the client rendering, data acquisition error will not jump to the 404 or 500 page alone, generally only pop-up prompt or display default data and other friendly operations.

When the browser enters a route that does not exist in the router or the current route ID does not exist, it generally responds 404 that the page does not exist. When the server fails, it responds 500. How to respond?

Note that when dealing with error pages, it is important to shift from client-side thinking to server-side thinking. Renderer. RenderToString: renderer.renderToString: renderer.renderToString: renderer.renderToString: renderer.renderToString: renderer.renderToString: renderer.renderToString: renderer.renderToString

If the browser visits www.xxx.com/join-us, the server entry will match the /join-us route to call asyncData in the component, and then return the vUE instance for the route. If not, Reject directly (the server entry returns a Promise instance for renderer. RenderToString to do subsequent processing)

This means that you cannot place error pages in the views directory as.vue, i.e. 404.vue or 500.vue, because users will not enter www.xxx.com/404 to access them themselves. How to deal with it? If you’ve written a Node project, it’s easy to simply return the error page directly from the server. Two ways:

  1. Passes the ERROR page’s HTML string directly throughCtx.body = 'I am the content of 400.html'return
  2. To borrowkoa-viewsejsTemplate engine, passctx.render('400')To return to

Where to return? As mentioned above, in the server entry, when the current route does not match reject, renderer. RenderToString is returned. Pseudo code:

// server-entry.js
router.onReady((a)= > {
  if(No match to the route) {return reject({ code: 404})}// Call asyncData for all matching routing components
  Promise.all(iterating over the matched component, calling the internal asyncData(context)).then((a)= > {
    resolve(app);// Return a component instance for
  }).catch(reject)
}, reject);

// ssr.js
renderer.renderToString(context).then(app= > {
    ctx.body = app // Match to the component HTML
}).catch((err) = > {
    ctx.body = 'I am the content of 400.html'
})
Copy the code

How do I inject the title tag

The SSR official also gave a detailed explanation of Head injection, so why summarize? In practice, the title tag is officially injected, but the title option must be set for every VUE file, even if the default title attribute is set for the context object before renderToString. In the following detailed analysis, this problem is also a more troublesome problem encountered by the author, and it also reveals my lack of a certain knowledge point mixed into the vUE global situation.

The problem is this:

When you enter a detail page for the first time, set the title to the title of the current detail page. When you enter the home page, set the title to the global default title.

Such as: The details page is Alibaba-details page, in which the title “Alibaba” is obtained after triggering the Action in asyncData. At this time, the title display is normal, but when using router-link to click to enter the home page, At this time, the title is still displayed as’ Alibaba – details page ‘. To see why, let’s first look at how we mixed in the client (check out the Vue SSR Guide official tutorial).

// Create a new instance of app.js. // Create a new instance of app.js. // Create an instance of app.js. In this project creation-app.js, define the following mixin code as shown in the HackerNews Demo:
function getTitle(vm) {
  const { title } = vm.$options
  if (title) {
    return typeof title === 'function'
      ? title.call(vm)
      : title
  }
}
const serverTitleMixin = {
  created() {
    const title = getTitle(this)
    if (title) {
      this.$ssrContext.title = title
    }
  }
}
const clientTitleMixin = {
  mounted() {
    const title = getTitle(this)
    if (title) {
      document.title = title
    }
  }
}
const titleMixin = process.env.VUE_ENV === 'server'
? serverTitleMixin
: clientTitleMixin

Vue.mixin(titleMixin) // Start mixing.Copy the code

A quick explanation of the code:

ServerTitleMixin: This.$ssrContext is used to directly access the server-side rendering context in the component and set the title of the current page

ClientTitleMixin: Client rendering under global mixin mounted, throughdocumentSet the title

As mentioned above, the problem appears after clicking the router-link tag. At this time, the client renders. After entering the home page from the details page, the mounted will be triggered. So undefined, so document.title is still the title of the detail page, because there is only one document.

“No, no, I have set the default title for the CTX context in server.js before calling renderer. RenderToString:

const context = {
  title: 'Vue HN 2.0'.// default title
  url: req.url
}
return renderer.renderToString(context).then(html= >{... }).catch((a)= >{... })Copy the code

You see, what I wrote is exactly the same as the HackerNews Demo, is it a big mistake?

Yeah, I don’t know, I’ve also posted an issue on Github regarding this issue in the Vue-HackerNews Demo, looking forward to your reply.

Again, the context is set to the default title, but don’t forget that server rendering does this, but not client rendering! So setting the default title on the server is invalid for client rendering.

Since this doesn’t work, you might think that it’s ok to add an else to the global mix. If the vue page doesn’t have a title set, set it to the default title. Ok, let’s try

// Client global mixin
const clientTitleMixin = {
    mounted() {
      const title = getTitle(this)
      document.title = title? title: 'the default title'}}Copy the code

Document. title should be ‘default title’.

Isn’t it? Obviously not. If you set it this way, whether it’s the home page or the details page, the title will always be the “default title”. Why? Because our mixin is injected globally. The following copy of the VUE official website for the global mixin special reminder:

Use global mixin with caution, as it affects each individually created Vue instance (including third-party components). In most cases, this should only apply to custom options. It is recommended to publish it as a plug-in to avoid mixing in duplicate applications.

Got it? To be honest, when I was working on the project, I completely ignored this: mixing in affects all individually created Vue instances. My detail page has N Vue instances, including element-UI component instances, and there is no guarantee that they are called in order, some asynchronous components. Mounted = “mounted”/” mounted “/” default title “/” default title “/” default title”

Solution:

  1. Set the title option on the home page. But there are too many project pages, home page, list page, search page… They’re all going to have the same title, so it’s a little cumbersome to set it up
  2. In client-entry, set document.title = ‘default title’ during the data prefetch phase, i.e. Router-beforeresolve. Only set this separately in the component where you want to display different titles. But there is a problem with this: when different routes are switched on the client, there is a flash before assignment. [root@localhost] [root@localhost] [root@localhost] [root@localhost] [root@localhost] [root@localhost] [root@localhost] [root@localhost]

Both of the above methods have been tried and found to be unsatisfactory. They are a little tricky.

Why did you clean document.title on router.beforeresolve in the first place? This is because of the negative impact of global blending in creating app factory functions (generic entry).

What if you don’t do global blending here (app.js)?

Look at our client entry, client-entry.js, where we create the application by calling the factory function exported by app.js, and globally mix in the beforeRouteUpdate so that we can prefetch the data when the route is updated. Is it possible to assign a value to document.title in the client rendering?

The answer is yes. How do you choose which component options to mix globally?

First, you must be able to get the title selection within the component when the component switches

Second, the component instance must be available

Looking at the Router navigation guard, we chose to use the beforeRouteEnter, but the beforeRouteEnter guard must be in the next callback to get the current vUE instance. The detailed code is as follows:

beforeRouteEnter(to, from, next) {
  next(vm= > {
    // Get the title option within the component
    const { title } = vm.$options
    // Call or assign if there is a title, otherwise use the default title
    document.title = title
    ? (typeof title === 'function'? title.call(vm): title)
    : 'I'm the default title'})}Copy the code

Global blending does not trigger any vUE instances or third-party components. It is a navigation guard. Each route maps to a component. So will not appear before the global interfuse brings the negative impact.

Have you solved it? No, in the case of client rendering, the problem was resolved, but in the case of server rendering, the problem occurred again because we canceled global blending in app.js.

If title is not set in the current.vue, document.title is the default title set in the server context. This is ok, but if title is set in the current.vue, document.title will blink:

The first title is the default title, which is returned by the server. The second title is the title set in the details page, which is set by the client. So of course not, the front end to set the title, SEO has a problem, this or SSR?

What’s the solution? It is easy to combine the two methods. Let’s modify the previous global mixin code:

// title.js
function getTitle(vm) {
  const { title } = vm.$options
  if (title) {
    return typeof title === 'function'
      ? title.call(vm)
      : title
  }
}
const serverTitleMixin = {
  created() {
    const title = getTitle(this)
    if (title) {
      this.$ssrContext.title = title
    }
  }
}

export default serverTitleMixin
Copy the code

Then judge in app.js, if it is rendered for the server, then mix:

// app.js
import titleMixin from './utils/title'
if(process.env.VUE_ENV === 'server') 
Vue.mixin(titleMixin)
Copy the code

Solve the problem perfectly. The main idea is to mix the title of the server separately in the generic entry, and the title of the client is mixed into the beforeRouteEnter globally in the client-entry, and then the title is set.

When HTML2Canvas turns the picture, it encounters the problem of repeated loading of resources, including JS CSS font and so on

Cause: Html2Canvas will parse the entire HTML document when translating images. When it encounters JS CSS font reference, it will load the static resources of the whole page again, resulting in repeated requests and bandwidth waste.

Html2canvas is set to ignoreElements, ignore the related files, as follows:

ignoreElements: ( element ) = > {
    if(element.tagName === 'LINK' || element.tagName === 'STYLE' || element.tagName === 'IMG' )
    return true
}
Copy the code

Do you really know how to configure Babelrc

At the beginning of the project setup, there was always ambiguity about the configuration of Babel, which plugin should be used, which plugin should be used to convert the advanced API? What are the dependencies? The following is a unified summary (the following analysis is based on the babel7 version).

Syntax conversion (either of the following)

One of the syntax conversions is @babel/preset-env

Regardless of the architecture of your project, once you use ES2015+ syntax, such as let const () => {} etc., you need Babel to convert it into a browser-recognized syntax.

This is where the @babel/preset-env is needed

Babel will compile and transform the syntax mentioned above (and more). Note: The @babel/preset-env at this point is just a syntax transform for new built-in objects (Promise…) , instance methods (includes…) , static methods (object.assign, array. from…) You can’t convert it.

Syntax conversion second plugins

@babel/preset-env is a collection of preset plug-ins. In other words, you don’t have to configure the default if you know which plugin is required to transform the arrow function for which syntax, such as @babel/ transform-arrowhead functions. Use @babel/plugin-transform-block-scoping to transform let const.

API conversion

By apis, I mean the new Javascript apis in ES6, such as Set, Maps, Proxy, Reflect, Symbol, Promise, and so on. To convert the API, you need a spacer, commonly known as a polyfill. There are two configurations of gaskets:

One of the spacers @babel/preset-env

As mentioned above, if nothing else is configured for this default, only the syntax will be converted, but this default is not as simple as that. API conversion is also supported as follows:

"presets": [["@babel/preset-env", { 
        "useBuiltIns": "usage".// or entry
        "corejs": 3}]],Copy the code

@babel/preset-env provides multiple polyfill implementations with the useBuiltIns parameter,

When it is set to entry, the coverage of gaskets is complete, so there is no need to worry about missing gaskets. The disadvantage is that it will pollute the whole situation. However, if it is not published to NPM and the third-party library for people to use, this need not be considered.

When set to UseAge, polyfill can be introduced on demand and the package size is small, but if node_modules is ignored in the package, compatibility issues can arise if third-party packages are not translated, which is encountered in the first step documented in this article.

@babel/ Runtime + @babel/plugin-transform-runtime + @babel/runtime-corejs3

@babel/ Runtime uses the helper function for API compatibility, and @babel/ plugin-transform-Runtime sandbox to prevent global contamination and pull out the public helper function.

In other words, you can’t have one without the other. @babel/ Runtime provides a series of helper functions.

What is a helper function? In other words, if you want to handle an API and @babel/ Runtime has a function that handles that API, this function is a helper function

So what does @babel/ plugin-transform-Runtime do?

Convert the new API by setting Corejs: 3 if install @babel/ Runtime – Corejs3

Sandbox gasket way to prevent global pollution

This common helper function is extracted and reused by @babel/ plugin-transform-Runtime when you are working with a higher version of the API multiple times in different modules. Such as:

var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));
var _assign = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/object/assign"));
Copy the code

To summarize, if you want to convert a higher version API, use @babel/preset-env or @babel/ plugin-transform-Runtime, choose one.

Note:@babel/plugin-transform-runtime, depends on the@babel/runtime-corejs3and@babel/runtimeAnd,@babel/runtimeYou must install as a build environment dependency

Note: Babel7 +@babel/ Polyfill has been deprecated and replaced by core-js and Regenerater-Runtime modules. Introducing these two modules in the entry file can also do API conversion, but it is not recommended. There are still problems with global contamination and importing all the spacers.

Encountered while compiling a higher version of the API using @babel/ plugin-transform-RuntimeCannot assign to read only property 'exports' of object '#<Object>'An error

Module. exports is commonJS syntax and import is ES6 syntax. If this is used, webPack will report an error.

The code that runs on the Node side strictly uses commonJS syntax. Since it is compiled, look at the compiled vue-ssR-server-bundle. json and find the problem.

Specific reasons: If the @babel/ plugin-transform-Runtime configuration does not tell Babel to distinguish between commonJS and ES6 files, Babel will default to ES6 files. The helper function for one of the transformation apis is then imported using import, resulting in a mixed error.

Solution: Set the sourceType: “unambiguous” in Babel to allow Babel to guess whether it is an ES6 file or a commonJS file based on the IMPORT or export declaration of ES6.

Added: sourceType, which tells Babel in which mode the code should be compiled. There are three options: Script, Module, and unambiguous. The default is script.

Above are some problems I encountered in the project and some summary of Vue SSR. The article is a little long, I hope after reading this article, can let you have some harvest. Of course, if you have a better solution to any of the above questions, or if you have any other questions or problems about SSR, I hope you can leave a comment and let us know.