經過 vue-cli3.0 + Element 構建項目前端,Node.js + Koa2 + MongoDB + Redis 實現數據庫和接口設計,包括郵箱驗證碼、用戶註冊、用戶登陸、查看刪除用戶等功能。css
"dependencies": { "axios": "^0.18.0", "crypto-js": "^3.1.9-1", "element-ui": "^2.4.5", "js-cookie": "^2.2.0", "jsonwebtoken": "^8.5.0", "koa": "^2.7.0", "koa-bodyparser": "^4.2.1", "koa-generic-session": "^2.0.1", "koa-json": "^2.0.2", "koa-redis": "^3.1.3", "koa-router": "^7.4.0", "mongoose": "^5.4.19", "nodemailer": "^5.1.1", "nodemon": "^1.18.10", "vue": "^2.5.21", "vue-router": "^3.0.1", "vuex": "^3.0.1" }
經過 vue-cli3.0 + Element 構建項目前端頁面html
發送驗證碼前須要驗證用戶名和郵箱,用戶名必填,郵箱格式需正確。前端
用戶設置頁(@/view/users/setting/Setting.vue)vue
用戶登陸後,能夠進入用戶設置頁查看用戶和刪除用戶node
經過 vuex 實現保存或刪除用戶 token,保存用戶名等功能。webpack
因爲使用單一狀態樹,應用的全部狀態會集中到一個比較大的對象。當應用變得很是複雜時,store 對象就有可能變得至關臃腫。ios
爲了解決以上問題,Vuex 容許咱們將 store 分割成模塊(module)。每一個模塊擁有本身的 state、mutation、action、getter。git
根目錄下新建store文件夾,建立modules/user.js:github
const user = { state: { token: localStorage.getItem('token'), username: localStorage.getItem('username') }, mutations: { BIND_LOGIN: (state, data) => { localStorage.setItem('token', data) state.token = data }, BIND_LOGOUT: (state) => { localStorage.removeItem('token') state.token = null }, SAVE_USER: (state, data) => { localStorage.setItem('username', data) state.username = data } } } export default user
建立文件 getters.js 對數據進行處理輸出:web
const getters = { sidebar: state => state.app.sidebar, device: state => state.app.device, token: state => state.user.token, username: state => state.user.username } export default getters
建立文件 index.js 管理全部狀態:
import Vue from 'vue' import Vuex from 'vuex' import user from './modules/user' import getters from './getters' Vue.use(Vuex) const store = new Vuex.Store({ modules: { user }, getters }) export default store
路由配置(router.js):
import Vue from 'vue' import Router from 'vue-router' const Login = () => import(/* webpackChunkName: "users" */ '@/views/users/Login.vue') const Register = () => import(/* webpackChunkName: "users" */ '@/views/users/Register.vue') const Setting = () => import(/* webpackChunkName: "tables" */ '@/views/setting/Setting.vue') Vue.use(Router) const router = new Router({ base: process.env.BASE_URL, routes: [ { path: '/login', name: 'Login', component: Login, meta: { title: '登陸' } }, { path: '/register', name: 'Register', component: Register, meta: { title: '註冊' } }, { path: '/setting', name: 'Setting', component: Setting, meta: { breadcrumb: '設置', requireLogin: true }, } ] })
路由攔截:
關於vue 路由攔截參考:http://www.javashuo.com/article/p-pzcvuewo-gy.html
// 頁面刷新時,從新賦值token if (localStorage.getItem('token')) { store.commit('BIND_LOGIN', localStorage.getItem('token')) } // 全局導航鉤子 router.beforeEach((to, from, next) => { if (to.meta.title) { // 路由發生變化修改頁面title document.title = to.meta.title } if (to.meta.requireLogin) { if (store.getters.token) { if (Object.keys(from.query).length === 0) { // 判斷路由來源是否有query,處理不是目的跳轉的狀況 next() } else { let redirect = from.query.redirect // 若是來源路由有query if (to.path === redirect) { // 避免 next 無限循環 next() } else { next({ path: redirect }) // 跳轉到目的路由 } } } else { next({ path: '/login', query: { redirect: to.fullPath } // 將跳轉的路由path做爲參數,登陸成功後跳轉到該路由 }) } } else { next() } }) export default router
封裝 Axios
// axios 配置 import axios from 'axios' import store from './store' import router from './router' //建立 axios 實例 let instance = axios.create({ timeout: 5000, // 請求超過5秒即超時返回錯誤 headers: { 'Content-Type': 'application/json;charset=UTF-8' }, }) instance.interceptors.request.use( config => { if (store.getters.token) { // 若存在token,則每一個Http Header都加上token config.headers.Authorization = `token ${store.getters.token}` console.log('拿到token') } console.log('request請求配置', config) return config }, err => { return Promise.reject(err) }) // http response 攔截器 instance.interceptors.response.use( response => { console.log('成功響應:', response) return response }, error => { if (error.response) { switch (error.response.status) { case 401: // 返回 401 (未受權) 清除 token 並跳轉到登陸頁面 store.commit('BIND_LOGOUT') router.replace({ path: '/login', query: { redirect: router.currentRoute.fullPath } }) break default: console.log('服務器出錯,請稍後重試!') alert('服務器出錯,請稍後重試!') } } return Promise.reject(error.response) // 返回接口返回的錯誤信息 } ) export default { // 發送驗證碼 userVerify (data) { return instance.post('/api/verify', data) }, // 註冊 userRegister (data) { return instance.post('/api/register', data) }, // 登陸 userLogin (data) { return instance.post('/api/login', data) }, // 獲取用戶列表 getAllUser () { return instance.get('/api/alluser') }, // 刪除用戶 delUser (data) { return instance.post('/api/deluser', data) } }
在根目錄下建立 server 文件夾,存放服務端和數據庫相關代碼。
建立 /server/dbs/config.js ,進行數據庫和郵箱配置
// mongo 鏈接地址 const dbs = 'mongodb://127.0.0.1:27017/[數據庫名稱]' // redis 地址和端口 const redis = { get host() { return '127.0.0.1' }, get port() { return 6379 } } // qq郵箱配置 const smtp = { get host() { return 'smtp.qq.com' }, get user() { return '1********@qq.com' // qq郵箱名 }, get pass() { return '*****************' // qq郵箱受權碼 }, // 生成郵箱驗證碼 get code() { return () => { return Math.random() .toString(16) .slice(2, 6) .toUpperCase() } }, // 定義驗證碼過時時間rules,5分鐘 get expire() { return () => { return new Date().getTime() + 5 * 60 * 1000 } } } module.exports = { dbs, redis, smtp }
使用 qq 郵箱發送驗證碼,須要在「設置/帳戶」中打開POP3/SMTP服務和MAP/SMTP服務。
建立 /server/dbs/models/users.js:
// users模型,包括四個字段 const mongoose = require('mongoose') const Schema = mongoose.Schema const UserSchema = new Schema({ username: { type: String, unique: true, required: true }, password: { type: String, required: true }, email: { type: String, required: true }, token: { type: String, required: true } }) module.exports = { Users: mongoose.model('User', UserSchema) }
建立 /server/interface/user.js:
const Router = require('koa-router') const Redis = require('koa-redis') // key-value存儲系統, 存儲用戶名,驗證每一個用戶名對應的驗證碼是否正確 const nodeMailer = require('nodemailer') // 經過node發送郵件 const User = require('../dbs/models/users').Users const Email = require('../dbs/config') // 建立和驗證token, 參考4.4 const createToken = require('../token/createToken.js') // 建立token const checkToken = require('../token/checkToken.js') // 驗證token // 建立路由對象 const router = new Router({ prefix: '/api' // 接口的統一前綴 }) // 獲取redis的客戶端 const Store = new Redis().client // 接口 - 測試 router.get('/test', async ctx => { ctx.body = { code: 0, msg: '測試', } }) // 發送驗證碼 的接口 router.post('/verify', async (ctx, next) => { const username = ctx.request.body.username const saveExpire = await Store.hget(`nodemail:${username}`, 'expire') // 拿到過時時間 console.log(ctx.request.body) console.log('當前時間:', new Date().getTime()) console.log('過時時間:', saveExpire) // 檢驗已存在的驗證碼是否過時,以限制用戶頻繁發送驗證碼 if (saveExpire && new Date().getTime() - saveExpire < 0) { ctx.body = { code: -1, msg: '發送過於頻繁,請稍後再試' } return } // QQ郵箱smtp服務權限校驗 const transporter = nodeMailer.createTransport({ /** * 端口465和587用於電子郵件客戶端到電子郵件服務器通訊 - 發送電子郵件。 * 端口465用於smtps SSL加密在任何SMTP級別通訊以前自動啓動。 * 端口587用於msa */ host: Email.smtp.host, port: 587, secure: false, // 爲true時監聽465端口,爲false時監聽其餘端口 auth: { user: Email.smtp.user, pass: Email.smtp.pass } }) // 郵箱須要接收的信息 const ko = { code: Email.smtp.code(), expire: Email.smtp.expire(), email: ctx.request.body.email, user: ctx.request.body.username } // 郵件中須要顯示的內容 const mailOptions = { from: `"認證郵件" <${Email.smtp.user}>`, // 郵件來自 to: ko.email, // 郵件發往 subject: '邀請碼', // 郵件主題 標題 html: `您正在註冊****,您的邀請碼是${ko.code}` // 郵件內容 } // 執行發送郵件 await transporter.sendMail(mailOptions, (err, info) => { if (err) { 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分鐘' } }) // 接口 - 註冊 router.post('/register', 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') // 過時時間 console.log(ctx.request.body) console.log('redis中保存的驗證碼:', saveCode) console.log('當前時間:', new Date().getTime()) console.log('過時時間:', saveExpire) // 用戶提交的驗證碼是否等於已存的驗證碼 if (code === saveCode) { if (new Date().getTime() - saveExpire > 0) { ctx.body = { code: -1, msg: '驗證碼已過時,請從新申請' } return } } else { ctx.body = { code: -1, msg: '請填寫正確的驗證碼' } return } } else { ctx.body = { code: -1, msg: '請填寫驗證碼' } return } // 用戶名是否已經被註冊 const user = await User.find({ username }) if (user.length) { ctx.body = { code: -1, msg: '該用戶名已被註冊' } return } // 若是用戶名未被註冊,則寫入數據庫 const newUser = await User.create({ username, password, email, token: createToken(this.username) // 生成一個token 存入數據庫 }) // 若是用戶名被成功寫入數據庫,則返回註冊成功 if (newUser) { ctx.body = { code: 0, msg: '註冊成功', } } else { ctx.body = { code: -1, msg: '註冊失敗' } } }) // 接口 - 登陸 router.post('/login', async (ctx, next) => { const { username, password } = ctx.request.body let doc = await User.findOne({ username }) if (!doc) { ctx.body = { code: -1, msg: '用戶名不存在' } } else if (doc.password !== password) { ctx.body = { code: -1, msg: '密碼錯誤' } } else if (doc.password === password) { console.log('密碼正確') let token = createToken(username) // 生成token doc.token = token // 更新mongo中對應用戶名的token try { await doc.save() // 更新mongo中對應用戶名的token ctx.body = { code: 0, msg: '登陸成功', username, token } } catch (err) { ctx.body = { code: -1, msg: '登陸失敗,請從新登陸' } } } }) // 接口 - 獲取全部用戶 須要驗證 token router.get('/alluser', checkToken, async (ctx, next) => { try { let result = [] let doc = await User.find({}) doc.map((val, index) => { result.push({ email: val.email, username: val.username, }) }) ctx.body = { code: 0, msg: '查找成功', result } } catch (err) { ctx.body = { code: -1, msg: '查找失敗', result: err } } }) // 接口 - 刪除用戶 須要驗證 token router.post('/deluser', checkToken, async (ctx, next) => { const { username } = ctx.request.body try { await User.findOneAndRemove({username: username}) ctx.body = { code: 0, msg: '刪除成功', } } catch (err) { ctx.body = { code: -1, msg: '刪除失敗', } } }) module.exports = { router }
上面實現了五個接口:
分別對應了前面 3.4 中 axios 中的5個請求地址
JSON Web Token(縮寫 JWT)是目前最流行的跨域認證解決方案。詳情參考:http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html
分別建立 /server/token/createToken.js 和 /server/token/checkToken.js
// 建立token const jwt = require('jsonwebtoken') module.exports = function (id) { const token = jwt.sign( { id: id }, 'cedric1990', { expiresIn: '300s' } ) return token }
// 驗證token const jwt = require('jsonwebtoken') // 檢查 token module.exports = async (ctx, next) => { // 檢驗是否存在 token // axios.js 中設置了 authorization const authorization = ctx.get('Authorization') if (authorization === '') { ctx.throw(401, 'no token detected in http headerAuthorization') } const token = authorization.split(' ')[1] // 檢驗 token 是否已過時 try { await jwt.verify(token, 'cedric1990') } catch (err) { ctx.throw(401, 'invalid token') } await next() }
根目錄建立 server.js:
// server端啓動入口 const Koa = require('koa') const app = new Koa(); const mongoose = require('mongoose') const bodyParser = require('koa-bodyparser') const session = require('koa-generic-session') const Redis = require('koa-redis') const json = require('koa-json') // 美化json格式化 const dbConfig = require('./server/dbs/config') const users = require('./server/interface/user.js').router // 一些session和redis相關配置 app.keys = ['keys', 'keyskeys'] app.proxy = true app.use( session({ store: new Redis() }) ) app.use(bodyParser({ extendTypes: ['json', 'form', 'text'] })) app.use(json()) // 鏈接數據庫 mongoose.connect( dbConfig.dbs, { useNewUrlParser: true } ) mongoose.set('useNewUrlParser', true) mongoose.set('useFindAndModify', false) mongoose.set('useCreateIndex', true) const db = mongoose.connection mongoose.Promise = global.Promise // 防止Mongoose: mpromise 錯誤 db.on('error', function () { console.log('數據庫鏈接出錯') }) db.on('open', function () { console.log('數據庫鏈接成功') }) // 路由中間件 app.use(users.routes()).use(users.allowedMethods()) app.listen(8888, () => { console.log('This server is running at http://localhost:' + 8888) })
詳情參考:http://www.javashuo.com/article/p-wedrkgsk-gu.html
vue 前端啓動端口9527 和 koa 服務端啓動端口8888不一樣,須要作跨域處理,打開vue.config.js:
devServer: { port: 9527, https: false, hotOnly: false, proxy: { '/api': { target: 'http://127.0.0.1:8888/', // 接口地址 changeOrigin: true, ws: true, pathRewrite: { '^/': '' } } } }
import axios from '../../axios.js' import CryptoJS from 'crypto-js' // 用於MD5加密處理
發送驗證碼:
// 用戶名不能爲空,而且驗證郵箱格式 sendCode() { let email = this.ruleForm2.email if (this.checkEmail(email) && this.ruleForm2.username) { axios.userVerify({ username: encodeURIComponent(this.ruleForm2.username), email: this.ruleForm2.email }).then((res) => { if (res.status === 200 && res.data && res.data.code === 0) { this.$notify({ title: '成功', message: '驗證碼發送成功,請注意查收。有效期5分鐘', duration: 1000, type: 'success' }) let time = 300 this.buttonText = '已發送' this.isDisabled = true if (this.flag) { this.flag = false; let timer = setInterval(() => { time--; this.buttonText = time + ' 秒' if (time === 0) { clearInterval(timer); this.buttonText = '從新獲取' this.isDisabled = false this.flag = true; } }, 1000) } } else { this.$notify({ title: '失敗', message: res.data.msg, duration: 1000, type: 'error' }) } }) } }
註冊:
submitForm(formName) { this.$refs[formName].validate(valid => { if (valid) { axios.userRegister({ username: encodeURIComponent(this.ruleForm2.username), password: CryptoJS.MD5(this.ruleForm2.pass).toString(), email: this.ruleForm2.email, code: this.ruleForm2.smscode }).then((res) => { if (res.status === 200) { if (res.data && res.data.code === 0) { this.$notify({ title: '成功', message: '註冊成功。', duration: 2000, type: 'success' }) setTimeout(() => { this.$router.push({ path: '/login' }) }, 500) } else { this.$notify({ title: '錯誤', message: res.data.msg, duration: 2000, type: 'error' }) } } else { this.$notify({ title: '錯誤', message: `服務器請求出錯, 錯誤碼${res.status}`, duration: 2000, type: 'error' }) } }) } else { console.log("error submit!!"); return false; } }) },
登陸:
login(formName) { this.$refs[formName].validate(valid => { if (valid) { axios.userLogin({ username: window.encodeURIComponent(this.ruleForm.name), password: CryptoJS.MD5(this.ruleForm.pass).toString() }).then((res) => { if (res.status === 200) { if (res.data && res.data.code === 0) { this.bindLogin(res.data.token) this.saveUser(res.data.username) this.$notify({ title: '成功', message: '恭喜,登陸成功。', duration: 1000, type: 'success' }) setTimeout(() => { this.$router.push({ path: '/' }) }, 500) } else { this.$notify({ title: '錯誤', message: res.data.msg, duration: 1000, type: 'error' }) } } else { this.$notify({ title: '錯誤', message: '服務器出錯,請稍後重試', duration: 1000, type: 'error' }) } }) } }) },
$ npm run serve
$ mongod
$ redis-server
安裝 nodemon 熱啓動輔助工具:
$ npm i nodemon
$ nodemon server.js