使用Nuxt.js改善现有项目

1 个月前

本文由票牛技术团队 @徐嘉轶 投稿。

缘由

票牛的移动站一直使用的是多页 web 应用的形式,分别使用 jade、less 和 es6+zepto 来分开书写页面、样式及脚本,再用 gulp 进行打包发布。api 接口则是和移动端保持同步,并在nginx 上做了反向代理以避免跨域问题。2017 年早些的时候,随着页面逻辑复杂度的上升,开始启用vue作为基础框架,并对部分老页面进行了重构。

这套方案工作得很不错,不过纯前端渲染有其天生的问题:搜索引擎爬虫抓不到啊。在这个问题上,业界有一些尝试,比如 Prerender.io,这些服务的思路基本就是发现爬虫过来的时候就把请求丢过去,他们会尝试用 PhantomJS 进行渲染,再把产生的 html 给回来。现代主流前端框架也都提供了服务端渲染 (Server Side Rendering) 的实现。

取舍上讲,我个人的看法是,如果只是要满足 SEO 的需求,Prerender 这样的方案是值得尝试的,然而实际上我们还有一些别的场景需要在页面标签里动刀子,比如想要配置 Open Graph 来使 iMessage 里的链接带预览图,就得知道苹果的爬虫长啥样,响应速度上也会欠佳一些。并且 SSR 也让开发者有更多的控制权,既然项目都已经迁到 Vue 上了,不如就试试看 2017 年的新科技吧。

Nuxt.js 是由一对法国的兄弟基于 vue 2.0 提供的 ssr 能力开发的框架,基于恰到好处的约定与配置,可以显著的降低开发者创建服务端渲染 web app 的门槛。如果你是从零开始,可以按照官方推荐的做法,使用 vue-cli 创建项目。我们因为是已有项目,所以做了这样几件事情:

初始化项目

Nuxt 默认的 srcDir 为 rootDir,这里我们的根目录已经有自己项目的内容了,于是建了个 nuxt 目录以区分对待,在 nuxt.config.js 做声明就好。middleware 下放一些中间件,用来解析当前页的城市信息等,配合 store 下的 action 存取,使得页面都可以访问这块信息。

pages 目录下则按照现有的 url 创建对应的文件,比如演出详情的 url 是 /activity/detail.html ,就创建 pages/activity/detail.html.vue,然后把之前的 jade、less、js 都合到这个文件中去。

改造网络请求

服务端渲染的话,一开始页面的数据也都是由服务端发起的了。之前对网络请求做过一层封装,使用自己的 fetch 模块,现在要做的事情就是在 fetch 的实现中区分客户还是服务端,切换实现就好。服务端我们使用了 axios 作为请求库,客户端就还是保留 zepto。可以通过 process.server / process.browser 来进行判断。

Nuxt 对 vue 的配置做了自己的扩展,作为页面入口的 vue 文件会多出 asyncData、head 等配置项,asyncData 即是我们发请求获取数据的地方。

适配现有组件

除了页面入口之外,其他的组件都可以在浏览器和服务端复用。需要改造的主要有两处:

  1. 组件内使用到浏览器相关api的部分需要判断环境,比如初始化 iscroll,测量宽度等
  2. 原来组件内直接从 url 上获取 query 参数的地方现在要改为由父级传入

这里遇到比较多的是 zepto 相关,一开始逐个判断

var$;if(process.browser){$=require('zepto')}

后来嫌麻烦,就做了个 $.js,把这段逻辑放进去,使用的地方直接:

var$=require('$')

如果量比较大的话可以狸猫换太子,把原来的 zepto 改为 zepto-origin,再建一个 zepto 做以上事情,业务代码就可以不用动了。

配置路由

路由优化也是 SEO 的一部分,虽然感觉如何优化基本上是玄学,不过好些策略又似乎确实有效果。

具体来讲,我们要做的就是把 http://m.piaoniu.com/activity/category-home.html?categoryId=2 这样的链接 变成 http://m.piaoniu.com/sh-dramas 这样,这也是使用 SSR 方案有更大的控制权才能做的优化。

对于简单的需求,Nuxt 支持配置如下的文件结构来支持:

pages/
--| activity/
-----| _id.vue

会生成如下配置:

router: {
  routes: [
    {
      name: 'activity-id',
      path: '/users/:id?',
      component: 'pages/activity/_id.vue'
    }
  ]
}

如果满足不了需要,也可以自行在 nuxt.config.js 中扩展,比如前文提到的场景我们是这么配置的:

router: {
  extendRoutes (routes, resolve) {
    // ...
    routes.push({
      name: 'category-home',
      path: '/:city-:category/:filter?',
      component: resolve(__dirname, 'nuxt/pages/activity/category-home.html.vue')
    })
    // ...
  }
}

参考: Routing - Nuxt.js

更新发布脚本

原先的发布脚本做的事情是把静态资源构建好,然后把普通资源和html先后分别发到不同的nginx服务器上,完事儿。现在多了一部,发现目录下存在nuxt.config.js,则执行

npm run nuxt-build

之后把项目整个目录打个压缩包,传到服务器上解压,并通过 pm2 做平滑重启。

nginx 上也要做相应的改动:先尝试 try_file,如果找不到,则转发给 node 服务进行服务端渲染,相应的错误页面也由 node 服务提供了。

这里由于我们要兼容已有的页面比如 activity/detail.html,而之前发不过的文件是不删的,这就需要在 nginx 上额外对这些页面进行配置。平稳运行一段时间之后,可以在项目和服务器上把这些文件删除,以简化配置。

添加监控

既然要负责服务端渲染了,那么相应的服务质量监控也要接手起来。后端我们使用的是点评出品的 CAT,nodejs也有对应的客户端:cat-client ,使用起来并不麻烦,倒是寻找 Nuxt 错误页的切入点花了一些功夫。官方没有给到推荐的做法,研究了下源码自己对错误入口做了切入。

Renderer.prototype.errorMiddleware = (err, req, res, next) => {
  Cat.logError(req.url, err)
  // 给用户展示错误页面,自己则可以更方便的看到具体错误信息
  if (req.cookies.ERROR_VISIBLE) {
    res.end(err.stack)
  } else {
    res.status(500).render('error', {
      code: 500,
      error: '出错了'
    })
  }
}

另外,利用 CAT 提供的 Transaction 功能,也可以方便的看到服务的性能表现如何,瓶颈在哪里,看起来长这样:

v2-ba4c31a6282187b83010b78273173dc1_r.dc6bec7fa520.jpg

嗯,返回一个演出详情耗时 160 毫秒,还不错。

2
推荐阅读