Vue-SSR相信各路大佬都不陌生,我前段时间刚了解到SSR,
SSR简单来说就是将本来要放在浏览器执行创建的组件,放到服务端先创建好,然后将编译好的内容(模板)下发(包括样式、内容、数据将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。
通过服务端渲染,可以优化SEO抓取,提升首页加载速度,提升用户体验等。由于我vue还可以,所以上手还是比较轻松的,通过几天的学习,我先是把nuxt.js 做了下整理,然后在B站找了项目实战撸了一把,所以想通过这篇文章,对其中的模块实现做一下总结,同时希望能够对学习SSR的小伙伴起到一点帮助。如果你觉得本文还行,请点亮左边的赞!
我过了一遍nuxt.js中文文档做了个初步了解。
Nuxt.js 是一个基于Vue.js的通用应用框架,预设了利用Vue.js开发服务端渲染的应用所需要的各种配置。基于Vue 2做的,包括Vue-Router,支持Vuex、Vue Server Render、vue-meta。
下面简单说一下nuxt的工作原理:(下图截自nuxt官网)
从浏览器发出一个请求,到最终服务端渲染完成,Nuxt的生命周期如下:
常用的有这几种:Basic Dynamic Nested
Nuxt封装了路由的生成,你不需要额外编写路由,pages文件夹下的结构,会自动生成对应的路由。
路由的生成主要是在lib/build.js里面处理的,大致步骤如下:
支持Promise async/await callback
在组件结构中,其属于宿主layout下的子组件,不属于页面组件,无法使用页面组件中的fetch方法,官方的解释是子组件无法使用阻塞异步请求,即:子组件得到的异步数据无法用于服务端渲染,这对于程序是合理的,避免异常阻塞,简化业务模型。
如果需要这些异步数据增强站内内链SEO,我们可以巧妙地使用内置vuex中的nuxtServerInit这个API,这个API实在nuxt程序实例化之后第一次执行的方法,其内部返回一个promise,我们可以在这里完成我们站内的所有子组件异步请求,随后将数据映射至对应子组件即可。
另外的方法是在mounted 方法去调用异步数据。
注:在这个data方法里面,我们获取不到this指针,因为data方法在组件初始化之前就已经被调用了。
了解原理后,开始搭建一个nuxt项目,由于篇幅原因,这里不对项目搭建过程展开,可自行百度。下面我放了一张很多文章不会写的nuxt项目结构介绍及基本的api:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7TlPXb0V-1589965874835)(https://user-gold-cdn.xitu.io/2020/5/19/1722c6511a8fd093?w=2251&h=2265&f=png&s=483366)]
项目搭建完成后,我们随便写点东西,然后打开打开源码,就会有惊奇的发现。服务器端渲染完页面后给浏览器端的html分了几个部分,第一个是样式 style,第二个是模板内容,例如上图中圈中的蓝色部分,第三个是服务端拿到的数据结果,例如上图中圈中的红色部分,为什么服务端拿到的数据给到浏览器端呢?
交互是在浏览器端完成的,也就是说浏览器端会有一个入口,进行预编译,但不会再渲染页面了,因为服务器端已经在页面渲染过一次了。它要做的是创建一个虚拟的编译结果(可以理解为虚拟dom), 和服务器端传过来的结果进行对比,如果有区别,它会重新请求数据。在nuxt项目中都是一套文件,没有特别指定是在浏览器端运行还是服务端运行,也就是SSR常说的同构,浏览器端编译虚拟dom,也依赖于 vue 文件,因此模板是有的,而编译这个dom,需要的是额外的数据,此数据是服务器端渲染之前请求而来的数据,如果数据不同步在浏览器端,编译出来的结果必然和服务器端编译结果不一致。
综上,服务器端异步获取的数据会同步在浏览器端,作对比,如果对比一致的话,浏览器端就会对对应的dom结点注册事件,达到交互作用。
这个项目是慕课网在2018年10月份左右推出的课程,到现在很多写法npm包都更新迭代了,但核心的东西还在,课程里面页面结构设计、组件设计、数据结构设计、某些样式写法、业务逻辑的完备性等包括项目目录的管理都是值得学习的。
项目地址:
传送门
① Star 本仓库,然后 Fork 到自己 github,下载代码到本地
$ git clone git@github.com:JakeZhangZJK/vue-node-koa--SSR-MongoDB-mt-site.git
② 下载并配置好后端数据库文件,启动 MongoDB 和 Redis 服务(安装与配置教程自行百度)
③ 安装依赖并启动项目
$ npm/cnpm install
$ npm/cnpm run dev
在构建页面时先整体将页面分成几个大的模块,整体要么head、body、foot下布局,要么左中右布局,然后从数据结构的角度去思考怎么简洁的设计页面的dom结构。有了整体思路后,可借助一些优秀的UI框架来快速完成页面的绘制。本项目中首页导航栏/主页菜单/城市服务页面/产品详情列表等每个复杂的页面dom节点几乎不超过10个,十分简洁。这样的设计个人觉得是非常值得学习的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oyqpVjWW-1589965874853)(https://user-gold-cdn.xitu.io/2020/5/19/1722ba3bb9467873?w=1189&h=866&f=png&s=389246)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-olSoCmHC-1589965874872)(https://user-gold-cdn.xitu.io/2020/5/19/1722ba90ea6b14c8?w=1059&h=364&f=png&s=62889)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qhzGCsme-1589965874877)(https://user-gold-cdn.xitu.io/2020/5/20/172314e57230d245?w=1738&h=800&f=png&s=55465)]
首先利用element的form表单组件稍加改造绘制页面,并且这个form组件提供了很好很方便的前端校验规则
rules: {
name: [{
required: true,
type: "string",
message: "请输入昵称",
trigger: "blur"
}],
email: [{
required: true,
type: "email",
message: "请输入邮箱",
trigger: "blur"
}],
pwd: [{
required: true,
message: "创建密码",
trigger: "blur"
}],
cpwd: [{
required: true,
message: "确认密码",
trigger: "blur"
},
{
validator: (rule, value, callback) => {
if (value === "") {
callback(new Error("请再次输入密码"));
} else if (value !== this.ruleForm.pwd) {
callback(new Error("两次输入密码不一致"));
} else {
callback();
}
},
trigger: "blur"
}
]
}
在config.js文件中进行配置工作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f5Uj3TDo-1589965874879)(https://user-gold-cdn.xitu.io/2020/5/20/172305da596f56db?w=916&h=831&f=png&s=91217)]
首先需要调用第三方接口完成发送邮件的功能:配置QQ邮箱SMTP服务createTransport()–>获取用户邮箱及验证码–>确定接收方,发送相关信息–>编写邮件中显示内容–>调用sendMail()发送邮件–>存储注册用户信息–>接口响应
//码验证接口
router.post('/verify', async (ctx, next) => {
let username = ctx.request.body.username
const saveExpire = await Store.hget(`nodemail:${username}`, 'expire')
//拦截频繁刷接口操作
if (saveExpire && new Date().getTime() - saveExpire < 0) {
ctx.body = {
code: -1,
msg: '请求过于频繁,1分钟1次'
}
return false
}
//发邮件功能
let transporter = nodeMailer.createTransport({
service: 'qq',
auth: {
user: Email.smtp.user,
pass: Email.smtp.pass
}
})
//确定接收方,发送相关信息
let ko = {
code: Email.smtp.code(),
expire: Email.smtp.expire(),
email: ctx.request.body.email,
user: ctx.request.body.username
}
//邮件中显示内容定义
let mailOptions = {
from: `"认证邮件" <${Email.smtp.user}>`,
to: ko.email,
subject: '【Jake Zhang 高仿美团网全栈开发】注册码',
html: `您正在【Jake Zhang 高仿美团网】网页中注册,您的邀请码是${ko.code},5分钟内有效,请勿泄露。`
}
//发送邮件
await transporter.sendMail(mailOptions, (error, info) => {
if (error) {
return console.log(error)
} else {
//存储注册方信息
Store.hmset(`nodemail:${ko.user}`, 'code', ko.code, 'expire', ko.expire, 'email', ko.email)
}
})
ctx.body = {
code: 0,
msg: '验证码已发送,可能会有延时,有效期5分钟'
}
})
注册后端接口
从redis中获取 在nodemail发验证码的时候 的存储数据,并将存储数据与浏览器获取的数据进行对比
router.post('/signup', async (ctx) => {//关键代码
const {
username,
password,
email,
code
} = ctx.request.body;
if (code) {
const saveCode = await Store.hget(`nodemail:${username}`, 'code')//验证码
const saveExpire = await Store.hget(`nodemail:${username}`, 'expire')//过期时间
// ...
} else {
ctx.body = {
// ...
}
}
})
this.$axios.post('/users/signin', {
username : window.encodeURIComponent(self.username),
password : CryptoJS.MD5(self.password).toString()
})
location.href="/"
浏览器发送一个 request
请求,根据 cookie
,服务器通过 passport
与 redis
来验证当前是否是登录状态,返回 username
。本项目用的是koa-generic-session
npm 包进行cookie的相关操作。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Me7rd22Y-1589965874891)(https://user-gold-cdn.xitu.io/2020/5/19/1722bbf727521a37?w=1550&h=851&f=png&s=114273)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cs56gd6q-1589965874893)(https://user-gold-cdn.xitu.io/2020/5/19/1722b441512bc191?w=1398&h=461&f=png&s=223713)]
城市定位实现原理:
浏览器在发出请求的时候,会有一个 request
,在服务器端可以拿到 requset.ip
,然后就可以取数据中心作映射,根据 ip
来定位城市,服务器拿到 city
后再下发给浏览器。
原本实现方式: 当页面渲染完了,向服务器发送请求,甚至可以发一个空内容,然后按照上述实现原理来获取 city
。即在 mounted 事件之后,向服务器发送请求,然后服务器下发城市名称。(页面发送请求渲染,然后又异步请求获取城市名,共两次请求)
缺点:网络请求浪费,影响用户体验,异步获取的城市会 “闪” 一下。
项目实现方式:当浏览器去请求文档的时候,服务端 ip已经知道了,那个时候就可以拿到对应的城市,立即返回数据给浏览器。做法就是通过 vuex
来同步状态,然后通过 SSR
异步请求就能得到数据。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h0JMVQER-1589965874895)(https://user-gold-cdn.xitu.io/2020/5/19/1722bbe031cca9b8?w=828&h=794&f=png&s=262232)]
后台接口没啥好说的,都是调用线上的接口,就是根据输入内容,获取相关最热门的吃喝玩乐,返回name和type,完成实时搜索。
在客户端每输入一个字母都进行一次请求,会造成浪费性能,因此引入lodash
插件,使用debounce
做一个延时处理。
import _ from 'lodash'
input:_.debounce(async function(){
let self=this;
let city=self.$store.state.geo.position.city.replace('市','')
self.searchList=[]
let {status,data:{top}}=await self.$axios.get('/search/top',{
params:{
input:self.search,
city
}
})
self.searchList=top.slice(0,10)
},300)
参考:由浅入深学习lodash的debounce函数
产品列表页
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-seCWhjIA-1589965874896)(https://user-gold-cdn.xitu.io/2020/5/19/1722baaca64576a0?w=1534&h=818&f=png&s=416367)]
产品详情页
每一个产品列表对应着多个 item
,每个 item
与详情页是一对一关系,而上述两个页面路由是没有关联关系的。由于本项目没有 产品库,因此路由没有根据 id
关联产品详情,依旧是根据搜索关键词 keyword
。另外,产品列表页和产品详情页之间做了登录拦截。
接着,就是从产品详情页是跳转到购物车了
购物车页面如下图所示,可以看到,页面路由依旧是没有任何关联,但从下图地址栏可见,有一个重要的id
属性。因为产品详情页不能与购物车创建一对一映射关系,即在进入产品详情页时,购物车页面是不存在的。当点击购买跳转到购物车时才会创建一个购物车。另外,产品详情页和购物车之间同样做了登录拦截。
父组件pages/cart.vue
通过asyncData获取数据(接口:/cart/getCart
)
传给子组件 list.vue
所有订单数据,由子组件全部渲染出来,通过cartData
变量联系,如果我在子组件中更改了购买商品的数量,也就是cartData中的值被更改了,那么,我们在父组件监听的total(所有订单总价),也会重新计算
另外,购物车会创建一个订单,创建成功后才会跳转支付页面,但需考虑支付的是哪一个订单,于是支付和订单之间有一个依赖逻辑联系,但是支付和购物车之间是没有任何依赖的,虽然支付的动作是由购物车发起的,但是购物车和支付之间的桥梁是订单。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xrcjGv0M-1589965874905)(https://user-gold-cdn.xitu.io/2020/5/19/1722bc8ef1e3348e?w=1585&h=875&f=png&s=1934564)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8123FzwU-1589965874913)(https://user-gold-cdn.xitu.io/2020/5/19/1722bd585bb21a6a?w=1471&h=640&f=png&s=186765)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EroaSXwg-1589965874918)(https://user-gold-cdn.xitu.io/2020/5/20/17231504fa30d70c?w=1312&h=738&f=png&s=383693)]
本项目为仿做项目,仅做练手和学习使用,非官方网站,禁止用于商业目的,产生的一切侵权著作法律后果,与本作者无关。转载使用请注明出处,谢谢!
Copyright (c) 2020 Jake Zhang