[egg] egg learning notes

Posted by Drannon on Wed, 09 Feb 2022 11:10:40 +0100

preface

  • I've been busy recently. Four projects have been completed in parallel, and the learning plan has been delayed. I'll catch fish and learn egg.

Official website

  • https://eggjs.org/zh-cn/intro/

install

  • use:
npx egg-init --type=ts packageName
  • You can also create a new directory and install it using the domestic image template:
npm init egg --type=simple --registry=china

route

  • The mapping relationship of route definition is as follows:
router.verb('path-match', controllerAction)
  • Where verb is generally the lowercase of HTTP verb, for example:
HEAD - router.head
OPTIONS - router.options
GET - router.get
PUT - router.put
POST - router.post
PATCH - router.patch
DELETE - router.delete or router.del
 In addition, there is a special verb router.redirect Indicates redirection.
  • controllerAction is to specify a specific function in a file under the controller directory by clicking, for example:
controller.home.index // Map to controller / home index method of JS file
controller.v1.user.create // controller/v1/user. create method of JS file
  • Here are some examples and explanations:
module.exports = app => {
  const { router, controller } = app
  // When the user accesses news, it will be handed over to the controller / news JS
  router.get('/news', controller.news.index)
  // Use the colon `: X 'to capture the named parameter x in the URL and put it into CTX params. x
  router.get('/user/:id/:name', controller.user.info)
  // Capture the grouping parameters in the URL through custom regularization and put them into CTX In params
  router.get(/^\/package\/([\w-.]+\/[\w-.]+)$/, controller.package.detail)
 
  app.router.get('index', '/home/index', app.controller.home.index)
  app.router.redirect('/', '/home/index', 302)
 
}

CRUD routing

  • In addition to the syntax of generating a route by using the verb "CREG", the following syntax is also provided:
 // Map posts to controller / posts in RESTful style JS
router.resources('posts', '/posts', controller.posts)
  • The following routes will be generated automatically:
HTTP method	Request path	Route name	Controller function
GET	/posts	posts	app.controller.posts.index
GET	/posts/new	new_post	app.controller.posts.new
GET	/posts/:id	post	app.controller.posts.show
GET	/posts/:id/edit	edit_post	app.controller.posts.edit
POST	/posts	posts	app.controller.posts.create
PATCH	/posts/:id	post	app.controller.posts.update
DELETE	/posts/:id	post	app.controller.posts.destroy
  • You only need to implement the corresponding method in the controller.

Route splitting

  • If there are too many routes and you want to split them by file, there are several ways:
  • Manual split:
// app/router.js
module.exports = app => {
  require('./router/news')(app)
  require('./router/admin')(app)
};

// app/router/news.js
module.exports = app => {
  app.router.get('/news/list', app.controller.news.list)
  app.router.get('/news/detail', app.controller.news.detail)
};

// app/router/admin.js
module.exports = app => {
  app.router.get('/admin/user', app.controller.admin.user)
  app.router.get('/admin/log', app.controller.admin.log)
};
  • 2. Use the egg router plus plug-in to automatically import app / router / * * / * JS and provides the namespace function:
// app/router.js
module.exports = app => {
  const subRouter = app.router.namespace('/sub')
  subRouter.get('/test', app.controller.sub.test) // The final path is / sub/test
}

middleware

  • koa packages all look like this:
async function gzip(ctx, next) {
  // Pre code
  await next()
  // Post code
}
  • egg stipulates that a middleware is a separate file placed in the app/middleware directory. It needs to export a common function, which accepts two parameters:

  • options: configuration item of middleware. The framework will add app Config [${middlewarename}] is passed in.

  • app: the instance of the current Application.

  • Try to build a middleware in Middleware / slow JS, write the following contents:

module.exports = (options, app) => {
  return async function (ctx, next) {
    const startTime = Date.now() // Record start time
    await next()
    const consume = Date.now() - startTime // Total recording time
    const { threshold = 0 } = options || {}
    if (consume > threshold) { // Print the log if the time exceeds the specified threshold
      console.log(`${ctx.url}Request time ${consume}millisecond`)
    }
  }
}
  • In config default. Add the middleware to JS:
	config.middleware = ["slow"];
  • The middleware configured here is enabled globally. If you only want to use the middleware in the specified route, for example, if you only use a middleware for url requests starting with / api prefix, there are two ways:

  • In config default. Set match or ignore attribute in JS configuration:

