eggjs快速初始化
$ mkdir egg-example && cd egg-example
$ npm init egg --type=simple
$ npm i
初始化后的目录结构如下:
egg-example
├── app
│ ├── controller
│ │ └── home.js
│ ├── service
│ │ └── user.js
│ ├── model
│ │ └── user.js
│ ├── view
│ │ └── login.html
│ └── router.js
├── config
│ └── config.default.js
└── package.json
业务需求在app里面开发,config里面主要是一些配置信息
启动项目:
$ npm run dev
$ open http://localhost:7001
任务启动后我们先来理解一下Router和Control
Router
主要用来描述请求 URL 和具体承担执行动作的 Controller 的对应关系
// app/router.js
module.exports = app => {
const { router, controller } = app;
router.get('/user/:id', controller.user.info);
};
框架约定了app/router.js
文件用于统一所有路由规则。
我们通过 Router 将用户的请求基于 method 和 URL 分发到了对应的 Controller 上,那 Controller 负责做什么?
Controller
负责解析用户的输入,处理后返回相应的结果
- 在RESTful接口中,Controller 接受用户的参数,从数据库中查找内容返回给用户或者将用户的请求更新到数据库中。
- 在 HTML 页面请求中,Controller 根据用户访问不同的 URL,渲染不同的模板得到 HTML 返回给用户。
- 在代理服务器中,Controller 将用户的请求转发到其他服务器上,并将其他服务器的处理结果返回给用户。
框架推荐 Controller 层主要对用户的请求参数进行处理(校验、转换),然后调用对应的service方法处理业务,得到业务结果后封装并返回:
- 获取用户通过 HTTP 传递过来的请求参数。
- 校验、组装参数。
- 调用 Service 进行业务处理,必要时处理转换 Service 的返回结果,让它适应用户的需求。
- 通过 HTTP 将结果响应给用户。
eggjs官方文档写的十分清楚,大家可以多看看文档。
接口搭建
上面我们简单了解了Router和Controller,接下来就开始写代码,首先我们先来梳理一遍功能,前端页面发送一个ajax请求,参数为账号和密码,后台接收到这个请求后先到数据库里查这个账号,如果没有就返回没找到这个账号;如果有再和数据库里的密码比对,当比对结果一样时才算登录成功。
首先在 app/view
里面创建一个login.html作为登录页面,并且写上ajax请求
如果post请求失败 { message: 'invalid csrf token' },这是因为Egg.js默认开启了csrf,POST请求都需要附带csrf请求头,所以我们需要读取一个cookie并在请求头里加上'x-csrf-token'字段。 解决方案
接着在 app/controller
新建一个index.js, 并定一个HomeController的类。类里面的每一个方法都可以作为一个 Controller 在 Router 中引用到,我们可以从app.controller
根据文件名和方法名定位到它。
下面代码定义了一个login方法,使用this.ctx.render(template)
来渲染模板生成 html,它会自动到app/views
文件夹里面找对应的html。
const Controller = require('egg').Controller;
class HomeController extends Controller {
async login() {
await this.ctx.render('login');
}
}
module.exports = HomeController;
然后在 app/router.js
里面配置/login路由
module.exports = app => {
const { router, controller } = app
router.get('/login', controller.home.login)
};
此时访问 http://127.0.0.1:7001/login 就能看到login.html里面的内容了。
这时点击登录按钮返回404,这是因为我们没有配置/v1/login
这个路由,所以我们要在router.js里面配置一下/v1/login
作为登录接口,由于是登录功能所以这里配置的是post请求
module.exports = app => {
const { router, controller } = app
router.get('/login', controller.home.login)
router.post('/v1/login', controller.v1.users.login);
};
因为这个需求用到了数据库,需要在数据库里面插入一条数据进行测试,所以先搞一下egg里面的mysql操作,因为文档里面有详细的说明这里不多阐述,直接开始在项目里面用。
先在config/config.default.js
配置数据库连接信息
module.exports = appInfo => {
const config = exports = {
sequelize: {
dialect: 'mysql', // support: mysql, mariadb, postgres, mssql
database: 'test',
host: 'localhost',
port: '3306',
username: 'root',
password: '123456',
},
};
config.keys = appInfo.name + '_1583298043384_5771';
config.view = {
defaultViewEngine: 'nunjucks',
mapping: {
'.html': 'nunjucks' //左边写成.html后缀,会自动渲染.html文件
}
}
return {
...config
};
};
然后在app里面创建model文件夹,新建一个user.js,里面是这次登录功能用到的所有操作数据库的方法
Model
app/model/**
用于放置领域模型,可选,由领域类相关插件约定,如
egg-sequelize。
我们使用egg-sequelize来操作数据库,通过映射数据库条目到对象,或者对象到数据库条目,这样,我们读写的都是JavaScript对象,并且还会辅助我们将定义好的 Model 对象加载到 app 和 ctx 上,对前端更加友好。
用到Sequelize的方法
-
findOne(options)
- 查询单条数据 -> Promise -
create(options)
- 构建一个新的模型实例,并保存到对应数据库表中。-> Promise
[options.where] - Object 一个描述查询限制范围(WHERE
条件)的对象
[options.attributes] - Array.include
和exclude
对象的键。
注意Sequelize的方法都是返回Promise对象,其余的方法可以到官网里查询Sequelize
// app/model/user.js
const bcrypt = require('bcryptjs');
module.exports = app => {
const { STRING } = app.Sequelize;
const User = app.model.define('users', { //定义模型`model`和表之间的映射关系使用`define`方法
name: { type: STRING(30), unique: true, allowNull: false }, // 用户名
password: STRING
}, {
timestamps: false
})
// 根据参数获取用户
User.getUserByArgs = function (params, exclude) {
return this.findOne({ // 搜索数据库中的一个特定元素
where: params,
attributes: {
exclude: exclude.split(',')
}
})
}
// 查询指定参数
User.queryUser = async function (params) {
return this.findOne({
where: params,
attributes: ['name']
})
}
// 密码hash
User.hashPassword = function (password) {
const salt = bcrypt.genSaltSync(10); // 处理数据的轮次数
return bcrypt.hashSync(password, salt); // 将密码转为加密值
}
// 注册用户
User.register = function (fields) {
fields.password = User.hashPassword(fields.password);
return this.create(fields) // 在数据库里创建一条数据
}
// 将密码跟加密的密码对比
User.compareSync = function (password, hashedPassword) {
return bcrypt.compareSync(password, hashedPassword)
}
return User
}
由于密码特殊所以我们需要将密码加密后再存到数据库里面,比对的时候也是比对的加密后的密码,这里用到的是加密算法
BCrypt
然后我们在app.js的didLoad里面初始一条数据 作为测试用,didLoad是eggjs里的生命周期函数 在这里所有文件加载完成
// app.js
class AppBootHook {
constructor(app) {
this.app = app;
}
async didLoad() {
// init数据库操作
const user = {
id: 1,
name: 'admin',
password: '123456'
};
// 只有数据库里面没有这个账号才创建
if (!(await this.app.model.User.queryUser({ name: user.name }))) {
await this.app.model.User.register(user);
}
}
}
module.exports = AppBootHook;
接下来写/v1/login
对应的Control,,新建一个v1文件夹用于管理所有的RESTful接口,并新建一个user.js并定义login方法
通过this.ctx.body能拿到用户参数,然后拿参数里的name到数据库里面查找这条数据,如果没有数据就返回没找到,如果找到了将参数的password和数据库里的加密密码对比,通过bcrypt.compareSync(password, hashedPassword)这个方法返回true就证明登录成功了
const Controller = require('egg').Controller;
class UsersController extends Controller {
async login() {
const ctx = this.ctx
const name = ctx.request.body.name
const password = ctx.request.body.password
const user = await ctx.model.User.getUserByArgs({name}, '')
if (!user) {
return ctx.body = {
errmsg: '没有找到该用户',
errcode: 10001
}
}
if (!(ctx.model.User.compareSync(password, user.dataValues.password))) {
return ctx.body = {
errmsg: '密码错误',
errcode: 10002
}
}
ctx.body = {
user,
errcode: 200
}
}
}
module.exports = UsersController;
下图是 await ctx.model.User.getUserByArgs({name}, '')返回的数据
这样一个使用eggjs做的简易登录接口就做好了