2017年跟着教程作了一個全棧的商場(vue + express + mongodb),2019年,工做中一直作前端,以前學過的都忘了,因此準備用 Nuxt + koa2 + mongodb 重寫一次。溫故而知新,會增長一些功能,讓這個項目更完善,適合初入全棧的前端工程師參考練手。小白看起來會比較吃力,這文檔裏就是點了幾處須要注意的東西,具體實現看源碼。
源碼地址: https://github.com/FinGet/koa-nuxt-mall,還過得去的話,斗膽請各位看官賞個star。
文檔地址: https://finget.github.io/2019/08/06/nuxt-koa-mongodb/
先來看看整個項目的目錄結構,不容易迷路。javascript
<pre>
├── .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配置文件
</pre>css
這個項目中要用到Mongodb,因此必須安裝。html
https://www.runoob.com/mongod...前端
https://www.runoob.com/mongod...vue
https://github.com/FinGet/koa...java
install dependencies
yarn installserve with hot reload at localhost:3000
yarn devnodebuild for production and launch server
yarn build
yarn startmysqlgenerate static project
yarn generateios
⚠️點這裏:Nuxt爬坑指南。git
項目中還用到了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'] },
官網說的 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' }, ] }
更多路由配置去官網查看
此方法在加載(渲染)組件(頁面組件,即pages文件夾下的文件,不包含components下的)以前在服務端或路由更新以前被調用,便可以進行異步獲取數據並返回當前組件。
該方法用於渲染頁面(頁面組件加載前被調用【服務端或切換至目標路由以前】)前填充應用的狀態樹(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交流羣:
微信交流羣: