基于 Express + MySql 的 Node.js 的后端开发

4,586 阅读7分钟

现在作为一名的前端,你会后端开发么?你需要后端开发么?

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 转换一下

  • 安装依赖
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以外的代码变化时,会重新启动项目。

  1. 添加命令

下面我们在package.json定义启动命令:

"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 全栈初体验