用 Nuxt 开发部署一个 v2ex

先放出Github地址:https://github.com/OrangeXC/n2ex

里面有线上网站的链接,因为链接随时可能变,在这里不直接给网站链接。

之前写过一篇 nuxt 入门级的文章 Vue 基于 NUXT 的 SSR,主要说一下 nuxt 是什么,以及为什么使用。

这里声明一下,不建议去阅读上一篇文章,因为当时写博文的时候是 0.8.0 版本,目前是 1.0.0alpha4,已经有一部分改动,建议去看最新的nuxt文档

了解 nuxt 后,就可以轻松的看下文了,简单易懂,也没写什么复杂的项目。

本着自己学习的目的分享给大家,因为上篇文章之后有好多读者问 orange,怎么开发,怎么部署到服务器。

下面进入正题

环境搭建

nuxt 相关的脚手架已经集成到了 vue-cli,同时提供 starter、express、koa、adonuxt

这里我们用的是 koa2(脚手架会询问使用 koa1 或 koa2)

1
2
3
4
vue init nuxt/koa <project-name>
cd <project-name> # move to your project
npm install # or yarn install*[see note below]
npm run dev

此时监听 3000 端口,如果有 bug,别犹豫,先升级 node 版本到最新。

项目跑起来之后,有一个简单的轮廓,两个页面,index 和 about。

v2ex API

写一个三方 API 项目时,首先要看看人家都支持什么 API,才能决定我们如何展示页面。

来看看官方 API 文档

这个文档说来仔细,但是仅仅提供了 4 个 API,对于我们来说远远不够,那本站的 API 从哪里来的呢

Github 的确是个好网站,我找到了这个项目下的一个文件:https://github.com/ochapman/v2ex/blob/master/v2ex.go

不会 go 语言的没关系,我也不熟悉 go 语言,读一读会发现给出了比官方文档更多的 API,当然还有更详细的 API 暂且不谈。

本项目取的就是这个文件里(隐藏)的 API

  • 热门话题
  • 最新话题
  • 节点列表
  • 节点信息
  • 话题详情
  • 话题评论
  • 用户详情
  • 用户话题

我们也就实现了上面列表这么多接口的前端展示

路由结构

nuxt 的特点之一就是以目录结构划分路由。

router 由 pages 目录决定,那么分析接口可以得到以下目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pages
|
|-- member
| |
| |-- _name.vue
|
|-- node
| |
| |-- _name.vue
|
|-- topic
| |
| |-- _id.vue
|
|-- index.vue
|
|-- new.vue

很清晰的可以看出我们的路由结构,细心的会发现 params 有的是 name 有的是 id,为什么?

这里详细解释下,v2ex 接口提供了 id 和 name 两种 url 传参形式,任何一种查询都可以匹配结果,唯独 topic 只能 id 查询,因为 name 不唯一,那用户和节点也提供了 id 查询啊,这里的坑就在评论的 @ 部分,当 @ 一个人时,在评论可以直接链到个人详情页,v2ex 在评论里默认解析的就是 username 对应的链接,所以为了统一,其它地方也用的 name,另外无形当中提供了 search,在对应 url 后面替换成要查找的节点或用户名就可以直接跳转过去。

组件

这里只说两个最应该抽离的业务组件

  • 话题 list
  • 评论 list

话题 list 几乎每个列表页面里都有,而评论 list 在每个详情页里都有

基础组件用的是 muse-ui,比较喜欢 Material 整体的设计风格,刚好在 muse-ui 的 2.0.3 版本支持了 SSR。

下面说下引入三方库相关的问题

引入三方库

muse-ui 建议使用 plugins 的方式引入,因为涉及到 Vue.use 挂载方法

在 plugins 下新建 muse-ui.js 如下

1
2
3
4
import Vue from 'vue'
import MuseUI from 'muse-ui'

Vue.use(MuseUI)

然后在 nuxt.config.js 里面加上

1
2
3
plugins: [
{ src: '~plugins/muse-ui.js', ssr: true }
]

另外值得注意的是,需要全局引入 google 字体库,这里我直接插入到了 head 的 link 标签里

1
2
3
4
link: [
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700,400italic' },
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/icon?family=Material+Icons' }
]

http 请求用的是前后端同构的 axios 库

打包的时候注意要在配置文件加进去

1
2
3
build: {
vendor: ['axios']
}

异步请求

nuxt 提供了 asyncData,可以在页面加载之前请求数据。

在这里使用 es7 的 async/await 来实现数据请求

例如:pages/index.vue

1
2
3
4
5
6
7
8
9
10
11
async asyncData () {
try {
const { data } = await axios.get(`https://proxy-uuptfgaypk.now.sh/topics/hot.json`)

return {
hotList: data
}
} catch (err) {
console.error(err)
}
}

可读性还是挺高的,请求回来的 object,取到里面的 data 赋值给 hotList,省去了 .then 的操作

在详情页需要同时得到话题详细内容和评论,走的是两个接口

那么问题来了,怎么才能同时请求多个资源,当多个资源全部请求完成时才返回。

