理解SSR
CSR vs SSR
先来看看传统的web开发
传统的web开发(前后端不分离时代)都属于服务端渲染,因为整个页面所有的内容都是在服务端生成的。
- 好处:这些页面在服务端就渲染完成了,所以当用户看见内容的时候是立刻就展示在页面中的,对SEO比较友好。
- 缺点:服务端的压力会比较大(数据库查询,html模板解析等等)
CSR(SPA单页应用)
页面的框架不会在变了,只会不停地刷新不同的数据。
可以看到这种方式打开的页面,初始状态下,只是一个具有id=app的一个空页面。
- 缺点1:首屏渲染等待时间长,必须得等js加载完毕,并且执行完毕,才能渲染出首屏。
- 缺点2:seo不友好,爬虫只能拿到一个div,认为页面是空的,不利于seo
SSR(服务端渲染)
为了解决这两个问题,出现了SSR解决方案,后端渲染出完整的首屏dom结构返回,前端拿到内容后放上首屏;后续的页面操作,再用单页的路由跳转和渲染,称之为服务端渲染(server side render)。
- 缺点1:学习难度较高
- 缺点2:第三方库有时候会有问题(有些生命周期只是在前端跑的)
- 缺点3:增加服务器压力
Nuxt框架
Nuxt.js是一个基于Vue.js的通用应用框架
通过对客户端/服务端基础架构的抽象组织,Nuxt.js主要关注的是应用的UI渲染
优点:
- nuxt不仅仅用于服务端渲染,也可以用于SPA应用开发。
- 利用nuxt提供的基础项目结构、路由生成、中间件、插件等等,可大幅提高开发效率。
- nuxt可用于网站静态化。
# 脚手架命令
npx create-nuxt-app@2.9.2 xxx(项目名称)
通过npx命令,npm5+版本自带到命令,这里注意要指定create-nuxt-app版本,目前最新版本是3.2.0,新的版本不自动搭建服务端文件了,这是我们需要指定对应版本的create-nuxt-app脚手架创建,才有server文件夹。
路由
路由生成
pages目录中所有 *.vue 文件自动生成应用的路由配置,新建:
- pages/admin.vue 商品管理页
- pages/login.vue 登录页
查看 .nuxt/router.js 验证了路由生成
默认布局
查看 layouts/default.vue
<template>
<nuxt />
</template>
自定义布局
创建空白布局页面 layouts/blank.vue,用于login.vue
<template>
<div>
<nuxt />
</div>
</template>
自定义错误页面
创建 layouts/error.vue
<template>
<div>
<h1 v-if="error.statusCode === 404">页面不存在</h1>
<h1 v-else>应用发生异常错误</h1>
<p>{{error}}</p>
<nuxt-link to="/">首 页</nuxt-link>
</div>
</template>
<script>
export default {
props: ['error'],
layout: 'blank'
};
</script>
页面
页面组件就是Vue组件,只不过Nuxt.js为这些组件添加了一些特殊的配置项。
给首页添加标题和meta等,index.vue
head() {
return {
title: '课程列表',
meta: [{ name: 'dylan', hid: 'haha', content: 'set page meta' }],
link: [{ rel: 'favicon', href: 'favicon.ico' }]
}
},
异步数据获取
asyncData 方法使得我们可以在设置组件数据之前异步获取或处理数据
接口准备:
- 安装@nuxt/axios模块:npm i @nuxtjs/axios -S
配置:nuxt.config.js
modules: [
'@nuxtjs/axios',
],
axios: {
proxy: true
},
proxy: {
"/api": "http://localhost:8080"
},
- 安装依赖:npm i koa-router koa-bodyparser -S
- 创建接口文件,setver/api.js
const Koa = require('koa');
const app = new Koa();
// 处理post请求参数
const bodyparser = require('koa-bodyparser');
// 路由配置
const router = require('koa-router')({ prefix: '/api' });
// 设置cookie加密密钥
app.keys = ["some secret", "another secret"];
const goods = [
{ id: 1, text: "web全栈架构师1", price: 10000 },
{ id: 2, text: "web全栈架构师2", price: 10000 },
]
// 获取商品列表接口 /api/goods
router.get('/goods', ctx => {
ctx.body = {
ok: 1,
goods
}
})
router.get('/detail', ctx => {
ctx.body = {
ok: 1,
data: goods.find(good => good.id == ctx.query.id)
}
})
// /api/login
router.post('/login', ctx => {
// 获取用户名和密码
const user = ctx.request.body;
if(user.username === 'jerry' && user.password === '123') {
// 将token存入cookie
const token = 'a mock token';
// 令牌存入cookie
ctx.cookies.set('token', token);
ctx.body = { ok: 1, token };
} else {
ctx.body = { ok: 0 };
}
})
// 解析post数据并注册路由
app.use(bodyparser());
app.use(router.routes());
app.listen(8080, () => console.log('api服务已启动'))
修改pages/index.vue文件
// 前后端都会执行,时间点在beforeCreate之前
// 传递一个上下文
// 里面不能使用this,因为这时组件还不存在
async asyncData({$axios, error, redirect, store, app}) {
try {
const { ok, goods } = await $axios.$get('/api/goods');
if(ok) {
// 这里返回的对象,最终会和data中的对象融合(这里对象的优先级更高)
return { goods }
}
// 错误处理
error({statusCode: 400, message: '数据查询失败'})
} catch (error) {
error(error)
}
},
保存后可以看到接口中的数据成功渲染出来了。
打开控制台,可以看到没有请求 /api/goods,说明服务器渲染成功!
接下来进行路由切换,进入admin路由,再回到首页,会发现goods接口又请求了。
说明其实只是首页的渲染,其实还是一个单页应用。
中间件
中间件会在一个页面或一组页面渲染之前运行我们定义的函数,常用于权限控制、校验等任务。
例如:管理员页面保护,创建 middleware/auth.js
export default function({ route, redirect, sotre }) {
// 上下文通过store访问vuex中的全局状态
// 判断是否登录,如果没有token,则重定向到login
if(!sotre.state.user.token) {
redirect('/login?redirect=' + route.path)
}
}
注册中间件 admin.vue
export default {
middleware: ['auth']
};
全局注册:将会对所有页面起作用,nuxt.config.js
router: {
middleware: ["auth"]
}
Vuex 状态管理
应用根目录下如果存在store目录,Nuxt.js将会启用vuex状态树。定义各状态树时具名导出state,mutations,getters,actions即可。
例如:用户登录及登录状态保存,创建store/user.js
// 具名导出vuex的选项即可
export const state = () => ({
token: ''
})
export const mutations = {
init(state, token) {
state.token = token;
}
}
export const getters = {
isLogin(state) {
return !!state.token;
}
}
export const actions = {
login({ commit, getters }, u) {
return this.$axios.$post("/api/login", u).then(({ token }) => {
if(token) {
commit("init", token);
}
return getters.isLogin;
})
}
}
修改login页面
<template>
<div>
<h2>用户登录</h2>
<el-input v-model="user.username"></el-input>
<el-input type="password" v-model="user.password"></el-input>
<el-button @click="onLogin">登录</el-button>
</div>
</template>
<script>
export default {
layout: 'blank',
data() {
return {
user: {
username: '',
password: ''
}
}
},
methods: {
onLogin() {
this.$store.dispatch('user/login', this.user).then(ok => {
console.log(ok);
if(ok) {
const redirect = this.$route.query.redirect || '/';
this.$router.push(redirect)
}
})
}
}
};
</script>
在登录后,就可以进admin页面了。
此时刷新页面,会发现页面的cookie还在,但登录态消失了,这是因为vuex是浏览器的缓存,需要服务器在首次获取cookie时,将其写入vuex
跨平台的cookie写法
npm i cookie-universal-nuxt -S
安装完后,修改nuxt.config.js
modules: [
'@nuxtjs/axios',
'cookie-universal-nuxt'
]
在store文件夹中添加index.js文件
export const actions = {
// 该action只能出现在index
// 且只能在服务端执行一次
// 参数2是nuxt上下文
nuxtServerInit({commit}, {app}) {
// 1、获取cookie
const token = app.$cookies.get('token')
// 2、写入user模块中
if(token) {
commit('user/init', token);
}
}
}
插件
Nuxt.js会在运行应用之前执行插件函数(只会在初始化时执行一次),需要引入或设置Vue插件、自定义模块和第三方模块时特别有用。
例:添加请求拦截器附加token,创建plugins/interceptor.js
export default function ({ $axios, store }) {
// 这是模块提供的帮助方法,可以拦截每一次请求
$axios.onRequest(config => {
if(store.state.user.token) {
config.headers.Authorization = 'Bearer ' + store.state.user.token;
}
return config;
})
}
注册插件,nuxt.config.js
plugins: ["@/plugins/interceptor"]
发布部署
服务端渲染应用部署
// 打包
npm run build
// 启动服务
npm start
生成内容在.nuxt/dist中
静态应用部署
Nuxt.js可依据路由配置将应用静态化,使得我们可以将应用部署至任何一个静态站点主机服务商。
npm run generate
注意渲染和接口服务器都需要处于启动状态
生成内容在dist中