一、渲染函数API(模板的代替方案)

渲染函数就派上用场了。

注:Vue 的渲染系统基于“虚拟 DOM”;虚拟 DOM (Virtual DOM,简称 VDOM) 是一种编程概念,意为将目标所需的 UI 通过数据结构“虚拟”地表示出来,保存在内存中,然后将真实的 DOM 与之保持同步。这个概念是由  React 率先开拓,随后在许多不同的框架中都有不同的实现,当然也包括 Vue。

  Vue 组件渲染管线:

    ① 编译:Vue 模板被编译为渲染函数:即用来返回虚拟 DOM 树的函数。

    ② 挂载:运行时渲染器调用渲染函数,遍历返回的虚拟 DOM 树,并基于它创建实际的 DOM 节点。

    ③ 更新:当一个依赖发生变化后,副作用会重新运行,这时候会创建一个更新后的虚拟 DOM 树。运行时渲染器遍历这棵新树,将它与旧树进行比较,然后将必要的更新应用到真实 DOM 上去。

1、基本用法
  1)Vue创建 Vnodes(Vnodes 必须唯一)

    h() 是 hyperscript 的简称——意思是“能生成 HTML (超文本标记语言) 的 JavaScript”(createVnode())。这个名字来源于许多虚拟 DOM 实现默认形成的约定。

import { h } from 'vue'

const vnode = h(
  'div', // type
  { id: 'foo', class: 'bar' }, // props
  [
    /* children */
  ]
)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

    补充:h() 函数示例:

h() 函数示例

// 除了类型必填以外,其他的参数都是可选的
h('div')
h('div', { id: 'foo' })

// attribute 和 property 都能在 prop 中书写;Vue 会自动将它们分配到正确的位置
h('div', { class: 'bar', innerHTML: 'hello' })

// 像 `.prop` 和 `.attr` 这样的的属性修饰符;可以分别通过 `.` 和 `^` 前缀来添加
h('div', { '.name': 'some-name', '^width': '100' })

// 类与样式可以像在模板中一样;用数组或对象的形式书写
h('div', { class: [foo, { bar }], style: { color: 'red' } })

// 事件监听器应以 onXxx 的形式书写
h('div', { onClick: () => {} })

// children 可以是一个字符串
h('div', { id: 'foo' }, 'hello')

// 没有 props 时可以省略不写
h('div', 'hello')
h('div', [h('span', 'hello')])

// children 数组可以同时包含 vnodes 与字符串
h('div', ['hello', h('span', 'hello')])

//上面的 vnode 得到的结果:
//const vnode = h('div', { id: 'foo' }, [])
//vnode.type // 'div'
//vnode.props // { id: 'foo' }
//vnode.children // []
//vnode.key // null
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  2)声明渲染函数

  我们可以使用render选项来声明渲染函数,render()函数可以访问同一个this组件实例。

import { h } from 'vue'

export default {
  data() {
    return {
      msg: 'hello'
    }
  },
  render() {
    return h('div', this.msg)
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
2、案例:
  1)v-if

    模板:

<div>
  <div v-if="ok">yes</div>
  <span v-else>no</span>
</div>
  • 1.
  • 2.
  • 3.
  • 4.

    解析后:

h('div', [this.ok ? h('div', 'yes') : h('span', 'no')])
  • 1.

    JSX 语法( JSX 是 JavaScript 的一个类似 XML 的扩展):

<div>{this.ok ? <div>yes</div> : <span>no</span>}</div>
  • 1.
  2)v-for

    模板:

<ul>
  <li v-for="{ id, text } in items" :key="id">
    {{ text }}
  </li>
</ul>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

    解析后:

h(
  'ul',
  this.items.map(({ id, text }) => {
    return h('li', { key: id }, text)
  })
)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

    JSX 语法( JSX 是 JavaScript 的一个类似 XML 的扩展):

<ul>
  {this.items.map(({ id, text }) => {
    return <li key={id}>{text}</li>
  })}
</ul>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

  3)更多案例见: 渲染函数案例

二、服务端渲染 (SSR)

1、为什么要使用“服务端渲染 (SSR)”:

  与客户端的单页应用 (SPA) 相比,SSR具有以下好处:

  1)更快的首屏加载:服务端渲染的 HTML 无需等到所有的 JavaScript 都下载并执行完成之后才显示,所以你的用户将会更快地看到完整渲染的页面。这通常可以带来更高的 核心 Web 指标评分、更好的用户体验,而对于那些“首屏加载速度与转化率直接相关”的应用来说,这点可能至关重要。(可以使用 SSG 静态站点生成技术来代替)

 2)统一的心智模型:就是指合并前后端的“技术栈”,可以统一使用nodejs技术栈进行开发。你可以使用相同的语言以及相同的声明式、面向组件的心智模型来开发整个应用,而不需要在后端模板系统和前端框架之间来回切换。

  3)更好的 SEO:搜索引擎爬虫可以直接看到完全渲染的页面。

