一直都很想尝试用node来写点东西,学习了一番以后依葫芦画瓢用koa框架加上sequelize ORM从零开始用MVC模式编写了个简单的后台项目,故在此作一下记录。javascript
mkdir node-koa-demo # 建立项目
cd node-koa-demo # 进入目录
cnpm init -y # 生成package.json
cnpm install koa koa-body koa-router koa-static koa2-cors path -S # 安装koa插件
touch app.js # 生成入口文件
复制代码
// package.json
{
"name": "node-koa-demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node app.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"koa": "^2.11.0",
"koa-body": "^4.1.1",
"koa-router": "^7.4.0",
"koa-static": "^5.0.0",
"koa2-cors": "^2.0.6",
"path": "^0.12.7"
}
}
复制代码
// app.js
const Koa = require('koa')
const app = new Koa()
const config = require('./config')
const path = require('path')
const koaBody = require('koa-body')({ // // 解析body的中间件
multipart: true, // 支持文件上传
encoding:'gzip',
formLimit: '5mb', // 限制表单请求体的大小
jsonLimit: '5mb', // JSON 数据体的大小限制
textLimit: '5mb', // 限制 text body 的大小
formidable:{
uploadDir: path.join(__dirname, '/public/upload'), // 设置文件上传目录
keepExtensions: true, // 保持文件的后缀
maxFieldsSize: 200 * 1024 * 1024, // 设置上传文件大小最大限制,默认2M
onFileBegin: (name, file) => { // 文件上传前的设置
console.log(`name: ${name}`)
console.log(file)
}
}
})
const static = require('koa-static')
// 解析body的中间件
app.use(koaBody)
app.use(static(path.join(__dirname)))
app.listen(config.service.port, () => {
console.log('server is running')
})
复制代码
.env文件:java
// .env
SERVE_PORT=[项目端口]
SERVE_ENVIROMENT=[项目所处环境]
复制代码
安装dotenv插件用来在项目中引用env文件node
cnpm install dotenv -S # 用来引入.env配置环境变量
复制代码
入口文件引入插件:mysql
const dotenv = require('dotenv') // 引入配置文件
dotenv.config()
复制代码
config.js:sql
module.exports = {
service: {
port: process.env['SERVE_PORT'],
enviroment: process.env['SERVE_ENVIROMENT'] || 'dev'
}
}
复制代码
入口文件引入config.js:数据库
const config = require('./utils/config')
global.config = config
复制代码
启动项目:
cnpm run start
npm
安装插件json
cnpm install nodemon -S
复制代码
// package.json
"start:dev": "nodemon node app.js"
复制代码
cnpm install mysql2 sequelize -S
复制代码
mkdir db
touch db/index.js
复制代码
// .env
DB_DATABASE=[数据库名称]
DB_USER=[数据库用户名]
DB_PSW=[数据库链接密码]
DB_HOST=[数据库端口]
复制代码
// db/index.js
const Sequelize = require('sequelize')
const sequelize = new Sequelize(
process.env['DB_DATABASE'],
process.env['DB_USER'],
process.env['DB_PSW'],
{
host: process.env['DB_HOST'], // 数据库地址
dialect: 'mysql', // 数据库类型
dialectOptions: { // 字符集
charset:'utf8mb4',
collate:'utf8mb4_unicode_ci',
supportBigNumbers: true,
bigNumberStrings: true
},
pool: {
max: 5, // 链接池最大连接数量
min: 0, // 最小链接数量
idle: 10000 // 若是一个线程10秒内没有被使用的花,就释放链接池
},
timezone: '+08:00', // 东八时区
logging: (log) => {
console.log('dbLog: ', log)
return false
} // 执行过程会打印一些sql的log,设为false就不会显示
}
)
module.exports = sequelize
复制代码
mkdir model
touch model/User.js
复制代码
const Sequelize = require('sequelize')
const sequelize = require('../db')
const User = sequelize.define('user', {
id: {
type: Sequelize.INTEGER,
allowNull: false, // 设置为false时,会给添加NOT NULL(非空)约束,数据保存时会进行非空验证
comment: 'ID', // 字段描述(自1.7+后,此描述再也不添加到数据库中
autoIncrement: true, // 是否自增
primaryKey: true, // 指定是不是主键
unique: true, // 设置为true时,会为列添加惟一约束
},
password: {
type: Sequelize.STRING(20),
validate: {}, // 模型每次保存时调用的验证对象。但是validator.js中的验证函数(参见 DAOValidator)、或自定义的验证函数
allowNull: false, // 设置为false时,会给添加NOT NULL(非空)约束,数据保存时会进行非空验证
comment: '密码' // 字段描述(自1.7+后,此描述再也不添加到数据库中)
},
name: {
type: Sequelize.STRING(20),
validate: {
notEmpty: true
}, // 模型每次保存时调用的验证对象。但是validator.js中的验证函数(参见 DAOValidator)、或自定义的验证函数
allowNull: false, // 设置为false时,会给添加NOT NULL(非空)约束,数据保存时会进行非空验证
comment: '用户名称' // 字段描述(自1.7+后,此描述再也不添加到数据库中)
},
email: {
type: Sequelize.STRING(20),
validate: {
isEmail: true
}, // 模型每次保存时调用的验证对象。但是validator.js中的验证函数(参见 DAOValidator)、或自定义的验证函数
allowNull: false, // 设置为false时,会给添加NOT NULL(非空)约束,数据保存时会进行非空验证
comment: 'email' // 字段描述(自1.7+后,此描述再也不添加到数据库中)
},
phone: {
type: Sequelize.STRING(11),
allowNull: false, // 设置为false时,会给添加NOT NULL(非空)约束,数据保存时会进行非空验证
comment: '手机号码' // 字段描述(自1.7+后,此描述再也不添加到数据库中)
},
birth: {
type: Sequelize.DATE,
validate: {
isDate: true
}, // 模型每次保存时调用的验证对象。但是validator.js中的验证函数(参见 DAOValidator)、或自定义的验证函数
allowNull: false, // 设置为false时,会给添加NOT NULL(非空)约束,数据保存时会进行非空验证
defaultValue: new Date(), // 字面默认值, JavaScript函数, 或一个 SQL 函数
comment: '生日' // 字段描述(自1.7+后,此描述再也不添加到数据库中)
},
sex: {
type: Sequelize.INTEGER,
validate: {
isInt: true,
len: 1
}, // 模型每次保存时调用的验证对象。但是validator.js中的验证函数(参见 DAOValidator)、或自定义的验证函数
allowNull: false, // 设置为false时,会给添加NOT NULL(非空)约束,数据保存时会进行非空验证
defaultValue: 0, // 字面默认值, JavaScript函数, 或一个 SQL 函数
comment: '性别,0-男 1-女' // 字段描述(自1.7+后,此描述再也不添加到数据库中)
},
}, {
freezeTableName: true, // 设置为true时,sequelize不会改变表名,不然可能会按其规则有所调整
timestamps: true, // 为模型添加 createdAt 和 updatedAt 两个时间戳字段
})
//建立表,默认是false,true则是删除原有表,再建立
User.sync({
force: false,
})
module.exports = User
复制代码
// db/index.js
/* 扫描全部的model模型 */
const fs = require('fs')
const files = fs.readFileSync(__dirname + '/model') // 遍历目录
const jsFiles = files.filter(item => {
return item.endsWith('.js')
}, files)
module.exports = {}
for (const file of jsFiles) {
console.log(`import model from file ${file}`)
const name = file.substring(0, file.length - 3)
module.exports[name] = require(__dirname + '/model/' + file)
}
复制代码
mkdir router
touch router/index.js
复制代码
// router/index.js
const router = require('koa-router')({
prefix: '/api'
})
router.get('/', async(ctx, next) => {
ctx.body = 'Hello World~'
})
module.exports = router
复制代码
在入口文件app.js引入api
// 路由中间件
const router = require('./router')
// 开始服务并生成路由
app.use(router.routes()).use(router.allowedMethods()) // 开始服务并生成路由
复制代码
mkdir middleware
复制代码
touch middleware/exception.js # 中间件文件
touch utils/http-exception.js # 定义已知异常类
复制代码
明确已知异常仍是未知异常bash
// utils/http-exception.js
/** * 默认的异常 */
class HttpException extends Error {
constructor(msg = '错误请求', errorCode = 10000, code = 400) {
super()
this.errorCode = errorCode
this.code = code
this.msg = msg
}
}
class ParameterException extends HttpException {
constructor(msg, errorCode) {
super()
this.code = 400
this.msg = msg || '参数错误'
this.errorCode = errorCode || 10000
}
}
class AuthFailed extends HttpException {
constructor(msg, errorCode) {
super()
this.code = 401
this.mag = msg || '受权失败'
this.errorCode = errorCode || 10004
}
}
class NotFound extends HttpException {
constructor(msg, errorCode) {
super()
this.code = 404
this.msg = msg || '未找到该资源'
this.errorCode = errorCode || 10005
}
}
class Forbidden extends HttpException {
constructor(msg, errorCode) {
super()
this.code = 403
this.msg = msg || '禁止访问'
this.errorCode = errorCode || 10006
}
}
class Oversize extends HttpException {
constructor(msg, errorCode) {
super()
this.code = 413
this.msg = msg || '上传文件过大'
this.errorCode = errorCode || 10007
}
}
class InternalServerError extends HttpException {
constructor(msg, errorCode) {
super()
this.code = 500
this.msg = msg || '服务器出错'
this.errorCode = errorCode || 10008
}
}
module.exports = {
HttpException,
ParameterException,
AuthFailed,
NotFound,
Forbidden,
Oversize,
InternalServerError
}
复制代码
// middleware/exception.js
const { HttpException } = require('../utils/http-exception')
// 全局异常监听
const catchError = async(ctx, next) => {
try {
await next()
} catch(error) {
// 已知异常
const isHttpException = error instanceof HttpException
// 开发环境
const isDev = global.config.service.enviroment === 'dev'
// 在控制台显示未知异常信息:开发环境下,不是HttpException 抛出异常
if (isDev && !isHttpException) {
throw error
}
/** * 是已知错误,仍是未知错误 * 返回: * msg 错误信息 * error_code 错误码 */
if (isHttpException) {
ctx.body = {
msg: error.msg,
error_code: error.errorCode
}
ctx.response.status = error.code
} else {
ctx.body = {
msg: '未知错误',
error_code: 9999
}
ctx.response.status = 500
}
}
}
module.exports = catchError
复制代码
// 加载全局异常
const errors = require('./utils/http-exception')
global.errs = errors
const app = new Koa()
// 全局异常中间件监听、处理,放在全部中间件的最前面
const catchError = require('./middleware/exception')
app.use(catchError)
复制代码
// utils/resJson.js
const ResultJson = {
success: (params) => {
return {
data: params.data || null, // 返回的数据
msg: params.msg || '操做成功', // 返回的提示信息
code: 1 // 返回的接口调用状态码,0-失败,1-成功
}
},
fail: (params) => {
return {
data: params.data || null,
msg: params.msg || '操做失败',
code: 0,
error_code: params.errorCode // 返回接口异常信息码
}
}
}
module.exports = ResultJson
复制代码
修改上文提到的异常处理中间件
/* 错误处理中间件 */
const { HttpException } = require('../utils/http-exception')
const resJson = require('../utils/resJson')
// ...省略上文
if (isHttpException) {
ctx.body = resJson.fail(error)
ctx.response.status = error.code
} else {
ctx.body = resJson.fail({
msg: '未知错误',
error_code: 9999
})
ctx.response.status = 500
}
// ...省略下文
复制代码
mkdir controller
touch controller/User.js
复制代码
const User = require('../model/User.js')
const resJson = require('../utils/resJson')
module.exports = {
selectAll: async (ctx, next) => {
await User.findAll({
raw: true,
attributes: { // 不返回password字段
exclude: ['password']
}
}).then((res) => {
// 成功返回
ctx.body = resJson.success({data: res})
}).catch((err) => {
// 失败,捕获异常并输出
ctx.body = resJson.fail(err)
})
}
}
复制代码
// router/index.js
const router = require('koa-router')({
prefix: '/api'
})
// User控制器
const User = require('../controller/user')
// 获取所有用户
router.get('/user/list', User.selectAll)
module.exports = router
复制代码
访问接口地址:http://localhost:3002/api/user/list
访问结果:
{
"data": [
{
"id": 1,
"name": "cai",
"email": "cai@qq.com",
"phone": "13234323453",
"birth": "2019-12-13T01:23:17.000Z",
"sex": 1,
"createdAt": "2019-12-13T01:23:42.000Z",
"updatedAt": "2019-12-13T01:23:42.000Z"
}
],
"msg": "操做成功",
"code": 1
}
复制代码