node.js学习随笔(增删改查)
最近看了廖雪峰的js教程里面有一章节专门讲的node.js(https://www.liaoxuefeng.com/wiki/001434446689867b27157e896e74d51a89c25cc8b43bdb3000/001434501245426ad4b91f2b880464ba876a8e3043fc8ef000),感觉很不错,于是照着廖大构建项目结构的思路自己试着写了个可以增删改查的商品列表页 + 商品详情页(见下图)

做的这个小demo主要用了koa2(node.js框架) + Sequelize(ORM框架) + Vue.js + bootstrap, 页面很简单, 主要目的是想学习一些后端的知识
项目目录如下:

在app.js中引入koa koa-router等其他的中间件
// 项目入口文件app.js const path = require('path') // import const Koa = require('koa') // const bodyParser = require('koa-bodyparser') const koaBody = require('koa-body') // 创建实例 const app = new Koa() // 判断当前环境 const isProduction = process.env.NODE_ENV === 'production' // 自动扫描加载controllers目录下的所有js文件 const controller = require('./controller') // 打印请求消息 app.use(async (ctx, next) => { console.log(`Request ${ctx.method}: ${ctx.url}`) await next() }) // 开发环境下处理静态文件的middleware if (!isProduction) { const staticFiles = require('./static-files') app.use(staticFiles('/static/', __dirname + '/static/')) } // 添加rest方法 const restFn = require('./rest') app.use(restFn.restify()) // 集成Nunjucks const templating = require('./templating') app.use(templating('views', { noCache: !isProduction, watch: !isProduction })) // 解析请求体 // app.use(bodyParser()) app.use(koaBody({ multipart: true, // 支持文件上传 formidable: { uploadDir: path.join(__dirname, 'static/upload/'), // 设置文件上传目录 maxFieldsSize: 2 * 1024 * 1024, // 限制文件上传大小(默认2mb) } })) // add controller app.use(controller()) // listen app.listen(80) console.log('启动成功!')
引入'koa-body'来解析表单请求中请求体的数据,比如文件上传, 用户提交登录的表单操作等。
通过引入js模板引擎Nunjucks来解析模板html文件输出到客户端,在app.js中引入了templating.js来集成Nunjucks
通过new Koa()创建了一个koa的实例, 每一个app.use(async (ctx, next) => { ... } )都是一个中间件。
前端页面主要使用vue.js + bootstrap来构建
index.html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>index</title> <!-- css --> <link rel="stylesheet" href="/static/css/bootstrap.css"> <link rel="stylesheet" href="/static/css/common.css"> </head> <body> <div id="app" v-cloak> <p style="display: none" id="username">{{ userinfo.username }}</p> <!-- header --> ...
<div class="jumbotron"> <h2>搜索</h2> ...
</div> <!-- list --> <table class="table"> <thead> <tr> <th style="width: 100px;">序号</th> <th style="width: 120px;">缩略图</th> <th>名称</th> <th>品牌</th> <th>价格</th> <th style="width: 200px;">编辑</th> </tr> </thead> <tbody> <tr v-for="(item, index) in productList" :key="item.id"> <td v-text="getItemIndex(index)"></td> <td> <img class="media-object" :src="item.avatar ? item.avatar : defaultAvatarImg" alt=".."> </td> <td> <a :href="'/detail/' + item.id" v-text="item.name"></a> </td> <td v-text="item.brand"></td> <td>¥ <span v-text="item.price" class="color_price"></span></td> <td> <div> <button type="button" class="btn btn-primary btn-sm" @click="showAddProdFormUpdate(item.id)">修改</button> <button type="button" class="btn btn-danger btn-sm" @click="showRemoveConfirm(item.id)">删除</button> </div> </td> </tr> </tbody> </table> <!-- 无数据的提示 --> <p v-if="!productList.length" class="list_tip">--暂无数据!--</p> <!-- 分页 --> ... <!-- layer --> <div v-show="showAddProd" class="layer_mask" style="display: none;"> <div class="add_prod_layer panel panel-default" :class="{ show_animate: showAddProd }"> <!-- header --> <div class="panel-heading"><span v-text="layerTitle"></span> <span class="icon_close glyphicon glyphicon-remove" aria-hidden="true" @click="closeLayer"></span></div> <div class="panel-body"> <div class="form-group"> <label for="">商品名称<span class="verify">*</span></label> <input type="text" class="form-control" placeholder="请输入商品名称" v-model.trim='addProdName'> </div> <div class="form-group"> <label for="">品牌名称<span class="verify">*</span></label> <input type="text" class="form-control" placeholder="请输入品牌名称" v-model.trim='addProdBrand'> </div> <div class="form-group"> <label for="">商品价格<span class="verify">*</span></label> <input type="text" class="form-control" placeholder="请输入商品价格" v-model.trim='addProdPrice'> </div> <div class="form-group"> <label for="">商品描述</label> <textarea class="form-control" placeholder="请输入商品描述" rows="3" v-model.trim='addProdDesc'></textarea> </div> <!-- 上传头像 --> <div class="form-group"> <label for="" style="display: block">上传缩略图</label> <div class="avatar_wrapper"> <img :src="uploadImg" alt="" class="avatar_img"> <input id="uploadInput" type="file" accept="image/*" @change="getFileVal"> </div> </div> <!-- submit --> <button v-if="showUpdate" type="button" class="btn btn-primary" @click="updateSubmit">确认修改</button> <button v-else type="button" class="btn btn-primary" @click="addSubmit">确认新增</button> </div> </div> </div> </div> <!-- js --> <script src="/static/js/jquery-1.9.1.min.js"></script> <script src="/static/layer/layer.js"></script> <script src="/static/js/vue.js"></script> ...
引入jquery主要使用它的ajax功能, 和基于jquery的layer弹出框控件, 实际项目里应引入不依赖于jquery的其他类库, 这里为了方便直接引入
已用户增加商品为例:

用户填写完对应的内容后点击‘确认新增’:
// 确认添加 addSubmit: function() { // 提交前先验证 var state = this.verifyForm() if (!state) { // 验证未通过 return } // loading var load = layer.load() // ajax var _this = this $.post('/api/addProd', { name: this.addProdName, brand: this.addProdBrand, price: this.addProdPrice, desc: this.addProdDesc, avatar: this.prodAvatarImg }).done(function(data) { console.log(data) layer.close(load) _this.closeLayer() // 重新加载列表数据 _this.loadListData() }).fail(function(err) { console.error(err) layer.close(load) layer.alert(err.message || '服务器错误', { icon: 2 }) }) },
首先验证必填字段是否为空,然后向后台发起一个ajax请求,把相对应的参数数据发送给后台.
然后后台通过路由来判断请求应该由哪一个controller来处理业务逻辑
// api.js ... // 添加商品 const addProd = async (ctx, next) => { // 获取对应的参数 let prodName = ctx.request.body.name let prodBrand = ctx.request.body.brand let prodPrice = ctx.request.body.price let prodDesc = ctx.request.body.desc let prodAvatar = ctx.request.body.avatar let creater = ctx.cookies.get('username') if (creater) { creater = new Buffer(creater, 'base64').toString() } let prod = await handleAddProd(prodName, prodBrand, prodPrice, prodDesc, prodAvatar, creater) // rest res ctx.rest(prod) } ... // exports module.exports = { ... 'POST /api/addProd': addProd, ... }
后台接受到前台传来的参数后进行组织 进一步传给Service(product.js)里的处理函数来执行存入数据库等操作
let prod = await handleAddProd(prodName, prodBrand, prodPrice, prodDesc, prodAvatar, creater)
product.js:
// product.js 处理商品相关的业务逻辑 // 导入model模型 const { Prod } = require('../model') const Sequelize = require('sequelize') const Op = Sequelize.Op // 设置商品的业务信息 function Product(name, brand, price, desc, avatar, creater) { this.name = name this.brand = brand this.price = price this.desc = desc || '' this.avatar = avatar || '' this.creater = creater || '匿名用户' } ... // exports module.exports = { ... // 添加商品 async handleAddProd (name, brand, price, desc, avatar, creater) { let prodData = new Product(name, brand, price, desc, avatar, creater) let prod = await Prod.create(prodData) return prod }, ... }
这里通过引入了Node的ORM框架Sequelize来操作数据库, 引入Prod(商品的model模型),把关系数据库mysql对应的表结构映射到Prod对象上,调用Prod的create方法往数据库添加商品数据。
添加成功后, 把相应的信息返回给客户端:
api.js:
// rest res ctx.rest(prod) // ctx.response.body = prod
客户端接受成功后重新加载列表 完成商品添加:
$.post('/api/addProd', {
name: this.addProdName,
brand: this.addProdBrand,
price: this.addProdPrice,
desc: this.addProdDesc,
avatar: this.prodAvatarImg
})
.done(function(data) {
// 请求成功
console.log(data)
...
// 重新加载列表数据
_this.loadListData()
})
.fail(function(err) {
...
})

至此一个添加商品的完整流程就完成了

浙公网安备 33010602011771号