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