vue:服務端渲染技術

服務端渲染:

簡單說:好比說一個模板,數據是從後臺獲取的,若是用客戶端渲染那麼瀏覽器會先渲染htmlcss,而後再經過jsajax去向後臺請求數據再更改渲染。就是在前端再用Node建個後臺,把首屏數據加載成一個完整的頁面在node建的後臺渲染好,瀏覽器拿到的就是一個完整的dom樹。根據項目打開地址,路由指到哪一個頁面就跳到哪。css

服務端比起客戶端渲染頁面的優勢:
  • 首屏渲染速度更快

客戶端渲染的一個缺點是,用戶第一次訪問頁面,此時瀏覽器沒有緩存,須要先從服務端下載js,而後再經過js操做動態添加dom並渲染頁面,時間較長。而服務端渲染的規則是,用戶第一次訪問瀏覽器能夠直接解析html文檔並渲染頁面,並屏渲染速度比客戶端渲染更快。html

  • SEO

服務端渲染可讓搜索引擎更容易讀取頁面的meta信息,以及其它SEO相關信息,大大增長了網站在搜索引擎中的速度。前端

  • 減小HTTP請求

服務端渲染能夠把一些動態數據在首次渲染時同步輸出到頁面,而客戶端渲染須要經過AJAX等手段異步獲取這些數據,這樣就至關於多了一次HTTP請求。vue

普通服務端渲染

vue提供了renderToString接口,能夠在服務端把vue組件渲染成模板字符串,咱們先看下用法:node

benchmarks/ssr/renderToString.js

const Vue = require('../../dist/vue.runtime.common.js')
const createRenderer = require('../../packages/vue-server-renderer').createRenderer
const renderToString = createRenderer().renderToString
const gridComponent = require('./common.js') // vue支行時的代碼,不包括編譯部分

console.log('--- renderToString --- ')
const self = (global || root)
self.s = self.performance.now()

renderToString(new Vue(gridComponent), (err, res) => {
  if (err) throw err
  // console.log(res)
  console.log('Complete time: ' + (self.performance.now() - self.s).toFixed(2) + 'ms')
  console.log()
})

這段代碼是支行在node.js環境中的,主要依賴vue.common.js,vue-server-render.其中vue.common.jsvue運行時代碼,不包括編譯部分:vue-server-render對外提供createRenderer方法,renderToStringcreateRenderer方法返回值的一個屬性,它支持傳入vue實例和渲染完成後的回調函數,這裏要注意,因爲引用的是隻包括運行時的vue代碼,不包括編譯部分,因此其中err表示是否出錯,result表示dom字符串。在實際應用中,咱們能夠將回調函數拿到的result拼接到模板中,下面看下renderToString的實現:ajax

src/server/create-renderer.js

const render = createRenderFunction(modules, directives, isUnaryTag, cache)
return {
    renderToString (
      component: Component,
      context: any,
      cb: any
    ): ?Promise<string> {
      if (typeof context === 'function') {
        cb = context
        context = {}
      }
      if (context) {
        templateRenderer.bindRenderFns(context)
      }

      // no callback, return Promise
      let promise
      if (!cb) {
        ({ promise, cb } = createPromiseCallback())
      }

      let result = ''
      const write = createWriteFunction(text => {
        result += text
        return false
      }, cb)
      try {
        //  render:把component轉換模板字符串str ,write方法不斷拼接模板字符串,用result作存儲,而後調用next,當component經過render完畢,執行done傳入resut,
        render(component, write, context, () => {
          if (template) {
            result = templateRenderer.renderSync(result, context)
          }
          cb(null, result)
        })
      } catch (e) {
        cb(e)
      }

      return promise
    }
}

renderToString方法支持傳入vue實例component和渲染完成後的回調函數done。它定義了result變量,同時定義了write方法,最後執行render方法。整個過程比較核心的就是render方法:promise

src/server/render.js

return function render (
    component: Component,
    write: (text: string, next: Function) => void,
    userContext: ?Object,
    done: Function
  ) {
    warned = Object.create(null)
    const context = new RenderContext({
      activeInstance: component,
      userContext,
      write, done, renderNode,
      isUnaryTag, modules, directives,
      cache
    })
    installSSRHelpers(component)
    normalizeRender(component)
    renderNode(component._render(), true, context)
  }
