Step-by-step,打造屬於本身的vue ssr

筆者最近在和小夥伴對vue項目進行ssr的升級,本文筆者將根據一個簡單拿vue cli構建的客戶端渲染的demo一步一步的教你們打造本身的ssr,拙見勿噴哈。javascript

what ? why ?

What ?

在學習一項新技術的時候咱們首先要了解一下他是什麼。這裏引用官網的一句話:html

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

Why ?

知道是什麼後咱們要知道這項技術對咱們現有的項目有什麼好處,簡單總結一下:vue

  • 利於SEO
  • 利於首屏渲染,vue-ssr會把拿到的數據渲染成html,不用等待所有的js資源都完成下載才顯示咱們的頁面。

do ? how to do ?

這裏咱們用vue-cli去簡單的作一個vue客戶端渲染的demo,具體過程就不作贅述了。java

demo地址: https://github.com/LNoe-lzy/v...

這裏咱們根據以前寫好的客戶端渲染的demo來一步一步的改形成服務端渲染。先甩下demo連接:node

demo地址: https://github.com/LNoe-lzy/v...

First step:理解下原理

先附一張鎮文之圖,官網的構建流程:
構建步驟webpack

這些都是個啥?

  • app.js用來構建咱們的vue實例,這個實例會跑在客戶端和服務端;
  • server entry是咱們的服務端entry,用來導出一個函數在每次請求中調用,也作組件匹配和初始化渲染數據的獲取。webpack會將其打包成server bundle;
  • client entry是咱們客戶端的entry,用來掛載咱們的vue實例到指定的dom元素上。webpack會將其打包成client bundle;

這些都作了啥?

  • 首先咱們的entry-server會獲取到當前router匹配到的組件,調用組件上asyncData方法,將數據存到服務端的vuex中,而後服務端vuex中的這些數據傳給咱們的context。
  • Node.js服務器經過renderToString將須要首屏渲染的html字符串send道咱們的客戶端上,這其中混入了window.__INITIAL_STATE__ 用來存儲咱們服務端vuex的數據。
  • 而後entry-client,此時服務端渲染時候拿到的數據寫入客戶端的vuex中。
  • 最後就是客戶端和服務端的組件作diff了,更新狀態更新的組件。

Secound step:main.js的改造

爲了不單例的影響,咱們須要在每一個請求都建立一個新的vue的實例,從而避免請求狀態的污染,咱們來封裝一個createApp的工廠函數:git

import Vue from 'vue'
import App from './App'

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

Third step:組件的改造

跑在服務端的Vue中全部的生命週期鉤子函數中,只有 beforeCreate 和 created 會在服務器端渲染過程當中被調用,而其餘的鉤子在客戶端纔會被調用,畢竟咱們的服務端是沒法執行dom操做的,因此咱們要在路由匹配的組件上定義一個靜態函數,這個函數要作的也很簡單,就是去dispatch咱們的action從而異步獲取數據:github

import { mapActions } from 'vuex'

export default {
  asyncData ({ store }) {
    return store.dispatch('getNav')
  },
  methods: {
    ...mapActions([
      'getList'
    ])
  }
  // ...
}

Fourth step:router和store的改造

一樣爲了不單例的影響,咱們也須要用工廠函數封裝咱們的router和storeweb

// router
export function createRouter () {
  return new Router({
    mode: 'history',
    routes: []
  })
}

// store
export function createStore () {
  return new Vuex.Store({
    state: {},
    actions,
    mutations
  })
}

Fifth step:兩個entry

根據構建流程圖咱們還須要webpack去構建兩個bundle,服務端根據Server Bundle去作ssr,瀏覽器根據Client Bundle去混合靜態標記。vue-router

爲此咱們在src目錄下新建兩個文件,entry-server.js 和 entry-client.js。前者在每次渲染中須要重複調用,執行服務端的路有匹配和數據預取邏輯。後者負責掛載DOM節點,以及先後端vuex數據狀態的同步。

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

export default context => {
  // 可能爲異步組件,返回一個promise
  return new Promise((resolve, reject) => {
    const { app, router, store } = createApp()
    const { url } = context
    const { fullPath } = router.resolve(url).route

    if (fullPath !== url) {
      return reject(new Error(`error: ${fullPath}`))
    }
    router.push(url)
    // 須要等到的異步組件和鉤子函數解析完
    router.onReady(() => {
      // 獲取匹配到的組件
      const matchedComponents = router.getMatchedComponents()
      if (!matchedComponents.length) {
        return reject({ code: 404 })
      }
      Promise.all(matchedComponents.map(({ asyncData }) => asyncData && asyncData({
        store,
        route: router.currentRoute
      }))).then(() => {
        // 將預取的數據從store中取出放到context中
        context.state = store.state
        resolve(app)
      }).catch(reject)
    }, reject)
  })
}

