热门搜索 :
考研考公
您的当前位置:首页正文

Egg进阶

来源:东饰资讯网

Egg进阶与实战

Debug

添加 npm scriptspackage.json

{
  "scripts": {
    "debug": "egg-bin debug"
  }
}

同时也支持自定义调试参数:

egg-bin debug --inpsect=9229

执行 debug 命令时,应用也是以 env: local 启动的,读取的配置是 config.default.jsconfig.local.js 合并的结果。

chrome浏览器调试窗口

image-20190704133631408.png

调式步骤

  1. 打开 chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:9999/__ws_proxy__
  2. 选择sources
  3. 找到你需要调试的地方,打上断点
  4. 进行http访问

日志

日志对于 Web 开发的重要性毋庸置疑,它对于监控应用的运行状态、问题排查等都有非常重要的意义。

日志路径

所有日志文件默认都放在 ${appInfo.root}/logs/${appInfo.name} 路径下。

如果想自定义日志路径:

一般生产环境下才需要自定义日志

// config/config.${env}.js
exports.logger = {
  dir: '/path/to/your/custom/log/dir',
};

日志分类

框架内置了几种日志,分别在不同的场景下使用:

  • appLogger ${appInfo.name}-web.log,例如 example-app-web.log,应用相关日志,供应用开发者使用的日志。我们在绝大数情况下都在使用它。
  • coreLogger egg-web.log 框架内核、插件日志。
  • errorLogger common-error.log 实际一般不会直接使用它,任何 logger 的 .error() 调用输出的日志都会重定向到这里,重点通过查看此日志定位异常。
  • agentLogger egg-agent.log agent 进程日志,框架和使用到 agent 进程执行任务的插件会打印一些日志到这里。

如果想自定义以上日志文件名称,可以在 config 文件中覆盖默认值:

// config/config.${env}.js
module.exports = appInfo => {
  return {
    logger: {
      appLogName: `${appInfo.name}-web.log`,
      coreLogName: 'egg-web.log',
      agentLogName: 'egg-agent.log',
      errorLogName: 'common-error.log',
    },
  };
};

日志级别

日志分为 NONEDEBUGINFOWARNERROR 5 个级别。

日志打印到文件中的同时,为了方便开发,也会同时打印到终端中。

  • Error 错误
  • Warn 警告,比如vue没有加key
  • Info 性能分析,这里一定是有用的信息
  • Debug 打印一些随意的信息
  • None 不要去打印

如何打印日志

如果我们在处理请求时需要打印日志,这时候使用 Context Logger,用于记录 Web 行为相关的日志。

ctx.logger.debug('debug info');
ctx.logger.info('info');
ctx.logger.warn('WARNNING!!!!');

如果我们想做一些应用级别的日志记录,如记录启动阶段的一些数据信息,可以通过 App Logger 来完成。

// app.js
module.exports = app => {
  app.logger.debug('debug info');
  app.logger.info('启动耗时 %d ms', Date.now() - start);
  app.logger.warn('warning!');

  app.logger.error(someErrorObj);
};

对于框架和插件开发者会使用到的 App Logger 还有 app.coreLogger

// app.js
module.exports = app => {
  app.coreLogger.info('启动耗时 %d ms', Date.now() - start);
};

在开发框架和插件时有时会需要在 Agent 进程运行代码,这时使用 agent.coreLogger

// agent.js
module.exports = agent => {
  agent.logger.debug('debug info');
  agent.logger.info('启动耗时 %d ms', Date.now() - start);
  agent.logger.warn('warning!');

  agent.logger.error(someErrorObj);
};

文件日志级别

默认只会输出 INFO 及以上(WARNERROR)的日志到文件中。

可通过如下方式配置输出到文件日志的级别:

打印所有级别日志到文件中:

// config/config.${env}.js
exports.logger = {
  level: 'DEBUG',
};

关闭所有打印到文件的日志:

// config/config.${env}.js
exports.logger = {
  level: 'NONE',
};

日志切割

默认日志切割方式,在每日 00:00 按照 .log.YYYY-MM-DD 文件名进行切割。

我们也可以按照文件大小进行切割。例如,当文件超过 2G 时进行切割。

例如,我们需要把 egg-web.log 按照大小进行切割:

