实现基于 Nuxt.js 的 SSR 应用

30,263 阅读14分钟

SEO

很重要,所以要普及。

SEO: 搜索引擎优化(Search Engine Optimization),它是指通过站内优化,如:网站结构调整、网站内容建设、网站代码优化以及站外优化等方法,来进行搜索引擎优化。

简单说: 通过各种技术(手段)来确保,你的Web内容被搜素引擎最大化收录,最大化提高权重,带来更多流量。

**常见关键词:**白帽、黑帽、SEM、Backlink、Linkbait、PageRank、Keyword Stuffing...,总之都围绕着一个核心:SEO;流量是变现的快车道,SEO 是低成本获取流量的最佳方法。

目前大部分的搜索引擎仅能抓取URI直接输出的数据资源,对于 Ajax 类的异步请求的数据无法抓取;Google 除外,Google 有自己的Google’s Webmaster AJAX Crawling Guidelines.技术支持。

SPA

**SPA:**单页 Web 应用(single page web application,SPA),就是只有一张 Web 页面的应用,是加载单个 HTML 页面并在用户与应用程序交互时动态更新该页面的 Web 应用程序。

简单说: Web 不再是一张张页面,而是一个整体的应用,一个由路由系统、数据系统、页面(组件)系统...组成的应用程序,其中路由系统是非必须的。

大部分的 Vue 项目,本质是 SPA 应用,Angular.js、Angular、Vue、React...还有最早的"Pjax"均如此。

SPA 时代,主要是在Web端使用了historyhash(主要是为了低版本浏览器的兼容)API,在首次请求经服务端路由输出整个应用程序后,接下来的路由都由前端掌控了,前端通过路由作为中心枢纽控制一系列页面(组件)的渲染加载和数据交互。

而上面所述的各类框架则是将以:路由、数据、视图为基本结构进行的规范化的封装。

最早的 SPA 应用,由 Gmail、Google Docs、Twitter 等大厂产品实践布道,广泛用于对SEO要求不高的场景中。

SSR

SSR: 服务端渲染(Server Side Render),即:网页是通过服务端渲染生成后输出给客户端。

在 SPA 之前的时代,我们的Web架构大都是 SSR,如:Wordpress(PHP)、JSP技术、JavaWeb...或者 DEDECMS、Discuz! 等这些程序都是传统典型的 SSR 架构, 即:服务端取出数据和模板组合生成 html 输出给前端,前端发生请求时,重新向服务端请求 html 资源,路由也由服务端来控制。

其次,有个概念叫预渲染(Prerendering)。

如果你只是用服务端渲染来改善一个少数的营销页面(如 首页,关于,联系 等等)的 SEO,那你可以用预渲染来实现。 预渲染不像服务器渲染那样即时编译 HTML,它只在构建时为了特定的路由生成特定的几个静态页面,等于我们可以通过 Webpack 插件将一些特定页面组件 build 时就编译为 html 文件,直接以静态资源的形式输出给搜索引擎。

但实际的商业应用中,大部分时候我们需要的是即时渲染,这也是我们今天讨论的主题。

Why

为什么要SSR,为了体验,还有SEO

首先,用户可能在网络比较慢的情况下从远处访问网站 - 或者通过比较差的带宽。 这些情况下,尽量减少页面请求数量,来保证用户尽快看到基本的内容。 可以用Webpack的代码拆分避免强制用户下载整个单页面应用,但是,这样也远没有下载个单独的预先渲染过的 HTML 文件性能高。

对于世界上的一些地区人,可能只能用1998年产的电脑访问互联网的方式使用计算机。 而 Vue 只能运行在 IE9 以上的浏览器,你可能也想为那些老式浏览器提供基础内容 - 或者是在命令行中使用 Lynx 的时髦的黑客。

在大部分的商业应用中,我们有 SEO 的需求,我们需要搜索引擎更多地抓取到我们的内容,更详细地认识到我们的网页结构,而不是仅对首页或特定静态页进行索引,这是 SSR 最重要的意义。

简单说就是,我们需要搜素引擎看到这样的代码:

而不是这样的代码:

且,我们还需要在 SSR 的基础上实现 SPA,即:首屏渲染

基本流程是:

在浏览器第一次访问某个 URI 资源的时候(首屏),Web 服务器根据路由拿到对应数据渲染并输出,且输出的数据中包含两部分:

  • 路由页对应的页面及已渲染好的数据
  • 完整的SPA程序代码

在客户端首屏渲染完成之后,此时我们看到的其实已经是一个和之前的 SPA 相差无几的应用程序了,接下来我们进行的任何操作都只是客户端的应用进行交互, 页面/组件由Web端渲染,路由也由浏览器控制,用户只需要和当前浏览器内的应用打交道就可以了。

之前在各大 SPA 框架还未正式官方支持 SSR 时,有一些第三方的解决方案,如:prerender.io, 它们做的事情就是建立HTTP一个中间层,在判断到访问来源是蜘蛛时,输出已缓存好的html数据,此数据若不存在,则调用第三方服务对 html 进行缓存,往复进行。

