精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

Vue服務端渲染實踐 ——Web應用首屏耗時最優(yōu)化方案

開發(fā) 前端
對于需要SEO、追求極致的首屏性能的應用,前端渲染的SPA是糟糕的。好在Vue 2.0后是支持服務端渲染的,零零散散花費了兩三周事件,通過改造現(xiàn)有項目,基本完成了在現(xiàn)有項目中實踐了Vue服務端渲染。

 隨著各大前端框架的誕生和演變,SPA開始流行,單頁面應用的優(yōu)勢在于可以不重新加載整個頁面的情況下,通過ajax和服務器通信,實現(xiàn)整個Web應用拒不更新,帶來了極致的用戶體驗。然而,對于需要SEO、追求極致的首屏性能的應用,前端渲染的SPA是糟糕的。好在Vue 2.0后是支持服務端渲染的,零零散散花費了兩三周事件,通過改造現(xiàn)有項目,基本完成了在現(xiàn)有項目中實踐了Vue服務端渲染。

關于Vue服務端渲染的原理、搭建,官方文檔已經講的比較詳細了,因此,本文不是抄襲文檔,而是文檔的補充。特別是對于如何與現(xiàn)有項目進行很好的結合,還是需要費很大功夫的。本文主要對我所在的項目中進行Vue服務端渲染的改造過程進行闡述,加上一些個人的理解,作為分享與學習。

概述

本文主要分以下幾個方面:

  •  什么是服務端渲染?服務端渲染的原理是什么?
  •  如何在基于Koa的Web Server Frame上配置服務端渲染? 
    • 基本用法
    •  Webpack配置
    •  開發(fā)環(huán)境搭建
      • 渲染中間件配置
  •  如何對現(xiàn)有項目進行改造?
    •   基本目錄改造;
    •   在服務端用vue-router分割代碼;
      •   在服務端預拉取數(shù)據(jù);
      •   客戶端托管全局狀態(tài);
      •   常見問題的解決方案;

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

Vue.js是構建客戶端應用程序的框架。默認情況下,可以在瀏覽器中輸出Vue組件,進行生成DOM和操作DOM。然而,也可以將同一個組件渲染為服務器端的HTML字符串,將它們直接發(fā)送到瀏覽器,最后將這些靜態(tài)標記"激活"為客戶端上完全可交互的應用程序。

上面這段話是源自Vue服務端渲染文檔的解釋,用通俗的話來說,大概可以這么理解:

  •  服務端渲染的目的是:性能優(yōu)勢。 在服務端生成對應的HTML字符串,客戶端接收到對應的HTML字符串,能立即渲染DOM,最高效的首屏耗時。此外,由于服務端直接生成了對應的HTML字符串,對SEO也非常友好;
  •  服務端渲染的本質是:生成應用程序的“快照”。將Vue及對應庫運行在服務端,此時,Web Server Frame實際上是作為代理服務器去訪問接口服務器來預拉取數(shù)據(jù),從而將拉取到的數(shù)據(jù)作為Vue組件的初始狀態(tài)。
  •  服務端渲染的原理是:虛擬DOM。在Web Server Frame作為代理服務器去訪問接口服務器來預拉取數(shù)據(jù)后,這是服務端初始化組件需要用到的數(shù)據(jù),此后,組件的beforeCreate和created生命周期會在服務端調用,初始化對應的組件后,Vue啟用虛擬DOM形成初始化的HTML字符串。之后,交由客戶端托管。實現(xiàn)前后端同構應用。

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

基本用法

需要用到Vue服務端渲染對應庫vue-server-renderer,通過npm安裝: 

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

