Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

Time-consuming Optimization method for first screen of Web Application in Vue Server rendering

2025-01-19 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

Shulou(Shulou.com)06/03 Report--

In this issue, the editor will bring you the time-consuming optimization method of the first screen of the Web application rendered on the Vue server. The article is rich in content and analyzes and describes it from a professional point of view. I hope you can get something after reading this article.

What is server-side rendering? What is the principle of server rendering?

Vue.js is the framework for building client applications. By default, you can output Vue components in a browser to generate DOM and manipulate DOM. However, you can also render the same component as server-side HTML strings, send them directly to the browser, and finally "activate" these static tags into fully interoperable applications on the client side.

The above paragraph is derived from the interpretation of rendering documents on the Vue server, which, in popular terms, can be understood as follows:

The purpose of server-side rendering is: performance advantage. The corresponding HTML string is generated on the server, and the client receives the corresponding HTML string and can render the DOM immediately. The most efficient first screen is time-consuming. In addition, because the server directly generates the corresponding HTML string, it is also very friendly to SEO.

The essence of server rendering is to generate a "snapshot" of the application. When Vue and its corresponding library are run on the server, Web Server Frame actually acts as a proxy server to access the interface server to pre-pull data, thus taking the pulled data as the initial state of the Vue component.

The principle of server rendering is: virtual DOM. After Web Server Frame acts as a proxy server to access the interface server to pre-pull data, this is the data needed by the server to initialize the component. After that, the beforeCreate and created life cycle of the component will be called at the server. After initializing the corresponding component, Vue enables virtual DOM to form the initialized HTML string. After that, it is hosted by the client. Realize the front and back end isomorphic application.

How to configure server-side rendering on Koa-based Web Server Frame?

Basic usage

You need to use the Vue server rendering corresponding library vue-server-renderer, and install it through npm:

Npm install vue vue-server-renderer-save

At its simplest, render a Vue instance first:

/ / step 1: create a Vue instance const Vue = require ('vue'); const app = new Vue ({template: `Hello World`}); / / step 2: create a rendererconst renderer = require (' vue-server-renderer'). CreateRenderer (); / / step 3: render the Vue instance as HTMLrenderer.renderToString (app, (err, html) = > {if (err) {throw err;} console.log (html); / / = > Hello World})

Integration with the server:

Module.exports = async function (ctx) {ctx.status = 200; let html =''; try {/ /. Html = await renderer.renderToString (app, ctx);} catch (err) {ctx.logger ('Vue SSR Render error', JSON.stringify (err)); html = await ctx.getErrorPage (err); / / rendering error page} ctx.body = html;}

Use the page template:

When you render a Vue application, renderer only generates HTML tags from the application. In this example, we must wrap the generated HTML tags with an additional HTML page wrapper container.

To simplify this, you can provide a page template directly when you create the renderer. Most of the time, we put the page template in a unique file:

Hello

We can then read and transfer the file to Vue renderer:

Const tpl = fs.readFileSync (path.resolve (_ dirname,'. / index.html'), 'utf-8'); const renderer = vssr.createRenderer ({template: tpl,})

Webpack configuration

However, in a real project, it is not as simple as the above example, and many aspects need to be considered: routing, data prefetching, componentization, global status, etc., so server rendering is not only done with a simple template, and then completed with vue-server-renderer, as shown in the following diagram:

As shown in the diagram, in a general Vue server rendering project, there are two project entry files, entry-client.js and entry-server.js, one runs only on the client side and the other runs only on the server side. After Webpack packaging, two Bundle are generated. The server Bundle is used to generate a "snapshot" of the application using the virtual DOM on the server side, and the Bundle of the client side is executed in the browser.

Therefore, we need two Webpack configurations, named webpack.client.config.js and webpack.server.config.js, which are used to generate client-side Bundle and server-side Bundle, respectively, and named vue-ssr-client-manifest.json and vue-ssr-server-bundle.json, respectively. Vue officially has relevant examples of how to configure vue-hackernews-2.0.

Construction of development environment

