基於 Express + MySql 的 Node.js 的後端開發

如今做爲一名的前端,你會後端開發麼?你須要後端開發麼?html

o(╥﹏╥)o......而後我遇到了這樣的需求,而後只能衝鴨!衝鴨!衝鴨!前端

技術棧

項目結構

|-- express-backend
    |-- src
        |-- api    // controller api文件
        |-- config // 項目配置目錄
        |-- container  // DI 容器
        |-- daos  // dao層
        |-- initialize  // 項目初始化文件
        |-- middleware  // 中間件
        |-- models  // 數據庫 model
        |-- services // service層
        |-- utils // 工具類相關目錄
        |-- app.js // 項目入口文件
複製代碼

搭建項目基礎

  1. 初始化項目
npm init
複製代碼
  1. 安裝依賴
npm i express sequelize mysql2 awilix awilix-express
複製代碼

配置

配置Babel

由於awilixawilix-express會用到ES6classdecorator語法,因此須要 @babel/plugin-proposal-class-properties@babel/plugin-proposal-decorators 轉換一下sql

  • 安裝依賴
npm install --save-dev @babel/core @babel/cli @babel/preset-env
複製代碼
npm install --save-dev @babel/node
複製代碼
npm install --save-dev @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators
複製代碼
  • 配置babel
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": false,
        "targets": {
          "node": "current"
        }
      }
    ]
  ],
  "plugins": [
    [
      "@babel/plugin-proposal-decorators",
      {
        "legacy": true
      }
    ],
    [
      "@babel/plugin-proposal-class-properties",
      {
        "loose": true
      }
    ]
  ]
}
複製代碼

熱更新

在開發過程當中,熱更新是必需的,在這裏,咱們使用的是nodemon數據庫

  1. 安裝依賴
npm install --save-dev nodemon
複製代碼
  1. 在項目根目錄下添加nodemon.json
{
  "ignore": [
    ".git",
    "node_modules/**/node_modules",
    "package-lock.json",
    "npm-debug.log*",
  ]
}
複製代碼

ignore表示要忽略的部分,即這部分文件變化時, 項目不會重啓,而ignore之外的代碼變化時,會從新啓動項目。express

  1. 添加命令

下面咱們在package.json定義啓動命令:npm

"scripts": {
  "dev": "cross-env NODE_ENV=development nodemon ./src/app.js --exec babel-node"
},
複製代碼

環境配置

在實踐過程當中,咱們每每會和一些敏感的數據信息打交道,好比數據庫的鏈接用戶名、密碼,第三方SDKsecret等。這些參數的配置信息最好不要進入到git倉庫的。一來在開發環境中,不一樣的開發人員本地的開發配置各有不一樣,不依賴於git版本庫配置。二來敏感數據的入庫,增長了人爲泄漏配置數據的風險,任何能夠訪問git倉庫的開發人員,均可以從中獲取到生產環境的secret key。一旦被惡意利用,後果不堪設想。

因此能夠引入一個被.gitignore.env的文件,以key-value的方式,記錄系統中所須要的可配置環境參數。並同時配套一個.env.example的示例配置文件用來放置佔位,.env.example能夠放心地進入git版本倉庫。

在本地建立一個.env.example文件做爲配置模板,內容以下:

# 服務的啓動名字和端口
HOST = 127.0.0.1
PORT = 3000
複製代碼

讀取.env中的配置

Node.js能夠經過env2的插件,來讀取.env配置文件,加載後的環境配置參數,能夠經過例如process.env來讀取信息。

npm i env2
複製代碼
require('env2')('./.env')
複製代碼

而後在配置目錄中:

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

export default {
  PORT: env.PORT,
  HOST: env.HOST,
};
複製代碼

代碼介紹

數據庫

後端開發經常涉及對數據庫的增刪改查操做,在這裏咱們使用的是 sequelizemysql2

1. 定義數據庫業務相關的model

咱們在models目錄下繼續建立一系列的model來與數據庫表結構作對應:

├── models                       # 數據庫 model
│   ├── index.js                 # model 入口與鏈接
│   ├── goods.js                 # 商品表
│   ├── shop.js                 # 店鋪表
複製代碼

以店鋪表爲例,定義店鋪的數據模型shop:

/* * 建立店鋪 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;
}
複製代碼

而後在models/index.js,用來導入modes目錄下的全部models:

// 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;
複製代碼

2. Sequelize鏈接MySQL數據庫

Sequelize鏈接數據庫的核心代碼主要就是經過new Sequelize(database, username, password, options) 來實現,options是配置選項,具體能夠查閱官方手冊

咱們先在config目錄下config.js文件,增長對數據庫的配置:

// 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,
  }
}
複製代碼

而後在initialize目錄下新建sequelize.js用來鏈接數據庫

/* * 建立並初始化 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;
複製代碼

上面關於數據庫方面,咱們導出了initModelinitSequelize方法,這兩個方法會在初始化入口這裏使用。

初始化入口

initialize目錄下新建index.js文件,用來初始化Model和鏈接數據庫:

// 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]); // 初始化 sequelize
  initModel(sequelize); // 初始化 Model
  container.register({
    sequelize: asValue(sequelize),
  });
}

複製代碼

model初始化完了以後,咱們就能夠定義咱們的Dao層來使用model了。

Dao層和Service

咱們定義Dao層來操做數據庫,定義Service層來鏈接外部和Dao

  1. 首先咱們在daos目錄下新建ShopDao.js文件,用來操做店鋪表:
// daos/ShopDao.js
import BaseDao from './base'

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

  // 分頁查找店鋪
  async findPage(params = {}) {
    const listParams = getListSql(params);
    const sql = {
      ...listParams
    };
    return await this.findAndCountAll(sql)
  }
  // ...
}
複製代碼

這裏shopDaoBaseDao的子類,而BaseDao封裝着一下數據庫的操做,好比增刪改查,戳源代碼

  1. services目錄下新建ShopService.js文件:
// services/ShopService.js
import BaseService from './BaseService';

export default class ShopService extends BaseService {
  constructor({ shopDao }) {
    super();
    this.shopDao = shopDao
  }
  // 分頁查找
  async findPage(params) {
    const [err, list] = await this.shopDao.findPage(params);
    if (err) {
      return this.fail('獲取列表失敗', err);
    }
    return this.success('獲取列表成功', list || []);
  }
  // ...
}
複製代碼

咱們定義好了Dao層和Service層,而後能夠使用依賴注入來幫咱們管理DaoService的實例。

依賴注入

依賴注入(DI)最大的做用是幫咱們建立咱們所須要是實例,而不須要咱們手動建立,並且實例建立的依賴咱們也不須要關心,全都由DI幫咱們管理,能夠下降咱們代碼之間的耦合性。

這裏用的依賴注入是awilix

  1. 首先咱們建立容器,在container目錄下新建index.js
/* * 建立 DI 容器 */

 // container/index.js

import { createContainer, InjectionMode } from 'awilix';

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

export default container;
複製代碼
  1. 而後告訴DI咱們全部的DaoService
// app.js
import container from './container';
import { asClass } from 'awilix';

// 依賴注入配置service層和dao層
container.loadModules(['./services/*Service.js', './daos/*Dao.js'], {
  formatName: 'camelCase',
  register: asClass,
  cwd: path.resolve(__dirname),
});
複製代碼

定義路由

如今底層的一切都作好了,就差向外部暴露接口,供其餘應用調用了;

在這裏定義路由,咱們使用awilix-express來定義後端router

咱們先來定義關於店鋪的路由。

api目錄下新建shopApi.js文件

// 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);
    }
  }
  // ...
}
複製代碼

咱們定義好了路由,而後在項目初始化的時候,用awilix-express初始化路由:

// 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,
  })
);
複製代碼

如今咱們能夠用postman試一下咱們定義的接口啦:

其餘

若是咱們須要在Service層或者Dao層使用當前的請求對象,這個時候咱們就能夠在DI中爲每一條請求注入requestresponse,以下中間件:

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

export function baseMiddleware(app) {
  return (req, res, next) => {
    res.success = (data, error = null, message = '成功', status = 0) => {
      res.json({
        error,
        data,
        type: 'SUCCRSS',
        // ...
      });
    };
    res.fail = (data, error = null, message = '失敗', 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();
  };
}
複製代碼

而後使用中間件

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

const app = express();
app.use(baseMiddleware(app));
複製代碼

部署

這裏部署使用的是pm2, 在項目根目錄新建pm2.json:

{
  "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"
      }
    }
  ]
}
複製代碼

而後在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",
}
複製代碼

npm run build構建命令,先清理dist目錄,而後編譯代碼到dist目錄下,最後執行npm run startpm2就會啓動應用。

最後

源代碼,戳!戳!戳!

參考文獻

Vue+Express+Mysql 全棧初體驗

相關文章
相關標籤/搜索