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
- When a page request is received, the
vite.ssrLoadModule
Read 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
- 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: and
createApp
The 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:
-
Build the source code and upload the artifacts to the CDN server
-
Create a separate repository for the SSR server
-
Create a custom server that can be express, KOA, any technology stack
- Using the Express framework as an example, directly copy the official Demo (Express style) server.js code
-
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)
-
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