使用nuxt前,須要瞭解的vue ssr基礎

Vue SSR概述

什麼是SSR

Server Side Rendering(服務端渲染)css

SSR的優勢

  • 更好的 SEO
  • 更快的內容到達時間

SSR方案的權衡之處

  • 開發條件所限
  • 涉及構建設置和部署的更多要求
  • 更多的服務器端負載

Vue SSR基本使用

一個最簡單的示例(官方)

const Vue = require('vue')
const server = require('express')()
const renderer = require('vue-server-renderer').createRenderer()

server.get('*', (req, res) => {
  const app = new Vue({
    data: {
      url: req.url
    },
    template: `<div>訪問的 URL 是: {{ url }}</div>`
  })

  renderer.renderToString(app, (err, html) => {
    if (err) {
      res.status(500).end('Internal Server Error')
      return
    }
    res.end(`
      <!DOCTYPE html>
      <html lang="en">
        <head><title>Hello</title></head>
        <body>${html}</body>
      </html>
    `)
  })
})

server.listen(8080)
複製代碼

vue ssr的核心就是:html

  • 創建node服務
  • 將vue對象轉換字符串,返回

Vue SSR須要作哪些事情呢

1)避免單例狀態

當編寫純客戶端 (client-only) 代碼時,咱們習慣於每次在新的上下文中對代碼進行取值,但Node.js 服務器是一個長期運行的進程。vue

當咱們的代碼進入該進程時,它將進行一次取值並留存在內存中。這意味着若是建立一個單例對象,它將在每一個傳入的請求之間共享node

咱們須要爲每一個請求建立一個新的根 Vue 實例,==若是咱們在多個請求之間使用一個共享的實例,很容易致使交叉請求狀態污染==webpack

// vue實例工廠
const createApp = createApp (context) {
  return new Vue({
    data: {
      url: context.url
    },
    template: `<div>訪問的 URL 是: {{ url }}</div>`
  })
}

server.get('*', (req, res) => {
  const context = { url: req.url }
  // 每次請求都生成一個新的實例
  const app = createApp(context)

  renderer.renderToString(app, (err, html) => {
    res.end(html)
  })
})
複製代碼

2)構建項目

(1)經過webpack 來打包咱們的 Vue 應用程序git

  • 一般 Vue 應用程序是由 webpack 和 vue-loader 構建,而且許多 webpack 特定功能不能直接在 Node.js 中運行(例如經過 file-loader 導入文件,經過 css-loader 導入 CSS)。
  • 儘管 Node.js 最新版本可以徹底支持 ES2015 特性,咱們仍是須要轉譯客戶端代碼以適應老版瀏覽器。這也會涉及到構建步驟。

因此,對於客戶端應用程序和服務器應用程序,咱們都要使用 webpack 打包:github

  • 服務器須要【服務器 bundle】而後用於服務器端渲染(SSR)
  • 而【客戶端 bundle】會發送給瀏覽器,用於混合靜態標記
    image

(2)webpack源碼結構web

image

基本上和普通vue項目沒什麼區別,主要強調一下下面幾個文件vue-router

router.js 路由vuex

服務器代碼使用了一個 * 處理程序,它接受任意 URL。這容許咱們將訪問的 URL 傳遞到咱們的 Vue 應用程序中,而後對客戶端和服務器複用相同的路由配置!

因此官方建議使用vue-router

vue ssr路由採用history方式

// router.js
import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

export function createRouter () {
    return new Router({
        mode: 'history',
        routes: [
            { path: '/', component: () => import('@/components/Home') }
        ]
    })
}
複製代碼

app.js

app.js 建立根實例的工廠函數

import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'
import { createStore } from './store'
import { sync } from 'vuex-router-sync'
import './assets/common.css'
import '@node_modules/font-awesome/css/font-awesome.min.css'

export function createApp () {
    // 建立router 和 store 實例
    const router = createRouter()
    const store = createStore()

    sync(store, router)

    const app = new Vue({
        // 注入router 到跟 vue實例
        router,
        store,
        render: h => h(App)
    })

    return { app, router, store }
}
複製代碼

entry-server.js 服務器 entry 使用 default export 導出函數,並在每次渲染中重複調用此函數

import { createApp } from './app'

