Online articles about Vite are divided into introductory tutorials or principles. In this article, we will take a look at how Vite makes SSR as the most popular build tool for 2021

This article is time-sensitive. The current Vite version is 2.7.13

Why SSR?

Before introducing Vite SSR, let’s make a brief introduction to SSR

Most of the projects I have contacted before are client-side rendering, also known as CSR (Client Side Render).

CSR adopts a back-end separation architecture, where when a client requests HTML, the server only returns “empty” HTML

Empty HTML is HTML that contains only a mount point and the Vue Runtime and does not contain any page content

Finally, the client dynamically creates the PAGE DOM by running the Vue Runtime to render the complete HTML

Also called SSR (Server side Render)

When the client requests HTML, the server returns the full HTML

The complete HTML contains the mount point, Vue Runtime and page content HTML

(Above quote from blog SSR and front-end compilation are the same in code generation)

By returning full HTML on the server side, the Vue Runtime eliminates the need for dynamic DOM generation, reducing client rendering (creating a large number of DOM dynamically can cause some lag) and allowing users to see the content earlier

These two things ultimately make First Paint very fast for projects that use SSR to render pages

CSR

SSR

In addition to client-side rendering and server-side rendering, there is also a pre-rendering technique called SSG (Static Site generate). The HTML artifacts that contain the content of the page are generated directly when you build the artifacts

SSG and SSR have the same optimization effect on the front screen, but are lighter and simpler to implement than SSR. It is suitable for blogs, official websites and other projects with little change in the first screen data

Nextjs.org/docs/basic-…

CSR vs SSR

CSR

  • advantages
    • Development is simple and you don’t need to worry about compatibility issues when the code runs on the server side
    • Simple deployment, involving only static resource servers
    • Ajax/FETCH is used to obtain data
  • disadvantages
    • The first screen speed is slow
    • SEO is poor

SSR

  • advantages

    • Fast first screen speed (the lower the browser version, the better)
    • Good SEO
    • Additional information about the request context can be obtained
  • disadvantages

    • Development complex, need to write SSR server code

    • Complex deployment requires SIMPLE sequence repeat (SSR) servers and static resource servers for Dr Degradation, which increases o&M costs

    • Writing code with platform compatibility in mind adds an additional mental burden

Usage scenarios

For toB projects such as background management system and data center, due to low requirements on the first screen and small number of users, CSR is generally adopted to simplify the development process and deliver the project quickly

For toC projects such as the homepage of the official website and the homepage of e-commerce, the speed of the first screen is directly related to the retention rate of users, and good SEO is also the premise to improve the conversion rate, so it is necessary to rely on SSR to explore further possibilities

Project Structure (Development environment)

To simplify the Vite official warehouse SSR project for example

The Vite SSR project has at least the following files

├ ─ ─ index. HTML// Template file containing SSR client entry├ ─ ─ package. Json ├ ─ ─ for server js// The server of the development environment├─ SRC │ ├─ app.vue// The Vue container that holds the page files│ ├ ─ ─ the main js// Public entry file│ ├ ─ ─ entry - client. Js// SSR client entry│ ├ ─ ─ entry - server. Js// SSR server entry│ └ ─ ─ pages// page file│ ├ ─ ├ ─ sci-imp. Vue ├ ─ sci-impCopy the code

index.html

Project template file

<! DOCTYPEhtml>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0" />
    <title>Vite App</title>
    <! --preload-links-->
  </head>
  <body>
    <div id="app"><! --app-html--></div>
    <script type="module" src="/src/entry-client.js"></script>
  </body>
</html>
Copy the code

The difference with CSR is that

  • Much more<! --app-html-->.<! --preload-links-->The annotation
  • The entry file becomes/SRC /entry-client.js

The
is a placeholder for the page content. After the server renders the HTML string with the page content, it replaces
placeholder app – HTML

The
is a placeholder for the preload node. It is used to generate a prerendered HTML string

After the SSR production environment is built, you can choose to generate manifest.json file, which records the dependence between the source file and the build product

{
  "src/App.vue": [
    "/assets/Inter-Italic.bab4e808.woff2"."/assets/Inter-Italic.7b187d57.woff"]."vite/preload-helper": [
    "/assets/Inter-Italic.bab4e808.woff2"."/assets/Inter-Italic.7b187d57.woff"]."src/router.js": [
    "/assets/Inter-Italic.bab4e808.woff2"."/assets/Inter-Italic.7b187d57.woff"],... }Copy the code

From this manifest file, generate an HTML string for pre-rendering and replace
placeholder

function renderPreloadLink(file) {
  if (file.endsWith('.js')) {
    return `<link rel="modulepreload" crossorigin href="${file}"> `
  } else if (file.endsWith('.css')) {
    return `<link rel="stylesheet" href="${file}"> `
  } else if (file.endsWith('.woff')) {
    return ` <link rel="preload" href="${file}" as="font" type="font/woff" crossorigin>`
  } else if (file.endsWith('.woff2')) {
    return ` <link rel="preload" href="${file}" as="font" type="font/woff2" crossorigin>`
  } else if (file.endsWith('.gif')) {
    return ` <link rel="preload" href="${file}" as="image" type="image/gif">`
  } else if (file.endsWith('.jpg') || file.endsWith('.jpeg')) {
    return ` <link rel="preload" href="${file}" as="image" type="image/jpeg">`
  } else if (file.endsWith('.png')) {
    return ` <link rel="preload" href="${file}" as="image" type="image/png">`
  } else {
    // TODO
    return ' '}}Copy the code

Why are preload placeholders not used in development environments?

Because the development environment does not have access to build information, such as building a hash, the manifest file cannot be generated

At the same time, the development environment generates almost no benefit from preload nodes

Why do YOU not need to consider preload for CSR and extra processing for SSR?

The full preload code is automatically generated when a CSR is built

SSR, on the other hand, allows you to load only the preload nodes required by the entry because it can get more information, such as the specific page the user visited. While sacrificing some convenience, you gain the ability to flexibly create preload nodes, reducing resource waste

Ssr.vuejs.org/zh/api/#sho…

This means that the full HTML returned to the client must contain at least one

  • Template index. HTML
  • Page content app-html
  • Preload node preload-link (not required in development environment)

The template is static, while the page content and preload information are dynamically generated by the server on each request

server.js

Development environment for debugging SSR server

const app = express()
const vite = await require('vite').createServer(config)
// use vite's connect instance as middleware
app.use(vite.middlewares)
app.use(The '*'.async (req, res) => {
  const url = req.url
  let template, render
  // always read fresh template in dev
  template = fs.readFileSync(resolve('index.html'), 'utf-8')
  template = await vite.transformIndexHtml(url, template)
  render = (await vite.ssrLoadModule('/src/entry-server.js')).render

  const [appHtml, preloadLinks] = await render(url, manifest)

  const html = template
  .replace(` <! --preload-links-->`, preloadLinks) // production only
  .replace(` <! --app-html-->`, appHtml)

  res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
})
Copy the code

This file will not be used in production environments

This file will not be used in production environments

This file will not be used in production environments

Three important things: the packaged product will not have any server.js related code

Why not put server.js in the artifact?

See the “Production environment not Working out of the box” section below

What server.js does is

  1. When a page request is received, thevite.ssrLoadModuleRead the SSR server entry file and return the function to render the content of the page.

Why use viet. ssrLoadModule instead of require?

Require cannot load ES module, vite. SsrLoadModule is compatible with commonJS module and ES module

  1. Run the render function to get the HTML for the page content and replace the index.html in<! --app-html-->A placeholder

Will other static resources (js/ CSS /img) also execute vite. SsrLoadModule?

No, only the entry HTML triggers the vite. SsrLoadModule. Other static resource requests are intercepted and returned by the built-in static resource server in Vite. Middlewares

main.js

Public entry file

import App from './App.vue'
import { createSSRApp } from 'vue'

// SSR requires a fresh app instance per request, therefore we export a function
// that creates a fresh app instance. If using Vuex, we'd also be creating a
// fresh store here.
export function bootstrap() {
  const app = createSSRApp(App)
  return { app }
}
Copy the code

In SSR, both the client entry file entry-client.js and the server entry file entry-server.js execute main.js

Unlike CSR, SSR main.js declares a factory function called bootstrap instead of executing it immediately

The name of the function is not specified. Only entry-client/entry-server can find the function correctly

A new Vue instance is created for each request, with factory functions ensuring that data and state are independent of each other for each request

