写在前面
记录一下 nodejs 实现最最最基础的增删改查
项目尽可能遵从 mvc 设计思想构建,适合初探 nodejs 的前端小伙伴作为参考
项目地址:https://github.com/JohnnyLuv/nodejs-easy-demo-202003/tree/base
技术栈
服务端:nodejs + koa2 + mongodb
客户端:vuejs + axios
设想
- 服务端 只负责数据库交互 和 输出接口
- 客户端 只负责页面展示 和 接口调用
- 分离要彻底,前后端 只通过接口交互
- 客户端渲染,不做模板引擎
- 实现最基础的用户管理(用户列表/添加用户/编辑用户/删除用户)操作
项目结构
src                      // 根目录
├─ assets                // 客户端静态资源
│  ├─ css
│  ├─ img
│  └─ js
│     ├─ axios.min.js
│     ├─ util.js
│     └─ vue.min.js
│
├─ controller            // 控制器 用于解析用户输入,处理后返回相应的结果
│  └─ userController.js
│
├─ module                // 模型 用于定义数据模型
│  ├─ config.js          // 服务端配置
│  ├─ db.js              // 数据库请求封装
│  └─ userModule.js
│
├─ views                 // 视图 返回客户端视图
│  ├─ addUser.html
│  ├─ editUser.html
│  └─ index.html
│
├─ app.js                // 入口文件
│
├─ package.json          // yarn/npm 配置文件
│
└─ router.js             // 客户端视图路由
服务端模块
// package.json
{
  "name": "koa-obj",
  "version": "1.0.0",
  "main": "app.js",
  "license": "MIT",
  "dependencies": {
    "koa": "^2.11.0",
    "koa-bodyparser": "^4.2.1",   // 解析客户端请求 request.body 
    "koa-router": "^8.0.8",       // 服务端路由分发
    "koa-static": "^5.0.0",       // 静态文件目录指向
    "koa-views": "^6.2.1",        // 渲染模板使用,本项目没有使用模板引擎,只用来渲染html文件
    "mongodb": "^3.5.5",          // mongodb 数据库模块
    "node": "^13.10.1",
    "nodemon": "^2.0.2"           // 监听服务端变动,热重载 node 服务
  },
  "scripts": {
    "serve": "nodemon app.js"
  }
}
App入口文件
实际开发中前后端分离应该是两个项目
也就是 app.js 中的 页面路由注册 是没有的
// app.js
const Koa = require('koa'),
  app = new Koa()
const Router = require('koa-router'),
  router = new Router()
// 指定渲染目录
const views = require('koa-views')
app.use(views('views'))
// request中间件
const bodyParser = require('koa-bodyparser')
app.use(bodyParser())
// 设置静态文件目录
const static = require('koa-static')
app.use(static(__dirname))
// 页面路由注册
const viewRouter = require('./router')
app.use(viewRouter.routes()).use(router.allowedMethods())
// 接口路由注册
const userApi = require('./controller/userController')
app.use(userApi.routes()).use(router.allowedMethods())
app.listen(3000, () => {
  console.log('service running at http://localhost:3000/')
})
Controller模块
负责控制 Api 的路由分发和业务逻辑指向
// controller/userController.js
const Router = require('koa-router'),
  router = new Router()
const { login, userList, userInfo, addUser, editUser, removeUser } = require('../module/userModule')
// 用户列表
router.get('/userList', async ctx => {
  const response = await userList(ctx)
  ctx.body = response
})
// 查询用户详情
router.get('/userInfo/:_id', async ctx => {
  const response = await userInfo(ctx)
  ctx.body = response
})
// 添加用户
router.post('/addUser', async ctx => {
  const response = await addUser(ctx)
  ctx.body = response
})
// 修改用户
router.post('/editUser', async ctx => {
  const response = await editUser(ctx)
  ctx.body = response
})
// 删除用户
router.del('/removeUser/:_id', async ctx => {
  const response = await removeUser(ctx)
  ctx.body = response
})
module.exports = router
Module模块
负责处理 Api 请求的业务逻辑
// module/userModule.js
const DB = require('./db')
/**
 * 用户列表
 * @param {Object} ctx 
 */