export default context => {
    // 有多是異步路由鉤子函數或組件,因此將返回一個Promise
    // 以便服務器可以等待全部的內容在渲染前 就已經準備就緒
    return new Promise((resolve, reject) => {
        const { app, router, store } = createApp()

        router.push(context.url)

        // 等到 router 將可能的異步組件和鉤子函數解析完
        router.onReady(() => {
            const matchedComponents = router.getMatchedComponents()
            if (!matchedComponents.length){
                return reject({ code: 404 })
            }
            // 對全部匹配的路由組件調用 asyncData
            Promise.all(matchedComponents.map(Component => {
                if(Component.asyncData){
                    return Component.asyncData({
                        store,
                        router: router.currentRoute
                    })
                }
            })).then(() => {
                // 在全部預取鉤子 resolve後, store已經填充渲染應用程序所需的狀態
                // 將狀態附加到上下文 
                // 狀態將自動序列化爲 `window.__INITIAL_STATE__`,並注入 HTML。
                context.state = store.state

                resolve(app)
            }).catch(reject)
        }, reject)
    })
}
複製代碼

router.onReady 是幹什麼用的

在全部的vue組件建立以前(包括App.vue)調用,這意味着它能夠解析全部的異步進入鉤子和路由初始化相關聯的異步組件。

這能夠有效確保服務端渲染時服務端和客戶端輸出的一致。

entry-client.js 客戶端 entry 只需建立應用程序,而且將其掛載到 DOM 中:

import Vue from 'vue'
import { createApp } from './app'

const { app, router, store } = createApp()

if (window.__INITIAL_STATE__) {
    store.replaceState(window.__INITIAL_STATE__)
}

router.onReady(() => {
  // 添加路由鉤子函數,用於處理 asyncData.
  // 在初始路由 resolve 後執行,
  // 以便咱們不會二次預取(double-fetch)已有的數據。
  // 使用 `router.beforeResolve()`,以便確保全部異步組件都 resolve。
  router.beforeResolve((to, from, next) => {
    const matched = router.getMatchedComponents(to)
    const prevMatched = router.getMatchedComponents(from)

    // 咱們只關心非預渲染的組件
    // 因此咱們對比它們,找出兩個匹配列表的差別組件
    let diffed = false
    const activated = matched.filter((c, i) => {
      return diffed || (diffed = (prevMatched[i] !== c))
    })

    if (!activated.length) {
      return next()
    }

    // 這裏若是有加載指示器 (loading indicator),就觸發

    Promise.all(activated.map(c => {
      if (c.asyncData) {
        return c.asyncData({ store, route: to })
      }
    })).then(() => {

      // 中止加載指示器(loading indicator)

      next()
    }).catch(next)
  })

  app.$mount('#app')
})
複製代碼

server.js

// server.js
const express = require('express')
const { createBundleRenderer } = require('vue-server-renderer')

const app = express()
function createRenderer (bundle, options) {
  return createBundleRenderer(bundle, Object.assign(options, {
    cache: new LRU({
      max: 1000,
      maxAge: 1000 * 60 * 15
    }),
    basedir: resolve('./dist'),
    runInNewContext: false
  }))
}

let renderer
let readyPromise
const templatePath = resolve('./src/index.template.html')
if (isProd) {
  // In production: create server renderer using template and built server bundle.
  // The server bundle is generated by vue-ssr-webpack-plugin.
  const template = fs.readFileSync(templatePath, 'utf-8')
  const bundle = require('./dist/vue-ssr-server-bundle.json')
  // The client manifests are optional, but it allows the renderer
  // to automatically infer preload/prefetch links and directly add <script>
  // tags for any async chunks used during render, avoiding waterfall requests.
  const clientManifest = require('./dist/vue-ssr-client-manifest.json')
  renderer = createRenderer(bundle, {
    template,
    clientManifest
  })
} else {
  // In development: setup the dev server with watch and hot-reload,
  // and create a new renderer on bundle / index template update.
  readyPromise = require('./build/setup-dev-server')(
    app,
    templatePath,
    (bundle, options) => {
      renderer = createRenderer(bundle, options)
    }
  )
}


function render (req, res) {
  const s = Date.now()

  res.setHeader("Content-Type", "text/html")
  res.setHeader("Server", serverInfo)

  const handleError = err => {
    if (err.url) {
      res.redirect(err.url)
    } else if(err.code === 404) {
      res.status(404).send('404 | Page Not Found')
    } else {
      // Render Error Page or Redirect
      res.status(500).send('500 | Internal Server Error')
      console.error(`error during render : ${req.url}`)
      console.error(err.stack)
    }
  }

  const context = {
    title: 'Vue HN 2.0', // default title
    meta: `<mata charset="utf-8">`,
    url: req.url
  }
  renderer.renderToString(context, (err, html) => {
    if (err) {
      return handleError(err)
    }
    res.send(html)
    if (!isProd) {
      console.log(`whole request: ${Date.now() - s}ms`)
    }
  })
}

