1. 程式人生 > >追求極致的用戶體驗ssr(基於vue的服務端渲染)

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

這樣的 console ports modules 為我 返回 意思 mage nop

首先這篇博客並不是ssr建議教程,需要ssr入門的我建議也不要搜索博客了,因為官網給出了詳細的入門步驟,只需要step by step就可以了,這篇博客的意義是如何使用ssr,可能不同的人有不同的意見,我舍棄了ssr中的vuex和vue-router增加了redis,serverfetch等等實現了適合自己公司的業務,個人認為並不是所有的東西都值得吸收,對我來說我能用到的只是ssr將vue生成一個html和對應的js。

蝦面我們來看看什麽是服務端渲染?

官網給出的解釋:

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

服務器渲染的 Vue.js 應用程序也可以被認為是"同構"或"通用",因為應用程序的大部分代碼都可以在服務器和客戶端上運行。

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

舉個例子:

正常情況下我們使用vue或react框架瀏覽器獲取所有資源後做的事情

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

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

ssr渲染

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

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

上手有一定難度!!!

首先你需要熟悉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中,既不占用內存,也能讓用戶有一個更高的用戶體驗。

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