Before reading this article, please browse Eggjs official example and understand Koajs
Author of this article: Dongdong Chapter
Start
The official gave an example of manually building Hacker News.
When we see this page, don't worry about reading the tutorial. First think about how to implement this page and what technologies are needed:
- Routing processing. We need a role to handle the
/news
. In addition, there is/
default homepage of 061a86df379bd7, which means at least 2 URLs. - Page display. You can use templates here, or you can directly splice HTML elements by yourself. The nodejs template has Pug , EJS , Handlebarsjs and many other templates.
- Access problem. There is a role that processes the request and gets the returned data.
- Consolidate data. Combine the template with the acquired data to display the final result.
MVC
On the server side, there is a classic MVC design pattern to solve this type of problem.
- Modal: Manage data and business logic. Usually subdivided into two layers: service (business logic) and dao (database management).
- View: Layout and page display.
- Controller: Route related requests to the corresponding Modal and View.
Take Java Spring MVC
as an example below
@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";
}
}
Template greeting.html
<body>
<p th:text="'Hello, ' + ${name} + '!'" />
</body>
- First with annotation
@Controller
defines aGreetingController
class. @GetMapping("/greeting")
accepted/greeting
and handed it topublic String greeting
processing, which belongs to the Controller layer.String name = ownerService.findOwner(ownerId);model.addAttribute("name", name);
acquires data, belonging to the Modal layer.return "greeting";
returns the corresponding template (View layer), and then combines with the obtained data to form the final result.
With the above experience, then we will turn our attention to Eggjs. We can complete the examples given based on the above MVC architecture.
Because there are actually two pages, one is /news
and the other is /
, we start with the home page /
.
First 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;
Use the CJS standard to introduce the Controller of the framework first, define a HomeController
, and have a method index
.
The class has been defined, the next step is the instantiation phase.
If you are familiar with the development of Koajs, you will generally use the new keyword
const Koa = require('koa');
const app = new Koa();
If you are familiar with Java development, you usually use annotations to instantiate. For example, the person below uses the @Autowired
to achieve automatic instantiation.
public class Customer {
@Autowired
private Person person;
private int type;
}
From the above example, it is found that annotations can not only process requests, but also instantiate objects, which is very convenient.
ES7 also has a similar concept decorator Decorators , and then cooperate with reflect-metadata achieve similar effects, which is also the standard method of the current Node framework.
However, for various reasons, Eggjs did not let you directly new an instance, and did not use the decorator method, but implemented a set of instance initialization rules by itself:
It will read the current file, then initialize an instance based on the file name, and finally bind to the built-in base object.
For example, app/controller/home.js
above will generate a home instance. Because it is the Controller role, it will be bound to the built-in object contoller. At the same time, the contoller object is also part of the built-in app object. For more built-in objects, see here .
In general, basically all instantiated objects are bound to the two built-in objects app and ctx, and the access rule is this.(app|ctx).type(controller|service...).self Defined file name. Method name.
In terms of requests, Eggjs uses a router object to process
// app/router.js
module.exports = app => {
const { router, controller } = app;
router.get('/', controller.home.index);
};
The above code means that the router passes the / request to the index method of the home instance for processing.
The file directory rules are also placed in accordance with the convention
egg-example
├── app
│ ├── controller
│ │ └── home.js
│ └── router.js
├── config
│ └── config.default.js
└── package.json
The app directory contains all the sub-element directories related to it.
At this point, we have completed the work on the home page, and then consider the /news list page.
List
In the same way, we first define C in MVC, and then deal with the remaining two roles.
With the above experience, we first create a list method of the NewsController class, and then add the processing of /news in router.js, and assign it to 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;
The data dataList is written to death first, and then replaced with service.this.ctx.render('news/list.tpl', dataList)
Here is the combination of template and data.
news/list.tpl
belongs to view. According to the naming convention we know above, the full directory path should be app/view/news/list.tpl
// app/router.js 添加了/news请求路径,指定news对象的list对象处理
module.exports = app => {
const { router, controller } = app;
router.get('/', controller.home.index);
router.get('/news', controller.news.list);
};
Template rendering.
According to the MVC model, now we have C, and the rest is M and V. The data of M is hard-coded, and the View is processed first.
I said before, there are nodejs template Pug , Ejs , handlebarsjs , Nunjucks other.
Sometimes in a project, it is necessary to choose a specific one from multiple templates according to the situation, so the framework needs to be:
- Declare multiple template types.
- The configuration specifically uses a certain template.
For better management, the declaration and use should be separated, and the configuration is generally placed in the config directory, so there are config/plugin.js
and config/config.default.js
. The former is defined, and the latter is specifically configured.
// config/plugin.js 声明了2个view模板
exports.nunjucks = {
enable: true,
package: 'egg-view-nunjucks'
};
exports.ejs = {
enable: true,
package: 'egg-view-ejs',
};
// config/config.default.js 具体配置使用某个模板。
exports.view = {
defaultViewEngine: 'nunjucks',
mapping: {
'.tpl': 'nunjucks',
},
};
Then write a nunjucks
. The specific content is 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>
The service is processed below, named news.js file path reference above, placed under the subdirectory service of the 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;
There are 2 paging parameters in const { serverUrl, pageSize } = this.config.news;
According to our above experience, config.default.js
configured with specific template usage parameters, so here is a more suitable place.
// config/config.default.js
// 添加 news 的配置项
exports.news = {
pageSize: 5,
serverUrl: 'https://hacker-news.firebaseio.com/v0',
};
The service is there, now the fixed hard-coded data is changed to the dynamic fetching mode, and the corresponding modification is 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;
In this line ctx.service.news.list(page)
, you can find that the service is not bound to the app like the controller, but to the ctx. This is intentional. For details, see Discussion
At this point, we have basically completed our entire page.
Directory Structure
When we finish the above work, take a look at the complete catalog specification
egg-project
├── package.json
├── app.js (可选)
├── agent.js (可选)
├── app
| ├── router.js
│ ├── controller
│ | └── home.js
│ ├── service (可选)
│ | └── user.js
│ ├── middleware (可选)
│ | └── response_time.js
│ ├── schedule (可选)
│ | └── my_task.js
│ ├── public (可选)
│ | └── reset.css
│ ├── view (可选)
│ | └── home.tpl
│ └── extend (可选)
│ ├── helper.js (可选)
│ ├── request.js (可选)
│ ├── response.js (可选)
│ ├── context.js (可选)
│ ├── application.js (可选)
│ └── agent.js (可选)
├── config
| ├── plugin.js
| ├── config.default.js
│ ├── config.prod.js
| ├── config.test.js (可选)
| ├── config.local.js (可选)
| └── config.unittest.js (可选)
└── test
├── middleware
| └── response_time.test.js
└── controller
└── home.test.js
When I saw this for the first time, I would be a bit confused, why there is an app directory, and also agent.js and app.js, what is the schedule directory, and what are a lot of things under the config directory.
Let me talk about the config directory first,
plugin.js said before that it defines plugins.
What the hell is the following bunch of config.xxx.js?
Let's first look at the configuration of ordinary webpack, there are generally three files.
scripts
├── webpack.common.js
├── webpack.dev.js
└── webpack.prod.js
In webpack.dev.js and webpack.prod.js inside, we Merge-by WebPACK manual merger webpack.common.js.
In Eggjs which will automatically merger config.default.js, which really annoying in the beginning, such as the time when you prod environment, config.prod.js will automatically merge config.default.js.
The environment is EGG_SERVER_ENV=prod npm start
, for more description, see configuration
The router.js, controller, service, view and other directories under the app directory are already clear. The middleware directory contains Koajs middleware. The extend directory is native object . Some of our commonly used methods are generally placed in util.js In the file, this corresponds to helper.js.
Next, let’s talk about the relationship between app.js, agent.js and app/schedule.
When we are in the local development stage, generally only one instance will be started, usually with node app.js
.
But when we deploy, there are usually more than one, usually managed by pm2, such as pm2 start app.js
. One instance corresponds to one process.
Eggjs itself has implemented a set of multi-process management methods, which have three roles: Master, Agent, and Worker.
Master: Number 1, stable performance, no specific work, responsible for the management of the other two, similar to pm2.
Agent: Quantity 1, stable performance, some back-end work, such as long connection monitoring back-end configuration, and then make some notifications.
Worker: The performance is unstable, the number is multiple (the default number of cores), the business code runs on this.
The above app.js (including the app directory) is running under the worker process, there will be multiple.
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 basically there will be 8 worker processes, one agent and one master process.
The picture below can be seen more clearly, you can see that there are 8 app_worker.js
, one agent_work.js
, and one master process
So what is schedule? Here is the worker process to perform timing tasks.
// app/schedule/force_refresh.js
exports.schedule = {
interval: '10m',
type: 'all', // 所有worker进程,8个都会执行
};
exports.schedule = {
interval: '10s',
type: 'worker', // 每台机器上只有一个 worker 会执行定时任务,每次执行定时任务的 worker 随机。
};
Schedule and agent.js determine which one to use according to their needs.
The above is Eggjs
multi-process, you can see here
Plug-in
If you are now asked to design a plug-in system that requires a dependency relationship between plug-ins, an environmental judgment, and a switch to control the startup of the plug-in, how to design it?
The first thing we think of is dependency processing. This front-end is already very mature and can rely on npm for dependency management.
In addition, for some parameters such as environment judgment, you can refer to third-party libraries such as browserslist, add a field configuration in package.json, or create a new .xxxxrc configuration.
//package.json 写法
{
"private": true,
"dependencies": {
"autoprefixer": "^6.5.4"
},
"browserslist": [
"last 1 version",
"> 1%",
"IE 10"
]
}
//.browserslistrc
# Browsers that we support
last 1 version
> 1%
IE 10 # sorry
From this, we can define our own configuration as follows
//package.json
{
myplugin:{
env:"dev",
others:"xxx"
}
}
The Eggjs plugin is also designed like this
{
"eggPlugin": {
"env": [ "local", "test", "unittest", "prod" ]
}
}
However, Eggjs handles its own names for dependency management, which makes it seem more redundant.
//package.json
{
"eggPlugin": {
"name": "rpc",
"dependencies": [ "registry" ],
"optionalDependencies": [ "vip" ],
"env": [ "local", "test", "unittest", "prod" ]
}
}
All of them are written in the configuration of eggPlugin, including the plug-in name, dependencies, etc., instead of using the existing fields and capabilities of package.json. This is also the place that is more confusing at the beginning.
The official explanation is:
First of all, the Egg plug-in not only supports npm packages, but also supports
Now you can manage plugins better through yarn's workspace and lerna's monorepo method.
Take a look at the directory and content of the plug-in, it is actually a simplified version of the application.
. egg-hello
├── package.json
├── app.js (可选)
├── agent.js (可选)
├── app
│ ├── extend (可选)
│ | ├── helper.js (可选)
│ | ├── request.js (可选)
│ | ├── response.js (可选)
│ | ├── context.js (可选)
│ | ├── application.js (可选)
│ | └── agent.js (可选)
│ ├── service (可选)
│ └── middleware (可选)
│ └── mw.js
├── config
| ├── config.default.js
│ ├── config.prod.js
| ├── config.test.js (可选)
| ├── config.local.js (可选)
| └── config.unittest.js (可选)
└── test
└── middleware
└── mw.test.js
- Removed the router and controller. This part said before that it mainly processes requests and forwards them, and the definition of plug-ins is enhanced middleware, so it is not necessary.
- Removed plugin.js. The main function of this file is to introduce or open other plug-ins. The framework has already done this part of the work, so there is no need here.
Since the plug-in is a small application, because there will be duplication in the plug-in and the framework, the loading order of is 161a86df37a7e9 plugin<frame<application .
For example, the plugin has a config.default.js, the framework also has config.default.js, and the application also has config.default.js.
Finally, it will be merged into a config.default.js, the execution order is
let finalConfig= Objeact.assign(插件的config,框架的config,应用的config)
Summarize
The emergence of Eggjs and the framework design have its own characteristics and factors of the times.
This article serves as an interpretation of the introduction, hoping to help you master this framework better.
This article was published from big front-end team of NetEase Cloud Music . Any unauthorized reprinting of the article is prohibited. We recruit front-end, iOS, and Android all year round. If you are ready to change jobs and you happen to like cloud music, then join us at grp.music-fe(at)corp.netease.com!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。