preface

ExpressJS is a concise and flexible Node.js Web application framework that provides a series of powerful features to help you create a variety of Web applications. Express moderately encapsulates the HTTP module and routing provided by NodeJS, and adds middleware functions, which is sufficient for most project development. The author also uses ExpressJS as the basic framework. After several projects, I want to adjust the traditional project structure in combination with the project team members and some characteristics of the project. I hope to divide the directory according to different business modules, each module directory can have independent controller, service, model, static, etc. The main purpose is also to let developers pay more attention to specific business, some chores should be handled by the framework.

This paper adopts the current mainstream framework and module to practice, the underlying framework uses Express, ORM uses SequelizeJS, log module Log4js, template engine Nunjucks. This article will not introduce the use of the above modules and the specific code implementation of the framework, just how to combine these framework modules to develop a set of their own framework.

Why encapsulate

In terms of realizing business requirements, no encapsulation is also possible. The main purposes of encapsulation are as follows:

  1. It is hoped that even those who have never touched Nodejs and other framework modules can develop quickly to achieve basic business requirements, and those who are familiar with javascript can complete front-end and back-end development tasks without background experience.
  2. According to the characteristics of team projects, more abstractions and reuse are made, and internal development norms are established to bring convenience to team development and improve development efficiency.
  3. It is convenient to expand and upgrade. If a function module at the bottom needs to be upgraded, the written business code will not be affected. Even if the underlying module is replaced, it can run normally as long as the method called is consistent.
  4. Through encapsulation practice, I can learn more about the underlying framework and usage, and purely learn and improve my design ability

How to design

Before packaging, consider how to use finally, namely the developer of the project structure should look like, what needs to be done, etc., and determines the framework for the development of norms and constraints of design, if you want to realize each module independently developed, each module has its own control layer and service layer, should need a moduels directory to hold all the directory, Usually do a WEB project is the most common features routing definition, view rendering, data operation and business logic to handle a few sports, in this I used the traditional way of MVC layered, in order to root more concise, I consider using app as the root directory, the module in the app directory, you also need to have a configuration directory, Used to distinguish configurations in different environments, the final design directory is as follows:

The directory structure

├ ─ app │ | - modules / / modules directory │ │ | ─ module_A / / business module A │ │ │ | ─ CTRLS / / controller directory │ │ │ | └ ─ controller1. Js │ │ │ | ─ views / / the view template directory │ │ │ | └ ─ index. The HTML │ │ │ | ─ [static] static files such as │ │ │ | ─ [models] data processing files │ │ │ | ─ [servs] service layer business processing │ │ │ └ ─ [the router. Js] module routing configuration file │ │ | ─ ─ module_B module B │ | ─ / models / / ORM model definition (top, Said gm) │ | ─ [routes] / / general router configuration │ | ─ / views / / general view template | | - [bridge] / / bridge file directory │ └ ─ [CTRLS] / / general controller files ├ ─ config - > │ environment configuration directory | ─ default. Js the default configuration file │ | ─ [development. Js] / / development environment configuration file │ | ─ [production. Js] / / production environment configuration text │ └ ─ [testing. Js] / / test environment configuration └ ─ Run.js // Start the file

The modules directory is used to store each module. Each module can write its own business code. Of course, you can’t force developers to use modules in all cases. So even if there is no modules module, we can directly write some code that is common or does not need to be divided into modules to the top level, that is, the app directory. Now from the perspective of project structure, we have decided on a development specification. The framework carries out routing and dynamic loading of modules according to the above directory structure, with square brackets indicating optional.

With the prototype structure in place, we can begin to encapsulate. Let’s start with routing. Let’s look at the definition of routing in Express:

// From official documentation
var express = require('express');
var app = express();

app.get('/'.function(req, res) {
  res.send('hello world');
});
Copy the code

This is a very simple example. First we need to import Express, then we need to define an HTTP request method GET, POST, etc. Normally we would write the route in a separate file, and then import it when we start the project. However, require(‘express’) is required no matter how many routing files are written. If the handle to the route (called controller) is also written to the file, then we need to introduce the controller file in the routing file. Here is an example:

Home.js (callback function handle – controller)

module.exports = {
    home: function(req, res){
        res.send('home');
    },
    
    about: function(req, res){
        res.send('about'); }}Copy the code

Main_router.js (routing file)

const express = require('express');
 // Express.Router is used to create a modular, mountable route
const router = express.Router();
// Import the controller
const homeController = require('controller/home')

// Match the request for the root path
app.get('/', homeController.home);
// Match /about the request for the path
app.get('/about', homeController.about );
Copy the code

app.js

// Load the routing module in the application:
var express = require('express');
var app = express();
var router = require('./main_router');

app.use('/', router); .Copy the code

The role of the routing file is how to define the application of the endpoint (URIs,), and how to respond to the client’s request, we think about it, in fact, several key point is to define the request method and specify the callback function, so can make a routing configuration file is ok, for implementation framework and controller are introduced to a framework, For example, write the routing file above like this:

//router.js
exports.routers = [
    { prefix: ["/"."/index.html"].ctrl: "home".action: "home" },
    { prefix: "/about".ctrl: "home".action: "about"}]Copy the code
  • Prefix Specifies the route path
  • CTRL specifies the controller to execute after the route is hit
  • Action specifies the default method to execute, corresponding to the method name specified in the CTRL controller
  • [method] Specifies the request method, such as GET, POST, and PUT. The default value is GET

When the framework starts, the router.js module should automatically load the controller file based on the CTRL parameter and inject the Action function into the Express route. After configuration, there is no need to import the routing file into the application. Developers only need to write the controller file and implement the business logic.

Since module routing is developed by module, generally, route addresses are differentiated by module. Therefore, if the route is under the module, the framework considers automatically adding the module directory name as a prefix to the URI. For example, the route under the module name user needs to be added with /user/.. Make an interview.

Of course, we also need to consider allowing the user to define the prefix name, so an additional parameter can be added to specify the prefix:

//router.js
// Specify the module alias. The first argument is the alias and the second is the module name
exports.url_prefix = ["a"."user"]...Copy the code

After aliases are added, all the routes in the module can be accessed through aliases. For example, to access the upper routers, you need to add aliases:

  • http://… /a/index.html
  • http://… /a/about

Of course, a simple configuration like this is not enough. For example, to implement a filter, which is a very common function, consider adding a new node filter to the configuration item, as follows:

//router.js
exports.routers = [
    { prefix: "/about".ctrl: "home".action: "about".filter: "login_required"}]// Define the filter
exports.filters = {
    // Define a filter name
    "login_required": {
        //prefix specifies a mount, and if it is *, all routes under the module are applied
        prefix: "mount".// Handler is the handle to execute
        handler:  function(req, res, next){
            if(req.session.loginUser == undefined ){
                res.redirect('/user/login.html')}elsenext(); }}}Copy the code

When accessing /about, the framework first executes the filter login_required method, which of course can be written in a separate file. In addition to the filter, the framework also realized the multi-controller processing and other functions, the implementation of the code is not explained in detail here, you can refer to the source code or sample source code

Data layer ORM encapsulation

The data layer uses Sequelize as the basic framework. To use ORM, you need to define the data model first.

/ / introduce sequelize
const Sequelize = require('sequelize');
// Define a model
const User = sequelize.define('user', {
  firstName: {
    type: Sequelize.STRING
  },
  lastName: {
    type: Sequelize.STRING
  }
});
// Export method
exports.addUser = function(userName, email) {
    Insert data into the user table
    returnUser.create(...) ; }// Find the user by username
exports.findByName = function(userName) {
    returnUser.findOne(...) ; }Copy the code

I want developers to declare the data model as a class, so I’m going to wrap it like this so the framework can define the model as a class:

class Users {
    constructor() {super(a)this.tableName = "user"
        this.fields = {
            firstName: {type: "string(11)"},
            lastName: {type: "string(11)"}}}// Add a method to get a list of users
	addUser (userName, email) {
        return this.create(...) ; }// Find the user by username
    findByName (){
        return this.findOne(...);
    }
}
Copy the code

