最近學習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))
})
}
}
複製代碼