[关闭]
@Bios 2019-08-06T06:50:45.000000Z 字数 11119 阅读 1056

Nuxt + Koa2 + Mongodb 手撸一个网上商城

Vue Koa2 NuxtJs


2017年跟着教程做了一个全栈的商场(vue + express + mongodb),2019年,工作中一直做前端,之前学过的都忘了,所以准备用 Nuxt + koa2 + mongodb 重写一次。温故而知新,会增加一些功能,让这个项目更完善,适合初入全栈的前端工程师参考练手。小白看起来会比较吃力,这文档里就是点了几处需要注意的东西,具体实现看源码。


源码地址:https://github.com/FinGet/koa-nuxt-mall
文档地址:https://finget.github.io/2019/08/06/nuxt-koa-mongodb/

项目目录

先来看看整个项目的目录结构,不容易迷路。

├── .nuxt     # nuxt 编译的文件
├── assets    # 静态资源
├── components # 组件
│   └── banner.vue        # 轮播图组件
│   └── footer.vue        # footer组件
│   └── goods.vue         # 首页商品组件
│   └── search.vue        # 搜索组件
│   └── topBar.vue        # topBar组件
│   └── user.vue          # 用户信息组件
├── layout
│   ├── default.vue       # 默认布局文件
├── middleware            # 中间件
│   ├── auth.js           # 用户是否登录
└── pages
│   └── detail            
│       └── _id.vue       # 商品详情页
│   └── cartLists.vue     # 购物车页
│   └── form_mixins.js    # 登录注册表单验证mixins
│   └── index.vue         # 首页
│   └── login.vue         # 登录页
│   └── register.vue      # 注册页
└── plugins
│   └── axios.js          # axios配置
│   └── element-ui.js     # elementui
│   └── filters.js        # 过滤器
└── store
│   └── index.js          # vuex状态管理
└── server                # koa服务端
│   └── dbs               # mongodb数据库配置
│       └── models        # models
│           └── banner.js # 轮播图model
│           └── goods.js  # 商品model
│           └── user.js   # 用户model
│       └── config.js     # 数据库配置连接
│   └── routers           # 服务端路由
│       └── banner.js     # 轮播图路由
│       └── goods.js      # 商品路由
│       └── users.js      # 用户路由
│   └── utils             # 工具函数
│       └── passport.js   # passport登录验证中间件
│   └── index.js          # 服务端入口
└── static  
└── nuxt.config.js        # nuxt配置文件

安装运行项目

这个项目中要用到Mongodb,所以必须安装。

https://www.runoob.com/mongodb/mongodb-osx-install.html

https://www.runoob.com/mongodb/mongodb-window-install.html

https://github.com/FinGet/koa-nuxt-mall

install dependencies
yarn install

serve with hot reload at localhost:3000
yarn dev

build for production and launch server
yarn build
yarn start

generate static project
yarn generate

⚠️点这里:Nuxt爬坑指南。

项目中还用到了Redis来存储session,也可以不用,直接存在内存中。

Redis安装指南。

从零开始手撸

Init Project

  1. npx create-nuxt-app nuxt-koa-mall
  2. // axios + koa + elementui + Eslint 就选这几样
  1. // Install
  2. yarn add @nuxtjs/axios
  3. // SetUp nuxt.config.js
  4. modules: [
  5. '@nuxtjs/axios'
  6. ],
  7. plugin: [
  8. '~/plugins/axios'
  9. ]
  10. // plugins/axios.js
  11. export default function ({ $axios, redirect }) {
  12. $axios.onRequest(config => {
  13. console.log('Making request to ' + config.url)
  14. })
  15. $axios.onResponse(response => {
  16. // console.log(response)
  17. if(response.status == 200) {
  18. return response.data;
  19. }
  20. })
  21. $axios.onError(error => {
  22. const code = parseInt(error.response && error.response.status)
  23. if (code === 400) {
  24. redirect('/400')
  25. }
  26. })
  27. }

