Koa+Sequelize 初体验

667 阅读6分钟

其实在几年前就已经接触到了express,当时觉得非常强大,也非常简洁。Koa其实和express是一样的,我们在日常开发web端页面的时候重要的环节就是和服务端人员联调接口,那用js去写服务端代码是什么样的体验呢,浅谈Koa+Sequelize 初体验。

Koa

npm install -g koa-generator
Koa2 + 项目名称

初始化项目我们看到的默认目录是这样的:

  • 在bin文件夹下是项目的启动入口,引入跟目录app.js入口文件,并且启动本地服务和端口的设置。
  • node_moudel为模块加载的生成文件夹,存放依赖模块源码。
  • public 公共文件夹,存放一些公共资源文件。
  • routes 路由文件,主要功能分发接口
  • views 视图层,pug 或者 jade是一个文本格式,内容也可以是html
  • app.js 配置文件也是入口文件

看起来非常简介,就像koa的源码一样,以简介的方式实现更多的功能,也做到了。 这个目录显然不能满足我们的日常开发,在改造结构之前,我们还是先来看下我们会用到的一些知识点:

一、中间件

没错,这个词从以前到现在再去理解真的有不同的见解,koa中使用use应用中间件

app .use(logger()) .use(bodyParser())

看到给app下的use函数传入参数就是一个函数,源码里面是这样的:

  /**
   * Use the given middleware `fn`.
   *
   * Old-style middleware will be converted.
   *
   * @param {Function} fn
   * @return {Application} self
   * @api public
   */

  use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    if (isGeneratorFunction(fn)) {
      deprecate('Support for generators will be removed in v3. ' +
                'See the documentation for examples of how to convert old middleware ' +
                'https://github.com/koajs/koa/blob/master/docs/migration.md');
      fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
    this.middleware.push(fn);
    return this;
  }

不难发现,this.middleware.push(fn)除了校验函数最后会把传入的函数或者中间件push到middleware数组中。使用 isGeneratorFunction(fn) 这个方法来判断是否为 Generator 语法,并通过 convert(fn) 这个方法进行了转换,转换成 async/await 语法(基于promise写法)。

我觉得中间件的中心还是那个经典的洋葱圈模型: 看到这个图片可能会有所感触,一层层进入就像是逐个执行中间件,到达中心位置,开始再从最后一个中间件执行next()方法后面内容。

app.use(async (ctx,next)=>{
  console.log("1");
  await next();
  console.log("2")
})
app.use(async (ctx,next)=>{
  console.log("3");
  await next()
  console.log("4")
})

这个的执行顺序是1234吗? 确实不是的,按照洋葱圈模型,应该就是1342,一层层进入然后走出来。理解这一点很重要,关系到我们后面目录结构的划分。

Sequelize 是一个基于 promise 的 Node.js ORM, 目前支持 Postgres, MySQL, MariaDB, SQLite 以及 Microsoft SQL Server. 它具有强大的事务支持, 关联关系, 预读和延迟加载,读取复制等功能。

Sequelize.js 提供对 MySQL,MariaDB,SQLite 和 PostgreSQL 数据库的简单访问,通过映射数据库条目到对象,或者对象到数据库条目。简而言之,就是 ORM(Object-Relational-Mapper)。Sequelize.js 完全是使用 JavaScript 编写,适用于 Node.js 的环境。 好吧,看着官方的介绍大概是不是可以理解为:

ORM:对象型关系映射

  • 数据表对应映射为一个类
  • 数据表中的数据行对应一个对象
  • 数据表中的字段对应对象的属性
  • 数据表的操作对应对象的方法

这样看来就是我们前端Object 的思想,把数据表对应一个类,数据行也就是一组数据代表一个对象,这里面包含各个字段,字段就是属性,最后操作数据库就是对象的方法,其实我李杰这块也可以是对应的中间件。

我们在操作数据库之前就是连接据库了,老规矩先安装:

 npm install --save mysql2
 npm install --save sequelize
const sequelize = new Sequelize('database(数据库名称)', 'username(用户名)', 'password(密码)', {
  host: 'localhost',
  dialect: 定义数据库'mysql'
});

测试链接成功预发,Sequelize提供了.authenticate() 函数

try {
  await sequelize.authenticate();
  console.log('Connection has been established successfully.');
} catch (error) {
  console.error('Unable to connect to the database:', error);
}

***模型定义 在 Sequelize 中可以用两种等效的方式定义模型:

  • 调用 sequelize.define(modelName, attributes, options)
  • 扩展 Model 并调用 init(attributes, options) 这个可以看下官网的文档,我们先来说下sequelize.define定义模型:
const { Sequelize, DataTypes } = require('sequelize');
const sequelize = new Sequelize('sqlite::memory:');
const User = sequelize.define('User', {
  // 在这里定义模型属性
  userName: {
    type: DataTypes.STRING,
    allowNull: false
  },
  passWord: {
    type: DataTypes.STRING
    // allowNull 默认为 true
  }
}, {
  // 这是其他模型参数
});
// `sequelize.define` 会返回模型
console.log(User === sequelize.models.User); // true

定义模型就 相当于我们用对象的形式表示了数据表。接下来我们整理一下从客户端到服务端接口请求到处理数据口再到返回结果的步骤:

-------------画图表示

可以看到,我们可以根据步骤把项目的结构改造一下,能够把各个职能划分出来,方便我们做业务代码。大致我们去分为四个大模块,

*** router路由层:在根目routes文件件下创建根据业务模块的js文件最为路由功能:

const Router = require('koa-router')
// 默认接口
router.post('/login', (ctx, next)=>{
  const { user_name } = ctx.request.body
  ctx.body = {}
  ......
})

当请求到当前login地址就会走到路由的中间件来处理逻辑,但是这样可能会在当前的中间件中写 大量的处理逻辑看着并不清晰,所以我们把处理接口参数的逻辑分离出来controller层。

*** controller层

这一层只要针对客户端请求带来的参数和基本的逻辑处理:

class UserController {
  async login(ctx, next) {
    const { user_name } = ctx.request.body
    // 操作数据库     await  getUerInfo({user_name})
    ctx.body = {}
    ......
  }
}

当设计到操作数据库信息的时候,我们再去分出service层来专门操作数据库

*** service 层

这一层其实并不是必须的,当然也可以把这些逻辑都写到controller层。

class UserService {
  async getUerInfo({ id, user_name, password, is_admin }) {

    const res = await User(数据对象模型).findOne({
      attributes: ['id', 'user_name', 'password', 'is_admin'],
      where: whereOpt,
    })

    return res ? res.dataValues : null
  }
}

当前是findOne查询一条数据,选择某些特定属性,可以使用 attributes 参数; where 参数用于过滤查询; User是定义的数据对象模型,能够对应数据库的表和字段。

*** Model层

首先还是先把前面说到的数据库连接放到根目录db文件夹下,新建sql.js。

const seq = new Sequelize(MYSQL_DB, MYSQL_USER, MYSQL_PWD, {
  host:'...',
  dialect: '...',
})
module.exports = seq  //导出数据库连接对象

然后在根目录的model文件下创建一个User.js放数据模型,sequelize.define 和 Model.init,两种方法本质上是等效的。

const User = seq.define('User', {
  user_name: {
    type: DataTypes.STRING, //数据类型
    allowNull: false, // 是否允许为空
    unique: true, // 
    comment: '用户名', //字段描述
  },
  password: {
    type: DataTypes.CHAR(64),
    allowNull: false,
    comment: '密码',
  },
  is_admin: {
    type: DataTypes.BOOLEAN,
    allowNull: false,
    defaultValue: 0,
    comment: '是否为管理员, 0: 不是管理员(默认); 1: 是管理员',
  },
})

到这里已经知道项目目录的分层,现在就添加一条用户的数据试一下,按照分层的思路应该是这个层级结构:

———————————————— 目录结构图

首先假设接口请求带来两个,用户名称和用户密码,router路由转发到 controller中

class UserController {
  async register(ctx, next) {
    const { user_name, password } = ctx.request.body
    const res = await createUser(user_name, password)  //来自service层
    ctx.body = {
        code: 0,
        message: '添加用户成功',
    }
}

在这个controller中拿到了用户请求参数,需要引入数据模型model,然后service来操作数据库,在执行INSERT操作的时候会用到create方法:

const User = require('../model/use.model')
class UserService {
  async createUser(userName, password) {
      //Sequelize提供了 create 方法
      const res = await User.create({ user_name, password })
      return res
  }
}

到这里只是一简单的添加功能,这页只是冰山一角,后面可以增加复杂的逻辑