Vue服务端渲染实践 ——Web应用首屏耗时最优化方案

开发 前端
对于需要SEO、追求极致的首屏性能的应用,前端渲染的SPA是糟糕的。好在Vue 2.0后是支持服务端渲染的,零零散散花费了两三周事件,通过改造现有项目,基本完成了在现有项目中实践了Vue服务端渲染。

 随着各大前端框架的诞生和演变,SPA开始流行,单页面应用的优势在于可以不重新加载整个页面的情况下,通过ajax和服务器通信,实现整个Web应用拒不更新,带来了极致的用户体验。然而,对于需要SEO、追求极致的首屏性能的应用,前端渲染的SPA是糟糕的。好在Vue 2.0后是支持服务端渲染的,零零散散花费了两三周事件,通过改造现有项目,基本完成了在现有项目中实践了Vue服务端渲染。

关于Vue服务端渲染的原理、搭建,官方文档已经讲的比较详细了,因此,本文不是抄袭文档,而是文档的补充。特别是对于如何与现有项目进行很好的结合,还是需要费很大功夫的。本文主要对我所在的项目中进行Vue服务端渲染的改造过程进行阐述,加上一些个人的理解,作为分享与学习。

概述

本文主要分以下几个方面:

  •  什么是服务端渲染?服务端渲染的原理是什么?
  •  如何在基于Koa的Web Server Frame上配置服务端渲染? 
    • 基本用法
    •  Webpack配置
    •  开发环境搭建
      • 渲染中间件配置
  •  如何对现有项目进行改造?
    •   基本目录改造;
    •   在服务端用vue-router分割代码;
      •   在服务端预拉取数据;
      •   客户端托管全局状态;
      •   常见问题的解决方案;

什么是服务端渲染?服务端渲染的原理是什么?

Vue.js是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出Vue组件,进行生成DOM和操作DOM。然而,也可以将同一个组件渲染为服务器端的HTML字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。

上面这段话是源自Vue服务端渲染文档的解释,用通俗的话来说,大概可以这么理解:

  •  服务端渲染的目的是:性能优势。 在服务端生成对应的HTML字符串,客户端接收到对应的HTML字符串,能立即渲染DOM,最高效的首屏耗时。此外,由于服务端直接生成了对应的HTML字符串,对SEO也非常友好;
  •  服务端渲染的本质是:生成应用程序的“快照”。将Vue及对应库运行在服务端,此时,Web Server Frame实际上是作为代理服务器去访问接口服务器来预拉取数据,从而将拉取到的数据作为Vue组件的初始状态。
  •  服务端渲染的原理是:虚拟DOM。在Web Server Frame作为代理服务器去访问接口服务器来预拉取数据后,这是服务端初始化组件需要用到的数据,此后,组件的beforeCreate和created生命周期会在服务端调用,初始化对应的组件后,Vue启用虚拟DOM形成初始化的HTML字符串。之后,交由客户端托管。实现前后端同构应用。

如何在基于Koa的Web Server Frame上配置服务端渲染?

基本用法

需要用到Vue服务端渲染对应库vue-server-renderer,通过npm安装: 

  1. npm install vue vue-server-renderer --save 

