可能大家在看到这个标题的时候,会觉得,只不过又是一篇烂大街的 SSR 从零入门的教程而已。别急,往下看,相信你或多或少会有一些不一样的收获呢。
在落地一种技术的时候,我们首先要想一想:
上面三个问题思考清楚之后,才能真正地去落地。上面三个问题思考清楚之后,才能真正地去落地。而有赞教育接入服务端渲染,正是为了优化 H5 页面的首屏内容到达时间,带来更好的用户体验(顺便利于 seo)。
说了这么多,以下开始正文。
在较早时期,前后端的配合模式为:后端负责服务层、业务逻辑层和模版渲染层(表现层);前端只是实现页面的交互逻辑以及发送 AJAX。比较典型的例子就是 JSP 或 FreeMarker 模板引擎负责渲染出 html 字符串模版,字符串模版里的 js 静态资源才是真正前端负责的东西。
而这种形式,就是天然的服务端渲染模式:用户请求页面 -> 请求发送到应用服务器 -> 后端根据用户和请求信息获取底层服务 -> 根据服务返回的数据进行组装,同时 JSP 或 FreeMarker 模版引擎根据组装的数据渲染为 html 字符串 -> 应用服务器讲 html 字符串返回给浏览器 -> 浏览器解析 html 字符串渲染 UI 及加载静态资源 -> js 静态资源加载完毕界面可交互。
那么既然后端模版引擎时代带来的效果就是我们想要的,那为啥还有以后让前端发展服务端渲染呢?因为很明显,这种模式从开发角度来讲还有挺多的问题,比如:
后来,诞生了 SPA(Single Page Application),解决了上面说的部分问题:
后端不需要关心表现层的逻辑,只需要注重服务层和业务逻辑层就可以了,暴露出相应的接口供前端调用。这种模式也同时实现了前后端解耦。
本地开发的时候,前端只需要启动一个本地服务,如:dev-server 就可以开始开发了。赋予了前端更多的能力,比如前端的路由控制和鉴权,比如通过 SPA + 路由懒加载的模式可以带来更好的用户体验。
但同时,也带来了一些问题:
正因为 SPA 带来的一些问题(尤其是首屏白屏的问题),接入服务端渲染显得尤为必要。// 终于讲到服务端渲染这个重点了。
而正是 Node 的发展和基于 Virtual DOM 的前端框架的出现,使得用 js 实现服务端渲染成为可能。因此在 SPA 的优势基础上,我们顺便解决了因为 SPA 引入的问题:
服务端渲染的首屏直出,使得输出到浏览器的就是完备的 html 字符串模板,浏览器可以直接解析该字符串模版,因此首屏的内容不再依赖 js 的渲染。
正是因为服务端渲染输出到浏览器的是完备的 html 字符串,使得搜索引擎能抓取到真实的内容,利于 SEO。
同时,通过基于 Node 和前端 MVVM 框架结合的服务端渲染,有着比后端模版引擎的服务端渲染更明显的优势:可以优雅降级为客户端渲染(这个后续会讲,先占个坑)。
既然服务端渲染能带来这么多好处,那具体怎么实现呢?从官网给出的原理图,我们可以清晰地看出:
Source 为我们的源代码区,即工程代码;
Universal Appliation Code 和我们平时的客户端渲染的代码组织形式完全一致,只是需要注意这些代码在 Node 端执行过程触发的生命周期钩子不要涉及 DOM 和 BOM 对象即可;
比客户端渲染多出来的 app.js、Server entry 、Client entry 的主要作用为:app.js 分别给 Server entry 、Client entry 暴露出 createApp() 方法,使得每个请求进来会生成新的 app 实例。而 Server entry 和 Client entry 分别会被 webpack 打包成 vue-ssr-server-bundle.json 和 vue-ssr-client-manifest.json(这两个 json 文件才是有用的,app.js、Server entry 、Client entry 可以抽离,开发者不感知);
Node 端会根据 webpack 打包好的 vue-ssr-server-bundle.json,通过调用 createBundleRenderer 生成 renderer 实例,再通过调用 renderer.renderToString 生成完备的 html 字符串;
Node 端将 render 好的 html 字符串返回给 Browser,同时 Node 端根据 vue-ssr-client-manifest.json 生成的 js 会和 html 字符串 hydrate,完成客户端激活 html,使得页面可交互。
按照 Vue SSR 官方文档建立起一个服务端渲染的工程后,是否就可以直接上线了呢?别急,我们先看看是否有什么可以优化的地方。
一个大的 SPA,主文件 js 往往很大,通过代码分割可以将主文件 js 拆分为一个个单独的路由组件 js 文件,可以很大程度上减小首屏的资源加载体积,其他路由组件可以预加载。
// router.js
constIndex =()=>import(/* webpackChunkName: "index" */'./pages/Index.vue');
constDetail =()=>import(/* webpackChunkName: "detail" */'./pages/Detail.vue');
constroutes = [
{
path:'/',
component: Index
},
{
path:'/detail',
component: Detail
}
];
constrouter =newRouter({
mode:'history',
routes
});
因为服务端渲染是 CPU 密集型操作,非首屏的模块或者不重要的模块(比如底部的推荐列表)完全可以采用客户端渲染,只有首屏的核心模块采用服务端渲染。这样做的好处是明显的:1. 较大地节省 CPU 资源;2. 减小了服务端渲染直出的 html 字符串长度,能够更快地响应给浏览器,减小白屏时间。
// Index.vue
asyncData({ store }) {
returnthis.methods.dispatch(store);// 核心模块数据预取,服务端渲染
}
mounted() {
this.initOtherModules();// 非核心模块,客户端渲染,在 mounted 生命周期钩子里触发
}
页面缓存一般适用于状态无关的静态页面,命中缓存直接返回页面;组件缓存一般适用于纯静态组件,也可以一定程度上提升性能。
// page-level caching
constmicroCache = LRU({
max:100,
maxAge:1000// 重要提示:条目在 1 秒后过期。
})
server.get('*', (req, res) => {
consthit = microCache.get(req.url)
if(hit) {// 命中缓存,直接返回页面
returnres.end(hit)
}
// 服务端渲染逻辑
...
})
// component-level caching
// server.js
constLRU =require('lru-cache')
constrenderer = createRenderer({
cache: LRU({
max:10000,
maxAge: ...
})
});
// component.js
exportdefault{
name:'item',// 必填选项
props: ['item'],
serverCacheKey:props=>props.item.id,
render (h) {
returnh('div',this.item.id)
}
};
如果工程中大部分页面都是状态相关的,所以技术选型采用了服务端渲染,但有部分页面是状态无关的,这个时候用服务端渲染就有点浪费资源了。像这些状态无关的页面,完全可以通过 Nginx Proxy Cache 缓存到 Nginx 服务器,可以避免这些流量打到应用服务器集群,同时也能减少响应的时间。
进行优化之后,是否就可以上线了呢?这时我们想一想,万一服务端渲染出错了怎么办?万一服务器压力飙升了怎么办(因为服务端渲染是 CPU 密集型操作,很耗 CPU 资源)?为了保证系统的高可用,我们需要设计一些降级方案来避免这些。具体可以采用的降级方案有:
压测可以分为多个阶段:本地开发阶段、QA 性能测试阶段、线上阶段。
作为生产环境的应用,肯定不能“裸奔”,必须接入日志平台,将一些报错信息收集起来,以便之后问题的排查。
如果上线服务端渲染的工程是提供核心服务的应用,应该采用灰度发布的方式,避免全量上线。一般灰度方案可以采用:百分比灰度、白名单灰度、自定义标签灰度。具体采用哪种灰度方式看场景自由选择,每隔一段时间观察灰度集群没有问题,所以渐渐增大灰度比例 / 覆盖范围,直到全量发布。
在有赞电商的服务端渲染的落地场景中,我们抽离了单独的依赖包,提供各个能力。
从最终的上线效果来看,相同功能的页面,服务端渲染的首屏内容时间比客户端渲染提升了 300%+。
A:首先我们明确一点,服务端渲染比客户端渲染快的是首屏的内容到达时间(而非首屏可交互时间)。至于为什么会更快,我们可以从两者的 DOM 渲染过程来对比:
客户端渲染:浏览器发送请求 -> CDN / 应用服务器返回空 html 文件 -> 浏览器接收到空 html 文件,加载的 css 和 js 资源 -> 浏览器发送 css 和 js 资源请求 -> CDN / 应用服务器返回 css 和 js 文件 -> 浏览器解析 css 和 js -> js 中发送 ajax 请求到 Node 应用服务器 -> Node 服务器调用底层服务后返回结果 -> 前端拿到结果 setData 触发 vue 组件渲染 -> 组件渲染完成
服务端渲染:浏览器发送请求 -> Node 应用服务器匹配路由 -> 数据预取:Node 服务器调用底层服务拿到 asyncData 存入 store -> Node 端根据 store 生成 html 字符串返回给浏览器 -> 浏览器接收到 html 字符串将其激活:
我们可以很明显地看出,客户端渲染的组件渲染强依赖 js 静态资源的加载以及 ajax 接口的返回时间,而通常一个 page.js 可能会达到几十 KB 甚至更多,很大程度上制约了 DOM 生成的时间。而服务端渲染从用户发出一次页面 url 请求之后,应用服务器返回的 html 字符串就是完备的计算好的,可以交给浏览器直接渲染,使得 DOM 的渲染不再受静态资源和 ajax 的限制。
A:比较常见的限制比如:
A:像这些和用户状态无关的静态页面,完全可以采用预渲染的方式(具体见 Vue SSR 官方指南),服务端渲染适用的更多场景会是状态相关的(比如用户信息相关),需要经过 CPU 计算才能输出完备的 html 字符串,因此服务端渲染是一个 CPU 密集型的操作。而静态页面完全不需要涉及任何复杂计算,通过预渲染更快且更节省 CPU 资源。
在使用vue的时候,我们都知道它是双向数据绑定的,但是在使用不熟的情况下,经常会遇到:data中的数据变化了,但是并没有触发页面渲染。下面就整理一些出现这种情况的场景以及解决办法。
这里结合art-template模板引擎说明。首先了解下前端页面中如何使用art-template。当不需要对SEO友好的时候,推荐使用客户端渲染;当需要对 SEO友好的时候,推荐使用服务器端渲染
在使用vue的时候,偶然发现多次刷新或者网络加载缓慢的时候,会一瞬间出现设置的模板的情况。实在很影响美观,可以使用vue现成的指令来解决这个问题:v-cloak
大部分Web应用的富文本内容都是以HTML字符串的形式存储的,通过HTML文档去展示HTML内容自然没有问题。但是,在微信小程序(下文简称为「小程序」)中,应当如何渲染这部分内容呢?
估计大家都听过,尽量将 CSS 放头部,JS 放底部,这样可以提高页面的性能。然而,为什么呢?大家有考虑过么?很长一段时间,我都是知其然而不知其所以然,强行背下来应付考核当然可以,但实际应用中必然一塌糊涂
原生JS改变页面数据,必须要获取页面节点,也即是进行DOM操作,jQuery之类的框架只是简化DOM操作的写法,实质并没有改变操作页面数据的底层原理,DOM操作影响性能(导致浏览器的重绘和回流),Vue是一个mvvm框架(库),大幅度减少了DOM操作
在决定渲染方式时,需要测量和理解真正的瓶颈在哪里。静态渲染或服务器渲染在多数情况都比较适用,尤其是可交互性对JS依赖较低的场景。下面是一张便捷的信息图,显示了服务器到客户端的技术频谱:
如果从服务端返回的数据量较少,或者只有几个字段,可以用vue的set方法,如果数据量较大,请直接看第二种情况。官网API是这样介绍的:Vue.set(target,key,value)
当数据需要异步加载时render获取不到数据可能会报一些错误,此时需要在render函数中加一个判断.行到render时,state对象的haveData为false, 所以此时页面展示 loading,当异步获取数据成功时
在vue.js中,要将一段字符串渲染成html,可以使用v-html指令。但是 官方文档 中的v-html部分也提醒了
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!