Author: Dong Dongzhang

Before reading this article, browse the official Eggjs example and learn about Koajs

start

The official example is to set up Hacker News manually.

When we see this page, don’t rush down to the tutorial. Start by thinking about how to implement this page and what techniques to use:

  1. Route processing. We need a character to handle acceptance/newsRequest, in addition to that, generally/Default home page, which means at least 2 urls.
  2. Page display. You can use templates or simply concatenate HTML elements yourself. Nodejs templates include Pug, EJS, Handlebarsjs and other templates.
  3. Number taking problem. One role processes the request and retrieves the returned data.
  4. Merge data. Combine the template with the retrieved data to display the final result.

MVC

There is a classic MVC design pattern on the server side to solve this kind of problem.

  1. Modal: Manages data and business logic. It is usually subdivided into service (business logic) and DAO (database management) layers.
  2. View: Layout and page presentation.
  3. Controller: Routes related requests to the corresponding Modal and View.

Let’s take Java Spring MVC as an example

@Controller
public class GreetingController {

	@GetMapping("/greeting")
	public String greeting(@RequestParam(name="ownerId", required=false, defaultValue="World") String ownerId, Model model) {
    String name = ownerService.findOwner(ownerId);
		model.addAttribute("name", name);
		return "greeting"; }}Copy the code

Template for the greeting. HTML


<body>
    <p th:text="'Hello, ' + ${name} + '! '" />
</body>
Copy the code
  1. First, annotate@ControllerDefines aGreetingControllerClass.
  2. @GetMapping("/greeting")To accept the/greetingAnd topublic String greetingProcessing, this is part of the Controller layer.
  3. String name = ownerService.findOwner(ownerId); model.addAttribute("name", name);Get data, which belongs to the Modal layer.
  4. return "greeting";Return the corresponding template (View layer) and combine it with the obtained data to form the final result.

With this experience in mind, let’s move on to Eggjs. We can follow the MVC architecture above to complete the example given.

Since there are actually two pages, one is /news and the other is /, we’ll start with the home page /.

Let’s define a Controller.

// app/controller/home.js

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

class HomeController extends Controller {
  async index() {
    this.ctx.body = 'Hello world'; }}module.exports = HomeController;
Copy the code

With CJS standard first introduced framework Controller, defined a HomeController class, and method index.

With the class defined, the instantiation phase is next.

If you are familiar with Koajs development, you will typically use the new keyword

const Koa = require('koa');
const app = new Koa();
Copy the code

If you are familiar with Java development, you will usually use annotations to instantiate. For example, the @AutoWired annotation is used to implement automatic instantiation for person.

public class Customer {
    @Autowired                               
    private Person person;                   
    private int type;
}
Copy the code

From the above example, it is very convenient to see that annotations can not only handle requests, but also instance objects.

ES7 has a similar concept decorator that works with reflect-metadata to achieve a similar effect, as is standard with the current Node framework.

However, Eggjs implements its own set of instance initialization rules instead of having you directly new an instance or using decorator methods:

It reads the current file, initializes an instance based on the filename, and binds to the built-in base object.

For example, app/controller/home.js above generates an instance of home. Because it is the Controller role, it is bound to the contoller built-in object. The Contoller object is also part of the built-in app object, more of which can be found here.

In general, basically all the instantiation objects are bound to the app and CTX two built-in objects, and access rules for this. (app | CTX). Type (controller | service…). . User-defined file name. The method name.

On the request side, Eggjs handles it with a Router object

// app/router.js
module.exports = app= > {
  const { router, controller } = app;
  router.get('/', controller.home.index);
};
Copy the code

The above code indicates that the router submits/requests to the index method of the home instance.

File directory rules are also placed by convention

├─ ├─ ├─ ├─ download.txt ├─ download.txt ├─ download.txtCopy the code

The app directory holds all of its associated subelement directories.

Now that we’re done with the front page, consider the /news list page.

List of pp.

Similarly, we define C in MVC first, and then deal with the remaining two roles.

