Hello, I’m Joon Eren. This article may be more applicable to large or medium projects with multiple people working together, but it is also applicable if your project has a messy code file for some reason.

It’s been 12 years since Ryan Dahl created NodeJS in 2009, and there are many mature frameworks like Express, KOA, Egg, and more. When writing server-side code, there are usually router, Controller, and Service layers, as well as some other layers, such as model and View.

  • Router: Listens for the route on the page and calls the route handler on the Controller layer
  • Controller: Provides services to the Router layer and invokes data processing provided by the Service layer
  • Service: Implements specific functions

The router is like the front desk, which refers to the entrance store according to customers’ needs. A controller is like a waitress. It is her job to take customers’ orders. A service is a chef who decides whether the food tastes good or not.

As mentioned above, the author built router, Controllr and Service folders in the server and stored the codes of each business module in them. This method is simple and quick, which is suitable for the quick start of the project. Later, the team gradually increased to four or five people working on the project.

Let’s take a look at the overall structure of the project

As you can see, the application app contains controller, Service, and Router. It is a typical single application. Then expand the Controller directory to see

Discover that there are many different modules (or features?) It’s going to converge in there.

Let me briefly summarize the problem above:

First of all, there was no strict module division in this project, and each team member created directories and files according to their own understanding. As time passed, the structure was chaotic, and it smelled rotten. The controller and service files of a module are in a large directory respectively, and the development of a module will span multiple directories of the whole project, resulting in poor development experience.

Secondly, there is no distinction between public code and business code, so it is difficult for developers to find and call some common logic, and the cost of learning is high for newcomers.

Every developer has different work habits and understands modules differently. New developers may not be aware of naming conventions for file directories, and they may not be aware of the separation of public and business content when writing utility classes.

Norms that are passed on by word of mouth cannot be fully enforced, which is what we call “tribal knowledge” (knowledge unique to an organization). If the developer is not limited to creating chaos, it may end up requiring the entire project to be restructured.

The above problems ultimately lie in unclear system architecture, lack of module division or unclear module division, low convergence and high coupling.

Imagine if we were to divide the project into modules, and when we developed a module, we could do everything in its directory. Wouldn’t it be better if bad code appeared and you only had to refactor the module directory instead of the entire project?

Good design

Microservices and domain-driven design are popular topics, but front-end services tend to be less complex. If the service is divided into multiple services, not only need to invest a lot of labor costs, but also may find that each service is relatively “thin”, in many cases, the slightly larger front-end service is “insufficient but not more than”.

In the practice of domain-driven design in Internet business development, details how microservices are broken down. The “divide and conquer” idea mentioned in it may be helpful to improve the maintainability of front-end services.

Divide-and-conquer divides the problem space into smaller and more manageable sub-problems. Partitioned problems need to be small enough that one person alone can solve them; Second, you must consider how to assemble the divided parts into a whole. The better the segmentation, the easier it is to understand and the less detail you need to track when assembling the whole. It’s easier to design how the parts work together. What is good divide-and-conquer is high cohesion and low coupling.

Therefore, it is important to design business modules and their relationships based on business requirements. As the system gets bigger and bigger, each module becomes more and more like a micro application. In multi-team collaboration, each person is responsible for their own modules, and it would be interesting if each module was a mini-application where everyone could develop all the functions.

If a business changes so frequently that the code rots, you simply refactor that module, with no effect on the other modules.

An egg framework

The author’s project uses the egg.js framework, which is one of its design principles

Develop according to convention, follow “convention over configuration”

An Egg can automatically grab files from router, Controller, Service, Middleware, and other directories to mount methods on specific context variables.

The default directory structure looks like this:

This is a large unit application structure, not easy to partition modules. How to divide the single application into multiple micro applications by means of engineering?

One nice feature of Egg is the ability to customize the upper-layer framework. See the framework development section on the website for more details

Implementation of microapplications

To realize the need for a single application to be divided into multiple micro-applications, you need to use the framework inheritance and custom loader capabilities provided by Egg.

Framework inherit

First, you need to create an NPM application, which contains the following files and code

// package.json { "name": "my-framework", "dependencies": { "egg": // index.js module.exports = require('./lib/framework.js'); // lib/framework.js const path = require('path'); const egg = require('egg'); const EGG_PATH = Symbol.for('egg#eggPath'); Class Application extends egg.application {get [EGG_PATH]() {return path.dirname(__dirname); } // Override Application module.exports = object.assign (Egg, {Application,});Copy the code

It is then imported in our service application via package.json

