目标是建立一个 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 #socketio 目录
├── App.JS #入口文件
└── router.JS #路由
入口文件 App.JS
主要就是几个中间件配置需要注意一下, 这里同时还加载了 socket.io 服务. socket.io 相关的基本知识点可以看我之前写的文章关于 socket.io 的使用.
- //App.JS
- //...
- const path = require("path");
- const baseDir = path.normalize(__dirname + "/..");
- // gzip
- 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, // 解析 FormData 数据
- 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")));
- //cors
- App.use(
- cors({
- origin: "http://localhost:" + config.clientPort,
- credentials: true,
- allowMethods: ["GET", "POST", "DELETE"],
- exposeHeaders: ["Authorization"],
- allowHeaders: ["Content-Type", "Authorization", "Accept"]
- })
- );
- //JSON-Web-token 中间件
- App.use(
- jwt({
- secret: config.secret,
- exp: config.exp
- })
- );
- // 登录验证中间件, exclude 表示不验证的页面, include 表示要验证的页面
- App.use(
- verify({
- exclude: ["/login", "/register", "/search"]
- })
- );
- // 错误处理中间件
- App.use(errorHandler());
- // 路由
- addRouters(router);
- App.use(router.routes()).use(router.allowedMethods());
- // 处理 404
- 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); //log all errors
- ctx.status = 500;
- ctx.statusText = "Internal Server Error";
- if (ctx.App.env === "development") {
- //throw the error to frontEnd when in the develop mode
- ctx.res.end(err.stack); //finish the response
- } else {
- ctx.body = { code: -1, message: "Server Error" };
- }
- });
- if (!module.parent) {
- const { port, socketPort } = config;
- /**
- * koa App
- */
- App.listen(port);
- log.info(`=== app server running on port ${port}===`);
- console.log("app server running at: http://localhost:%d", port);
- /**
- * socket.io
- */
- 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, // 访问 header, 要写明具体域名才行
- credentials: true, // 将凭证暴露出来, 前端才能获取 cookie
- allowMethods: ["GET", "POST", "DELETE"],
- exposeHeaders: ["Authorization"], // 将 header 字段 expose 出去
- allowHeaders: ["Content-Type", "Authorization", "Accept"] // 允许添加到 header 的字段
- })
- );
中间件 middleware
koa 的中间件就是 Web 开发的利器, 通过它可以非常方便的实现 强类型语言中的 aop 切面编程, 而 koa2 中间件 的编写也足够简单 http://koajs.cn/ .
项目在以下几个地方都用中间件进行了封装, 很多重复的样板代码因此得以简化.
JSON Web token(jwt)
登录验证 (verify)
错误处理 (errorHandler)
就以最简单的错误处理中间件为例子, 如果不使用错误处理中间件, 我们需要每个控制器方法进行 try{...} catch{...} , 其他中间件编写方式类似, 就不再介绍.
- /**
- * error handler 中间件
- */
- 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
- }
- };
- };
- // 控制器代码使用 error handler 中间件后, 每个方法都不需要 try catch 处理错误, 记录错误日志, 处理逻辑都集中在中间件里面了.
- exports.getInfo = async function(ctx) {
- // try {
- 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
- }
- };
- // } catch (err) {
- // log.error(err);
- // let obj = {
- // code: -1,
- // message: "服务器错误"
- // };
- // if (ctx.App.env === "development") {
- // obj.err = err;
- // }
- // ctx.body = obj;
- // }
- };
路由配置
路由配置只使用了 get,post 方法, 当然要使用 put,delete 也只是改一下名字就行.
- // router.JS
- 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 构建 数据库访问层.
来源: https://www.cnblogs.com/edwardloveyou/p/10673663.html