大家好,我是 17
SSR 特别指支持在 Node.js 中运行相同应用程序的前端框架(例如 React、Preact、Vue 和 Svelte),将其预渲染成 HTML,最后在客户端 hydrating。
下面是关于 vue3 预渲染和服务端渲染的示例讲解。
本示例虽然是用 hotpack 工具,但原理是相通的,与工具无关。
示例项目
hotpack 为服务端渲染(SSR)提供了内建支持。下面的范例包含了Vue3 的 SSR 示例,可以作为本指南的参考。
源码结构
一个典型的 SSR 应用应该有如下的源文件结构
index
是一个页面的文件夹,里面包含index
页面需要的内容
- vue vue组件
- index.b.js 客户端专用入口 b 是 browser的第一个字母
- index.html html模板
- index.s.js 服务端专用入口 s 是 server的第一个字母
情景体验
hotpack 可以进行浏览器渲染,预渲染和服务端渲染,支持多页,单页,可以自由选择
为了体验全部功能,我们先准备一下环境
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 显示如下内容
在页面上右键,选择 显示网页源代码
页面上只有空的 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 显示如下内容
查看源文件
<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 根目录为基准,所以 /
一律省略不写。
多页浏览器渲染
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')
完事。单页是有客户端路由的,需要哪些数据是由路由决定的,一个想法是根据路由信息直接获取数据,但更好的做法是把获取数据的方法放在模块中。
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 中判断一下就好
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 并不支持单页面的预渲染,但如果你愿意,是可以对默认首页进行预渲染的。
相比于预渲染,服务端渲染会传一个 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目录。
结束语
本篇文章主要是让大家体验一下,可能后面有更多详细介绍。
hotpack 并不会转换只能在服务端运行的代码,可以遵循下面的规则来避免环境问题。
- 只在服务端运行的文件名以 .s.js 结尾
- index.b.js,index.s.js(入口文件的名字可以不叫 index,叫什么并没有限制) 已经从源头上做了隔离,只在务端运行的文件只在 index.s.js中引用,只在浏览器中运行的文件只在 index.s.js 中引用
- 只在浏览器中运行的逻辑不要写在
beforeCreate,created
方法里
最后还可以在代码中做逻辑判断
//浏览器环境 if(typeof global==='undefined'){ ... } //node 环境 else{ ... } 复制代码
共用的文件 正常命名 xx.js 即可。hotpack 的缓存非常强大,但是也可能会带来问题,你可以用
-c
参数来清除缓存。非必须不要清除缓存。
#清除开发环境缓存 hotpack dev -c #清除发布环境缓存 hotpack pro -c