app.get('*', isProd ? render : (req, res) => {
  readyPromise.then(() => render(req, res))
})

const port = process.env.PORT || 8080
app.listen(port, () => {
  console.log(`server started at localhost:${port}`)
})
複製代碼

這麼長,那server入口文件都作了什麼呢,總結一下:

  • 建立node服務
  • 定義render邏輯
  • 加入緩存邏輯(一般爲頁面緩存)
  • 返回頁面字符串

(3)webpack配置

webpack配置是一個很複雜的過程,不建議本身從頭搭建

咱們能夠參照一個網上的例子 github.com/mtgr1020/vu…

3)數據預取和狀態

首屏渲染依賴於一些異步數據,那麼在開始渲染過程以前,須要先預取和解析好這些數據

==另外一個須要關注的問題是在客戶端,在掛載 (mount) 到客戶端應用程序以前,須要獲取到與服務器端應用程序徹底相同的數據 - 不然,客戶端應用程序會由於使用與服務器端應用程序不一樣的狀態,而後致使混合失敗。==

爲了解決這個問題,獲取的數據須要位於視圖組件以外,即放置在專門的數據預取存儲容器

因而須要引入vuex

// store.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

import { fetchItem } from '../api'

export function createStore () {
    return new Vuex.Store({
        state: {
            items: {}
        },
        actions: {
            fetchItem ({ commit }, id) {
                return fetchItem(id).then(item => {
                    commit('setItem', { id, item })
                })
            }
        },
        mutations: {
            setItem (state, { id, item }) {
                Vue.set(state.items, id, item)
            }
        }
    })
}
複製代碼

而後再看 app.js:

// app.js
import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'
import { createStore } from './store'
import { sync } from 'vuex-router-sync'

export function createApp () {
  // 建立 router 和 store 實例
  const router = createRouter()
  const store = createStore()

  // 同步路由狀態(route state)到 store
  sync(store, router)

  // 建立應用程序實例,將 router 和 store 注入
  const app = new Vue({
    router,
    store,
    render: h => h(App)
  })

  // 暴露 app, router 和 store。
  return { app, router, store }
}
複製代碼

每一次訪問都要建立一個新的vue對象,同時應用新的router和store對象

帶有邏輯配置的組件

Vue SSR路由組件上暴露出一個自定義靜態函數 asyncData

注意:因爲此函數會在組件實例化以前調用,因此它沒法訪問 this。須要將 store 和路由信息做爲參數傳遞進去

<!-- Item.vue -->
<template>
  <div>{{ item.title }}</div>
</template>

<script>
export default {
  asyncData ({ store, route }) {
    // 觸發 action 後,會返回 Promise
    return store.dispatch('fetchItem', route.params.id)
  },
  computed: {
    // 從 store 的 state 對象中的獲取 item。
    item () {
      return this.$store.state.items[this.$route.params.id]
    }
  }
}
</script>
複製代碼

服務器端數據預取

在 entry-server.js 中,咱們能夠經過路由得到與 router.getMatchedComponents() 相匹配的組件,若是組件暴露出 asyncData,咱們就調用這個方法。而後咱們須要將解析完成的狀態,附加到渲染上下文(render context)中。

// entry-server.js
import { createApp } from './app'

export default context => {
  return new Promise((resolve, reject) => {
    const { app, router, store } = createApp()

    router.push(context.url)

    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents()
      if (!matchedComponents.length) {
        return reject({ code: 404 })
      }

      // 對全部匹配的路由組件調用 `asyncData()`
      Promise.all(matchedComponents.map(Component => {
        if (Component.asyncData) {
          return Component.asyncData({
            store,
            route: router.currentRoute
          })
        }
      })).then(() => {
        // 在全部預取鉤子(preFetch hook) resolve 後,
        // 咱們的 store 如今已經填充入渲染應用程序所需的狀態。
        // 當咱們將狀態附加到上下文,
        // 而且 `template` 選項用於 renderer 時,
        // 狀態將自動序列化爲 `window.__INITIAL_STATE__`,並注入 HTML。
        context.state = store.state

        resolve(app)
      }).catch(reject)
    }, reject)
  })
}
複製代碼

客戶端數據預取

1)在路由導航以前解析數據

使用此策略,應用程序會等待視圖所需數據所有解析以後,再傳入數據並處理當前視圖。好處在於,能夠直接在數據準備就緒時,傳入視圖渲染完整內容,可是若是數據預取須要很長時間,用戶在當前視圖會感覺到"明顯卡頓"。所以,若是使用此策略,建議提供一個數據加載指示器

