Nuxt + Koa2 + Mongodb 手擼一個網上商城

2017年跟着教程作了一個全棧的商場(vue + express + mongodb),2019年,工做中一直作前端,以前學過的都忘了,因此準備用 Nuxt + koa2 + mongodb 重寫一次。溫故而知新,會增長一些功能,讓這個項目更完善,適合初入全棧的前端工程師參考練手。小白看起來會比較吃力,這文檔裏就是點了幾處須要注意的東西,具體實現看源碼。javascript


源碼地址:github.com/FinGet/koa-…,還過得去的話,斗膽請各位看官賞個star。 文檔地址:finget.github.io/2019/08/06/…css

項目目錄

先來看看整個項目的目錄結構,不容易迷路。html

├── .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,因此必須安裝。前端

  • Mac 安裝mongodb

www.runoob.com/mongodb/mon…vue

  • Windows 安裝mongodb

www.runoob.com/mongodb/mon…java

  • 克隆項目

github.com/FinGet/koa-…node

install dependencies yarn installmysql

serve with hot reload at localhost:3000 yarn devios

build for production and launch server yarn build yarn startgit

generate static project yarn generate

⚠️點這裏:Nuxt爬坑指南。

項目中還用到了Redis來存儲session,也能夠不用,直接存在內存中。

Redis安裝指南。

從零開始手擼

Init Project

npx create-nuxt-app nuxt-koa-mall
// axios + koa + elementui + Eslint 就選這幾樣
複製代碼
  • Install & SetUp Axios
// Install
yarn add @nuxtjs/axios


// SetUp nuxt.config.js 
modules: [
  '@nuxtjs/axios'
],
plugin: [
  '~/plugins/axios'
]
// plugins/axios.js
export default function ({ $axios, redirect }) {
  $axios.onRequest(config => {
    console.log('Making request to ' + config.url)
  })
  $axios.onResponse(response => {
    // console.log(response)
    if(response.status == 200) {
      return response.data;
    }
  })
  $axios.onError(error => {
    const code = parseInt(error.response && error.response.status)
    if (code === 400) {
      redirect('/400')
    }
  })
}
複製代碼
  • Install & SetUp Less

我不推薦用sass,反正我每次用yarnnodesass 都會有問題,棄坑!

// Install
yarn less less-loader @nuxtjs/style-resources

// SetUp nuxt.config.js 
modules: [
  '@nuxtjs/style-resources'
],
styleResources: {
  // 全局注入 less變量 這樣在任何頁面均可以使用 variate \ mixins
  less: ['./assets/css/variate.less','./assets/css/mixins.less']
},
複製代碼

官網說的:warning: You cannot use path aliases here (~ and @),你須要使用相對或絕對路徑

Nuxt 開發頁面

layouts 頁面

默認狀況下,pages的全部頁面都會引入/layouts/default.vue,另外,/layouts/error.vue也會引入default.vue。能夠定義一個空白layout:black.vue做爲特殊頁面的layout。

// 在頁面中設置layout
export default {
  layout: 'blank' //默認是default
}
複製代碼
// 在layout中
<template>
  <div>
    <nuxt /> // 這個是必須定義的,就像是vue的router-view
  </div>
</template>
複製代碼

全局過濾器

Nuxt的全局過濾器,定義在plugins下面,在nuxt.config.js中引入。

// plugins/filters
import Vue from 'vue';

Vue.filter('moneyFormat', (value) => {
  return `${value}.00`
});

// nuxt.config.js
plugins: [
  '~/plugins/filters'
],
複製代碼

Nuxt路由

  • 基礎路由

在pages下面新建一個vue文件就會生成一個對應的路由,文件名就是路由名。

  • 動態路由

在這個項目中,商品詳情頁就是動態路由。在 Nuxt.js 裏面定義帶參數的動態路由,須要建立對應的如下劃線做爲前綴的 Vue 文件 或 目錄。

pages
--| detail/
-----| _id.vue
複製代碼

Nuxt.js 生成對應的路由配置表爲:

router: {
  routes: [
    {
      name: 'detail-id',
      path: '/detail/:id?',
      component: 'pages/detail/_id.vue'
    },
  ]
}
複製代碼

更多路由配置去官網查看

asyncData 和 fetch

  • asyncData 此方法在加載(渲染)組件(頁面組件,即pages文件夾下的文件,不包含components下的)以前在服務端或路由更新以前被調用,便可以進行異步獲取數據並返回當前組件。

  • fetch 該方法用於渲染頁面(頁面組件加載前被調用【服務端或切換至目標路由以前】)前填充應用的狀態樹(store)數據,與asyncData方法相似,不一樣的是它不會設置組件的數據。

