前端开发所要掌握的——egg框架

3,774 阅读11分钟

egg框架

什么是Egg.js?

Egg.js 是为企业级框架和应用而生的,希望由 Egg.js 孕育出更多上层框架,帮助开发团队和开发人员降低开发和维护成本

注意:Egg.js 缩写为 Egg,我们平常所说的egg就是egg.js

  • Egg 的插件机制有很高的可扩展性,一个插件只做一件事,Egg 通过框架聚合这些插件,并根据自己的业务场景定制配置,这样应用的开发成本就变得很低

  • Egg 奉行『约定优于配置』,按照一套统一的约定进行应用开发,团队内部采用这种方式可以减少开发人员的学习成本,但约定不等于扩展性差,相反 Egg 有很高的扩展性,可以按照团队的约定定制框架。使用 Loader 可以让框架根据不同环境定义默认配置,还可以覆盖 Egg 的默认约定

egg的特性

  • 提供基于 Egg 定制上层框架的能力
  • 高度可扩展的插件机制
  • 内置多进程管理
  • 基于 Koa 开发,性能优异
  • 框架稳定,测试覆盖率高
  • 渐进式开发
  • egg环境的安装
  • 推荐直接使用脚手架,只需几条简单指令,即可快速生成项目,免去了许多自己安装的麻烦,也不会出现因为安装失误而导致的环境集成失败

1、使用脚手架快速创建项目 ,小黑窗输入:npm init egg --type=simple

2、输入指令:npm i

2.1如果卡住了,点小黑窗,按回车

2.2如果丢包了,就可以多次输入指令:npm i

2.3如果还是丢包了,就把node_modules删了,再npm i (因为package.json在,它会根据里面的信息进行下载)

2.4也可以把以前用过的文件直接压缩过后拿过来,更改相应的信息就可以了

3、启动项目:npm run dev   或者  egg-bin dev

(这个是在package.json文件里面写的 "dev":"egg-bin dev")

4、浏览器访问: http://localhost:7001

上面启动后,就需要取访问。

网址可以多种写法:

http://127.8.0.0.1:7001 或者

http://localhost:7001

或者 自己的网址 : http://192.168.1.6:7001

注:如果安装指令没有报错,运行报错一般就是网络卡,防火墙等原因导致丢包了,删除整个项目文件夹,重新安装项目即可

egg的约定规则

app/router.js:用于配置URL路由规则
app/controller/ :用于解析用户的输入,处理后返回相应的结果
app/service/: 用于编写业务逻辑层
app/public/: 用于放置静态资源
config/config.{env}.js: 用于编写配置文件
config/plugin.js 用于配置需要加载的插件\

路由

Router 主要用来描述请求 URL 和具体承担执行动作的 Controller 的对应关系, 框架约定了 app/router.js 文件用于统一所有路由规则

通过统一的配置,我们可以避免路由规则逻辑散落在多个地方,从而出现未知的冲突,集中在一起我们可以更方便的来查看全局的路由规则

路由:指不同的网址去执行不同的分支或者程序

  • 定义路由的实例 在app/router.js 里面定义 URL 路由规则
module.exports = app => {
  const { router, controller } = app;
  router.get('/home', controller.user.info);
};

app/controller 目录下面实现 Controller

class UserController extends Controller {
  async info() {
    const { ctx } = this;
  	ctx.body = '<h1>hi, user</h1>';
  }
}

class类里面可以写constructure,不写就有一个默认有一个空的对象 ,对象里面其中有一个功能ctx(context的缩写)

this.ctx里面有一个功能body ==>如果将this.ctx.body设置了一个值,它会自动监听到body的值发生变化,它就会调end函数,传给前端。而且在取出body值的时候,会自动转为JSON数据。所以可以赋值任何类型的值

(this.ctx就是Controller提供的功能 主要就是使用它提供的:给前端发送数据,访问插件功能)

