Now as a front-end, will you be developing for the end? Do you need back-end development?

O (╥ man ╥) o… Then I encountered such a need, and then can only rush duck! Duck! Duck!

Technology stack

  • Framework: express

  • Database ORM: Sequelize, mysql2

  • Dependency injection: awilix

  • Routing plug-in: Awilix-Express

The project structure

| - express - backend | - SRC | - | API / / controller API documents - config / / project configuration directory | - container / / DI container | | -- - daos / / the dao layer The initialize / / project initialization file | - middleware / / | - models / / database middleware model | | -- - services / / service layer utils / / tools | - related directory App.js // project entry fileCopy the code

Building project Foundation

  1. Initialize the project
npm init
Copy the code
  1. Install dependencies
npm i express sequelize mysql2 awilix awilix-express
Copy the code

configuration

configurationBabel

Because Awilix and Awilix-Express use ES6 class and decorator syntax, @babel/plugin-proposal-class-properties and @babel/plugin-proposal-decorators need to convert

  • Install dependencies
npm install --save-dev @babel/core @babel/cli @babel/preset-env
Copy the code
npm install --save-dev @babel/node
Copy the code
npm install --save-dev @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators
Copy the code
  • configurationbabel
{
  "presets": [["@babel/preset-env",
      {
        "useBuiltIns": false."targets": {
          "node": "current"}}]],"plugins": [["@babel/plugin-proposal-decorators",
      {
        "legacy": true}], ["@babel/plugin-proposal-class-properties",
      {
        "loose": true}}]]Copy the code

Hot update

Hot updates are required during development, and here we are using Nodemon

  1. Install dependencies
npm install --save-dev nodemon
Copy the code
  1. Add in the project root directorynodemon.json
{
  "ignore": [
    ".git"."node_modules/**/node_modules"."package-lock.json"."npm-debug.log*"]},Copy the code

Ignore represents the part to be ignored, that is, the project will not restart if this part of the file changes, whereas code other than ignore will restart the project if it changes.

  1. Add a command

Here we define the startup command in package.json:

"scripts": {
  "dev": "cross-env NODE_ENV=development nodemon ./src/app.js --exec babel-node"
},
Copy the code

Environment configuration

In practice, we often deal with some sensitive data information, such as database connection user name and password, secret of third-party SDK, etc. Configuration information for these parameters is best kept out of git repositories. First, in the development environment, different developers have different local development configurations, which do not depend on git repository configurations. Second, the entry of sensitive data into the repository increases the risk of artificial leakage of configuration data. Any developer with access to git repository can obtain the secret key of the production environment from it. Once maliciously used, the consequences are unimaginable.

Therefore, you can introduce a.gitignore. Env file to record the required configurable environment parameters in the system in key-value mode. An example configuration file for.env.example is used to place placeholders, and.env.example can safely enter git repositories.

Create a local. Env. example file as a configuration template with the following contents:

HOST = 127.0.0.1 PORT = 3000Copy the code

read.envThe configuration in

Node.js can read the. Env configuration file through the env2 plug-in, and the loaded environment configuration parameters can be read through such as process.env.

npm i env2
Copy the code
require('env2') ('./.env')
Copy the code

Then in the configuration directory:

// config/index.js
const { env } = process;

export default {
  PORT: env.PORT,
  HOST: env.HOST,
};
Copy the code

The code is introduced

The database

Back-end development often involves adding, deleting, changing, and querying databases, using sequelize and mysqL2

1. Define database servicesmodel

We continue to create a series of models in the Models directory to correspond to the database table structure:

├ ─ ─ models# database model│ ├ ─ ─ index. Js# Model entry and connection│ ├ ─ ─ goods. JsTable # goods│ ├ ─ ─ shop. Js# stores table
Copy the code

Take the store table as an example to define the data model shop:

/* * Create store model */

 // models/shop.js
import Sequelize from 'sequelize';

export default function (sequelize, DataTypes) {
  class Shop extends Sequelize.Model {}
  Shop.init(
    {
      id: {
        type: DataTypes.INTEGER,
        primaryKey: true.autoIncrement: true,},name: {
        type: DataTypes.STRING,
        allowNull: false,},thumbUrl: {
        type: DataTypes.STRING,
        field: 'thumb_url',},createdDate: {
        type: DataTypes.DATE,
        defaultValue: DataTypes.NOW,
        field: 'created_date',
      },
    },
    {
      sequelize,
      modelName: 'shop'.tableName: 't_shop'});return Shop;
}
Copy the code