另一方法是自行构建蜘蛛渲染逻辑,当识别 UA 为搜索引擎时,拿服务端已准备好的模板和数据进行渲染输出 html 数据,反之,则输出 SPA 应用代码;

我当时也考虑过此方法,但有很多弊端,如:

  • 需要针对蜘蛛编写一套独立的渲染模板,因为大部分情况下 SPA 的代码是没法直接在服务端使用的
  • 搜索引擎若检测到蜘蛛抓取数据和真实访问数据不一致,会做降权惩罚,也就意味着渲染模板还必须和SPA预期输出一模一样

所以,最好的方法是 SPA 能和服务端使用同一套模板,且使用同一个服务端逻辑分支,再简单说:最好 Vue、Ng2... 能直接在服务端跑起来

于是,陆续诞生了基于 React 的Next.js、基于 Vue 的Nuxt.js、Ng2 诞生之日便支持。

没错,Nuxt.js 就是今天的主角。

Nuxt.js

官方是这么介绍自己的:

Nuxt.js 是一个基于 Vue 的通用应用框架。

通过对客户端/服务端基础架构的抽象组织,Nuxt.js 主要关注的是应用的 UI渲染。

我们的目标是创建一个灵活的应用框架,你可以基于它初始化新项目的基础结构代码,或者在已有 Node.js 项目中使用 Nuxt.js。

Nuxt.js 预设了利用 Vue 开发服务端渲染的应用所需要的各种配置。

除此之外,我们还提供了一种命令叫:nuxt generate,为基于 Vue 的应用提供生成对应的静态站点的功能。

我们相信这个命令所提供的功能,是向开发集成各种微服务(miscroservices)的 Web 应用迈开的新一步。

作为框架,Nuxt.js 为 客户端/服务端 这种典型的应用架构模式提供了许多有用的特性,例如异步数据加载、中间件支持、布局支持等。

太啰嗦了,用我的话说:

Nuxt.js是使用 Webpack 和 Node.js 进行封装的基于Vue的SSR框架,使用它,你可以不需要自己搭建一套 SSR 程序,而是通过其约定好的文件结构和API就可以实现一个首屏渲染的 Web 应用。

之所以叫 Nuxt.js 也是因为受到了 Next.js 的启发。

作者是法国的兄弟俩,EvenYou 在微博多次提到,也在欧洲见过哥俩。

在此之前,国内有一些对 Vue SSR 的整合尝试,但都没有成功,主要在于 Webpack 和 Node 的结合上没有实践出最佳方案, 当我看到 Nuxt.js 以约束文件夹和配置文件nuxt.config.js的方式来管理多个程序组件之间的关系时,就觉得,很酷!

接下来,我不会提供具体更多的学习资料,因为官方文档已经非常全面和成熟,已经 0.10.5 了(现在是 RC-11),只讲下其架构和原理,和一些生产环境会遇到的问题。

首先,Nuxt.js 是一个 Node 程序,就像上面说的,我们是要把 Vue 跑在服务端,所以必须使用 Node 环境。

我们对 Nuxt.js 应用的访问,实际上是在访问这个 Node.js 程序的路由,程序输出首屏渲染内容 + 用以重新渲染的 SPA 的脚本代码,而路由是由 Nuxt.js 约定好的 pages 文件夹生成的。

所以,整体上,Nuxt.js 通过各个文件夹和配置文件的约束来管理我们的程序,而又不失扩展性,其有自己的插件机制


按照目前的版本,Nuxt.js 的程序的文件结构大概分为以下部分:

  • pages:各页面组件,用于生成对应路由,支持嵌套,支持动态路由
  • components:各组件,用于你自己管理公共组件或非公共组件
  • layouts:宿主布局页面模板组件,用于你可以把不同的页面指定使用不同的布局
  • assets:用于 Webpack 编译的各类资源,通常是一些小的资源,如代替雪碧图之类的图片等东西
  • middleware:中间件,首屏渲染和路由跳转前均执行对应中间件,可以返回promise或直接next(很实用!)
  • plugins:插件,SPA中用的各类第三方组件和一些node模块都可以在这引入,甚至可以引入自己编写的第三方库
  • store:内置了vuex,可以直接返回数据模块或返回一个自建vuex根对象,具体要翻文档
  • 其他:你可以自定义文件夹和别名映射,文档都有提及,这里有配置代码