In addition, SSR uses createSSRApp to create Vue instances. Different from createApp in CSR, createSSRApp implements different functions based on the current environment

  • SSR client: “activate” static HTML (the next section explains what that means)
  • SSR server: andcreateAppThe function is similar except that one creates Vue instances on the client side and the other creates Vue instances on the server side

entry-client.js

SSR client entry file

import { bootstrap } from './main.js'
const { app, router } = bootstrap()

app.mount('#app')
Copy the code

The HTML returned by the server to the client contains a script tag pointing to the SSR client entry/SRC /entry-client.js

After the client obtains the entry file, it executes createSSRApp in main.js to “activate” the static HTML

Because the server sends the full HTML containing the page content to the client, Vue does not recreate the HTML node. Instead, it uses createSSRApp to bind events to the resulting HTML node, the state of the Vue instance behind initialization, and so on. So that it looks the same as the page rendered by the CSR

Github.com/vuejs/core/…

The activated HTML is taken over by the client and behaves in the same way as the CSR, so there is no association with the SSR server

This article is only for the simplest scenario. For SSR frameworks such as next.js, Nuxt.js, remix.js, which advocate integration of SSR clients with SSR servers, there may be data communication with SSR servers after hydrate

entry-server.js

SSR server entry file

import { bootstrap } from './main.js'
import { renderToString } from 'vue/server-renderer'

export async function render(url, manifest) {
  const { app, router } = bootstrap()
  // passing SSR context object which will be available via useSSRContext()
  // @vitejs/plugin-vue injects code into a component's setup() that registers
  // itself on ctx.modules. After the render, ctx.modules would contain all the
  // components that have been instantiated during this render call.
  const ctx = {
    url: url
  }
  const html = await renderToString(app, ctx)
  // the SSR manifest generated by Vite contains module -> chunk/asset mapping
  // which we can then use to determine what files need to be preloaded for this
  // request.
  const preloadLinks = renderPreloadLinks(ctx.modules, manifest)
  return [html, preloadLinks]
}
Copy the code

Entry-server.js needs to export a function that generates page content

Vue provides renderToString that makes it easy to convert Vue instances into HTML strings for page content

In addition, in the example, entry-server.js is also responsible for dynamically generating preload nodes (the development environment has no real role)

There are no strict requirements for entry, exit, and function names of entry-server.js. Server.js can get the render function correctly from entry-server.js

What does the CTX object in the render function do?

The figure above is explained in detail and is mainly used for data communication between the SERVER and Vue components in the SSR server (the SSR client is unavailable)

CTX can store data that only SSR servers can get (cookies, headers) and pass it as parameters to renderToString

The Vue component can then retrieve this data by returning a CTX object via useSSRContext

// App.vue
import { useSSRContext } from 'vue'
const isServer = typeof window= = ='undefined'
const ctx = isServer ? useSSRContext() : {}
Copy the code

The flow chart

Project Structure (Production environment)

The product structure in the production environment is as follows

├ ─ ─ the client// SSR client file│ ├ ─ ─ assets// Static resource file│ │ ├ ─ ─ Home. 149 d59e2. CSS │ │ ├ ─ ─ Home. C041839f. Js │ │ ├ ─ ─ but b988c137. CSS │ │ ├ ─ ─ but cd252472. Js │ │ └ ─ ─ Vendor. 9 ebeb296. Js │ ├ ─ ─ index. The HTML// Template file, containing the built entry file│ └ ─ ─ SSR - manifest. Json// Build the list└ ─ ─ server// SSR server file└ ─ ─ entry - server. Js// SSR server entry
Copy the code

index.html

<! DOCTYPEhtml>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0" />
    <title>Vite App</title>
    <! --preload-links-->
    <script type="module" crossorigin src="/assets/index.cd252472.js"></script>
    <link rel="modulepreload" href="/assets/vendor.9ebeb296.js">
    <link rel="stylesheet" href="/assets/index.b988c137.css">
  </head>
  <body>
    <div id="app"><! --app-html--></div>
  </body>
</html>
Copy the code

The ENTRY file of SSR client entry-client.js is developed into a static resource file through Vite construction

/assets/vendor.js contains the Vue Runtime, which is used to activate HTML for the client