若是組件不是和路由綁定的頁面組件,原則上是不可使用異步數據的。由於 Nuxt.js 僅僅擴展加強了頁面組件的 data 方法,使得其能夠支持異步數據處理。--簡而言之就是fetchasyncData 在組件上不能用。

Vuex

⚠️在nuxt中,vuex須要導出一個方法。

let store = () => new Vuex.Store({
  state,
  mutations,
  actions
})

export default store
複製代碼

剩下的就跟寫vue頁面沒啥區別了。

koa服務端

koa這裏面默認不支持 import xxx from xxx語法,我也沒有去改配置,就默認用的moudle.exportsrequire

用到的幾個插件:

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.

var json = require('koa-json');
var Koa = require('koa');
var app = new Koa();
 
app.use(json());
 
app.use((ctx) => {
  ctx.body = { foo: 'bar' };
});
複製代碼
$ GET /
 
{
  "foo": "bar"
}
複製代碼

koa-bodyparser

koa.js並無內置Request Body的解析器,當咱們須要解析請求體時須要加載額外的中間件,官方提供的koa-bodyparser是個很不錯的選擇,支持x-www-form-urlencoded, application/json等格式的請求體,但不支持form-data的請求體。

也就是說不用這個插件,就拿不到post請求傳過來的body內容。

var bodyParser = require('koa-bodyparser');
var Koa = require('koa');

var app = new Koa();
app.use(bodyParser({
  enableTypes:['json', 'form', 'text']
}))
複製代碼

koa-generic-session

這就是koa的seesion中間件。koa-passport也須要用到它

const session = require('koa-generic-session');
const Koa = require('koa');
app.keys = ['keys', 'keyskeys']
app.use(session({
  key: 'fin',
  prefix: 'fin:uid',
  maxAge: 1000, /** (number) maxAge in ms (default is 1 days),cookie的過時時間 */
  overwrite: true, /** (boolean) can overwrite or not (default true) */
  httpOnly: true, /** cookie是否只有服務器端能夠訪問 (boolean) httpOnly or not (default true) */
  signed: true, /** (boolean) signed or not (default true) */
  store: new Redis() // 將session存入redis 不傳options 默認就是鏈接127.0.0.1:6379
}))
複製代碼

koa-passport

這是這個項目中很重要的一箇中間件。大概邏輯就是,用戶登陸,它就幫忙把用戶信息存在session裏,在瀏覽器端也會生成對應的cookie,還提供了幾個方法ctx.isAuthenticated() 用戶是否登陸,ctx.login()用戶登陸, ctx.logout()用戶退出

passport.js是Nodejs中的一個作登陸驗證的中間件,極其靈活和模塊化,而且可與Express、Sails等Web框架無縫集成。Passport功能單一,即只能作登陸驗證,但很是強大,支持本地帳號驗證和第三方帳號登陸驗證(OAuth和OpenID等),支持大多數Web網站和服務。

const passport = require('koa-passport')
const LocalStrategy = require('passport-local')
const User = require('../dbs/models/user')

// 提交數據(策略)
passport.use(new LocalStrategy({
  usernameField: 'userName',
  passwordField: 'userPwd'
},async function(username,password,done){
  let where = {
    userName: username
  };
  let result = await User.findOne(where)
  if(result!=null){
    if(result.userPwd===password){
      return done(null,result)
    }else{
      return done(null,false,'密碼錯誤')
    }
  }else{
    return done(null,false,'用戶不存在')
  }
}))
// 序列化ctx.login()觸發
passport.serializeUser(function(user,done){
  // 用戶登陸成功以後,會把用戶數據存到session當中
  done(null,user)
})

// 反序列化(請求時,session中存在"passport":{"user":"1"}觸發)
passport.deserializeUser(function(user,done){
  return done(null,user)
})

module.exports = passport
複製代碼
const passport = require('./utils/passport');
const Koa = require('koa');

const app = new Koa();

app.use(passport.initialize())
app.use(passport.session())
複製代碼
  • 報錯Missing credentials

默認狀況下passport使用username和password,也能夠自由定義:

passport.use(new LocalStrategy({
    usernameField: 'userName',
    passwordField: 'password'
  },
  function(username, password, done) {
  // ...
  }
));
複製代碼
  • 報錯 ctx.login is not a function

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

點擊這裏:passport學習資料。

mongodb

MongoDB 是一個基於分佈式文件存儲的數據庫。由 C++ 語言編寫。旨在爲 WEB 應用提供可擴展的高性能數據存儲解決方案。

MongoDB 是一個介於關係數據庫和非關係數據庫之間的產品,是非關係數據庫當中功能最豐富,最像關係數據庫的。