Then, in models/index.js, import all models in the modes directory:

// models/index.js
import fs from 'fs';
import path from 'path';
import Sequelize from 'sequelize';

const db = {};

export function initModel(sequelize) {
  fs.readdirSync(__dirname)
    .filter(
      (file) = >
        file.indexOf('. ')! = =- 1 &&
        file.slice(- 3) = = ='.js'&& file ! = ='index.js'
    )
    .forEach((file) = > {
      const model = sequelize.import(path.join(__dirname, file));
      db[model.name] = model;
    });
  Object.keys(db).forEach((moduleName) = > {
    if(db[moduleName].associate) { db[moduleName].associate(db); }}); db.sequelize = sequelize; db.Sequelize = Sequelize; }export default db;
Copy the code

2. SequelizeThe connectionMySQLThe database

Sequelize Connects to the database using new Sequelize (database, username, password, options). Options is the configuration option. For details, see the official manual.

Js file in config directory to add database configuration:

// config/config.js
const env2 = require('env2')

if (process.env.NODE_ENV === 'production') {
  env2('./.env.prod')}else {
  env2('./.env')}const { env } = process

module.exports = {
  development: {
    username: env.MYSQL_USER,
    password: env.MYSQL_PASSWORD,
    database: env.MYSQL_DATABSAE,
    host: env.MYSQL_HOST,
    port: env.MYSQL_PORT,
    dialect: 'mysql'.operatorsAliases: false,},production: {
    username: env.MYSQL_USER,
    password: env.MYSQL_PASSWORD,
    database: env.MYSQL_DATABSAE,
    host: env.MYSQL_HOST,
    port: env.MYSQL_PORT,
    dialect: 'mysql'.operatorsAliases: false,}}Copy the code

Create sequelize.js in the Initialize directory to connect to the database

/* * Create and initialize Sequelize */

// initialize/sequelize.js

import Sequelize from 'sequelize';

let sequelize;

const defaultConfig = {
  host: 'localhost'.dialect: 'mysql'.port: 3306.operatorsAliases: false.define: {
    updatedAt: false.createdAt: 'createdDate',},pool: {
    max: 100.min: 0.acquire: 30000.idle: 10000,}};export function initSequelize(config) {
  const { host, database, username, password, port } = config;
  sequelize = new Sequelize(
    database,
    username,
    password,
    Object.assign({}, defaultConfig, {
      host,
      port
    })
  );
  return sequelize;
}

export default sequelize;
Copy the code

On the database side above, we exported the initModel and initSequelize methods, which are used in the initialization entry.

Initialization entry

Create a new index.js file in the initialize directory to initialize the Model and connect to the database:

// initialize/index.js
import { initSequelize } from './sequelize';
import { initModel } from '.. /models';
import { asValue } from 'awilix';
import container from '.. /container';

import config from '.. /config/config'

export default function initialize() {
  const env = process.env.NODE_ENV || 'development'
  const sequelize = initSequelize(config[env]); // Initialize sequelize
  initModel(sequelize); // Initialize the Model
  container.register({
    sequelize: asValue(sequelize),
  });
}

Copy the code

Once the Model is initialized, we can define our Dao layer to use the Model.

DaoLayer and theServicelayer

We defined the Dao layer to operate the database and the Service layer to connect the external and Dao layers

  1. First of all, we havedaosCreating a DirectoryShopDao.jsFile used to operate store table:
// daos/ShopDao.js
import BaseDao from './base'

export default class ShopDao extends BaseDao {
  modelName = 'shop'

  // paging search shop
  async findPage(params = {}) {
    const listParams = getListSql(params);
    constsql = { ... listParams };return await this.findAndCountAll(sql)
  }
  // ...
}
Copy the code

Here shopDao is a subclass of BaseDao, and BaseDao encapsulates the following database operations, such as add, delete, change, check, stamp source code

  1. inservicesCreating a DirectoryShopService.jsFile:
// services/ShopService.js
import BaseService from './BaseService';

export default class ShopService extends BaseService {
  constructor({ shopDao }) {
    super(a);this.shopDao = shopDao
  }
  // paging lookup
  async findPage(params) {
    const [err, list] = await this.shopDao.findPage(params);
    if (err) {
      return this.fail('Failed to get list', err);
    }
    return this.success('Obtaining list succeeded', list || []);
  }
  // ...
}
Copy the code

Having defined the Dao and Service layers, we can then use dependency injection to help us manage the Dao and Service instances.

Dependency injection

The biggest benefit of dependency injection (DI) is that it helps us create the instances we need without having to create them manually, and we don’t need to care about the dependencies created by instance. DI manages them all for us, reducing the coupling between our code.

The dependency injection used here is awilix,

  1. First we create the container incontainerCreating a Directoryindex.js:
/* * Create a DI container */

 // container/index.js

import { createContainer, InjectionMode } from 'awilix';

const container = createContainer({
  injectionMode: InjectionMode.PROXY,
});

export default container;
Copy the code
  1. And then tellDIAll of usDaoandService:
// app.js
import container from './container';
import { asClass } from 'awilix';

Dependency injection configures the Service layer and dao layer
container.loadModules(['./services/*Service.js'.'./daos/*Dao.js'] and {formatName: 'camelCase'.register: asClass,
  cwd: path.resolve(__dirname),
});
Copy the code

Define the routing

Now all that is left is to expose the interface externally for other applications to call;

To define the route here, we use awilix-Express to define the back-end router

Let’s first define the route to the store.

Create the shopapi.js file in the API directory

// api/shopApi.js
import bodyParser from 'body-parser'
import { route, POST, before } from 'awilix-express'

@route('/shop')
export default class ShopAPI {
  constructor({ shopService }) {
    this.shopService = shopService;
  }

  @route('/findPage')
  @POST()
  @before([bodyParser.json()])
  async findPage(req, res) {
    const { success, data, message } = await this.shopService.findPage(
      req.body
    );
    if (success) {
      return res.success(data);
    } else {
      res.fail(null, message); }}// ...
}
Copy the code

We define the route, and then use awilix-Express to initialize the route during project initialization:

// app.js
import { Lifetime } from 'awilix';
import { scopePerRequest, loadControllers } from 'awilix-express';
import container from './container';

const app = express();

app.use(scopePerRequest(container));

app.use(
  '/api',
  loadControllers('api/*Api.js', {
    cwd: __dirname,
    lifetime: Lifetime.SINGLETON,
  })
);
Copy the code

Now we can use postman to try out the interface we defined:

other

If we need to use the current request object in the Service or Dao layer, we can inject request and Response for each request in DI, as follows:

// middleware/base.js
import { asValue } from 'awilix';

export function baseMiddleware(app) {
  return (req, res, next) = > {
    res.success = (data, error = null, message = 'success', status = 0) = > {
      res.json({
        error,
        data,
        type: 'SUCCRSS'.// ...
      });
    };
    res.fail = (data, error = null, message = 'failure', status = 0) = > {
      res.json({
        error,
        data,
        type: 'FAIL'.// ...
      });
    };

    req.app = app;
    req.container = req.container.createScope();
    req.container.register({
      request: asValue(req),
      response: asValue(res),
    });
    next();
  };
}
Copy the code

And then use middleware

// app.js
import express from 'express';

const app = express();
app.use(baseMiddleware(app));
Copy the code

The deployment of

This deployment uses pm2. Create a new pm2.

{
  "apps": [{"name": "express-backend"."script": "./dist/app.js"."exp_backoff_restart_delay": 100."log_date_format": "YYYY-MM-DD HH:mm Z"."output": "./log/out.log"."error": "./log/error.log"."instances": 1."watch": false."merge_logs": true."env": {
        "NODE_ENV": "production"}}}]Copy the code

Then add the command under package.json:

"scripts": {
  "clean": "rimraf dist",
  "dev": "cross-env NODE_ENV=development nodemon ./src/main.js --exec babel-node",
  "babel": "babel ./src --out-dir dist",
  "build": "cross-env NODE_ENV=production npm run clean && npm run babel",
  "start": "pm2 start pm2.json",
}
Copy the code

The NPM run build command cleans up the dist directory, compiles the code to the dist directory, and runs the NPM run start command to start the pM2 application.

The last

Source code, stamp! Stamp! Stamp!

reference

Vue+Express+Mysql full stack