module.exports = {
  middleware: [ 'slow' ],
  slow: {
    threshold: 1,
    match: '/api'
  },
};
  • If config is written like this:
	config.slow = {
		threshold: 1,
		match: "/api",
	};
  • Or add in the route: (note that the middleware needs to be removed in config.default, so app.config.appMiddleware should not have this middleware)
module.exports = app => {
  const { router, controller } = app
  // Add any middleware before controller processing
  router.get('/api/home', app.middleware.slow({ threshold: 1 }), controller.home.index)
}
  • You can see the middleware of the framework through the app
  console.log(app.config.appMiddleware)
  console.log(app.config.coreMiddleware)
  • coreMiddleware is the default. If you don't need it, you can turn it off:
module.exports = {
  i18n: {
    enable: false
  }
}

controller

  • The simple controller is as follows:
const { Controller } = require('egg')
class HomeController extends Controller {
  async index() {
    const { ctx } = this
    ctx.body = 'hi, egg'
  }
}
module.exports = HomeController
  • In the Controller, click this CTX can obtain context objects to facilitate the acquisition and setting of relevant parameters, such as:
ctx.query: URL Request parameters in (ignoring duplicates) key)
ctx.quries: URL Request parameters in (duplicate) key (put into array)
ctx.params: Router Named parameters on
ctx.request.body: HTTP Content in request body
ctx.request.files: File object uploaded from the front end
ctx.getFileStream(): Get uploaded file stream
ctx.multipart(): obtain multipart/form-data data
ctx.cookies: Reading and setting cookie
ctx.session: Reading and setting session
ctx.service.xxx: Get specified service Instance of object (lazy load)
ctx.status: Set status code
ctx.body: Set response body
ctx.set: Set response header
ctx.redirect(url): redirect
ctx.render(template): Render template

Service

  • The Controller can call any method on any Service. It is worth noting that the Service is lazy loaded, that is, the framework will instantiate it only when it is accessed.
  • service case:
// 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;
  • Call in 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;
  }
}
  • Note that the Service file must be placed in the app/service directory, which supports multi-level directories. When accessing, you can cascade access through directory names:
app/service/biz/user.js => ctx.service.biz.user
app/service/sync_user.js => ctx.service.syncUser
app/service/HackerNews.js => ctx.service.hackerNews
  • The functions in service can be understood as the smallest unit of a specific business logic. Other services can also be called in service. It is worth noting that service is not a single instance, but a request level object. The framework accesses CTX for the first time in each request service. XX delay and late instantiation are inherited from egg Service, each service instance will have the following properties:
this.ctx: Context of the current request Context Object instance
this.app: Current application Application Object instance
this.service: Equivalent to this.ctx.service
this.config: Application runtime configuration items
this.logger: logger Object, which has four methods( debug,info,warn,error),Each represents printing four different levels of logs

Template rendering

  • The egg framework has built-in egg view as a template solution and supports a variety of template rendering, such as ejs, handlebars, nunjunks and other template engines. Each template engine is introduced in the form of plug-ins. By default, all plug-ins will find the files in the app/view directory, and then according to config \ config default. JS to select different template engines:
config.view = {
  defaultExtension: '.nj',
  defaultViewEngine: 'nunjucks',
  mapping: {
    '.nj': 'nunjucks',
    '.hbs': 'handlebars',
    '.ejs': 'ejs',
  },
}

The above configuration indicates that when the file:

The suffix is .nj When using nunjunks template engine
 The suffix is .hbs When using handlebars template engine
 The suffix is .ejs When using ejs template engine
 When no suffix is specified, the default is .html
 The default is when no template engine is specified nunjunks

Next, install the template engine plug-in:

$ npm i egg-view-nunjucks egg-view-ejs egg-view-handlebars --save
  • Then in config / plugin JS to enable the plug-in:
exports.nunjucks = {
  enable: true,
  package: 'egg-view-nunjucks',
}
exports.handlebars = {
  enable: true,
  package: 'egg-view-handlebars',
}
exports.ejs = {
  enable: true,
  package: 'egg-view-ejs',
}
  • Add the following files under app/view:
app/view
├── ejs.ejs
├── handlebars.hbs
└── nunjunks.nj
<!-- ejs.ejs File code -->
<h1>ejs</h1>
<ul>
  <% items.forEach(function(item){ %>
    <li><%= item.title %></li>
  <% }); %>
</ul>
      
<!-- handlebars.hbs File code -->
<h1>handlebars</h1>
{{#each items}}
  <li>{{title}}</li>
{{~/each}}
    
<!-- nunjunks.nj File code -->
<h1>nunjunks</h1>
<ul>
{% for item in items %}
  <li>{{ item.title }}</li>
{% endfor %}
</ul>
  • Configure in router:
module.exports = app => {
  const { router, controller } = app
  router.get('/ejs', controller.home.ejs)
  router.get('/handlebars', controller.home.handlebars)
  router.get('/nunjunks', controller.home.nunjunks)
}
  • The controller returns:
const Controller = require('egg').Controller

class HomeController extends Controller {
  async ejs() {
    const { ctx } = this
    const items = await ctx.service.view.getItems()
    await ctx.render('ejs.ejs', {items})
  }

  async handlebars() {
    const { ctx } = this
    const items = await ctx.service.view.getItems()
    await ctx.render('handlebars.hbs', {items})
  }

  async nunjunks() {
    const { ctx } = this
    const items = await ctx.service.view.getItems()
    await ctx.render('nunjunks.nj', {items})
  }
}

module.exports = HomeController
  • Put data into service
const { Service } = require('egg')

class ViewService extends Service {
  getItems() {
    return [
      { title: 'foo', id: 1 },
      { title: 'bar', id: 2 },
    ]
  }
}

module.exports = ViewService
  • Egg view extends the context by adding render, renderView and renderString methods to the ctx context object.

Integrating mysql

  • Here's a brief note. You need to check the document again.
  • Install plug-ins
$ npm i egg-mysql
  • In config / plugin JS to open the plug-in:
exports.mysql = {
  enable: true,
  package: 'egg-mysql',
}
  • In config / config default. JS to define connection parameters:
config.mysql = {
  client: {
    host: 'localhost',
    port: '3306',
    user: 'root',
    password: 'root',
    database: 'cms',
  }
}
  • You can get the mysql object:
class UserService extends Service {
  async find(uid) {
    const user = await this.app.mysql.get('users', { id: 11 });
    return { user }
  }
}
  • If mysql starts with an error, it may be a password problem. Here's how to solve it:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password'
flush privileges
  • Integrate sequenize
npm install egg-sequelize --save 
  • Then in config / plugin Open the egg serialize plug-in in JS:
exports.sequelize = {
  enable: true,
  package: 'egg-sequelize',
}
  • Also in config / config default. JS
config.sequelize = {
  dialect: 'mysql',
  host: '127.0.0.1',
  port: 3306,
  database: 'example',
}
  • Then in egg_ Create books table in example library:
CREATE TABLE `books` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'primary key',
  `name` varchar(30) DEFAULT NULL COMMENT 'book name',
  `created_at` datetime DEFAULT NULL COMMENT 'created time',
  `updated_at` datetime DEFAULT NULL COMMENT 'updated time',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='book';
  • Create model / book JS file, the code is:
module.exports = app => {
  const { STRING, INTEGER } = app.Sequelize
  const Book = app.model.define('book', {
    id: { type: INTEGER, primaryKey: true, autoIncrement: true },
    name: STRING(30),
  })
  return Book
}

Add / book.controller JS controller:

const Controller = require('egg').Controller

class BookController extends Controller {
  async index() {
    const ctx = this.ctx
    ctx.body = await ctx.model.Book.findAll({})
  }

  async show() {
    const ctx = this.ctx
    ctx.body = await ctx.model.Book.findByPk(+ctx.params.id)
  }

  async create() {
    const ctx = this.ctx
    ctx.body = await ctx.model.Book.create(ctx.request.body)
  }

  async update() {
    const ctx = this.ctx
    const book = await ctx.model.Book.findByPk(+ctx.params.id)
    if (!book) return (ctx.status = 404)
    await book.update(ctx.request.body)
    ctx.body = book
  }

  async destroy() {
    const ctx = this.ctx
    const book = await ctx.model.Book.findByPk(+ctx.params.id)
    if (!book) return (ctx.status = 404)
    await book.destroy()
    ctx.body = book
  }
}

module.exports = BookController
  • Finally, configure RESTful routing mapping:
module.exports = app => {
  const {router, controller} = app
  router.resources('books', '/books', controller.book)
}
  • Using mysql and mongoose at the same time seems to have a hole, and the configuration needs to be changed: https://github.com/eggjs/egg/issues/805

Scheduled task

  • The egg framework provides the function of scheduled tasks. In the app/schedule directory, each file is an independent scheduled task. You can configure the properties of the scheduled task and the methods to be executed, such as creating an update_cache.js update cache task, executed every minute:
const Subscription = require('egg').Subscription

class UpdateCache extends Subscription {
  // Use the schedule attribute to set the execution interval and other configurations of scheduled tasks
  static get schedule() {
    return {
      interval: '1m', // 1 minute interval
      type: 'all', // Specify that all worker s need to execute
    }
  }

  // subscribe is a function that is run when a real scheduled task is executed
  async subscribe() {
    const res = await this.ctx.curl('http://www.api.com/cache', {
      dataType: 'json',
    })
    this.ctx.app.cache = res.data
  }
}

module.exports = UpdateCache
*    *    *    *    *    *
┬    ┬    ┬    ┬    ┬    ┬
│    │    │    │    │    |
│    │    │    │    │    └ day of week (0 - 7) (0 or 7 is Sun)
│    │    │    │    └───── month (1 - 12)
│    │    │    └────────── day of month (1 - 31)
│    │    └─────────────── hour (0 - 23)
│    └──────────────────── minute (0 - 59)
└───────────────────────── second (0 - 59, optional)
  • There are two types of tasks:

  • Worker type: only one worker will execute this scheduled task (randomly selected)

  • all type: each worker will execute this scheduled task

error handling

  • If our project is separated from the front end and the back end, and all returns are in JSON format, it can be found in config / plugin JS is configured as follows:
module.exports = {
  onerror: {
    accepts: () => 'json',
  },
};
  • Then the error call stack will be returned directly in JSON format:
{
    "message": "Cannot read property 'find' of undefined",
    "stack": "TypeError: Cannot read property 'find' of undefined\n    at UserController.index (/Users/keliq/code/egg-project/app/controller/user.js:7:37)",
    "name": "TypeError",
    "status": 500
}
  • html can also be returned
module.exports = {
  onerror: {
      accepts: (ctx) => {
        if (ctx.get('content-type') === 'application/json') return 'json';
        return 'html';
      }
  },
};
  • Custom error page:
  • In config / config default. Custom error in JS:
module.exports = {
  onerror: {
    errorPageUrl: '/public/error.html',
  },
};
  • 404 is not handled as an exception.
  • Many factories write 404 pages by themselves. If you also have this requirement, you can write an HTML by yourself and then in config / config default. JS specifies:
module.exports = {
  notfound: {
    pageUrl: '/404.html',
  }
}

life cycle

class AppBootHook {
  constructor(app) {
    this.app = app
  }

  configWillLoad() {
    // The config file has been read and merged, but it has not yet taken effect. This is the last time for the application layer to modify the configuration
    // Note: this function only supports synchronous calls
  }

  configDidLoad() {
    // All configurations have been loaded and can be used to load application customization files and start customized services
  }

  async didLoad() {
    // All configurations have been loaded and can be used to load application customization files and start customized services
  }

  async willReady() {
    // All plug-ins have been started, but the whole application is not ready yet
    // You can do some operations such as data initialization. The application will not start until these operations are successful
  }

  async didReady() {
    // The app has started
  }

  async serverDidReady() {
    // The http / https server has started to accept external requests
    // At this point, you can use the app Server gets an instance of the server
  }

  async beforeClose() {
    // App will close soon
  }
}

module.exports = AppBootHook

Topics: egg