最简单的,首先渲染一个Vue实例: 

  1. // 第 1 步:创建一个 Vue 实例  
  2. const Vue = require('vue');  
  3. const app = new Vue({  
  4.   template: `<div>Hello World</div> 
  5. });  
  6. // 第 2 步:创建一个 renderer  
  7. const renderer = require('vue-server-renderer').createRenderer();  
  8. // 第 3 步:将 Vue 实例渲染为 HTML  
  9. renderer.renderToString(app, (err, html) => {  
  10.   if (err) {  
  11.       throw err;  
  12.   }  
  13.   console.log(html);  
  14.   // => <div data-server-rendered="true">Hello World</div>  
  15. }); 

与服务器集成: 

  1. module.exports = async function(ctx) {  
  2.     ctx.status = 200 
  3.     let html = '' 
  4.     try {  
  5.         // ...  
  6.         html = await renderer.renderToString(app, ctx);  
  7.     } catch (err) {  
  8.         ctx.logger('Vue SSR Render error', JSON.stringify(err));  
  9.         html = await ctx.getErrorPage(err); // 渲染出错的页面  
  10.     }  
  11.     ctx.body = html 

使用页面模板:

当你在渲染Vue应用程序时,renderer只从应用程序生成HTML标记。在这个示例中,我们必须用一个额外的HTML页面包裹容器,来包裹生成的HTML标记。

为了简化这些,你可以直接在创建renderer时提供一个页面模板。多数时候,我们会将页面模板放在特有的文件中: 

  1. <!DOCTYPE html>  
  2. <html lang="en">  
  3.   <head><title>Hello</title></head>  
  4.   <body>  
  5.     <!--vue-ssr-outlet-->  
  6.   </body>  
  7. </html> 

然后,我们可以读取和传输文件到Vue renderer中: 

  1. const tpl = fs.readFileSync(path.resolve(__dirname, './index.html'), 'utf-8');  
  2. const renderer = vssr.createRenderer({  
  3.     template: tpl,  
  4. }); 

Webpack配置

然而在实际项目中,不止上述例子那么简单,需要考虑很多方面:路由、数据预取、组件化、全局状态等,所以服务端渲染不是只用一个简单的模板,然后加上使用vue-server-renderer完成的,如下面的示意图所示:

如示意图所示,一般的Vue服务端渲染项目,有两个项目入口文件,分别为entry-client.js和entry-server.js,一个仅运行在客户端,一个仅运行在服务端,经过Webpack打包后,会生成两个Bundle,服务端的Bundle会用于在服务端使用虚拟DOM生成应用程序的“快照”,客户端的Bundle会在浏览器执行。

因此,我们需要两个Webpack配置,分别命名为webpack.client.config.js和webpack.server.config.js,分别用于生成客户端Bundle与服务端Bundle,分别命名为vue-ssr-client-manifest.json与vue-ssr-server-bundle.json,关于如何配置,Vue官方有相关示例vue-hackernews-2.0

开发环境搭建

我所在的项目使用Koa作为Web Server Frame,项目使用koa-webpack进行开发环境的构建。如果是在产品环境下,会生成vue-ssr-client-manifest.json与vue-ssr-server-bundle.json,包含对应的Bundle,提供客户端和服务端引用,而在开发环境下,一般情况下放在内存中。使用memory-fs模块进行读取。 

  1. const fs = require('fs')  
  2. const path = require( 'path' );  
  3. const webpack = require( 'webpack' );  
  4. const koaWpDevMiddleware = require( 'koa-webpack' );  
  5. const MFS = require('memory-fs');  
  6. const appSSR = require('./../../app.ssr.js');  
  7. let wpConfig;  
  8. let clientConfig, serverConfig;  
  9. let wpCompiler;  
  10. let clientCompiler, serverCompiler;  
  11. let clientManifest;  
  12. let bundle;  
  13. // 生成服务端bundle的webpack配置  
  14. if ((fs.existsSync(path.resolve(cwd,'webpack.server.config.js')))) {  
  15.   serverConfig = require(path.resolve(cwd, 'webpack.server.config.js'));  
  16.   serverCompiler = webpack( serverConfig );  
  17.  
  18. // 生成客户端clientManifest的webpack配置  
  19. if ((fs.existsSync(path.resolve(cwd,'webpack.client.config.js')))) {  
  20.   clientConfig = require(path.resolve(cwd, 'webpack.client.config.js'));  
  21.   clientCompiler = webpack(clientConfig);  
  22.  
  23. if (serverCompiler && clientCompiler) {  
  24.   let publicPath = clientCompiler.output && clientCompiler.output.publicPath;  
  25.   const koaDevMiddleware = await koaWpDevMiddleware({  
  26.     compiler: clientCompiler,  
  27.     devMiddleware: {  
  28.       publicPath,  
  29.       serverSideRender: true  
  30.     },  
  31.   });  
  32.   app.use(koaDevMiddleware);  
  33.   // 服务端渲染生成clientManifest  
  34.   app.use(async (ctx, next) => {  
  35.     const stats = ctx.state.webpackStats.toJson();  
  36.     const assetsByChunkName = stats.assetsByChunkName;  
  37.     stats.errors.forEach(err => console.error(err));  
  38.     stats.warnings.forEach(err => console.warn(err));  
  39.     if (stats.errors.length) {  
  40.       console.error(stats.errors);  
  41.       return;  
  42.     }  
  43.     // 生成的clientManifest放到appSSR模块,应用程序可以直接读取  
  44.     let fileSystem = koaDevMiddleware.devMiddleware.fileSystem;  
  45.     clientManifest = JSON.parse(fileSystem.readFileSync(path.resolve(cwd,'./dist/vue-ssr-client-manifest.json'), 'utf-8'));  
  46.     appSSR.clientManifest = clientManifest;  
  47.     await next();  
  48.   });  
  49.   // 服务端渲染的server bundle 存储到内存里  
  50.   const mfs = new MFS();  
  51.   serverCompiler.outputFileSystem = mfs 
  52.   serverCompiler.watch({}, (err, stats) => {  
  53.     if (err) {  
  54.       throw err;  
  55.     }  
  56.     statsstats = stats.toJson();  
  57.     if (stats.errors.length) {  
  58.       console.error(stats.errors);  
  59.       return;  
  60.     }  
  61.     // 生成的bundle放到appSSR模块,应用程序可以直接读取  
  62.     bundle = JSON.parse(mfs.readFileSync(path.resolve(cwd,'./dist/vue-ssr-server-bundle.json'), 'utf-8'));  
  63.     appSSR.bundle = bundle;  
  64.   });  

渲染中间件配置

产品环境下,打包后的客户端和服务端的Bundle会存储为vue-ssr-client-manifest.json与vue-ssr-server-bundle.json,通过文件流模块fs读取即可,但在开发环境下,我创建了一个appSSR模块,在发生代码更改时,会触发Webpack热更新,appSSR对应的bundle也会更新,appSSR模块代码如下所示: 

  1. let clientManifest;  
  2. let bundle;  
  3. const appSSR = {  
  4.   get bundle() {  
  5.     return bundle;  
  6.   },  
  7.   set bundle(val) {  
  8.     bundle = val 
  9.   },  
  10.   get clientManifest() {  
  11.     return clientManifest;  
  12.   },  
  13.   set clientManifest(val) {  
  14.     clientManifest = val 
  15.   }  
  16. };  
  17. module.exports = appSSR

通过引入appSSR模块,在开发环境下,就可以拿到clientManifest和ssrBundle,项目的渲染中间件如下: 

  1. const fs = require('fs');  
  2. const path = require('path');  
  3. const ejs = require('ejs');  
  4. const vue = require('vue');  
  5. const vssr = require('vue-server-renderer');  
  6. const createBundleRenderer = vssr.createBundleRenderer;  
  7. const dirname = process.cwd();  
  8. const env = process.env.RUN_ENVIRONMENT;  
  9. let bundle;  
  10. let clientManifest;  
  11. if (env === 'development') {  
  12.   // 开发环境下,通过appSSR模块,拿到clientManifest和ssrBundle  
  13.   let appSSR = require('./../../core/app.ssr.js');  
  14.   bundle = appSSR.bundle;  
  15.   clientManifest = appSSR.clientManifest;  
  16. } else {  
  17.   bundle = JSON.parse(fs.readFileSync(path.resolve(__dirname, './dist/vue-ssr-server-bundle.json'), 'utf-8'));  
  18.   clientManifest = JSON.parse(fs.readFileSync(path.resolve(__dirname, './dist/vue-ssr-client-manifest.json'), 'utf-8'));  
  19.  
  20. module.exports = async function(ctx) {  
  21.   ctx.status = 200 
  22.   let html;  
  23.   let context = await ctx.getTplContext();  
  24.   ctx.logger('进入SSR,context为: ', JSON.stringify(context));  
  25.   const tpl = fs.readFileSync(path.resolve(__dirname, './newTemplate.html'), 'utf-8');  
  26.   const renderer = createBundleRenderer(bundle, {  
  27.     runInNewContext: false,  
  28.     template: tpl, // (可选)页面模板  
  29.     clientManifest: clientManifest // (可选)客户端构建 manifest  
  30.   });  
  31.   ctx.logger('createBundleRenderer  renderer:', JSON.stringify(renderer));  
  32.   try {  
  33.     html = await renderer.renderToString({  
  34.       ...context,  
  35.       url: context.CTX.url,  
  36.     });  
  37.   } catch(err) {  
  38.     ctx.logger('SSR renderToString 失败: ', JSON.stringify(err));  
  39.     console.error(err);  
  40.   }  
  41.   ctx.body = html 
  42. }; 

如何对现有项目进行改造?

基本目录改造

使用Webpack来处理服务器和客户端的应用程序,大部分源码可以使用通用方式编写,可以使用Webpack支持的所有功能。

一个基本项目可能像是这样: 

  1. src  
  2. ├── components  
  3. │   ├── Foo.vue  
  4. │   ├── Bar.vue  
  5. │   └── Baz.vue  
  6. ├── frame  
  7. │   ├── app.js # 通用 entry(universal entry)  
  8. │   ├── entry-client.js # 仅运行于浏览器  
  9. │   ├── entry-server.js # 仅运行于服务器  
  10. │   └── index.vue # 项目入口组件  
  11. ├── pages  
  12. ├── routers  
  13. └── store 

app.js是我们应用程序的「通用entry」。在纯客户端应用程序中,我们将在此文件中创建根Vue实例,并直接挂载到DOM。但是,对于服务器端渲染(SSR),责任转移到纯客户端entry文件。app.js简单地使用export导出一个createApp函数: 

  1. import Router from '~ut/router';  
  2. import { sync } from 'vuex-router-sync';  
  3. import Vue from 'vue';  
  4. import { createStore } from './../store';  
  5. import Frame from './index.vue';  
  6. import myRouter from './../routers/myRouter';  
  7. function createVueInstance(routes, ctx) {  
  8.     const router = Router({  
  9.         base: '/base',  
  10.         mode: 'history',  
  11.         routes: [routes],  
  12.     });  
  13.     const store = createStore({ ctx });  
  14.     // 把路由注入到vuex中  
  15.     sync(store, router);  
  16.     const app = new Vue({  
  17.         router,  
  18.         render: function(h) {  
  19.             return h(Frame);  
  20.         },  
  21.         store,  
  22.     });  
  23.     return { app, router, store };  
  24.  
  25. module.exports = function createApp(ctx) {  
  26.     return createVueInstance(myRouter, ctx);   

 注:在我所在的项目中,需要动态判断是否需要注册DicomView,只有在客户端才初始化DicomView,由于Node.js环境没有window对象,对于代码运行环境的判断,可以通过typeof window === 'undefined'来进行判断。

避免创建单例

如Vue SSR文档所述:

当编写纯客户端 (client-only) 代码时,我们习惯于每次在新的上下文中对代码进行取值。但是,Node.js 服务器是一个长期运行的进程。当我们的代码进入该进程时,它将进行一次取值并留存在内存中。这意味着如果创建一个单例对象,它将在每个传入的请求之间共享。如基本示例所示,我们为每个请求创建一个新的根 Vue 实例。这与每个用户在自己的浏览器中使用新应用程序的实例类似。如果我们在多个请求之间使用一个共享的实例,很容易导致交叉请求状态污染 (cross-request state pollution)。因此,我们不应该直接创建一个应用程序实例,而是应该暴露一个可以重复执行的工厂函数,为每个请求创建新的应用程序实例。同样的规则也适用于 router、store 和 event bus 实例。你不应该直接从模块导出并将其导入到应用程序中,而是需要在 createApp 中创建一个新的实例,并从根 Vue 实例注入。

如上代码所述,createApp方法通过返回一个返回值创建Vue实例的对象的函数调用,在函数createVueInstance中,为每一个请求创建了Vue,Vue Router,Vuex实例。并暴露给entry-client和entry-server模块。

在客户端entry-client.js只需创建应用程序,并且将其挂载到DOM中: 

  1. import { createApp } from './app';  
  2. // 客户端特定引导逻辑……  
  3. const { app } = createApp();  
  4. // 这里假定 App.vue 模板中根元素具有 `id="app" 
  5. app.$mount('#app'); 

服务端entry-server.js使用default export 导出函数,并在每次渲染中重复调用此函数。此时,除了创建和返回应用程序实例之外,它不会做太多事情 - 但是稍后我们将在此执行服务器端路由匹配和数据预取逻辑: 

  1. import { createApp } from './app';  
  2. export default context => {  
  3.   const { app } = createApp();  
  4.   return app;  

在服务端用vue-router分割代码

与Vue实例一样,也需要创建单例的vueRouter对象。对于每个请求,都需要创建一个新的vueRouter实例: 

  1. function createVueInstance(routes, ctx) {  
  2.     const router = Router({  
  3.         base: '/base',  
  4.         mode: 'history',  
  5.         routes: [routes],  
  6.     });  
  7.     const store = createStore({ ctx });  
  8.     // 把路由注入到vuex中  
  9.     sync(store, router);  
  10.     const app = new Vue({  
  11.         router,  
  12.         render: function(h) {  
  13.             return h(Frame);  
  14.         },  
  15.         store,  
  16.     });  
  17.     return { app, router, store };  

同时,需要在entry-server.js中实现服务器端路由逻辑,使用router.getMatchedComponents方法获取到当前路由匹配的组件,如果当前路由没有匹配到相应的组件,则reject到404页面,否则resolve整个app,用于Vue渲染虚拟DOM,并使用对应模板生成对应的HTML字符串。 

  1. const createApp = require('./app');  
  2. module.exports = context => {  
  3.   return new Promise((resolve, reject) => {  
  4.     // ...  
  5.     // 设置服务器端 router 的位置  
  6.     router.push(context.url);  
  7.     // 等到 router 将可能的异步组件和钩子函数解析完  
  8.     router.onReady(() => {  
  9.       const matchedComponents = router.getMatchedComponents();  
  10.       // 匹配不到的路由,执行 reject 函数,并返回 404  
  11.       if (!matchedComponents.length) {  
  12.         return reject('匹配不到的路由,执行 reject 函数,并返回 404');  
  13.       }  
  14.       // Promise 应该 resolve 应用程序实例,以便它可以渲染  
  15.       resolve(app);  
  16.     }, reject);  
  17.   });  

在服务端预拉取数据

在Vue服务端渲染,本质上是在渲染我们应用程序的"快照",所以如果应用程序依赖于一些异步数据,那么在开始渲染过程之前,需要先预取和解析好这些数据。服务端Web Server Frame作为代理服务器,在服务端对接口服务发起请求,并将数据拼装到全局Vuex状态中。

另一个需要关注的问题是在客户端,在挂载到客户端应用程序之前,需要获取到与服务器端应用程序完全相同的数据 - 否则,客户端应用程序会因为使用与服务器端应用程序不同的状态,然后导致混合失败。

目前较好的解决方案是,给路由匹配的一级子组件一个asyncData,在asyncData方法中,dispatch对应的action。asyncData是我们约定的函数名,表示渲染组件需要预先执行它获取初始数据,它返回一个Promise,以便我们在后端渲染的时候可以知道什么时候该操作完成。注意,由于此函数会在组件实例化之前调用,所以它无法访问this。需要将store和路由信息作为参数传递进去:

举个例子: 

  1. <!-- Lung.vue -->  
  2. <template>  
  3.   <div></div>  
  4. </template>  
  5. <script>  
  6. export default {  
  7.   // ...  
  8.   async asyncData({ store, route }) {  
  9.     return Promise.all([  
  10.       store.dispatch('getA'),  
  11.       store.dispatch('myModule/getB', { root:true }),  
  12.       store.dispatch('myModule/getC', { root:true }),  
  13.       store.dispatch('myModule/getD', { root:true }),  
  14.     ]);  
  15.   },  
  16.   // ...  
  17.  
  18. </script> 

在entry-server.js中,我们可以通过路由获得与router.getMatchedComponents()相匹配的组件,如果组件暴露出asyncData,我们就调用这个方法。然后我们需要将解析完成的状态,附加到渲染上下文中。 

  1. const createApp = require('./app');  
  2. module.exports = context => {  
  3.   return new Promise((resolve, reject) => {  
  4.     const { app, router, store } = createApp(context);  
  5.     // 针对没有Vue router 的Vue实例,在项目中为列表页,直接resolve app  
  6.     if (!router) {  
  7.       resolve(app);  
  8.     }  
  9.     // 设置服务器端 router 的位置  
  10.       router.push(context.url.replace('/base', ''));  
  11.     // 等到 router 将可能的异步组件和钩子函数解析完  
  12.     router.onReady(() => {  
  13.       const matchedComponents = router.getMatchedComponents();  
  14.       // 匹配不到的路由,执行 reject 函数,并返回 404  
  15.       if (!matchedComponents.length) {  
  16.         return reject('匹配不到的路由,执行 reject 函数,并返回 404');  
  17.       }  
  18.       Promise.all(matchedComponents.map(Component => {  
  19.         if (Component.asyncData) {  
  20.           return Component.asyncData({  
  21.             store,  
  22.             route: router.currentRoute,  
  23.           });  
  24.         }  
  25.       })).then(() => {  
  26.         // 在所有预取钩子(preFetch hook) resolve 后,  
  27.         // 我们的 store 现在已经填充入渲染应用程序所需的状态。  
  28.         // 当我们将状态附加到上下文,并且 `template` 选项用于 renderer 时,  
  29.         // 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。  
  30.         context.state = store.state;  
  31.         resolve(app);  
  32.       }).catch(reject);  
  33.     }, reject);  
  34.   });  

客户端托管全局状态

当服务端使用模板进行渲染时,context.state将作为window.__INITIAL_STATE__状态,自动嵌入到最终的HTML 中。而在客户端,在挂载到应用程序之前,store就应该获取到状态,最终我们的entry-client.js被改造为如下所示: 

  1. import createApp from './app';  
  2. const { app, router, store } = createApp();  
  3. // 客户端把初始化的store替换为window.__INITIAL_STATE__  
  4. if (window.__INITIAL_STATE__) {  
  5.   store.replaceState(window.__INITIAL_STATE__);  
  6.  
  7. if (router) {  
  8.   router.onReady(() => {  
  9.     app.$mount('#app')  
  10.   });  
  11. } else {  
  12.   app.$mount('#app');  

常见问题的解决方案

至此,基本的代码改造也已经完成了,下面说的是一些常见问题的解决方案:

  •  在服务端没有window、location对象:

对于旧项目迁移到SSR肯定会经历的问题,一般为在项目入口处或是created、beforeCreate生命周期使用了DOM操作,或是获取了location对象,通用的解决方案一般为判断执行环境,通过typeof window是否为'undefined',如果遇到必须使用location对象的地方用于获取url中的相关参数,在ctx对象中也可以找到对应参数。

  •  vue-router报错Uncaught TypeError: _Vue.extend is not _Vue function,没有找到_Vue实例的问题:

通过查看Vue-router源码发现没有手动调用Vue.use(Vue-Router);。没有调用Vue.use(Vue-Router);在浏览器端没有出现问题,但在服务端就会出现问题。对应的Vue-router源码所示: 

  1. VueRouter.prototype.init = function init (app /* Vue component instance */) {  
  2.     var this$1 = this 
  3.   process.env.NODE_ENV !== 'production' && assert(  
  4.     install.installed,  
  5.     "not installed. Make sure to call `Vue.use(VueRouter)` " +  
  6.     "before creating root instance."  
  7.   );  
  8.   // ...  
  •  服务端无法获取hash路由的参数

由于hash路由的参数,会导致vue-router不起效果,对于使用了vue-router的前后端同构应用,必须换为history路由。

  •  接口处获取不到cookie的问题:

由于客户端每次请求都会对应地把cookie带给接口侧,而服务端Web Server Frame作为代理服务器,并不会每次维持cookie,所以需要我们手动把

cookie透传给接口侧,常用的解决方案是,将ctx挂载到全局状态中,当发起异步请求时,手动带上cookie,如下代码所示: 

  1. // createStore.js  
  2. // 在创建全局状态的函数`createStore`时,将`ctx`挂载到全局状态  
  3. export function createStore({ ctx }) {  
  4.     return new Vuex.Store({  
  5.         state: {  
  6.             ...state,  
  7.             ctx,  
  8.         }, 
  9.          getters,  
  10.         actions,  
  11.         mutations,  
  12.         modules: {  
  13.             // ...  
  14.         },  
  15.         plugins: debug ? [createLogger()] : [],  
  16.     });  

当发起异步请求时,手动带上cookie,项目中使用的是Axios: 

  1. // actions.js  
  2. // ...  
  3. const actions = {  
  4.   async getUserInfo({ commit, state }) {  
  5.     let requestParams = {  
  6.       params: {  
  7.         random: tool.createRandomString(8, true),  
  8.       },  
  9.       headers: {  
  10.         'X-Requested-With': 'XMLHttpRequest',  
  11.       },  
  12.     };  
  13.     // 手动带上cookie  
  14.     if (state.ctx.request.headers.cookie) {  
  15.       requestParams.headers.Cookie = state.ctx.request.headers.cookie;  
  16.     }  
  17.     // ...  
  18.     let res = await Axios.get(`${requestUrlOrigin}${url.GET_A}`, requestParams);  
  19.     commit(globalTypes.SET_A, {  
  20.       res: res.data,  
  21.     });  
  22.   }  
  23. };  
  24. // ... 
  •  接口请求时报connect ECONNREFUSED 127.0.0.1:80的问题

原因是改造之前,使用客户端渲染时,使用了devServer.proxy代理配置来解决跨域问题,而服务端作为代理服务器对接口发起异步请求时,不会读取对应的webpack配置,对于服务端而言会对应请求当前域下的对应path下的接口。

解决方案为去除webpack的devServer.proxy配置,对于接口请求带上对应的origin即可: 

  1. const requestUrlOriginrequestUrlOrigin = requestUrlOrigin = state.ctx.URL.origin;  
  2. const res = await Axios.get(`${requestUrlOrigin}${url.GET_A}`, requestParams); 
  •  对于vue-router配置项有base参数时,初始化时匹配不到对应路由的问题

在官方示例中的entry-server.js: 

  1. // entry-server.js  
  2. import { createApp } from './app';  
  3. export default context => {  
  4.   // 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,  
  5.   // 以便服务器能够等待所有的内容在渲染前,  
  6.   // 就已经准备就绪。  
  7.   return new Promise((resolve, reject) => {  
  8.     const { app, router } = createApp();  
  9.     // 设置服务器端 router 的位置  
  10.     router.push(context.url);  
  11.     // ...  
  12.   });  

原因是设置服务器端router的位置时,context.url为访问页面的url,并带上了base,在router.push时应该去除base,如下所示: 

  1. router.push(context.url.replace('/base', '')); 

小结

本文为笔者通过对现有项目进行改造,给现有项目加上Vue服务端渲染的实践过程的总结。

首先阐述了什么是Vue服务端渲染,其目的、本质及原理,通过在服务端使用Vue的虚拟DOM,形成初始化的HTML字符串,即应用程序的“快照”。带来极大的性能优势,包括SEO优势和首屏渲染的极速体验。之后阐述了Vue服务端渲染的基本用法,即两个入口、两个webpack配置,分别作用于客户端和服务端,分别生成vue-ssr-client-manifest.json与vue-ssr-server-bundle.json作为打包结果。最后通过对现有项目的改造过程,包括对路由进行改造、数据预获取和状态初始化,并解释了在Vue服务端渲染项目改造过程中的常见问题,帮助我们进行现有项目往Vue服务端渲染的迁移。 

责任编辑:庞桂玉 来源: segmentfault
相关推荐

2021-04-26 13:20:06

Vue服务端渲染前端

2022-10-08 00:01:00

ssrvuereact

2024-01-16 08:05:53

2020-11-03 14:10:29

Vue服务端渲染前端

2023-08-24 16:54:05

2022-08-02 07:46:26

C端编译过程幸福里APP

2022-12-29 08:56:30

监控服务平台

2021-04-30 09:32:38

服务端渲染SSR

2017-08-16 10:57:25

H5HTML开发

2012-12-24 09:55:15

JavaJava WebJava优化

2022-02-18 11:13:53

监控架构系统

2023-03-22 18:31:10

Android页面优化

2017-11-22 14:24:21

Reactjavaajax

2017-11-30 09:20:06

2024-05-27 00:00:00

PHP阿里云OSS

2023-09-06 08:14:34

性能优化模式

2016-03-18 09:04:42

swift服务端

2023-09-11 10:53:32

2019-01-22 15:28:04

Javascriptvue.jsnuxt.js

2012-10-19 10:06:04

Ubuntu Twea
点赞
收藏

51CTO技术栈公众号