追求極致的用戶體驗ssr(基於vue的服務端渲染)

首先這篇博客並非ssr建議教程,須要ssr入門的我建議也不要搜索博客了,由於官網給出了詳細的入門步驟,只須要step by step就能夠了,這篇博客的意義是如何使用ssr,可能不一樣的人有不一樣的意見,我捨棄了ssr中的vuex和vue-router增長了redis,serverfetch等等實現了適合本身公司的業務,我的認爲並非全部的東西都值得吸取,對我來講我能用到的只是ssr將vue生成一個html和對應的js。

蝦面咱們來看看什麼是服務端渲染?

官網給出的解釋:css

Vue.js 是構建客戶端應用程序的框架。默認狀況下,能夠在瀏覽器中輸出 Vue 組件,進行生成 DOM 和操做 DOM。然而,也能夠將同一個組件渲染爲服務器端的 HTML 字符串,將它們直接發送到瀏覽器,最後將靜態標記"混合"爲客戶端上徹底交互的應用程序。html

服務器渲染的 Vue.js 應用程序也能夠被認爲是"同構"或"通用",由於應用程序的大部分代碼均可以在服務器和客戶端上運行。前端

ssr的服務端渲染大體的意思就是vue在客戶端將標籤渲染成的整個html片斷的工做在服務端完成,服務端造成的html片斷直接返回給客戶端這個過程就叫作服務端渲染。vue

舉個例子:

正常狀況下咱們使用vue或react框架瀏覽器獲取全部資源後作的事情node

1.瀏覽器加載全部資源(html,css,js,img...)-->2.cdn-->3.返回資源-->4.vue請求server獲取業務數據-->5.返回數據渲染成html片斷-->6.css渲染片斷成一個網頁-->用戶react

沒錯這裏面最耗時的時間是4,5這兩步驟,h5請求serverapi的過程自己除了服務器的限制,還有用戶網絡,寬帶等等諸多限制,而且當頁面邏輯過多,數據過於繁瑣的狀況下,咱們的vue在client端渲染也會成爲性能瓶頸,最明顯的就是一些電商公司的首頁,商品詳情頁等等。測試這個過程在優化前大概須要500ms左右,即便通過優化也須要200ms左右,這個時間幾乎是難以接受的,而且咱們在用戶網絡不是很好的狀況下,若是咱們serverfetch的過程須要500ms,再加上其餘的各類請求資源,手機性能等等,用戶就要看到將近一秒的白屏時間,這個明顯是不好的用戶體驗。jquery

ssr渲染webpack

1.瀏覽器加載全部資源(html,css,js,img...)-->2.cdn-->3.返回資源-->4.css渲染片斷成一個網頁-->用戶ios

這裏咱們不僅是用ssr,咱們也須要把全部的html片斷緩存在node內存中,這個html片斷必定只能放在內存中,不要想着要一小片redis內存和其餘server端共用,由於併發亮極大的狀況下出得流量有可能直接讓redis掛掉。而這個性能放在node的內存中幾乎能夠忽略不計。咱們若是須要存的時間很短的話,那麼咱們放在內存中並無問題,由於實時數據刷新五秒可能就換一分內存數據,可是若是咱們長時間去存這個備份可能就會出現數據不一致的問題,咱們都知道通常線上部署node服務最少須要三臺服務,而每一臺的數據咱們很難保證一隻,用戶a可能兩個請求一個打到nodeA服務器上,另外一個打到nodeB服務器上,這樣就會出問題。這種內存只適合存那種時間很短的緩存,若是咱們須要存幾個小時那種咱們還要考慮redis,由於咱們須要數據實時同步,可是咱們只能存儲serverfetch的數據,而不能存整個html。一個ssr的時間大概是5ms左右,一臺服務器的1s承受量就是1000/5*60% = 120個請求,也就是說咱們三臺服務器的請求併發量大概能承受360-400左右,超出就要紅色預警了!!這對那些併發量極大的項目並不合適,全部咱們中和考慮,這個無非就是時間換空間,空間換時間的遊戲。咱們能夠選擇增長緩存,也能夠添加服務器!es6

上手有必定難度!!!

首先你須要熟悉webpack2,vue,vuex,vue-router(vue的全家桶),node,express。個別邏輯還須要redis等等後端資源,若是你想作到極致(併發狀況下不穿透),咱們還須要瞭解鎖的概念,同時咱們也須要知道如何處理避免死鎖,事務等等機制!