// package.json
{
  "scripts": {
    "dev": "egg-bin dev"
  },
  "egg": {
    "framework": "my-framework"
  }
}
Copy the code

This would implement a framework inheritance, but it still doesn’t implement the functionality we need. We can take advantage of inherited features to override functionality provided by existing Eggs.

Custom loader

Egg automatically grabs files from directories because of its Loader. If you want to automatically fetch the directories under the module, you need to customize the file loading functionality of the Egg.

You can inherit the Loader class and override its methods to achieve custom file loading.

// lib/framework.js const path = require('path'); const egg = require('egg'); const EGG_PATH = Symbol.for('egg#eggPath'); + class MyAppWorkerLoader extends egg.AppWorkerLoader {// What is AppWorkerLoader? + load() { + super.load(); Application {get [EGG_PATH]() {// Returns the framework path return path.dirname(__dirname); } get [EGG_LOADER]() {return YadanAppWorkerLoader; }} // Apply module.exports = Object. Assign (Egg, {Application, + // custom Loader also needs export, The framework needs to be based on this extension + AppWorkerLoader: MyAppWorkerLoader,});Copy the code

AppWorkerLoader () : AppWorkerLoader () : AppWorkerLoader () : AppWorkerLoader ()

  • LoadPlugin () : Loads the plug-in
  • LoadConfig () : loads the configuration
  • LoadAgentExtend () : The extend object that loads the Agent object
  • LoadApplicationExtend () : Loads the extend object of the app object
  • LoadRequestExtend (): Loads the Request object
  • LoadResponseExtend () : Loads the Response object
  • LoadContextExtend (): loads the context object
  • LoadHelperExtend (): Loads the tool method
  • LoadCustomAgent (): loads the defined Agent object
  • LoadCustomApp (): Loads the defined app object
  • LoadService () : load the service
  • LoadMiddleware (): Loads middleware
  • LoadController () : load controller
  • LoadRouter (): loads routing files

MyAppWorkerLoader -> AppWorkerLoader -> Loader -> MyAppWorkerLoader -> MyAppWorkerLoader

Custom controller loading

The loadController method of the Loader class implements fetching the Controller directory, which can be found in the egg-core package.

// egg-core/lib/loader/mixin/controller.js const path = require('path'); const is = require('is-type-of'); Module.exports = {/** * Load app/controller * @param {Object} opt-loaderOptions * @since 1.0.0 */ loadController(opt) {opt = object. assign({caseStyle: 'lower', // Convert the first letter of the filename to directory: Path. join(this.options.baseDir, 'app/controller'), // Initializer: If (is.function(obj) &&! is.generatorFunction(obj) && ! is.class(obj) && ! Is.asyncfunction (obj)) {// Check whether controller is a function obj = obj(this.app); Prototype. PathName = opt.pathName; // If (is.class(obj)) {// If (is.class(obj)) { obj.prototype.fullPath = opt.path; return wrapClass(obj); } if (is.object(obj)) {return wrapObject(obj, opt.path); Support generatorFunction for forward compatbility if (is.generatorFunction(obj))  || is.asyncFunction(obj)) { return wrapObject({ 'module.exports': obj }, opt.path)['module.exports']; } return obj; }, }, opt); const controllerBase = opt.directory; this.loadToApp(controllerBase, 'controller', opt); // Hang the controller exported object in the app context object},};Copy the code

As you can see from the code above, you can define the directory where the controller resides by passing in the opt.directory parameter

// lib/framework.js const path = require('path'); const egg = require('egg'); const EGG_PATH = Symbol.for('egg#eggPath'); class MyAppWorkerLoader extends egg.AppWorkerLoader { + loadController(opt) { + super.loadController(Object.assign({ // Call the loadController method of the parent class + directory: [+...['your/controller/path'], // Custom controller path + path.resolve(process.cwd(), 'app/controller'), Override: false; // Override: false; // Override: false; +} class Application extends egg.application {get [EGG_PATH]() {return path.dirname(__dirname); } get [EGG_LOADER]() {return YadanAppWorkerLoader; }} // Apply module.exports = Object. Assign (Egg, {Application, // custom Loader also needs export, The upper framework needs to base this extension on AppWorkerLoader: MyAppWorkerLoader,});Copy the code

With this code, you can extend the Controller directory.

We can also extend the Router, Service, Middleware, and extend directories. If you want to know how to do this, you can refer to the packaged NPM package egg-micro-app. The core content is in the file lib/framework.js and less than 100 lines of code is very simple.

In this way, large projects can be broken down into multiple independent modules, each module is a micro application, and you can put business modules and common files in different locations according to your own ideas.