// config/config.${env}.js
const path = require('path');

module.exports = appInfo => {
  return {
    logrotator: {
      filesRotateBySize: [
        path.join(appInfo.root, 'logs', appInfo.name, 'egg-web.log'),
      ],
      maxFileSize: 2 * 1024 * 1024 * 1024,
    },
  };
};

我们也可以选择按照小时进行切割,这和默认的按天切割非常类似,只是时间缩短到每小时。

// config/config.${env}.js
const path = require('path');

module.exports = appInfo => {
  return {
    logrotator: {
      filesRotateByHour: [
        path.join(appInfo.root, 'logs', appInfo.name, 'common-error.log'),
      ],
    },
  };
};

性能

通常 Web 访问是高频访问,每次打印日志都写磁盘会造成频繁磁盘 IO,为了提高性能,我们采用的文件日志写入策略是:日志同步写入内存,异步每隔一段时间(默认 1 秒)刷盘

多进程模型和进程间通信

我们知道 JavaScript 代码是运行在单线程上的,换句话说一个 Node.js 进程只能运行在一个 CPU 上。那么如果用 Node.js 来做 Web Server,就无法享受到多核运算的好处。作为企业级的解决方案,我们要解决的一个问题就是:

如何榨干服务器资源,利用上多核 CPU 的并发优势?

  • 负责启动其他进程的叫做 Master 进程,他好比是个『包工头』,不做具体的工作,只负责启动其他进程。
  • 其他被启动的叫 Worker 进程,顾名思义就是干活的『工人』。它们接收请求,对外提供服务。
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  // Fork workers.
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', function(worker, code, signal) {
    console.log('worker ' + worker.process.pid + ' died');
  });
} else {
  // Workers can share any TCP connection
  // In this case it is an HTTP server
  http.createServer(function(req, res) {
    res.writeHead(200);
    res.end("hello world\n");
  }).listen(8000);
}

egg多进程模型

作为企业级的解决方案,要考虑的东西还有很多。

  • Worker 进程异常退出以后该如何处理?
  • 多个 Worker 进程之间如何共享资源?
  • 多个 Worker 进程之间如何调度?

进程守护

健壮性(又叫鲁棒性)是企业级应用必须考虑的问题,除了程序本身代码质量要保证,框架层面也需要提供相应的『兜底』机制保证极端情况下应用的可用性。

一般来说,Node.js 进程退出可以分为两类:

未捕获异常
  1. 关闭异常 Worker 进程所有的 TCP Server(将已有的连接快速断开,且不再接收新的连接),断开和 Master 的 IPC 通道,不再接受新的用户请求。
  2. Master 立刻 fork 一个新的 Worker 进程,保证在线的『工人』总数不变。
  3. 异常 Worker 等待一段时间,处理完已经接受的请求后退出。
image-20190704185701491.png
OOM,系统异常

而当一个进程出现异常导致 crash 或者 OOM 被系统杀死时,不像未捕获异常发生时我们还有机会让进程继续执行,只能够让当前进程直接退出,Master 立刻 fork 一个新的 Worker。

  • OOM
    • 内存用尽

Agent机制

egg和nodejs原生多进程不同的地方

说到这里,Node.js 多进程方案貌似已经成型,这也是我们早期线上使用的方案。但后来我们发现有些工作其实不需要每个 Worker 都去做,如果都做,一来是浪费资源,更重要的是可能会导致多进程间资源访问冲突。举个例子:生产环境的日志文件我们一般会按照日期进行归档,在单进程模型下这再简单不过了:

  1. 每天凌晨 0 点,将当前日志文件按照日期进行重命名
  2. 销毁以前的文件句柄,并创建新的日志文件继续写入

试想如果现在是 4 个进程来做同样的事情,是不是就乱套了。所以,对于这一类后台运行的逻辑,我们希望将它们放到一个单独的进程上去执行,这个进程就叫 Agent Worker,简称 Agent。Agent 好比是 Master 给其他 Worker 请的一个『秘书』,它不对外提供服务,只给 App Worker 打工,专门处理一些公共事务。现在我们的多进程模型就变成下面这个样子了

image-20190704190149301.png

那我们框架的启动时序如下:

image-20190704190421232.png
  1. Master 启动后先 fork Agent 进程
  2. Agent 初始化成功后,通过 IPC 通道通知 Master
  3. Master 再 fork 多个 App Worker
  4. App Worker 初始化成功,通知 Master
  5. 所有的进程初始化成功后,Master 通知 Agent 和 Worker 应用启动成功

另外,关于 Agent Worker 还有几点需要注意的是:

  1. 由于 App Worker 依赖于 Agent,所以必须等 Agent 初始化完成后才能 fork App Worker
  2. Agent 虽然是 App Worker 的『小秘』,但是业务相关的工作不应该放到 Agent 上去做,不然把她累垮了就不好了
  3. 由于 Agent 的特殊定位,我们应该保证它相对稳定。当它发生未捕获异常,框架不会像 App Worker 一样让他退出重启,而是记录异常日志、报警等待人工处理

Agent用法

// agent.js
module.exports = agent => {
  // 在这里写你的初始化逻辑

  // 也可以通过 messenger 对象发送消息给 App Worker
  // 但需要等待 App Worker 启动成功后才能发送,不然很可能丢失
  agent.messenger.on('egg-ready', () => {
    const data = { ... };
    agent.messenger.sendToApp('xxx_action', data);
  });
};
// app.js
module.exports = app => {
  app.messenger.on('xxx_action', data => {
    // ...
  });
};

master VS agent VS worker

当一个应用启动时,会同时启动这三类进程。

image-20190704192617114.png
master
agent

在大部分情况下,我们在写业务代码的时候完全不用考虑 Agent 进程的存在,但是当我们遇到一些场景,只想让代码运行在一个进程上的时候,Agent 进程就到了发挥作用的时候了。

由于 Agent 只有一个,而且会负责许多维持连接的脏活累活,因此它不能轻易挂掉和重启,所以 Agent 进程在监听到未捕获异常时不会退出,但是会打印出错误日志,我们需要对日志中的未捕获异常提高警惕

  • 应用场景
    • 长连接

worker

Worker 运行的是业务代码,相对会比 Agent 和 Master 进程上运行的代码复杂度更高,稳定性也低一点,当 Worker 进程异常退出时,Master 进程会重启一个 Worker 进程。

进程间通信

虽然每个 Worker 进程是相对独立的,但是它们之间始终还是需要通讯的,叫进程间通讯(IPC)。下面是 Node.js 官方提供的一段示例代码

'use strict';
const cluster = require('cluster');

if (cluster.isMaster) {
  const worker = cluster.fork();
  worker.send('hi there');
  worker.on('message', msg => {
    console.log(`msg: ${msg} from worker#${worker.id}`);
  });
} else if (cluster.isWorker) {
  process.on('message', (msg) => {
    process.send(msg);
  });
}

细心的你可能已经发现 cluster 的 IPC 通道只存在于 Master 和 Worker/Agent 之间,Worker 与 Agent 进程互相间是没有的。那么 Worker 之间想通讯该怎么办呢?是的,通过 Master 来转发。

image-20190704192933130.png

messenger 对象

  • app.messenger.broadcast(action, data):发送给所有的 agent / app 进程(包括自己)

  • app.messenger.sendToApp(action, data): 发送给所有的 app 进程

    • 在 app 上调用该方法会发送给自己和其他的 app 进程
    • 在 agent 上调用该方法会发送给所有的 app 进程
  • app.messenger.sendToAgent(action, data): 发送给 agent 进程

    • 在 app 上调用该方法会发送给 agent 进程
    • 在 agent 上调用该方法会发送给 agent 自己
  • agent.messenger.sendRandom(action, data):

    • app 上没有该方法(现在 Egg 的实现是等同于 sentToAgent)
    • agent 会随机发送消息给一个 app 进程(由 master 来控制发送给谁)
  • app.messenger.sendTo(pid, action, data): 发送给指定进程

// app.js
module.exports = app => {
  // 注意,只有在 egg-ready 事件拿到之后才能发送消息
  app.messenger.once('egg-ready', () => {
    app.messenger.sendToAgent('agent-event', { foo: 'bar' });
    app.messenger.sendToApp('app-event', { foo: 'bar' });
  });
}

上面所有 app.messenger 上的方法都可以在 agent.messenger 上使用。

上面的示例中提到,需要等 egg-ready 消息之后才能发送消息。只有在 Master 确认所有的 Agent 进程和 Worker 进程都已经成功启动(并 ready)之后,才会通过 messenger 发送 egg-ready 消息给所有的 Agent 和 Worker,告知一切准备就绪,IPC 通道可以开始使用了。

在 messenger 上监听对应的 action 事件,就可以收到其他进程发送来的信息了。

app.messenger.on(action, data => {
  // process data
});
app.messenger.once(action, data => {
  // process data
});

异常捕获( 错误处理 )

得益于框架支持的异步编程模型,错误完全可以用 try catch 来捕获。在编写应用代码时,所有地方都可以直接用 try catch 来捕获异常。

// app/service/test.js
try {
  const res = await  { dataType: 'json' });
  if (res.status !== 200) throw new Error('response status is not 200');
  return res.data;
} catch (err) {
  this.logger.error(err);
  return {};
}

按照正常代码写法,所有的异常都可以用这个方式进行捕获并处理,但是一定要注意一些特殊的写法可能带来的问题。打一个不太正式的比方,我们的代码全部都在一个异步调用链上,所有的异步操作都通过 await 串接起来了,但是只要有一个地方跳出了异步调用链,异常就捕获不到了。

// app/controller/home.js
class HomeController extends Controller {
  async buy () {
    const request = {};
    const config = await ctx.service.trade.buy(request);
    // 下单后需要进行一次核对,且不阻塞当前请求
    setImmediate(() => {
      ctx.service.trade.check(request);
    });
  }
}

在这个场景中,如果 service.trade.check 方法中代码有问题,导致执行时抛出了异常,尽管框架会在最外层通过 try catch 统一捕获错误,但是由于 setImmediate 中的代码『跳出』了异步链,它里面的错误就无法被捕捉到了。因此在编写类似代码的时候一定要注意。

当然,框架也考虑到了这类场景,提供了 ctx.runInBackground(scope) 辅助方法,通过它又包装了一个异步链,所有在这个 scope 里面的错误都会统一捕获。

class HomeController extends Controller {
  async buy () {
    const request = {};
    const config = await ctx.service.trade.buy(request);
    // 下单后需要进行一次核对,且不阻塞当前请求
    ctx.runInBackground(async () => {
      // 这里面的异常都会统统被 Backgroud 捕获掉,并打印错误日志
      await ctx.service.trade.check(request);
    });
  }
}

为了保证异常可追踪,必须保证所有抛出的异常都是 Error 类型,因为只有 Error 类型才会带上堆栈信息,定位到问题。

框架层统一异常处里

尽管框架提供了默认的统一异常处理机制(dev 会显示堆栈信息),但是应用开发中经常需要对异常时的响应做自定义,特别是在做一些接口开发的时候。框架自带的 onerror 插件支持自定义配置错误处理方法,可以覆盖默认的错误处理方法。

// config/config.default.js
module.exports = {
  onerror: {
    all(err, ctx) {
      // 在此处定义针对所有响应类型的错误处理方法
      // 注意,定义了 config.all 之后,其他错误处理方法不会再生效
      ctx.body = 'error';
      ctx.status = 500;
    },
  },
};
404

框架并不会将服务端返回的 404 状态当做异常来处理,但是框架提供了当响应为 404 且没有返回 body 时的默认响应。

  • 当请求被框架判定为需要 JSON 格式的响应时,会返回一段 JSON:
{ "message": "Not Found" }
  • 当请求被框架判定为需要 HTML 格式的响应时,会返回一段 HTML:
<h1>404 Not Found</h1>

框架支持通过配置,将默认的 HTML 请求的 404 响应重定向到指定的页面。

// config/config.default.js
module.exports = {
  notfound: {
    pageUrl: '/404.html',
  },
};
自定义404响应

在一些场景下,我们需要自定义服务器 404 时的响应,和自定义异常处理一样,我们也只需要加入一个中间件即可对 404 做统一处理:

// app/middleware/notfound_handler.js
module.exports = () => {
  return async function notFoundHandler(ctx, next) {
    await next();
    if (ctx.status === 404 && !ctx.body) {
      if (ctx.acceptJSON) {
        ctx.body = { error: 'Not Found' };
      } else {
        ctx.body = '<h1>Page Not Found</h1>';
      }
    }
  };
};

