简介
Egg.js 为企业级框架和应用而生,我们希望由 Egg.js 孕育出更多上层框架,帮助开发团队和开发人员降低开发和维护成本。Egg 继承于 Koa。
设计原则:一个插件制作一件事情,奉行『约定优于配置』
特性:
- 提供基于 Egg 定制上层框架的能力
- 高度可扩展的插件机制
- 内置多进程管理
- 基于 Koa 开发,性能优异
- 框架稳定,测试覆盖率高
- 渐进式开发
下包
$ mkdir egg-example && cd egg-example 新建一个文件夹
$ npm init egg --type=simple 搭建egg项目
$ npm i 下载项目需要的所有的包
启动项目:
$ npm run dev || yarn dev
$ open http://localhost:7001
简单配置、让后台先起来
proxy
想要前后端练习起来的话,需要配置 proxy
react
react 的项目在 package.jsonwen 文件里面配置
proxy:'http://127.0.0.1:7001';
vue
vue 项目的话在 vue.config.js 文件里面配置
module.exports = {
devServer: {
proxy: "http://127.0.0.1:7001"
}
}
安全协议
CSRF(Cross-site request forgery 跨站请求伪造,也被称为 One Click Attack 或者 Session Riding,通常缩写为 CSRF 或者 XSRF,是一种对网站的恶意利用。 CSRF 攻击会对网站发起恶意伪造的请求,严重影响网站的安全。因此框架内置了 CSRF 防范方案。
// config.default.js 文件
config.security = {
// 安全协议 关闭
csrf: false,
};
mysql 库
安装对应的插件 egg-mysql
npm i --save egg-mysql
开启插件
// config/plugin.js
exports.mysql = {
enable: true,
package: 'egg-mysql',
};
配置 mysql
// config.default.js 文件
// 配置mysql
config.mysql = {
// 单数据库信息配置
client: {
host: 'localhost', // host
port: '3306', // 端口号
user: 'root', // 用户名
password: '1234', // 密码
database: 'abei', // 数据库名
},
// 是否加载到 app 上,默认开启
app: true,
// 是否加载到 agent 上,默认关闭
agent: false,
};
使用方式:
// 单实例可以直接通过 app.mysql 访问
await app.mysql.query(sql, values);
Cookie 与 Session
Cookie
HTTP 请求都是无状态的,但是我们的 Web 应用通常都需要知道发起请求的人是谁。为了解决这个问题,HTTP 协议设计了一个特殊的请求头:Cookie。服务端可以通过响应头(set-cookie)将少量数据响应给客户端,浏览器会遵循协议将数据保存,并在下次请求同一个服务的时候带上(浏览器也会遵循协议,只在访问符合 Cookie 指定规则的网站时带上对应的 Cookie 来保证安全性)。
设置 Cookie 其实是通过在 HTTP 响应中设置 set-cookie 头完成的,每一个 set-cookie 都会让浏览器在 Cookie 中存一个键值对。在设置 Cookie 值的同时,协议还支持许多参数来配置这个 Cookie 的传输、存储和权限。
参数:
- {Number} maxAge: 设置这个键值对在浏览器的最长保存时间。是一个从服务器当前时刻开始的毫秒数。
- {Date} expires: 设置这个键值对的失效时间,如果设置了 maxAge,expires 将会被覆盖。如果 maxAge 和 expires 都没设置,Cookie 将会在浏览器的会话失效(一般是关闭浏览器时)的时候失效。
- {String} path: 设置键值对生效的 URL 路径,默认设置在根路径上(/),也就是当前域名下的所有 URL 都可以访问这个 Cookie。
- {String} domain: 设置键值对生效的域名,默认没有配置,可以配置成只在指定域名才能访问。
- {Boolean} httpOnly: 设置键值对是否可以被 js 访问,默认为 true,不允许被 js 访问。
- {Boolean} secure: 设置键值对只在 HTTPS 连接上传输,框架会帮我们判断当前是否在 HTTPS 连接上自动设置 secure 的值。
除了这些属性之外,框架另外扩展了 3 个参数的支持:
- {Boolean} overwrite:设置 key 相同的键值对如何处理,如果设置为 true,则后设置的值会覆盖前面设置的,否则将会发送两个 set-cookie 响应头。
- {Boolean} signed:设置是否对 Cookie 进行签名,如果设置为 true,则设置键值对的时候会同时对这个键值对的值进行签名,后面取的时候做校验,可以防止前端对这个值进行篡改。默认为 true。
- {Boolean} encrypt:设置是否对 Cookie 进行加密,如果设置为 true,则在发送 Cookie 前会对这个键值对的值进行加密,客户端无法读取到 Cookie 的明文值。默认为 false。
// 如果想要 Cookie 在浏览器端可以被 js 访问并修改:
ctx.cookies.set(key, value, {
httpOnly: false,
signed: false,
});
// 如果想要 Cookie 在浏览器端不能被修改,不能看到明文:
ctx.cookies.set(key, value, {
httpOnly: true, // 默认就是 true
encrypt: true, // 加密传输
});
Session
Cookie 在 Web 应用中经常承担标识请求方身份的功能,所以 Web 应用在 Cookie 的基础上封装了 Session 的概念,专门用做用户身份识别。
// 设置的时候要避免:
// 不要以 _ 开头
// 不能为 isNew
// 默认配置
exports.session = {
key: 'EGG_SESS', //密钥
maxAge: 24 * 3600 * 1000, // 1 天 失效时间
httpOnly: true, // 是否允许前端访问
encrypt: true, //是否加密传输
};
拦截器
axios
Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。
特性
- 从浏览器中创建 XMLHttpRequests
- 从 node.js 创建 http 请求
- 支持 Promise API
- 拦截请求和响应
- 转换请求数据和响应数据
- 取消请求
- 自动转换 JSON 数据
- 客户端支持防御 XSRF
二次封装 axios
import axios from 'axios'
const request = axios.create({
oittime: 5000 // 响应时间 过时不响应
})
// 添加请求拦截器
request.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
// 添加响应拦截器
request.interceptors.response.use(function (response) {
// 对响应数据做点什么
return response;
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
});
export default request;
api
接口处理
import request from '../utils/request'
// post请求方式
export function Login({ phone, code }) {
return request.post('/api/login', { //后台路径
phone,
code
})
}
// get请求方式
export function GetCode(phone) {
return request.get('/api/getcode', {
params: {
phone
}
})
}
后台接口
module.exports = app => {
const { router, controller } = app;
router.get('/api/getcode', controller.user.getcode);
router.post('/api/login', controller.user.login);
router.post('/api/search', controller.products.search);
router.post('/api/detail', controller.products.detail);
//controller文件夹里面的products文件里面的detail方法
};
中间件(middleware)
// 配饰插件
config.middleware = [ 'errorHandler', 'authortication' ];
// 统一处理错误的中间件
'use strict';
module.exports = () => {
return async function errorHandler(ctx, next) {
try {
await next();
} catch (err) {
// 所有的异常都在 app 上触发一个 error 事件,框架会记录一条错误日志
ctx.app.emit('error', err, ctx);
const status = err.status || 500;
// 生产环境时 500 错误的详细错误内容不返回给客户端,因为可能包含敏感信息
const error = status === 500 && ctx.app.config.env === 'prod'
? '服务端异常,请稍后重试'
: err.message;
// 从 error 对象上读出各个属性,设置到响应中
ctx.response.status = status;
ctx.body = {
code: 0,
message: error,
};
}
};
};
//登录权限的中间件
'use strict';
//token鉴权
const jwt = require('jsonwebtoken');
//白名单
const url = [
'/api/userinfo',
];
module.exports = () => {
return async function errorHandler(ctx, next) {
const token = ctx.cookies.get('token');
try {
if (url.includes(ctx.request.path)) {
const user = jwt.verify(token, 'abei');
ctx.userinfo = user;
await next();
} else {
await next();
}
} catch (error) {
if (token) {
ctx.throw(422, '用户登录失效');
} else {
ctx.throw(422, '用户未登录');
}
}
};
};
目录结构
egg-project
├── package.json
├── app.js (可选) // 用于自定义启动时的初始化工作
├── agent.js (可选)
├── app
| ├── router.js // 路由
│ ├── controller //用于解析用户的输入,处理后返回相应的结果
│ | └── home.js
│ ├── service (可选) // 用于编写业务逻辑层,可选,建议使用
│ | └── user.js
│ ├── middleware (可选) //用于编写中间件
│ | └── response_time.js
│ ├── schedule (可选)
│ | └── my_task.js
│ ├── public (可选) //用于放置静态资源
│ | └── reset.css
│ ├── view (可选) // 用于放置模板文件
│ | └── home.tpl
│ └── extend (可选) //用于框架的扩展
│ ├── helper.js (可选)
│ ├── request.js (可选)
│ ├── response.js (可选)
│ ├── context.js (可选)
│ ├── application.js (可选)
│ └── agent.js (可选)
├── config
| ├── plugin.js // 用于配置需要加载的插件
| ├── config.default.js //用于编写配置文件
│ ├── config.prod.js
| ├── config.test.js (可选)
| ├── config.local.js (可选)
| └── config.unittest.js (可选)
└── test // 用于单元测试
├── middleware
| └── response_time.test.js
└── controller
└── home.test.js
controller
用于解析用户的输入,处理后返回相应的结果
/app/controller/user文件
'use strict';
const Controller = require('egg').Controller;
class UserController extends Controller {
async home() {
this.ctx.body = "hi,egg"
}
}
service
用于处理业务逻辑
下面是一个 Service 中访问 MySQL 数据库的例子。
更多 Service 层的介绍,可以参考 Service
// app/service/user.js
class UserService extends Service {
async find(uid) {
// 假如 我们拿到用户 id 从数据库获取用户详细信息
const user = await this.app.mysql.get('users', { id: 11 });
return { user };
}
}
之后可以通过 Controller 获取 Service 层拿到的数据。
// app/controller/user.js
class UserController extends Controller {
async info() {
const ctx = this.ctx;
const userId = ctx.params.id;
const user = await ctx.service.user.find(userId);
ctx.body = user;
}
}
mysql
在 Web 应用方面 MySQL 是最常见,最好的关系型数据库之一。非常多网站都选择 MySQL 作为网站数据库。
egg-mysql
框架提供了 egg-mysql 插件来访问 MySQL 数据库。这个插件既可以访问普通的 MySQL 数据库,也可以访问基于 MySQL 协议的在线数据库服务。
插入 insert
// 插入
const result = await this.app.mysql.insert('posts', { title: 'Hello World' }); // 在 post 表中,插入 title 为 Hello World 的记录
//也可以写成这样
const result = await this.app.mysql.query('INSERT INTO `posts`(`title`) VALUES('Hello World')')
=> INSERT INTO `posts`(`title`) VALUES('Hello World');
//返回的值
console.log(result);
=>
{
fieldCount: 0,
affectedRows: 1, // 有一条发生变化
insertId: 3710,
serverStatus: 2,
warningCount: 2,
message: '',
protocol41: true,
changedRows: 0
}
// 判断插入成功
const insertSuccess = result.affectedRows === 1;
查询
可以直接使用 get 方法或 select 方法获取一条或多条记录。select 方法支持条件查询与结果的定制。
- 查询一条记录
const post = await this.app.mysql.get('posts', { id: 12 });
=> SELECT * FROM `posts` WHERE `id` = 12 LIMIT 0, 1;
- 查询全表
const results = await this.app.mysql.select('posts');
=> SELECT * FROM `posts`;
- 条件查询和结果定制
const results = await this.app.mysql.select('posts', { // 搜索 post 表
where: { status: 'draft', author: ['author1', 'author2'] }, // WHERE 条件
columns: ['author', 'title'], // 要查询的表字段
orders: [['created_at','desc'], ['id','desc']], // 排序方式
limit: 10, // 返回数据量
offset: 0, // 数据偏移量
});
=> SELECT `author`, `title` FROM `posts`
WHERE `status` = 'draft' AND `author` IN('author1','author2')
ORDER BY `created_at` DESC, `id` DESC LIMIT 0, 10;
删除 delete
const result = await this.app.mysql.delete('posts', {
author: 'fengmk2',
});
=> DELETE FROM `posts` WHERE `author` = 'fengmk2';
修改 upadate
// 修改数据,将会根据主键 ID 查找,并更新
const row = {
id: 123,
name: 'fengmk2',
otherField: 'other field value', // any other fields u want to update
modifiedAt: this.app.mysql.literals.now, // `now()` on db server
};
const result = await this.app.mysql.update('posts', row); // 更新 posts 表中的记录
=> UPDATE `posts` SET `name` = 'fengmk2', `modifiedAt` = NOW() WHERE id = 123 ;
// 判断更新成功
const updateSuccess = result.affectedRows === 1;
// 如果主键是自定义的 ID 名称,如 custom_id,则需要在 `where` 里面配置
const row = {
name: 'fengmk2',
otherField: 'other field value', // any other fields u want to update
modifiedAt: this.app.mysql.literals.now, // `now()` on db server
};
const options = {
where: {
custom_id: 456
}
};
const result = await this.app.mysql.update('posts', row, options); // 更新 posts 表中的记录
=> UPDATE `posts` SET `name` = 'fengmk2', `modifiedAt` = NOW() WHERE custom_id = 456 ;
// 判断更新成功
const updateSuccess = result.affectedRows === 1;