实现基于Nuxt.js的SSR应用

https://static.surmon.me/nodepress/image/nuxtjs.jpg

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

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

https://static.surmon.me/17-4-26/49756414-file_1493136433180_126e8.png

而不是这样的代码:

https://static.surmon.me/17-4-26/20713366-file_1493136387152_a4d5.png

且,我们还需要在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.js 的通用应用框架。

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

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

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

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

我们相信这个命令所提供的功能,是向开发集成各种微服务(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的方式来管理多个程序组件之间的关系时,就觉得太TM6了,简直酷毙了!

接下来,我不会提供具体更多的学习资料,因为官方文档已经非常全面和成熟,已经0.10.5了,我主要想讲一讲他的架构和原理,和一些生产环境会遇到的问题。

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

我们对Nuxt.js应用的访问,实际上是在访问这个Node.js程序的路由,程序输出首屏渲染,而路由是由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程序实例化之后第一次执行的方法, 其内部返回一个promise,我们可以在这里完成我们站内的所有子组件异步请求,随后将数据映射至对应子组件即可,这里有实践代码

内存问题

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

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

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

最终的解决方案是:

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

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

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

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

移动版本适配问题

几乎所有的搜索引擎对于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程序,源码在这里

若有差池,期待指正

本文于 2017/4/21 下午 发布在 Code 分类下,当前已被围观 2297 次

相关标签:Web开发VueJavascript

永久地址:https://surmon.me/article/48

版权声明:自由转载-署名-非商业性使用  |  Creative Commons BY-NC 3.0 CN
  • Jooger
    JoogerWindows 10Chrome | 57CN - Beijing#184

    套总用的多少内存的机器?

  • Surmon
    SurmonMac OS XChrome | 57CN - Xian#185

    回复 #184 @Jooger

    垃圾最低配,服务端build一次挂一次

  • Jooger
    JoogerWindows 10Chrome | 57CN - Beijing#186

    我看了一下最低配都是1G的内存呀?那我512M的机器岂不是...

  • Surmon
    SurmonMac OS XChrome | 57CN - Xian#187

    回复 #186 @Jooger

    对,跑生产的话基本没法用🌚

  • jooger
    JoogerWindows 10Chrome | 57CN - Beijing#188

    回复 #187 @Surmon

    厉害了😱

  • 叶子
    叶子Windows 7Chrome | 58CN - Nanjing#249

    我再使用富文本编辑器vue-quill-editor的时候, 使用图片上传发现它是把图片转为base后插在文本中, 然后我直接是提交了,但发现太大了在查询的时候很慢。

    所以想问下vue-quill-editor怎么把上传图片怎么改为后台上传然后返回个图片的url再插入。看了整天英文文档,找来找去找不到。恳求解答下。

  • Surmon
    SurmonMac OS XChrome | 58CN - Xian#250

    回复 #249 @叶子

    所以你看已关闭的Issues了吗,看留言板的相关留言了吗🌚

  • 叶子
    叶子Windows 7Chrome | 58CN - Nanjing#251

    回复 #250 @Surmon

    没有找到相关问题的解答~~如何单独把文件类型上传到服务器呢?O(∩_∩)O谢谢

  • Gemini
    GeminiMac OS XChrome | 58CN - Shenzhen#255

    正在学习中!

  • wubin
    WubinWindows 7Chrome | 50CN - Tianjin#274

    请教大神一个问题呗:怎样处理调用接口返回的错误码?比如说接口服务器挂了,那在前端这边怎样处理以及怎样把错误信息显示给用户呢?谢谢您了

  • Surmon
    SurmonMac OS XChrome | 58CN - Fuzhou#287

    回复 #274 @wubin

    封装中间层,一般是拦截器的形式,过滤机制:HTTP => HTTP CODE => RESULT CODE

  • roy
    RoyMac OS XChrome | 58CN - Shenzhen#307

    这样的话,能不能把后端和前端做一台服务器上,也不需要跨域了!

  • jie
    JieMac OS XChrome | 57CN - Hangzhou#315

    请教个问题, /proxy/images2015.cnblogs.com/blog/801714/201707/801714-20170705194153050-969489879.png

    难道要把这么一堆文件扔到服务器上吗,这太不爽了。

  • Surmon
    SurmonMac OS XChrome | 59CN - Fuzhou#322

    回复 #315 @jie

    应该在服务器部署build进程,监听git hook

  • leolai
    LeolaiWindows 7Chrome | 60CN - Guangzhou#385

    请问大神,有什么第三方的支持ssr的ui框架吗,如element-ui,但element-ui貌似有问题?

  • Surmon
    SurmonMac OS XChrome | 62CN - Fuzhou#386

    回复 #385 @leolai

    不知道哎