With that in mind, we’ll create a List method of the NewsController class, and then add the /news handler to router.js, specifying the corresponding method as follows.

// app/controller/news.js
const Controller = require('egg').Controller;

class NewsController extends Controller {
  async list() {
    const dataList = {
      list: [{id: 1.title: 'this is news 1'.url: '/news/1' },
        { id: 2.title: 'this is news 2'.url: '/news/2'}};await this.ctx.render('news/list.tpl', dataList); }}module.exports = NewsController;
Copy the code

DataList is written to death and replaced with service. This.ctx. render(‘news/list.tpl’, dataList) here is the combination of template and data.

News /list. TPL belongs to View, and the full directory path should be app/view/news/list. TPL according to the naming conventions we know above


// app/router.js adds the /news request path, specifying the list object of the news object to handle
module.exports = app= > {
  const { router, controller } = app;
  router.get('/', controller.home.index);
  router.get('/news', controller.news.list);
};

Copy the code

Template rendering.

According to the MVC model, now that we have C, we’re left with M and V, and M is dead, so let’s deal with the View.

As mentioned earlier, nodeJS templates are Pug,Ejs, HandleBarsJS,Nunjucks, and many more.

Sometimes in a project you need to select a specific template from multiple templates, so you need the framework to do:

  1. Declare multiple template types.

