控制器Controller
所有的 Controller 文件都必须放在 app/controller 目录下,
可以支持多级目录,访问的时候可以通过目录名级联访问
Controller定义
const Controller = require('egg').Controller;
class PostController extends Controller {
async create() {
const { ctx, service } = this;
const createRule = {
title: { type: 'string' },
content: { type: 'string' },
};
ctx.validate(createRule);
const author = ctx.session.userId;
const req = Object.assign(ctx.request.body, { author });
const res = await service.post.create(req);
ctx.body = { id: res.id };
ctx.status = 201;
}
}
module.exports = PostController;
我们通过上面的代码定义了一个 PostController 的类,类里面的每一个方法都可以作为一个 Controller 在 Router 中引用到,
我们可以从 app.controller 根据文件名和方法名定位到它。
module.exports = app => {
const { router, controller } = app;
router.post('createPost', '/api/posts', controller.post.create);
router.post('createPost', '/api/posts', controller.sub.post.create);
}
定义Controller类,会在每一个请求访问到server时实例化一个全新的对象,
项目中Controller类继承于egg.Controller,会有几个属性挂载this上
· this.ctx: 当前请求的上下文Context对象的实例,处理当前请求的各种属性和方法
· this.app: 当前应用Application对象的实例,获取框架提供的全局对象和方法
· this.service: 应用定义的Service,可以访问到抽象出的业务层,等价于this.ctx.service
· this.config: 应用运行时的配置项
· this.logger: logger对象,对象上有四个方法(debug, info, warn, error)分别代表打印不同级别的日志
HTTP 基础
Controller 基本上是业务开发中唯一和 HTTP 协议打交道的地方
如果发起一个post请求访问Controller,
axios({
url: '/home',
method: 'post',
data: {name: 'jack', age:18},
headers: {'Content-Type':'application/json; charset=UTF-8'}
})
// 发出的HTTP请求的内容就是下面这样
POST /home HTTP/1.1
Host: localhost:3000
Content-Type: application/json; charset=UTF-8
{name: 'jack', age:18}
1. 请求行,第一行包含三个信息,我们比较常用的是前面两个
method: 请求方式为post
path: 值为/home,如果用户请求包含query也会显示在这里
2. 请求头,第二行开始直到遇到第一个空行位置,都是请求headers的部分
Host: 浏览器会将域名和端口号放在host头中一并发给服务端
Content-Type: 当请求有body的时候,都会有content-type来标明我们的请求体时什么格式的
还有Cookie,User-agent等等,都在这个请求头中
3. 空行,发送回车符和换行符,通知服务器以下不再有请求头,它的作用是通过一个空行,告诉服务器请求头部到此为止。
4. 最后一行,请求体/请求数据
如果请求方式为post,请求参数和值就会放在这里,会把数据以key: value的形式发送请求
如果请求方式为post,请求参数和值就会包含在资源路径(URL)上
// 服务端处理完这个请求后,会返送一个HTTP响应给客户端
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Content-Length: 8
Date: Mon, 09 Jan 2017 08:40:28 GMT
Connection: keep-alive
{"id": 1}
1. 状态行,第一行,响应状态码,这个例子中的值为201,含义是在服务端成功创建一条资源
2. 响应头,第二行开始到第一个空行,
Content-Type: 表示响应的格式为json格式
Content-Length: 表示长度为8个字节
3. 空行,响应结束
4. 返回响应的内容
获取HTTP请求参数
框架通过在Comtroller上绑定Context实例,提供许多方法和属性来获取用户通过HTTP发送过来的参数
1.query
在URL中?后面的部分是一个Query String,经常用域get类型的请求中传递参数
name=jack&age=18就是用户传递过来的参数,我们通过ctx.query获取解析过后的参数体
class PostController extends Controller {
async listPosts() {
const query = this.ctx.query;
}
}
当Query String中的key值重复,ctx.query只获取key第一次出现的值,后面的忽略
有时候我们的系统会设计成让用户传递相同的 key,例如 GET /posts?category=egg&id=1&id=2&id=3。
针对此类情况,框架提供了 ctx.queries 对象, 但是会将每一个数据都放进一个数组中
class PostController extends Controller {
async listPosts() {
console.log(this.ctx.queries);
}
}
2. Router params
在router动态路由中,也可以申明参数,这些参数都可以通过ctx.params获取到
class AppController extends Controller {
async listApp() {
this.ctx.response.body = `projectid:${this.ctx.params.projectId}, appid:${this.ctx.params.appId}`;
}
}
3. body
虽然可以通过url传递参数,但是有许多限制
浏览器中会对url的长度有所限制,如果参数过多就会无法传递
服务端经常会将访问的完整的url暴露在请求信息中,一些敏感数据通过url传递不安全
前面HTTP请求报文实例中,header之后还有一个body部分,通常会在这个部分传递post,put等方法的参数
一般请求中有body的时候,客户端(浏览器)会同时发送Content-Type告诉服务端这次请求的body什么格式的
web开发数据传递最常用的格式json和Form,框架内置了bodyParse中间件来对这两类格式的请求body解析成obj
将obj对象挂载到全局ctx.request.body上,GET,HEAD方法无法获取到内容
let options = {
url: '/form',
method: 'post',
data: {name: 'jack', age: 18},
headers: {'Content-Type': 'application/json'}
}
axios(options).then(data=> {console.log(data)})
class PostController extends Controller {
async listPosts() {
console.log(this.ctx.request.body.name)
console.log(this.ctx.request.body.age)
}
}
1. Content-Type为application/json,application/json-patch+json,
application/vnd.api+json和application/csp-report时,
会按照json格式对请求body进行解析,并限制最大长度为100kb
2. Content-Type为application/x-www-form-urlencoded时,
会按照form格式对请求body进行解析,限制最大长度为100kb
3. 如果解析成功,body一定会是一个object(可能时一个数组)
module.exports = {
bodyParser: {
jsonLimit: '1mb',
formLimit: '1mb',
},
};
如果超出最大长度,抛出状态码413的异常。请求body解析失败,抛出错误状态码400的异常
获取上传文件
请求body除了可以带参数以外,还可以发送文件,一般浏览器上都是通过Multipart/form-data格式发送文件
框架通过内置Multipart插件来支持获取用户上传的文件
1. 在config文件中启用file模式
exports.multipart = {
mode: 'file',
};
2. 上传单个文件/前端静态页面form标签
<form method="POST" action="http://localhost:7001/upload" enctype="multipart/form-data">
title: <input name="title" />
file: <input name="file" type="file" />
<button type="submit">Upload</button>
</form>
3. 对应的后端代码
const Controller = require('egg').Controller;
const fs = require('fs');
module.exports = class extends Controller {
async upload() {
const { ctx } = this;
const file = ctx.request.files[0];
let result;
try {
fs.rename(file.filepath, `${__dirname}/../public/upload/${file.filename}`)
result = 'success'
} catch(e){
console.log(e)
}
ctx.response.body = result;
}
};
<form method="POST" action="http:localhost:7001/upload" enctype="multipart/form-data">
title: <input name="title" />
file1: <input name="file1" type="file" />
file2: <input name="file2" type="file" />
<button type="submit">Upload</button>
</form>
const Controller = require('egg').Controller;
const fs = require('fs');
module.exports = class extends Controller {
async upload() {
const { ctx } = this;
console.log(ctx.request.body)
for (const file of ctx.request.files) {
console.log('field: ' + file.fieldname);
console.log('filename: ' + file.filename);
console.log('encoding: ' + file.encoding);
console.log('mime: ' + file.mime);
console.log('tmp filepath: ' + file.filepath);
try {
do something....
} catch (e) {
....
}
}
}
}
jpg,png,gif,svg,js,json,zip,mp3,mp4.......
用户可以通过在 config/config.default.js 中配置来新增支持的文件扩展名,或者重写整个白名单
module.exports = {
multipart: {
fileExtensions: [ '.apk' ]
},
};
Cookie
HTTP 请求都是无状态的,但是我们的 Web 应用通常都需要知道发起请求的人是谁
为了解决这个问题,HTTP 协议设计了一个特殊的请求头:Cookie
通过 ctx.cookies,我们可以在 Controller 中便捷、安全的设置和读取 Cookie。
'use strict';
const Controller = require('egg').Controller;
class CookieController extends Controller {
async add() {
const ctx = this.ctx;
let count = ctx.cookies.get('count');
count = count ? Number(count) : 0;
ctx.cookies.set('count', ++count);
ctx.body = count;
}
async remove() {
const ctx = this.ctx;
ctx.cookies.set('count', null);
ctx.status = 204;
}
}
module.exports = CookieController;
module.exports = {
cookies: {
},
};
Session
通过 Cookie,我们可以给每一个用户设置一个 Session,用来存储用户身份相关的信息,
这份信息会加密后存储在 Cookie 中,实现跨请求的用户身份保持,
Cookie 在 Web 应用中经常承担标识请求方身份的功能,
所以 Web 应用在 Cookie 的基础上封装了 Session 的概念,专门用做用户身份识别。
class PostController extends Controller {
async fetchPosts() {
const ctx = this.ctx;
const userId = ctx.session.userId;
const posts = await ctx.service.post.fetch(userId);
ctx.session.visited = ctx.session.visited ? ++ctx.session.visited : 1;
ctx.body = {
success: true,
posts,
};
}
}
ctx.session._visited = 1
ctx.session.get = 'haha'
ctx.session.visited = 1
exports.session = {
key: 'EGG_SESS',
maxAge: 24 * 3600 * 1000,
httpOnly: true,
encrypt: true,
renew: true,
};
参数校验
在获取到用户请求的参数后,不可避免的要对参数进行一些校验。
借助 Validate 插件提供便捷的参数校验机制,帮助我们完成各种复杂的参数校验。
下载安装插件 npm install egg-validata --save
exports.validate = {
enable: true,
package: 'egg-validate'
}
rule: 校验规则,
body: 可选,不传递该参数会自动校验 ctx.request.body
class PostController extends Controller {
async create() {
const ctx = this.ctx;
const createRule = {
title: { type: 'string' },
content: { type: 'string' },
};
try {
ctx.validate(createRule);
} catch (err) {
ctx.logger.warn(err.errors);
ctx.body = { success: false };
return;
}
}
};
调用Service
我们并不想在 Controller 中实现太多业务逻辑,所以提供了一个 Service 层进行业务逻辑的封装,
这不仅能提高代码的复用性,同时可以让我们的业务逻辑更好测试。
在 Controller 中可以调用任何一个 Service 上的任何方法,
同时 Service 是懒加载的,只有当访问到它的时候框架才会去实例化它。
class PostController extends Controller {
async create() {
const ctx = this.ctx;
const author = ctx.session.userId;
const req = Object.assign(ctx.request.body, { author });
const res = await ctx.service.post.create(req);
ctx.body = { id: res.id };
ctx.status = 201;
}
}
发送HTTP响应
当业务逻辑完成之后,Controller 的最后一个职责就是将业务逻辑的处理结果通过 HTTP 响应发送给用户。
1. 设置status
HTTP 设计了非常多的状态码,每一个状态码都代表了一个特定的含义,通过设置正确的状态码,可以让响应更符合语义。
class PostController extends Controller {
async create() {
this.ctx.status = 201;
}
};
2. 设置body
绝大多数的数据都是通过 body 发送给请求方的,和请求中的 body 一样,在响应中发送的 body,也需要有配套的 Content-Type 告知客户端如何对数据进行解析。
· 作为一个 RESTful 的 API 接口 controller,我们通常会返回 Content-Type 为 application/json 格式的 body,内容是一个 JSON 字符串
· 作为一个 html 页面的 controller,我们通常会返回 Content-Type 为 text/html 格式的 body,内容是 html 代码段。
class ViewController extends Controller {
async show() {
this.ctx.body = {
name: 'egg',
category: 'framework',
language: 'Node.js',
};
}
async page() {
this.ctx.body = '<html><h1>Hello</h1></html>';
}
}
我们通过状态码标识请求成功与否、状态如何,
在 body 中设置响应的内容。而通过响应的 Header,还可以设置一些扩展信息。
通过 ctx.set(key, value) 方法可以设置一个响应头,ctx.set(headers) 设置多个 Header。
class ProxyController extends Controller {
async show() {
const ctx = this.ctx;
const start = Date.now();
ctx.body = await ctx.service.post.get();
const used = Date.now() - start;
ctx.set('show-response-time', used.toString());
}
};
JSONP
有时我们需要给非本域的页面提供接口服务,又由于一些历史原因无法通过 CORS(跨域) 实现,可以通过 JSONP 来进行响应。
1. 通过 app.jsonp() 提供的中间件来让一个 controller 支持响应 JSONP 格式的数据。在路由中,我们给需要支持 jsonp 的路由加上这个中间件:
module.exports = app => {
const jsonp = app.jsonp();
app.router.get('/api/posts/:id', jsonp, app.controller.posts.show);
app.router.get('/api/posts', jsonp, app.controller.posts.list);
};
2. 在Controller中,正常编写
class PostController extends Controller {
async show() {
this.ctx.body = {
name: 'egg',
category: 'framework',
language: 'Node.js',
};
}
}
3. JSONP配置
exports.jsonp = {
callback: 'callback',
limit: 100,
};
通过上面的方式配置之后,如果用户请求 /api/posts/1?callback=fn,响应为 JSONP 格式,
如果用户请求 /api/posts/1,响应格式为 JSON。
module.exports = app => {
const { router, controller, jsonp } = app;
router.get('/api/posts/:id', jsonp({ callback: 'callback' }), controller.posts.show);
router.get('/api/posts', jsonp({ callback: 'cb' }), controller.posts.list);
};
module.exports = {
jsonp: {
csrf: true,
},
};
exports.jsonp = {
whiteList: /^https?:\/\/test.com\//,
};
1. 正则表达式: 此时只有请求的 Referrer 匹配该正则时才允许访问 JSONP 接口
2. 字符串:当字符串以 . 开头,例如 .test.com 时,代表 referrer 白名单为 test.com 的所有子域名,包括 test.com 自身
当字符串不以 . 开头,例如 sub.test.com,代表 referrer 白名单为 sub.test.com 这一个域名。
exports.jsonp = {
whiteList: '.test.com',
};
exports.jsonp = {
whiteList: 'sub.test.com',
};
3. 数组: 当设置的白名单为数组时,满足数组中任意一个
exports.jsonp = {
whiteList: [ 'sub.test.com', 'sub2.test.com' ],
};
重定向
框架通过 security 插件覆盖了 koa 原生的 ctx.redirect 实现,以提供更加安全的重定向。
1. ctx.redirect(url) 如果不在配置的白名单域名内,则禁止跳转。
2. ctx.unsafeRedirect(url) 不判断域名,直接跳转,一般不建议使用,明确了解可能带来的风险后使用。
exports.security = {
domainWhiteList:['.domain.com'],
};
若用户没有配置 domainWhiteList 或者 domainWhiteList数组内为空,则默认会对所有跳转请求放行,即等同于ctx.unsafeRedirect(url)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!