2)匹配要渲染的視圖後,再獲取數據

此策略將客戶端數據預取邏輯,放在視圖組件的 beforeMount 函數中。當路由導航被觸發時,能夠當即切換視圖,所以應用程序具備更快的響應速度。然而,傳入視圖在渲染時不會有完整的可用數據。所以,對於使用此策略的每一個視圖組件,都須要具備條件加載狀態。

這兩種策略是根本上不一樣的用戶體驗決策,應該根據你建立的應用程序的實際使用場景進行挑選。可是不管你選擇哪一種策略,當路由組件重用(同一路由,可是 params 或 query 已更改,例如,從 user/1 到 user/2)時,也應該調用 asyncData 函數

客戶端激活

Vue 在瀏覽器端接管由服務端發送的靜態 HTML,使其變爲由 Vue 管理的動態 DOM 的過程。

在 entry-client.js 中,咱們用下面這行掛載(mount)應用程序:

因爲服務器已經渲染好了 HTML,咱們顯然無需將其丟棄再從新建立全部的 DOM 元素。相反,咱們須要"激活"這些靜態的 HTML,而後使他們成爲動態的(可以響應後續的數據變化)。

<div id="app" data-server-rendered="true">
複製代碼

data-server-rendered 特殊屬性,讓客戶端 Vue 知道這部分 HTML 是由 Vue 在服務端渲染的,而且應該以激活模式進行掛載

// 強制使用應用程序的激活模式
app.$mount('#app', true)
複製代碼

在開發模式下,Vue 將推斷客戶端生成的虛擬 DOM 樹 (virtual DOM tree),是否與從服務器渲染的 DOM 結構 (DOM structure) 匹配。若是沒法匹配,它將退出混合模式,丟棄現有的 DOM 並從頭開始渲染。在生產模式下,此檢測會被跳過,以免性能損耗

SSR項目和普通h5項目的區別(重點)

1)服務器上的數據響應

  • 數據進行響應式的過程在服務器上是多餘的,因此默認狀況下禁用。
  • 禁用響應式數據,還能夠避免將「數據」轉換爲「響應式對象」的性能開銷

2)組件生命週期鉤子函數

  • 僅會執行beforeCreate 和 created兩個鉤子函數
  • 避免在這兩個生命週期中執行全局反作用的代碼,如setInterval

3)訪問特定平臺(Platform-Specific) API

  • 如:window 或 document
  • 對於僅瀏覽器可用的 API,一般方式是,在「純客戶端 (client-only)」的生命週期鉤子函數中惰性訪問 (lazily access) 它們。
  • 考慮到若是第三方 library 不是以上面的通用用法編寫,則將其集成到服務器渲染的應用程序中,可能會很棘手。你可能要經過模擬 (mock) 一些全局變量來使其正常運行,但這只是 hack 的作法,而且可能會干擾到其餘 library 的環境檢測代碼。

4)自定義指令

大多數自定義指令直接操做 DOM,所以會在服務器端渲染 (SSR) 過程當中致使錯誤

  • 推薦使用組件做爲抽象機制,並運行在「虛擬 DOM 層級(Virtual-DOM level)」(例如,使用渲染函數(render function))
  • 若是你有一個自定義指令,但不是很容易替換爲組件,則能夠在建立服務器 renderer 時,使用 directives 選項所提供"服務器端版本(server-side version)"。

5)服務端沒法自動補齊標籤

使用「SSR + 客戶端混合」時,須要瞭解的一件事是,瀏覽器可能會更改的一些特殊的 HTML 結構。例如,當你在 Vue 模板中寫入:

<table>
  <tr><td>hi</td></tr>
</table>
複製代碼

瀏覽器會在

內部自動注入 ,然而,因爲 Vue 生成的虛擬 DOM (virtual DOM) 不包含 ,因此會致使沒法匹配。爲可以正確匹配,請確保在模板中寫入有效的 HTML。

6)避免單例狀態

7)複雜的webpack配置

總結:那本身寫SSR都需作哪些事情呢

1)自行維護node服務

  • 建立node服務,如express服務
  • 請求相關內容及容錯處理
  • 各部分銜接邏輯

2)本身維護vue

  • 複雜的webpack配置
  • 路由邏輯
  • 模板和拼接邏輯(render部分)
  • 先後端數據一致處理

3)基礎優化部分要本身處理

ok,以上就最近看的一些vue ssr基礎

由於最近一直在用nuxt開發項目,具體vue ssr都作了什麼不清楚。 主要目的仍是基礎掃盲。

相關文章
相關標籤/搜索