/**
 * // render其實是執行了renderNode方法,並把component._render()方法生成的vnode對象做爲參數傳入。
 * @param node 先判斷node類型,若是是component Vnode,則根據這個Node建立一個組件的實例並調用_render方法做爲當前node的childVnode,而後遞歸調用renderNode
 * @param isRoot 若是是一個普通dom Vnode對象,則調用renderElement渲染元素,不然就是一個文本節點,直接用write方法。
 * @param context
 */
function renderNode (node, isRoot, context) {
  if (node.isString) {
    renderStringNode(node, context)
  } else if (isDef(node.componentOptions)) {
    renderComponent(node, isRoot, context)
  } else if (isDef(node.tag)) {
    renderElement(node, isRoot, context)
  } else if (isTrue(node.isComment)) {
    if (isDef(node.asyncFactory)) {
      // async component
      renderAsyncComponent(node, isRoot, context)
    } else {
      context.write(`<!--${node.text}-->`, context.next)
    }
  } else {
    context.write(
      node.raw ? node.text : escape(String(node.text)),
      context.next
    )
  }
}
/**主要功能是把VNode對象渲染成dom元素。
 * 先判斷是否是根元素,而後渲染開始開始標籤,若是是自閉合標籤<img/>直接寫入write,再執行next方法 
 * 若是沒有子元素,又不是閉合標籤,經過write寫入開始-閉合標籤。再執行next.dom渲染完畢
 * 不然就經過write寫入開始標籤,接着渲染全部的子節點,再經過write寫入閉合標籤,最後執行next
 * @param context
 */
function renderElement (el, isRoot, context) {
  const { write, next } = context

  if (isTrue(isRoot)) {
    if (!el.data) el.data = {}
    if (!el.data.attrs) el.data.attrs = {}
    el.data.attrs[SSR_ATTR] = 'true'
  }

  if (el.functionalOptions) {
    registerComponentForCache(el.functionalOptions, write)
  }

  const startTag = renderStartingTag(el, context)
  const endTag = `</${el.tag}>`
  if (context.isUnaryTag(el.tag)) {
    write(startTag, next)
  } else if (isUndef(el.children) || el.children.length === 0) {
    write(startTag + endTag, next)
  } else {
    const children: Array<VNode> = el.children
    context.renderStates.push({
      type: 'Element',
      rendered: 0,
      total: children.length,
      endTag, children
    })
    write(startTag, next)
  }
}

流式服務端渲染

普通服務器有一個痛點——因爲渲染是同步過程,因此若是這個app很複雜的話,可能會阻塞服務器的event loop,同步服務器在優化不當時甚至會給客戶端得到內容的速度帶來負面影響。vue提供了renderToStream接口,在渲染組件時返回一個可讀的stream,能夠直接pipeHTTP Response中,流式渲染能確保在服務端響應度,也能讓用戶更快地得到渲染內容。renderToStream源碼:瀏覽器

benchmarks/ssr/renderToStream.js

const Vue = require('../../dist/vue.runtime.common.js')
const createRenderer = require('../../packages/vue-server-renderer').createRenderer
const renderToStream = createRenderer().renderToStream
const gridComponent = require('./common.js')

console.log('--- renderToStream --- ')
const self = (global || root)
const s = self.performance.now()

const stream = renderToStream(new Vue(gridComponent))
let str = ''
let first
let complete
stream.once('data', () => {
  first = self.performance.now() - s
})
stream.on('data', chunk => {
  str += chunk
})
stream.on('end', () => {
  complete = self.performance.now() - s
  console.log(`first chunk: ${first.toFixed(2)}ms`)
  console.log(`complete: ${complete.toFixed(2)}ms`)
  console.log()
})

這段代碼也是一樣運行在node環境中的,與rendetToString不一樣,它會把vue實例渲染成一個可讀的stream。源碼演示的是監聽數據的讀取,並記錄讀取數據的時間
,而在實際應用中,咱們也能夠這樣寫:緩存

const Vue = require('../../dist/vue.runtime.common.js')
const createRenderer = require('../../packages/vue-server-renderer').createRenderer
const renderToStream = createRenderer().renderToStream
const gridComponent = require('./common.js')

const stream = renderToStream(new Vue(gridComponent))
app.use((req,res)=>{
    stream.pipe(res)
})

若是代碼運行在Express框架中,則能夠經過app.use方法建立middleware,而後直接把stream piperes中,這樣客戶端就能很快地得到渲染內容了,下面看下renderToStream的實現:服務器

src/server/create-renderer.js

  const render = createRenderFunction(modules, directives, isUnaryTag, cache)
  
  return {
    ...

    renderToStream (component: Component,context?: Object): stream$Readable {
        if (context) {
            templateRenderer.bindRenderFns(context)
        }
        const renderStream = new RenderStream((write, done) => {
            render(component, write, context, done)
        })
        if (!template) {
            return renderStream
        } else {
            const templateStream = templateRenderer.createStream(context)
            renderStream.on('error', err => {
                templateStream.emit('error', err)
            })
            renderStream.pipe(templateStream)
            return templateStream
        }
   }

renderToStream傳入一個Vue對象實例,返回的是一個RenderStream對象的實例,咱們來看下RenderStream對象的實現:

src/server/create-stream.js

// 繼承了node的可讀流stream.Readable;必須提供一個_read方法從底層資源抓取數據。經過Push(chunk)調用_read。向隊列插入數據,push(null)結束
export default class RenderStream extends stream.Readable {
  buffer: string; // 緩衝區字符串
  render: (write: Function, done: Function) => void; // 保存傳入的render方法,最後分別定義了write和end方法
  expectedSize: number;  // 讀取隊列中插入內容的大小
  write: Function;
  next: Function;
  end: Function;
  done: boolean;

  constructor (render: Function) {
    super() // super調用父類的構造函數
    this.buffer = ''
    this.render = render
    this.expectedSize = 0
    
    // 首先把text拼接到buffer緩衝區,而後判斷buffer.length,若是大於expecteSize,用this.text保存                
    //text,同時調用this.pushBySize把緩衝區內容推入讀取隊列中。
    this.write = createWriteFunction((text, next) => {
      const n = this.expectedSize
      this.buffer += text
      if (this.buffer.length >= n) {
        this.next = next
        this.pushBySize(n)
        return true // we will decide when to call next
      }
      return false
    }, err => {
      this.emit('error', err)
    })

     // 渲染完成後;咱們應該把最後一個緩衝區推掉.
    this.end = () => {
      this.done = true // 標誌組件的渲染已經完畢,而後調用push將緩衝區剩餘內容推入讀取隊列中
      this.push(this.buffer) //把緩衝區剩餘內容推入讀取隊列中
    }
  }

  //截取buffer緩衝區前n個長度的數據,推入到讀取隊列中,同時更新buffer緩衝區,刪除前n條數據
  pushBySize (n: number) {
    const bufferToPush = this.buffer.substring(0, n)
    this.buffer = this.buffer.substring(n)
    this.push(bufferToPush)
  }

  tryRender () {
    try {
      this.render(this.write, this.end) // 開始渲染組件,在初始化RenderStream方法時傳入。
    } catch (e) {
      this.emit('error', e)
    }
  }

  tryNext () {
    try {
      this.next() // 繼續渲染組件
    } catch (e) {
      this.emit('error', e)
    }
  }

  _read (n: number) {
    this.expectedSize = n
    // 可能最後一個塊增長了緩衝區到大於2 n,這意味着咱們須要經過屢次讀取調用來消耗它
    // down to < n.
    if (isTrue(this.done)) { // 若是爲true,則表示渲染完畢;
      this.push(null) //觸發結束信號
      return
    }
    if (this.buffer.length >= n) { // 緩衝區字符串長度足夠,把緩衝區內容推入讀取隊列。
      this.pushBySize(n)
      return
    }
    if (isUndef(this.next)) {
         this.tryRender() //false,開始渲染組件
    } else {
         this.tryNext() //繼續渲染組件
    }
  }
}

回顧一下,首先調用renderToStream(new Vue(option))建立好stream對象後,經過stream.pipe()方法把數據發送到一個WritableStream中,會觸發RenderToStream內部_read方法的調用,不斷把渲染的組件推入讀取隊列中,這個WritableStream就能夠不斷地讀取到組件的數據,而後輸出,這樣就實現了流式服務端渲染技術。

相關文章
相關標籤/搜索