current position:Home>Poor maintainability of front-end services? Practice sharing of minimum cost dismantling of eggs

Poor maintainability of front-end services? Practice sharing of minimum cost dismantling of eggs

2021-08-27 08:21:56 Cao Jun_ Eren

Hello everyone , I'm Cao Jun _Eren. This article may be more suitable for medium and large-scale projects developed by multiple people , Of course, if your project has a messy code file for various reasons , Also applicable .

Ryan Dahl since 2009 It's a year that creates nodejs It has been 12 year , There are already many mature frameworks in the industry , such as express、koa、egg etc. . When writing server-side code , It is often divided into router layer 、controller Layer and the service layer , Of course, there will be some other layers , such as model Layer and the view layer .

  • router: Listen to the routing of the page , And call Controller Layer routing processing function
  • controller: to Router Layer provides services , call Service Data processing provided by layer
  • service: To achieve specific functions

router Like the front desk , Guide customers into the store according to their needs ;controller Like a waiter , It's her job to order for customers ;service It's the cook , It's up to him to decide whether the food tastes good or not .

As mentioned above , The author has built... In the server router、controllr、service Three folders , Store the codes of each business module in it , This way is simple and quick , It is very suitable for the rapid start of the project . Later, the number of developers in the team slowly increased , Close to have 4~5 Personal development of this project .

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

image.png

You can see the application app There are controller、service and router, It is a typical monomer application . And then unfold controller The table of contents can be seen

image.png

Found that there are many different modules ( Or function ?) Gather in it .

Let me briefly summarize the problem in the figure above :

First , This project There is no strict module division , Each team member creates directories and files according to their own understanding , Over time, the structure is chaotic , Has smelled the smell of decay . A modular controller、service The files are in a large directory , When developing a module, you need to span multiple directories of the whole project , Bad development experience .

secondly , There is no distinction between public code and business code , Developers want to find 、 It's difficult to call some general logic , Newcomers have high learning costs .

Every developer has different working habits , Different understanding of module division . New developers may not be aware of the naming conventions for file directories , When writing tool classes, you may not notice the separation of public content and business content .

Word of mouth norms cannot be fully implemented , That's what we call “ Tribal knowledge ”( Unique knowledge in an organization ). If the confusion caused by developers is not limited to a certain range , Finally, you may need to refactor the entire project .

The above problems , In the final analysis The system architecture is not clear , Lack of module division or unclear module division , Low degree of aggregation 、 High coupling .

Just imagine , If we divide the project into multiple modules , When developing a module , All functions can be completed in its directory . If bad code appears , Just refactor the directory where the module is located , Without having to refactor the entire project , Wouldn't it be better ?

Good design

Microservices 、 Domain driven design is a very popular topic , But the complexity of front-end services is often not high . If split into multiple services , Not only need Invest a lot of manpower cost , You may also find that each service is more “ Thin ”, Most of the time, a slightly larger front-end service is “ It's more than enough ”.

stay The practice of Domain Driven Design in Internet business development Explain in detail how microservices are split , Mentioned in “ Divide and conquer ” The idea may be helpful to improve the maintainability of front-end Services .

Divide and conquer   Divide the problem space into smaller and easier to deal with subproblems . The problem after segmentation needs to be small enough , So that one person alone can solve them ; secondly , It is necessary to consider how to assemble the divided parts as a whole . The more reasonable the division, the easier it is to understand , When assembled as a whole , The less detail you need to track . That is, it's easier to design collaborative ways of various parts . Judge what is good division , High cohesion and low coupling .

Therefore, according to business needs , It is very important to design business modules and the relationship between them . As the system gets bigger and bigger , Each module will be closer to one Micro applications . When many people work together , Each person in charge will have their own module , If each module is a micro application , Everyone can complete the development of all functions in the module , It's a very interesting thing .

image.png

If a business causes code corruption due to frequent changes , Just refactor the module , No impact on other modules .

egg frame

The author's project uses egg.js frame , This is one of its design principles

Develop as agreed , Pursue 『 Convention over configuration 』

egg It can automatically grab router 、controller、service、middleware And other files in the directory , Mount the method in a specific context variable .

The default directory structure is like this :

image.png

This is a structure for large monomer applications , It's hard to divide modules . How to use engineering means , The single application is divided into multiple micro applications ?

egg A good function is to customize the upper framework , For details, please refer to the chapter on the official website Framework development , Let's take a closer look at

Implementation of micro application

To realize the requirement of dividing a single application into multiple micro applications , Need to be used egg Provided Framework inheritance and Custom loader The function of .

Framework inheritance

First you need to create a npm application , The project contains the following files and code

// package.json
{
  "name": "my-framework",
  "dependencies": {
    "egg": "^2.0.0"
  }
}

// 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  framework  route 
    return path.dirname(__dirname);
  }
}

//  covers  Egg  Of  Application
module.exports = Object.assign(egg, {
  Application,
});
 Copy code 

Then in our service application , adopt package.json introduce

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