In this way, the Sequelize method is omitted. The purpose of this method is for non-NodeJS developers to pay too much attention to the use of Sequelize and only focus on how to implement the business. Another point is that if the database framework is changed or upgraded, the model can be changed without affecting the business. Since the encapsulation is in class form, can we define a base class for each model, so that we can attach some basic methods to the model, such as:

class Users extends APP.DB.DBModel{... }Copy the code

App.db. DBModel is a base class provided by the framework that provides basic or extended methods, such as formatting and converting data. Of course, what if you want to use some native methods after encapsulating the methods of your class? We can consider making the class a proxy, exposing the native methods through reflection:

// Invoke the Sequelize native method by proxy
var cProxy = new Proxy(class.{
    get: function(target, key, receiver) {
        if (target[key] == undefined) {
            // Target. ORM is a sequelize object
            return target.ORM[key];
        }
        returntarget[key]; }});Copy the code

Once you have defined the model, you can use it in the code that needs to use the data model. Example code:

// Import the model
const models = require("./models")
var m_user = models.Users;
let u = await m_user.getone({where: {"firstName": "xx"}})
console.log(u.firstName)
Copy the code

The bridge file

Bridge files are mainly used by developers to write additional generic business code, and also used by the framework itself to load execution, such as adding method variables to the template engine, adding some generic middleware, etc. Look at an example for a template engine:

Templateext.js is specifically used to customize template files
module.exports.tpl  = {
    //init initialization method, which will be executed automatically
    init (app){
        // Get the template object
        let tpl = app.get('tpl');
        // Add a date filter
        tpl.addFilter('formatTimestamp'.function(t, f="yyyy-MM-dd HH:mm:ss"){
            return new Date(t*1000).pattern(f) }) ... }}//comm.js generic file
module.exports.comm = { methodA (){ ... }}Copy the code

Anywhere in the code, we can access members of the bridge file using the global APP variable provided by the framework. For example, to access the methodA method in comm.js, we can use the app.m.methoda form.

The configuration file

Configuration files are mainly used to allow developers to specify configuration parameters required in different environments, such as specifying database and log information. Usually, we use a JSON file to define the configuration, and configure node parameters in JSON for different environments, such as:

//config.js
{
    'development':...'testing':...'production':... }Copy the code

Here, I consider using the class to define it in a way that makes it look more clear and independent. Finally, when deploying it, I only need the corresponding configuration file:

//config.js
class Config{
    constructor() {// Whether to enable debugging mode
        this.debug = false;
        // Database connection configuration
        this.database = {... };// Custom variables
        this.BaseUrl = "/"
        // Configure log information
        this.log4js = { ... }}}module.exports = Config;

//development.js
var Config = require('./config');
class Development extends Config{
    constructor() {super(a);this.debug = true; }}module.exports = Development;
Copy the code

Start the file run.js

The startup file is relatively simple, many are done by the framework, so the startup file only needs to introduce the framework, start the service can be:

//run.js starts the file
var em = require('express-moduledev');
var config = {
    // Specify a port, default port 8000
    "port":801.// Use the environment 'default','development','production','testing'
    "use_env": "development",}// Start the service
em.Run(config)
Copy the code

At this point, what kind of framework to present to the user is clear, as long as the framework to implement these functions.

Frame structure

Based on the requirements defined above, we know what the core functions of the framework should be, and we can probably draw a brain map as follows:

  • Main file index.js: This file contains the web service startup, configuration file loading, core module initialization, etc., similar to the main entry for Express application development
  • Router module router.js: used for loading and processing routes, automatic loading and related controller files, filter and middleware processing
  • Base file base.js: This file provides load management for the controller and exports a global object APP that makes it easy to load bridge files in projects using various functional interfaces provided by the framework
  • The database db.js handles database-related matters, parses user-defined models, encapsulates the underlying ORM, and provides the most basic methods
  • The logging module log.js provides a logging API for developers to handle logging related matters
  • This file is mainly for internal use of the framework, and contains some general judgment functions, etc

The framework source code

Attach a framework source code, welcome to browse improvement:) https://github.com/rob668/express-moduledev