My project uses Koa as the Web Server Frame, and the project uses koa-webpack to build the development environment. If it is in a production environment, vue-ssr-client-manifest.json and vue-ssr-server-bundle.json will be generated, including the corresponding Bundle, providing client-side and server-side references, while in the development environment, it is generally placed in memory. Use the memory-fs module for reading.

Const fs = require ('fs') const path = require (' path'); const webpack = require ('webpack'); const koaWpDevMiddleware = require (' koa-webpack'); const MFS = require ('memory-fs'); const appSSR = require ('.. /.. / app.ssr.js'); let wpConfig;let clientConfig, serverConfig;let wpCompiler;let clientCompiler, serverCompiler;let clientManifest;let bundle / / generate webpack configuration if of server bundle ((fs.existsSync (path.resolve (cwd,'webpack.server.config.js') {serverConfig = require (path.resolve (cwd,'webpack.server.config.js')); serverCompiler = webpack (serverConfig) } / / generate webpack configuration if for client clientManifest ((fs.existsSync (path.resolve (cwd,'webpack.client.config.js') {clientConfig = require (path.resolve (cwd,'webpack.client.config.js')); clientCompiler = webpack (clientConfig);} if (serverCompiler & & clientCompiler) {let publicPath = clientCompiler.output & & clientCompiler.output.publicPath; const koaDevMiddleware = await koaWpDevMiddleware ({compiler: clientCompiler, devMiddleware: {publicPath, serverSideRender: true},}) App.use (koaDevMiddleware); / / server rendering generates clientManifest app.use (async (ctx, next) = > {const stats = ctx.state.webpackStats.toJson (); const assetsByChunkName = stats.assetsByChunkName; stats.errors.forEach (err = > console.error (err)); stats.warnings.forEach (err = > console.warn (err)); if (stats.errors.length) {console.error (stats.errors); return } / / the generated clientManifest is put into the appSSR module, and the application can directly read let fileSystem = koaDevMiddleware.devMiddleware.fileSystem; clientManifest = JSON.parse (fileSystem.readFileSync (path.resolve (cwd,'./dist/vue-ssr-client-manifest.json'), 'utf-8'); appSSR.clientManifest = clientManifest; await next ();}); / / the server rendered server bundle is stored in memory const mfs = new MFS (); serverCompiler.outputFileSystem = mfs ServerCompiler.watch ({}, (err, stats) = > {if (err) {throw err;} stats = stats.toJson (); if (stats.errors.length) {console.error (stats.errors); return;} / / the generated bundle is put into the appSSR module, and the application can read bundle = JSON.parse (mfs.readFileSync (path.resolve (cwd,'./dist/vue-ssr-server-bundle.json'), 'utf-8') directly) AppSSR.bundle = bundle;});}

Rendering middleware configuration

In the product environment, the packaged client and server Bundle will be stored as vue-ssr-client-manifest.json and vue-ssr-server-bundle.json, which can be read through the file stream module fs. However, in the development environment, I have created an appSSR module. When code changes occur, the Webpack hot update will be triggered, and the bundle corresponding to appSSR will also be updated. The appSSR module code is as follows:

Let clientManifest;let bundle;const appSSR = {get bundle () {return bundle;}, set bundle (val) {bundle = val;}, get clientManifest () {return clientManifest;}, set clientManifest (val) {clientManifest = val;}}; module.exports = appSSR

By introducing the appSSR module, you can get clientManifest and ssrBundle in the development environment. The rendering middleware of the project is as follows:

Const fs = require ('fs'); const path = require (' path'); const ejs = require ('ejs'); const vue = require (' vue'); const vssr = require ('vue-server-renderer'); const createBundleRenderer = vssr.createBundleRenderer;const dirname = process.cwd (); const env = process.env.RUN_ENVIRONMENT;let bundle;let clientManifest If (env = = 'development') {/ / Development environment, through the appSSR module, get clientManifest and ssrBundle let appSSR = require ('. /.. /.. / core/app.ssr.js'); bundle = appSSR.bundle; clientManifest = appSSR.clientManifest;} else {bundle = JSON.parse (fs.readFileSync (path.resolve (_ dirname,'. / dist/vue-ssr-server-bundle.json'), 'utf-8')) ClientManifest = JSON.parse (fs.readFileSync (path.resolve (_ dirname,'. / dist/vue-ssr-client-manifest.json'), 'utf-8'));} module.exports = async function (ctx) {ctx.status = 200; let html; let context = await ctx.getTplContext (); ctx.logger (' enter SSR,context is:', JSON.stringify (context)); const tpl = fs.readFileSync (path.resolve (_ dirname,'. / newTemplate.html'), 'utf-8') Const renderer = createBundleRenderer (bundle, {runInNewContext: false, template: tpl, / / (optional) page template clientManifest: clientManifest / / (optional) client build manifest}); ctx.logger ('createBundleRenderer renderer:', JSON.stringify (renderer)); try {html = await renderer.renderToString ({. Context, url: context.CTX.url,});} catch (err) {ctx.logger (' SSR renderToString failure:', JSON.stringify (err)) Console.error (err);} ctx.body = html;}

How to transform the existing project?

Transformation of basic catalogue

Using Webpack to handle server and client applications, most of the source code can be written in a common way, using all the features supported by Webpack.

A basic project might look like this:

Src ├── components │ ├── Foo.vue │ ├── Bar.vue │ └── Baz.vue ├── frame │ app.js # generic entry (universal entry) │ ├── entry-client.js # runs only on the browser │ ├── entry-server.js # runs only on the server │ └── index.vue # project entry component ├── pages ├── routers └── store

App.js is the "generic entry" of our application. In a pure client application, we will create a root Vue instance in this file and mount it directly to DOM. However, for server-side rendering (SSR), the responsibility shifts to pure client-side entry files. App.js simply uses export to export a createApp function:

Import Router from'~ ut/router';import {sync} from 'vuex-router-sync';import Vue from' vue';import {createStore} from'. /.. / store';import Frame from'. / index.vue';import myRouter from'. /.. / routers/myRouter';function createVueInstance (routes, ctx) {const router = Router ({base:'/ base', mode: 'history', routes: [routes],}); const store = createStore ({ctx}) / / inject routes into vuex sync (store, router); const app = new Vue ({router, render: function (h) {return h (Frame);}, store,}); return {app, router, store};} module.exports = function createApp (ctx) {return createVueInstance (myRouter, ctx) } Note: in my project, you need to dynamically determine whether you need to register DicomView. DicomView is initialized only on the client side. Since there is no window object in the Node.js environment, the code running environment can be judged by typeof window = 'undefined'.

Avoid creating singletons

As described in the Vue SSR documentation:

When writing pure client (client-only) code, we are used to valuing the code in a new context each time. However, the Node.js server is a long-running process. When our code enters the process, it will take a value and keep it in memory. This means that if you create a singleton object, it will be shared between each incoming request. As shown in the basic example, we create a new root Vue instance for each request. This is similar to an instance of each user using a new application in his or her own browser. If we use a shared instance among multiple requests, it is easy to cause cross request status contamination (cross-request state pollution). Therefore, instead of directly creating an application instance, we should expose a factory function that can be executed repeatedly to create a new application instance for each request. The same rules apply to router, store, and event bus instances. Instead of exporting directly from the module and importing it into the application, you need to create a new instance in createApp and inject it from the root Vue instance.

As described in the code above, the createApp method creates a function call that returns a value for the object of the Vue instance, and in the function createVueInstance, the Vue,Vue Router,Vuex instance is created for each request. And exposed to entry-client and entry-server modules.

On the client side entry-client.js, simply create the application and mount it to the DOM:

Import {createApp} from'. / app';// client specific boot logic. Const {app} = createApp (); / / it is assumed that the root element in the App.vue template has `root "app" `app.$mount ('# app')

The server entry-server.js uses default export to export the function and calls it repeatedly in each rendering. At this point, it won't do much except to create and return an application instance-but we'll perform server-side route matching and data prefetch logic here later:

Import {createApp} from'. / app';export default context = > {const {app} = createApp (); return app;}

Split the code with vue-router on the server side

Like the Vue instance, you need to create a singleton vueRouter object. For each request, you need to create a new instance of vueRouter:

Function createVueInstance (routes, ctx) {const router = Router ({base:'/ base', mode: 'history', routes: [routes],}); const store = createStore ({ctx}); / / inject routes into vuex sync (store, router); const app = new Vue ({router, render: function (h) {return h (Frame);}, store,}); return {app, router, store} }

At the same time, you need to implement the server-side routing logic in entry-server.js, and use the router.getMatchedComponents method to get the components that match the current route. If the current route does not match the corresponding component, then reject to page 404. Otherwise, the whole app is used for Vue to render the virtual DOM, and the corresponding template is used to generate the corresponding HTML string.

Const createApp = require ('. / app'); module.exports = context = > {return new Promise ((resolve, reject) = > {/ /... / set the location of the server-side router router.push (context.url); / / wait until router parses the possible asynchronous components and hook functions router.onReady () = > {const matchedComponents = router.getMatchedComponents () / / execute the reject function and return 404 if (! matchedComponents.length) {return reject ('mismatched routes, execute the reject function and return 404');} / / Promise should resolve the application instance so that it can render resolve (app);}, reject);});}

Pre-pull data on the server side

Rendering on the Vue server is essentially rendering a "snapshot" of our application, so if the application relies on some asynchronous data, it needs to be prefetched and parsed before starting the rendering process. The server side Web Server Frame acts as the proxy server, initiates the request to the interface service at the server side, and assembles the data into the global Vuex state.

Another concern is that the client needs to get exactly the same data as the server-side application before mounting it to the client-side application-otherwise, the client application will fail because it uses a different state from the server-side application.

At present, the better solution is to give an asyncData to the first-level subcomponents of the route matching. In the asyncData method, the dispatch corresponds to the action. AsyncData is our agreed function name, which means that the rendering component needs to execute it in advance to get the initial data, and it returns a Promise so that we can know when the operation is completed when we render at the back end. Note that because this function is called before the component is instantiated, it cannot access the this. You need to pass in the store and routing information as parameters:

For example:

Export default {/ /... Async asyncData ({store, route}) {return Promise.all ([store.dispatch ('getA'), store.dispatch (' myModule/getB', {root:true}), store.dispatch ('myModule/getC', {root:true}), store.dispatch (' myModule/getD', {root:true}),]);}, / /.}

In entry-server.js, we can route the component that matches router.getMatchedComponents (), and if the component exposes asyncData, we call this method. Then we need to append the parsed state to the rendering context.

Const createApp = require ('. / app'); module.exports = context = > {return new Promise ((resolve, reject) = > {const {app, router, store} = createApp (context); / / for Vue instances without Vue router, for the list page in the project, direct resolve app if (! router) {resolve (app);} / set the location router.push (context.url.replace ('/ base',') of server-side router) / wait until router parses the possible asynchronous components and hook functions router.onReady (() = > {const matchedComponents = router.getMatchedComponents (); / / matches the mismatched route, executes the reject function, and returns 404 if (! matchedComponents.length) {return reject ('unmatched route, executes the reject function and returns 404') } Promise.all (matchedComponents.map (Component = > {if (Component.asyncData) {return Component.asyncData ({store, route: router.currentRoute,});}}) .then () = > {/ / after all the prefetched preFetch hook resolve, / / our store is now populated with the state required by the rendering application. / / when we attach the state to the context and the `template` option is used for renderer, the / / state will be automatically serialized to `template` and HTML will be injected. Context.state = store.state; resolve (app);}) .catch (reject);}, reject);});}

Client managed global state

When the server uses a template for rendering, the context.state will be automatically embedded in the final HTML as a window.__INITIAL_STATE__ state. On the client side, store should get the status before mounting to the application, and eventually our entry-client.js is modified to look like this:

Import createApp from'. / app';const {app, router, store} = createApp (); / / the client replaces the initialized store with window.__INITIAL_STATE__if (window.__INITIAL_STATE__) {store.replaceState (window.__INITIAL_STATE__);} if (router) {router.onReady (() = > {app.$mount ('# app')});} else {app.$mount ('# app');}

Solutions to common problems

At this point, the basic code modification has been completed, and here are some solutions to common problems:

There are no window or location objects on the server:

For the problems that must be experienced in the migration of an old project to SSR, generally, the DOM operation is used at the project entrance or the created or beforeCreate life cycle, or the location object is obtained. The general solution is to determine whether the typeof window is' undefined',. If you encounter a place where you must use the location object to obtain the relevant parameters in the url, you can also find the corresponding parameters in the ctx object.

Vue-router reported an error Uncaught TypeError: _ Vue.extend is not _ Vue function, but the problem with the _ Vue instance was not found:

By looking at the Vue-router source code, it is found that Vue.use (Vue-Router); is not called manually. There is no call to Vue.use (Vue-Router); there is no problem on the browser side, but there is a problem on the server side. The corresponding Vue-router source code is as follows:

VueRouter.prototype.init = function init (app / * Vue component instance * /) {var this$1 = this; process.env.NODE_ENV! = 'production' & & assert (install.installed, "not installed. Make sure to call `Vue.use (VueRouter) `" + "before creating root instance."); / /.}

The server cannot get the parameters of hash routing.

Because of the parameters of hash routing, vue-router will have no effect. For applications that use the same structure of vue-router, it must be replaced with history routing.

Problem with not getting cookie at the interface:

Since the client brings the cookie to the interface side corresponding to each request, and the server Web Server Frame, as the proxy server, does not maintain the cookie every time, we need to manually set the

The cookie is transmitted transparently to the interface side. The common solution is to mount the ctx to the global state, and manually bring the cookie when initiating an asynchronous request, as shown in the following code:

/ / when createStore.js// creates the global state function `createStore`, mount `ctx` to global state export function createStore ({ctx}) {return new Vuex.Store ({state: {... state, ctx,}, getters, actions, mutations, modules: {/ /.}, plugins: debug? [createLogger ()]: [],});}

When initiating an asynchronous request, manually bring cookie. Axios is used in the project:

/ / actions.js//... const actions = {async getUserInfo ({commit, state}) {let requestParams = {params: {random: tool.createRandomString (8, true),}, headers: {'XMLHttpRequest',},}; / / bring cookie if (state.ctx.request.headers.cookie) {requestParams.headers.Cookie = state.ctx.request.headers.cookie;} / / manually. Let res = await Axios.get (`${requestUrlOrigin} ${url.GET_A}`, requestParams); commit (globalTypes.SET_A, {res: res.data,});}}; / /.

The interface requests the question of connect ECONNREFUSED 127.0.0.1VOL80

The reason is that before the modification, devServer.proxy proxy configuration was used to solve the cross-domain problem when using client rendering. When the server initiates an asynchronous request to the interface as a proxy server, it does not read the corresponding webpack configuration. For the server, it will request the interface under the corresponding path under the current domain.

The solution is to remove the devServer.proxy configuration of webpack, and bring the corresponding origin to the API request:

Const requestUrlOrigin = requestUrlOrigin = state.ctx.URL.origin;const res = await Axios.get (`${requestUrlOrigin} ${url.GET_A}`, requestParams)

When the vue-router configuration item has a base parameter, the corresponding route cannot be matched during initialization.

Entry-server.js in the official example:

/ / entry-server.jsimport {createApp} from'. / app';export default context = > {/ / because it may be an asynchronous routing hook function or component, we will return a Promise so that the server can wait for everything to be ready before rendering. Return new Promise ((resolve, reject) = > {const {app, router} = createApp (); / / set the location of the server-side router router.push (context.url); / /...});}

The reason is that when setting the location of the server-side router, context.url is the url that accesses the page with base, and base should be removed when router.push, as shown below:

Router.push (context.url.replace ('/ base',')). The above is the time-consuming optimization method for the first screen of Web rendered by the Vue server shared by the editor. If you happen to have similar doubts, please refer to the above analysis to understand. If you want to know more about it, you are welcome to follow the industry information channel.

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Development

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report