如今做爲一名的前端,你會後端開發麼?你須要後端開發麼?html
o(╥﹏╥)o......而後我遇到了這樣的需求,而後只能衝鴨!衝鴨!衝鴨!前端
框架: expressnode
依賴注入: awilixgit
路由插件: awilix-expressgithub
|-- express-backend
|-- src
|-- api // controller api文件
|-- config // 項目配置目錄
|-- container // DI 容器
|-- daos // dao層
|-- initialize // 項目初始化文件
|-- middleware // 中間件
|-- models // 數據庫 model
|-- services // service層
|-- utils // 工具類相關目錄
|-- app.js // 項目入口文件
複製代碼
npm init
複製代碼
npm i express sequelize mysql2 awilix awilix-express
複製代碼
Babel
由於awilix
和awilix-express
會用到ES6
的class
和decorator
語法,因此須要 @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數據庫
npm install --save-dev nodemon
複製代碼
nodemon.json
{
"ignore": [
".git",
"node_modules/**/node_modules",
"package-lock.json",
"npm-debug.log*",
]
}
複製代碼
ignore
表示要忽略的部分,即這部分文件變化時, 項目不會重啓,而ignore
之外的代碼變化時,會從新啓動項目。express
下面咱們在package.json
定義啓動命令:npm
"scripts": {
"dev": "cross-env NODE_ENV=development nodemon ./src/app.js --exec babel-node"
},
複製代碼
在實踐過程當中,咱們每每會和一些敏感的數據信息打交道,好比數據庫的鏈接用戶名、密碼,第三方SDK
的secret
等。這些參數的配置信息最好不要進入到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,
};
複製代碼
後端開發經常涉及對數據庫的增刪改查操做,在這裏咱們使用的是 sequelize和mysql2
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;
複製代碼
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;
複製代碼
上面關於數據庫方面,咱們導出了initModel
和initSequelize
方法,這兩個方法會在初始化入口這裏使用。
在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
層
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)
}
// ...
}
複製代碼
這裏shopDao
是BaseDao
的子類,而BaseDao
封裝着一下數據庫的操做,好比增刪改查,戳源代碼
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
層,而後能夠使用依賴注入來幫咱們管理Dao
和Service
的實例。
依賴注入(DI
)最大的做用是幫咱們建立咱們所須要是實例,而不須要咱們手動建立,並且實例建立的依賴咱們也不須要關心,全都由DI
幫咱們管理,能夠下降咱們代碼之間的耦合性。
這裏用的依賴注入是awilix,
container
目錄下新建index.js
:/* * 建立 DI 容器 */
// container/index.js
import { createContainer, InjectionMode } from 'awilix';
const container = createContainer({
injectionMode: InjectionMode.PROXY,
});
export default container;
複製代碼
DI
咱們全部的Dao
和Service
:// 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
中爲每一條請求注入request
和response
,以下中間件:
// 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 start
,pm2
就會啓動應用。
源代碼,戳!戳!戳!