Nuxt.js实战经验总结
共 8846字,需浏览 18分钟
· 2020-12-18
作者:随便看看用户
来源:SegmentFault 思否
近期公司成立了一个新项目,需要做SEO。做过前后端半分离(部分数据是后端模板渲染,部分用AJAX获取)项目的同学都知道跟后端人员把代码写在同一个页面有多难受,项目不易维护且开发体验很差,因此采用了Nuxt.js
SEO
大家都知道,网页在搜索引擎中能被搜索出来是因为他们有自己的爬虫,可以爬取网页中的内容并保存下来,但是它们只会保存网页中的第一个请求返回的内容,即HTML文档。
在传统的前后端不分离网站中,在浏览器上查看源代码,可以看到网页上有什么内容,源代码中就有与之对应的代码。随着前后端分离开发越来越流行,也衍生了越来越多的单页面应用(SPA),在浏览器中打开一个SPA的源代码,往往只能看到寥寥无几的几个标签,其余内容全都是由JS渲染的,也就是客户端渲染。因此单页面应用的一大痛点就是搜索引擎无法收录,有痛点就会有相应的方案产生,出现了SSR。
SSR
SSR意思是服务端渲染,乍一看会以为是回到以前的前后不分离开发模式,实际上相比以前,客户端与服务端之间多了一个中间层,用于接收数据并渲染页面返回到客户端。
有了中间层帮客户端渲染页面,在搜索引擎爬到页面时,就解决了前后端分离页面不收录的的痛点。不仅如此,由于中间层也是一个服务端,前端工程师可以做更多事儿了,比如简单处理数据、做代理等等。
Nuxt.js
Vue官方提供了vue-server-renderer模块实现SSR,而Nuxt.js是对其封装后的框架,用于提供一个开箱即用的NodeJS SSR服务。
生命周期
通过在地址栏输入链接,或者从其他网站点击链接跳转到Nuxt站点的页面时(首屏访问),呈现的第一个页面的数据会由中间层向服务端发起请求,然后渲染Vue组件,再将渲染完成后生成的HTML文档返回到客户端。随后点击页面的 组件或者通过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的生命周期钩子。
以上生命周期中,只有前两个是完全在中间层执行的,其余的有可能在中间层执行,也有可能在客户端执行,这主要取决于页面是首屏渲染还是站内跳转,因此搞懂哪些代码什么时候在什么环境运行非常重要。
插件
vant-ui
// nuxt.config.js
module.exports = {
...,
plugins: [
'@/plugins/vant-ui'
],
...
}
// /plugins/vant-ui.js
import Vue from 'vue'
import {
Button,
Search,
Toast
} from 'vant'
Vue.use(Button)
Vue.use(Search)
Vue.use(Toast)
// 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
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) {
case 401:
// 未登录
Toast('请先登录')
break
case 403:
// 操作被拒绝(没有相应权限)
Toast('您的操作被拒绝')
break
case 404:
Toast('未找到资源')
break
case 500:
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()
// /plugins/axios.js
import axios from '@/utils/request'
// 将context对象挂载到axios实例上
export default context => {
axios.nuxtContext = context
}
Vuex持久化
// /plugins/vuex-persistedstate.js
import createPersistedState from 'vuex-persistedstate'
import Cookies from 'js-cookie'
export default ({ store }) => {
// “将Vuex中的数据同步到cookie中”这种事只有客户端会做
if (!process.client) {
return
}
createPersistedState({
// 通过配置修改vuex-persistedstate的读写行为
// 将操作目标改为cookie
storage: {
getItem: key => {
return Cookies.get(key)
},
setItem: (key, value) => {
Cookies.set(key, value, {
expires: 365
})
},
removeItem: key => {
Cookies.remove(key)
}
}
})(store)
}
// /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)
}
}
# /.nuxtignore
# ignore store mutation types
store/mutation-types.js
使用SCSS
// 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: []
}
...
}
代理
// nuxt.config.js
module.exports = {
...,
modules: [
'@nuxtjs/proxy'
],
proxy: {
'/api': {
target: 'https://xxx.com',
pathRewrite: {
'^/api': '/'
}
}
}
...
}
自定义入口文件
// 根目录创建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 in dev mode with hot-reloading
if (isDev) {
build(nuxt)
}
// Listen the server
app.listen(port, host)
console.log('Server listening on `localhost:' + port + '`.')
}
start()
{
"scripts": {
...,
"start": "node server.js",
...
}
}