在配置中引入中间件:

// config/config.default.js
module.exports = {
  middleware: [ 'notfoundHandler' ],
};

多实例插件

  • 在一个应用中同时使用同一个服务的不同实例(连接到两个不同的 MySQL 数据库)。
  • 从其他服务获取配置后动态初始化连接(从配置中心获取到 MySQL 服务地址后再建立连接)。

如果让插件各自实现,可能会出现各种奇怪的配置方式和初始化方式,所以框架提供了 app.addSingleton(name, creator) 方法来统一这一类服务的创建。需要注意的是在使用 app.addSingleton(name, creator) 方法时,配置文件中一定要有 client 或者 clients 为 key 的配置作为传入 creator 函数 的 config

插件写法

// egg-mysql/app.js
module.exports = app => {
  // 第一个参数 mysql 指定了挂载到 app 上的字段,我们可以通过 `app.mysql` 访问到 MySQL singleton 实例
  // 第二个参数 createMysql 接受两个参数(config, app),并返回一个 MySQL 的实例
  app.addSingleton('mysql', createMysql);
}

/**
 * @param  {Object} config   框架处理之后的配置项,如果应用配置了多个 MySQL 实例,会将每一个配置项分别传入并调用多次 createMysql
 * @param  {Application} app 当前的应用
 * @return {Object}          返回创建的 MySQL 实例
 */
function createMysql(config, app) {
  // 省略。。。通过config,创建一个mysql实例

  return client;
}

初始化方法也支持 Async function,便于有些特殊的插件需要异步化获取一些配置文件。

单实例

  1. 在配置文件中声明 MySQL 的配置。
// config/config.default.js
module.exports = {
  mysql: {
    client: {
      host: 
      port: '3306',
      user: 'test_user',
      password: 'test_password',
      database: 'test',
    },
  },
};
  1. 直接通过 app.mysql 访问数据库。
// app/controller/post.js
class PostController extends Controller {
  async list() {
    const posts = await this.app.mysql.query(sql, values);
  },
}

多实例

  1. 同样需要在配置文件中声明 MySQL 的配置,不过和单实例时不同,配置项中需要有一个 clients 字段,分别申明不同实例的配置,同时可以通过 default 字段来配置多个实例中共享的配置(如 host 和 port)。需要注意的是在这种情况下要用 get 方法指定相应的实例。(例如:使用 app.mysql.get('db1').query(),而不是直接使用 app.mysql.query() 得到一个 undefined)。

// config/config.default.js
exports.mysql = {
  clients: {
    // clientId, access the client instance by app.mysql.get('clientId')
    db1: {
      user: 'user1',
      password: 'upassword1',
      database: 'db1',
    },
    db2: {
      user: 'user2',
      password: 'upassword2',
      database: 'db2',
    },
  },
  // default configuration for all databases
  default: {
    host: 
    port: '3306',
  },
};
  1. 通过 app.mysql.get('db1') 来获取对应的实例并使用。
// app/controller/post.js
class PostController extends Controller {
  async list() {
    const posts = await this.app.mysql.get('db1').query(sql, values);
  },
}

动态创建实例

// app.js
module.exports = app => {
  app.beforeStart(async () => {
    // 从配置中心获取 MySQL 的配置 { host, post, password, ... }
    const mysqlConfig = await app.configCenter.fetch('mysql');
    // 动态创建 MySQL 实例
    app.database = await app.mysql.createInstanceAsync(mysqlConfig);
  });
};

通过 app.database 来使用这个实例。

// app/controller/post.js
class PostController extends Controller {
  async list() {
    const posts = await this.app.database.query(sql, values);
  },
}

注意,在动态创建实例的时候,框架也会读取配置中 default 字段内的配置项作为默认配置。

多进程增强版

在前面讲解 的多进程模型中, 其中适合使用 Agent 进程的有一类常见的场景:一些中间件客户端需要和服务器建立长连接,理论上一台服务器最好只建立一个长连接,但多进程模型会导致 n 倍(n = Worker 进程数)连接被创建。

image-20190705175043007.png