'use strict';  //严格模式
const Controller = require('egg').Controller; //这是commonjs,不属于ES6
//以下是ES6  extends是继承,Controller是官方的功能。HomeController是继承官方的功能
class HomeController extends Controller {  //HomeController这是类名,可自己取,首字母一般大写
  async fn() {
    const { ctx } = this;
    ctx.body = 'hi, egg';
    ctx.body = [1,2];  //这行代码不会运行了,上面一行的代码只会执行一次就断开网络连接
  }
}
//以下是commonjs
module.exports = HomeController;  //导出

路由完整定义主要包括5个主要部分:

  • verb - 用户触发动作,支持 get,post 等所有 HTTP 方法。
  • router-name 给路由设定一个别名,可以通过 Helper 提供的辅助函数 pathFor 和 urlFor 来生成 URL。(可选)
  • path-match - 路由 URL 路径。
  • middleware1 - 在 Router 里面可以配置多个 Middleware。(可选)
  • controller - 指定路由映射到的具体的 controller 上,controller 可以有两种写法:app.controller.user.fetch - 直接指定一个具体的 controller’user.fetch’

注意:注册路由时 路由名不要跟静态文件名冲突 不然会优先访问静态资源 router.get(“/*”,controller.home.all); 当使用 *号时,表示与所以的网址适配,因此这个应该写在最后一个位置,不然随便输入什么就与之适配了,就不会匹配到对应的网址请求来的数据了 注册了两个相同的路由,只能访问到第一个

控制器Controller

Controller 负责解析用户的输入,处理后返回相应的结果

在 RESTful 接口中,Controller 接受用户的参数,从数据库中查找内容返回给用户或者将用户的请求更新到数据库中。 在 HTML 页面请求中,Controller 根据用户访问不同的 URL,渲染不同的模板得到 HTML 返回给用户。 在代理服务器中,Controller 将用户的请求转发到其他服务器上,并将其他服务器的处理结果返回给用户。 Controller 层主要对用户的请求参数进行处理(校验、转换),然后调用对应的 service 方法处理业务,得到业务结果后封装并返回:

  • 获取用户通过 HTTP 传递过来的请求参数。
  • 校验、组装参数。
  • 调用 Service 进行业务处理,必要时处理转换 Service 的返回结果,让它适应用户的需求。
  • 通过 HTTP 将结果响应给用户。
  • 所有的Controller 文件都必须放在 app/controller目录下
  • 支持多级目录,访问时可以通过目录名级联访问

Controller 负责解析用户的输入,处理后返回相应的结果,一个最简单的 helloworld 示例:

const { Controller } = require('egg');
class HomeController extends Controller {
  async index() {
    const { ctx } = this;
    ctx.body = 'hi, egg';
  }
}
module.exports = HomeController;

当然,我们实际项目中的代码不会这么简单,通常情况下,在 Controller 中会做如下几件事情:

  • 接收、校验、处理 HTTP 请求参数
  • 向下调用服务(Service)处理业务
  • 通过 HTTP 将结果响应给用户

一个真实的案例如下:

const { Controller } = require('egg');
class PostController extends Controller {
  async create() {
    const { ctx, service } = this;
    const createRule = {
      title: { type: 'string' },
      content: { type: 'string' },
    };
    // 校验和组装参数
    ctx.validate(createRule);
    const data = Object.assign(ctx.request.body, { author: ctx.session.userId });
    // 调用 Service 进行业务处理
    const res = await service.post.create(data);
    // 响应客户端数据
    ctx.body = { id: res.id };
    ctx.status = 201;
  }
}
module.exports = PostController;

由于 Controller 是类,因此可以通过自定义基类的方式封装常用方法,例如:

// app/core/base_controller.js
const { Controller } = require('egg');
class BaseController extends Controller {
  get user() {
    return this.ctx.session.user;
  }
  success(data) {
    this.ctx.body = { success: true, data };
  }
  notFound(msg) {
    this.ctx.throw(404, msg || 'not found');
  }
}
module.exports = BaseController;

然后让所有 Controller 继承这个自定义的 BaseController:

// app/controller/post.js
const Controller = require('../core/base_controller');
class PostController extends Controller {
  async list() {
    const posts = await this.service.listByUser(this.user);
    this.success(posts);
  }
}

在 Controller 中通过 this.ctx 可以获取上下文对象,方便获取和设置相关参数,例如:

  • ctx.query:URL 中的请求参数(忽略重复 key)
  • ctx.quries:URL 中的请求参数(重复的 key 被放入数组中)
  • ctx.params:Router 上的命名参数
  • ctx.request.body:HTTP 请求体中的内容
  • ctx.request.files:前端上传的文件对象
  • ctx.getFileStream():获取上传的文件流
  • ctx.multipart():获取 multipart/form-data 数据
  • ctx.cookies:读取和设置 cookie
  • ctx.session:读取和设置 session
  • ctx.service.xxx:获取指定 service 对象的实例(懒加载)
  • ctx.status:设置状态码
  • ctx.body:设置响应体
  • ctx.set:设置响应头
  • ctx.redirect(url):重定向
  • ctx.render(template):渲染模板

this.ctx 上下文对象是 egg 框架和 koa 框架中最重要的一个对象,我们要弄清楚该对象的作用,不过需要注意的是,有些属性并非直接挂在 app.ctx 对象上,而是代理了 request 或 response 对象的属性,我们可以用 Object.keys(ctx) 看一下:

[  'request', 'response', 'app', 'req', 'res', 'onerror', 'originalUrl', 'starttime', 'matched',  '_matchedRoute', '_matchedRouteName', 'captures', 'params', 'routerName', 'routerPath']

服务(Service)

Service 是具体业务逻辑的实现,一个封装好的 Service 可供多个 Controller 调用,而一个 Controller 里面也可以调用多个 Service,虽然在 Controller 中也可以写业务逻辑,但是并不建议这么做,代码中应该保持 Controller 逻辑简洁,仅仅发挥「桥梁」作用。

Controller 可以调用任何一个 Service 上的任何方法,值得注意的是:Service 是懒加载的,即只有当访问到它的时候框架才会去实例化它。

通常情况下,在 Service 中会做如下几件事情:

  • 处理复杂业务逻辑
  • 调用数据库或第三方服务(例如 GitHub 信息获取等)

一个简单的 Service 示例,将数据库中的查询结果返回出去:

// app/service/user.js
const { Service } = require('egg').Service;

class UserService extends Service {
  async find(uid) {
    const user = await this.ctx.db.query('select * from user where uid = ?', uid);
    return user;
  }
}

module.exports = UserService;

在 Controller 中可以直接调用:

class UserController extends Controller {
  async info() {
    const { ctx } = this;
    const userId = ctx.params.id;
    const userInfo = await ctx.service.user.find(userId);
    ctx.body = userInfo;
  }
}

注意,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

Service 里面的函数,可以理解为某个具体业务逻辑的最小单元,Service 里面也可以调用其他 Service,值得注意的是:Service 不是单例,是 请求级别 的对象,框架在每次请求中首次访问 ctx.service.xx 时延迟实例化,所以 Service 中可以通过 this.ctx 获取到当前请求的上下文。

egg的跨域处理

egg默认开启了跨域安全插件 如果需要自己自定义token等可以选择 关闭
因为egg框架自动开启了安全威胁 CSRF 的防范,所以要么在 config.default.js 中配置关闭 csrf,要么老老实实配置 cookies

'use strict';
/** @type Egg.EggPlugin */
module.exports = {
   cors:{
      enable: true,
      package: 'egg-cors',
    }
};

### 3.配置插件

  config/config.default.js文件  
	config.cors = {
		origin: '*',
		allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH'
	}
默认origin只支持一个域名或者*表示全部,如果想支持具体的多个指定域名可以如下设置:


	config.cors = {
		// origin: ['http://localhost'],
		origin: function (ctx) { //设置允许来自指定域名请求
			console.log(ctx);
			const whiteList = ['http://www.baidu.com', 'http://www.hqyj.com']; //放白名单
			let url = ctx.request.header.origin;
			if (whiteList.includes(url)) {
				return url;
			}
			return 'http://localhost' //默认允许本地请求可跨域
		},
		allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH'
	};

### 4.使用:

router.js文件 ==>router.get('/cors',controller.home.cors)

home.js文件 ==>async cors(){    this.ctx.body={info:"这个接口可以通过跨域访问"} }

5. 注意:
跨域前端请求时需要携带跨域凭证,不然缓存会失效
​
前端网络请求时配置:
原生ajax:
xhr.withCredentials = true;
​
axios:
axios.post(url,{},{withCredentials:true}).then((res)=>{})
​
    
后端配置:
     config.cors = {
        origin: '具体的ip和端口 不能写* ',
        credentials:true  
  }

2.实现jsonp接口

方法一:
如果前端的参数中有cb=fn参数(jsonp接口参数),将会返回JSONP格式的数据,否则返回JSON格式的数据。
​
1.配置:config/config.default.js文件

config.jsonp = {
  callback: 'cb', // 识别 query 中的 `cb` 参数
  limit: 100, // 函数名最长为 100 个字符
};
2.写接口app/router.js==>

module.exports = app => {
  const jsonp = app.jsonp();
  app.router.get('/api/posts', jsonp, app.controller.posts.list);
};

方法二:

也可以直接在jsonp方法中直接配置,功能一样:

如果前端的参数中有cb=fn参数(jsonp接口参数),将会返回JSONP格式的数据,否则返回JSON格式的数据。app/router.js文件==>

	module.exports = app => {
		const jsonp = app.jsonp({
			callback: 'cb',  //JSONP的接口
			limit: 100,
		});
		app.router.get('/api/posts', jsonp, app.controller.posts.list);
	};

代理配置

代理服务:我们自己前端直接请求别的服务的数据会有跨域限制,所以我们自己的前端请求自己的后端服务器,然后后端的服务器在用require请求别的服务器,别的服务器把数据传给我们的后端,我们的后端再把数据传给我们自己的前端,前端在操作数据写入页面,我们的后端服务器就叫做代理服务器

this.ctx.curl(url, option)

option常用配置:

method:'GET/POST'

data:{name:"karen"} //会自动字符串化

返回promise对象

如
router.js文件
module.exports = app => {
	router.get('/baidu', controller.home.baidu);
};
controller/home.js 文件
class MyController extends Controller {
  async baidu(){
		var url = "http://www.baidu.com";
		let res=await this.ctx.curl(url)
		this.ctx.body=res.data
  }
}
module.exports = MyController;


案例:
<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title></title>
		<script src='https://s1.pstatp.com/cdn/expire-1-M/axios/0.19.2/axios.js'></script>
	</head>
	<body>
		<h1>ajax</h1>
		<button onclick="fn()">请求sina</button>
		<script>
			function fn() {
				var url = "http://192.168.6.60:7001/sina"
				axios(url)
					.then(res => console.log(res))
			}
		</script>
	</body>
</html>
//路由配置
'use strict';
/**
 * @param {Egg.Application} app - egg application
 */
module.exports = app => {
	const {
		router,
		controller		
	} = app;
	router.get('/sina', controller.home.sina);
};

'use strict';
const Controller = require('egg').Controller;
class MyController extends Controller {
  async sina(){
	//url是新浪的数据
	//curl是window自带的网络工具,它集成答上下文对象中了
		var url="https://api.weibo.com/2/statuses/home_timeline.json?access_token=2.00ZmCkcDlel8HDd856f9b9ccsVwsYD"
		let res=await this.ctx.curl(url)  //this.ctx.curl(url)它的返回值是promise对象,所以用await取值
		this.ctx.body=res.data.toString()  //res.data是buffer类型
  }
}
module.exports = MyController;

接下来的文章便是关于跨域网络请求的两大方式