vue+koa2+mongodb点餐系统总结

4,667 阅读5分钟

关于项目

这是一个点餐系统,包含用户点餐、商家出餐、管理员管理三部分功能 这个项目本来是校内实训,需要用java编写,我负责一部分。但是我不太喜欢用java,且时间足够,就自己独自做了一份,用于学习。 项目的功能和需求是根据前期小组讨论出来的,也基本都是仿饿了么的 各项功能基本都实现了

线上地址:(比较慢)47.93.254.91:3333

源码地址:chihuobao

登录账号:
  用户:12345678910
  商家:11112222333
  管理员:admin2
登录密码都是123456

功能结构

调试运行

npm install
npm run dev

cd server                 #打开koa2后台,会开启3333端口
npm install
node bin/www
npm run build                #打包
cp dist/* server/public/     #将打包好的文件放到koa2静态目录

页面截图

总体分析

使用的框架、插件等

  • 用Vue-cli脚手架、vue-router、vuex
  • 用element-ui样式框架
  • 用axios发请求
  • 用koa2做后台,在node高版本直接用async、await
  • 用mongoose连接mongodb数据库

包含的功能

  • 手机注册,登录,重置密码
  • 用户点餐,该商家会收到消息提示有新订单(用轮询实现)
  • 用户查看自己的订单,评价、删除等
  • 修改自己的信息,申请成为商户等
  • 商家管理订单,接单等
  • 统计商家订单数,评分等(页面上的月销量是总销量)
  • 商家管理菜单、查看评论
  • 管理员管理用户、商铺、分类等
  • 搜索功能

目录结构

顶层就是vue-cli的结构,主要看前端src和后台server的结构

─ src
 ├── common                         #
 │  ├── audio                       #音频
 │  ├── images                      #图片
 │  ├── javascript                  #api接口、cache、config等js文件
 │  ├── style                       #公用style
 ├── components                     #组件
 ├── pages                          #页面,处理业务,主要分为三个模块
 │  ├── admin
 │  ├── seller
 │  ├── user
 │  ├── index.vue
 │  ├── login.vue
 ├── router                         #路由
 │  ├── index.js
 ├── store                          #vuex的store,分了三个模块
 │  ├── admin
 │  ├── seller
 │  ├── user
 │  ├── index.js
 ├── App.vue
 ├── main.js
─ server
 ├── app
 ├── ├── common            # 工具
 ├── ├── controllers       # 业务
 ├── ├── models            # 定义数据库模型
 ├── db_vue                # 导出来的数据库数据
 ├── routes                # 路由
 ├── app.js
 ├── config.js             # 短信api的key相关

开发过程

使用vue-cli

我之前用react,为了熟悉webpack就没有使用脚手架(如yeoman),深深感受到了babel的复杂,webpack配置的繁琐。用到vue-cli简直就是一个字:爽,各种复杂的配置都配好了,如使用sass下载后在style配置一下就好了,不用再到webpack配置,这些发杂的配置本该就不要重复做。现在Parceiljs打包工具也出来了,以后可以更爽快的开发了

对vue的感觉就是真的对新手很友好,官网教程很全,例子很多,上手快。 使用vuex + map辅助函数用起来很方便 下面是一个登陆的例子

# login.vue
# 先请求登录,返回用户信息,通过vuex的mapAction函数调用actions,这里vuex分了user、seller、admin模块
methods: {
  ...mapAction('user',
    [
      'saveUserInfo'
    ]
  ),
  login () {
    _loginApi(phone, pass).then(res => {
      this.saveUserInfo(res.data)
      this.$router.push('/home')
    })
  }
}
# user/actions.js
# 调用函数,先做一个客户端存储存到localStorage,再存到state中
import { _saveUserInfo } from 'common/javascript/cache'

export function saveUserInfo ({commit, state}, info) {
  commit(types.SET_USER_INFO, _saveUserInfo(info))
}
# index.vue
# 需要数据的组件用vuex的mapGetters函数获取
<template>
  <user-header :userInfo='userInfo'></user-header>
</template>
<script>
export default {
  computed: {
    ...mapGetters(
      'user',
      [
        'userInfo',
        'reLogin'
      ]
    )
  }
}
</script>

数据的流向是单向的

开发遇到的问题

vuex分模块的修改

一开始没有分模块是这样写的

# store.js
# 
export default new Vuex.Store({
  getter,
  state,
  mutations,
  actions
})

# 组件调用,直接调用
computed: {
  ...mapGetters(['suggestion'])
},
methods: {
  ...mapActions(['saveUserInfo']),
  ...mapMutations({
    setCoordinate: 'SET_COORDINATE'
  })
}

分了模块写法有区别的

# store.js
# 各模块分别有各自的state、getters、actions
# 模块结构自己定义,所以可以定义一个顶层公用的,再在里层分模块
export default new Vuex.Store({
  modules: {
    user,
    seller,
    admin
  }
})

# 组件调用
# 调用要有模块名,mapActions取不同模块时要分开取
computed: {
  ...mapGetters(
    'user',
      [
        'suggestionList',
        'userInfo'
      ]
  )
},
methods: {
  ...mapMutations({
    setCoordinate: 'user/SET_COORDINATE'
  }),
  ...mapActions('user',
    [
      'saveInfo'
    ]
  ),
  ...mapActions('seller',
    [
      'saveSellerInfo'
    ]
  )
}

父子组件通信

一般父子组件,是父组件向子组件传入数据,子组件显示数据,数据单向流动。 当子组件需要传递数据给父组件时,通过触发函数,以参数的形式向父组件传递数据,跟react数据传递一样

# 父组件
<food-card @addOne='addOne' :info='info'></food-card>

# 子组件
<p class='name'>{{info.dishName}}</p>
<span class='money'>¥{{info.dishPrice}}</span>
<div :class='_status' @click='addToCart'>加入购物车</div>
...
props: {
  info: {
    type: Object,    #定义父组件传入的数据类型,当传入类型和定义的不一致,vue会警告
    default: {}
  }
},
methods: {
  addToCart () {
    this.$emit('addOne', this.info)   #用this.$emit触发父组件的addOne函数
  }
}

上面的例子中,不能修改父组件传入的数据。若要修改数据,则需要在$emit前复制一份数据然后修改,再传递给父组件,也可以用sync实现父子组件数据双向绑定。sync在2.0被移除因为这破坏了单向数据流,但2.3又引入了,因为有场景需要如一些复用的组件。但sync和以前的实现又有点不一样,它只是一个语法糖,会被扩展为一个自动更新父组件属性的 v-on 监听器。 并且子组件需要显示触发更新:this.$emit('update:xx', newVal)

# 父组件
<card-item :data.sync='item'></card-item>
  #会被扩展为这样
<comp :data="item" @update:data="newVal => item = newVal"></comp>

# 子组件
<input class='commend' type="text" v-model='commend' placeholder="写下对此菜品的评价">
  export default {
    data () {
      return {
        commend: ''
      }
    },
    watch: {
      commend (newC) {
        this.data.commend = newC
        this.$emit('update:data', this.data)   #显示触发data的更新达到双向数据绑定
      }
    },
    props: {
      data: {
        type: Object,
        default: {}
      }
    }
  }

element-ui设置样式无效

使用了element-ui样式框架,有时需要对他们的组件做一些样式的修改。但它是封装好的,我就需要查看源代码才知道它内部定义的类或标签来自定义样式,但是发现无效,举个例子

  <el-rate
    v-model="item.score"
    disabled
    show-text
    text-color="#ff9900">
  </el-rate>

<style scoped lang='sass'>
  .el-rate              #组件都自带同名的类
    div
      background: red
<style>

#发现element-ui通过jsfiddle演示的代码却没问题,就查找不同点,然后发现是style标签的scoped导致的,可能局限了样式的作用范围。去掉就可以了,此时要注意样式是全局的,所以要注意类名的使用

监听$route要仔细

在查看商铺页面,可以选择不同类型商家,也可以搜索商家,可以有不同的实现方法。可以把状态全放在在组件内或vuex管理,但是这样刷新后状态就消失了。所以我选择用url的hash来保存状态,通过监听路由变化来加载不同数据。商家列表数据放在vuex

# 商家页面,place.vue
data () {
  return {
    pageNum: 1,
    totalPage: 1,
    keyword: '',
    loading: false
  }
},
created () {
    this.getList()
    # 滚动加载下一页
    window.onscroll = () => {
      if (!this.loading && this.__getScrollHeight() <= (this.__getWindowHeight() + window.scrollY + 100)) {
        if (this.pageNum < this.totalPage) {
          this.loading = true
          this.pageNum++
          this.getList()
        }
      }
    }
  },
watch: {
  $route () {
    this.getList()
  }
},
methods: {
  changeTag (tag) {
    this.pageNum = 1
    this.shopType = tag
    this.keyword = ''
    # this.clearShopList()
    this.$router.push({path: '/place', query: {shopType: code, keyword: undefined}})
  },
  search (str) {
    this.keyword = str
    this.pageNum = 1
    this.shopType = 1
    # this.clearShopList()
    this.$router.push({path: '/place', query: {shopType: undefined, keyword: str}})
  },
  getList () {
    const { keyword, shopType } = this.$router.currentRoute.query
    this.loading = true
    _getShopList(keyword, shopType, this.pageNum).then(res => {    #请求数据,然后concat到商家list存入vuex
      ...
    })
  }
}

后面使用时,发现了bug:在别的页面变动路由,这里会加载了重复的数据。所以要限定监听路由变动的路由,在本页面才有效

watch: {
  $route () {
    if (this.$router.currentRoute.name === 'place') {
      this.getList()
    }
  }
}

然后又发现bug:从别的页面回到这里,也加载了重复的数据,解决办法是离开组件时把原数据删除。要这样做是因为我把数据存入了vuex,感觉不必要存入vuex..

beforeDestroy () {
  window.onscroll = null
  this.clearShopList()
},
methods: {
  ...mapMutations({
      clearShopList: 'user/CLEAR_SHOP_LIST'
  })
}

请求异常跳转登录页

请求有时需要出现异常如401,需要让用户重新登录,我用的是axios

# 这是一个请求封装,返回异常全部调用reLogin的action返回登录页

import store from '../../store'
export function basePOST (api, params) {
  return axios({
    method: 'post',
    url: api,
    headers: {
      'content-Type': 'application/x-www-form-urlencoded'
    },
    data: config.toFormData({
      ...params
    })
  }).then(res => {
    return res.data
  }).catch(() => {
    store.dispatch('user/reLogin')
  })
}

koa2基本配置

使用koa生成器初始化项目

npm install koa-generator -g
koa2 server
cd server && npm install
npm start

加入session中间件

const session = require('koa-session2')
app.use(session({
  key: 'sessionid---'
}))

设置静态资源缓存

# 注意,时间需要用变量放入,否则无效
var staticCache = require('koa-static-cache')
const cacheTime = 365 * 24 * 60 * 60
app.use(staticCache(path.join(__dirname, 'public'), {
  maxAge: cacheTime
}))

传输文件压缩

var compress = require('koa-compress')
app.use(compress({
  filter: function (contentType) {
    return /text/i.test(contentType)
  },
  threshold: 2048,
  flush: require('zlib').Z_SYNC_FLUSH
}))

mongoose使用遇到的坑

在建表时需要注意数据类型,若schema定义是Number,存入的却是String,会报错。 若schema没有定义字段,创建collection时传入其他字段,会存不进去 数据库的Number数据,用字符查找会找不到如:user.find({age: '18'})

moment解决mongodb时区问题

mongodb用的是中时区的时间,我们是东八区,所以时间都会晚8小时,用moment插件处理 moment是用在客户端,而不是存储。存储的数据是中时区的,在显示数据时修正 因为moment很多地方都要用,所以我直接将它放入Vue的原型,这样所有vue实例都可以拿到这个方法

#main.js
import Vue from 'vue'
import moment from 'moment'
Object.defineProperty(Vue.prototype, '$moment', {value: moment})

#组件中
<p class='fr date'>{{$moment(item.commentDate).format('YYYY-MM-DD HH:mm:ss')}}</p>

其他

传输数据格式转换

用post传输数据,用x-www-form-urlencoded格式,但是表单里有个对象数组:

{
  userId: 13546515,
  dishs: [{id: 4545, num: 2, price: 12}, {id: 1446, num: 1, price: 8}],
  ...
}

我用node做后台,拿这个数据一点问题都没有,但是小组内跟java后台配合,不能这样传对象数组字符串。他需要直接用List<>包装,需要这样传递才行

看到这样的取值,我想:卧槽,还有这样传的。。 虽然感觉很不合理,但还是做了转换。下面的代码就能达到这个目的

  let dishs = {}
  _dishs.forEach((item, index) => {
    for (let key in item) {
      if (!dishs[`dishs[${index}]`]) {
        dishs[`dishs[${index}]`] = {}
      }
      dishs[`dishs[${index}]`][key] = item[key]
    }
  })
  let temp = {}
  let result = {}
  for (let i in dishs) {
    for (let j in dishs[i]) {
      if (!temp[i]) temp[i] = {}
      result[`${i}.${j}`] = dishs[i][j]
    }
  }

mongodb导入数据

server/db_vue路径是导出的数据,可以导入到自己的mongodb数据库

# d是数据库名,c是collection名
mongoimport -d vue -c users --file vue/users.json

总结

总体来说,项目结构还算清晰,我对Vue还不是很熟悉,所以运用的还不是很好,比如使用Vuex的使用,我对于不同组件需要共享的数据存入store或同时存在本地,对于单个组件内的数据,感觉没必要存入store。 对koa2也不是很熟悉,一开始总是忘记await等各种小问题,写完虽然做了缓存压缩,因为不是一个后端,所以性能上还是弄不好,线上的可能比较卡,因为是学生服务器。 写完这个对Vue熟悉一点了,接下来会继续学学Vue的原理,学学新东西 以上就是对项目的总结,如果有错,望指正