/assets/index.js contains Vue components for each page

and
is still reserved, and the SSR server dynamically replaces the two placeholders when it receives a page request

ssr-manifest.json

Generate preload instructions

When executing the render function, the server will read ssR-manifest. json in addition to dynamically generate the preload node

entry-server.js

Vite additionally compiles all Vue components to entry-server.js at build time

Looking at the production structure, you can see that the static resource files generated by the SSR client at build time and the additional code injected by Entry-server.js actually represent the same Vue component

However, considering the differences between SSR client and server, Vite does not directly reuse static resource files of the client for entry-server.js, but packages them once more. I think there is still some room for optimization

Server.js is also not included in the built artifacts to verify that server.js exists only in the development environment

In general, entry-server.js is a function that only returns page content, and entry-server.js alone cannot start an SSR server

So how to start the SSR server in the production environment?

Here we need a custom server that acts as “server.js” for the production environment, as described in the “Production Environment does not Work out of the box” section below

The flow chart

Current problems

As of now (Vite 2.7.13), Vite SSR is still in experimental stage

Vite officially has a separate discussion section in the Issue section

Why is it that Vite SSR is still not officially available today, more than two years after the launch of Vue3 and more than a year after the launch of Vite?

The discussion around the issue area is mainly divided into the following questions

The production environment cannot be used out of the box

Related issue: github.com/vitejs/vite…

As mentioned earlier, Vite only provides servers in the development environment, server.js does not exist in the production. So in order for your code to work in production, you also need a production server

So why not pack server.js?

Guess Vite’s motives based on the discussion in the issue section

Since there are so many node-based server frameworks out there (Express, KOA, Nest, Serverless), once Vite is bundled with one style of server framework, developers of other styles have to follow it

In addition, providing servers out of the box limits the ability to customize them. In order to increase the versatility of the framework, Vite simply lets the product not bind to any server-side framework, but be adapted by the developer alone

For example, if you want to deploy an SSR server in a production environment, you need to do the following:

  1. Build the source code and upload the artifacts to the CDN server

  2. Create a separate repository for the SSR server

  3. Create a custom server that can be express, KOA, any technology stack

  1. Using the Express framework as an example, directly copy the official Demo (Express style) server.js code

  1. Redirect the server. Js require file address to the CDN (you don’t need the CDN, just make sure you can read the product of step 1)

  2. Set process.env.node_env to “prod” (server.js distinguishes the environment from NODE_ENV) and run Node server.js to start the server

As you can see, there is a cost to deploying a production server, and the community recognized this pain point and came up with a solution, Vite-plugin-Node

The plugin provides several major server framework adaptors for Vite. Developers need only choose one of the server framework, eliminating the need for server.js in the development environment.

import { defineConfig } from 'vite';
import { VitePluginNode } from 'vite-plugin-node';

export default defineConfig({
  plugins: [
    ...VitePluginNode({
      // Nodejs native Request adapter
      // currently this plugin support 'express', 'nest', 'koa' and 'fastify' out of box,
      // you can also pass a function if you are using other frameworks, see Custom Adapter section
      adapter: 'express',]}}));Copy the code

Specific Vite official response, still to be verified

The externalization controversy

Related issue:

  • Cn. Vitejs. Dev/guide/SSR. H…
  • Github.com/vitejs/vite…
  • Github.com/vitejs/vite…
  • Github.com/vitejs/vite…

Understand externalization by the difference between the two products.

Externalize the products

// dist/server/entry-server.js
"use strict";
Object.defineProperty(exports."__esModule", { value: true });
exports[Symbol.toStringTag] = "Module";
var vue = require("vue");
var serverRenderer = require("vue/server-renderer");
var vueRouter = require("vue-router");
var path = require("path");
var vuex = require("vuex");
var App_vue_vue_type_style_index_0_lang = "";
var _export_sfc = (sfc, props) = > {
  const target = sfc.__vccOpts || sfc;
  for (const [key, val] of props) {
    target[key] = val;
  }
  return target;
};
// ...
Copy the code

Non-externalized products

