最近学习Vue全家桶+SSR+Koa2全栈开发美团网课程,主讲以Vue SSR+Koa2全栈技术为目标,最终实现美团网项目。html
源码连接:github.com/zhanglichun…前端
const state = () => ({
position: {},
})
const mutations = {
setPosition(state, position) {
state.position = position
},
setCity(state, city) {
state.position.city = city
},
setProvince(state, province) {
state.position.province
},
}
const actions = {
setPosition: ({commit}, position) => {
commit('setPosition', position)
},
setCity: ({commit}, city) => {
commit('setPosition', city)
},
setProvince: ({commit}, province) => {
commit('setPosition', province)
},
}
export default {
namespaced: true,
state,
mutations,
actions
}
复制代码
2.由于store里的数据是保存在运行内存中的,当刷新网页后,保存在vuex实例store里的数据会丢失(即页面会从新加载vue实例,store里面的数据就会被从新赋值。)vue
nuxt提供的fetch钩子和nuxtServerInit(均运行在服务端)起做用了,都能帮助咱们在页面渲染(组件加载 )前快速操做storenode
这样不管如何跳转页面,state的city数据都不会丢失ios
参考文章: nuxt - nuxtServerInit & 页面渲染前的store处理 & contextgit
import Vue from 'vue'
import Vuex from 'vuex'
import geo from './models/geo'
Vue.use(Vuex)
const store = () =>
new Vuex.Store({
modules: {
geo
},
actions: {
async nuxtServerInit({commit}, {req, app}) {
const {status, data:{province, city}} = await app.$axios.get('https://restapi.amap.com/v3/ip?key=b598c12de310236d9d40d3e28ea94d03')
commit('geo/setPosition', status === 200 ? {province, city} : {province: '', city: ''})
}
}
})
export default store
复制代码
{{$store.state.geo.position.city}}
复制代码
<div class="wrapper">
<input v-model="search" placeholder="搜索商家或地点" @focus="focus" @blur="blur" @input="input"/>
<button class="el-icon-search"></button>
</div>
<dl class="searchList" v-if="isSearchList">
<dd v-for="(item, i) in searchList" :key="i">{{ item.name }}</dd>
</dl>
export default {
data() {
return {
search: '',
isFocus: false,
searchList: []
}
},
computed: {
isSearchList() {
return this.isFocus && this.search
}
},
methods: {
focus() {
this.isFocus = true
},
blur() {
this.isFocus = false
},
input: _.debounce(async function () {
const { data: { pois } } = await this.$axios.get(`https://restapi.amap.com/v3/place/text?keywords=${this.search}&city=${this.$store.state.geo.position.city}&offset=7&page=1&key=a776091c1bac68f3e8cda80b8c57627c&extensions=base`)
this.searchList = pois
})
},
}
复制代码
<template>
<div class="m-container">
<div class="scenes-container">
<dl @mouseover="over">
<dt class="dt">有格调</dt>
<!-- <dd keywords="美食|spa|电影|酒店" kind="all">所有</dd> -->
<dd keywords="美食">约会聚餐</dd>
<dd keywords="SPA">丽人SPA</dd>
<dd keywords="电影">电影演出</dd>
<dd keywords="酒店">品质出游</dd>
</dl>
<div class="detial">
<nuxt-link to="item.url" v-for="(item, i) in list" :key="item.name">
<img :src='item.photos[0].url' alt="美团">
<ul>
<li class="title">{{ item.name }}</li>
<li class="other">{{ item.adname }} {{ item.address }}</li>
<li class="price">
<span>¥{{ item.biz_ext.cost.length?item.biz_ext.cost:'暂无' }}</span>
</li>
</ul>
</nuxt-link>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
kind: 'all',
keywords: '',
list: []
}
},
methods: {
async over(e) {
const current = e.target.tagName.toLowerCase()
if (current === 'dd') {
this.keywords = e.target.getAttribute('keywords')
const {status, data: {pois}} = await this.$axios.get(`https://restapi.amap.com/v3/place/text?keywords=${this.keywords}&city=${this.$store.state.geo.position.city}&offset=10&page=1&key=b598c12de310236d9d40d3e28ea94d03&extensions=all`)
if (status === 200) {
const r = pois.filter(item => item.photos.length)
this.list= r.slice(0, 6)
} else {
this.list = []
}
}
}
},
async mounted() {
const {status, data: { pois }} = await this.$axios.get(`https://restapi.amap.com/v3/place/text?keywords=美食&city=${this.$store.state.geo.position.city}&offset=100&page=1&key=b598c12de310236d9d40d3e28ea94d03&extensions=all`)
if (status === 200) {
const r = pois.filter((item) => item.biz_ext.cost.length && item.photos.length)
this.list = r.slice(0, 6)
} else {
this.list = []
}
}
}
</script>
复制代码
/**
* -----给客户发邮箱获取验证码接口-----
*/
router.post("/verify", async (ctx) => {
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
}
//用Nodemail给用户发邮箱获取验证码
let transporter = nodeMailer.createTransport({
host: Email.smtp.host,
port: 587,
secure: false,
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: "美团注册码",
html: `您在美团注册,您的邀请码是${ko.code}`
}
await transporter.sendMail(mailOptions, (err, info) => {
if (err) {
return console.log(err);
} else {
Store.hmset(`nodemail:${ko.user}`, 'code', ko.code, 'expire', ko.expire, 'email', ko.email)
}
})
ctx.body = {
code: 0,
msg: "验证码已经发送,可能会有延时,有效期1分钟"
}
})
/**
* -----注册接口-----
*/
router.post("/signup", async (ctx) => {
const {
username,
password,
email,
code
} = ctx.request.body;//post方式
//验证码是否正确?正确了,验证码是否已过时
const saveCode = await Store.hget(`nodemail:${username}`, "code");
const saveExpire = await Store.hget(`nodemail:${username}`, "expire");
if (code === saveCode) {
if (new Date().getTime() - saveExpire > 0) {
ctx.body = {
code: -1,
msg: "验证码已过时,请从新获取"
}
return false;
}
} else {
ctx.body = {
code: -1,
msg2: "请输入正确的验证码"
}
return false
}
//查询mongoose数据库,是否存在用户名。存在,用户名已注册,不存在,写入数据库
let user = await User.find({
username
})
console.log(user)
if (user.length) {
ctx.body = {
code: -1,
msg1: "用户名已被注册"
}
return false
}
let nuser = await User.create({
username,
password,
email
})
if (nuser) {
ctx.body = {
code: 0,
msg: "注册成功",
}
} else {
ctx.body = {
code: -1,
msg: "注册失败"
}
}
})
复制代码
3.在前端使用element-ui的form表单,在点击发送验证码的时候,会请求给客户发邮箱获取验证码的接口并进行相关的逻辑判断。在点击注册的时候,会请求注册接口并进行相关的逻辑判断,一旦注册成功,就写入mongoose数据库。github
sendMsg() {
const self = this
let namePass, emailPass
//对用户名和密码进行客户端表单校验,是否填写,格式是否正确
this.$refs['ruleForm'].validateField('username', (valid) => {
namePass = valid
})
if (namePass) {
return false
}
this.$refs['ruleForm'].validateField('email', (valid) => {
emailPass = valid
})
self.statusMsg = ''
if (!namePass && !emailPass) {
this.$axios.post('/users/verify', {
username: encodeURIComponent(self.ruleForm.username),
email: self.ruleForm.email
}).then(({ status, data}) => {
if(status===200 && data && data.code===0) {
let count = 60
self.statusMsg = `验证码已发送,剩余${count--}秒`
self.timerid = setInterval(() => {
self.statusMsg = `验证码已发送,剩余${count--}秒`
if (count === 0) {
clearInterval(self.timerid)
self.statusMsg = '请从新获取验证码'
}
}, 1000);
} else {
self.statusMsg = data.msg
}
})
}
}
register() {
let self = this
this.$refs["ruleForm"].validate((valid) => {
if (valid) {
this.$axios.post("/users/signup", {
username: window.encodeURIComponent(this.ruleForm.username),
password: cryptoJs.MD5(this.ruleForm.pwd).toString(),
email: this.ruleForm.email,
code: this.ruleForm.code
})
.then(({ status, data }) => {
if (status === 200) {
if (data && data.code === 0) {
location.href = "/login"
} else {
self.statusMsg = data.msg
self.error1 = data.msg1
self.error2 = data.msg2
}
}
else {
self.error = `服务器出错,错误码:${status}`
}
})
}
})
}
}
复制代码
参考文章: koa-passport学习笔记
koa2 使用passport权限认证中间件web
const passport = require('koa-passport')
const LocalStrategy = require('passport-local')
const UserModel = require('../../dbs/models/users.js')
//定义本地登陆策略及序列化与反序列化操做
passport.use(new LocalStrategy(async function(username, password, done) {
let where = {
username
};
//先在mongoose数据库中查询是否有该用户
let result = await UserModel.findOne(where)
if (result != null) {
if (result.password === password) {
return done(null, result)
} else {
return done(null, false, '密码错误')
}
} else {
return done(null, false, '用户不存在')
}
}))
//session序列化
passport.serializeUser(function(user, done) {
done(null, user)
})
//session反序列化
passport.deserializeUser(function(user, done) {
done(null, user)
})
module.exports = passport
复制代码
2.应用passport中间件ajax
app.use(passport.initialize())
app.use(passport.session())
复制代码
3.在后台设置登陆接口redis
/**
* -----登陆接口-----
*/
router.post('/signin', async (ctx, next) => {
let {username, password} = ctx.request.body
//不存在用户名,存在密码
if (!username && password !== "d41d8cd98f00b204e9800998ecf8427e") {
ctx.body = {
code: -1,
msg: '请输入用户名'
}
return false
}
//存在用户名,不存在密码
if (username && password === "d41d8cd98f00b204e9800998ecf8427e") {
ctx.body = {
code: -1,
msg: '请输入密码'
}
return false
}
//不存在用户名和密码
if (!username && password === "d41d8cd98f00b204e9800998ecf8427e") {
ctx.body = {
code: -1,
msg: '请输入用户名和密码'
}
return false
}
//进行本地登陆认证
return Passport.authenticate("local", function (err, user, info, status) {
if (err) {
ctx.body = {
code: -1,
msg: err
}
} else {
if (user) {
ctx.body = {
code: 0,
msg: "登陆成功",
user
}
return ctx.login(user)
} else {
ctx.body = {
code: 1,
msg: info
}
}
}
})(ctx, next)
})
复制代码
4.在客户端使用element-ui的form表单,在登陆注册的时候,会请求登陆接口并进行相关的逻辑判断及本地登陆验证,一旦登陆成功,就写入redis数据库,跳转到首页。
login() {
this.$axios.post('/users/signin', {
username: window.encodeURIComponent(this.username),
password: cryptoJs.MD5(this.password).toString()
}).then(({ status, data }) => {
if (status === 200) {
if (data && data.code === 0) {
location.href = '/' //成功后跳转页面
} else {
this.error = data.msg
}
} else {
this.error = `服务器出错,状态码${status}`
}
})
}
复制代码
将省份与城市关联,必须选择省份,才能够选择省份下面的城市
获取高德web服务api接口的行政区域查询,返回下两级行政区(其中行政区级别包括:国家、省/直辖市、市)
选用element-ui的组件(select选择器)
<span>按省份选择:</span>
<el-select v-model="pvalue" placeholder="省份">
<el-option v-for="item in province" :key="item.value" :label="item.label" :value="item.value">
</el-option>
</el-select>
<!--city.length为空,选择城市下框将禁用-->
<el-select v-model="cvalue" placeholder="城市" :disabled="!city.length" @visible-change="select" ref="currentCity">
<el-option v-for="item in city" :key="item.value" :label="item.label" :value="item.value">
</el-option>
</el-select>
复制代码
export default {
data() {
return {
pvalue: '',
cvalue: '',
search: '',
public: [], //全部的数据
province: [], //全部的省份
city: [], //根据省份获取城市
}
},
//1.获取全部的数据,全部的国家/城市/省份
async mounted() {
const {status, data: { districts:[{ districts }]} } = await this.$axios.get('https://restapi.amap.com/v3/config/district?subdistrict=2&key=b598c12de310236d9d40d3e28ea94d03')
if (status === 200) {
//获取所有数据
this.public = districts
// 获取省份
this.province = districts.map(item => {
return {
value: item.adcode,
label: item.name
}
})
}
}
}
</script>
复制代码
export default {
watch: {
//监听pvalue的变化,根据省份获取城市
pvalue: function (newPvalue) {
this.city = this.public.filter(item => item.adcode===newPvalue)[0].districts
this.city = this.city.map(item => {
return {
value: item.name,
label: item.name
}
})
}
}
}
复制代码
import { mapMutations } from 'vuex'
export default {
methods: {
...mapMutations({
setPosition: 'geo/setPosition'
}),
async select () {
const isSelect = this.$refs.currentCity.value
if (isSelect) {
this.$store.commit('geo/setCity', isSelect)
location.href = '/'
}
}
}
}
复制代码
缘由:
每一刷新页面,vuex数据丢失,从新加载vue实例,store里面的数据就会被从新赋值。
在前面nuxtServerInit函数中,根据用户ip地址所获取的城市,在页面渲染(组件加载
)前快速操做了store。因此即便在页面刷新前,改变store的city,页面刷新后,仍是变回
根据用户ip地址所获取的城市。
复制代码
解决方案:
将state里的数据保存一份到本地存储(sessionStorage),来实现数据持久化,由于咱们是
只有在刷新页面时才会丢失state里的数据,因此在点击页面刷新时(触发beforeunload事件)
先将state数据保存到sessionStorage,在页面加载时读取sessionStorage里的状态信息。
复制代码
但每一个页面都写入这个,太麻烦。因此我把它放在layouts文件夹的default.vue文件中。
export default {
mounted () {
//在页面加载时读取sessionStorage里的状态信息
if (window.sessionStorage.getItem("store") ) {
this.$store.replaceState(Object.assign({}, this.$store.state, JSON.parse(sessionStorage.getItem("store"))))
}
//在页面刷新时将vuex里的信息保存到sessionStorage里
window.addEventListener("beforeunload",()=>{
window.sessionStorage.setItem("store",JSON.stringify(this.$store.state))
})
}
}
复制代码