目标是建立一个 web QQ 的项目,使用的技术栈如下:
后端是基于 koa2 的 web api 服务层,提供 curd 操作的 http 接口,登录验证使用的是 json web token,跨域方案使用的是 cors;
数据库使用的是 mysql;
为了实时通信,使用的是基于 websocket 协议的 socket.io 框架;
前端则使用的是 vue + vuex。
本篇则讲叙服务端的搭建,之所以使用 koa,而不使用其他封装过的框架,比如 Egg.js, Thinkjs。因为在我看来, koa2 已经够方便,插件也足够多,完全可以根据自己的需求,像搭积木一样构建出最适合业务需求的框架。这样不但摒弃了很多用不到的插件,使整个框架更加精简,也能对整个框架知根知底,减少了很多不可预知因素的影响。
当然我觉得最主要的是我比较懒 😄,不想再去学其他框架特有的 api,特有的配置。因为前端有太多框架太多 api 需要掌握了,对于非互联网公认的技术标准,我觉得学习的优先级还是要靠后一点的。因为这些个框架,三天两头就冒出个热门的,简直多不胜数,学不过来啊,而 koa 基本都是这些框架的底层,明显靠谱多了。
基本框架搭建
这几个 koa 插件大部分项目八九不离十要用到:
- koa-body 解析 http 数据
- koa-compress gzip 压缩
- koa-router 路由
- koa-static 设置静态目录
- koa2-cors 跨域 cors
- log4js 老牌的日志组件
- jsonwebtoken jwt 组件
基本的目录结构
public src server ├── common ├── config ├── controller ├── daos ├── logs ├── middleware ├── socket ├── app.js └── router.js
|
入口文件 app.js
主要就是几个中间件配置需要注意一下,这里同时还加载了 socket.io 服务。socket.io 相关的基本知识点可以看我之前写的文章关于 socket.io 的使用。
const path = require('path'); const baseDir = path.normalize(__dirname + '/..');
app.use( compress({ filter: function(content_type) { return /text|javascript/i.test(content_type); }, threshold: 2048, flush: require('zlib').Z_SYNC_FLUSH, }), );
app.use( koaBody({ jsonLimit: 1024 * 1024 * 5, formLimit: 1024 * 1024 * 5, textLimit: 1024 * 1024 * 5, multipart: true, formidable: { uploadDir: path.join(baseDir, 'public/upload') }, }), );
app.use(static(path.join(baseDir, 'public'), { index: false })); app.use(favicon(path.join(baseDir, 'public/favicon.ico')));
app.use( cors({ origin: 'http://localhost:' + config.clientPort, credentials: true, allowMethods: ['GET', 'POST', 'DELETE'], exposeHeaders: ['Authorization'], allowHeaders: ['Content-Type', 'Authorization', 'Accept'], }), );
app.use( jwt({ secret: config.secret, exp: config.exp, }), );
app.use( verify({ exclude: ['/login', '/register', '/search'], }), );
app.use(errorHandler());
addRouters(router); app.use(router.routes()).use(router.allowedMethods());
app.use(async (ctx, next) => { log.error(`404 ${ctx.message} : ${ctx.href}`); ctx.status = 404; ctx.body = { code: 404, message: '404! not found !' }; });
app.on('error', (err, ctx) => { log.error(err); ctx.status = 500; ctx.statusText = 'Internal Server Error'; if (ctx.app.env === 'development') { ctx.res.end(err.stack); } else { ctx.body = { code: -1, message: 'Server Error' }; } });
if (!module.parent) { const { port, socketPort } = config;
app.listen(port); log.info(`=== app server running on port ${port}===`); console.log('app server running at: http://localhost:%d', port);
addSocket(io); server.listen(socketPort); }
|
跨域 cors 和 json web token
这里解释一下 koa-cors 参数的设置,我项目使用的是 json web token,需要把认证字段 Authorization 添加到 header,前端获取该 header 字段,之后给后台发送 http 请求的时候,再带上该 Authorization。
- origin:如果要访问 header 里面的字段或者设置 cookie,要写具体的域名地址,用 星号 * 是不行的;
- credentials:主要是给前端获取 cookie;
- allowMethods:允许访问的方法;
- exposeHeaders:前端如果要获取该 header 字段,必须写明(json web token 用);
- allowHeaders:添加到 header 的字段;
至于 json web token 的原理,网上资料齐全,这里不再介绍了。
app.use( cors({ origin: 'http://localhost:' + config.clientPort, credentials: true, allowMethods: ['GET', 'POST', 'DELETE'], exposeHeaders: ['Authorization'], allowHeaders: ['Content-Type', 'Authorization', 'Accept'], }), );
|
中间件 middleware
koa 的中间件就是 web 开发的利器,通过它可以非常方便的实现 强类型语言中的 aop 切面编程,而 koa2 中间件 的编写也足够简单 koajs。
项目在以下几个地方都用中间件进行了封装,很多重复的样板代码因此得以简化。
- json web token(jwt)
- 登录验证(verify)
- 错误处理(errorHandler)
就以最简单的错误处理中间件为例子,如果不使用错误处理中间件,我们需要每个控制器方法进行 try{…} catch{…} ,其他中间件编写方式类似,就不再介绍。
module.exports = () => { return async (ctx, next) => { try { await next(); } catch (err) { log.error(err); let obj = { code: -1, message: '服务器错误', }; if (ctx.app.env === 'development') { obj.err = err; } ctx.body = obj; } }; };
exports.getInfo = async function(ctx) { const token = await ctx.verify(); const [users, friends] = await Promise.all([userDao.getUser({ id: token.uid }), getFriends([token.uid])]);
const msgs = applys.map(formatTime); ctx.body = { code: 0, message: '好友列表', data: { user: users[0], friends: mergeReads(friends, reads), groups, msgs, }, }; };
|
路由配置
路由配置只使用了 get,post 方法,当然要使用 put,delete 也只是改一下名字就行。
const { uploadFile } = require('./controller/file'); const { login, register } = require('./controller/sign'); const { addGroup, delGroup, updateGroup } = require('./controller/group');
module.exports = function(router) { router .post('/login', login) .post('/register', register) .post('/upload', uploadFile) .post('/addgroup', addGroup) .post('/delgroup', delGroup) .post('/updategroup', updateGroup); };
|
控制器
以 updateInfo 方法为例,koa2 已经全面支持 async await,编写方式和同步代码没多大区别。
exports.updateInfo = async function(ctx) { const form = ctx.request.body; const token = await ctx.verify(); const ret = await userDao.update([form, token.uid]); if (!ret.affectedRows) { return (ctx.body = { code: 2, message: '更新失败', }); } ctx.body = { code: 0, message: '更新成功', }; };
|
后续
接着下一编就是基于 mysql 构建 数据库访问层。