This website requires JavaScript.
BING
Because the mountain is there
OG

实现基于 Nuxt.js 的 SSR 应用

8,063 words, 20 min read2017/04/21 PM22,204 views

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 最重要的意义。

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

包含有意义内容的 HTML 代码
包含有意义内容的 HTML 代码

而不是这样的代码:

无内容的 HTML 代码
无内容的 HTML 代码

且,我们还需要在 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.js 模块都可以在这引入,甚至可以引入自己编写的第三方库
  • 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.js 早期版本中被废除的功能,主要负责空闲时的堆内存回收,然后早期被认为有 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 程序,源码在这里

若有差池,期待指正

Creative Commons BY-NC 3.0 CNhttps://surmon.me/article/48
50 / 72 comments
Guest
Join the discussion...
  • RP
    Rp🇨🇳CNBeijingMac OSChrome
    #2027

    套总的富文本编辑器是用的哪个组件啊,

  • ooooo
    Ooooo🇨🇳CNWuhanWindowsChrome
    #1910

    🐮🍺

  • caiguanrong
    Caiguanrong🇨🇳CNKunmingWindowsEdge
    #1836

    非常喜欢这个风格   可以用您的这个项目搭建个人博客吗

  • dongkirk
    Dongkirk🇨🇳CNHangzhouWindowsChrome
    #1765

    溜达一圈 试试功能 😂

  • yuxiaohui
    Yuxiaohui🇨🇳CNBeijingMac OSChrome
    #1612

    是是是

  • monty
    Monty🇨🇳CNBeijingWindowsChrome
    #1534

    最近1g内存的主机经常内存泄漏经常崩,大佬有什么好建议吗,代码优化该怎么具体优化呢,

  • zhanfu
    Zhanfu🇨🇳CNGuangzhou ShiWindowsChrome
    #1494

    大佬,nginx咋配置https的

    • Surmon
      Surmon🇨🇳CNShanghaiMac OSChrome
      #1495

      reply:

      请不要向我提问任何搜索引擎可以轻易解决的问题

  • lin100
    Lin100🇨🇳CNHangzhouWindowsChrome
    #1406

    你好,请问下你上面的那个图形监控界面工具叫什么名字?是pm2自带的吗?

    • Surmon
      Surmon🇨🇳CNShanghaiMac OSChrome
      #1407

      reply:

      keymetrics.io

    • lin100
      Lin100🇨🇳CNHangzhouWindowsChrome
      #1408

      reply:

      thank you😃

  • lin100
    Lin100🇨🇳CNHangzhouWindowsChrome
    #1403

    我有一个想法: 用户使用浏览器访问页面,我们使用js加个cookie标识,如果再次访问并且有这个标识的话,那么判断是用户访问,用nginx返回纯js渲染的页面。 如果没有cookie标识的话判断是搜索引擎爬虫, 走nuxt.js的服务端渲染。

    • Surmon
      Surmon🇨🇳CNShanghaiMac OSChrome
      #1404

      reply:

      几年前 angularjs 的时代就有 prerender.io 和 phontomjs 这样的解决方案,工作原理和你描述的基本一致,但这些方案已被取代或者说淘汰,因为同构的诞生就是从根源更好地解决 SPA 的 SEO 问题,是 solution,而不是 "patch"。

      另外还有很重要的一点:绝大部分搜索引擎都有一条反作弊规则,当发现网站对 spider 和用户 UA 返回的内容做不同的处理时,会认为网站作弊,做降权处理,这种 SEO 手段也就失去了价值。

    • lin100
      Lin100🇨🇳CNHangzhouWindowsChrome
      #1405

      reply:

      明白了,谢谢回答😃

  • Leon
    Leon🇵🇭PHRosarioWindowsChrome
    #1307

    在高并发的场景下还是不太适合用 Node 做 SSR 的,太耗性能了。你可以试一下,在浏览器打开调试模式,禁缓存,疯狂刷新你的这个博客首页,看看 Node 的 CPU 占用是不是会飙升。这个问题可以通过组件缓存缓解,但是组件缓存又对组件依赖的数据有一定的要求,如果没做好,复用的组件有可能只是得到组件一开始的样式,不是想要的最终样式,优化下来效果也不是特别好

    • Surmon
      Surmon🇻🇳VNHanoiMac OSChrome
      #1310

      reply:

      如果真的是 cpu 飙升这种情况,我理解为 “优化失败” 或 “可用性失败”。 具体的好几点,我可能需要一个空闲的时间来详细罗列。

    • 小虾
      小虾🇨🇳CNChangshaMac OSChrome
      #1368

      reply:

      😂

    • monty
      Monty🇨🇳CNBeijingWindowsChrome
      #1535

      reply:

      有什么好方法,针对ssr内存泄漏这块

  • leeruigan
    Leeruigan🇨🇳CNZiboWindowsChrome
    #1302

    🌹

  • 该用户已注销
    该用户已注销🇨🇳CNShanghaiWindowsChrome
    #1268

    膜拜大哥💪

  • 郝腾飞
    郝腾飞🇨🇳CNBeijingWindowsChrome
    #1223

    122😭

  • 小飞普
    小飞普🇨🇳CNXiamenWindowsChrome
    #1206

    帅哥 问个问题,为什么我封装个vue插件,在nuxt项目里通过npm引入,运行之后可以显示,但是会报错,看起来是我的插件不支持ssr,但是如果我把插件封装在项目里引用就不会报错呢

    • Surmon
      Surmon🇨🇳CNShanghaiMac OSChrome
      #1207

      reply:

      “我把插件封装在项目里引用” 这一步你做了什么

    • 小飞普
      小飞普🇨🇳CNXiamenWindowsChrome
      #1208

      reply:

      只是引入组件啊,写法上和引入npm的插件没有丝毫差别,传了个对象过去

  • 波哥
    波哥🇨🇳CNShenzhenWindowsChrome
    #1191

    你怎么可以如此 的帅

    • Surmon
      Surmon🇨🇳CNShanghaiMac OSChrome
      #1195

      reply:

      我父亲给的

  • 郝腾飞
    郝腾飞🇨🇳CNBeijingWindowsChrome
    #1185

    asyncData可以发post请求吗?

  • 郝腾飞
    郝腾飞🇨🇳CNBeijingWindowsChrome
    #1184

    haotengfei

  • 郝腾飞
    郝腾飞🇨🇳CNBeijingWindowsChrome
    #1183

    nuxt

  • cotex
    Cotex🇨🇳CNShanghaiMac OSChrome
    #1098

    主站有管理系统吗

  • Jonas Hao
    Jonas hao🇨🇳CNShenzhenMac OSChrome
    #1042

    从掘金看到这篇文章, 感觉受益匪浅, 然后来到这里, 发现新天地, 多多学习

    • Surmon
      Surmon🇨🇳CNShanghaiMac OSChrome
      #1048

      reply:

      这个天地里,只有一个浑身充满魅力的博主除了一副帅气的皮囊什么也不曾拥有

  • roastwind
    Roastwind🇨🇳CNBeijingMac OSChrome
    #990

    您好,您有碰到过在

              
    • 1
    data() { return { date: Date.now() } } 

    服务端渲染过一次,然后会在客户端再次渲染过这样的问题么?主要是看 cached-components缓存组件时,遇到的一个问题。vue-server-render

    • Surmon
      Surmon🇨🇳CNShanghaiMac OSChrome
      #991

      reply:

      没,因为我没怎么深入了解缓存

  • Morgan
    Morgan🇨🇳CNShanghaiMac OSChrome
    #944

    请问您的Nuxt 渲染只用了Nuxt吗 请问版本用的是哪个版本,收录刚开始的时候是什么样的,我们公司做了个 但是收录不是很好,请问你在收录上遇到什么问题没

    • Surmon
      Surmon🇨🇳CNShanghaiMac OSChrome
      #945

      reply:

      2,SEO 好不好找问题啊,找工具分析啊,谁说上个 SSR 就一定等于做好 SEO 的

  • jonyjay
    Jonyjay🇨🇳CNShanghaiWindowsChrome
    #756

    你好,这个nuxt渲染 能够做到  在network中看不到接口请求 吗

  • zhmm
    Zhmm🇨🇳CNQinnanWindowsFirefox
    #754

    reply:

    😃😅👍回凤凰山覅

  • shuaisijia
    Shuaisijia🇨🇳CNXiamenWindowsChrome
    #752

    膜拜大神👍👍👍👍

  • 林荣烺
    林荣烺🇨🇳CNNanjingMac OSChrome
    #744

    666

  • Chin Lennon
    Chin lennon🇨🇳CNShanghaiMac OSChrome
    #733

    没有用过Nuxt.js,不知道体验如何,我用原生来写SRR,Koa驱动,pm2部署,发现部署后存在内存泄漏的问题,目测是代码写得有bug,不知道Nuxt.js实现的SSR有没有内存问题?

  • mike
    Mike🇨🇳CNShenzhenMac OSChrome
    #631

    用到的这个方法的意思是在全局加载的同时也会异步加载这些方法的意思吗?「我们可以巧妙地使用内置 vuex 中的nuxtServerInit这个 API」

    • Surmon
      Surmon🇨🇳CNXiamenMac OSChrome
      #633

      reply:

      nuxtServerInit 会在任何一次 [ 请求来到时 + 渲染输出前 ] 完成,所以你可以在这里面搞事情,但是它会阻塞的

    • Mike
      Mike🇨🇳CNShenzhenMac OSChrome
      #636

      reply:

      还有个问题,您是怎么解决文章页自刷新不冒出404 的 , 因为我是用nginx 做的服务器, 网上大多数说得都是要页面重定向到某个指定页面,如「index.html/article.html」之类的,但是面对携带参数的页面如「https://surmon.me/article/48」这种的时候,不太清楚怎么解决,谢谢大大了。🙏

    • Surmon
      Surmon🇨🇳CNQingzhouMac OSChrome
      #637

      reply:

      难以理解,已经说过,nuxt 本质是一个 node 应用,路由是由 nuxt 管理的,nginx 的请求应该全转发,所以你搜到的 SPA 的 nginx 转发方式和这里有什么关系

  • ray
    Ray🇨🇳CNSuzhouMac OSSafari
    #567

    reply:

    那您之前提到的前后端必须都可以用的一些库或者方法只是针对nuxt提供的一些api是吗