// dist/server/entry-server.js
"use strict";
Object.defineProperty(exports."__esModule", { value: true });
exports[Symbol.toStringTag] = "Module";
function getAugmentedNamespace(n) {
  if (n.__esModule)
    return n;
  var a = Object.defineProperty({}, "__esModule", { value: true });
  Object.keys(n).forEach(function(k) {
    var d = Object.getOwnPropertyDescriptor(n, k);
    Object.defineProperty(a, k, d.get ? d : {
      enumerable: true.get: function() {
        returnn[k]; }}); });return a;
}
var runtimeDom_cjs = {};
var runtimeCore_cjs = {};
function makeMap(str, expectsLowerCase) {}
// ...
Copy the code

The difference is whether the code is packaged

The externalized product introduces dependency packages (vUE, VUe-Router, vuex) in require form. Non-externalization packages dependencies (similar to Webpack)

The advantage of externalization is that modules are referred to directly without any processing. Therefore, externalizing modules as much as possible will significantly optimize the construction speed and product volume

BUT, for some special environments such as worker, Serverless, deno, docker, there may be limited support for node’s native loading module (require/import). At this point, the project and dependencies need to be repackaged to generate a JS file that is independent of any hosting environment

Some feel that in production, all modules should be externalized to improve performance and avoid problems when packaging modules

Others feel that all modules should be packaged to ensure that any platform works perfectly

Therefore, there is still no perfect solution, and even the judgment conditions for externalization have not been stable

Incompatible with commonJS modules

Related issue:

  • Kill CommonJS
  • Github.com/vitejs/vite…

The following error occurs when the commonJS module is introduced in the project itself

// src/entry-server.js
import { bootstrap } from './main.js'
import { renderToString } from 'vue/server-renderer'
+ const path = require("path")

export async function render(url, manifest) {
  const { app, router } = bootstrap()
  // passing SSR context object which will be available via useSSRContext()
  // @vitejs/plugin-vue injects code into a component's setup() that registers
  // itself on ctx.modules. After the render, ctx.modules would contain all the
  // components that have been instantiated during this render call.
  const ctx = {}
  const html = await renderToString(app, ctx)
  // the SSR manifest generated by Vite contains module -> chunk/asset mapping
  // which we can then use to determine what files need to be preloaded for this
  // request.
  const preloadLinks = renderPreloadLinks(ctx.modules, manifest)
  return [html, preloadLinks]
}
Copy the code

The reason is that Vite’s loader consists of New AsyncFunction + Dynamic import

We know that when running a JS file with Node, the Node Runtime wraps the file as a function and injects some specific parameters

// https://nodejs.org/api/modules.html#the-module-wrapper
(function(exports.require.module, __filename, __dirname) {
// Module code actually lives in here
})
Copy the code

Vite SSR borrowed node’s implementation by wrapping a file with a new AsyncFunction, using global as the context, and passing in ssrModule, ssrImportMeta and other parameters

 const initModule = new AsyncFunction(
      `global`,
      ssrModuleExportsKey,
      ssrImportMetaKey,
      ssrImportKey,
      ssrDynamicImportKey,
      ssrExportAllKey,
      result.code + `\n//# sourceURL=${mod.url}`
 )
Copy the code

However, Vite does not inject require, so once the require function is executed, it will tell you that the variable cannot be found

Server.js is not part of the Vite take over code, can use require

Vite should run in an ESM module, which does not support commonJS syntax by default

Vite theoretically does not allow syntaxes such as require, module.exports

However, to match the large number of commonJS modules in the NPM community, Vite will still use pre-bundling to convert commonJS to ESM for NPM packages, so the above scenario is only for the project code

Of course, ESM reserves a “back door” for commonJS. CreateRequire allows you to dynamically create a commonJS module loader in an ESM module

import { createRequire } from 'module';
const require = createRequire(import.meta.url);

// sibling-module.js is a CommonJS module.
const siblingModule = require('./sibling-module');
Copy the code

conclusion

SSR refers to the technology to generate complete HTML content on the server, which can improve the time of the first screen and increase SEO. It is suitable for the system with large number of users and high requirements on the first screen

SSR involves both client and server, requiring the writing of compatible dual-end code, and additional SSR servers for deployment

Vite SSR is still in experimental nature, the structure of the product, how compatible with NPM ecology needs further discussion

The resources

Why SSR?

Vite SSR trampling records

SSR support of enterprise-class framework from Next. Js

SSR and front-end compilation are the same in code generation