用戶體驗優化,如何作到更快的讓用戶看到頁面呢?

首先最開始考慮的就是模版渲染,咱們知道咱們在本地打開本地html文件的時候幾乎是瞬間就能看到頁面的全部內容,那麼咱們有可能讓用戶直接看到一個用戶頁面麼?

首先我想到的就是node的各類渲染模版,ejs?jade?咱們能夠經過node server端去fetch咱們後臺的全部數據,以後把數據拼成一個html直接給用戶,這樣確實能實現咱們想要的東西,但不是最好的,首先咱們目前市面上的三大框架vue,react,angular咱們須要摒棄,咱們還要把全部的業務邏輯拆分,由於有了框架的限制,這些都是不現實的,而且咱們直接用server端的模版對於咱們前端開發來講效率也是極低。

不論是react仍是vue都有基於本身框架的服務端渲染。

今天咱們來講一下基於vue的ssr

ssr官網

https://ssr.vuejs.org/zh/

ssr的好處官網已經給出,最吸引個人只有兩點

1.更好的 SEO

2.更快的內容到達時間(time-to-content)

基本上按照官網step by step均可以寫一個很小的vuessr的demo一些基礎細節咱們不去介紹了。官網給出的ssr大概的流程

vue-router在ready以前fetch全部vue的業務數據調用asyncData鉤子,以後獲取的數據去更新vuex以後咱們渲染vue組件的時候組件獲取全部的vuex的store數據,拼接成一個html字符串。

首先咱們的須要兩份webpack打包入口,一份去壓縮client,一份去壓縮server。

client的一端是new一個vue的實例而後經過app.$mount('#app')將其掛載到 DOM 

server的一端咱們須要返回一個promise,咱們能夠在這裏fetchpro的數據放在這個promise裏面return,這裏咱們能夠new一個promise,也可使用fetch,或vue的axios。(注意咱們全部須要在服務端渲染的數據都要在這裏獲取到,而後再client端也要獲取到,咱們全部的數據不能放在vue中的mounted中獲取,由於這樣和客戶端渲染沒什麼區別,vue暴漏的這個環境支持window也就是說這個位置實際上是client端作的,也就是在ssr全部功能實現以後在執行,這樣咱們和以前就沒有任何區別了)

client,和server須要import你的vue全部組件,以後就會吧全部的vue組件渲染成你須要的html,這裏官網給的例子須要大家去使用vue的全家桶,而我剛剛說的serverfetch就不須要使用vue-router和vuex,咱們已經把全部須要的數據在ssr以前就直接放進vue中,經過props的形式傳給組件

app.js

export function createApp (obj) {
    const app = new Vue({
      render: h => h(App.default, obj)
    })
	return { app }
}

  咱們在client和server壓縮入口就把全部內容傳入組件,這樣咱們就能夠實現把內容數據傳到組件裏面,實現vue的ssr

個人webpack:client

var webpack = require('webpack');
var ExtractTextPlugin = require("extract-text-webpack-plugin");
const VueSSRServerPlugin = require('vue-server-renderer/client-plugin')
const isProd = process.env.NODE_ENV === 'production'
console.log('NODE_ENV--->', process.env.NODE_ENV)
module.exports = {
    //頁面入口文件配置
    entry: {
        index : './build/index/entry-client.js'
    },
    target: 'web',
    devtool: isProd?false:'#source-map',
    //入口文件輸出配置
    output: {
        path: 'dist/index',
        filename: 'client_index_[hash].js',
      },
    module: {
      noParse: /es6-promise\.js$/, // avoid webpack shimming process
      rules: [
        {
        	test: /\.vue$/, 
      	  loader: 'vue-loader'
        },
        {
          test: /\.js$/,
          loader: 'babel-loader',
          exclude: /node_modules/
        },
        {
          test: /\.(png|jpg|gif|svg)$/,
          loader: 'url-loader',
          options: {
            limit: 10000,
            name: '[name].[ext]?[hash]'
          }
        },
        {
          test: /\.css$/,
          use: ['vue-style-loader', 'css-loader']
        },
        {
          test: /\.es6$/,
          loader: "babel-loader",
          exclude: /node_modules/
        },
      ]
    },
    resolve: {
      alias: {
        'vue$': 'vue/dist/vue.common.js',
      }
    },
    externals: {
      "jquery": "$",
      'Vue': true,
      'Swiper': true,
      'VueLazyload': true,
      '$': true
    },
    plugins: [
        // new webpack.optimize.UglifyJsPlugin({
        //   compress: { warnings: isProd?false:true }
        // }),
        // new ExtractTextPlugin({
        //   filename: 'common.[chunkhash].css'
        // }),
        new webpack.DefinePlugin({
          'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
          'process.env.VUE_ENV': '"server"'
        }),
        new VueSSRServerPlugin()
    ]
};

  server:

