原文连接javascript
Vue 开发一个单页面应用,相信不少前端工程师都已经学会了,可是单页面应用有一个致命的缺点,就是 SEO 极不友好。除非,vue 能在服务端渲染(ssr)并直接返回已经渲染好的页面,而并不是只是一个单纯的 <div id="app"></div>
。css
Nuxt.js 就是一个极简的 vue 版的 ssr 框架。基于它,咱们能够快速开发一个基于 vue 的 ssr 单页面应用。html
Nuxt.js 官方提供了一个模板,可使用 vue-cli 直接安装。前端
$ vue init nuxt-community/starter-template <project-name>
. ├── README.md ├── assets ├── components ├── layouts ├── middleware ├── node_modules ├── nuxt.config.js ├── package.json ├── pages ├── plugins ├── static ├── store └── yarn.lock
其中:vue
<nuxt />
标签中。若是须要在普通页面中使用下级路由,则须要在页面中添加 <nuxt-child />
。该目录名为Nuxt.js保留的,不可更改。 middleware: 'middlewareName'
。pages: 页面。一个 vue 文件即为一个页面。index.vue 为根页面。java
_id.vue
(以 _
开头),则为动态路由页面,_
后为匹配的变量(params)。index.vue
。更多的配置请移步至 官网 。根vue.js应用
实例化以前须要运行的 Javascript 插件。须要注意的是,在任何 Vue 组件的生命周期内, 只有 beforeCreate
和 created
这两个钩子方法会在 客户端和服务端均被调用。其余钩子方法仅在客户端被调用。 nuxt.config.js
文件用于组织Nuxt.js 应用的个性化配置,以便覆盖默认配置。具体配置请移步至 官网。首先,了解一下在 nuxt 的页面中独有的函数/变量:node
asyncData
方法使得你可以在渲染组件以前异步获取数据。该方法在服务端中执行的,因此,请求数据时,不存在跨域问题。返回的数据将与 data()
返回的数据进行合并。因为asyncData
方法是在组件 初始化 前被调用的,因此在方法内是没有办法经过 this
来引用组件的实例对象。webpack
context
变量的可用属性一览:ios
属性字段 | 类型 | 可用 | 描述 |
---|---|---|---|
isClient |
Boolean | 客户端 & 服务端 | 是否来自客户端渲染 |
isServer |
Boolean | 客户端 & 服务端 | 是否来自服务端渲染 |
isDev |
Boolean | 客户端 & 服务端 | 是不是开发(dev) 模式,在生产环境的数据缓存中用到 |
route |
vue-router 路由 | 客户端 & 服务端 | vue-router 路由实例。 |
store |
vuex 数据流 | 客户端 & 服务端 | Vuex.Store 实例。只有vuex 数据流存在相关配置时可用。 |
env |
Object | 客户端 & 服务端 | nuxt.config.js 中配置的环境变量, 见 环境变量 api |
params |
Object | 客户端 & 服务端 | route.params 的别名 |
query |
Object | 客户端 & 服务端 | route.query 的别名 |
req |
http.Request | 服务端 | Node.js API 的 Request 对象。若是 nuxt 以中间件形式使用的话,这个对象就根据你所使用的框架而定。nuxt generate 不可用。 |
res |
http.Response | 服务端 | Node.js API 的 Response 对象。若是 nuxt 以中间件形式使用的话,这个对象就根据你所使用的框架而定。nuxt generate 不可用。 |
redirect |
Function | 客户端 & 服务端 | 用这个方法重定向用户请求到另外一个路由。状态码在服务端被使用,默认 302。redirect([status,] path [, query]) |
error |
Function | 客户端 & 服务端 | 用这个方法展现错误页:error(params) 。params 参数应该包含 statusCode 和 message 字段。 |
fetch 方法用于在渲染页面前填充应用的状态树(store)数据, 与 asyncData 方法相似,不一样的是它不会设置组件的数据。为了让获取过程能够异步,你须要返回一个 Promise,Nuxt.js 会等这个 promise 完成后再渲染组件。git
fetch 会在组件每次加载前被调用(在服务端或切换至目标路由以前)。
Nuxt.js 使用了 vue-meta
更新应用的 头部标签(Head)
和 html 属性
。
用于更新 头部信息。如 title,descripe 等。在 head
方法里可经过 this
关键字来获取组件的数据。
指定该页面使用哪一个布局文件。默认值为 default
。
须要执行的中间件,如鉴权的 auth
等。
指定页面切换时的动画效果。支持传入 String
, Object
, Function
。具体配置请移步至 官网 。
Nuxt.js 可让你在动态路由对应的页面组件中配置一个校验方法用于校验动态路由参数的有效性。
返回 true
说明路由有效,则进入路由页面。返回不是 true
则显示 404 页面。
在这里,咱们使用 CNode API 进行开发 Demo.
请求数据,咱们使用 Nuxt 官方提供的 @nuxtjs/axios 安装后,在 nuxt.config.js 中加上:
export default { ... modules: [ '@nuxtjs/axios' ], axios: { baseURL: 'https://cnodejs.org/api/v1', // or other axios configs. } ... }
就能够在页面中经过 this.$axios.$get
来获取数据,不须要在每一个页面都单独引入 axios.
须要先安装 sass-loader 和 node-sass
$ yarn add sass-loader node-sass --dev
若是须要在项目中全局使用某个 scss 文件(如 mixins, vars 等),须要借助 sass-resources-loader : yarn add sass-resources-loader —dev
, 还须要在 nuxt.config.js 的 build 配置中调整导出的 loader 配置:
export default { ... build: { extend(config, { isDev, isClient }) { const sassResourcesLoader = { loader: 'sass-resources-loader', options: { resources: [ // 填写须要全局注入 scss 的文件。引入后,全部页面均有效。 'assets/styles/mixins.scss' ] } } // 修改 scss sass 引用的 loader。 config.module.rules.forEach((rule) => { if (rule.test.toString() === '/\\.vue$/') { rule.options.loaders.sass.push(sassResourcesLoader) rule.options.loaders.scss.push(sassResourcesLoader) } if (['/\\.sass$/', '/\\.scss$/'].indexOf(rule.test.toString()) !== -1) { rule.use.push(sassResourcesLoader) } }) } } ... }
首页通常只须要简单的获取首页数据并渲染便可。
主要 代码:
asyncData({app, query}) { console.log(query) // 根据不用的标签获取不一样的数据,最后返回话题列表。 return app.$axios.$get(`topics?tab=${query.tab || ''}`).then(res => { // console.log(res) // console.log(JSON.parse(res)) return {list: res.data} }) }
当进入首页时,该函数会被执行, nuxt 会等到获取数据后再和组件的 data 合并,进而渲染数据。在模板中,能够直接使用 list 变量获取数据。
<div class="card fluid topic" v-for="topic in list" :key="topic.id" > <div class="section"> <h3><nuxt-link :to="{name: 'topic-id', params: {id: topic.id}}" class="topic-title">{{topic.title}}</nuxt-link></h3> <p class="topic-info"> <mark v-if="topic.top" class="tertiary">精华</mark> <mark v-else>{{tabsObj[topic.tab]}}</mark> <span class="avatar"> <img :src="topic.author.avatar_url" alt=""> </span> <span class="username"> {{topic.author.loginname}} </span> </p> </div> </div>
在这里说起一下, <nuxt-link />
和 <a />
的区别是: nuxt-link
走的是 vue-router 的路由,即网页已为单页面,而且浏览器不会重定向。而 a
标签走的是 window.location.href
,每一次点击 a
标签后的页面,都会进行一次服务端渲染,和普通的 PHP 混合开发没有太大的区别。
在这里使用了 nuxt-link
是由于 CNode 的 API 不存在跨域问题,所以能够做为一个单页面应用,体验更好。
由于列表页数据类型有多种,该页面可能会被复用,因此当路由对象发生变化时,须要从新获取数据,这时能够监听路由的变化以作出响应:
watch: { '$route': function() { console.log('$route has changed.') this.getData() } }
配置 seo 优化(这里只是单纯的复制罢了,demo 使用,侵删):
head() { return { title: '首页' + (this.$route.query.tab ? `- ${this.tabsObj[this.$route.query.tab]}` : ''), meta: [{ hid: 'description', name: 'description', content: 'CNode:Node.js专业中文社区' }] } }
一样的,使用 asyncData
函数进行获取数据,再渲染页面。
asyncData({app, params}) { console.log(params) return app.$axios.$get('topic/' + params.id).then(res => { // let data = res.data instanceof String ? JSON.parse(res.data) : res.data let data = res.data // console.log(res) // let div = document.createElement('div') // div.innerHTML = res.data.data.content // res.data.summary = div.innerText.substr(0, 120) data.summary = data.content.replace(/<[^>]+>/g,"").substr(0, 120).replace(/\s+/g, '') return {detail: data} }).catch(err => { console.log('axios.get failed.') console.error(err) }) }
在这里,踩过坑。想使用 div 的 innerText 来过滤掉正文中的 HTML 标签,可是,若是用户是直接进入这个页面的时候,执行 asyncData
时,document
对象是不存在的,从而会报错。也就是说,当 asyncData
在服务端执行时,是没有 document
和 window
对象的,请你们注意一下。
做为一个社区,seo 尤其重要,假若每一个页面都须要写一大堆的 head 对象,就会显得尤为的繁琐。因此能够借助 nuxt 的 plugin 机制,将其封装成一个函数,并注入到每个页面当中:
// plugins/global.js import Vue from 'vue' Vue.mixin({ methods: { // 必传 标题,描述。其余的 meta 标签经过 payload 注入,其中,每一个 meta 的 hid 须要是惟一的。 $seo(title, content, payload = []) { return { title, meta: [{ hid: 'description', name: 'description', content }].concat(payload) } } } })
在 nuxt.config.js 中加上:
export default { plugins: [ '~plugins/global.js' ] }
这样,只须要在页面的 head
的函数中,返回该函数便可:
head() { return this.$seo(this.detail.title, this.detail.summary) }
可见,详情页已经成功的设置了部分 seo 的标签。
以上是 Nuxt 的一些基础配置及应用。
我再去研究一下, fetch 和 store 的结合,将该 demo 继续完善。