introduce

Eggjs is a framework based on Koajs, so it should be a framework on top of the framework. It inherits the high-performance advantages of Koa, and at the same time adds some constraints and development specifications to avoid the problem that Koajs framework itself has too high development freedom.

Koajs is a relatively basic framework in NodeJS. It has very high degree of freedom without too many constraints and specifications. Every developer has his or her own “operations” when implementing his or her own services. In order to adapt to enterprise development, Egg added some development norms and constraints to solve the problem that Koajs is not suitable for enterprise use due to its high degree of freedom. Egg was born under this background.

An Egg is an “Egg” created by the Alibaba team. Eggs have infinite possibilities, and eggs hatched by chickens are chickens, and eggs hatched by dinosaurs are dinosaurs, which better embodies one of the biggest highlights of Egg “plug-in mechanism”, where every company, every team, and even individual developers can hatch the most suitable framework. For example, different departments within Ali have incubated their own suitable egg frames, such as ant chair, UC Nut, aliyun-egg, etc., as can be seen in the following picture.

features

  • Provides the ability to customize the upper-layer framework based on Egg
  • Highly extensible plug-in mechanism
  • Built-in multi-process management
  • Based on Koa development, excellent performance
  • The framework is stable and the test coverage is high
  • Incremental development

Environment setup, creation, and operation

$ npm  i egg-init -g
$ mkdir egg-example && cd egg-example
$ npm init egg --type=simple
$ npm i
Copy the code

Start the project

$ npm run dev
$ gooopen http://localhost:7001
Copy the code

Directory Structure

An egg - project ├ ─ ─ package. Json ├ ─ ─ app. Js (optional) ├ ─ ─ agent. The js (optional) ├ ─ ─ app (project directory) | ├ ─ ─ the router, js (used to configure the URL routing rules) │ ├ ─ ─ Controller (used to parse user input, Return the corresponding results after processing) │ | └ ─ ─ home. Js │ ├ ─ ─ service (for writing business logic layer) │ | └ ─ ─ the user. The js │ ├ ─ ─ middleware (for writing middleware) │ | └ ─ ─ Response_time. Js │ ├ ─ ─ the schedule (optional) │ | └ ─ ─ my_task. Js │ ├ ─ ─ public (for static resources) │ | └ ─ ─ reset. CSS │ ├ ─ ─ the view (optional) │ | └ ─ ─ home. TPL │ └ ─ ─ the extend (for the expansion of the framework) │ ├ ─ ─ helper. Js (optional) │ ├ ─ ─ request. Js (optional) │ ├ ─ ─ the response. The js (optional) │ ├ ─ ─ the context, js (optional) │ ├ ─ ─ application. Js (optional) │ └ ─ ─ agent. The js (optional) ├ ─ ─ the config (used to write configuration files) | ├ ─ ─ the plugin, js (used in configuration needs to be loaded plug-ins) | ├ ─ ─ Config. Default. Js │ ├ ─ ─ config. Prod. Js | ├ ─ ─ config. The test. The js (optional) | ├ ─ ─ config. Local, js (optional) | └ ─ ─ config. The unittest. Js (optional) └ ─ ─ test (for a unit test) ├ ─ ─ middleware | └ ─ ─ response_time. Test. The js └ ─ ─ controller └ ─ ─ home. Test. JsCopy the code

Main Content Introduction


What is the MVC

The design of egg fully conforms to the better MVC design pattern

  • Model – A Model represents an object that accesses data. It can also have logic to update the controller as the data changes.
  • View – A View represents a visualization of the data contained in the model.
  • Controller (Controller) – Controllers act on models and views. It controls the flow of data to model objects and updates the view as the data changes. It separates the view from the model.

Controller (the controller)

Controller is implemented in the app/ Controller directory

'use strict';

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

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

Services (service)

'use strict';

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

class HomeService extends Service {
  async index() {
    return {ok:1}}}module.exports = HomeService;

Copy the code

Modify the controller/home. Js

'use strict';

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

class HomeController extends Controller {
  async index() {
    const {
      ctx,
      service
    } = this;
    const res = await service.home.index();
    ctx.body = res
  }
}

module.exports = HomeController;
Copy the code

Router (routes)

'use strict';

/** * @param {Egg.Application} app - egg application */
module.exports = app= > {
  const { router, controller } = app;
  router.get('/', controller.home.index);
};
Copy the code