MongoDB 將數據存儲爲一個文檔,數據結構由鍵值(key=>value)對組成。MongoDB 文檔相似於 JSON 對象。字段值能夠包含其餘文檔,數組及文檔數組。

更多的mongodb學習資料。

  • 安裝mongodb可視化工具

下載連接

安裝過程就是選擇對應的系統,下一步下一步...

這個項目中沒有涉及到關聯collection,操做(CURD)起來就像是操做json數據。

  • mongoose

Mongoose:一款爲異步工做環境設計的 MongoDB 對象建模工具。

去官網看看

mongoose裏面有三個概念,schemal、model、entity:

Schema : 一種以文件形式存儲的數據庫模型骨架,不具有數據庫的操做能力 Model : 由Schema發佈生成的模型,具備抽象屬性和行爲的數據庫操做 Entity : 由Model建立的實體,他的操做也會影響數據庫

  • 鏈接數據庫
const mongoose = require('mongoose')

const dburl = 'mongodb://127.0.0.1:27017/mall' // mall表明數據庫名稱

// 連接MongoDB數據庫
const db = mongoose.connect(dburl)
// 連接成功
mongoose.connection.on("connected", function() {
    console.log("MongoDB connected success")
})
// 連接失敗
mongoose.connection.on("error", function() {
    console.log("MongoDB connected error")
})
// 斷開了
mongoose.connection.on("disconnected", function() {
    console.log("MongoDB connected disconnected")
})

module.exports = db;
複製代碼
  • 定義和添加模型

就是mysql裏的表結構。

模型使用 Schema 接口進行定義。 Schema 能夠定義每一個文檔中存儲的字段,及字段的驗證要求和默認值。

mongoose.model() 方法將模式「編譯」爲模型。模型就能夠用來查找、建立、更新和刪除特定類型的對象。

注:MongoDB 數據庫中,每一個模型都映射至一組文檔。這些文檔包含 Schema 模型定義的字段名/模式類型。

const mongoose = require('mongoose')
const Schema = mongoose.Schema

// 定義模型
const produtSchema = new Schema({
  "type": String,
  "img_url": String,
  "price": Number,
  "title": String,
  "imgs": Array
})

// 使用模式「編譯」模型
module.exports = mongoose.model('Goods', produtSchema)
複製代碼
  • 常見字段類型和聲明方式
const schema = new Schema(
{
  name: String,
  binary: Buffer,
  living: Boolean,
  updated: { type: Date, default: Date.now },
  age: { type: Number, min: 18, max: 65, required: true },
  mixed: Schema.Types.Mixed,
  _someId: Schema.Types.ObjectId,
  array: [],
  ofString: [String], // 其餘類型也可以使用數組
  nested: { stuff: { type: String, lowercase: true, trim: true } }
})
複製代碼

沒有基礎的必定得看看:一篇文章帶你入門Mongoose。

koa-router

服務端的路由,定義各個接口的請求方式以及返回的數據。

  • 示例banner 輪播圖
const Router = require('koa-router')
const Banner = require('../dbs/models/banner.js')

const router = new Router({
  prefix: '/banner' // 路由前綴
})

// 獲取商品列表 請求方式爲get
router.get('/lists', async (ctx) => {
  const lists = await Banner.find() // 返回查到的全部數據

  ctx.body = {
    status: 200,
    data: lists
  }
})

module.exports = router;
複製代碼

用戶註冊

router.post('/signup', async (ctx) => {
  // ctx.request.body 獲取post請求的參數
  let { userName, userPwd, email } = ctx.request.body
 
  // 查找數據庫中是否存在該用戶
  let user = await User.find({ userName })
  if (user.length) {
    ctx.body = {
      code: -1,
      msg: '該用戶,已被註冊'
    }
    return
  }
  // 建立新用戶
  let nuser = await User.create({
    userName, userPwd, email
  })

  if (nuser) {
    ctx.body = {
      status: 200,
      data: { userName, email },
      msg: '註冊成功'
    }
  } else {
    ctx.body = {
      status: 0,
      msg: '註冊失敗'
    }
  }

})
複製代碼

用戶登陸

router.post('/signin', async (ctx, next) => {
  // Passport 本地登陸 這是固定用法
  return Passport.authenticate('local', function (err, user, info, status) {
    if (err) {
      ctx.body = {
        status: -1,
        msg: err
      }
    } else {
      if (user) {
        ctx.body = {
          status: 200,
          msg: '登陸成功',
          user: {
            userName: user.userName,
            email: user.userPwd
          }
        }
        // Passport中間件帶的ctx.login
        return ctx.login(user)
      } else {
        ctx.body = {
          status: 0,
          msg: info
        }
      }
    }
  })(ctx, next)
})
複製代碼

