vue:服务端衬着手艺

服务端衬着:

简单说:比如说一个模板,数据是从背景猎取的,假如用客户端衬着那末浏览器会先衬着htmlcss,然后再经由历程jsajax去处背景要求数据再变动衬着。就是在前端再用Node建个背景,把首屏数据加载成一个完全的页面在node建的背景衬着好,浏览器拿到的就是一个完全的dom树。依据项目翻开地点,路由指到哪一个页面就跳到哪。

服务端比起客户端衬着页面的长处:

  • 首屏衬着速率更快

客户端衬着的一个瑕玷是,用户第一次接见页面,此时浏览器没有缓存,须要先从服务端下载js,然后再经由历程js操纵动态增加dom并衬着页面,时候较长。而服务端衬着的规则是,用户第一次接见浏览器能够直接剖析html文档并衬着页面,并屏衬着速率比客户端衬着更快。

  • SEO

服务端衬着能够让搜索引擎更轻易读取页面的meta信息,以及别的SEO相干信息,大大增加了网站在搜索引擎中的速率。

  • 削减HTTP要求

服务端衬着能够把一些动态数据在初次衬着时同步输出到页面,而客户端衬着须要经由历程AJAX等手腕异步猎取这些数据,如许就相当于多了一次HTTP要求。

一般服务端衬着

vue供应了renderToString接口,能够在服务端把vue组件衬着成模板字符串,我们先看下用法:

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的完成:

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要领:

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就可以够不停地读取到组件的数据,然后输出,如许就完成了流式服务端衬着手艺。

    原文作者:Waxiangyu
    原文地址: https://segmentfault.com/a/1190000013488610
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