var webpack = require('webpack');
var ExtractTextPlugin = require("extract-text-webpack-plugin");
const nodeExternals = require('webpack-node-externals')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
const isProd = process.env.NODE_ENV === 'production'
console.log('NODE_ENV--->', process.env.NODE_ENV)
module.exports = {
    //頁面入口文件配置
    entry: {
        index : './build/index/entry-server.js'
    },
    target: 'node',
    devtool: isProd?false:'#source-map',
    //入口文件輸出配置
    output: {
        path: 'dist/index',
        filename: 'server-bundle.js',
        libraryTarget: 'commonjs2'
      },
    module: {
      noParse: /es6-promise\.js$/, // avoid webpack shimming process
      rules: [
        {
          test: /\.vue$/, 
        	loader: 'vue-loader'
        },
        {
          test: /\.js$/,
          loader: 'babel-loader',
          exclude: /node_modules/
        },
        {
          test: /\.(png|jpg|gif|svg)$/,
          loader: 'url-loader',
          options: {
            limit: 10000,
            name: '[name].[ext]?[hash]'
          }
        },
        {
          test: /\.css$/,
          use: ['vue-style-loader', 'css-loader']
        },
        {
          test: /\.es6$/,
          loader: "babel-loader",
          exclude: /node_modules/
        },
      ]
    },
    resolve: {
      // alias: {
      //   'vue$': 'vue/dist/vue.js' // 'vue/dist/vue.common.js' for webpack 1
      // }
    },
    externals: nodeExternals({
    // do not externalize CSS files in case we need to import it from a dep
        whitelist: /\.css$/,
        "jquery": "$",
        'Vue': true,
        'Swiper': true,
        'VueLazyload': true,
        '$': true,
    }),
    plugins: [
        // new webpack.optimize.UglifyJsPlugin({
        //   compress: { warnings: isProd?false:true }
        // }),
        // new ExtractTextPlugin({
        //   filename: 'common.[chunkhash].css'
        // }),
        new webpack.DefinePlugin({
          'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
          'process.env.VUE_ENV': '"server"'
        }),
        new VueSSRServerPlugin()
    ]
};

  可是上面的形式咱們須要每次訪問頁面都須要請求後臺server的接口,這樣的接口徹底是不必的,試想一下若是首頁咱們每秒都有500個請求,那麼咱們server端就先擋雨請求了1000次api,這樣的消耗毫無疑問是過大的,那麼咱們須要怎麼去作到接口的緩存呢?

  咱們使用的node是express框架,而後在進入/index的時候咱們去fetch後臺server的數據,而後咱們能夠把數據傳到client和server的config中,而不是每次在client,server中請求,而後咱們每次內存緩存失效咱們再去重新fetch後臺server,這樣咱們假設每秒500個請求量,咱們在node 端緩存5s,一共是2500個請求數量,咱們在node其實只是請求了一次後臺的server以後每次拿的node內存去返回用戶html,這種效果很定是極好的,也極大的緩解了咱們後臺server的壓力!

我作的公司首頁遷移ssr效果:

43ms就獲取了全部的數據,mobile端流量大概是電腦*10的時間,(其實4g狀態下和電腦wifi也是不相上下的,幾乎上下波動都在1m~2m左右),假設咱們手機網速很通常,時間*10就是0.4s的時間,也就是說在用戶首次訪問過咱們頁面的狀況下,只要手機中有緩存咱們能夠一最快的數據打開頁面,即便用戶在首次訪問,咱們的時間也能夠控制在1s就能讓用戶看到大致的網頁框架,而不是看了一秒的白屏!由於用戶獲取的其實就是node緩存的html,這個就跟在網上看一個html的專題頁面沒什麼區別!咱們節省的時間也就說咱們client去請求接口的時間和框架渲染的時間,這個白屏的時間咱們至關於緩存在了node中,既不佔用內存,也能讓用戶有一個更高的用戶體驗。

相關文章
相關標籤/搜索