大家好,我是 17

SSR 特别指支持在 Node.js 中运行相同应用程序的前端框架(例如 React、Preact、Vue 和 Svelte),将其预渲染成 HTML,最后在客户端 hydrating。

下面是关于 vue3 预渲染和服务端渲染的示例讲解。

本示例虽然是用 hotpack 工具,但原理是相通的,与工具无关。

示例项目

hotpack 为服务端渲染(SSR)提供了内建支持。下面的范例包含了Vue3 的 SSR 示例,可以作为本指南的参考。

  • vue3 示例项目

源码结构

一个典型的 SSR 应用应该有如下的源文件结构

vue3预渲染和服务端渲染(同构)示例讲解_开发环境

index 是一个页面的文件夹,里面包含index页面需要的内容

  • vue vue组件
  • index.b.js 客户端专用入口 b 是 browser的第一个字母
  • index.html html模板
  • index.s.js 服务端专用入口 s 是 server的第一个字母

情景体验

hotpack 可以进行浏览器渲染,预渲染和服务端渲染,支持多页,单页,可以自由选择

为了体验全部功能,我们先准备一下环境

  1. 安装 node 最低版本要求 14.0
  2. 安装 hotpack 并下载 vue3 ssr 示例项目
npm install -g hotpack
git clone https://github.com/duhongwei/hotpack-tpl-vue3.git my-app
cd my-app/main
npm install

环境准备完毕,请保证在 my-app 项目的 main 目录执行后续的命令

在 hotpack 项目中 多页和单页并没有区别,单页只是在多页的基础上增加了路由而已。先从简单的多页说起

多页浏览器渲染

hotpack dev

执行hotpack dev会启动开发环境,默认使用 3000 端口

hotpack dev -p 3001 指定为 3001端口

hotpack 的默认命令是 dev 所以 hotpack dev 也可以写成 hotpack

打开浏览器输入网址 localhost:3000 显示如下内容

vue3预渲染和服务端渲染(同构)示例讲解_数据_02

在页面上右键,选择 显示网页源代码 页面上只有空的 div,内容是浏览器请求到 js 后填充的

<div id='app' pre-ssr></div>

pre-ssr 表示 可以 使用预渲染。但现在还没有起作用。

多页预渲染

预渲染不像服务器渲染那样即时编译 HTML,它只在构建时为了特定的路由生成特定的几个静态页面。

在开发环境,即使有 pre-ssr 标记,预渲染也是不开启的(开发环境配置文件一般配置为不开启,因为在我们开发页面的时候,是不需要预渲染的)在开发环境要启用预渲染很简单

hotpack dev -r

默认还是 3000 端口, localhost:3000 打开页面后,在页面上右键,选择 显示网页源代码 我们清楚的看到页面已经渲染好了。

<div id='app'><div class="index"><h1>Hotpack Vue3 Multi Page Egxample</h1>...

vue3 与 vue2 不同,在 div 上并没有渲染标记

多页服务端渲染

与预渲染不同,服务端渲染会时时编译生成 HTML,根据路径和数据实时渲染页面。

打开 page/index/index.html

<div id='app' pre-ssr ></div>

修改 pre-ssr 为 ssr

<div id='app' ssr ></div>

ssr 表示 可以 使用服务端渲染。在开发环境,即使有 ssr 标记,和预渲染原因一样,服务端渲染也是不开启的。开发环境默认都走浏览器渲染,这样开发效率较高。在命令行加上 -r 开启服务端渲染

hotpack dev -r

默认 3000 端口, localhost:3000 打开页面后,在页面上右键,选择 显示网页源代码

<div id='app' ssr></div>

还是空的div , 明明已经启用服务端渲染了呀!

其实是没错的,因为现在的server是 hotpack 的 server 并不是服务端的 server,hotpack 的 server 并没有根据数据时时编译的功能。在开发环境,编译好的文件都发到了 dev 目录,为了避免受到干扰,把 dev目录copy到上一级,我们可以到这里查看效果

cp -r dev ../
cd ../dev
npm install
node index.js

查看源文件,果然已经渲染好了。

<div id='app'><div class="index"><h1>Hotpack Vue3 Multi Page Egxample</h1>...

能不能实时编译呢?当然是可以的。

只不过...

为了简化项目,数据现在是固定的,直接在 api 函数里返回。所以就不能看到实时编译的效果了。如果大家有兴趣话,我会再写一篇时时请求真实数据的例子。

多页体验完了,下面我们体验下单页