這裏咱們須要注意兩點,一個是咱們的數據預取是調用組件的asyncData方法,因此須要Promise.all來保證拿到所有的預渲染數據;另外一點是context.state = store.state,這時候服務端拿到的預渲染數據會封在window.__INITIAL_STATE__中經過node服務器send到客戶端。

import Vue from 'vue'
import { createApp } from './main'
const { app, router, store } = createApp()

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

// 也是處理異步組件
router.onReady(() => {
  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))
    })

    const asyncDataHooks = activated.map(c => c.asyncData).filter(_ => _)
    if (!asyncDataHooks.length) {
      return next()
    }

    Promise.all(asyncDataHooks.map(hook => hook({ store, route: to })))
      .then(() => {
        next()
      })
      .catch(next)
  })
  console.log('router ready')
  app.$mount('#app')
})

看到window.__INITIAL_STATE__咱們就能夠知道了客戶端拿到了預取的數據,而後去存到客戶端的vuex中,這也就是你們常常談論的經過vuex實現先後端的狀態共享。

至於vuex是否是必須的,固然不是(尤大issuse有說),題外話,筆者也實現了沒有vuex的版本哦。

Sixth step:編寫服務端代碼

服務端框架咱們採用Express(固然Koa2也是能夠的):

const express = require('express')
const fs = require('fs')
const path = require('path')
const {
  createBundleRenderer
} = require('vue-server-renderer')

const app = express()
const resolve = file => path.resolve(__dirname, file)
// 生成服務端渲染函數
const renderer = createBundleRenderer(require('./dist/vue-ssr-server-bundle.json'), {
  runInNewContext: false,
  template: fs.readFileSync(resolve('./index.html'), 'utf-8'),
  clientManifest: require('./dist/vue-ssr-client-manifest.json'),
  basedir: resolve('./dist')
})

// 引入靜態資源
app.use(express.static(path.join(__dirname, 'dist')))
// 分發路由

app.get('*', (req, res) => {
  res.setHeader('Content-Type', 'text/html')

  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 SSR demo', // default title
    url: req.url
  }
  renderer.renderToString(context, (err, html) => {
    console.log('render')
    if (err) {
      return handleError(err)
    }
    res.send(html)
  })
})

app.on('error', err => console.log(err))
app.listen(3000, () => {
  console.log(`vue ssr started at localhost:3000`)
})

經過觀察localhost咱們能夠很清楚的發現,經過服務端send過來的html字符串僅包括咱們根據數據預取渲染出來的dom結構以及服務端混入的window.__INITIAL_STATE__
服務端渲染html

經過Performance咱們也能夠看出在採用了ssr的應用中,咱們的首屏渲染並不依賴於客服端的js文件了,這就大大加快了首屏的渲染速度,畢竟傳統的SPA應用時須要拿到客戶端js文件後才能夠進行虛擬dom的構建以及數據的獲取工做才渲染頁面的。

ssr

不僅是題外話

  • vue-router不是必須的,不用router其實作個vue的preRender就能夠了,徹底不必作ssr;
  • vuex不是必須的,vuex是實現咱們客戶端和服務端的狀態共享的關鍵,咱們能夠不使用vuex,可是咱們得去實現一套數據預取的邏輯;

不使用vuex其實很頭疼,但又有了點靈感,平時咱們在開發項目的時候是如何處理組件間通訊的,一個是vuex,另外一個是EventBus,EventBus就是個Vue的實例啊,數據存這裏不也行麼?

在此筆者的思路是:建立一個Vue的實例充當倉庫,那麼咱們能夠用這個實例的data來存儲咱們的預取數據,而用methods中的方法去作數據的異步獲取,這樣咱們只須要在須要預取數據的組件中去調用這個方法就能夠了。demo很簡單,戳這裏

還有一個思路是在筆者學習的時候看別人博客學到的:只用了vuex的store和一些支持服務端渲染的api,沒有走action、mutation那套,而是將數據手動寫入state,爲了表示對別人博客的尊重,細節就請轉到做者的博客吧,戳這裏


寫在最後

本文經過一個簡單的客戶端渲染demo來一步一步的交你們如何搭建屬於本身的ssr程序,文筆拙略還請你們諒解了。

不過學習雖好,可是細節到使用上,你們仍是斟酌是否適合在本身的項目中。

多謝支持!

相關文章
相關標籤/搜索