这里我们使用 Egg 为我们提供的脚手架生成指令,几行代码就能初始化一个 Egg 项目,如下:

  1. $ mkdir egg-example && cd egg-example
  2. $ npm init egg --type=simple
  3. $ npm i

依赖安装完毕以后,运行npm start
image.png
在浏览器中打开http://127.0.0.1:7001/,就可以看到页面了
image.png

目录结构

Egg 作为一套解决方案,它内部高度集成了封装好的项目目录结构,现代开发俗称“约定式开发”。正常情况下,你从 0 开始搭建一个 Node 服务端代码,需要结合很多工具插件来辅助完成项目的搭建,而 Egg 则提前为你提供好了这些繁琐的初始工作,让你能专心与业务层面的开发。

app/router.js

用于配置 URL 路由规则,比如上述初始化代码中的 get 请求,npm run dev 启动项目之后,你可以直接在浏览器中访问启动的端口 + 路径,默认是 http://localhost:7001/,你将会拿到 app/controller 文件夹下,home.js 脚本中 index 方法返回的内容。
image.png
这就是路由配置的作用,当然,抛出的形式有多种,如router.get、router.post、router.delete、router.put 等,都是支持的,更加具体的内容可阅读 Router 配置

app/controller/xx

用于解析用户的输入,处理后返回相应的结果。上述我们也提到了,通过请求路径将用户的请求基于 method 和 URL 分发到对应的 Controller 上,而 Controller 要做的事情就是响应用户的诉求。举个例子,我想拿到 A 用户的个人信息,于是我们要在控制器(Controller)里,通过请求携带的 A 用户的 id 参数,从数据库里获取指定用户的个人信息。我画了一个简易流程图如下:
image.png
上述是一个 get 接口,浏览器地址栏直接请求便可,通过 /user 路径,找到对应的控制器,这里需要提前定义好 /user 对应的控制器。控制器需要做的就是处理数据和响应请求返回数据。更加详细的描述请移步至 Controller 文档

app/service/xx

简单来说,Service 就是在复杂业务场景下用于做业务逻辑封装的一个抽象层。上述初始化项目中未声明 service 文件夹,它是可选项,但是官方建议我们操作业务逻辑最好做一层封装。我们换一种理解方式,Service 层就是用于数据库的查询,我们尽量将粒度细化,这样以便多个 Controller 共同调用同一个 Service。后续我们链接数据库操作的时候,再进行详细分析。更加详细的描述请移步至 Service 文档

app/middleware/xx

用于编写中间件,中间件的概念就是在路由配置里设置了中间件的路由,每次请求命中后,都要过一层中间件。在我们后续的开发中,也会利用到这个中间件的原理做用户鉴权。当用户未登录的情况下,是不能调用某些接口的。
当然,你可以每次都在 Controller 判断,当前请求是否携带有效的用户认证信息。接口一多,到处都是这样的判断,逻辑重复。所以,中间件在某种程度上,也算是优化代码结构的一种方式。更加详细的描述请移步至 Middleware 文档

app/public/xx

用于放置静态资源。后续我们将会有一个上传静态资源的接口,包括图片、文本文档、excel、word等,都可以通过服务端读取文件之后,将其写入 app/public 文件夹中。在目前没有 OSS 服务的情况下,姑且先用这种方式存储静态资源,会消耗一点服务器的内存。

config/config.{env}.js

用于编写配置文件。目前本小册项目只配置了 config/config.default.js 文件,这个是 Egg 框架约定好的,你可以在内部设置一些全局的配置常量,在任何地方都可以通过 app.config 获取到 config.default.js 文件内的配置项。

config/plugin.js
用于配置需要加载的插件。比如 egg-mysql、egg-cors、egg-jwt 等官方提供的插件,你也可以自己编写 Egg 插件,社区很多优秀的插件都可以在 Github 中搜到,点击前往
上述目录内容都是我们后续开发会使用到的,同学们做一个基本的了解,做到心中有数便可,后续实战开发部分具体使用到的时候,会结合实践进行分析讲解。

编写基础的 GET 和 POST 接口

我们先上手一些简单的知识点,以此来熟悉如何使用 Egg,为后续的实战部分作准备。

GET 请求参数获取

我们通过 npm run dev 启动项目之后,浏览器添加如下请求地址:

  1. http://localhost:7002/?id=全栈课程

打开 app/controller/home.js,通过如下形式获取到浏览器查询参数。

  1. 'use strict';
  2. const { Controller } = require('egg');
  3. class HomeController extends Controller {
  4. async index() {
  5. const { ctx } = this;
  6. const { id } = ctx.query;
  7. ctx.body = id;
  8. }
  9. }
  10. module.exports = HomeController;