退出登陸

router.get('/exit', async (ctx) => {
// passport 自帶logout方法,會清除session cookie
  await ctx.logout()
  if (!ctx.isAuthenticated()) {
    ctx.body = {
      status: 200,
      msg: '退出登陸'
    }
  } else {
    ctx.body = {
      code: -1
    }
  }
})
複製代碼

分頁模糊查詢

  • 第一種商品列表

分頁查詢主要涉及兩個方法:skiplimit

skip表示跳過多少個。舉個例子,頁碼(page),每頁條數(pageSize),若是page=1,pageSize=10,就是要取前10條數據,那skip就應該 等於0,表示跳過0條。第二頁,page=2,再取10條,此時skip就該等於10,要跳過前10條,也就是第一頁的10條。一次類推得出:skip = (page - 1) * pageSize

limit就表示限制返回的條數。

// 獲取商品列表
router.get('/lists', async (ctx) => {
  let pageSize = ctx.request.query.pageSize?parseInt(ctx.request.query.pageSize) : 10
  let page = ctx.request.query.page?parseInt(ctx.request.query.page) : 1
  let title = ctx.request.query.keyword || ''
  let type = ctx.request.query.type || ''
    
 // 數據量很少,因此當搜索含有女的都返回全部女裝
  if (title.indexOf('女') > -1) {
    title = '';
    type = 'dress'
  } else if (title.indexOf('鞋') > -1) {
    title = '';
    type = 'shoes'
  } else if (title.indexOf('男') > -1) {
    title = '';
    type = 'manwear'
  }
  // 跳多少條數據
  let skip = (page - 1) * pageSize

  // 在nodejs中,必需要使用RegExp,來構建正則表達式對象。模糊查詢
  let reg = new RegExp(title, 'i')

  let params = {}
  if (type !== 'all' && type !== '') {
    params = {
      type: type,
      $or: [{ title: { $regex: reg } }]
    }
  } else {
    params = { $or: [{ title: { $regex: reg } }] }
  }
  
  // 這params就是搜索條件,這裏有個細節,若是要搜索全部類型,type不能傳空,不要type就好了

  // 總數
  const total = await Goods.find(params).count()
  // 數據
  const lists = await Goods.find(params).skip(skip).limit(pageSize)

  if (lists) {
    let isMore = total - (((page-1) * pageSize) + lists.length)>0?true:false
    ctx.body = {
      status: 200,
      data: lists,
      isMore: isMore
    }
  } else {
    ...
  }
})
複製代碼
  • 第二種購物車列表

經過slice方法,其實就是對數組的截取操做。

router.get('/cartLists', async (ctx) => {
  let pageSize = 10
  let page = ctx.request.query.page?parseInt(ctx.request.query.page) : 1
  let skip = (page - 1) * pageSize
  let { _id } = ctx.session.passport.user
  
  if (ctx.isAuthenticated()) { 
    const {cartList} = await User.findOne({'_id': _id}, {"cartList": 1})
    
    // const lists = await User.find({'_id': _id}, {"cartList":{ "$slice":[skip,pageSize]}})

    const lists = cartList.slice(skip, pageSize)

    if (cartList) {
      let isMore = cartList.length - (((page-1) * pageSize) + lists.length)>0?true:false
      ctx.body = {
        status: 200,
        data: lists,
        isMore: isMore
      }
    } else {
        ....
    }
  } else {
    ...
  }
})
複製代碼

遺留的一些問題和擴展

  1. Nuxt 的 middleware判斷用戶是否登陸。因爲components中無法使用fetch,頁面刷新時,middleware已經執行了,此時vuex中是沒有參數的,就判斷爲用戶沒有登陸?
  2. mongoose 獲取內嵌數組的長度,有沒有更好的辦法,或者說是既能返回總數也能進行分頁?
  3. mongodb我也是現學現賣,查詢語句寫的可能不是最優的,僅做參考。
  4. 訂單是在數據中庫存了的,沒有展現,收貨地址也只有增長。這兩處均可以擴展增刪改查的功能。

最後

項目中全部圖片均來自網絡,若是存在侵權狀況,請第一時間告知。本項目僅作學習交流使用,請勿用於其餘用途。

建立了一個前端學習交流羣,感興趣的朋友,一塊兒來嗨呀!源碼中沒有放商品的數據庫文件,加羣能夠得到一份,也能夠本身根據數據結構去建立數據。

qq交流羣:

qq羣

微信交流羣:

微信羣
相關文章
相關標籤/搜索