现代化的服务端渲染
对于Vue所构建的单页面应用(SPA)虽然具有用户体验好,渲染性能好,可维护高等优点。但也有一些缺陷,其中只要涉及两点:
- 首屏加载时间长
因为用户需要等待客户端JS解析执行完成才能看到页面 - 不利于SEO
当搜索引擎爬取网站HTML文件时,单页应用的HTML没有内容,因为他需要客户端JS解析才能生成网页内容。
因此,就出现了现代化的服务端渲染,也叫同构渲染。就是指服务端构建渲染+客户端构建渲染。
Nuxt.js 是一个基于 Vue.js 生态开发的一个第三方服务端渲染框架,通过它我们可以轻松构建现代化的服务端渲染应用。
- 通过服务端渲染首屏直出,解决首屏渲染慢以及不利于SEO问题
- 然后再客户端将来自服务端渲染的内容激活为一个SPA应用,也就是说之后的页面内容交互都是通过客户端渲染处理。
- 具体流程:
- 客户端发起请求
- 服务端渲染首屏内容 + 生成客户端 SPA 相关资源
- 服务端将生成的首屏资源发送给客户端
- 客户端直接展示服务端渲染好的首屏内容
- 首屏中的 SPA 相关资源执行之后会激活客户端 Vue
- 之后客户端所有的交互都由客户端 SPA 处理
服务端渲染同样也存在问题:
- 开发条件有限。浏览器特定的代码,只能在某些生命周期钩子函数中使用,一些外部扩展库可能需要特殊处理才能在服务器渲染应用程序中运行
- 涉及构建设置和部署的更多要求,服务器渲染应用程序,需要处于Node.js server运行环境。
- 更多的服务端负载。在Node.js中渲染完整的应用程序,会比仅仅提供静态文件的server更加大量占用CPU资源。
Nuxt.js
Nuxt.js 是一个基于 Vue.js 的服务端渲染应用框架,它可以帮我们轻松的实现同构应用。
特性:
- 基于 Vue.js
- Vue、Vue Router、Vuex、Vue SSR
- 自动代码分层
- 服务端渲染
- 强大的路由功能,支持异步数据
- 静态文件服务
- ES2015+ 语法支持
- 打包和压缩 JS 和 CSS
- HTML 头部标签管理
- 本地开发支持热加载
- 集成 ESLint
- 支持各种样式预处理器: SASS、LESS、 Stylus 等等
- 支持 HTTP/2 推送
创建项目
Nuxt 提供了两种方式用来创建项目:
- 使用 create-nuxt-app 脚手架工具
- 手动创建
mkdir nuxt-app-demo
cd nuxt-app-demo
npm init -y
npm innstall nuxt
- 在 package.json 文件的 scripts 中新增:
"scripts": { "dev": "nuxt" },
Nuxt路由
Nuxt.js 依据 pages 目录结构自动生成 vue-router 模块的路由配置。
路由导航
- a 链接,刷新导航,走服务端渲染
- 组件
- 编程式导航
动态路由
在 Nuxt.js 里面定义带参数的动态路由,需要创建对应的以下划线作为前缀的 Vue 文件 或 目录。
嵌套路由
你可以通过 vue-router 的子路由创建 Nuxt.js 应用的嵌套路由。
创建内嵌子路由,你需要添加一个 Vue 文件,同时添加一个与该文件同名的目录用来存放子视图组件。
Warning: 别忘了在父组件( .vue 文件) 内增加 用于显示子视图内容。
自定义路由配置
可以通过配置文件配置自定义选项
视图
模版
布局
默认布局
自定义布局
异步数据
asyncData 方法
Nuxt.js 扩展了 Vue.js,增加了一个叫* asyncData *的方法,使得我们可以在设置组件的数据之前能异步获取或处理数据。
- 基本用法
- 他会将asyncData返回的数据融合组件data方法返回数据一并给组件
- 调用时机:服务端渲染期间和客户端路由更新之前
- 注意事项
- 只能在页面组件中使用,分页面组件不可以调用,只能通过组件之间传参获取值
- 没有this,因为他是在组件初始化之前被调用的,要获取vue实例的内容可以通过context来获取上下文对象
状态存储
在项目开发的过程中,会涉及到一些登录状态的处理。既要客户端可以访问,也要服务端可以访问
login.js
// 仅在客户端加载 js-cookie 包
const Cookie = process.client ? require('js-cookie') : undefined
export default {
middleware: 'notAuthenticated',
name: 'LoginIndex',
computed: {
isLogin () {
return this.$route.name === 'login'
}
},
data () {
return {
user: {
username: '',
email: 'lpzmail@163.com',
password: '12345678'
},
errors: {} // 错误信息
}
},
methods: {
async onSubmit () {
try {
// 提交表单请求登录
const { data } = this.isLogin
? await login({
user: this.user
})
: await register({
user: this.user
})
// console.log(data)
// TODO: 保存用户的登录状态
this.$store.commit('setUser', data.user)
// 为了防止刷新页面数据丢失,我们需要把数据持久化
Cookie.set('user', data.user)
// 跳转到首页
this.$router.push('/')
} catch (err) {
// console.log('请求失败', err)
this.errors = err.response.data.errors
}
}
}
}
store/index.js
注:这里的store目录比较特殊,必须叫store,因为nuxt在发现这个store目录之后会自动加载里面的容器模块
const cookieparser = process.server ? require('cookieparser') : undefined
// 在服务端渲染期间运行都是同一个实例
// 为了防止数据冲突,务必要把 state 定义成一个函数,返回数据对象
export const state = () => {
return {
// 当前登录用户的登录状态
user: null
}
}
export const mutations = {
setUser (state, data) {
state.user = data
}
}
export const actions = {
// nuxtServerInit 是一个特殊的 action 方法
// 这个 action 会在服务端渲染期间自动调用
// 作用:初始化容器数据,传递数据给客户端使用
nuxtServerInit ({ commit }, { req }) {
let user = null
// 如果请求头中有 Cookie
if (req.headers.cookie) {
// 使用 cookieparser 把 cookie 字符串转为 JavaScript 对象
const parsed = cookieparser.parse(req.headers.cookie)
try {
user = JSON.parse(parsed.user)
} catch (err) {
// No valid cookie found
}
}
// 提交 mutation 修改 state 状态
commit('setUser', user)
}
}
中间件
又称路由中间件,中间件允许定义一个自定义函数运行在一个页面或一组页面渲染之前。
每一个中间件应该放置在middleware/目录。文件名的名称将成为中间件名称,一个中间件接收context作为第一个参数
middleware/authenticated.js
/**
* 验证是否登录的中间件
*/
export default function ({ store, redirect }) {
// If the user is not authenticated
if (!store.state.user) {
return redirect('/login')
}
}
使用方法
export default {
// 在路由匹配组件渲染之前会先执行中间件处理
middleware: 'authenticated',
name: 'EditorIndex'
}
路由监听
Nuxt中默认情况下query参数的变化不能引起asyncData代码的执行,所以我们可以通过使用watchQuery参数监听到路由的变化,触发asyncData的调用。
使用watchQuery属性可以监听参数字符串的更改。 如果定义的字符串发生变化,将调用所有组件方法(asyncData, fetch, validate, layout, …)。 为了提高性能,默认情况下禁用。
export default {
watchQuery: ['page']
}
axios请求拦截器
如果要在请求的时候统一发送token,则需要添加请求拦截器,获取用户统一token需要拿到store中存储的内容,所以需要通过插件属性获取
在Nuxt配置文件中注册插件:
Nuxt.config.js
module.exports = {
// 注册插件
plugins: [
'~/plugins/request.js', // 波浪线开头表示从根路径触发
]
}
新建文件plugins/request.js
/**
* 基于 axios 封装的请求模块
*/
import axios from 'axios'
// 创建请求对象
export const request = axios.create({
baseURL: 'http://realworld.api.fed.lagounews.com'
})
// 通过插件机制获取到上下文对象(query、params、req、res、app、store...)
// 插件导出函数必须作为 default 成员
export default ({ store }) => {
// 请求拦截器
// Add a request interceptor
// 任何请求都要经过请求拦截器
// 我们可以在请求拦截器中做一些公共的业务处理,例如统一设置 token
request.interceptors.request.use(function (config) {
// Do something before request is sent
// 请求就会经过这里
const { user } = store.state
if (user && user.token) {
config.headers.Authorization = `Token ${user.token}`
}
// 返回 config 请求配置对象
return config
}, function (error) {
// 如果请求失败(此时请求还没有发出去)就会进入这里
// Do something with request error
return Promise.reject(error)
})
}
日期插件dayjs
markdown转HTML
markdown-it
设置页面meta的SEO优化
Nuxt 在 head 方法里可通过 this 关键字来获取组件的数据,你可以利用页面组件的数据来设置个性化的 meta 标签。
注意:为了避免子组件中的 meta 标签不能正确覆盖父组件中相同的标签而产生重复的现象,建议利用 hid 键为 meta 标签配一个唯一的标识编号
head() {
return {
title: `${this.article.title} - RealWorld`,
meta: [
{
hid: 'description',
name: 'description',
content: this.article.description
}
]
}
}