为了尽可能的复用长连接(因为它们对于服务端来说是非常宝贵的资源),我们会把它放到 Agent 进程里维护,然后通过 messenger 将数据传递给各个 Worker。这种做法是可行的,但是往往需要写大量代码去封装接口和实现数据的传递,非常麻烦。

另外,通过 messenger 传递数据效率是比较低的,因为它会通过 Master 来做中转;万一 IPC 通道出现问题还可能将 Master 进程搞挂。

我们提供一种新的模式来降低这类客户端封装的复杂度。通过建立 Agent 和 Worker 的 socket 直连跳过 Master 的中转。Agent 作为对外的门面维持多个 Worker 进程的共享连接。

核心思想

  • 客户端会被区分为两种角色:

    • Leader: 负责和远程服务端维持连接,对于同一类的客户端只有一个 Leader。
    • Follower: 会将具体的操作委托给 Leader,常见的是订阅模型(让 Leader 和远程服务端交互,并等待其返回)。
  • 如何确定谁是 Leader,谁是 Follower 呢?有两种模式:

    • 自由竞争模式:客户端启动的时候通过本地端口的争夺来确定 Leader。例如:大家都尝试监听 7777 端口,最后只会有一个实例抢占到,那它就变成 Leader,其余的都是 Follower。
    • 强制指定模式:框架指定某一个 Leader,其余的就是 Follower。
  • 框架里面我们采用的是强制指定模式,Leader 只能在 Agent 里面创建,这也符合我们对 Agent 的定位

  • 框架启动的时候 Master 会随机选择一个可用的端口作为 Cluster Client 监听的通讯端口,并将它通过参数传递给 Agent 和 App Worker。

  • Leader 和 Follower 之间通过 socket 直连(通过通讯端口),不再需要 Master 中转。

新的模式下,客户端的通信方式如下:

image-20190707004732019.png

客户端接口类型抽象

抽象是类的描述

我们将客户端接口抽象为下面两大类,这也是对客户端接口的一个规范,对于符合规范的客户端,我们可以自动将其包装为 Leader/Follower 模式。

  • 订阅、发布类(subscribe / publish):

    • subscribe(info, listener) 接口包含两个参数,第一个是订阅的信息,第二个是订阅的回调函数。
    • publish(info) 接口包含一个参数,就是订阅的信息。
  • 调用类 (invoke),支持 callback, promise 和 generator function 三种风格的接口,但是推荐使用 generator function。

客户端示例

const Base = require('sdk-base');

class Client extends Base {
  constructor(options) {
    super(options);
    // 在初始化成功以后记得 ready
    this.ready(true);
  }

  /**
   * 订阅
   *
   * @param {Object} info - 订阅的信息(一个 JSON 对象,注意尽量不要包含 Function, Buffer, Date 这类属性)
   * @param {Function} listener - 监听的回调函数,接收一个参数就是监听到的结果对象
   */
  subscribe(info, listener) {
    // ...
  }

  /**
   * 发布
   *
   * @param {Object} info - 发布的信息,和上面 subscribe 的 info 类似
   */
  publish(info) {
    // ...
  }

  /**
   * 获取数据 (invoke)
   *
   * @param {String} id - id
   * @return {Object} result
   */
  async getData(id) {
    // ...
  }
}

异常处理

  • Leader 如果“死掉”会触发新一轮的端口争夺,争夺到端口的那个实例被推选为新的 Leader。
  • 为保证 Leader 和 Follower 之间的通道健康,需要引入定时心跳检查机制,如果 Follower 在固定时间内没有发送心跳包,那么 Leader 会将 Follower 主动断开,从而触发 Follower 的重新初始化。

具体使用方法

下面我用一个简单的例子,介绍在框架里面如何让一个客户端支持 Leader/Follower 模式:

  • 第一步,我们的客户端最好是符合上面提到过的接口约定,例如:
// registry_client.js     就是进行socket的基础类
const URL = require('url');
const Base = require('sdk-base');

class RegistryClient extends Base {
  constructor(options) {
    super({
      // 指定异步启动的方法
      initMethod: 'init',
    });
    this._options = options;
    this._registered = new Map();
  }

  /**
   * 启动逻辑
   */
  async init() {
    this.ready(true);
  }

  /**
   * 获取配置
   * @param {String} dataId - the dataId
   * @return {Object} 配置
   */
  async getConfig(dataId) {
    return this._registered.get(dataId);
  }