我不推荐用sass,反正我每次用yarnnodesass 都会有问题,弃坑!

  1. // Install
  2. yarn less less-loader @nuxtjs/style-resources
  3. // SetUp nuxt.config.js
  4. modules: [
  5. '@nuxtjs/style-resources'
  6. ],
  7. styleResources: {
  8. // 全局注入 less变量 这样在任何页面都可以使用 variate \ mixins
  9. less: ['./assets/css/variate.less','./assets/css/mixins.less']
  10. },

官网说的:warning: You cannot use path aliases here (~ and @),你需要使用相对或绝对路径

Nuxt 开发页面

layouts 页面

默认情况下,pages的所有页面都会引入/layouts/default.vue,另外,/layouts/error.vue也会引入default.vue。可以定义一个空白layout:black.vue作为特殊页面的layout。

  1. // 在页面中设置layout
  2. export default {
  3. layout: 'blank' //默认是default
  4. }
  1. // 在layout中
  2. <template>
  3. <div>
  4. <nuxt /> // 这个是必须定义的,就像是vue的router-view
  5. </div>
  6. </template>

全局过滤器

Nuxt的全局过滤器,定义在plugins下面,在nuxt.config.js中引入。

  1. // plugins/filters
  2. import Vue from 'vue';
  3. Vue.filter('moneyFormat', (value) => {
  4. return `${value}.00`
  5. });
  6. // nuxt.config.js
  7. plugins: [
  8. '~/plugins/filters'
  9. ],

Nuxt路由

在pages下面新建一个vue文件就会生成一个对应的路由,文件名就是路由名。

在这个项目中,商品详情页就是动态路由。在 Nuxt.js 里面定义带参数的动态路由,需要创建对应的以下划线作为前缀的 Vue 文件 或 目录。

  1. pages
  2. --| detail/
  3. -----| _id.vue

Nuxt.js 生成对应的路由配置表为:

  1. router: {
  2. routes: [
  3. {
  4. name: 'detail-id',
  5. path: '/detail/:id?',
  6. component: 'pages/detail/_id.vue'
  7. },
  8. ]
  9. }

更多路由配置去官网查看

asyncData 和 fetch

如果组件不是和路由绑定的页面组件,原则上是不可以使用异步数据的。因为 Nuxt.js 仅仅扩展增强了页面组件的 data 方法,使得其可以支持异步数据处理。--简而言之就是fetchasyncData 在组件上不能用。

Vuex

⚠️在nuxt中,vuex需要导出一个方法。

  1. let store = () => new Vuex.Store({
  2. state,
  3. mutations,
  4. actions
  5. })
  6. export default store

剩下的就跟写vue页面没啥区别了。

koa服务端

koa这里面默认不支持 import xxx from xxx语法,我也没有去改配置,就默认用的moudle.exportsrequire

用到的几个插件:

  1. yarn add koa-json koa-generic-session koa-bodyparser koa-redis koa-passport passport-local koa-router mongoose

koa-json

JSON pretty-printed response middleware. Also converts node object streams to binary.

  1. var json = require('koa-json');
  2. var Koa = require('koa');
  3. var app = new Koa();
  4. app.use(json());
  5. app.use((ctx) => {
  6. ctx.body = { foo: 'bar' };
  7. });
  1. $ GET /
  2. {
  3. "foo": "bar"
  4. }

koa-bodyparser

koa.js并没有内置Request Body的解析器,当我们需要解析请求体时需要加载额外的中间件,官方提供的koa-bodyparser是个很不错的选择,支持x-www-form-urlencoded, application/json等格式的请求体,但不支持form-data的请求体。

也就是说不用这个插件,就拿不到post请求传过来的body内容。

  1. var bodyParser = require('koa-bodyparser');
  2. var Koa = require('koa');
  3. var app = new Koa();
  4. app.use(bodyParser({
  5. enableTypes:['json', 'form', 'text']
  6. }))

koa-generic-session

