目录
Egg.js 为企业级框架和应用而生,我们希望由 Egg.js 孕育出更多上层框架,帮助开发团队和开发人员降低开发和维护成本。
使用Egg 脚手架创建一个Egg项目 node >= 14.20.0
Egg.js 为企业级框架和应用而生,我们希望由 Egg.js 孕育出更多上层框架,帮助开发团队和开发人员降低开发和维护成本。
npm init egg --type=simple npm install
用于配置 URL 路由规则,比如上述初始化代码中的
get
请求,npm run dev
启动项目之后,你可以直接在浏览器中访问启动的端口 + 路径
,默认是http://localhost:7001/
,你将会拿到app/controller
文件夹下,home.js
脚本中index
方法返回的内容。
路由的配置中,除过get 之外,还有router.post/ router.put/ router.delete 等
前面有说到,我们通过router的将用户的所有请求基于method和url分发到了对应的Controller上。
简单的说Controller 负责解析用户的输入,处理后返回相应的结果。
egg推荐COntroller层主要对用户的请求参数进行处理(校验、转换),然后调用对应的service方法处理业务,得到业务结果后封装并返回。
一般的controller 体现在如下方面
获取用户通过http传递过来的请求参数。
校验、组装参数。
调用Service进行业务处理,必要时处理转换Service的返回结果,让它适应用户的需求。
通过Http将结果响应给用户。
举个例子:
我想拿到A用户的个人信息,于是我们要在控制器(controller)中通过请求携带的A用户的id参数,从数据库中获取指定用户的个人信息
例如上述,它是一个get接口,游览器通过地址栏请求可以访问到,通过/user路径,我们找到对应的控制器,这里需要提前定义好/user对应的控制器,而控制器需要做的就是处理数据和响应请求返回的数据。
注意:
controller 文件都必须放在app/controller目录中,可以支持多级目录,访问的适合可以通过目录名级连访问。Controller 支持多种形式进行编写,可以根据不同的项目场景和开发习惯来选择。
关于级联访问:
Controller 支持多级目录,例如如果我们将上面的 Controller 代码放到 app/controller/sub/post.js
中,则可以在 router 中这样使用:
- // app/router.js
- module.exports = (app) => {
- app.router.post('createPost', '/api/posts', app.controller.sub.post.create);
- };
上面的app.controller.sub目录.post文件.create方法。
也就是说,每一个请求访问到server时,实例化一个全新的对象,而项目中的Controller类继承于egg.Controller。则会有一部分属性挂载在 this上。
this.ctx : 当前请求的上下文Context对象的示例,通过它我们可以拿到框架封装好的处理当前请求的各种边界属性和方法。
this.app : 当前应用 APp 对象的实例,通过它我们可以拿到框架提供的全局对象和方法。
this.service : 应用定义的service,通过它我们可以直接访问到抽象出的业务层。等价于 this.ctx.service
this.config : 应用运行时的配置项
this.logger : logger 对象,上面有四个方法 (debug,info,warn,error),分别代表打印四个不同级别的日志。
service是在复杂业务场景下用于做业务逻辑封装的一个抽象层,上述初始化项目中未直接声明service文件夹,是因为它是可选项,但是官方建议我们操作业务逻辑最好做一层封装。
换个理解就是,service就是来用于访问数据库进行查询的。我们尽量细化,这样以便更多的controller 共同调用同一个service。也能使 controller 的逻辑更加简介。
它的使用场景:
复杂数据的处理,比如要展现的信息要从数据库中进行获取,并且还需要一定的规则计算,才能返回用户显示。或者计算完成后,需要更新到数据库。
第三方服务的调用,如:gitHUb等信息获取。
它所调用的属性
与controller 一样,service 也同样集成于egg.Service.也就是说它同样拥有一些属性供我们进行使用.
this.ctx
: 当前请求的上下文 Context 对象的实例,通过它我们可以拿到框架封装好的处理当前请求的各种便捷属性和方法。
this.app
: 当前应用 Application 对象的实例,通过它我们可以拿到框架提供的全局对象和方法。
this.service
:应用定义的 Service,通过它我们可以访问到其他业务层,等价于 this.ctx.service
。
this.config
:应用运行时的配置项。
this.logger
:logger 对象,上面有四个方法(debug
,info
,warn
,error
),分别代表打印四个不同级别的日志
关于细化this.ctx
为了可以获取用户请求的链路,我们在service初始化中,注入了请求上下文,用户在方法中可以直接通过this.ctx 来获取上下文相关信息。
this.ctx.curl 发起网络调用
this.ctx.service.otherService 调用其它自己的service
this.ctx.db 发起数据库调用等。db可能是其它插件提前挂载到app上的模块。
注意事项:
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文件只能包含一个类,这个类需要通过module.exports的方式返回。
service需要通过class 的方式定义,父类必须是egg.Service。(必须继承于egg.Service)
service 不是单例,是请求级别的对象。框架在每次请求中首次访问 ctx.service.xx 时延迟实例化,所以service 中可以通过 this.ctx获取到当前请求的上下文。
一个service 的使用过程如下:
- // app/router.js
- module.exports = (app) => {
- app.router.get('/user/:id', app.controller.user.info);
- };
-
- // app/controller/user.js
- const Controller = require('egg').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;
- }
- }
- module.exports = UserController;
-
- // app/service/user.js
- const Service = require('egg').Service;
- class UserService extends Service {
- // 默认不需要提供构造函数。
- // constructor(ctx) {
- // super(ctx); 如果需要在构造函数做一些处理,一定要有这句话,才能保证后面 `this.ctx`的使用。
- // // 就可以直接通过 this.ctx 获取 ctx 了
- // // 还可以直接通过 this.app 获取 app 了
- // }
- async find(uid) {
- // 假如 我们拿到用户 id 从数据库获取用户详细信息
- const user = await this.ctx.db.query(
- 'select * from user where uid = ?',
- uid,
- );
-
- // 假定这里还有一些复杂的计算,然后返回需要的信息。
- const picture = await this.getPicture(uid);
-
- return {
- name: user.user_name,
- age: user.age,
- picture,
- };
- }
-
- async getPicture(uid) {
- const result = await this.ctx.curl(`http://photoserver/uid=${uid}`, {
- dataType: 'json',
- });
- return result.data;
- }
- }
- module.exports = UserService;
-
用于编写中间件,中间件的概念就是在路由配置里设置了中间件的路由,每次请求命中后,都要过一层中间件。首先第一个我们能想到的就是用户鉴权。当用户没有登录的情况下,是不能调用某些接口的。
关于用户鉴权,其实我们也可以在请求命中的 controller中判断是否有携带用户认证信息。但是当我们接口一多的时候,到处都是这样的判断,逻辑重复。所以在某种程度上,中间件也算是一种优化代码结构的方式。
用来放置静态资源,也就是我们可以通过服务端读取文件后,将其写入app/public 文件夹中。这个适合用于没有oss服务的情况下,这种属于存储静态资源,会消耗一点服务器的内存。
用于编写配置文件,类似config.default.js 这个文件,是egg自己约定好的,我们可以在内部设置一些全局的配置常量,在任何地方都可以通过app.config获取到config.default.js文件内的配置。
用于配置需要加载的插件,比如 egg-mysql egg-cors egg-jwt 等官方提供的插件,我们也可以自己编写egg插件。
到这里项目初始化的基本目录我们就熟悉结束了。
我们先来尝试下,如何编写最基础的 GET和post 接口吧,以此做为egg的基础知识,来熟悉如何使用egg。
GET请求参数获取
我们打开项目。通过npm run dev打开项目并进行访问地址。
http://localhost:7001/?id=测试项目
然后我们在app/controller/home.js 中,修改index方法如下:
- class HomeController extends Controller {
- async index () {
- const {ctx} = this;
- const {id} = ctx.query;
- ctx.body = id;
- }
- }
然后我们在游览器地址栏输入地址后进行访问。
已经成功显示出来了。
除此之外还有另外一种获取申明参数的方法。比如 http://127.0.0.1:7001/user/1234 我们像获取到user 的 id 为 1234.我们这样来做。
首先,我们需要打开路由,在 app/controller/home.js 中,增加一个方法,user。
- class HomeController extends Controller {
- async index () {
- ...
- }
-
- async user () {
- const {ctx} = this;
- const {id} = ctx.params;
- ctx.body = id;
- }
- }
我们修改完controller 之后。因为要请求user这个接口。所以我们还需要在路由中增加请求地址
app/router.js
- module.exports = app => {
- const {router,controller} = app;
- router.app('/',controller.home.index);
- router.app('/user/:id', controller.home.user);
- }
修改完之后保存代码,回到游览器 我们输入 http://127.0.0.1:7001/user/1234.
页面也是正确打印出来的。
post接口需要借助 postMan 或者 api post 等工具进行请求。
首先我们需要声明一个post的请求地址。
- // app/router.js
-
- router.post('/add', controller.home.add)
其次,需要去新增这个请求地址对应的操作controller 。
- // app/controller/home.js
-
- async add () {
- const {ctx} = this;
- const {title } = ctx.request.body;// 因为egg 框架内置了bodyParser 中间件对post请求body解析成 object,解析后会挂在在ctx.request.body上。所以我们直接从 ctx.request.body 中进行选择。
- ctx.body = {
- title
- }
-
- }
完成以上后我们打开我们的请求工具。
发现 返回的是一个html页面。
我们再看控制台。
发现控制台报错了!
安全威胁 csrf 的防范
它是什么意思呢,简单来说就是网络请求的安防策略,比如 我们的egg项目地址是 http://127.0.0.1:7001 但是我们请求的post 或者 get 接口是非本地计算机(别人的电脑),或者我们使用postman或者 apipost等工具发起请求,都会触发安防策略。
那么,怎么办呢?
解决办法就是,做好白名单。
- // config/config.default.js
-
- config.security = {
- csrf: {
- enable: false,
- lgnoreJSON: true
- },
- domainWhiteList: [ '*' ] // 配置白名单
- }
然后重启服务,再进行尝试
这次就发现。返回值是正确了。
虽然项目刚开始,我们还没用到mysql等数据库,但是初始认识还是有必要了解,所以我们先进行模拟。
在app目录下新建 service,并且创建一个 home.js
- // app/service/home.js
-
- const Service = require('egg').Service;
-
- class HomeService extends Service {
- async user () {
- // 假设从数据库中取数据
- return {
- name: '赵小左',
- slogen: '这是一个新项目',
- }
- }
- }
-
- module.exports = HomeService;
然后我们打开controller 修改 user 的controller。
- async user () {
- const { ctx } = this;
- const { name, slogen } = await ctx.service.home.user()
- ctx.body = {
- name,
- slogen,
- }
- }
最后打开post 请求
http://127.0.0.1:7001/user/1234
我们如果想开发一个简单的网页,想快速部署到云服务器上,就可以使用前端模板的开发形式。
先安装 egg-view-ejs
npm install egg-view-ejs --save
然后在config/publin.js 中声明需要用到的插件
- module.export = {
- ejs: {
- enable: true,
- package: 'egg-view-ejs',
- }
- };
紧跟着我们要去 config/config.default.js 中配置ejs,这一步会将.ejs的后缀改成.html 的后缀。
- config.view = {
-
- mapping: {'.html', 'ejs'} // 左侧选项写成 .html 会自动替换为 .html文件
-
- }
然后在app下创建view文件夹,新建一个html,做为前端模板,如下图
- DOCTYPE html>
-
-
-
-
<%-title%> -
-
<%-title%>
然后我们在请求命中后在controller中将变量入到 index.html 中,模板通过 <%-title%> 关键字获取到传入的变量。
最后,我们修改 home 的controller 即可
- async index() {
- const { ctx } = this;
- // ctx.render 默认会去 view 文件夹寻找 index.html,这是 Egg 约定好的。
- await ctx.render('index.html', {
- title: '我是尼克陈', // 将 title 传入 index.html
- });
- }
然后我们启动页面。直接访问地址。就会发现,标题改变了。页面内容也改变了。