  /**
   * 订阅
   * @param {Object} reg
   *   - {String} dataId - the dataId
   * @param {Function}  listener - the listener
   */
  subscribe(reg, listener) {
    const key = reg.dataId;  // 时间名称
    this.on(key, listener);

    const data = this._registered.get(key);
    if (data) {
      process.nextTick(() => listener(data));
    }
  }

  /**
   * 发布
   * @param {Object} reg
   *   - {String} dataId - the dataId
   *   - {String} publishData - the publish data
   */
  publish(reg) {
    const key = reg.dataId;
    let changed = false;

    if (this._registered.has(key)) {
      const arr = this._registered.get(key);
      if (arr.indexOf(reg.publishData) === -1) {
        changed = true;
        arr.push(reg.publishData);
      }
    } else {
      changed = true;
      this._registered.set(key, [reg.publishData]);
    }
    if (changed) {
      this.emit(key, this._registered.get(key).map(url => URL.parse(url, true)));
    }
  }
}

module.exports = RegistryClient;
  • 第二步,使用 agent.cluster 接口对 RegistryClient 进行封装:
// agent.js
const RegistryClient = require('registry_client');

module.exports = agent => {
  // 对 RegistryClient 进行封装和实例化
  agent.registryClient = agent.cluster(RegistryClient)
    // create 方法的参数就是 RegistryClient 构造函数的参数
    .create({});

  agent.beforeStart(async () => {
    await agent.registryClient.ready();
    agent.coreLogger.info('registry client is ready');
  });
};
  • 第三步,使用 app.cluster 接口对 RegistryClient 进行封装:
const RegistryClient = require('registry_client');

module.exports = app => {
  app.registryClient = app.cluster(RegistryClient).create({});
  app.beforeStart(async () => {
    await app.registryClient.ready();
    app.coreLogger.info('registry client is ready');

    // 调用 subscribe 进行订阅
    app.registryClient.subscribe({
      dataId: 'demo.DemoService',
    }, val => {
      // ...
    });

    // 调用 publish 发布数据
    app.registryClient.publish({
      dataId: 'demo.DemoService',
      publishData: 'xxx',
    });

    // 调用 getConfig 接口
    const res = await app.registryClient.getConfig('demo.DemoService');
    console.log(res);
  });
};

实战-简易博客

学习目标

使用egg + mongoDb实现一个简易博客的增删改查。

框架选型

egg + mongoose

准备工作

  • mongoDb可视化工具
  • postman
  • 安装mongoDb

mongoose简介

  • 简介: mongoos对mongoDb的一层封装和抽象,方便在nodejs中操作mongoDb数据库 。

  • 依赖: mongoDb 和 nodejs

安装
npm install mongoose
连接
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/test');
快速开始
  • schema是对数据模型的描述

    对应数据库存储的字段

var postsSchema = mongoose.Schema({ // 对文章的描述
  title: String,
  content: String
});
var Posts = mongoose.model('posts', postsSchema);

model 是我们构造 document 的 Class。 在例子中,每个 document 都是一篇文章,它的属性和行为都会被声明在 schema。 现在我们来“创造”一篇文章:

var a_news = new Posts({ title: 'xxx', content: 'xxx' });
  a_news.save(function (err, fluffy) {
    if (err) return console.error(err);
  });
Posts.find(function (err, kittens) {
  if (err) return console.error(err);
  console.log(kittens);
})
// 这么写可以获取所有 name 为 "book" 开头的数据
Posts.find({ name: /^book/ }, callback);

正式开始

安装依赖

npm init egg type-simple

npm install egg-mongoose --save

开发顺序

  1. 安装环境,配置数据库插件
  2. 编写schema,设计存储的字段
  3. 进行路由设计, 通过控制器添加数据
    1. 错误处理
    2. 返回值
    3. 校验参数
  4. 数据库的查询
    1. 查询全部数据
    2. 查询单个数据
  5. 数据库的删除
  6. 数据库的更新
  7. service提取
配置
// config/plugin.js
mongoose: {
    enable: true,
    package: 'egg-mongoose',
},
// config/config.default.js
config.mongoose = {
  url: 'mongodb://127.0.0.1/example',
  options: {},
}
路由配置
  1. 解析用户请求,给相应的controller
  2. 通过resources 直接创建restFul api