这就是koa的seesion中间件。koa-passport也需要用到它

  1. const session = require('koa-generic-session');
  2. const Koa = require('koa');
  3. app.keys = ['keys', 'keyskeys']
  4. app.use(session({
  5. key: 'fin',
  6. prefix: 'fin:uid',
  7. maxAge: 1000, /** (number) maxAge in ms (default is 1 days),cookie的过期时间 */
  8. overwrite: true, /** (boolean) can overwrite or not (default true) */
  9. httpOnly: true, /** cookie是否只有服务器端可以访问 (boolean) httpOnly or not (default true) */
  10. signed: true, /** (boolean) signed or not (default true) */
  11. store: new Redis() // 将session存入redis 不传options 默认就是连接127.0.0.1:6379
  12. }))

koa-passport

这是这个项目中很重要的一个中间件。大概逻辑就是,用户登录,它就帮忙把用户信息存在session里,在浏览器端也会生成对应的cookie,还提供了几个方法ctx.isAuthenticated() 用户是否登录,ctx.login()用户登录, ctx.logout()用户退出

passport.js是Nodejs中的一个做登录验证的中间件,极其灵活和模块化,并且可与Express、Sails等Web框架无缝集成。Passport功能单一,即只能做登录验证,但非常强大,支持本地账号验证和第三方账号登录验证(OAuth和OpenID等),支持大多数Web网站和服务。

  1. const passport = require('koa-passport')
  2. const LocalStrategy = require('passport-local')
  3. const User = require('../dbs/models/user')
  4. // 提交数据(策略)
  5. passport.use(new LocalStrategy({
  6. usernameField: 'userName',
  7. passwordField: 'userPwd'
  8. },async function(username,password,done){
  9. let where = {
  10. userName: username
  11. };
  12. let result = await User.findOne(where)
  13. if(result!=null){
  14. if(result.userPwd===password){
  15. return done(null,result)
  16. }else{
  17. return done(null,false,'密码错误')
  18. }
  19. }else{
  20. return done(null,false,'用户不存在')
  21. }
  22. }))
  23. // 序列化ctx.login()触发
  24. passport.serializeUser(function(user,done){
  25. // 用户登录成功之后,会把用户数据存到session当中
  26. done(null,user)
  27. })
  28. // 反序列化(请求时,session中存在"passport":{"user":"1"}触发)
  29. passport.deserializeUser(function(user,done){
  30. return done(null,user)
  31. })
  32. module.exports = passport
  1. const passport = require('./utils/passport');
  2. const Koa = require('koa');
  3. const app = new Koa();
  4. app.use(passport.initialize())
  5. app.use(passport.session())

默认情况下passport使用username和password,也可以自由定义:

  1. passport.use(new LocalStrategy({
  2. usernameField: 'userName',
  3. passwordField: 'password'
  4. },
  5. function(username, password, done) {
  6. // ...
  7. }
  8. ));

app.use(passport.initialize()) app.use(passport.session())要在路由前使用。

点击这里:passport学习资料。

mongodb

MongoDB 是一个基于分布式文件存储的数据库。由 C++ 语言编写。旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。

MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。

MongoDB 将数据存储为一个文档,数据结构由键值(key=>value)对组成。MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组。

更多的mongodb学习资料。

下载链接

安装过程就是选择对应的系统,下一步下一步...

这个项目中没有涉及到关联collection,操作(CURD)起来就像是操作json数据。

Mongoose:一款为异步工作环境设计的 MongoDB 对象建模工具。

去官网看看

mongoose里面有三个概念,schemal、model、entity:

Schema : 一种以文件形式存储的数据库模型骨架,不具备数据库的操作能力
Model : 由Schema发布生成的模型,具有抽象属性和行为的数据库操作
Entity : 由Model创建的实体,他的操作也会影响数据库

  1. const mongoose = require('mongoose')
  2. const dburl = 'mongodb://127.0.0.1:27017/mall' // mall代表数据库名称
  3. // 链接MongoDB数据库
  4. const db = mongoose.connect(dburl)
  5. // 链接成功
  6. mongoose.connection.on("connected", function() {
  7. console.log("MongoDB connected success")
  8. })
  9. // 链接失败
  10. mongoose.connection.on("error", function() {
  11. console.log("MongoDB connected error")
  12. })
  13. // 断开了
  14. mongoose.connection.on("disconnected", function() {
  15. console.log("MongoDB connected disconnected")
  16. })
  17. module.exports = db;

