使用eggjs+微信小程序开发一个时间管理小程序(二)——后端项目搭建



系列文章目录

使用eggjs+微信小程序开发一个时间管理小程序(一)——项目介绍
使用eggjs+微信小程序开发一个时间管理小程序(二)——后端项目搭建
使用eggjs+微信小程序开发一个时间管理小程序(三)——前端项目搭建
使用eggjs+微信小程序开发一个时间管理小程序(四)——自动登录实现


本章要点

上一篇文章介绍了小程序【轻记时刻】的基本功能,接下来我将从后端项目搭建开始,对涉及的知识点详细展开分享。

  1. 创建项目
  2. 目录结构
  3. 基础配置
  4. mysql和jwt
  5. router、controller、service的开发约定

一、创建项目

后端我用的是nodejs,采用了阿里的eggjs框架。

Egg 是阿里巴巴基于 Koa 的有约束和规范的企业级 Web 开发框架,基于 Egg 的项目目录结构和名称有严格的规定,和 ESLint 一样,如果不符合规定那么项目将无法运行,此外,Egg 基于 MVC 的架构模式,M —— Model 层负责应用程序的数据逻辑部分,类似于 Service、V —— View 层负责应用程序的数据显示部分(静态/动态网页),类似于 Router、C —— Controller 层负责应用程序的业务逻辑部分,将数据和页面关联

我有一个开源的eggjs搭建的项目,结构与该项目基本一致,放在gitee上,大伙儿有兴趣可以下载参考下。

// 创建一个文件夹
mkdir clock
cd clock
// 初始化一个node项目
npm int -y
// 安装egg包
npm i -S egg
npm i -D egg-bin

在package.json的script中添加一句

"dev": "egg-bin dev"

之后我们将使用npm run dev命令来启动项目。

二、目录结构

egg项目需要按照规范的文件目录来建立,否则启动时会报错
官网中的目录规范
我们先依照上图创建必要的文件夹和文件(上图已做标记)

三、基础配置

config.**.js表示不同环境下的配置,default是在各个环境下都生效的配置,实际上某个环境的最终配置,是default和相应环境配置的合并。
配置中必须要有keys这个属性,其值是一个自定义的字符串,所以我们写在config.default.js中。
config有多种exports的方法,具体可以参见官方文档,写的很详细。

// config/config.default.js
exports.keys = 'clock';
// 服务的端口号 不设置的话,默认是7001
exports.cluster = {
    listen: {
        path: '',
        port: 8888,
        hostname: '0.0.0.0'
    }
}

csrf配置

  • egg 框架内置了安全系统,默认开启防止 XSS 攻击 和 CSRF 攻击,每次请求得时候请求头必须携带csrfToken字段,通过该配置,指定了header的字段名是x-csrf-token
  • 当发起请求时,前端可以通过cookie获取eggjs服务生成的csrfToken,然后将该值传到header中的x-csrf-token中,才能通过egg校验,成功发起请求。
  • 不过小程序没有cookie,所以我们将enable设置为false,禁用该安全系统
// config/config.default.js
exports.security = {
    csrf: {
        enable: false, // 禁用这个
        headerName: 'x-csrf-token'
    }
}

router.js中定义一个路由规则

// app/router.js
module.exports = app => {
    const { router,  controller } = app;
    router.get('/home/index', controller.home.index); // 首页
    router.redirect('/', '/home/index', 302); // 当没有指定路由的时候,重定向到首页
}
// app/controller/home.js
'use strict';
const Controller = require('egg').Controller;
class HomeController extends Controller {
  async index() { // 首页
    this.ctx.body = 'hello world';
  }
}
module.exports = HomeController;

有以上代码,项目就可以跑起来了,执行npm run dev,访问http://127.0.0.1:7001
可以看到,路由被重定向到了http://127.0.0.1:7001/home/index, 页面上显示hello world。说明我们的项目已经成功搭建并运行起来了

四、MySQL和JWT

1. MySQL

  • 首先要安装mysqlnavicat(可视化的数据库管理工具)
    大家可以去官网下载,mysql推荐用社区版,不收费。navicat下载后需要破解,百度很容易找到攻略,相信难不倒你
  • 软件安装完毕后,在egg项目中使用mysql,还得安装egg-mysql这个包
    npm i -S egg-mysql
  • config中添加mysql相关配置,plugin.js中指定插件的使用情况
// config/config.default.js中添加一段
exports.mysql = {
    client: {
        // host
        host: 'localhost',
        // 端口号
        port: '3306',
        // 用户名
        user: 'root',
        // 密码
        password: '你的密码',
        // 数据库名
        database: 'clock',
    },
    // 是否加载到 app 上,默认开启
    app: true,
    // 是否加载到 agent 上,默认关闭
    agent: false,
};
// config/plugin.js
exports.mysql = {
    enable: true, // 是否启用mysql插件
    package: 'egg-mysql', // 插件对应的node包
};

在业务代码中使用MySQL

async testFn(data) {
    // 开启事务
    const result = await this.app.mysql.beginTransactionScope(async conn => {
         // 查询
         const res = await conn.select('invite', {
            where: {
               invite_code: data.code
            }
         });
         // 或者直接写sql
         const res = await conn.query(`select * from invite where invit_code=${data.code}`);
    }, this.ctx);
    return result;
}

2. JWT

  • JWT是json web token的缩写,是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
  • jwt提供了signverify两个方法,sign用于签发token,verify用于校验token是否有效。
    签发的代码可以参考上面service/user.js中的login方法,签发时可以传入一些非敏感用户信息,在校验的时候可以被解析出来,用于确认当前访问的用户。
  • 关于JWT的详细解析可以点这里,就不展开了。

JWT的安装和使用

服务端要支持CORS(跨来源资源共享)策略

npm i -S jsonwebtoken
npm i -S egg-cors

启用插件

// config/plugin.js 增加一段
exports.cors = {
    enable: true,
    package: 'egg-cors'
}

// config/config.default.js 增加一段
exports.cors = {
    origin: '*', // 不限制来源
    allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH,OPTIONS',
}
exports.jwt = {
    enable: true,
    secret: 'yourPrivateKey', // 自定义的秘钥,用于token的签发和校验,不应该流露出去
    expiresIn: 60 * 60 // 有效时间,单位秒
};

JWT的工作流程

  1. 登录时,当账号密码校验通过后,使用JWT签发生成token
// 登录成功,签发jwt
   const token = JWT.sign({
        userId: '当前登录用户的userId',
   }, this.config.jwt.secret, {
        expiresIn: this.config.jwt.expiresIn
   });

token的生成原理

JWT生成的token是由三部分组成的字符串,大体结构是"header.payload.signature"

  1. header
    header 通常由两部分组成:令牌的类型,即 JWT;常用的散列算法,如 HMAC SHA256RSA
    例如:
    { "arg": "SHA256", "typ": "JWT" }
    这部分的 JSONbase64 编码,形成 token 的第一部分
  2. payload
    这里放一些非敏感信息,这块也被base64编码,形成token第二部分
    这一块就是JWT.sign方法的第一个参数,上面示例是传入了userId
  3. signature用来验证发送请求者身份,由前两部分base64的结果用.拼接后,在用指定的加密算法,以jwt配置中的密钥进行加密生成。
    HMACSHA256(base64Encode(header) + '.' + base64Encode(payload), secret)
  1. 登录接口将生成的token返回给前端,前端在之后的请求中,将该token放在请求头header的某个字段中。
  2. 服务端的某些接口如果需要在登录状态下,则需要验证这个token是否有效,要用到JWT.verify方法
  3. 封装登录状态校验方法
// extend/helper.js
checkLogin(app) {
    let obj = {
      result: false,
      body: {}
    };
    // 拿到请求体header中authorization中存储的token
    const token = app.ctx.request.header.authorization;
    if (!token) { // 不存在token
      obj = {
        result: false,
        body: {
          status: 401,
          message: '未登录, 请先登录',
          result: null
        }
      }
    } else { // 存在token
      let decode;
      try {
        // 验证当前token
        decode = JWT.verify(token, app.config.jwt.secret);
        app.ctx.user = decode;
        obj = {
          result: true,
          body: {
            result: decode
          }
        }
      } catch (e) {
        obj = {
          result: false,
          body: {
            status: 401,
            message: '登录信息已过期,请重新登录',
            result: null
          }
        }
      }
    }
    return obj;
  }

后面将说使用方法

五、router、controller、service的开发约定

1. router

router.js中定义对外暴露的接口

module.exports = app => {
    const { router,  controller } = app;
    router.get('/user/index', controller.user.index); // 首页
    router.redirect('/', '/user/index', 302); // 当没有指定路由的时候,重定向到首页

    // 用户相关
    router.post('/user/login', controller.user.login); // 用户登录
    router.post('/user/getUserInfo', controller.user.getUserInfo); // 查用户详情
}

router.get / router.post有两个参数,第一个是请求地址,第二个是对应处理的方法,我们都指向相应controller下面的方法中。get/post对应的是需要的请求method

2. controller

一个服务一个文件,我们以user服务为例

// controller/user.js
'use strict';
const Controller = require('egg').Controller;
class UserController extends Controller {
  async index() { // 首页
    this.ctx.body = 'hello world';
  }
  async login() { // 用户登录
    const res = await this.ctx.service.user.login();
    this.ctx.body = res;
    this.ctx.status = res.status;
  }
  async getUserInfo() { // 获取用户信息
    const checkResult = this.ctx.helper.checkLogin(this);
    if (checkResult.result) {
      const res = await this.ctx.service.user.getUserInfo();
      this.ctx.body = res;
      this.ctx.status = res.status;
    } else {
      this.ctx.body = checkResult.body;
      this.ctx.status = checkResult.body.status;
    }
  }
}
module.exports = UserController;

我们在controller里会按需校验登录信息,比如getUserInfo这个方法需要登录情况下才能调用,所以要先调用上面提到的checkLogin方法,先验证下token是否有效。如果无效直接返回,如果有效则调用service里面的方法。比如login不需要登录就能调用,则不用checkResult

3. service

controller一样,一个服务一个文件
service负责输入输出操作,对数据库的操作等。基础的业务逻辑校验可以放在controller

// service/user.js
'use strict';
const Service = require('egg').Service;
class UserService extends Service {
    async getUserInfo() { // 获取用户信息
        // 开启事务
        const result = await this.app.mysql.beginTransactionScope(async conn => {
            try {
                const queryUser = await conn.select('user', {
                    where: {
                        open_id: this.ctx.user.openid
                    }
                });
                if (queryUser && queryUser.length) {
                    return {
                        message: '查询成功',
                        status: 200,
                        result: queryUser[0]
                    }
                } else {
                    return {
                        message: '用户不存在',
                        status: 500,
                        result: null
                    }
                }

            } catch (err) {
                return {
                    message: '系统异常,请稍后再试',
                    status: 501,
                    result: null
                }
            }
        });
        return result;
    }
}
module.exports = UserService;

这里的return的内容,要用相同的结构,方便统一处理。


总结

本文详细介绍了后端项目的搭建过程,并对mysql、jwt的安装和使用进行了说明,希望对大家有帮助。下一篇我们将介绍前端项目的搭建。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值