// app/router.js
module.exports = app => {
  const { router, controller } = app;
  router.resources('posts', '/api/posts',controller.posts);
};
控制器
  1. 解析request参数
  2. 校验参数
  3. 调用service
  4. 返回结果
'use strict';

// const Controller = require('egg').Controller;

const httpController = require('./base/http');
// GET /posts -> app.controller.posts.index
// GET /posts/new -> app.controller.posts.new
// GET /posts/:id -> app.controller.posts.show
// GET /posts/:id/edit -> app.controller.posts.edit
// POST /posts -> app.controller.posts.create
// PUT /posts/:id -> app.controller.posts.update
// DELETE /posts/:id -> app.controller.posts.destroy

const createRule = {  // 参数校验
  title: { type: 'string' },
  content: { type: 'string' },
}

const updateRule = {
  title: { type: 'string', required: false },
  content: { type: 'string', required: false },
}

class PostsController extends httpController {
  async create() { // 创建文章
    const { ctx } = this;

    const requestBody = ctx.request.body;

    try {
      this.ctx.validate(createRule);
      const res = await ctx.service.posts.create(requestBody);
      this.success(res);
    } catch (err) {
      // console.log('=======', err)
      this.fail(err)
    }
  }

  async index() {// 读取所有文章
    const { ctx } = this;
    ctx.body = await ctx.service.posts.find({});
  }

  async show() {
    const { ctx } = this;

    try {
      const params_id = ctx.params.id;
      const res = await ctx.service.posts.find({
        _id: params_id
      });
      this.success(res);
    } catch (err) {
      this.fail(err);
    }
  }

  async update() {  // 更新文章
    const { ctx } = this;
    // console.log('================', ctx.request.body)
    try {
      const params_id = ctx.params.id;
      const requestBody = ctx.request.body;
      ctx.validate(updateRule);

      const res = await ctx.service.posts.update({
        _id: params_id,
      }, {
          $set: {
            ...requestBody
          }
        });
      this.success(res);
    } catch (err) {
      this.fail(err);
    }
  }

  async destroy() { // 删除文章

    const { ctx } = this;

    // console.log('------------', ctx.model);
    try {
      const params_id = ctx.params.id;
      const res = await ctx.service.posts.remove({
        _id: params_id
      });
      this.success(res);
    } catch (err) {
      this.fail(err);
    }
  }
}

module.exports = PostsController;

base controller
  1. 对controller的一种抽象
  2. 抽取通用逻辑
// controller/base/http.js
'use strict';

const Controller = require('egg').Controller;

class HttpController extends Controller {
    success(data) {
        // msg 和 code这样的2个字段 在所有的请求里面都需要返回。
        // 好好理解oop
        this.ctx.body = {
            msg: data && data.msg || 'success',
            code: 0,
            data
        }
    }

    fail(data) {
        this.logger.error(data)
        this.ctx.body = {
            msg: data && data.msg || 'fail',
            code: data && data.code || 1,
            data
        }
    }
}

module.exports = HttpController;

model
  1. 对数据模型的描述和创建
// {app_root}/app/model/user.js
module.exports = app => {
    const mongoose = app.mongoose;
    const Schema = mongoose.Schema;
    const postsSchema = new Schema({
        title: { type: String, unique: true },
        content: { type: String },
    });
    return mongoose.model('Posts', postsSchema);
}

service
  1. 具体操作数据库的逻辑
  2. 进行增删改查,并暴露方法给控制器调用
const Service = require('egg').Service;

class PostsService extends Service {
    async find(data) {  // 查
        const res = await this.ctx.model.Posts.find(data);
        return res
    }

    async update(findData, updateData) { // 改
        const res = await this.ctx.model.Posts.update(findData, updateData);
        return res;
    }

    async remove(data) {  // 删除
        const res = await this.ctx.model.Posts.remove(data);
        return res;
    }

    async create(data) {  // 增
        // console.log(this.ctx.model)
        const postsInstance = new this.ctx.model.Posts({
            title: data.title,
            content: data.content
        });

        const res = await postsInstance.save();
        return res;
    }
}

module.exports = PostsService;
Top