前言
作为一位 Vuer(vue开发者),如果还不会这个框架,那么你的 Vue
技术栈还没被点亮。
Nuxt.js 是什么
Nuxt.js 官方介绍:
Nuxt.js 是一个基于 Vue.js 的通用应用框架。
通过对客户端/服务端基础架构的抽象组织,Nuxt.js 主要关注的是应用的 UI渲染。
我们的目标是创建一个灵活的应用框架,你可以基于它初始化新项目的基础结构代码,或者在已有 Node.js 项目中使用 Nuxt.js。
Nuxt.js 预设了利用 Vue.js 开发服务端渲染的应用所需要的各种配置。
如果你熟悉 Vue.js
的使用,那你很快就可以上手 Nuxt.js
。开发体验也和 Vue.js
没太大区别,相当于为 Vue.js
扩展了一些配置。当然你对 Node.js
有基础,那就再好不过了。
Nuxt.js 解决什么问题
现在 Vue.js
大多数用于单页面应用,随着技术的发展,单页面应用已不足以满足需求。并且一些缺点也成为单页面应用的通病,单页面应用在访问时会将所有的文件进行加载,首屏访问需要等待一段时间,也就是常说的白屏,另外一点是总所周知的 SEO
优化问题。
Nuxt.js
的出现正好来解决这些问题,如果你的网站是偏向社区需要搜索引擎提供流量的项目,那就再合适不过了。
我的第一个 Nuxt.js 项目
我在空闲的时间也用 Nuxt.js
仿掘金 web
网站:
nuxt-juejin-project
是一个使用 Nuxt.js
仿写掘金的学习项目,主要使用 :nuxt
+ koa
+ vuex
+ axios
+ element-ui
。该项目所有数据与掘金同步,因为接口都是通过 koa
作为中间层转发。主要页面数据通过服务端渲染完成。
在项目完成后的几天,我将记录的笔记整理一下,并加入一些常用的技术点,最后有了这篇文章,希望能够帮到正在学习的小伙伴。
项目介绍里有部分截图,如果jio得可以,请来个 star~
项目地址:https://github.com/ChanWahFung/nuxt-juejin-project
基础应用与配置
项目的搭建参照官网指引,跑个项目相信难不到你们,这里不赘述了。
️跑起来 https://www.nuxtjs.cn/guide/installation
关于项目的配置,我选择的是:
- 服务端:Koa
- UI框架:Element UI
- 测试框架:None
- Nuxt模式:Universal
- 使用集成的 Axios
- 使用 EsLint
context
context 是从 Nuxt 额外提供的对象,在”asyncData”、”plugins”、”middlewares”、”modules” 和 “store/nuxtServerInit” 等特殊的 Nuxt 生命周期区域中都会使用到 context。
所以,想要使用 Nuxt.js
,我们必须要熟知该对象的有哪些可用属性。
context
官方文档描述戳这里 https://www.nuxtjs.cn/api/context
下面我列举几个在实际应用中比较重要且常用的属性:
app
app
是 context
中最重要的属性,就像我们 Vue
中的 this
,全局方法和属性都会挂载到它里面。因为服务端渲染的特殊性,很多Nuxt
提供的生命周期都是运行在服务端,也就是说它们会先于 Vue
实例的创建。因此在这些生命周期中,我们无法通过 this
去获取实例上的方法和属性。使用 app
可以来弥补这点,一般我们会把全局的方法同时注入到 this
和 app
中,在服务端的生命周期中使用 app
去访问该方法,而在客户端中使用 this
,保证方法的共用。
举个例子:
假设 $axios
已被同时注入,一般主要数据通过 asyncData
(该生命周期发起请求,将获取到的数据交给服务端拼接成html返回) 去提前请求做服务端渲染,而次要数据通过客户端的 mounted
去请求。
export default {
async asyncData({ app }) {
// 列表数据
let list = await app.$axios.getIndexList({
pageNum: 1,
pageSize: 20
}).then(res => res.s === 1 ? res.d : [])
return {
list
}
},
data() {
return {
list: [],
categories: []
}
},
async mounted() {
// 分类
let res = await this.$axios.getCategories()
if (res.s === 1) {
this.categories = res.d
}
}
}
store
store
是 Vuex.Store
实例,在运行时 Nuxt.js
会尝试找到是应用根目录下的 store
目录,如果该目录存在,它会将模块文件加到构建配置中。
所以我们只需要在根目录的 store
创建模块js文件,即可使用。
/store/index.js
:
export const state = () => ({
list: []
})
export const mutations = {
updateList(state, payload){
state.list = payload
}
}
而且 Nuxt.js
会很贴心的帮我们将 store
同时注入,最后我们可以在组件这样使用::
export default {
async asyncData({ app, store }) {
let list = await app.$axios.getIndexList({
pageNum: 1,
pageSize: 20
}).then(res => res.s === 1 ? res.d : [])
// 服务端使用
store.commit('updateList', list)
return {
list
}
},
methods: {
updateList(list) {
// 客户端使用,当然你也可以使用辅助函数 mapMutations 来完成
this.$store.commit('updateList', list)
}
}
}
为了明白 store
注入的过程,我翻阅 .nuxt/index.js
源码(.nuxt
目录是 Nuxt.js
在构建运行时自动生成的),大概知道了流程。首先在 .nuxt/store.js
中,对 store
模块文件做出一系列的处理,并暴露 createStore
方法。然后在 .nuxt/index.js
中,createApp
方法会对其同时注入:
import { createStore } from './store.js'
async function createApp (ssrContext) {
const store = createStore(ssrContext)
// ...
// here we inject the router and store to all child components,
// making them available everywhere as `this.$router` and `this.$store`.
// 注入到this
const app = {
store
// ...
}
// ...
// Set context to app.context
// 注入到context
await setContext(app, {
store
// ...
})
// ...
return {
store,
app,
router
}
}
除此之外,我还发现 Nuxt.js
会通过 inject
方法为其挂载上 plugin
(plugin
是挂载全局方法的主要途径,后面会讲到,不知道可以先忽略),也就是说在 store
里,我们可以通过 this
访问到全局方法:
export const mutations = {
updateList(state, payload){
console.log(this.$axios)
state.list = payload
}
}
params、query
params
和 query
分别是 route.params
和 route.query
的别名。它们都带有路由参数的对象,使用方法也很简单。这个没什么好说的,用就完事了。
export default {
async asyncData({ app, params }) {
let list = await app.$axios.getIndexList({
id: params.id,
pageNum: 1,
pageSize: 20
}).then(res => res.s === 1 ? res.d : [])
return {
list
}
}
}
redirect
该方法重定向用户请求到另一个路由,通常会用在权限验证。用法:redirect(params)
,params
参数包含 status
(状态码,默认为302)、path
(路由路径)、query
(参数),其中 status
和 query
是可选的。当然如果你只是单纯的重定向路由,可以传入路径字符串,就像 redirect('/index')
。
举个例子:
假设我们现在有个路由中间件,用于验证登录身份,逻辑是身份没过期则不做任何事情,若身份过期重定向到登录页。
export default function ({ redirect }) {
// ...
if (!token) {
redirect({
path: '/login',
query: {
isExpires: 1
}
})
}
}
error
该方法跳转到错误页。用法:error(params)
,params
参数应该包含 statusCode
和 message
字段。在实际场景中,总有一些不按常理的操作,页面因此无法展示真正想要的效果,使用该方法进行错误提示还是有必要的。
举个例子:
标签详情页面请求数据依赖于 query.name
,当 query.name
不存在时,请求无法返回可用的数据,此时跳转到错误页
export default {
async asyncData({ app, query, error }) {
const tagInfo = await app.$api.getTagDetail({
tagName: encodeURIComponent(query.name)
}).then(res => {
if (res.s === 1) {
return res.d
} else {
error({
statusCode: 404,
message: '标签不存在'
})
return
}
})
return {
tagInfo
}
}
}
Nuxt常用页面生命周期
asyncData
你可能想要在服务器端获取并渲染数据。Nuxt.js添加了asyncData方法使得你能够在渲染组件之前异步获取数据。
asyncData
是最常用最重要的生命周期,同时也是服务端渲染的关键点。该生命周期只限于页面组件调用,第一个参数为 context
。它调用的时机在组件初始化之前,运作在服务端环境。所以在 asyncData
生命周期中,我们无法通过 this
引用当前的 Vue
实例,也没有 window
对象和 document
对象,这些是我们需要注意的。
一般在 asyncData
会对主要页面数据进行预先请求,获取到的数据会交由服务端拼接成 html
返回前端渲染,以此提高首屏加载速度和进行 seo
优化。
看下图,在谷歌调试工具中,看不到主要数据接口发起请求,只有返回的 html
文档,证明数据在服务端被渲染。
最后,需要将接口获取到的数据返回:
export default {
async asyncData({ app }) {
let list = await app.$axios.getIndexList({
pageNum: 1,
pageSize: 20
}).then(res => res.s === 1 ? res.d : [])
// 返回数据
return {
list
}
},
data() {
return {
list: []
}
}
}
值得一提的是,asyncData
只在首屏被执行,其它时候相当于 created
或 mounted
在客户端渲染页面。
什么意思呢?举个例子:
现在有两个页面,分别是首页和详情页,它们都有设置 asyncData
。进入首页时,asyncData
运行在服务端。渲染完成后,点击文章进入详情页,此时详情页的 asyncData
并不会运行在服务端,而是在客户端发起请求获取数据渲染,因为详情页已经不是首屏。当我们刷新详情页,这时候详情页的 asyncData
才会运行在服务端。所以,不要走进这个误区(诶,不是说服务端渲染吗,怎么还会发起请求?)。
fetch
fetch 方法用于在渲染页面前填充应用的状态树(store)数据, 与 asyncData 方法类似,不同的是它不会设置组件的数据。
查看官方的说明,可以得知该生命周期用于填充 Vuex
状态树,与 asyncData
同样,它在组件初始化前调用,第一个参数为 context
。
为了让获取过程可以异步,你需要返回一个 Promise
,Nuxt.js
会等这个 promise
完成后再渲染组件。
export default {
fetch ({ store, params }) {
return axios.get('http://my-api/stars')
.then((res) => {
store.commit('setStars', res.data)
})
}
}
你也可以使用 async 或 await 的模式简化代码如下:
export default {
async fetch ({ store, params }) {
let { data } = await axios.get('http://my-api/stars')
store.commit('setStars', data)
}
}
但这并不是说我们只能在 fetch
中填充状态树,在 asyncData
中同样可以。
validate
Nuxt.js 可以让你在动态路由对应的页面组件中配置一个校验方法用于校验动态路由参数的有效性。
在验证路由参数合法性时,它能够帮助我们,第一个参数为 context
。与上面有点不同的是,我们能够访问实例上的方法 this.methods.xxx
。
打印 this
如下:
生命周期可以返回一个 Boolean
,为真则进入路由,为假则停止渲染当前页面并显示错误页面:
export default {
validate({ params, query }) {
return this.methods.validateParam(params.type)
},
methods: {
validateParam(type){
let typeWhiteList = ['backend', 'frontend', 'android']
return typeWhiteList.includes(type)
}
}
}
或者返回一个Promise:
export default {
validate({ params, query, store }) {
return new Promise((resolve) => setTimeout(() => resolve()))
}
}
还可以在验证函数执行期间抛出预期或意外错误:
export default {
async validate ({ params, store }) {
// 使用自定义消息触发内部服务器500错误
throw new Error('Under Construction!')
}
}
watchQuery
监听参数字符串更改并在更改时执行组件方法 (asyncData, fetch, validate, layout, …)
watchQuery
可设置 Boolean
或 Array
(默认: [])。使用 watchQuery
属性可以监听参数字符串的更改。 如果定义的字符串发生变化,将调用所有组件方法(asyncData
, fetch
, validate
, layout
, …)。 为了提高性能,默认情况下禁用。
在 nuxt-juejin-project
项目的搜索页中,我也用到了这个配置:
<template>
<div class="search-container">
<div class="list__header">
<ul class="list__types">
<li v-for="item in types" :key="item.title" @cli