资讯专栏INFORMATION COLUMN

vue:服务端渲染技术

gnehc / 1369人阅读

摘要:在实际应用中,我们可以将回调函数拿到的拼接到模板中,下面看下的实现把转换模板字符串方法不断拼接模板字符串,用做存储,然后调用当通过完毕,执行传入方法支持传入实例和渲染完成后的回调函数。

服务端渲染:

简单说:比如说一个模板,数据是从后台获取的,如果用客户端渲染那么浏览器会先渲染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 {
      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(``, context.next)
    }
  } else {
    context.write(
      node.raw ? node.text : escape(String(node.text)),
      context.next
    )
  }
}
/**主要功能是把VNode对象渲染成dom元素。
 * 先判断是不是根元素,然后渲染开始开始标签,如果是自闭合标签直接写入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 = ``
  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 = 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就可以不断地读取到组件的数据,然后输出,这样就实现了流式服务端渲染技术。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/93079.html

相关文章

  • 珠峰前架构师培养计划

    摘要:公司的招聘要求都提到了至少熟悉其中一种前端框架,有前端工程化与模块化开发实践经验相关字眼。我们主要从端公众号移动端小程序三大平台进行前端的技术选型,并来说说选其技术的几大优势。技术的优势互联网前端大潮后,前端出现了大框架,分别是与。 1、技术选型的背景前端技术发展日新月异,互联网上出现的新型框架也比较多,如何让新招聘的人员...

    ccj659 评论0 收藏0
  • 细说 Vue 组件的服务渲染

    摘要:所以,这次就来聊聊组件的服务器端渲染。这种模式下,后端只提供接口,传统的服务器端路由模板渲染则都有层接管。这样,前端开发人员可以自由的决定哪些组件需要在服务器端渲染,哪些组件可以放在客户端渲染,前后端完全解耦,但又保留了服务器端渲染的功能。 细说 Vue 组件的服务器端渲染 声明:需要读者对 NodeJs、Vue 服务器端渲染有一定的了解 现在,前后端分离与客户端渲染已经成为前端开发的...

    reclay 评论0 收藏0
  • vue项目技术选型、开发工具、周边生态

    摘要:有目录结构书写方式组件集成项目构建等的约束,整个应用中是没有文件的,所有的响应都是动态渲染的,包括里面的元信息路径等。更多参考细说后端模板渲染客户端渲染中间层服务器端渲染开发工具开发时主要会用到的工具。 vue 前端项目技术选型、开发工具、周边生态 声明:这不是一篇介绍 Vue 基础知识的文章,需要熟悉 Vue 相关知识 主架构:vue, vue-router, vuex UI 框...

    Awbeci 评论0 收藏0
  • vue项目技术选型、开发工具、周边生态

    摘要:有目录结构书写方式组件集成项目构建等的约束,整个应用中是没有文件的,所有的响应都是动态渲染的,包括里面的元信息路径等。更多参考细说后端模板渲染客户端渲染中间层服务器端渲染开发工具开发时主要会用到的工具。 vue 前端项目技术选型、开发工具、周边生态 声明:这不是一篇介绍 Vue 基础知识的文章,需要熟悉 Vue 相关知识 主架构:vue, vue-router, vuex UI 框...

    enali 评论0 收藏0
  • 2017前技术总结:收获非浅,但仍需进步

    摘要:平台主要功能如下支持客户端渲染和服务端渲染微信登录鉴权页面组件增删改查,复制移动等图片上传微信文章一键复制等等动态组件的配置原理之后专门用一篇文章详细写吧持续集成这个其实也不算是项目,算是前端的工具。 2017年算是踏入真正的前端的一年,从实习到去年,说是前端的岗位,但却因为实习生的身份、公司技术不够等原因,一直停留在传统的html+css+jq,那时候感觉前端的世界在翻天覆地地变化,...

    txgcwm 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<