就是mysql里的表结构。

模型使用 Schema 接口进行定义。 Schema 可以定义每个文档中存储的字段,及字段的验证要求和默认值。

mongoose.model() 方法将模式“编译”为模型。模型就可以用来查找、创建、更新和删除特定类型的对象。

注:MongoDB 数据库中,每个模型都映射至一组文档。这些文档包含 Schema 模型定义的字段名/模式类型。

  1. const mongoose = require('mongoose')
  2. const Schema = mongoose.Schema
  3. // 定义模型
  4. const produtSchema = new Schema({
  5. "type": String,
  6. "img_url": String,
  7. "price": Number,
  8. "title": String,
  9. "imgs": Array
  10. })
  11. // 使用模式“编译”模型
  12. module.exports = mongoose.model('Goods', produtSchema)
  1. const schema = new Schema(
  2. {
  3. name: String,
  4. binary: Buffer,
  5. living: Boolean,
  6. updated: { type: Date, default: Date.now },
  7. age: { type: Number, min: 18, max: 65, required: true },
  8. mixed: Schema.Types.Mixed,
  9. _someId: Schema.Types.ObjectId,
  10. array: [],
  11. ofString: [String], // 其他类型也可使用数组
  12. nested: { stuff: { type: String, lowercase: true, trim: true } }
  13. })

没有基础的一定得看看:一篇文章带你入门Mongoose。

koa-router

服务端的路由,定义各个接口的请求方式以及返回的数据。

  1. const Router = require('koa-router')
  2. const Banner = require('../dbs/models/banner.js')
  3. const router = new Router({
  4. prefix: '/banner' // 路由前缀
  5. })
  6. // 获取商品列表 请求方式为get
  7. router.get('/lists', async (ctx) => {
  8. const lists = await Banner.find() // 返回查到的所有数据
  9. ctx.body = {
  10. status: 200,
  11. data: lists
  12. }
  13. })
  14. module.exports = router;

用户注册

  1. router.post('/signup', async (ctx) => {
  2. // ctx.request.body 获取post请求的参数
  3. let { userName, userPwd, email } = ctx.request.body
  4. // 查找数据库中是否存在该用户
  5. let user = await User.find({ userName })
  6. if (user.length) {
  7. ctx.body = {
  8. code: -1,
  9. msg: '该用户,已被注册'
  10. }
  11. return
  12. }
  13. // 创建新用户
  14. let nuser = await User.create({
  15. userName, userPwd, email
  16. })
  17. if (nuser) {
  18. ctx.body = {
  19. status: 200,
  20. data: { userName, email },
  21. msg: '注册成功'
  22. }
  23. } else {
  24. ctx.body = {
  25. status: 0,
  26. msg: '注册失败'
  27. }
  28. }
  29. })

用户登录

  1. router.post('/signin', async (ctx, next) => {
  2. // Passport 本地登录 这是固定用法
  3. return Passport.authenticate('local', function (err, user, info, status) {
  4. if (err) {
  5. ctx.body = {
  6. status: -1,
  7. msg: err
  8. }
  9. } else {
  10. if (user) {
  11. ctx.body = {
  12. status: 200,
  13. msg: '登录成功',
  14. user: {
  15. userName: user.userName,
  16. email: user.userPwd
  17. }
  18. }
  19. // Passport中间件带的ctx.login
  20. return ctx.login(user)
  21. } else {
  22. ctx.body = {
  23. status: 0,
  24. msg: info
  25. }
  26. }
  27. }
  28. })(ctx, next)
  29. })

退出登录

  1. router.get('/exit', async (ctx) => {
  2. // passport 自带logout方法,会清除session cookie
  3. await ctx.logout()
  4. if (!ctx.isAuthenticated()) {
  5. ctx.body = {
  6. status: 200,
  7. msg: '退出登录'
  8. }
  9. } else {
  10. ctx.body = {
  11. code: -1
  12. }
  13. }
  14. })

