说明:
node.js提供接口,vue展现页面,前后端分离,出于编辑器功能和编辑习惯,vue用HbuilderX,node.js用VScode。(PS:仅作为学习笔记,如有不当之处欢迎指出,在此先谢为敬~~~)
环境:
首先需要有node.js环境,安装教程 在这里,最好下载较新的版本,对es6、es7有更好的支持,再装个 淘宝镜像,完毕!
后台:
1、安装mysql
1.1、mysql下载地址
解压到安装位置,修改环境变量,win10编辑环境变量很方便了,win7的话记得以 ; 分割开
1.2、添加配置文件
在mysql的bin目录下,新建my.ini文件(如果没有),打开my.ini文件,写入以下配置内容
[mysqld] # 设置3306端口 port=3306 # 设置mysql的安装目录 basedir=D:\\myInstalls\\mysql-8.0.11 # 设置mysql数据库的数据的存放目录 datadir=D:\\myInstalls\\mysql-8.0.11\\Data # 允许最大连接数 max_connections=200 # 允许连接失败的次数。这是为了防止有人从该主机试图攻击数据库系统 max_connect_errors=10 # 服务端使用的字符集默认为UTF8 character-set-server=utf8 # 创建新表时将使用的默认存储引擎 default-storage-engine=INNODB # 默认使用“mysql_native_password”插件认证 default_authentication_plugin=mysql_native_password [mysql] # 设置mysql客户端默认字符集 default-character-set=utf8 [client] # 设置mysql客户端连接服务端时默认使用的端口 port=3306 default-character-set=utf8
1.3、安装
以管理员身份运行cmd,进入mysql的bin目录下,不进入也行,因为我们已经配置了环境变量
初始化数据库,运行 mysqld --initialize --console,记住红色框内的初始密码
安装mysql服务,运行 mysqld --install [服务名] ,服务名可以不写,安装完毕 net start mysql 启动mysql
启动成果,mysql停止指令 net stop mysql
默认密码太复杂,改个简单的,首先运行 mysql -u root -p 进入mysql,密码是刚才记住的初始密码
修改密码指令:ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '新密码';
OK,mysql我们已经有了,接下来搭建koa2!
2、搭建koa2项目
(你可以使用系统自带的cmd窗口,也可以用编辑器自带的。我这里用VScode的命令行终端,看起来特别虚浮~~~)
2.1、我们不一步步搭建,采用koa2框架,并使用koa-generator生成项目,类似vue-cli
安装指令:cnpm install koa-generator -g
2.2、在你的项目目录下,运行 koa2 项目名,生成项目,如:koa2 paopao(泡泡是我的猫的名字~~~)
成功,根据上面提示走~~~
cd paopao 进入项目目录
cnpm install 安装项目依赖
cnpm start paopao 运行项目(cnpm是淘宝镜像)
有个报错大概意思是这个包不再维护了,cnpm uninstall koa-onerror 卸载,重新装最新的版本 cnpm install koa-onerror --save
在浏览器输入:localhost:3000,浏览器运行结果(左),项目结构(右)
3、实现API
3.1、用sequelize来操作数据库,同时安装mysql、mysql2
cnpm install sequelize mysql mysql2 --save
所有安装的依赖可以在package.json里查看:
注意:我在使用时发现koa-static(处理静态文件的中间件),默认3.0.0版本会报错,于是更新成了最新版本
使用cnpm install koa-static@5.0.0 --save更新,再查看package.json,版本变成了5.0.0即可
3.2、连接数据库
在项目根目录下建一个config文件夹,在该文件夹建一个js文件,取名db.js,用来配置数据库连接
config-->db.js
var Sequelize = require("sequelize") var sequelize = new Sequelize('paopao','root','happy',{ host:'localhost', dialect:'mysql', operatorsAliases:false, dialectOptions:{ //字符集 charset:'utf8mb4', collate:'utf8mb4_unicode_ci', supportBigNumbers: true, bigNumberStrings: true }, pool:{ max: 5, min: 0, acquire: 30000, idle: 10000 }, timezone: '+08:00' //东八时区 }); module.exports = { sequelize };
paopao是我的数据库表名,root数据库用户名,happy数据库用户密码
3.3、定义数据库模型
在根目录建一个module文件夹,在module文件下面建一个user.js,用来定义数据模型,告诉sequelize怎么跟数据库的数据一一对应
module-->user.js
module.exports = function(sequelize,DataTypes){ return sequelize.define( 'user', { userId:{ type: DataTypes.INTEGER, primaryKey: true, allowNull: true, autoIncrement: true }, mobileNo:{ type: DataTypes.STRING, allowNull: false, field: 'mobileNo' }, password:{ type: DataTypes.STRING, allowNull: false, field: 'password' } }, { timestamps: false } ); }
3.4、数据库操作和功能处理
controller-->user.js 添加以下代码
//引入db配置 const db = require('../config/db') //引入sequelize对象 const Sequelize = db.sequelize //引入数据表模型 const user = Sequelize.import('../module/user') //自动创建表 user.sync({ force: false }); //数据库操作类 class userModule { static async userRegist(data) { return await user.create({ password: data.password, mobileNo: data.mobileNo }) } static async getUserInfo(mobileNo) { return await user.findOne({ where: { mobileNo } }) } }
数据库操作有了,接下来进行功能处理,还是在该文件添加
controller-->user.js 里添加该userController 类,并将之exports出去
//功能处理 class userController { } module.exports = userController;
用户注册:
在 userController 类里添加用户注册逻辑
//注册用户 static async create(ctx) { const req = ctx.request.body; if (req.mobileNo && req.password) { try { const query = await userModule.getUserInfo(req.mobileNo); if (query) { ctx.response.status = 200; ctx.body = { code: -1, desc: '用户已存在' } } else { const param = { password: req.password, mobileNo: req.mobileNo, userName: req.mobileNo } const data = await userModule.userRegist(param); ctx.response.status = 200; ctx.body = { code: 0, desc: '用户注册成功', userInfo: { mobileNo: req.mobileNo } } } } catch (error) { ctx.response.status = 416; ctx.body = { code: -1, desc: '参数不齐全' } } } }
因为还要做登录超时token验证,用户登录成功还要返回token,为了生成token,我们需要安装几个中间件
cnpm install jsonwebtoken --save 导入jwt模块
cnpm install koa-jwt --save koa提供的jwt中间件
在app.js里添加如下代码:
unless()表示里面的regist、login不做token验证
const koajwt = require('koa-jwt') // logger app.use(async (ctx, next) => { return next().catch((err) => { if(err.status === 401){ ctx.status = 401; ctx.body = { code: '-2000', desc: '登陆过期,请重新登陆' }; }else{ throw err; } }) }) app.use(koajwt({ secret: '123456' }).unless({ path: [/^\/user\/regist/,/^\/user\/login/] }))
为了解析token,在public目录下新建tool.js,加入解析token的代码
const getToken = require('jsonwebtoken') exports.verToken = function(token){ return new Promise((resolve,rejece) => { const info = getToken.verify(token.split(' ')[1],"123456"); resolve(info); }) }
返回controller-->user.js,添加
//引入jwt做token验证 const jwt = require('jsonwebtoken') //解析token const tools = require('../public/tool') //统一设置token有效时间 为了方便观察,设为10s const expireTime = '10s'
用户登录:
之后就可以写用户登录逻辑了
controller-->user.js-->userController 类里添加
通过 jwt.asign() 方法生成token,这里的123456跟app.js里的123456相同,就理解为一个秘钥吧~~
//密码登陆 static async login(ctx) { const req = ctx.request.body; if (!req.mobileNo || !req.password) { return ctx.body = { code: '-1', msg: '用户名或密码不能为空' } } else { const data = await userModule.getUserInfo(req.mobileNo); if (data) { if (data.password === req.passWord) { //生成token,验证登录有效期 const token = jwt.sign({ user: req.mobileNo, passWord: req.password }, '123456', { expiresIn: expireTime }); const info = { createdAt: data.createdAt, updatedAt: data.updatedAt, mobileNo: data.mobileNo, userId: data.userId } return ctx.body = { code: '0', token: token, userInfo: JSON.stringify(info), desc: '登陆成功' } } else { return ctx.body = { code: '-1', desc: '用户密码错误' } } } else { return ctx.body = { code: '-1', desc: '该用户尚未注册' } } }; }
为了验证token是否过期,我们再定义一个获取用户信息的逻辑,登陆10s后获取用户信息,验证token是否过期
获取用户信息:
controller-->user.js-->userController 类里添加
//获取用户信息(除密码外) static async getUserInfo(ctx){ const req = ctx.request.body; const token = ctx.headers.authorization; if(token){ try { const result = await tools.verToken(token); if (!req.mobileNo) { return ctx.body = { code: '-1', desc: '参数错误' } } else { let data = await userModule.getUserInfo(req.mobileNo); if (req.mobileNo == data.mobileNo) { const info = { createdAt: data.createdAt, updatedAt: data.updatedAt, mobileNo: data.mobileNo, userId: data.userId }; return ctx.body = { code: '0', userInfo: JSON.stringify(info), desc: '获取用户信息成功' } } } } catch (error) { ctx.status = 401; return ctx.body = { code: '-1', desc: '登陆过期,请重新登陆' } } }else{ ctx.status = 401; return ctx.body = { code: '-1', desc: '登陆过期,请重新登陆' } } }
3.5、路由,即处理请求的url,使用koa-router
不用重新导入,koa-generator已经帮我们导入了,直接使用
在routes目录下新建文件 user.js
写入以下代码:
routes-->user.js
const Router = require('koa-router'); const userController = require('../controller/user') const router = new Router({ prefix: '/user' }); //用户注册 router.post('/regist',userController.create) //密码登陆 router.post('/login',userController.login) //获取用户信息 router.post('/getUserInfo',userController.getUserInfo) module.exports = router;
然后在入口文件app.js引入
使用
完成这些以后,cnpm run dev 启动项目(依赖nodemon,package.json里面有,这样每次更改代码以后不用手动重新启动)
启动正常如下:
如果有报错,提示缺少这包那包的,不用着急!
把根目录下的node_modules目录删除
检查一遍package.json
确认无误后重新cnpm install
再次启动 cnpm run dev ~~~
补充一点,如果想在其他端口启动,在app.js里添加 app.listen(3333),修改为3333端口,自动热刷新~~~蛋是此时接口仍然不可调试,因为存在跨域问题
3.6、解决跨域,koa-cors
koa同样提供了解决跨域的依赖包
cnpm install koa-cors --save
在app.js添加:
现在可以测试接口了,随便写个ajax或者使用postman,postman测试结果:
注册:
登录:
查看数据库结果(使用的是破解版Navicat图形化数据库管理工具):
到此为止,API就完成了,最后一步,验证token过期有没有效果
4、结合VUE验证token
写到太晚了,想起来今天还没给泡泡铲屎,VUE就不写那么详细了,有空再补上 ~.~
我就贴一下代码和验证结果
vue项目里,在接口文件里:
import axios from 'axios'; import qs from 'qs'; import route from '../router'; import { message } from 'ant-design-vue' axios.interceptors.request.use(function(config) { // 处理请求参数 config.data = qs.stringify(config.data) //将token写入请求头 if (window.localStorage.getItem('token')) { config.headers.Authorization = `Bearer ${window.localStorage.getItem('token')}`; } return config; }, function(error) { // 对请求错误做些什么 return Promise.reject(error); }); axios.interceptors.response.use( response => { return response }, error => { if (error.response) { switch (error.response.status) { case 401: message.error("登录过期,请重新登录!", ()=>{ window.localStorage.removeItem("token"); //可能是token过期,清除它 route.replace({ //跳转到登录页面 path: '/login', query: { // 将跳转的路由path作为参数,登录成功后跳转到该路由 redirect: route.currentRoute.fullPath } }); }) } } return Promise.reject(error) // 返回接口返回的错误信息 } ); //注册 export const regist = params => { return axios.post('http://localhost:3333/user/regist', params, {}).then(res => res.data) } //登录 export const login = params => { return axios.post('http://localhost:3333/user/login', params, {}).then(res => res.data) } //获取用户信息 export const getUserInfo = params => { return axios.post('http://localhost:3333/user/getUserInfo', params, {}).then(res => res.data) }
axios.interceptors.request.use拦截请求,给请求头加上token
axios.interceptors.response.use拦截响应,如果返回401,token过期,跳回login路由
登录后10s再请求用户数据,返回登录过期:
总结:完结撒花,如有不当指出,欢迎各位大神指出,我该铲屎去了 ~.~