1.Koa是什么
Koa是下一代的Node.js的Web框架。既然说是下一代,那么肯定有上一代咯。 没错,上一代Node.js的Web框架叫Express。
要问它俩有什么共同点,它俩的共同点是两个框架来自同一个团队。 要问它俩最大的不同点,它俩最大的不同点就是轻与重。
讲人话,Koa到底如何理解呢?简单理解,Koa就是一个轻量的服务器,与Java的Tomact类似。
2.Koa的特点
1.轻量
Koa的核心文件总共不超过40KB,Koa只提供封装好的http上下文、请求、响应,以及基于async/await的中间件容器。
2.灵活
轻的好处当然就是灵活,可以直接使用Koa的各类包,来满足你对业务不同的需求。NPM官网到目前为止,已经收录2584个关于Koa的npm包。
3.先进
这个怎么解释呢?koa2和koa1有很大的区别,koa2利用ES7的async/await的来处理传统回调嵌套问题和代替koa1的generator。 可能听不懂什么意思,没关系,简单讲就是为解决异步。 以前的ES5,接着ES6,最后ES7,举个简单例子。
1.使用ES5传统回调嵌套
function ajaxs(callback){
$.get('a.html',function(dataa) {
console.log(dataa);
$.get('b.html',function(datab) {
console.log(datab);
$.get('c.html',function(datac) {
console.log(datac);
callback();
});
});
});
}
ajaxs(function(){
console.log('OK');
});
2.使用ES6的generator
function request(url) {
$.get(url, function(response){
console.log(response);
it.next(response);
});
}
function* ajaxs() {
yield request('a.html');
yield request('b.html');
yield request('c.html');
return console.log('OK');
}
let it = ajaxs();
it.next();
3.使用ES7的async/await
async function request(url) {
await $.get(url, function(response){
console.log(response);
});
}
async function ajaxs() {
await request('a.html');
await request('b.html');
await request('c.html');
console.log('OK');
}
ajaxs();
koa2可以让异步逻辑用同步写法实现,因为其支持async
和await
。该特性可以通过多层 async function
的同步写法代替传统的callback
嵌套。
这样写的后端代码逻辑既清晰,又美观,是不是很先进。以后如果还有能解决异步的最佳方法,相信koa也会与时俱进,及时跟进,就目前而言,使用async/await是解决异步的最佳方法。
3.Koa的中间件
中间件是什么?中间件就是指上面的2584个关于Koa的npm包,有点类似Java的jar包。 使用任意一个npm包可没有像Java那么简单,Java只需要导入jar包后build path即可,而JS需要引入且配置。
下面主要讲解常用的koa中间件:koa-router、koa-logger、koa-session、koa-bodyparser。 当然koa和mongoose整合也是必不可少的。 直接上完整的项目代码,首先看下整体的目录结构:
目录讲解:
app/models 模型层,定义各种表结构
app/controllers 业务层,实现各种功能
app/service 服务层,调用第三方的服务,如:发短信验证码
config 配置请求路径
app.js 服务器主入口
1.主入口app.js功能讲解
'use strict'
/**
* mongoose Node连接MongoDB数据库
* bluebird 功能全面的Promise库
* koa web框架核心库
* koa-router 路由中间件
* koa-logger 打印日志中间件
* koa-session session中间件
* koa-bodyparser body解析器(如:POST请求传参)
*
* speakeasy 验证码生成器
* xss 防止xss攻击
* uuid 唯一识别码token
* sms 发送螺丝帽短信工具
*
**/
//引入node读取文件和路径库
const fs = require('fs')
const path = require('path')
//引入mongoose,配置MongoDB数据库
const mongoose = require('mongoose')
const url = 'mongodb://localhost/xjs'
//mongoose默认的promise库已过时
mongoose.Promise = require('bluebird')
//创建数据库连接
//方法1
//const db = mongoose.createConnection('localhost', 'xjs');
//方法2
mongoose.connect(url);
const db = mongoose.connection;
//监听数据库连接状态
db.on('error', ctx => console.log('连接异常:' + ctx))
db.on('connected', ctx => console.log('连接成功'))
db.on('disconnected', ctx => console.log('连接断开'))
//当前models路径
const models_path = path.join(__dirname, '/app/models')
//循环读取models
const walk = function(modelPath) {
fs
.readdirSync(modelPath)
.forEach(function(file) {
let filePath = path.join(modelPath, '/' + file)
let stat = fs.statSync(filePath)
console.log(filePath);
if (stat.isFile()) {
if (/(.*)\.(js|coffee)/.test(file)) {
require(filePath)
}
} else if (stat.isDirectory()) {
walk(filePath)
}
})
}
//执行
walk(models_path)
//引入Koa框架
const Koa = require('koa')
const logger = require('koa-logger')
const session = require('koa-session')
const bodyParser = require('koa-bodyparser')
//实例化koa
const app = new Koa()
//使用中间件
//引入koa-logger,支持日志打印
app.use(logger())
//引入koa-bodyparser,支持body为json、form、text格式
app.use(bodyParser())
//引入session,设置自定义的cookie名,默认是koa.sid
app.keys = ['xjs']
app.use(session(app))
//引入koa-router配置路由
const router = require('./config/routes')()
app.use(router.routes()).use(router.allowedMethods())
//监听端口3001
app.listen(3001)
console.log('Listening:3001')
1.引入nodejs核心功能模块fs、path
path:处理文件的路径 fs:提供本地文件的读写能力
2.配置MongoDB数据库
a.使用fs、path循环读取models文件夹下面的文件,导入多个表结构model b.修改mongoose已过时的promise库,将其修改成bluebird c.配置本地具体的数据库,开启连接 d.监听数据库连接状态
3.引入Koa中间件
a.koa-logger,实现控制台日志的打印 b.koa-bodyparser,实现Post请求传参 c.koa-session,需要用到session修改用户信息 d.koa-router,配置请求路由
4.启动Koa服务器,监听3001端口
2.路由routes.js功能讲解
'use strict'
//引入koa-router中间件
const Router = require('koa-router')
//引入业务层
const User = require('../app/controllers/user')
const App = require('../app/controllers/app')
//导出一个匿名方法在配置koa时调用(app.js)
module.exports = function() {
//实例化一个router,并声明前缀
let router = new Router({
prefix: '/api'
});
//用户请求 中间件依次执行
router.post('/user/signup', App.hasBody, User.signup);
router.post('/user/verify', App.hasBody, User.verify);
router.post('/user/update', App.hasBody, App.hasToken, User.update);
//公用请求或工具请求,如:加密,校验accessToken
router.get('/app/jm', App.jm);
//TODO
return router;
}
1.引入koa-router,引入业务层,如用户、公用的方法。
2.配置各类请求及业务层对应的方法,如用户的获取验证码、验证登录、修改用户昵称等请求。
3.配置时可以使用中间件,举例说明:
router.post('/user/update', App.hasBody, App.hasToken, User.update);
该方法的意思是配置一个地址为:api/user/update
的方法,该方法能够更新用户的昵称。
当用户确认修改昵称后,会依次执行公共业务层的hasBody、hasToken及用户业务层的update方法。
hasBody方法判断参数是否缺少、hasToken方法判断是否有accessToken用户的唯一标识,update方法用来更新用户昵称。
这是个递进的方法,一层一层拦截,如果没参数不会执行下面的方法,依次类推。
3.模型层user.js讲解
'use strict'
//引入mongoose
const mongoose = require('mongoose')
//定义表结构
const UserSchema = new mongoose.Schema({
number: { //手机号
unique: true,
type: String
},
code: String, //验证码
accessToken: String, //用户唯一标识
nickname: String, //昵称
avatar: String, //头像
verified: { //是否验证过
type: Boolean,
default: false
},
meta: {
createAt: { //创建时间
type: Date,
default: Date.now()
},
updateAt: { //更新时间
type: Date,
default: Date.now()
}
}
})
//定义添加数据的拦截器
UserSchema.pre('save', function(next) {
if (!this.isNew) { //老数据
this.meta.updateAt = Date.now()
}
next()
})
//导出用户Model
module.exports = mongoose.model('User', UserSchema)
1.定义用户表结构,如手机号String类型且唯一,验证码String,是否验证过Boolean类型且默认是没验证过,创建和更新时间Date类似且默认是当前服务器时间。
2.定义拦截器,功能是如果是老数据,记录下更新时间。
3.导出用户Model,记录用户Model在mongoose里面,方便在业务层里面调用,调用如下:
const mongoose = require('mongoose')
const User = mongoose.model('User')
4.业务层user.js讲解
/**
* xss 防止xss攻击
* uuid 唯一识别码token
* sms 发送螺丝帽短信工具
**/
const xss = require('xss')
const mongoose = require('mongoose')
const User = mongoose.model('User')
const uuid = require('uuid')
const sms = require('../service/sms')
//获取验证码
exports.signup = async ctx => {
let number = ctx.request.body.number; //post
//let number = ctx.query.number;//get
let user = await User.findOne({
number: number
}).exec();
let code = sms.getCode();
console.log(code);
if (!user) { //新用户
let accessToken = uuid.v4();
user = new User({
number: xss(number),
code: code,
accessToken: accessToken
})
console.log('新用户');
} else {
user.code = code;
console.log('老用户');
}
try {
await user.save();
//await sms.send(number, code);
ctx.body = {
success: true,
msg: '验证码已发送'
};
} catch (e) {
ctx.body = {
success: false,
msg: '验证码发送失败'
};
}
}
//验证登录
exports.verify = async ctx => {
let code = ctx.request.body.code;
let number = ctx.request.body.number;
if (!code || !number) {
ctx.body = {
success: false,
msg: '验证没通过'
}
return ctx;
}
let user = await User.findOne({
number: number,
code: code
}).exec();
if (user) {
user.verified = true;
user = await user.save();
ctx.body = {
success: true,
msg: '验证通过',
data: user
}
} else {
ctx.body = {
success: false,
msg: '验证没通过'
}
}
}
//修改用户昵称
exports.update = async ctx => {
let body = ctx.request.body;
let user = ctx.session.user;
user.nickname = xss(body['nickname'].trim());
user = await user.save();
ctx.body = {
success: true,
data: {
nickname: user.nickname,
_id: user._id
}
}
}
1.引入其他第三方工具库,xss防止xss攻击的,uuid获取唯一识别码保存accessToken的,sms自定义发送短信的。
2.使用最新的ES7async
和await
,再加上ES6的语法,代码简单清爽,一目了然。
3.GET请求和POST请求在获取前端的参数上有所不同:
GET请求:
let number = ctx.query.number;//从query里面获取
POST请求:
let number = ctx.request.body.number;//从request的body里面获取
4.可以通过上下文的session直接获取到当前用户的信息
let user = ctx.session.user;
5.可直接使用模型层User进行CRUD操作:
let user = User.findOne(...);//查询一条用户数据
user.save();//添加或更新用户数据
5.业务层app.js讲解
//引入mongoose
const mongoose = require('mongoose')
//引入用户Model
const User = mongoose.model('User')
//导出加密方法 暂未写
exports.jm = ctx => {
console.log(ctx);
console.log(ctx.method);
ctx.body = '加密';
}
//导出是否缺少参数方法
exports.hasBody = async(ctx, next) => {
let body = ctx.request.body || {}
if (Object.keys(body).length === 0) {
ctx.body = {
success: false,
msg: '参数缺失'
}
return ctx
}
await next();
}
//导出是否有accessToken方法
exports.hasToken = async(ctx, next) => {
var accessToken = ctx.query.accessToken
if (!accessToken) {
accessToken = ctx.request.body.accessToken
}
if (!accessToken) {
ctx.body = {
success: false,
msg: '没accessToken'
}
return ctx
}
var user = await User.findOne({
accessToken: accessToken
}).exec();
if (!user) {
ctx.body = {
success: false,
msg: '用户没登录'
}
return ctx
}
ctx.session = ctx.session || {}
ctx.session.user = user;
await next();
}
1.注意在hasToken方法里,如果该用户的accessToken存在,则获取到该用户,且放进session里面供业务层user.js使用。
ctx.session.user = user;//设置该用户信息进入session
2.hasToken方法可以在需要校验用户登录后才能正常调用的接口,在配置路径的时候很方便。
如:更新用户昵称、上传用户头像、支付等。
router.post('/user/update', App.hasBody, App.hasToken, User.update);//更新用户昵称
router.post('/user/avator', App.hasBody, App.hasToken, User.update);//上传头像
....
5.第三方服务sms.js讲解
'use strict'
/**
* speakeasy 验证码生成器
**/
const https = require('https')
const querystring = require('querystring')
const Promise = require('bluebird')
const speakeasy = require('speakeasy')
exports.getCode = function() {
var code = speakeasy.totp({
secret: 'xjsapp',
digits: 6
})
return code
}
exports.send = function(number, code) {
return new Promise(function(resolve, reject) {
if (!number) {
return reject(new Error('手机号不能为空!'))
}
var postData = {
mobile: number,
message: '您的验证码是' + code + '。请在页面中提交验证码完成验证。【享健身】'
}
var content = querystring.stringify(postData)
var options = {
host: 'sms-api.luosimao.com',
path: '/v1/send.json',
method: 'POST',
auth: 'api:key-xxxxxxxxxxxxxxxxxxxxxxxxxx',
agent: false,
rejectUnauthorized: false,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': content.length
}
}
var str = ''
var req = https.request(options, function(res) {
if (res.statusCode === 404) {
reject(new Error('短信服务器没有响应'))
return
}
res.setEncoding('utf8')
res.on('data', function(chunk) {
str += chunk
})
res.on('end', function() {
var data
try {
data = JSON.parse(str)
} catch (e) {
reject(e)
}
if (data.error === 0) {
resolve(data)
} else {
var errorMap = {
'-10': '验证信息失败 检查apikey是否和各种中心内的一致,调用传入是否正确',
'-11': '用户接口被禁用滥发违规内容,验证码被刷等,请联系客服解除',
'-20': '短信余额不足 进入个人中心购买充值',
'-30': '短信内容为空 检查调用传入参数:message',
'-31': '短信内容存在敏感词 接口会同时返回 hit 属性提供敏感词说明,请修改短信内容,更换词语',
'-32': '短信内容缺少签名信息 短信内容末尾增加签名信息eg.【公司名称】',
'-33': '短信过长,超过300字(含签名) 调整短信内容或拆分为多条进行发送',
'-40': '错误的手机号 检查手机号是否正确',
'-41': '号码在黑名单中 号码因频繁发送或其他原因暂停发送,请联系客服确认',
'-42': '验证码类短信发送频率过快 前台增加60秒获取限制',
'-50': '请求发送IP不在白名单内 查看触发短信IP白名单的设'
}
reject(new Error(errorMap[data.error]))
}
})
})
req.write(content)
req.end()
})
}
1.引入第三方库speakeasy,该库能随机生成6位数的验证码。
2.这个js封装对螺丝帽服务的短信接口和生成随机的二维码,供业务层user.js使用。
let code = sms.getCode();//获取6位数验证码
...
sms.send(number, code);//发送手机验证码
总结一下:
Koa功能还是很强大的,和MongoDB一起使用完全能应对中小型业务。
骚年们,继续加油吧!