分页模糊查询

分页查询主要涉及两个方法:skiplimit

skip表示跳过多少个。举个例子,页码(page),每页条数(pageSize),如果page=1,pageSize=10,就是要取前10条数据,那skip就应该 等于0,表示跳过0条。第二页,page=2,再取10条,此时skip就该等于10,要跳过前10条,也就是第一页的10条。一次类推得出:skip = (page - 1) * pageSize

limit就表示限制返回的条数。

  1. // 获取商品列表
  2. router.get('/lists', async (ctx) => {
  3. let pageSize = ctx.request.query.pageSize?parseInt(ctx.request.query.pageSize) : 10
  4. let page = ctx.request.query.page?parseInt(ctx.request.query.page) : 1
  5. let title = ctx.request.query.keyword || ''
  6. let type = ctx.request.query.type || ''
  7. // 数据量不多,所以当搜索含有女的都返回所有女装
  8. if (title.indexOf('女') > -1) {
  9. title = '';
  10. type = 'dress'
  11. } else if (title.indexOf('鞋') > -1) {
  12. title = '';
  13. type = 'shoes'
  14. } else if (title.indexOf('男') > -1) {
  15. title = '';
  16. type = 'manwear'
  17. }
  18. // 跳多少条数据
  19. let skip = (page - 1) * pageSize
  20. // 在nodejs中,必须要使用RegExp,来构建正则表达式对象。模糊查询
  21. let reg = new RegExp(title, 'i')
  22. let params = {}
  23. if (type !== 'all' && type !== '') {
  24. params = {
  25. type: type,
  26. $or: [{ title: { $regex: reg } }]
  27. }
  28. } else {
  29. params = { $or: [{ title: { $regex: reg } }] }
  30. }
  31. // 这params就是搜索条件,这里有个细节,如果要搜索所有类型,type不能传空,不要type就行了
  32. // 总数
  33. const total = await Goods.find(params).count()
  34. // 数据
  35. const lists = await Goods.find(params).skip(skip).limit(pageSize)
  36. if (lists) {
  37. let isMore = total - (((page-1) * pageSize) + lists.length)>0?true:false
  38. ctx.body = {
  39. status: 200,
  40. data: lists,
  41. isMore: isMore
  42. }
  43. } else {
  44. ...
  45. }
  46. })

通过slice方法,其实就是对数组的截取操作。

  1. router.get('/cartLists', async (ctx) => {
  2. let pageSize = 10
  3. let page = ctx.request.query.page?parseInt(ctx.request.query.page) : 1
  4. let skip = (page - 1) * pageSize
  5. let { _id } = ctx.session.passport.user
  6. if (ctx.isAuthenticated()) {
  7. const {cartList} = await User.findOne({'_id': _id}, {"cartList": 1})
  8. // const lists = await User.find({'_id': _id}, {"cartList":{ "$slice":[skip,pageSize]}})
  9. const lists = cartList.slice(skip, pageSize)
  10. if (cartList) {
  11. let isMore = cartList.length - (((page-1) * pageSize) + lists.length)>0?true:false
  12. ctx.body = {
  13. status: 200,
  14. data: lists,
  15. isMore: isMore
  16. }
  17. } else {
  18. ....
  19. }
  20. } else {
  21. ...
  22. }
  23. })

遗留的一些问题和扩展

  1. Nuxt 的 middleware判断用户是否登录。由于components中没法使用fetch,页面刷新时,middleware已经执行了,此时vuex中是没有参数的,就判断为用户没有登录?
  2. mongoose 获取内嵌数组的长度,有没有更好的办法,或者说是既能返回总数也能进行分页?
  3. 订单是在数据中库存了的,没有展示,收货地址也只有增加。这两处都可以扩展增删改查的功能。

最后

项目中所有图片均来自网络,如果存在侵权情况,请第一时间告知。本项目仅做学习交流使用,请勿用于其他用途。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注