In this way, the inheritance of a framework can be realized , But it still doesn't realize the functions we need . We can take advantage of inherited features , rewrite existing egg Features provided .

Custom loader

egg The reason why you can automatically grab the files in the directory , Because of it loader (Loader). If you want to automatically grab the directory under the module , You need to customize egg File loading function .

By inheritance Loader class , Rewrite its method , To realize 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 { // AppWorkerLoader  What is it? ?
+   load() {
+     super.load();
+     //  Expand yourself 
+   }
+ }

class Application extends egg.Application {
  get [EGG_PATH]() {
    //  return  framework  route 
    return path.dirname(__dirname);
  }
  //  Cover  Egg  Of  Loader, Use this at startup  Loader
  get [EGG_LOADER]() {
    return YadanAppWorkerLoader;
  }
}

//  covers  Egg  Of  Application
module.exports = Object.assign(egg, {
  Application,
+   //  Self defined  Loader  Also needed  export, The upper framework needs to be based on this extension 
+   AppWorkerLoader: MyAppWorkerLoader,
});
 Copy code 

In code AppWorkerLoader Inherited from Loader Base class , On the official website loader The chapter says Loader There are the following methods :

  • loadPlugin(): Add plug-ins
  • loadConfig(): Load the configuration
  • loadAgentExtend(): load agent Object's extend object
  • loadApplicationExtend(): load app Object's extend object
  • loadRequestExtend(): load request object
  • loadResponseExtend(): load response object
  • loadContextExtend(): load context object
  • loadHelperExtend(): Loading tool method
  • loadCustomAgent(): Load defined agent object
  • loadCustomApp(): Load defined app object
  • loadService(): load service
  • loadMiddleware(): Loading middleware
  • loadController(): load controller
  • loadRouter(): Load the routing file

From the perspective of inheritance MyAppWorkerLoader -> AppWorkerLoader -> Loader It seems , We can be in MyAppWorkerLoader Customize all the above methods in

customized controller Loading

Loader Class loadController Method implemented controller Directory crawling , stay egg-core The bag can find it .

// 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 file name 
      directory: path.join(this.options.baseDir, 'app/controller'), // controller The position of 
      initializer: (obj, opt) => { //  For each file  export  The value is processed 
        if (is.function(obj) && !is.generatorFunction(obj) && !is.class(obj) && !is.asyncFunction(obj)) { //  Judge controller Is it a function 
          obj = obj(this.app); //  Pass in app Object to controller Function 
        }
        if (is.class(obj)) { //  Judge controller Is it a class 
          obj.prototype.pathName = opt.pathName;
          obj.prototype.fullPath = opt.path;
          return wrapClass(obj);  //  It mainly defines the execution in controller Ahead middleware
        }
        if (is.object(obj)) {
          return wrapObject(obj, opt.path); //  It mainly defines the execution in controller Ahead middleware
        }
        // support generatorFunction for forward compatbility
        if (is.generatorFunction(obj) || is.asyncFunction(obj)) {
          return wrapObject({ 'module.exports': obj }, opt.path)['module.exports']; //  It mainly defines the execution in controller Ahead middleware
        }
        return obj;
      },
    }, opt);
    const controllerBase = opt.directory;

    this.loadToApp(controllerBase, 'controller', opt); //  take controller The exported object is hung in app In the context object 
  },
};
 Copy code 

As can be seen from the code above , By passing in parameters opt.directory Can define controller Directory of

// 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({ //  Calling the loadController Method 
+      directory: [
+        ...['your/controller/path'],  //  Self defined controller route 
+        path.resolve(process.cwd(), 'app/controller'),  // egg Of controller The default path  
+      ],
+      override: false, //  Whether to directly overwrite or throw an exception when encountering an existing file 
+    }, opt));
+  }

class Application extends egg.Application {
  get [EGG_PATH]() {
    //  return  framework  route 
    return path.dirname(__dirname);
  }
  //  Cover  Egg  Of  Loader, Use this at startup  Loader
  get [EGG_LOADER]() {
    return YadanAppWorkerLoader;
  }
}

//  covers  Egg  Of  Application
module.exports = Object.assign(egg, {
  Application,
   //  Self defined  Loader  Also needed  export, The upper framework needs to be based on this extension 
   AppWorkerLoader: MyAppWorkerLoader,
});
 Copy code 

Through the above code, you can realize controller Directory extension .

Besides , We can also expand router、service、middleware、extend Catalog , Students who want to understand the specific implementation , You can refer to the encapsulated npm package egg-micro-app . The core content is in the document lib/framework.js in , The code is less than 100 OK, it's very simple .

In this way, large projects can be disassembled into multiple independent modules , Each module is a micro application , You can put business modules and general files in different locations according to your own ideas , End of the flower .

image.png

copyright notice
author[Cao Jun_ Eren],Please bring the original link to reprint, thank you.
https://en.qdmana.com/2021/08/20210827082149950n.html

Random recommended