image.png
还有另一种获取申明参数,比如我希望通过这样一个地址获取用户参数 /user/5,想获取用户 id 为 5 的用户信息。我们可以这样操作,首先添加路由,打开 app/router.js 添加一个路由:

  1. 'use strict';
  2. /**
  3. * @param {Egg.Application} app - egg application
  4. */
  5. module.exports = app => {
  6. const { router, controller } = app;
  7. router.get('/', controller.home.index);
  8. router.get('/user/:id', controller.home.user);
  9. };

其次在 app/controller/home.js 下添加一个 user 方法如下:

  1. 'use strict';
  2. const { Controller } = require('egg');
  3. class HomeController extends Controller {
  4. async index() {
  5. const { ctx } = this;
  6. const { id } = ctx.query;
  7. ctx.body = id;
  8. }
  9. async user() {
  10. const { ctx } = this;
  11. const { id } = ctx.params;
  12. ctx.body = id;
  13. }
  14. }
  15. module.exports = HomeController;

我们通过如下请求获取参数,打印在网页上:
image.png
通过上述操作,大家可以想到,其实用 GET 接口,就能把一切都做了,包括 POST 能做的,GET 也是可以轻松应对,只不过我们为了数据安全以及大小的限制,有些请求还是需要用 POST 来完成。所以市面上的面试题有关于 GET 和 POST 请求的区别,其实都是表象上的区别,而内在都是基于 TCP 协议,从理论上讲,可以说它们没差。

POST 请求参数获取

POST 接口需要借助 ApiFox 工具进行请求,因为通过浏览器无法手动发起 POST 请求,只能通过浏览器地址栏发起 GET 请求。所以这里大家可以去搜索引擎自行下载安装。我们来编写一个 POST 请求:

  1. // app/router.js
  2. // ...
  3. router.post('/add', controller.home.add);
  1. // app/controller/home.js
  2. // post 请求方法
  3. async add() {
  4. const { ctx } = this;
  5. const { title } = ctx.request.body;
  6. // Egg 框架内置了 bodyParser 中间件来对 POST 请求 body 解析成 object 挂载到 ctx.request.body 上
  7. ctx.body = {
  8. title
  9. };
  10. }

Apifox中打开如下
image.png
全部填写好,点击 「发送」按钮之后,控制台报错如下:image.png
注意关键字「安全威胁 csrf 的防范」,简单说就是网络请求的安防策略,比如你 Egg 启动的是本地地址 http://127.0.0.1:7001 ,但是你请求的 POST 或 GET 接口是非本地计算机(别人的电脑),或者使用 Postman 发起请求,都会触发安防策略。
编程的过程就是解决问题的过程,不要怕麻烦,自己踩过坑之后,才能变得更加强大。
前往 config/config.default.js 做好白名单配置,这里我直接全部允许请求:

  1. /* eslint valid-jsdoc: "off" */
  2. 'use strict';
  3. /**
  4. * @param {Egg.EggAppInfo} appInfo app info
  5. */
  6. module.exports = appInfo => {
  7. /**
  8. * built-in config
  9. * @type {Egg.EggAppConfig}
  10. **/
  11. const config = exports = {};
  12. // 添加csrf配置
  13. config.security = {
  14. csrf: {
  15. enable: false,
  16. ignoreJSON: true,
  17. },
  18. domainWhiteList: [ '*' ], // 配置白名单
  19. };
  20. return {
  21. ...config,
  22. };
  23. };

再次点击发送(可以重启一下项目)
image.png成功拿到服务端返回的参数。

从 Service 内获取数据

我们在 app 目录下新建 service,并且创建一个 home.js,为其添加如下代码:

  1. 'use strict';
  2. const Service = require('egg').Service;
  3. class HomeService extends Service {
  4. async user() {
  5. return {
  6. name: 'xiumubai',
  7. slogen: '朽木可雕也',
  8. };
  9. }
  10. }
  11. module.exports = HomeService;

我们可以在 Controller 内拿到上述方法,如下所示:

  1. // app/controller/home.js
  2. // 获取用户信息
  3. async user() {
  4. const { ctx } = this;
  5. const { name, slogen } = await ctx.service.home.user();
  6. ctx.body = {
  7. name,
  8. slogen
  9. }
  10. }

打开 Apifox 请求如下所示:
image.png
HomeService 中的 user 方法内部,在后续链接数据库之后,可以 this 上下文,拿到 MySQL 的实力方法,对数据库进行 CRUD 操作。