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

Posted 前端自习课

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vuejs184-Vue 服务端渲染实践 ——Web应用首屏耗时最优化方案相关的知识,希望对你有一定的参考价值。

链接:https://segmentfault.com/a/1190000018577041


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

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

概述

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

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

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

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

    • 在服务端预拉取数据;

    • 客户端托管全局状态;

    • 常见问题的解决方案;

    • 基本目录改造;

    • 在服务端用 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;

  12. }

使用页面模板:

当你在渲染 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. stats = 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. });

  65. }

渲染中间件配置

产品环境下,打包后的客户端和服务端的 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);

  27. }

注:在我所在的项目中,需要动态判断是否需要注册 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, VueRouter, 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;

  5. }

在服务端用 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 };

  18. }

同时,需要在 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. });


  18. }

在服务端预拉取数据

在 Vue服务端渲染,本质上是在渲染我们应用程序的"快照",所以如果应用程序依赖于一些异步数据,那么在开始渲染过程之前,需要先预取和解析好这些数据。服务端 WebServer 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. });

  35. }

客户端托管全局状态

当服务端使用模板进行渲染时, 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');

  13. }

常见问题的解决方案

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

对于旧项目迁移到 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. // ...

  9. }

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

由于客户端每次请求都会对应地把 cookie带给接口侧,而服务端 Web ServerFrame作为代理服务器,并不会每次维持 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. });

  17. }

当发起异步请求时,手动带上 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 requestUrlOrigin = 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. });

  13. }

原因是设置服务器端 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服务端渲染的迁移。


每一个“在看”,都是对我最大的肯定!

以上是关于Vuejs184-Vue 服务端渲染实践 ——Web应用首屏耗时最优化方案的主要内容,如果未能解决你的问题,请参考以下文章

服务器端渲染(SSR)vuejs 前端项目

angular10预渲染实践笔记

angular10预渲染实践笔记

angular10预渲染实践笔记

React 服务器渲染原理解析与实践

React 服务器渲染原理解析与实践