自己试着写了个可以增删改查的商品列表页 + 商品详情页(见下图)
做的这个小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) {
...
})
至此一个添加商品的完整流程就完成了