await 只能顺次请求,promis + await ???

不不不,只要 promis 的 all 方法就可以了,axios 有相应的封装

1
2
3
4
5
6
7
8
9
10
11
12
13
asyncData ({ params, error }) {
return axios.all([
axios.get(`https://proxy-uuptfgaypk.now.sh/topics/show.json?id=${params.id}`),
axios.get(`https://proxy-uuptfgaypk.now.sh/replies/show.json?topic_id=${params.id}`)
])
.then(axios.spread(function (detail, comments) {
return {
detail: detail.data[0],
comments: comments.data
}
}))
.catch(error => console.log(error))
}

这样一来解决了同时请求多个接口的问题。

CORS

跨域 http 请求,在这里不详细解释,给大家 MDN 链接

细心的小伙伴发现上文代码的 url 是 http://proxy...,为什么不是官方给的 https://www.v2ex.com/api

那是因为跨域请求时浏览器限制请求跨域资源,正常走官方的请求会报错,信息如下

1
XMLHttpRequest cannot load https://www.v2ex.com/api/topics/latest.json. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'localhost:3000' is therefore not allowed access.

报错很明显没有 Access-Control-Allow-Origin 返回头,打开控制台发现有数据返回,但是被浏览器拦截了,并没有加载到页面中去

初次玩服务端渲染的还会遇到的问题就是我什么首屏刷新不会报错,而路由跳转的请求会报错呢?

这要从服务端渲染机制说起,首屏的请求是在服务端完成,服务端不存在跨域问题,而接下来的交互操作和页面跳转是在浏览器端进行,所以产生了类似的问题。够简单直接吧,不相信的可以自己打 console,看是在终端控制台输出还是浏览器控制台输出。

找到了问题接下来就需要解决问题,上面有说在服务端不存在跨域请求的问题。

那么我们就自己写一层 proxy 就好啦,写一个 node 服务,转发请求,然后在返回头里加上,Access-Control-Allow-Origin: *

这个服务实际上不到十行的代码,用到两个依赖,express 和 request

1
2
3
4
5
6
7
8
9
10
const express = require('express')
const request = require('request')
const app = express()

app.use('/', function(req, res) {
const url = 'https://www.v2ex.com/api' + req.url
req.pipe(request(url)).pipe(res.set('Access-Control-Allow-Origin', '*'))
})

app.listen(process.env.PORT || 3001)

开发环境下先启动代理服务,然后将 url 指向本地服务就可以了

上面的方法呢,说实话有点蠢,实际项目当中呢可以直接让后端返回的接口支持跨域,当然了任何人都可以使用你们的 API,不是十分合理

再有就是 nuxt 官方有个 modules 组件库不知道大家有没有注意,地址:https://github.com/nuxt/modules

里面其中有 axios 和 proxy 的封装,意在解决 axios 的 baseUrl 和 proxy 跨域限制,安装配置都十分方便,本次为什么没用?

好问题,因为存在未知的坑,代码没有丝毫报错,就是不生效,只能静等 nuxt 官方修复主库与插件之间的 bug。

部署

怎么部署是大家最关心的问题,项目倒是好写,只要你会 vue 看看文档就可以写。

部署实际上官方提供了两个命令,打包和运行

1
2
npm run build
npm start

这里需要一个安装了 node 的服务器,可以安装一个 pm2 来跑 node 服务

当然喜欢 docker,也可以用 docker 去部署

之前又有人问了,看了这两个东西,依旧不会部署,那我也无能为力了,只能说,科学上网,教程一大堆。

如果就是想跑一个自己的 DEMO 玩玩,不想单独买服务器,也不涉及到企业项目部署和安全问题

那么好!给两个可以免费跑 node 服务的供应商 heroku 和 now.sh

nuxt 项目怎么如何跑在这两个服务上官网有写 https://zh.nuxtjs.org/faq/heroku-deployment

本项目是跑在 now.sh 上的,这也就解释了为什么说这个在线链接打开速度超级慢,因为我们用的是三方的免费服务,为了提高服务器资源的利用率,减小服务器压力,当一段时间没人访问网站时,会自动把网站设置为 frozen

1
2
3
The deployment n2ex-yrgirchtae.now.sh was frozen
The deployment proxy-uuptfgaypk.now.sh was frozen
The deployment n2ex-nzkjwvytxe.now.sh was unfrozen

这是我控制台的最新报告,当有人访问时会切换到 unfrozen,算了下默认 frozen 时间是 15min 内无访问后。

不知道 heroku 是不是也有类似问题

未来

这个项目会持续更新,逐步加新的功能,大家感兴趣的可以提 issue,或者直接提 pr 给我。

总结

从项目分析到开发部署上线,一个 nuxt 项目就这样完成了,开发遇到的坑也随着项目递进渗透进去了,项目十分简单,没使用 vuex,写到这里,依旧不推荐大家深入使用,但是十分推荐玩一玩,抛开了 SSR 复杂的那一面,用着还是挺爽的。

坚持原创技术分享,您的支持将鼓励我继续创作!