Nuxt.js总结

简单说nuxtjs项目,它其实就是一个vue的项目融合一个node.js server项目,这里node服务有两个作用,第一点是代替浏览器的工作,笼统理解就是在created时的请求数据和页面渲染,第二点是当作静态文件服务器,把渲染好的页面返回给用户。

SEO

大家都知道,网页在搜索引擎中能被搜索出来是因为他们有自己的爬虫,可以爬取网页中的内容并保存下来,但是它们只会保存网页中的第一个请求返回的内容,即HTML文档。

在传统的前后端不分离网站中,在浏览器上查看源代码,可以看到网页上有什么内容,源代码中就有与之对应的代码。随着前后端分离开发越来越流行,也衍生了越来越多的单页面应用(SPA),在浏览器中打开一个SPA的源代码,往往只能看到寥寥无几的几个标签,其余内容全都是由JS渲染的,也就是客户端渲染。因此单页面应用的一大痛点就是搜索引擎无法收录,有痛点就会有相应的方案产生,出现了SSR。

SSR

SSR意思是服务端渲染,乍一看会以为是回到以前的前后不分离开发模式,实际上相比以前,客户端与服务端之间多了一个中间层,用于接收数据并渲染页面返回到客户端。

有了中间层帮客户端渲染页面,在搜索引擎爬到页面时,就解决了前后端分离页面不收录的的痛点。不仅如此,由于中间层也是一个服务端,前端工程师可以做更多事儿了,比如简单处理数据、做代理等等。

Nuxt.js

Vue官方提供了vue-server-renderer模块实现SSR,而Nuxt.js是对其封装后的框架,用于提供一个开箱即用的NodeJS SSR服务。

下图展示了从客户端请求到Nuxt.js进行服务端渲染的整体的工作流程:

  1. 用户打开浏览器,输入网址请求到Node.js
  2. 部署在Node.js的应用Nuxt.js接收浏览器请求,并请求服务端获取数据
  3. Nuxt.js获取到数据后进行服务端渲染
  4. Nuxt.js将html网页响应给浏览器

生命周期

通过在地址栏输入链接,或者从其他网站点击链接跳转到Nuxt站点的页面时(首屏访问),呈现的第一个页面的数据会由中间层向服务端发起请求,然后渲染Vue组件,再将渲染完成后生成的HTML文档返回到客户端。随后点击页面的<NuxtLink>组件或者通过vue-router的push,replace等方法跳转页面都是在客户端完成的,除非刷新浏览器,否则不会再次访问中间层服务器。因此在页面中请求服务端的数据有可能是中间层发起的,也有可能是客户端发起的。

访问Nuxt站点的页面时,会经历以下几个生命周期,我们可以在不同的节点对站点的行为做处理:

  • 按照nuxt.config.js中的plugins配置顺序执行插件。
  • nuxtServerInit:如果Nuxt应用中使用了Vuex,会运行actions中的nuxtServerInit函数,用于在进入页面之前将数据填充到Vuex中。
  • Middleware:按顺序执行nuxt.config.js中配置的router中间件、layout中间件、page中间件,可以用来对用户做权限验证。
  • validate:返回true、false或者Promise,返回false时,将会跳转到错误页面,主要用于验证动态路由的参数。
  • asyncData:返回一个对象,执行完毕之后Nuxt会将返回的对象合并到Vue实例的data属性中,用于在访问到页面之前调用接口获取数据,这个钩子取代了SPA中常用的created钩子。
  • beforeCreate:Vue组件生命周期钩子。
  • created:Vue组件生命周期钩子。
  • fetch:与asyncData一样,也是用于调用接口获取数据,不同的是它不会将返回值合并到data,一般用于对本地数据处理,比如Vuex,值得一提的是,这个钩子在Vue实例创建之后执行,因此可以用this访问Vue实例。
  • 执行beforeMount、mounted等其他Vue的生命周期钩子。

Nuxt扩展了Vue的生命周期,大概如下:

export default {
  middleware () {}, //服务端
  validate () {}, // 服务端
  asyncData () {}, //服务端
  fetch () {}, // store数据加载
  beforeCreate () {  // 服务端和客户端都会执行},
  created () { // 服务端和客户端都会执行 },
  beforeMount () {}, 
  mounted () {} // 客户端
}

以上生命周期中,middleware、validate、asyncData是完全在中间层执行的;beforeCreate、created先在中间层执行,然后在客户端还会执行一遍,相同的数据显示时客户端执行的结果会覆盖中间层执行的结果,但页面源码中数据是中间层执行的结果(一般情况下,这两者是一致的,除非你用Math.random()之类的函数生成数据);beforeMount、mounted只在客户端执行,因此搞懂哪些代码什么时候在什么环境运行非常重要。

插件

vant-ui

项目中使用了vant ui作为组件库,Nuxt项目中没有类似main.js的入口文件,如果需要安装依赖,需要在plugins中声明。

每次访问Nuxt站点的首屏时,都会将nuxt.config.js中声明的plugins执行一遍,从而让开发这可以把自己所需的插件注册到应用中

// nuxt.config.js

module.exports = {
    ...,
plugins: [
    '@/plugins/vant-ui'
],
    ...
}

 在根目录的plugins目录下需要创建一个对应的文件

// /plugins/vant-ui.js

import Vue from 'vue'
import { Button, Search, Toast } from 'vant'

Vue.use(Button)
Vue.use(Search)
Vue.use(Toast)

为了按需引入,还需要安装babel-plugin-import和进行相关配置

// nuxt.config.js

module.exports = {
	build: {
		analyze: true,
		transpile: [/vant.*?less/],
		babel: {
			// 按需引入配置
			plugins: [
				[
					'import',
					{
						libraryName: 'vant',
						style: name => ` ${name}/style/less.js`
					},
					'vant'
				]
			]
		},
		loaders: {
			less: {
				lessOptions: {
					modifyVars: {
						// 此处可以修改vant样式中的less变量,从而自定义ui样式
						'@button-primary-background-color': '#000'
					}
				}
			}
		}
	}
}

axios

Nuxt官方提供了专门封装的@nuxtjs/axios模块,看过文档后发现与平常的用法略有不同,闲麻烦就还是用了原汁原味的axios,毕竟此前也做了进一步的封装(官方强烈推荐的模块尽量能用就用,像我这种不听话的就会默默踩坑,经过探索才悟出了下面这段话)。但是与SPA不同的是,axios发起的请求可能是在中间层服务端,也可能是在客户端,这取决于是否是首屏渲染(在上面生命周期有提到),因此需要在代码中利用process.client和process.server对相应的环境做出判断和处理。

import axios from 'axios'
import { Toast } from 'vant'

const { CancelToken } = axios

// 用于获取在nuxt.config.js配置中的baseURL
const nuxtConfig = require('../nuxt.config')

// 让请求函数带有axios cancelToken
export const cancelToken = fn => {
	const newFn = (...arg) => {
		// 每个cancelToken只能使用一次,之后会保持状态
		// 因此每次发起请求都创建一个新的cancelToken
		const source = CancelToken.source
		newFn.token = source.token
		newFn.cancel = source.cancel
		return fn(...arg)
	}

	return newFn
}

// 创建实例
function createAxios {
	const instance = axios.create({
		baseURL: '/api', // 中间层代理地址
		timeout: 10000
	})

	// 请求拦截
	instance.interceptors.request.use(config => {
		// 服务端不需要代理
		if (process.server) {
			config.baseURL = nuxtConfig.env.baseUrl
		}
		return config
	}, err => {
		return Promise.reject(err)
	})

	// 响应拦截
	instance.interceptors.response.use(res => {
		const { data } = res

		if (data.code !== 200) {
			if (process.server) {
				// 在plugins中,我把context挂载到了axios实例上
				// 在服务端发起请求出错时,可以跳转到错误页面
				instance.nuxtContext.error({
					statusCode: 500,
					message: ''
				})
			} else {
				Toast(data.msg)
			}
			return Promise.reject(res)
		}
		return data.data
	}, err => {
		const { response } = err

		// 服务端和客户端有不一样的错误处理方式
		if (process.client) {
			if (response) {
				switch (response.status) {
					case401:
						// 未登录
						Toast('请先登录')
					break

					case403:
						// 操作被拒绝(没有相应权限)
						Toast('您的操作被拒绝')
					break

					case404:
						Toast('未找到资源')
					break

					case500:
						Toast('系统出错了')
					break
				}
				return Promise.reject(response)
			}

			// 取消请求
			if (axios.isCancel(err)) {
				console.log('请求取消')
				return Promise.reject(err)
			}

			// 请求超时
			if (err.message ? .includes('timeout')) {
				Toast('网络超时,请重试')
				return Promise.reject(err)
			}

			if (!window.navigator.onLine) {
				// 断网
				Toast('请检查网络')
				return Promise.reject(err)
			} else {
				Toast('未知错误')
				return Promise.reject(err)
			}
		} else {
			console.log(response)
			instance.nuxtContext.error({
				statusCode: response.status,
				message: ''
			})
		}
	})
	return instance
}

export default createAxios

添加一个插件将nuxt context挂载到axios实例上,用于出错时跳转到错误页面,此外需要记得在nuxt.config.js中声明此插件。

// /plugins/axios.js
import axios from '@/utils/request'

// 将context对象挂载到axios实例上
export default context => {
	axios.nuxtContext = context
}

Vuex持久化

在SPA中,通常会将token存储在localStorage中,但是访问SSR站点首屏时,需要在中间层做登录等权限验证,无法获取到客户端存在localStorage中的数据。但是可以将其存在cookie中,利用cookie的特性,在访问站点时将其带到中间层,于是中间层就可以获取cookie中的数据做权限验证,并且将其填充进Vuex供客户端使用。

首先安装vuex-persistedstate和js-cookie,把Vuex中的数据同步到cookie,然后写一个插件将vuex-persistedstate注入到Vuex中(别忘了在nuxt.config.js中声明)。

// plugins/vuex-persistedstate.js
import createPersistedState from 'vuex-persistedstate'
import Cookies from 'js-cookie'

export default({ store }) => {

	// “将Vuex中的数据同步到cookie中”这种事只有客户端会做
	if (!process.client) {
		return
	}

	create PersistedState({
		// 通过配置修改vuex-persistedstate的读写行为
		// 将操作目标改为cookie
		storage: {
			getItem: key => {
				returnCookies.get(key)
			},
			setItem: (key, value) => {
				Cookies.set(key, value, {
					expires: 365
				})
			},
			removeItem: key => {
				Cookies.remove(key)
			}
		}
	})(store)
}

然后在nuxtServerInit这个生命周期钩子中读取访问时的cookie。

// /store/index.js

import { SET_USER_INFO } from './mutation-types'

export const state = => ({ token: '' })

export const mutations = {
	[SET_TOKEN](state, token) {
		state.token = token
	}
}

export const actions = {
	nuxtServerInit({
		commit
	}, {
		req
	}) {
		// 通过Nuxt context获取cookie有很多方法,这里我用了cookie-parser
		const {
			vuex
		} = req.cookies

		if (!vuex) {
			return
		}
		commit(SET_TOKEN, JSON.parse(vuex).token)
	}
}

这样一来,就可以在后续的Middleware、validate 等钩子通过context.store获取到所需的数据了。

此外,为了方便管理,我在文件中引入了/store/mutation-types.js,默认情况下Nuxt会将/store目录下的.js文件当作一个Vuex的模块,因此需要配置一下Nuxt的忽略文件,让它忽略掉mutation-types.js。

# /.nuxtignore

# ignore store mutation types

store/mutation-types.js

使用SCSS

为了更舒服地编写CSS,当然需要一个CSS预处理器!为了能让通用的.scss(变量、mixin等)能够用在任何组件中使用,需要借助@nuxtjs/style-resources模块实现,第一步安装,第二步配置。

// nuxt.config.js

module.exports = {
	...,
	buildModules: [
		'@nuxtjs/style-resources'
	],

	// 配置全局的 function,mixin,variable
	styleResources: {
		scss: [
			// 注意顺序!被后面文件依赖的文件需要放在前面
			'@/assets/scss/_variables.scss',
			'@/assets/scss/_functions.scss',
			'@/assets/scss/_mixins.scss'
		]
		// sass: [],
		// less: [],
		// stylus: []
	}
	...
}

代理

由于浏览器的安全机制,会拦截跨域的AJAX请求,为了解决跨域问题,可以设置代理,而这次的代理可以不是Nginx了,可以直接用中间层作为代理,为客户端请求数据。安装完@nuxtjs/proxy之后进行配置即可。

// nuxt.config.js

module.exports = {
	...,
	modules: [
		'@nuxtjs/proxy'
	],
	proxy: {
		'/api': {
			target: 'https://xxx.com',
			pathRewrite: {
				'^/api': '/'
			}
		}
	}
	...
}

自定义入口文件

入口文件的主要作用是启动服务和初始化Nuxt核心类,Nuxt已经将入口文件封装好并隐藏了,看不到也改不了,不过我们可以自己把这两件事情做了,让入口文件在自己掌控之下,变得更具有扩展性。

// 根目录创建server.js

const {
	loadNuxt,
	build
} = require('nuxt')

const Express = require('express') // 使用express启动服务,这样就可以对中间层做更多的处理,比如使用更多中间件等等

const cookieParser = require('cookie-parser') // 前面Vuex持久化中使用的cookie-parser就是这儿来的

const app = Express

// 自定义入口文件之后,nuxt.config.js的server选项会失效,需要手动调用

const config = require('./nuxt.config')
const isDev = process.env.NODE_ENV !== 'production'

const {
	host,
	port
} = config.server

async function start {
	// We get Nuxt instance
	const nuxt = await loadNuxt(isDev ? 'dev' : 'start')
	app.use(cookieParser)

	// Render every route with Nuxt.js
	app.use(nuxt.render)

	// Build only indev mode with hot-reloading
	if (isDev) {
		build(nuxt)
	}

	// Listen the server
	app.listen(port, host)
	console.log('Server listening on `localhost:' + port + '`.')
}

start

然后在package.json中修改一下启动命令。

{
	"s": {
		...,
		"start": "node server.js",
		...
	}
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值