自己试着写了个可以增删改查的商品列表页 + 商品详情页(见下图)

廖雪峰pythonpdf 廖雪峰nodejs_jquery

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

项目目录如下:

 

廖雪峰pythonpdf 廖雪峰nodejs_廖雪峰pythonpdf_02

在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的其他类库, 这里为了方便直接引入

已用户增加商品为例:

廖雪峰pythonpdf 廖雪峰nodejs_css_03

用户填写完对应的内容后点击‘确认新增’:

// 确认添加
          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) {
              ...
            })

廖雪峰pythonpdf 廖雪峰nodejs_廖雪峰pythonpdf_04

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