单页浏览器渲染

打开浏览器输入网址 localhost:3000/single.html 显示如下内容

vue3预渲染和服务端渲染(同构)示例讲解_数据_03

查看源文件

<div id='app' ssr></div>

并没有执行服务端渲染,原因和预渲染一样,开发环境需要加参数 -r 开启,并copy到上层目录查看效果

hotpack dev -rs
cp -r dev ../
cd ../dev
npm install
node index.js

hotpack dev -s -s 参数在开发环境会阻止启动 server

查看原文件,内容在服务端已经渲染好了。

情景逻辑

浏览器渲染

浏览器渲染的入口在 index.b.js

import './index.html=>index.html'

当前目录下的 index.html 作为模板,经过转换,发布到 /index.html web 目录,因为路径都是以 web 根目录为基准,所以 / 一律省略不写。

多页浏览器渲染

查看 page/index/index.b.js

if (window.__state__) {
  store.initState = window.__state__
}
else {
  store.dispatch('init')
}

初始化数据。如果已经预渲染或服务端渲染,初始化的数据会保存在 window._state 中和页面一起发送到浏览器中。也就是客户端 hydrating。

多页比较简单,不需要路由。

store.dispatch('user') 是为了和 store.dispatch('init') 做对比。多页的初始化数据是可以预见的,所以把它们放在一起处理。并不是所有的数据都适合用同步的方式,异步数据可以单独请求。

单页浏览器渲染

单页在感觉上只是多了一个路由,但是复杂度可是增加了好多。如果window._state有数据,

if (window.__state__) {
  storeInfo.state = window.__state__
}

如果没有window._state并不能象多页那样直接发一个 store.dispatch('init') 完事。单页是有客户端路由的,需要哪些数据是由路由决定的,一个想法是根据路由信息直接获取数据,但更好的做法是把获取数据的方法放在模块中。

page/single/vue/index

export default {
  name: 'index',
  ssr(store) {
    return store.dispatch('index')
  },
  ...

在 index.b.js 中 根据路由找到所有相关的组件,再一一触发组件内的 ssr 方法

router.beforeResolve((to) => {
  ...
  to.matched.forEach(record => {
    const components = Object.values(record.components)
    components.forEach(item => {
      if (item.ssr) {
        item.ssr(store)
      }
    })
  })
})

最后要注意一个问题,如果window._state有数据,并不需要重复请求数据了,在 store 中判断一下就好

page/single/js/store.js

actions: {
    ...
    async index({ commit, state }) {

      if (state.index) return state.index

      const data = await getIndex()
      commit('index', data)
      return data
    }
 }

.b.js 结尾的文件只在浏览器中运行

预渲染和服务端渲染

预渲染不需要服务端支持,是编译工具完成的。预渲染的结果是不变的。对于没有数据,或数据不常变化的页面非常适合预渲染。

服务端渲染的页面是时时变化的,是真正的动态页面。

预渲染和服务端渲染都是以 index.s.js 为入口

import './index.html=>index.html'

这句除了指明模板路转换之外,还指明,这个 html 文件是渲染入口是 index.s.js。 用这种声明的方式来指明 html 与 js 的关联,是为了灵活性。html模板在源码中放在哪里没有关系,js 放在哪里也没有关系,随你所愿。

hotpack 在编译的时候,发现这句声明并把 html 和 js 的关联信息保存起来,方便后面查用。

.s.js 结尾的文件只在服务端运行

多页预渲染和多页服务端渲染

export default async function () {
    let store = Vuex.createStore(storeInfo)
    const state = await store.dispatch('init')
    let app = await init(component)
    app.use(store)
    return {
        app,
        state
    }
}

hotpack 会执行这个函数,函数会返回 vue 的实例 app 和 初始化的数据 state,因为这个初始化数据是作为 store 的 state ,所以就命名 state 了。

对于预渲染,这个函数每编译一次就执行一次,对于服务端渲染,每次请求页面都会渲染一次。

服务渲染只需要初始化同步数据即可,所以这里没有 store.dispatch('user')

单页服务端渲染

对于单页而言,也是可以预渲染的,但是单页除首页外的页面本来就是异步请求的,所以对于异步页面,预渲染所带来的速度优势没有意义。hotpack 并不支持单页面的预渲染,但如果你愿意,是可以对默认首页进行预渲染的。

page/single/index.s.js

相比于预渲染,服务端渲染会传一个 ctx 进来,ctx 包含 url 等 请求相关的信息

export default async function (ctx = {}) {
...
const url = ctx.originalUrl || '/single'
router.push(url)
...
}

根据路由找到相关的组件,触发组件内的 ssr 函数,获得相关数据,与浏览器逻辑不同的是,需要等待数据完成,再渲染内容

let components = null
  router.currentRoute.value.matched.flatMap(record => {
       components = Object.values(record.components)
  })

  let promiseList = components.map(item => item.ssr(store, ctx))

  await Promise.all(promiseList)

整个页面都可以用插值的来修改内容,比如 title

<title>{{{title}}}</title>
return {
      pageData: {
          title: router.currentRoute.value.meta.title
      },
      app,
      state: store.state
  }

注意: <div id=“app ssr">这里不要放任何内容</div>

情景选择

不同渲染方式各有利弊。

浏览器渲染

成本最低,是最常用的方式,也是 hotpack 的默认方式。

预渲染

成本稍高,可以获得明显的速度优势,对于静态页面非常推荐。

服务端渲染

成本最高。若非必须,不建议采用。这并不光是成本的问题,还有对开发者能力的要求,需要掌握服务端的各种知识。


在一个应用中,有的页面适合浏览器渲染,有的页面适合预渲染,有的页面适合服务端渲染,有的适合单页,有的适合多页的是 在 hotpack 项目中各种渲染方式和页面形式都是直接支持的,它们之间是渐进的关系,可以随时相互转换。pre-ssr , ssr 标记、配置文件和命令行可以非常灵活的完成转换。

配置文件

配置文件在项目根目录的 .hotpack 文件加下,有三个文件 base.js, dev.js,pro.js,对应公共配置,开发配置和发布配置

预渲染和服务端渲染的配置很简单,如果只是预渲染,src 是可以不写的。

render: {
    //optional,服务端渲染必须,hotpack编译的时候把 render 里的文件 copy 到 dev(dist) 目录
    src: "render",
    //required,必须,是否启用
    enable: false
  },

在 dev.js 中 ssr 是关闭的 enable:false。不过 命令行的优先级最高,可以随时在命令行开启 ssr

hotpack dev -r

配置和命令行只能影响有标记 pre-ssr , ssr 的页面。

更多配置信息

开发实践

三种渲染方式的开发测试成本是逐渐升高的。用 hotpack 开发应用可以完美的协调成本与体验。

配置文件:开发环境配置为不启用 SSR,发布环境配置为启用 SSR。

开发的时候完全按浏览器渲染的方式开发。开发完成后,增加预渲染服务端渲染入口,通过 hotpack dev -r 查看效果。开发环境没有问题,可以发布 hotpack pro 在发布环境是不需要加 -r 的,因为配置文件中已经启用 了。发布的时候默认不会启动 server ,如果要启动可以 用 -s参数 hotpack pro -s。开发环境和发布环境的 -s 参数效果正好相反。

对于浏览器渲染和预渲染的页面,可以直接查看。查看服务端渲染的页面,需要把整个目录 copy 到一个纯净的环境中。因为发布的目录是需要 copy 到服务器上的,服务器上是一个全新的环境。开发环境发布目录默认是 dev 目录,发布环境默认是 pro 目录,上线的时候把 pro 目录 copy 到服务器上 ,执行 npm install 。服务端需要的文件放在 render 目录下。在示例项目中包含了最基本的文件,hotpack 会自动把 render 下的文件 copy 到 dev 或 pro目录。

/render

结束语

本篇文章主要是让大家体验一下,可能后面有更多详细介绍。

hotpack 并不会转换只能在服务端运行的代码,可以遵循下面的规则来避免环境问题。

  1. 只在服务端运行的文件名以 .s.js 结尾
  2. index.b.js,index.s.js(入口文件的名字可以不叫 index,叫什么并没有限制) 已经从源头上做了隔离,只在务端运行的文件只在 index.s.js中引用,只在浏览器中运行的文件只在 index.s.js 中引用
  3. 只在浏览器中运行的逻辑不要写在 beforeCreate,created 方法里

最后还可以在代码中做逻辑判断

//浏览器环境
if(typeof global==='undefined'){
  ...
}
//node 环境
else{
  ...
}

共用的文件 正常命名 xx.js 即可。 hotpack 的缓存非常强大,但是也可能会带来问题,你可以用 -c 参数来清除缓存。非必须不要清除缓存。

#清除开发环境缓存
hotpack dev -c
#清除发布环境缓存
hotpack pro -c