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,因此必須安裝。前端
www.runoob.com/mongodb/mon…vue
www.runoob.com/mongodb/mon…java
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,也能夠不用,直接存在內存中。
npx create-nuxt-app nuxt-koa-mall
// axios + koa + elementui + Eslint 就選這幾樣
複製代碼
// 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')
}
})
}
複製代碼
我不推薦用
sass
,反正我每次用yarn
裝nodesass
都會有問題,棄坑!
// 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 @),你須要使用相對或絕對路徑
默認狀況下,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'
],
複製代碼
在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 此方法在加載(渲染)組件(頁面組件,即pages文件夾下的文件,不包含components下的)以前在服務端或路由更新以前被調用,便可以進行異步獲取數據並返回當前組件。
fetch 該方法用於渲染頁面(頁面組件加載前被調用【服務端或切換至目標路由以前】)前填充應用的狀態樹(store)數據,與asyncData方法相似,不一樣的是它不會設置組件的數據。
若是組件不是和路由綁定的頁面組件,原則上是不可使用異步數據的。由於 Nuxt.js 僅僅擴展加強了頁面組件的 data 方法,使得其能夠支持異步數據處理。--簡而言之就是
fetch
和asyncData
在組件上不能用。
⚠️在nuxt中,vuex須要導出一個方法。
let store = () => new Vuex.Store({
state,
mutations,
actions
})
export default store
複製代碼
剩下的就跟寫vue頁面沒啥區別了。
koa這裏面默認不支持
import xxx from xxx
語法,我也沒有去改配置,就默認用的moudle.exports
和require
。
用到的幾個插件:
yarn add koa-json koa-generic-session koa-bodyparser koa-redis koa-passport passport-local koa-router mongoose
複製代碼
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.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的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
}))
複製代碼
這是這個項目中很重要的一箇中間件。大概邏輯就是,用戶登陸,它就幫忙把用戶信息存在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())
複製代碼
默認狀況下passport使用username和password,也能夠自由定義:
passport.use(new LocalStrategy({
usernameField: 'userName',
passwordField: 'password'
},
function(username, password, done) {
// ...
}
));
複製代碼
app.use(passport.initialize())
app.use(passport.session())
要在路由前使用。
點擊這裏:passport學習資料。
MongoDB 是一個基於分佈式文件存儲的數據庫。由 C++ 語言編寫。旨在爲 WEB 應用提供可擴展的高性能數據存儲解決方案。
MongoDB 是一個介於關係數據庫和非關係數據庫之間的產品,是非關係數據庫當中功能最豐富,最像關係數據庫的。
MongoDB 將數據存儲爲一個文檔,數據結構由鍵值(key=>value)對組成。MongoDB 文檔相似於 JSON 對象。字段值能夠包含其餘文檔,數組及文檔數組。
安裝過程就是選擇對應的系統,下一步下一步...
這個項目中沒有涉及到關聯collection,操做(CURD)起來就像是操做json數據。
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。
服務端的路由,定義各個接口的請求方式以及返回的數據。
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
}
}
})
複製代碼
分頁查詢主要涉及兩個方法:skip
和limit
。
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 {
...
}
})
複製代碼
項目中全部圖片均來自網絡,若是存在侵權狀況,請第一時間告知。本項目僅作學習交流使用,請勿用於其餘用途。
建立了一個前端學習交流羣,感興趣的朋友,一塊兒來嗨呀!源碼中沒有放商品的數據庫文件,加羣能夠得到一份,也能夠本身根據數據結構去建立數據。
qq交流羣:
微信交流羣: