天天看點

追求極緻的使用者體驗ssr(基于vue的服務端渲染)

首先這篇部落格并不是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效果:

追求極緻的使用者體驗ssr(基于vue的服務端渲染)

43ms就擷取了所有的資料,mobile端流量大概是電腦*10的時間,(其實4g狀态下和電腦wifi也是不相上下的,幾乎上下波動都在1m~2m左右),假設我們手機網速很一般,時間*10就是0.4s的時間,也就是說在使用者首次通路過我們頁面的情況下,隻要手機中有緩存我們可以一最快的資料打開頁面,即使使用者在首次通路,我們的時間也可以控制在1s就能讓使用者看到大體的網頁架構,而不是看了一秒的白屏!因為使用者擷取的其實就是node緩存的html,這個就跟在網上看一個html的專題頁面沒什麼差別!我們節省的時間也就說我們client去請求接口的時間和架構渲染的時間,這個白屏的時間我們相當于緩存在了node中,既不占用記憶體,也能讓使用者有一個更高的使用者體驗。

追求極緻的使用者體驗ssr(基于vue的服務端渲染)