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交流群:
微信交流群: