1. 初始化项目
npm i egg-init -g
egg-init 项目名 --type=simple
npm i
npm run dev
open http://localhost:7001
目录结构
egg-project
├── package.json
├── app.js (可选) //用于自定义启动时的初始化工作,可选。
├── agent.js (可选) //用于自定义启动时的初始化工作,可选。
├── app
| ├── router.js //用于配置 URL 路由规则。
│ ├── 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
2.初始Egg.js中的路由
- 创建 product 控制器
//app/controller/test.js
"use strict";
const Controller = require("egg").Controller;
class TestController extends Controller {
async index() {
const { ctx } = this; //ctx 主要用来存储用户请求的信息,每次请求时都会实例化
ctx.body = {
code: 0,
data: "测试数据!",
message: "success",
};
}
}
module.exports = TestController;
- 添加路由
"use strict";
/**
* @param {Egg.Application} app - egg application
*/
module.exports = (app) => {
const { router, controller } = app;
router.get("/", controller.home.index);
router.get("/test", controller.test.index);
};
3.GET方式请求中的两种传参方式
- 方式一:query
获取方式
async detail() {
// 获取上下文
const { ctx } = this;
console.log('方式一:query', ctx.query);
ctx.body = `id==${ctx.query.id}`;
}
路由配置
router.get('/product/detail', controller.product.detail);
- 方式二:params
获取方式
async detail2() {
const { ctx } = this;
console.log('方式二:params', ctx.params);
ctx.body = `id==${ctx.params.id}`;
}
路由配置
router.get('/product/detail2/:id', controller.product.detail2);
4.Egg.js 中的请求方法
- GET 方法
// 商品详情
async detail() {
const { ctx } = this;
console.log(ctx.query);
ctx.body = `id==${ctx.query.id}`;
}
async detail2() {
const { ctx } = this;
console.log(ctx.params);
ctx.body = `id==${ctx.params.id}`;
}
//app/router.js
router.get('/product/detail', controller.product.detail);
router.get('/product/detail2/:id', controller.product.detail2);
- POST 方法
在config.default.js需要先关闭 csrf j检查
//config/config.default.js
config.security = {
csrf: {
enable: false,
}
};
// 新增
async create() {
const { ctx } = this;
console.log(ctx.request.body);
const { name, weight } = ctx.request.body;
ctx.body = {
name,
weight
};
}
//app/router.js
router.post('/product/create', controller.product.create);
- PUT 方法
// 更新
async update() {
const { ctx } = this;
console.log(ctx.params);
ctx.body = {
id: ctx.params.id
}
}
//app/router.js
router.put('/product/update/:id', controller.product.update);
- DELETE 方法
// 删除
async delete() {
const { ctx } = this;
console.log(ctx.params);
ctx.body = {
id: ctx.params.id
}
}
router.delete('/product/delete/:id', controller.product.delete);
5.Egg.js 中的Service服务
- 创建service文件
app/service/test.js
const Service = require('egg').Service;
class TestService extends Service {
async index() {
return {
id: 100,
name: '测试'
}
}
}
module.exports = TestService;
- 调用
async index() {
const { ctx } = this;
const res = await ctx.service.product.index();
ctx.body = res;
}
- 服务的命名规则
/***
* 服务的命名规则
* Service 文件必须放在 app/service 目录,可以支持多级目录,访问的时候可以通过目录名级联访问。
* app/service/biz/user.js => ctx.service.biz.user (推荐)
* app/service/sync_user.js => ctx.service.syncUser
* app/service/HackerNews.js => ctx.service.hackerNews
*/
6.Egg.js 中模板引擎的使用
- 安装egg-view-ejs
cnpm i --save egg-view-ejs
- 修改配置
config/plugin.js
'use strict';
exports.ejs = {
enable: true,
package: 'egg-view-ejs'
};
config/config.default.js
config.view = {
mapping: {
'.html': 'ejs'
}
};
- 创建 html 文件
app/view/test.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>首页</title>
</head>
<body>
<h1>这是首页内容</h1>
<p>id:<%=res.id%></p>
<p>name:<%=res.name%></p>
<ul>
<%for(var i=0; i<lists.length; i++) {%>
<li><%=lists[i]%></li>
<%}%>
</ul>
</body>
</html>
- 传值
app/controller/test.js
'use strict';
const Controller = require('egg').Controller;
class HomeController extends Controller {
async index() {
const { ctx } = this;
const res = await ctx.service.product.index();
// ctx.body = res;
await ctx.render('index.html', {
res,
lists: ['a', 'b', 'c']
});
}
}
module.exports = HomeController;
- 效果
7.Egg.js 连接 mysql 数据库
- 安装 egg-mysql
npm i --save egg-mysql
- 配置 plugin.js
exports.mysql = {
enable: true,
package: 'egg-mysql',
};
- 配置 config.default.js
config.mysql = {
// 单数据库信息配置
client: {
// host
host: 'localhost',
// 端口号
port: '3306',
// 用户名
user: 'root',
// 密码
password: 'root',
// 数据库名
database: 'egg_article',
},
// 是否加载到 app 上,默认开启
app: true,
// 是否加载到 agent 上,默认关闭
agent: false,
};
};
return {
...config,
...userConfig,
};
- egg-mysql 的使用
//app/service/user.js
"use strict";
const Service = require("egg").Service;
class UserService extends Service {
async find() {
// "users" 为test数据库数据表名
const user = this.app.mysql.get("users", { id: 1 });
return { user };
}
}
module.exports = UserService;
//app/controller/home.js
"use strict";
const Controller = require("egg").Controller;
class HomeController extends Controller {
async index() {
const { ctx } = this;
// ctx.body = "hi, egg";
const user = await ctx.service.user.find();
ctx.body = user;
}
}
module.exports = HomeController;
//app/router.js
module.exports = app => {
const { router, controller } = app;
router.get('/', controller.home.index);
};
egg.js常用方法:
- get 查找一条
let result = await this.app.mysql.get("user",{id:1})
- 查找数据的另一种方式
let result = await this.app.mysql.select("user",{
where:{id:1}
})
- 增加数据
let result = await this.app.mysql.insert("user",{username:"lisi",password:"1234"})
- 修改数据的第一种方式:根据主键修改
let result = await this.app.mysql.update('user',{ id:2, username:'赵四' });
//修改数据的第二种方式:通过 sql 来修改数据
let results=await this.app.mysql.query('update user set username = ? where id = ?',["王五",2]);
- 删除数据
let result= await this.app.mysql.delete('user',{ id:3 });
- 执行 sql
this.app.mysql.query(sql,values);
- mysql 事务
const conn=await this.app.mysql.beginTransaction();
try{
await conn.insert('user',{'username':'xiao1','password':'1111'});
await conn.update('user',{id:2,username:'黑子'});
await conn.commit(); //提交事务
}catch(err){
await conn.rollback();//回滚事务
throw err;
}
8.Egg middleware 中间件
中间件:匹配路由前、匹配路由完成做的一系列操作。
一般来说中间件也会有自己的配置。在框架中,一个完整的中间件是包含了配置处理的。我们约定一个中间件是一个放置在 app/middleware 目录下的单独文件,它需要 exports 一个普通的 function,接受两个参数:
options: 中间件的配置项,框架会将 app.config[${middlewareName}] 传递进来。
app: 当前应用 Application 的实例。
- app/middleware 下面新建 forbidip.js 内容如下:
/**
* 拦截特殊ip,防止爬虫
* options: 中间件的配置项,框架会将 app.config[${middlewareName}] 传递进来
* app: 当前应用 Application 的实例
* 每次路由变化都会触发 中间件
*/
module.exports = (options, app) => {
return async function forbidipMiddleware(ctx, next) {
/**
* 要屏蔽的ip
* 1、从数据库获取
* 2、从参数传入
*/
console.log(options);
console.log(ctx.request.ip);
// 获取客户端的ip
var sourceIp = ctx.request.ip;
const match = options.ip.some(val => {
if(val == sourceIp){
return true;
}
});
if(match){
ctx.status = 403;
// message 接受的字符不能有中文,否则会报错
// ctx.message = 'Your IP has been blocked';
ctx.body = '您的ip已经被屏蔽';
}else{
await next();
}
}
}
- 找到 config.default.js 配置当前项目需要使用的中间件以及中间件的参数
// 增加配置中间件
config.middleware = ['forbidip'];
// 给pforbidip中间件传入的参数
config.forbidip = {
ip: ['192.168.0.10']
}
- 注意 传入的中间件名称 需要与 中间件文件名 相同
9.Egg 安全机制 CSRF 的防范
方式一:
在 controller 中通过参数传入模板
/**
* 方式一:在 controller 中通过参数传入模板
* this.ctx.csrf 用户访问这个页面的时候生成一个密钥
*/
await ctx.render('home', {
csrf: this.ctx.csrf
});
方式二:
通过创建中间件,设置模板全局变量
app/middleware/auth.js
/**
* 同步表单的 CSRF 校验
*/
module.exports = (options, app) => {
return async function auth(ctx, next) {
// 设置模板全局变量
ctx.state.csrf = ctx.csrf;
await next();
}
}
config/config.default.js
// 增加配置中间件
config.middleware = ['auth'];
controller 中使用
/**
* 方式二:通过创建中间件,设置模板全局变量
* config.middleware = [auth'];
*/
await ctx.render('home');
模板:
app/view/home.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>首页</title>
</head>
<body>
<form action="/add?_csrf=<%=csrf%>" method="POST">
<!-- 通过隐藏的表单域传值 -->
<!-- <input type="hidden" name="_csrf" value="<%=csrf%>" /> -->
用户名:<input type="text" name="username" /><br />
密 码:<input type="password" name="password" /><br />
<button type="submit">提交</button>
</form>
</body>
</html>
注:配置 域白名单,跳过 crsf 检测
config/config.default.js
// 安全配置
config.security = {
csrf: {
enable: false,
ignoreJSON: true
},
// 允许访问接口的白名单
domainWhiteList: ['*'] // ['http://localhost:8080']
}
10.解决跨域问题
- 安装 egg-cors
npm i egg-cors --save
- 配置plugin.js
exports.cors = {
enable: true,
package: 'egg-cors',
};
- 配置config.default.js
config.security = {
csrf: {
enable: false,
ignoreJSON: true
},
domainWhiteList: ['*']//[]中放放出的白名单,*代表所有
};
config.cors = {
origin:'*',
allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH',
};
11.生成token
- 安装egg-jwt
npm i --save egg-jwt
- 配置plugin.js
import { EggPlugin } from 'egg';
const plugin: EggPlugin = {
jwt: {
enable: true,
package: "egg-jwt"
}
};
export default plugin;
- 配置config.default.js
config.jwt = {
secret: "123456"//自定义 token 的加密条件字符串
};
- 创建访问路由
import { Application } from 'egg';
export default (app: Application) => {
const { controller, router, jwt } = app;
//正常路由
router.post('/admin/login', controller.admin.login);
/*
* 这里的第二个对象不再是控制器,而是 jwt 验证对象,第三个地方才是控制器
* 只有在需要验证 token 的路由才需要第二个 是 jwt 否则第二个对象为控制器
**/
router.post('/admin',jwt, controller.admin.index);
};
- 编写控制器,在根目录下的 app/controller/home.ts 编写内容:
//登录 根据用户名查询
async getUserByUsername() {
const { ctx, app } = this;
const { username, password } = ctx.query;
let user = await ctx.service.user.getUserByUsername(username);
if (user.user.length) {
//数据库中有该用户
let service_password = user.user[0].password;
if (password === service_password) {
//生成 token 的方式
const token = app.jwt.sign({ username }, app.config.jwt.secret);
// 生成的token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE1NjAzNDY5MDN9.B95GqH-fdRpyZIE5g_T0l8RgzNyWOyXepkLiynWqrJg
// 返回 token 到前端
ctx.body = {
code: 0,
data: token,
message: "登录成功",
};
} else {
ctx.throw(500, "密码错误,请重新输入");
ctx.body = ctx.request.body;
}
} else {
//数据库中没有该用户
ctx.throw(500, "用户名或密码错误");
ctx.body = ctx.request.body;
}
}
// 验证token,请求时在header配置 Authorization=`Bearer ${token}`
// 特别注意:token不能直接发送,要在前面加上Bearer字符串和一个空格
//访问admin数据时进行验证token,并且解析 token 的数据
public async index() {
const { ctx ,app} = this;
console.log(ctx.state.user);
/*
* 打印内容为:{ username : 'admin', iat: 1560346903 }
* iat 为过期时间,可以单独写中间件验证,这里不做细究
* 除了 iat 之后,其余的为当时存储的数据
**/
ctx.body = {code:0,msg:'验证成功'};
}
- 前端请求的时候需要在 headers 里面上默认的验证字断 Authorization 就可以了
12.egg-mysql做分页查询
//获取users表中所有用户
async getUserList(username, page, pageSize) {
let params = {
limit: Number(pageSize), // 返回数据量
offset: (page - 1) * pageSize, // 数据偏移量
};
if (username) params.where = { username };
const user = await this.app.mysql.select("users", params);//数据
const totalCount = await this.app.mysql.count("users");//数量
return { data: user, count: totalCount };
}
13.Egg.js 实现向服务器上传图片
- 安装时间处理 及 压缩 模块
npm i --save silly-datetime pump formidable
- 文件保存路径.
config/config.default.js
config.uploadDir = 'app/public/avatar/upload';
- app/service/user.js
'use strict';
const Service = require("egg").Service;
const path = require("path");
const sd = require("silly-datetime");
const mkdirp = require("mkdirp");
class UserService extends Service {
/**
* 获取文件上传目录
* @param {*} filename
*/
async getUploadFile(id, filename) {
// 1.获取当前日期
let day = sd.format(new Date(), "YYYYMMDD");
// 2.创建图片保存的路径
let dir = path.join(this.config.uploadDir, day);
await mkdirp(dir); // 不存在就创建目录
let date = Date.now(); // 毫秒数
// 返回图片保存的路径
let uploadDir = path.join(dir, date + path.extname(filename));
// app\public\avatar\upload\20200312\1536895331666.png
let headerImg = this.ctx.origin + uploadDir.slice(3).replace(/\\/g, "/");
const user = await this.app.mysql.update("users", {
id,
headerImg,
});
return {
uploadDir,
saveDir: headerImg,
};
}
}
module.exports = UserService;
- 调用 app/controller/user.js
"use strict";
const Controller = require("egg").Controller;
const fs = require("fs");
const pump = require("pump");
const formidable = require("formidable");
async parse(req) {
const form = new formidable.IncomingForm();
return new Promise((resolve, reject) => {
form.parse(req, (err, fields, files) => {
resolve({ fields, files });
});
});
}
// 保存头像/封面
async saveAvatar() {
const { ctx } = this;
const parts = ctx.multipart({ autoFields: true });
const extraParams = await this.parse(ctx.req);
const id = extraParams.fields.id;
let files = {};
let stream;
while ((stream = await parts()) != null) {
if (!stream.filename) {
break;
}
const fieldname = stream.fieldname; // file表单的名字
// 上传图片的目录
const dir = await this.service.user.getUploadFile(id, stream.filename);
const target = dir.uploadDir;
const writeStream = fs.createWriteStream(target);
await pump(stream, writeStream);
files = Object.assign(files, {
[fieldname]: dir.saveDir,
});
}
if (Object.keys(files).length > 0) {
ctx.body = {
code: 200,
message: "图片上传成功",
data: files,
};
} else {
ctx.body = {
code: 500,
message: "图片上传失败",
data: {},
};
}
}
- 配置路由
router.post("/user/saveavatar", controller.user.saveAvatar);
- vue UI组件
<el-form-item label="头像">
<el-upload
class="avatar-uploader"
:headers="uploadHeader"
:action="uploadAction"
:data="{ id: formData.id }"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
>
<img v-if="imageUrl" :src="imageUrl" class="avatar" />
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
</el-form-item>
<script>
export default {
data() {
return {
uploadAction: 'http://127.0.0.1:7001/user/saveavatar',
uploadHeader: { Authorization: localStorage.getItem('Authorization') || '' },
imageUrl: '',
};
},
methods: {
handleAvatarSuccess(res, file) {
this.imageUrl = URL.createObjectURL(file.raw);
this.$message.success('头像上传成功');
this.$router.push('/mycenter');
},
beforeAvatarUpload(file) {
const isJPG = file.type === 'image/jpeg';
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isJPG) {
this.$message.error('上传头像图片只能是 JPG 格式!');
}
if (!isLt2M) {
this.$message.error('上传头像图片大小不能超过 2MB!');
}
return isJPG && isLt2M;
},
},
};
</script>