2、SSR 渲染的弊端:

 1)增大运营成本:服务端需要安装Node.js 服务运行环境;增加了服务器的负载。

 2)开发中的限制:浏览器端特定的代码只能在某些生命周期钩子中使用;一些外部库可能需要特殊处理才能在服务端渲染的应用中运行。

 3)跨请求状态污染:如果使用了 响应式 API 的简单状态管理模式,该模式会在一个 JavaScript 模块的根作用域中声明共享的状态,这是一种单例模式——即在应用的整个生命周期中只有一个响应式对象的实例。这在纯客户端的 Vue 应用中是可以的(因为对于浏览器的每一个页面访问,应用模块都会重新初始化)然而,在 SSR 环境下,该模式会造成状态污染(应用模块通常只在服务器启动时初始化一次。同一个应用模块会在多个服务器请求之间被复用)。

  4)js获取到的时间与客户端不一致:获取到的时间为服务器时区的时间,不会跨时区。

  5)不能访问平台上浏览器的特有 API:这些API在nodejs环境中是不被支持的。

  6)Html解析兼容性降低:存在错误的Html在nodejs环境中的解析完不会被浏览器优化,SSR模式下返回的页面效果和浏览器解析的会有些许不同。

3、官方提供的更通用的 SSR应用框架

  1)Nuxt:一个构建于 Vue 生态系统之上的全栈框架,它为编写 Vue SSR 应用提供了丝滑的开发体验。更棒的是,你还可以把它当作一个静态站点生成器来用!我们强烈建议你试一试。

  2)Quasar:是一个基于 Vue 的完整解决方案,它可以让你用同一套代码库构建不同目标的应用,如 SPA、SSR、PWA、移动端应用、桌面端应用以及浏览器插件。除此之外,它还提供了一整套 Material Design 风格的组件库。

  3)Vite SSR:Vite 提供了内置的  Vue 服务端渲染支持,但它在设计上是偏底层的。如果你想要直接使用 Vite,可以看看  vite-plugin-ssr,一个帮你抽象掉许多复杂细节的社区插件。

4、手写一个原生SSR应用

  1)创建SSR应用(服务器nodejs环境中编程)

  ① 创建一个新的文件夹,cd 进入

  ② 执行 npm init -y

  ③ 在 package.json 中添加 "type": "module" 使 Node.js 以  ES modules mode 运行

  ④ 执行 npm install vue

  ⑤ 创建一个 example.js 文件:

// 此文件运行在 Node.js 服务器上
import { createSSRApp } from 'vue'
// Vue 的服务端渲染 API 位于 `vue/server-renderer` 路径下
import { renderToString } from 'vue/server-renderer'

const app = createSSRApp({
  data: () => ({ count: 1 }),
  template: `<button @click="count++">{{ count }}</button>`
})

renderToString(app).then((html) => {  // 接收一个 Vue 应用实例作为参数,返回一个 Promise,当 Promise resolve 时得到应用渲染的 HTML。
  console.log(html)
})
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

  ⑥ 接着运行:> node example.js

  ⑦ 它应该会在命令行中打印出如下内容:

<button>1</button>
  • 1.

  ⑧ 执行 npm install express

  ⑨ 创建下面的 server.js 文件:

import express from 'express'
import { renderToString } from 'vue/server-renderer'
import { createSSRApp } from 'vue'

const server = express()

server.get('/', (req, res) => {
  const app = createSSRApp({
    data: () => ({ count: 1 }),
    template: `<button @click="count++">{{ count }}</button>`
  })

  renderToString(app).then((html) => {
    res.send(`
    <!DOCTYPE html>
    <html>
      <head>
        <title>Vue SSR Example</title>
      </head>
      <body>
        <div id="app">${html}</div>
      </body>
    </html>
    `)
  })
})

server.listen(3000, () => {
  console.log('ready')
})
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.

  ⑩ 最后,执行 node server.js;访问 http://localhost:3000,可以看到应用的静态页面。

    如果你点击该按钮,你会发现数字并没有改变。这段 HTML 在客户端是完全静态的,因为我们没有在浏览器中加载 Vue。

  2)客户端激活应用(服务器nodejs环境中编程)

    在激活过程中,Vue 会创建一个与服务端完全相同的应用实例,然后将每个组件与它应该控制的 DOM 节点相匹配,并添加 DOM 事件监听器。为了在激活模式下挂载应用,我们应该使用  createSSRApp() 而不是 createApp()

  ① 单独的应用创建逻辑文件 app.js

// app.js (在服务器和客户端之间共享)
import { createSSRApp } from 'vue'

export function createApp() {
  return createSSRApp({
    data: () => ({ count: 1 }),
    template: `<button @click="count++">{{ count }}</button>`
  })
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

  ② 客户端入口导入通用代码,创建应用并执行挂载:

// client.js
import { createApp } from './app.js'

createApp().mount('#app')
  • 1.
  • 2.
  • 3.
  • 4.

  ③ 服务器在请求处理函数中使用相同的应用创建逻辑:

import express from 'express';
import { renderToString } from 'vue/server-renderer';
import { createApp } from './app.js';

const server = express();

server.get('/', (req, res) => {
  const app = createApp();

  renderToString(app).then((html) => {
    res.send(`
    <!DOCTYPE html>
    <html>
      <head>
        <title>Vue SSR Example</title>
        <!-- 通过在 HTML 外壳中添加 Import Map 以支持在浏览器中使用 import * from 'vue' -->
        <script type="importmap">
          {
            "imports": {
              "vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
            }
          }
        </script>
        <!-- 将 <script type="module" src="/client.js"></script> 添加到 HTML 外壳以加载客户端入口文件。 -->
        <script type="module" src="/client.js"></script>
      </head>
      <body>
        <div id="app">${html}</div>
      </body>
    </html>
    `);
  });
});

server.use(express.static('.'));  // 添加 server.use(express.static('.')) 来托管客户端文件。

server.listen(3000, () => {
  console.log('ready');
});
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.

 

作者:꧁执笔小白꧂