Visit: http://locoalhost:7001

Actual project demonstration

Add, delete, change and check the user table

The case is based on mongoose non-relational database

Use egg-Mongoose to link the database

download

npm i egg-mongoose -S
Copy the code

configuration

config/plugin.js

exports.mongoose = {
  enable: true.package: 'egg-mongoose'};Copy the code

config/config.default.js

config.mongoose = {
    url: "Mongo: / / 127.0.0.1:27017 / egg - test".options: {useUnifiedTopology: true.useCreateIndex:true}}Copy the code

Creating a user model

model/user.js

module.exports = app= > {
  const mongoose = app.mongoose;
  const UserSchema = new mongoose.Schema({
    username: {
      type: String.unique: true.required: true
    },
    password: {
      type: String.required: true
    },
    avatar: {
      type: String.default: 'https://1.gravatar.com/avatar/a3e54af3cb6e157e496ae430aed4f4a3?s=96&d=mm'
    },
    createdAt: {
      type: Date.default: Date.now
    }
  })
  return mongoose.model('User', UserSchema);
}
Copy the code

Create a user

router.js

 // Create a user
  router.post('/api/user',controller.user.create);
Copy the code

controller/user.js

// Create a user
async create() {
    const {
      ctx,
      service
    } = this;
    const payLoad = ctx.request.body || {};
    const res = await service.user.create(payLoad);
    ctx.body = {res};
 }
Copy the code

service/user.js

async create(payload) {
    const {
        ctx
    } = this;
    return ctx.model.User.create(payload);
}
Copy the code

Get all Users

router.js

router.get('/api/user',controller.user.index);
Copy the code

controller/user.js

// Get all users
async index() {
    const {
        ctx,
        service
    } = this;
    const res = await service.user.index();
    ctx.body = res;
}
Copy the code

service/user.js

async index() {
    const {
        ctx
    } = this;
    return ctx.model.User.find();
}
Copy the code

Obtain user details by ID

router.js

// Get user details by ID
router.get('/api/user/:id',controller.user.detail);
Copy the code

controller/user.js

async detail() {
    const id = this.ctx.params.id;
    const res = await this.service.user.detail(id);
    ctx.body = res;
}
Copy the code

service/user.js

async detail(id){
   return this.ctx.model.User.findById({_id:id})
}
Copy the code

Update user

router.js

// Modify the user
router.put('/api/user/:id',controller.user.update);
Copy the code

controller/user.js

async update() {
    const id = this.ctx.params.id;
    const payLoad = this.ctx.request.body;
    // Call Service for business processing
    await this.service.user.update(id, payLoad);
    // Set the response content and response status code
    ctx.body = {msg:'User modified successfully'};
}
Copy the code

service/user.js

async update(_id, payLoad) {
    return this.ctx.model.User.findByIdAndUpdate(_id,payLoad);
}
Copy the code

Delete user

router.js

// Delete the user
router.delete('/api/user/:id',controller.user.delete);
Copy the code

controller/user.js

  async delete() {
    const id = this.ctx.params.id;
     // Call Service for business processing
    await this.service.user.delete(id);
     // Set the response content and response status code
    ctx.body = {msg:"User deleted successfully"};
  }
Copy the code

service/user.js

async delete(_id){
    return this.ctx.model.User.findByIdAndDelete(_id);
}
Copy the code

The middleware

configuration

Typically, middleware also has its own configuration. In the framework, a complete middleware contains configuration processing. We agreed that a middleware is a separate file placed in the app/middleware directory that requires exports a normal function that takes two arguments:

  • Options: middleware configuration items that the framework will useapp.config[${middlewareName}]Pass it in.
  • App: instance of the current Application Application.
module.exports = (option, app) = > {
  return async function (ctx, next) {
    try {
      await next();
    } catch (err) {
      // All exceptions raise an error event on the app, and the framework logs an error
      app.emit('error', err, this);
      const status = err.status || 500;
      // The details of the 500 errors in the build environment are not returned to the client because they may contain sensitive information
      const error = status === 500 && app.config.env === 'prod' ? 'Internal Server Error' : err.message
      // Read each property from the error object and set it into the response
      ctx.body = {
        code: status, // Server's own processing logic error (including frame error 500 and custom business logic error 533 start) client request parameter error (4xx start), set different status code
        error:error
      }
      if(status === 422){
        ctx.body.detail = err.errors;
      }
      ctx.status = 200}}}Copy the code