const userList = async ctx => {
  let response = {}
  const data = await DB.find('user', {})
  response = {
    status: 200,
    data,
    msg: 'success',
  }
  return response
}
/**
 * 用户详情
 * @param {Object} ctx 
 */
const userInfo = async ctx => {
  const _id = ctx.params._id
  // console.log(_id)
  const result = await DB.find('user', { _id: DB.ObjectId(_id) })
  const response = {
    status: 200,
    data: result[0],
    msg: 'success',
  }
  return response
}
/**
 * 添加用户
 * @param {Object} ctx 
 */
const addUser = async ctx => {
  const query = ctx.request.body
  // console.log(query)
  let response = {}
  switch (true) {
    case query.username === '':
      response = {
        status: 201,
        msg: 'username 必填',
      }
      break
    case query.password === '':
      response = {
        status: 201,
        msg: 'password 必填',
      }
      break
    default:
      const data = await DB.insert('user', query)
      response = {
        status: 200,
        data: data.ops[0],
        msg: '添加成功',
      }
      break
  }
  return response
}
/**
 * 修改用户
 * @param {Object} ctx 
 */
const editUser = async ctx => {
  const query = ctx.request.body
  // console.log(query)
  await DB.update('user', { _id: DB.ObjectId(query._id) }, { username: query.username, password: query.password })
  const response = {
    status: 200,
    data: { _id: query._id },
    msg: 'success',
  }
  return response
}
/**
 * 删除用户
 * @param {Object} ctx 
 */
const removeUser = async ctx => {
  const _id = ctx.params._id
  await DB.delete('user', { _id: DB.ObjectId(_id) })
  const response = {
    status: 200,
    data: { _id },
    msg: 'success',
  }
  return response
}
module.exports = {
  login,
  userList,
  userInfo,
  addUser,
  editUser,
  removeUser,
}
DB库封装
这里需要注意的是,如果通过 _id 来查询,需要提前暴露 mongodb 模块的 ObjectId 方法
// module/db.js
const { MongoClient, ObjectId } = require('mongodb')
const Config = require('./config.js')
class Db {
  static getInstance() { // 单例模式解决多次实例化问题
    if (!Db.instance) {
      Db.instance = new Db()
    }
    return Db.instance
  }
  constructor() {
    this.dbClient = ''
    this.ObjectId = ObjectId
    this.connect() // 实例化时自动连接数据库,提高查询效率
  }
  connect() {
    return new Promise((resolve, reject) => {
      if (!this.dbClient) { // 解决数据库多次链接问题
        MongoClient.connect(Config.dbUrl, (err, client) => {
          if (err) {
            reject(err)
          }
          let db = client.db(Config.dbName)
          this.dbClient = db
          resolve(this.dbClient)
          // db.close() 注释成为长连接
        })
      } else {
        resolve(this.dbClient)
      }
    })
  }
  find(collectionName, json) {
    return new Promise((resolve, reject) => {
      this.connect().then(db => {
        let result = db.collection(collectionName).find(json)
        result.toArray((err, docs) => {
          if (err) {
            reject(err)
          }
          resolve(docs)
        })
      })
    })
  }
  insert(collectionName, json) {
    return new Promise((resolve, reject) => {
      this.connect().then(db => {
        db.collection(collectionName).insertOne(json, (err, res) => {
          if (err) {
            reject(err)
          } else {
            resolve(res)
          }
        })
      })
    })
  }
  update(collectionName, json1, json2) {
    return new Promise((resolve, reject) => {
      this.connect().then(db => {
        db.collection(collectionName).updateOne(json1, { $set: json2 }, (err, res) => {
          if (err) {
            reject(err)
          } else {
            resolve(res)
          }
        })
      })
    })
  }
  
  delete(collectionName, json) {
    return new Promise((resolve, reject) => {
      this.connect().then(db => {
        db.collection(collectionName).removeOne(json, (err, res) => {
          if (err) {
            reject(err)
          } else {
            resolve(res)
          }
        })
      })
    })
  }
}
module.exports = Db.getInstance()
文档和攻略
- mongodb 配置使用:http://blog.starpoetry.cn/
- koa2 文档:https://koa.bootcss.com
- koa-router 服务端路由文档:https://github.com/koajs/router/
- axios 客户端路由文档:https://github.com/axios/