項目地址:https://github.com/caochangkui/vue-element-responsive-demo/tree/login-register
通過 vue-cli3.0 + Element 構建項目前端,Node.js + Koa2 + MongoDB + Redis 實現數據庫和接口設計,包括郵箱驗證碼、用戶注冊、用戶登錄、查看刪除用戶等功能。
1. 技術棧
-
前端
- 初始化項目:vue-cli3.0
- 組件庫:Element-ui
- 路由控制/攔截:Vue-router
- 狀態管理:Vuex
-
服務端
- 運行環境:Node.js
- 后台開發框架:Koa2
- 路由中間件:Koa-router
- 發送郵件: nodemailer
-
HTTP通訊
- 接口請求/攔截:Axios
- Token認證:jsonwebtoken
-
數據庫
2. 項目依賴:
"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"
}
3. 前端實現步驟
3.1 登錄注冊頁面
通過 vue-cli3.0 + Element 構建項目前端頁面
登錄頁(@/view/users/Login.vue):

注冊頁(@/view/users/Register.vue):
發送驗證碼前需要驗證用戶名和郵箱,用戶名必填,郵箱格式需正確。

用戶設置頁(@/view/users/setting/Setting.vue)
用戶登錄后,可以進入用戶設置頁查看用戶和刪除用戶
3.2 Vuex 狀態管理
通過 vuex 實現保存或刪除用戶 token,保存用戶名等功能。
由於使用單一狀態樹,應用的所有狀態會集中到一個比較大的對象。當應用變得非常復雜時,store 對象就有可能變得相當臃腫。
為了解決以上問題,Vuex 允許我們將 store 分割成模塊(module)。每個模塊擁有自己的 state、mutation、action、getter。
根目錄下新建store文件夾,創建modules/user.js:
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 對數據進行處理輸出:
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
3.3 路由控制/攔截
路由配置(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 路由攔截參考:https://www.cnblogs.com/cckui/p/10319013.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
3.4 Axios 封裝
封裝 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)
}
}
4. 服務端和數據庫實現
在根目錄下創建 server 文件夾,存放服務端和數據庫相關代碼。
4.1 MongoDB和Redis
創建 /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服務。
4.2 Mongo 模型
創建 /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)
}
4.3 接口實現
創建 /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
}
上面實現了五個接口:
- 發送驗證碼至郵箱: router.post('/verify')
- 注冊:router.post('/register')
- 登錄:router.post('/login')
- 獲取用戶列表:router.get('/alluser')
- 刪除數據庫中的某個用戶:router.post('/deluser')
分別對應了前面 3.4 中 axios 中的5個請求地址
4.4 JSON Web Token 認證
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()
}
4.5 服務端入口
根目錄創建 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)
})
5. 跨域處理
詳情參考:https://www.cnblogs.com/cckui/p/10331432.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: {
'^/': ''
}
}
}
}
6. 接口對接
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'
})
}
})
}
})
},
7. 啟動項目 測試接口
7.1 vue端:
$ npm run serve
7.2 啟動mogod:
$ mongod
7.3 啟動Redis:
$ redis-server
7.4 啟動服務端server.js:
安裝 nodemon 熱啟動輔助工具:
$ npm i nodemon
$ nodemon server.js
8. 項目目錄