Using middleware

After the middleware is written, we also need to manually mount it. The following methods are supported:

In an application, we can load custom middleware and determine their order entirely through configuration.

If we need to load the above error_handler middleware, add the following configuration to config.default.js to complete the middleware startup and configuration:

// add your middleware config here
config.middleware = ['errorHandler'];
Copy the code

The plug-in

The plug-in mechanism is a major feature of our framework. It can not only ensure that the core of the framework is concise, stable and efficient, but also promote the reuse of business logic and the formation of the ecosystem. One might ask

  • Koa already has middleware mechanisms, why do they need plug-ins?
  • What are the relationships and differences between middleware, plug-ins, and applications?
  • How do I use a plugin?
  • How to write a plug-in?
  • .

Let’s talk about each of them

Why use plug-ins

We found the following issues in using Koa middleware:

  1. In fact, the middleware load order, but the middleware itself can not manage the order, only to the user. This is actually very unfriendly and can make all the difference if the order is out of order.
  2. Middleware is positioned to intercept user requests and do things around them, such as authentication, security checks, access logging, and so on. However, the reality is that some functions are independent of the request, such as scheduled tasks, message subscriptions, background logic, and so on.
  3. Some features have very complex initialization logic that needs to be done at application startup time. This is obviously not a good place to implement in middleware.

In summary, we need a more powerful mechanism to manage and orchestrate the relatively independent business logic.

Relationships between middleware, plug-ins, and applications

A plug-in is essentially a “mini app”, almost the same as an app:

  • It includes services, middleware, configuration, framework extensions, and so on.
  • It has no separate Router and Controller.
  • It has noplugin.js, can only declare dependencies with other plug-ins, andCan’t decideWhether to enable other plug-ins.

Their relationship is:

  • Applications can be imported directly into Koa’s middleware.
  • In the case of scheduled tasks, message subscription, and background logic, the application needs to introduce plug-ins.
  • The plug-in itself can contain middleware.
  • Multiple plug-ins can be wrapped as a single upper-layer framework.
The use of plug-in

The egg-Mongoose we used above is a plugin.

Plug-ins are typically reused as NPM modules:

npm i egg-validate -S
Copy the code

You then need to declare this in the config/plugin.js of your application or framework:

exports.validate = {
  enable: true.package: 'egg-validate'};Copy the code

You can directly use the functionality provided by the plug-in:

controller/user.js

'use strict';

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

class UserController extends Controller {
  constructor(props) {
    super(props);
    this.UserCreateRule = {
      username: {
        type: 'string'.required: true.allowEmpty: false.// The username must contain 3 to 10 letters, underscores (_), at signs (@), and cannot start with a number
        format: / ^ [A - Za - z_ @.] {3, 10} /
      },
      password: {
        type: 'password'.require: true.allowEmpty: false.min: 6}}}async create() {
    const {
      ctx,
      service
    } = this;
    // Check parameters
    ctx.validate(this.UserCreateRule)
    const payLoad = ctx.request.body || {};
    const res = await service.user.create(payLoad);
    this.ctx.helper.success({
      ctx: this.ctx, res }); }}module.exports = UserController;
Copy the code

Framework extension


The Helper functions are used to provide utility functions.

What it does is it allows you to separate common actions from helper.js into separate functions, so that you can write complex logic in JavaScript without the logic being scattered around. Another benefit is that Helper, a simple function, makes it easier to write test cases.

There are some common Helper functions built into the framework. We can also write custom Helper functions.

The framework merges the objects defined in app/extend/helper.js with the prototype object of the built-in helper and generates helper objects based on the extended Prototype when processing requests.

For example, add a helper.success() method:

extend/helper.js

module.exports = {
    success:function({res=null,msg='Request successful'}) {
        // This is a helper object from which other helper methods can be called
        // this.ctx =>context object
        // this.app => Application object
        this.ctx.body = {
            code:200.data:res,
            msg
        }
        this.ctx.status = 200; }}Copy the code

controller/user.js

async index() {
    const res = await this.service.user.index();
    this.ctx.helper.success({
        res
    });
}
Copy the code

Timing task


Although the HTTP Server we developed through the framework is request-response model, there are still many scenarios that require some scheduled tasks, such as:

  1. Report application status periodically. (Order time-out feedback, order details processing, etc.)
  2. Periodically update the local cache from the remote interface.
  3. Periodically cut files and delete temporary files.

The framework provides a mechanism to make the writing and maintenance of scheduled tasks more elegant

Writing a scheduled Task

All scheduled tasks are stored in the APP /schedule directory. Each file is an independent scheduled task. You can configure the attributes and execution methods of scheduled tasks.

As a simple example, we can create an update_cache.js file in the app/schedule directory by defining a scheduled task to update remote data to the in-memory cache

const Subscription = require('egg').Subscription;
class UpdateCache extends Subscription {
  // Use the schedule attribute to set the execution interval of scheduled tasks
  static get schedule() {
    return {
      interval: '5s'.// 1 minute interval
      type: 'all'.// specify that all workers need to be executed
    };
  }
  // subscribe is the function that is run when the actual scheduled task is executed
  async subscribe() {
    console.log("Task execution:" + new Date().toString());

    // const res = await this.ctx.curl('https://free-api.heweather.net/s6/weather/now?location=beijing&key=4693ff5ea653469f8bb0c29638035976', {
    // dataType: 'json',
    // })
    // this.ctx.app.cache = res.data;}}module.exports = UpdateCache;


Copy the code

Can be abbreviated

module.exports = {
schedule: {
    interval: '1m'.// 1 minute interval
    type: 'all'.// specify that all workers need to be executed
  },
  async task(ctx) {
    const res = await ctx.curl('https://free-api.heweather.net/s6/weather/now?location=beijing&key=4693ff5ea653469f8bb0c29638035976', {
      dataType: 'json'}); ctx.app.cache = res.data; }};Copy the code

This scheduled task will be executed every 1 minute on each Worker process to mount the remote data request back to app.cache.

Regular way

Timing tasks can be set to interval or CRon.

interval

Interval specifies the execution time of a scheduled task. The scheduled task will be executed at a specified interval. Interval can be configured as

  • A number in milliseconds, for example5000.
  • The character type will passmsConvert to milliseconds, for example5s.
Module. exports = {schedule: {// execute interval every 10 seconds:'10s',}};Copy the code
cron

The schedule.cron parameter is used to set the execution time of scheduled tasks. The scheduled tasks will be executed at a specific point in time based on the CRON expression. Cron expressions are parsed by cron-parser.

Note: Cron-Parser supports optional seconds (Linux crontab does not).

* * * * * * ┬ ┬ ┬ ┬ ┬ ┬ │ │ │ │ │ | │ │ │ │ │ └ day of week (0 to 7) (0 or 7 is Sun │ │ │ │ └ ─ ─ ─ ─ ─ the month (1-12) │ │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ day of the month (1-31) │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ the adrenaline-charged (0-23) │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ minute (0-59) └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ the second (0 to 59, optional)Copy the code
Module. exports = {schedule: {// run a crON every 3 hours:'0 0 */3 * * *',}};Copy the code
type

By default, the framework supports two types of scheduled tasks, worker and All. Both workers and all support the above two timing modes, but when it comes to execution time, different workers will execute the scheduled task:

  • workerType: Only one worker on each machine will perform the scheduled task, and the selection of the worker to perform the scheduled task is random.
  • allType: Each worker on each machine will perform this scheduled task.
The other parameters

In addition to the parameters just described, scheduled tasks also support these parameters:

  • cronOptionsFor details, see configuring the crON time zonecron-parserThe document
  • immediate: If this parameter is set to true, the scheduled task will be executed immediately after the application is started and ready.
  • disable: If this parameter is set to true, the scheduled task will not be started.
  • env: array. The scheduled task is started only in the specified environment.
Dynamically configure scheduled tasks

config/config.default.js

 config.cacheTick = {
    interval: '5s'.// 1 minute interval
    type: 'all'.// specify that all workers need to be executed
    immediate: true.// If this parameter is set to true, the scheduled task will be executed immediately after the application is started and ready
    // disable: true. // True indicates that the scheduled task will not be started
  };
Copy the code

schedule/update_cache.js

module.exports = app= > {
  return {
    schedule: app.config.cacheTick,
    async task(ctx) {
      console.log("Task execution:" + new Date().toString()); }}};Copy the code

Start the project and view the console output.

Like old tie, add the following, blog posts will be updated regularly