nuxt.config.js对程序的扩展管理可大概分为以下类:

  • build:主要对应 Webpack 中的各配置项,可以对默认的 Webpack 配置进行扩展,如这里代码
  • cache:主要对应内置的组件缓存模块lru-cache的配置对象,有默认值,可选关闭
  • css:对应我们在SPA随处引用样式文件的require语句
  • dev:用于自定义配置环境变量,对应之前webpack.config.js相关文件中的变量语句
  • env:同上息息相关
  • generate:对generate命令执行时的行为做一些定制
  • head:对应vue-meta插件的全局配置,vue-meta用于VUE/SSR程序的文档元信息的管理
  • loading:用于定制化Nuxt.js内置的进度条组件
  • performance:用于配置Node.js服务器性能上的配置
  • plugins:用于管理和应用对应plugins文件夹中的插件
  • rootdir:用于设置 Nuxt.js 应用的根目录(这俩api有很大合并的意义)
  • srcdir:用于设置 Nuxt.js 应用的源码目录(这俩api有很大合并的意义)
  • router:用于对vue-router的扩展和定制,其中还包括了中间件的配置,但并不完美(后面说)
  • transition:用于定制Nuxt.js内置的页面切换过渡效果的默认属性值
  • watchers:用于定制Nuxt.js内置的文件监听模块chokidar和 Webpack 的相关配置项

generate

同时,Nuxt.js 支持以generate命令将程序直接构建为静态 html ,就像上面说的,可以作为静态资源直接输出。

生产环境实践

特殊的异步需求

这是生产环境最常见的问题,没有之一。

我的博客右侧 Sidebar 为例,在组件结构中,其属于宿主 layout 下的子组件,不属于页面组件,无法使用页面组件中的fetch方法, 官方的解释是子组件无法使用阻塞异步请求,即:子组件得到的异步数据无法用于服务端渲染,这对于程序是合理的,避免异常阻塞,简化业务模型;

但实际需求中,我需要这些异步数据增强站内内链 SEO;于是,我们可以巧妙地使用内置 vuex 中的nuxtServerInit这个 API,这个 API 是在 Nuxt.js 程序实例化之后第一次执行的方法, 其内部返回一个promise,我们可以在这里完成我们站内的所有子组件异步请求,随后将数据映射至对应子组件即可,这里有实践代码

内存问题

在阿里云低配机上出现内存膨胀的问题,一个 Blog 程序 Run 起的内存高达 100M+,当然也由于 Node.js 的特殊单线程异步机制,暂不关心。

但在经过一段时间的访问之后,特别是瞬间高并发访问,会导致内存膨胀爆表宕机,经分析,是由于组件缓存引起的,将组件缓存减少至10,问题有所改观,但不明显;

更深原因是,每次用户访问,程序均会重新渲染组件输出,组件数据即在一段时间内驻存在内存中,直到 V8GC 回收。

最终的解决方案是:

使用官方推荐的"使用编码中的 Nuxt.js "方法,自定义Node.js程序的入口,对程序进行一些优化; 如果你对业务和程序都需要有深度掌控的话,我很推荐此方法,它可以使你以管理 Node.js 程序的方式管理应用。

具体的优化方法是使用了一个叫idle-gc的垃圾回收模块来优化内存管理,

idle-gc是在node早期版本中被废除的功能,主要负责空闲时的堆内存回收,然后早期被认为有 BUG,经常会导致 CPU 满载,于是从 Node.js 中移除了,此项目作者修复了这个 BUG,并发布了模块。

另外,如果机器配置足够,建议开启缓存,即cache选项,且适当往大的配置,cache 的意义在于使用内存常驻来减轻 CPU 的计算压力,这对于单线程的 Node.js 是很好的业务实践。

最新更新:已不再使用此模块,最终靠 [ 优化业务逻辑 ] + [ 优化页面结构和抽象粒度 ] + [ 升级硬件 ] 来解决了问题。

这是 PM2 监控进程的日常数据之一:

移动版本适配问题

几乎所有的搜索引擎对于 PC 和移动端业务都是分开的,所以我们可以巧妙地使用layouts布局模块来实现我们移动端和 PC 端业务的分离; 在我的博客项目里,由于业务逻辑和页面均不够复杂,故使用了 CSS3 媒体查询 + 组件内判断的形式实现了移动端的适配。

Route自定义meta问题

目前 API 中对 router 的支持不够全面,如自定义的配置都还无法实现,不过可以通过宿主组件对应周期的hook来实现对实例化后的 router 对象进行修改和管理,尽管这不够优雅。

Window问题

由于 Vue 的底层使用 Virtual DOM,所以 Nuxt.js 在 Node.js 环境中的编译实际上是对象计算为字符串的过程,并没有依赖 Window/Dom,或者说任何基于 Vue 的 SSR 程序均如此。

我们在实际生产时可能用到一些需要依赖 DOM 的插件/扩展,正确的方法是根据官方文档 - 只在浏览器里使用的插件推荐的方法,通过变量判断插件/扩展的应用环境, 这里有实践代码,或者使用SSR版本的组件,如:vue-awesome-swiper, 或自行封装directive类型的插件,而非component, 切记不要使用jsdom等类似 Node.js 中的 DOM 库,这类库本身是为爬虫或测试诞生的,且本身会占据大量的内存,这不是真正的解决方案!

有关更多常用使用问题,可以参考官方解答

最后:这是我的博客,也是一个完整的 Nuxt.js 程序,源码在这里

若有差池,期待指正