  2. Configure a specific template.

For better management, declare and use should be separated. The configuration is usually placed in the config directory, so there are config/plugin.js and config/config.default.js. The former is defined, and the latter is configured.

// config/plugin.js declares two view templates
exports.nunjucks = {
  enable: true.package: 'egg-view-nunjucks'
};

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

// config/config.default.js A template is used for specific configurations.
exports.view = {
  defaultViewEngine: 'nunjucks'.mapping: {
    '.tpl': 'nunjucks',}};Copy the code

Then write a Nunjucks specific template with the specific content as follows

// app/view/news/list.tpl
<html>
  <head>
    <title>Hacker News</title>
    <link rel="stylesheet" href="/public/css/news.css" />
  </head>
  <body>
    <ul class="news-view view">
      {% for item in list %}
        <li class="item">
          <a href="{{ item.url }}">{{ item.title }}</a>
        </li>
      {% endfor %}
    </ul>
  </body>
</html>
Copy the code

Js file path as above, under service subdirectory of app directory.

// app/service/news.js 
const Service = require('egg').Service;

class NewsService extends Service {
  async list(page = 1) {
    // read config
    const { serverUrl, pageSize } = this.config.news;

    // use build-in http client to GET hacker-news api
    const { data: idList } = await this.ctx.curl(`${serverUrl}/topstories.json`, {
      data: {
        orderBy: '"$key"'.startAt: `"${pageSize * (page - 1)}"`.endAt: `"${pageSize * page - 1}"`,},dataType: 'json'});// parallel GET detail
    const newsList = await Promise.all(
      Object.keys(idList).map(key= > {
        const url = `${serverUrl}/item/${idList[key]}.json`;
        return this.ctx.curl(url, { dataType: 'json'}); }));return newsList.map(res= >res.data); }}module.exports = NewsService;
Copy the code

const { serverUrl, pageSize } = this.config.news; This line has two paging parameters. Where should you configure them?

Based on our experience above, config.default.js configures specific template usage parameters, so this is a good place to be.

// config/config.default.js
// Add the news configuration item
exports.news = {
  pageSize: 5.serverUrl: 'https://hacker-news.firebaseio.com/v0'};Copy the code

The service has changed the mode of fixed write dead data to dynamic fetch, and the corresponding changes are as follows


// app/controller/news.js
const Controller = require('egg').Controller;

class NewsController extends Controller {
  async list() {
    const ctx = this.ctx;
    const page = ctx.query.page || 1;
    const newsList = await ctx.service.news.list(page);
    await ctx.render('news/list.tpl', { list: newsList }); }}module.exports = NewsController;

Copy the code

Ctx.service.news.list (page), service is not bound to app like controller, but to CTX, which is intentional

At this point, we’re almost done with our entire page.

The directory structure

When we’re done, take a look at the full catalog specification

An egg - project ├ ─ ─ package. Json ├ ─ ─ app. Js (optional) ├ ─ ─ agent. The js (optional) ├ ─ ─ app | ├ ─ ─ the router. The js │ ├ ─ ─ controller │ | └ ─ ─ home. Js │ │ ├ ─ ─ service (optional) | └ ─ ─ the user. The js │ ├ ─ ─ middleware (optional) │ | └ ─ ─ response_time. Js │ ├ ─ ─ the schedule (optional) │ | └ ─ ─ my_task. Js │ │ ├ ─ ─ public (optional) | └ ─ ─ reset. CSS │ ├ ─ ─ the view (optional) │ | └ ─ ─ home. TPL │ └ ─ ─ the extend (optional) │ ├ ─ ─ helper. Js (optional) │ ├ ─ ─ Request. Js (optional) │ ├ ─ ─ the response. The js (optional) │ ├ ─ ─ the context, js (optional) │ ├ ─ ─ application. Js (optional) │ └ ─ ─ agent. The js (optional) ├ ─ ─ the config | ├ ─ ─ plugin. Js | ├ ─ ─ config. The default. The js │ ├ ─ ─ config. Prod. Js | ├ ─ ─ config. The test. The js (optional) | ├ ─ ─ config. Local, js (optional) | └ ─ ─ Config. Unittest. Js (optional) └ ─ ─ the test ├ ─ ─ middleware | └ ─ ─ response_time. Test. The js └ ─ ─ controller └ ─ ─ home. Test. JsCopy the code

When I first saw this, I was a little confused about why there was app directory, agent. Js and app.js, what was the schedule directory, and what was all the stuff under the config directory.

Let’s start with the config directory. Plugin.js defines plug-ins as mentioned earlier.

What on earth is the following pile of config.xxx.js?

Let’s take a look at the normal WebPack configuration, which typically has three files.

Scripts ├ ─ ─ webpack.com mon. Js ├ ─ ─ webpack. Dev. Js └ ─ ─ webpack. Prod. JsCopy the code

In both webpack.dev.js and webpack.prod.js, we manually merge webpack.common.js with webpack-merge.

Eggjs automatically merges config.default.js, which can be confusing at first. For example, if your environment is prod, config.prod.js automatically merges config.default.js.

The environment is specified by EGG_SERVER_ENV=prod NPM start. For more details, see Configuration

Router.js, Controller, Service, View, etc. The middleware directory houses Koajs middleware. The extend directory is an extension to native objects. Some of our common methods are in the util.js file, in this case helper.js.

Next, let’s talk about the relationship among app.js, Agent.js and APP /schedule.

When developing locally, only one instance will be created, usually with Node app.js. However, when we deploy, we usually have more than one, usually managed by Pm2, such as pm2 start app.js. One instance corresponds to one process.

Eggjs implements a set of multi-process management, including Master, Agent, and Worker roles.

Master: Quantity 1, stable performance, no specific work, responsible for the other two management work, similar to PM2.

Agent: Quantity 1, stable performance, some back-end work, such as long connection to listen to back-end configuration, and then do some notification.

Worker: Inconsistent performance, multiple (default number of cores), business code running on this.

Then app.js (including app directory) above is running under the worker process, there will be more than one. Agent.js runs under the Agent process.

Take my computer MacBook Pro (13-inch, M1, 2020) as an example. This computer has 8 cores, so there are basically 8 worker processes, one agent and one master process.

You can see it more clearly in the picture below. You can see 8app_worker.js, aagent_work.jsAnd a master process

What is schedule? Here is the worker process performing scheduled tasks.

// app/schedule/force_refresh.js
exports.schedule = {
  interval: '10m'.type: 'all'.// All worker processes will be executed
};
exports.schedule = {
  interval: '10s'.type: 'worker'.// Only one worker on each machine will perform the scheduled task, and the worker performing the scheduled task each time is random.
};
Copy the code

Schedule and agent.js can determine which one to use according to their own needs.

The above is a simple analysis of Eggjs multiple processes, which can be seen here

The plug-in

If you are now asked to design a plug-in system, plug-ins need to have dependencies, environmental judgment, and a switch to control the start of the plug-in, how to design?

The first thing that comes to mind is dependency processing, which is a very mature front end for dependency management with the help of NPM.

For other parameters such as environment judgments, you can refer to third-party libraries such as Browserslist to add a field configuration to package.json, or create a special.xxxxrc configuration.

/ / package. Json notation
{
  "private": true."dependencies": {
    "autoprefixer": "^ 6.5.4"
  },
  "browserslist": [
    "last 1 version"."1%" >."IE 10"]}Copy the code
//.browserslistrc

# Browsers that we support
last 1 version
> 1%
IE 10 # sorry
Copy the code

From this, we can define our own configuration as follows

//package.json
{
	myplugin: {env:"dev".others:"xxx"}}Copy the code

Eggjs’s plug-in is also designed this way

{
  "eggPlugin": {
    "env": [ "local"."test"."unittest"."prod"]}}Copy the code

However, Eggjs handles dependency management and names itself, which makes them seem redundant.

//package.json
{
  "eggPlugin": {
    "name": "rpc"."dependencies": [ "registry"]."optionalDependencies": [ "vip"]."env": [ "local"."test"."unittest"."prod"]}}Copy the code

All of this is written in the eggPlugin configuration, including the plug-in name, dependencies, etc., rather than taking advantage of package.json’s existing fields and capabilities. This is where it was confusing at first.

The official explanation:

First, the Egg plugin supports not only NPM packages, but also finding plug-ins through directories

Plugins can now be managed in a monorepo way using Yarn workspace and Lerna.

Take a look at the plugin’s catalog and content. It’s a simplified version of the app.

. An egg - hello ├ ─ ─ package. Json ├ ─ ─ app. Js (optional) ├ ─ ─ agent. The js (optional) ├ ─ ─ app │ ├ ─ ─ the extend (optional) │ | ├ ─ ─ helper. Js (optional) │ | ├ ─ ─ Request. Js (optional) │ | ├ ─ ─ the response. The js (optional) │ | ├ ─ ─ the context, js (optional) │ | ├ ─ ─ application. Js (optional) │ | └ ─ ─ agent. The js (optional) │ ├ ─ ─ Service (optional) │ └ ─ ─ middleware (optional) │ └ ─ ─ mw. Js ├ ─ ─ the config | ├ ─ ─ config. The default. The js │ ├ ─ ─ config. Prod. Js | ├ ─ ─ Config. Test. Js (optional) | ├ ─ ─ config. Local, js (optional) | └ ─ ─ config. The unittest, js (optional) └ ─ ─ the test └ ─ ─ middleware └ ─ ─ mw. Test. JsCopy the code
  1. Router and Controller are removed. This section mentioned earlier mainly handles requests and forwards, and the definition of a plug-in is enhanced middleware, so it is not necessary.
  2. Plugin.js has been removed. The main purpose of this file is to introduce or enable other plug-ins. The framework has already done that, so it’s not necessary here.

Since the plug-in is a small application, Eggjs is loaded in the order of plug-in < framework < application because there is duplication between the plug-in and the framework.

For example, plug-ins have config.default.js, frameworks have config.default.js, and applications have config.default.js.

In the end, a config.default.js file is merged, and executed in the following order

letFinalConfig = objeact. assign(plugin config, framework config, application config)Copy the code

conclusion

The emergence and framework design of Eggjs have their own characteristics and factors of The Times. This article, as an introductory interpretation, hopes to help people better grasp this framework.

This article is published from netease Cloud Music big front end team, the article is prohibited to be reproduced in any form without authorization. Grp.music – Fe (at) Corp.Netease.com We recruit front-end, iOS and Android all year long. If you are ready to change your job and you like cloud music, join us!