最簡單的,首先渲染一個Vue實例: 

  1. // 第 1 步:創(chuàng)建一個 Vue 實例  
  2. const Vue = require('vue');  
  3. const app = new Vue({  
  4.   template: `<div>Hello World</div> 
  5. });  
  6. // 第 2 步:創(chuàng)建一個 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標記。

為了簡化這些,你可以直接在創(chuàng)建renderer時提供一個頁面模板。多數(shù)時候,我們會將頁面模板放在特有的文件中: 

  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配置

然而在實際項目中,不止上述例子那么簡單,需要考慮很多方面:路由、數(shù)據(jù)預取、組件化、全局狀態(tài)等,所以服務端渲染不是只用一個簡單的模板,然后加上使用vue-server-renderer完成的,如下面的示意圖所示:

如示意圖所示,一般的Vue服務端渲染項目,有兩個項目入口文件,分別為entry-client.js和entry-server.js,一個僅運行在客戶端,一個僅運行在服務端,經過Webpack打包后,會生成兩個Bundle,服務端的Bundle會用于在服務端使用虛擬DOM生成應用程序的“快照”,客戶端的Bundle會在瀏覽器執(zhí)行。

因此,我們需要兩個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

開發(fā)環(huán)境搭建

我所在的項目使用Koa作為Web Server Frame,項目使用koa-webpack進行開發(fā)環(huán)境的構建。如果是在產品環(huán)境下,會生成vue-ssr-client-manifest.json與vue-ssr-server-bundle.json,包含對應的Bundle,提供客戶端和服務端引用,而在開發(fā)環(huán)境下,一般情況下放在內存中。使用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.   });  

渲染中間件配置

產品環(huán)境下,打包后的客戶端和服務端的Bundle會存儲為vue-ssr-client-manifest.json與vue-ssr-server-bundle.json,通過文件流模塊fs讀取即可,但在開發(fā)環(huán)境下,我創(chuàng)建了一個appSSR模塊,在發(fā)生代碼更改時,會觸發(fā)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模塊,在開發(fā)環(huán)境下,就可以拿到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.   // 開發(fā)環(huán)境下,通過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. }; 

如何對現(xiàn)有項目進行改造?

基本目錄改造

使用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」。在純客戶端應用程序中,我們將在此文件中創(chuàng)建根Vue實例,并直接掛載到DOM。但是,對于服務器端渲染(SSR),責任轉移到純客戶端entry文件。app.js簡單地使用export導出一個createApp函數(shù): 

  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);   

 注:在我所在的項目中,需要動態(tài)判斷是否需要注冊DicomView,只有在客戶端才初始化DicomView,由于Node.js環(huán)境沒有window對象,對于代碼運行環(huán)境的判斷,可以通過typeof window === 'undefined'來進行判斷。

避免創(chuàng)建單例

如Vue SSR文檔所述:

當編寫純客戶端 (client-only) 代碼時,我們習慣于每次在新的上下文中對代碼進行取值。但是,Node.js 服務器是一個長期運行的進程。當我們的代碼進入該進程時,它將進行一次取值并留存在內存中。這意味著如果創(chuàng)建一個單例對象,它將在每個傳入的請求之間共享。如基本示例所示,我們?yōu)槊總€請求創(chuàng)建一個新的根 Vue 實例。這與每個用戶在自己的瀏覽器中使用新應用程序的實例類似。如果我們在多個請求之間使用一個共享的實例,很容易導致交叉請求狀態(tài)污染 (cross-request state pollution)。因此,我們不應該直接創(chuàng)建一個應用程序實例,而是應該暴露一個可以重復執(zhí)行的工廠函數(shù),為每個請求創(chuàng)建新的應用程序實例。同樣的規(guī)則也適用于 router、store 和 event bus 實例。你不應該直接從模塊導出并將其導入到應用程序中,而是需要在 createApp 中創(chuàng)建一個新的實例,并從根 Vue 實例注入。

如上代碼所述,createApp方法通過返回一個返回值創(chuàng)建Vue實例的對象的函數(shù)調用,在函數(shù)createVueInstance中,為每一個請求創(chuàng)建了Vue,Vue Router,Vuex實例。并暴露給entry-client和entry-server模塊。

在客戶端entry-client.js只需創(chuàng)建應用程序,并且將其掛載到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 導出函數(shù),并在每次渲染中重復調用此函數(shù)。此時,除了創(chuàng)建和返回應用程序實例之外,它不會做太多事情 - 但是稍后我們將在此執(zhí)行服務器端路由匹配和數(shù)據(jù)預取邏輯: 

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

在服務端用vue-router分割代碼

與Vue實例一樣,也需要創(chuàng)建單例的vueRouter對象。對于每個請求,都需要創(chuàng)建一個新的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中實現(xiàn)服務器端路由邏輯,使用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 將可能的異步組件和鉤子函數(shù)解析完  
  8.     router.onReady(() => {  
  9.       const matchedComponents = router.getMatchedComponents();  
  10.       // 匹配不到的路由,執(zhí)行 reject 函數(shù),并返回 404  
  11.       if (!matchedComponents.length) {  
  12.         return reject('匹配不到的路由,執(zhí)行 reject 函數(shù),并返回 404');  
  13.       }  
  14.       // Promise 應該 resolve 應用程序實例,以便它可以渲染  
  15.       resolve(app);  
  16.     }, reject);  
  17.   });  

在服務端預拉取數(shù)據(jù)

在Vue服務端渲染,本質上是在渲染我們應用程序的"快照",所以如果應用程序依賴于一些異步數(shù)據(jù),那么在開始渲染過程之前,需要先預取和解析好這些數(shù)據(jù)。服務端Web Server Frame作為代理服務器,在服務端對接口服務發(fā)起請求,并將數(shù)據(jù)拼裝到全局Vuex狀態(tài)中。

另一個需要關注的問題是在客戶端,在掛載到客戶端應用程序之前,需要獲取到與服務器端應用程序完全相同的數(shù)據(jù) - 否則,客戶端應用程序會因為使用與服務器端應用程序不同的狀態(tài),然后導致混合失敗。

目前較好的解決方案是,給路由匹配的一級子組件一個asyncData,在asyncData方法中,dispatch對應的action。asyncData是我們約定的函數(shù)名,表示渲染組件需要預先執(zhí)行它獲取初始數(shù)據(jù),它返回一個Promise,以便我們在后端渲染的時候可以知道什么時候該操作完成。注意,由于此函數(shù)會在組件實例化之前調用,所以它無法訪問this。需要將store和路由信息作為參數(shù)傳遞進去:

舉個例子: 

  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,我們就調用這個方法。然后我們需要將解析完成的狀態(tài),附加到渲染上下文中。 

  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 將可能的異步組件和鉤子函數(shù)解析完  
  12.     router.onReady(() => {  
  13.       const matchedComponents = router.getMatchedComponents();  
  14.       // 匹配不到的路由,執(zhí)行 reject 函數(shù),并返回 404  
  15.       if (!matchedComponents.length) {  
  16.         return reject('匹配不到的路由,執(zhí)行 reject 函數(shù),并返回 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 現(xiàn)在已經填充入渲染應用程序所需的狀態(tài)。  
  28.         // 當我們將狀態(tài)附加到上下文,并且 `template` 選項用于 renderer 時,  
  29.         // 狀態(tài)將自動序列化為 `window.__INITIAL_STATE__`,并注入 HTML。  
  30.         context.state = store.state;  
  31.         resolve(app);  
  32.       }).catch(reject);  
  33.     }, reject);  
  34.   });  

客戶端托管全局狀態(tài)

當服務端使用模板進行渲染時,context.state將作為window.__INITIAL_STATE__狀態(tài),自動嵌入到最終的HTML 中。而在客戶端,在掛載到應用程序之前,store就應該獲取到狀態(tài),最終我們的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對象,通用的解決方案一般為判斷執(zhí)行環(huán)境,通過typeof window是否為'undefined',如果遇到必須使用location對象的地方用于獲取url中的相關參數(shù),在ctx對象中也可以找到對應參數(shù)。

  •  vue-router報錯Uncaught TypeError: _Vue.extend is not _Vue function,沒有找到_Vue實例的問題:

通過查看Vue-router源碼發(fā)現(xiàn)沒有手動調用Vue.use(Vue-Router);。沒有調用Vue.use(Vue-Router);在瀏覽器端沒有出現(xiàn)問題,但在服務端就會出現(xiàn)問題。對應的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路由的參數(shù)

由于hash路由的參數(shù),會導致vue-router不起效果,對于使用了vue-router的前后端同構應用,必須換為history路由。

  •  接口處獲取不到cookie的問題:

由于客戶端每次請求都會對應地把cookie帶給接口側,而服務端Web Server Frame作為代理服務器,并不會每次維持cookie,所以需要我們手動把

cookie透傳給接口側,常用的解決方案是,將ctx掛載到全局狀態(tài)中,當發(fā)起異步請求時,手動帶上cookie,如下代碼所示: 

  1. // createStore.js  
  2. // 在創(chuàng)建全局狀態(tài)的函數(shù)`createStore`時,將`ctx`掛載到全局狀態(tài)  
  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.     });  

當發(fā)起異步請求時,手動帶上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代理配置來解決跨域問題,而服務端作為代理服務器對接口發(fā)起異步請求時,不會讀取對應的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參數(shù)時,初始化時匹配不到對應路由的問題

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

  1. // entry-server.js  
  2. import { createApp } from './app';  
  3. export default context => {  
  4.   // 因為有可能會是異步路由鉤子函數(shù)或組件,所以我們將返回一個 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', '')); 

小結

本文為筆者通過對現(xiàn)有項目進行改造,給現(xiàn)有項目加上Vue服務端渲染的實踐過程的總結。

首先闡述了什么是Vue服務端渲染,其目的、本質及原理,通過在服務端使用Vue的虛擬DOM,形成初始化的HTML字符串,即應用程序的“快照”。帶來極大的性能優(yōu)勢,包括SEO優(yōu)勢和首屏渲染的極速體驗。之后闡述了Vue服務端渲染的基本用法,即兩個入口、兩個webpack配置,分別作用于客戶端和服務端,分別生成vue-ssr-client-manifest.json與vue-ssr-server-bundle.json作為打包結果。最后通過對現(xiàn)有項目的改造過程,包括對路由進行改造、數(shù)據(jù)預獲取和狀態(tài)初始化,并解釋了在Vue服務端渲染項目改造過程中的常見問題,幫助我們進行現(xiàn)有項目往Vue服務端渲染的遷移。 

責任編輯:龐桂玉 來源: segmentfault
相關推薦

2021-04-26 13:20:06

Vue服務端渲染前端

2024-01-16 08:05:53

2022-10-08 00:01:00

ssrvuereact

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

監(jiān)控服務平臺

2021-04-30 09:32:38

服務端渲染SSR

2017-08-16 10:57:25

H5HTML開發(fā)

2012-12-24 09:55:15

JavaJava WebJava優(yōu)化

2025-07-04 03:00:00

2025-07-01 08:24:10

2022-02-18 11:13:53

監(jiān)控架構系統(tǒng)

2023-03-22 18:31:10

Android頁面優(yōu)化

2017-11-22 14:24:21

Reactjavaajax

2017-11-30 09:20:06

2023-09-06 08:14:34

性能優(yōu)化模式

2024-05-27 00:00:00

PHP阿里云OSS

2016-03-18 09:04:42

swift服務端

2023-09-11 10:53:32

點贊
收藏

51CTO技術棧公眾號

欧美成在线视频| 67194成人在线观看| 麻豆精品视频| 一本一道人人妻人人妻αv| 伊人久久大香线蕉综合四虎小说| 欧美成人福利视频| 日韩无套无码精品| a视频在线播放| 91久色porny| 成人一区二区电影| 手机看片久久久| 中文精品电影| 中文日韩在线观看| 国产精品无码一区二区三| 成人18视频在线观看| 亚洲国产精品一区二区www| 先锋影音亚洲资源| 亚洲欧美一区二区三| 全国精品久久少妇| 97视频在线观看网址| 天天操天天操天天操天天操天天操| 久久精品论坛| 日韩欧美你懂的| 成人性生生活性生交12| 123区在线| 亚洲三级在线观看| 色噜噜色狠狠狠狠狠综合色一| 刘亦菲毛片一区二区三区| 久久99精品久久久久久国产越南| 欧美在线观看网站| 欧美日韩激情在线观看| 999国产精品视频| 亚洲欧美在线一区| 三级男人添奶爽爽爽视频| 日韩三级久久| 91精品欧美福利在线观看| 五月天婷婷激情视频| av日韩电影| 天天色综合成人网| 水蜜桃色314在线观看| 在线欧美三级| 亚洲精品美国一| 一区二区国产日产| 在线免费av网站| 中文文精品字幕一区二区| 日本不卡一区| 国产在线色视频| 久久久噜噜噜久噜久久综合| 精品国产电影| 亚洲 欧美 自拍偷拍| 成人av免费在线播放| 高清不卡日本v二区在线| 午夜精品久久久久久久96蜜桃| 国产又粗又猛又爽又黄91精品| 国产美女久久精品| 亚洲综合免费视频| 国产自产视频一区二区三区| 成人福利网站在线观看| 国产免费黄色片| 国产乱码精品1区2区3区| 亚洲综合一区二区不卡| 超碰免费在线97| 国产成人在线视频网址| 国产二区不卡| 全色精品综合影院| 久久精品一区蜜桃臀影院| 视频在线观看成人| 免费**毛片在线| 亚洲最新在线观看| 青青草精品视频在线| 是的av在线| 欧美最新大片在线看| 在线观看免费视频高清游戏推荐| 中文字幕综合| 精品国产一区二区精华| av无码一区二区三区| 韩日一区二区三区| 精品国产一区二区三区久久狼5月| 欧美激情图片小说| 最新国产乱人伦偷精品免费网站| 欧美一级高清免费| 91麻豆成人精品国产| 国产精品自拍av| 久久久影院一区二区三区 | 精品中文字幕久久久久久| 色婷婷在线影院| 99久久这里只有精品| 欧美激情精品久久久久久久变态 | 96久久精品| 日本在线丨区| 亚洲人成影院在线观看| 波多野结衣综合网| 狠狠久久伊人中文字幕| 欧美精品一区二区在线观看| 国产人妻大战黑人20p| 欧美91福利在线观看| 欧美在线激情网| 国产精品嫩草影院桃色| 91丨porny丨国产| 大桥未久一区二区| 精品国产免费人成网站| 日韩精品一区二区三区在线| 日韩av在线看免费观看| 欧美成熟视频| 国产精品久久二区| 搡老岳熟女国产熟妇| 欧美极品美女视频| 久久黄色片视频| 国产精品日本一区二区三区在线| 日韩黄色av网站| 国产这里有精品| 奇米精品一区二区三区在线观看| 国产精品久久久久久久久婷婷 | 日本一区二区三区久久久久久久久不| 伊人久久在线观看| 国产精品久久久久久久久免费高清 | 一本色道久久综合狠狠躁篇怎么玩| 男女性高潮免费网站| 天堂成人国产精品一区| 国产亚洲情侣一区二区无| 久久亚洲天堂| 欧美中文字幕亚洲一区二区va在线| 催眠调教后宫乱淫校园| 亚洲精品国产成人影院| 国产精品女视频| 欧美香蕉爽爽人人爽| 午夜精品在线看| 被黑人猛躁10次高潮视频| 日韩精品欧美激情一区二区| 国产不卡精品视男人的天堂| 欧洲av在线播放| 亚洲线精品一区二区三区| 超碰中文字幕在线观看| 天天做天天爱天天综合网2021| 国产精品mp4| 青青草免费在线视频| 午夜视频在线观看一区二区三区| 女人扒开双腿让男人捅 | 中文在线免费看视频| www国产成人免费观看视频 深夜成人网| 97干在线视频| 高清精品视频| 久久久久五月天| 亚洲精品视频网| 亚洲一区二区三区四区在线免费观看 | 精品国产百合女同互慰| 青青草在线观看视频| 国产精品一区三区| 97超碰在线视| 91精品啪在线观看国产爱臀 | 亚洲免费在线精品一区| 国产综合色在线观看| 中文字幕亚洲欧美在线| 中国女人一级一次看片| 中文字幕在线观看不卡视频| av亚洲天堂网| 亚洲一区在线| 成人女人免费毛片| 僵尸再翻生在线观看免费国语| 亚洲国产欧美精品| 4438国产精品一区二区| 亚洲国产高清在线观看视频| 欧美成人三级在线播放| 午夜av一区| 7777精品伊久久久大香线蕉语言 | 欧美中文在线视频| 国产一级二级三级在线观看| 欧美三级日韩三级国产三级| 欧美a级片免费看| 国产福利91精品一区| 被灌满精子的波多野结衣| 欧美日日夜夜| 国产精品久久久久久久久久东京| 午夜视频在线观看网站| 日韩小视频在线观看专区| 日本熟妇成熟毛茸茸| 久久久久国产精品厨房| 国产日韩欧美久久| 国内精品久久久久久久影视麻豆 | 五月天欧美精品| www在线观看免费视频| 久久精品国产免费看久久精品| 三年中文高清在线观看第6集| 亚洲视频一起| 日韩男女性生活视频| 国产不卡在线| 日韩极品精品视频免费观看| 亚洲一区精品在线观看| 亚洲成人tv网| 色www亚洲国产阿娇yao| 处破女av一区二区| 91视频免费版污| 国内精品久久久久久久影视麻豆| 色一情一乱一伦一区二区三区| 欧州一区二区三区| 国产成人免费91av在线| 午夜小视频在线观看| 亚洲人成电影网站色www| 国产激情久久久久久熟女老人av| 欧美午夜久久久| 黄页网站免费观看| 欧美国产一区二区在线观看| 国模无码视频一区| 激情综合网天天干| 亚洲熟妇av一区二区三区| 欧美影院一区| 亚洲精品久久久久久一区二区| 超碰成人福利| 成人欧美一区二区三区黑人| 中文字幕21页在线看| 欧美日韩国产成人在线观看| 国产一区二区影视| 日韩av在线精品| 99免费在线视频| 欧美日韩一区三区| 亚洲色成人www永久网站| 亚洲午夜视频在线| 天天鲁一鲁摸一摸爽一爽| 久久久久久久久久看片| 荫蒂被男人添免费视频| 国产精品一卡二| av中文字幕网址| 男女性色大片免费观看一区二区| 无码专区aaaaaa免费视频| 欧美片第1页综合| 一级黄色录像免费看| 日韩久久电影| 天堂资源在线亚洲视频| 亚洲涩涩av| 久久超碰亚洲| 国产成人在线中文字幕| 古典武侠综合av第一页| 精品三级久久久| 亚洲qvod图片区电影| 青草综合视频| 91久久久久久| 福利一区三区| 97超碰人人模人人爽人人看| 成人污版视频| 91网站免费看| 精品一区二区三区中文字幕视频| 成人国内精品久久久久一区| 日韩av懂色| 91精品视频免费看| 精品国产乱码久久久久久樱花| 国产免费一区二区三区香蕉精| 亚洲成人av观看| 国产精品丝袜久久久久久高清| 欧美最新精品| 国产欧美精品xxxx另类| 亚洲狼人在线| 亚洲影院色在线观看免费| 香蕉大人久久国产成人av| 99久久免费国| 久久久久久毛片免费看| 鲁丝一区鲁丝二区鲁丝三区| 久久93精品国产91久久综合| 神马影院我不卡| 天天色天天射综合网| 国产一级大片免费看| 亚洲国产精品第一区二区| 欧美女人性生活视频| 久久婷婷亚洲| 午夜激情av在线| 国产一区二区久久| 成人免费看片载| 91视频观看视频| 亚洲一级理论片| 一区二区成人在线视频| 91精品国产乱码久久久张津瑜| 色欲综合视频天天天| 91精品国产乱码久久久| 欧美mv日韩mv国产| 男人的天堂在线视频| 日韩中文字幕在线看| 黄色羞羞视频在线观看| 青青在线视频一区二区三区| 日韩第二十一页| 国产女人水真多18毛片18精品 | 国产成人精品一区二区三区免费| 亚洲一区二区三区乱码aⅴ蜜桃女| 亚洲国产欧美国产第一区| 精品久久久久久综合日本| 成人羞羞动漫| 免费看欧美一级片| 日本在线观看不卡视频| 少妇愉情理伦片bd| 久久精品一二三| 激情五月少妇a| 日本乱人伦一区| www.精品久久| 亚洲无线码在线一区观看| av片在线观看网站| 青青a在线精品免费观看| 激情综合婷婷| 日韩资源av在线| 国内自拍视频一区二区三区| 波多结衣在线观看| 99精品一区二区三区| www.xxxx日本| 色综合激情五月| 懂色av一区二区三区四区| 中文字幕久久久av一区| 成人影音在线| 国产在线日韩在线| 久久不见久久见国语| 欧美日韩福利在线| 韩国理伦片一区二区三区在线播放| 中文字幕精品久久久| 亚洲另类春色国产| 中文字幕久久网| 日韩激情第一页| 2020国产在线| 亚洲在线第一页| 91一区二区三区四区| 国产免费视频传媒| aa级大片欧美| 久久午夜无码鲁丝片| 7799精品视频| 日本暖暖在线视频| 国产精品久久久久91| 女人抽搐喷水高潮国产精品| 国产高清不卡无码视频| 精品一区二区三区免费观看 | 欧美精品一区二区三区精品| 男女超爽视频免费播放| 国产成人丝袜美腿| 一区二区三区四区五区| 欧美日韩小视频| 成年人在线看| 国产成+人+综合+亚洲欧洲| 日韩深夜福利| 波多野结衣家庭教师在线播放| 国产成人av影院| 久久久无码精品亚洲国产| 欧美一级黄色录像| 伊人春色在线观看| 91在线观看免费观看| 久久久精品久久久久久96| www.com久久久| 国产精品国产三级国产有无不卡| 成人小视频在线播放| 亚洲一级黄色av| 亚洲a∨精品一区二区三区导航| 欧美一区1区三区3区公司| 久久精品亚洲| 国内精品卡一卡二卡三| 欧美亚洲国产怡红院影院| aⅴ在线视频男人的天堂| 国产精品影片在线观看| 欧美gayvideo| 91视频福利网| 午夜日韩在线电影| 亚洲欧美综合在线观看| 欧洲中文字幕国产精品| 国产a久久精品一区二区三区| 青青草精品视频在线观看| 国产精品狼人久久影院观看方式| 一级特黄aaaaaa大片| 久色乳综合思思在线视频| 伊人久久大香线蕉av超碰| 青青草成人免费在线视频| 久久久国产午夜精品 | 欧美大片在线观看| av色在线观看| 欧美精品欧美精品| 蜜桃av一区二区三区| 成人免费黄色小视频| 欧美精品一区二区三区蜜桃| 色在线免费观看| 亚洲视频小说| 国产成人免费av在线| 国产精品视频久久久久久久| 亚洲午夜未满十八勿入免费观看全集| jvid一区二区三区| 中文字幕色呦呦| 26uuu色噜噜精品一区| 国产精品成人久久久| 欧美黄色小视频| 综合国产视频| 成人免费黄色av| 欧美性xxxxxxx| 国产原创视频在线观看| 国产主播一区二区三区四区| 日本亚洲天堂网| 青青草激情视频| 日韩风俗一区 二区| 亚洲tv在线| 99999精品视频| 亚洲欧美综合色| 亚欧在线观看视频| 成人写真福利网| 亚洲专区一区二区三区| 视频国产一区二区| 亚洲精品自产拍| 麻豆一二三区精品蜜桃| 99精品免费在线观看| 一区二区在线观看视频| 国产尤物视频在线| 国产精品区一区二区三在线播放 |