Vue全家桶+MongoDB+Koa2全栈开发网站
github网址MT-PC实战准备项目安装:npm install -g npxnpx create-nuxt-app project-namenpm install --update-binary出现问题:当更改server下面的index.js文件的时候,就是将require改为import后会报错原因:因为node本身不支持import这个指令,解决:使用babe...
·
github网址MT-PC
实战准备
- 项目安装:
- npm install -g npx
- npx create-nuxt-app project-name
- npm install --update-binary
-
- 出现问题:当更改server下面的index.js文件的时候,就是将require改为import后会报错
- 原因:因为node本身不支持import这个指令,
- 解决:使用babel
1. 在package.json文件中更改dev和start,都在配置的末尾加上--exec babel-node 2. 建立.babelrc文件,文件内容为 { "presets": ["es2015"] } 3. 安装插件:npm install babel-preset-es2015 4. 重启服务 npm run dev
- 支持sass语法,安装插件:npm i sass-loader node-sass eslint@^3.18
- 支持axios npm install @nuxtjs/axios
nuxt.config.js: modules: [ '@nuxtjs/axios', ], axios: { },
- 版本介绍:
- Node v10.15.0
- Vue 2.9.6
- NPM 6.4.1
- Webpack 4.1.0
- Nuxt 2.0.0
- 新知识点的网址:
- 项目目录
components changeCity -->城市选择页面的所有 iselect.vue -->按省份选择等,那一栏的 hot.vue -->热门城市 那栏 categroy.vue -->按拼音首字母选择 那栏 products -->产品列表页,就是点击搜索出来的页面 categroy.vue -->分类,区域的部分 crumbs.vue -->中间哈尔滨美团>哈尔滨失恋博物馆 iselect.vue -->分类,区域栏中偏右边的部分,像周边游,香坊区等部分 list.vue --> product.vue --> detail -->产品详情页,就是点击产品出现的页面 crumbs.vue --> item.vue --> list.vue --> summary.vue --> index artistic.vue -->页面下半部分,有格调的那个部分 life.vue -->中间包括轮播图的那部分,几乎全是图片的部分 menu.vue -->全部分类部分 silder.vue -->单独的轮播图组件,在life.vue文件中引用 public header -->包括搜索框往上面的部分 index.vue -->用于导出header下的其他组件 nav.vue -->页面右上角,什么我的美团,网址导航那部分 searchbar.vue -->整个搜索框部分 topbar.vue -->除了搜索框的所有顶部部分 user.vue -->用户登陆注册部分 geo.vue -->页面左上角,城市切换部分 footer index.vue -->底部部分 pages index.vue -->中间部分 register.vue -->注册组件 login.vue -->登录组件 exit.vue -->退出组件 register.vue -->注册组件 changeCity -->城市选择组件 products.vue -->产品列表页 detail.vue -->产品详情页 layout default.vue -->最终显示页面 blank.vue -->放置register.vue,login,exit的模版文件 server dbs models -->放置数据库数据 user.js -->users表,包括usename,password,email categroy.js city.js menu.js poi.js province.js config.js -->数据库配置文件(smtp服务, redis连接, mongodb连接) interface utils axios.js -->定义axios的配置项 passport.js -->利用koa-passport简便的实现登录注册功能(序列化,反序列化,local策略) users.js --> 登录系列接口定义(登录,退出,获取用户名,注册,验证等) geo.js -->城市,系列接口定义(获取所有城市,热门城市,获取省份等) index.js -->定义支持服务的接口文件(passport, session, 路由, 数据库, 处理post请求等) store modules -->vuex子模块 geo.js -->当前城市 home.js -->全部分类下的详细分类 index.js -->vuex模块(汇总子模块并且定义一些操作) redis启动->找到安装目录(develop)->redis-server mongoose启动->找到安装目录(develop)->mongod 支付逻辑在13-1的7.06分处,可以自己写 nuxt.config.js 配置文件:可以引入项目所需文件,像css文件,还可以配置很多其他文件
- 逻辑目录:
layouts/default.vue header组件 topBar Geo User navBar searchBar content:按需要加载 footer组件
首页开发
需求分析(模板设计)
- 思考
- 如何节省网络请求
- 有时候可以考虑将 部分的内容直接写死
- 如何语义化
- DOM最简化
- 如何节省网络请求
城市服务组件
- 业务逻辑:
首先浏览器发出request请求,建立http连接,服务器端可以拿到request.ip,也就是浏览器端向我发起请求的时候,根据http协议,我就可以知道ip地址,然后我拿到ip地址去数据中心做映射,这个ip对应哪个城市,然后就可拿到城市名称,服务器拿到city之后下发给浏览器 - 思考:如何节省网络请求?
- 传统方法:发送两次请求
- 当页面渲染完了,我向服务器发一个请求(可以是空的内容,因为空的内容也会建立链接),建立链接,拿到ip,然后…,最后拿到city,也就是组件是在mounted事件之后发送一个请求,然后服务器给你这个城市的名称,再渲染到组件上去
- 弊端:拿到页面,获取城市,一共发了两次请求,除了浪费请求,还有体验问题,就是闪了一下
- 节省网络请求:发送一次请求
在请求文档的时候,那个时候服务器已经知道你的ip了,在那个时候,完全可以拿到ip对应的城市,这个数据是可以当时返回给你的,不需要额外再建立一次连接,利用vuex同步状态,再利用ssr,就可以做到一次请求就可以拿到数据
- 传统方法:发送两次请求
用户数据&状态
- 业务逻辑:首先浏览器发一个request请求,然后服务器根据passport来验证当前是否是登录用户,passport会查当前redis,因为你发这个请求的时候,它会带着cookie过来,服务器的passport会用你的cookie再和redis去做认证,如果是登录状态的话,它会返回你的用户名
- 网络请求和上面城市组件一样
组件设计:
默认模板配置layouts/default.vue
- layouts/default.vue
<template> <el-container class="layout-default"> <el-header height="197px"> <myHeader></myHeader> </el-header> <el-main> <nuxt/> </el-main> <el-footer height="100%"> <myFooter></myFooter> </el-footer> </el-container> </template> <script> import myHeader from '@/components/public/header/index.vue'; import myFooter from '@/components/public/footer/index.vue'; export default { components:{ myHeader, myFooter, } } </script>
- header部分:mt-app/components/public/header/index.vue
- footer部分:mt-app/components/public/footer/index.vue
- footer注意的地方
在default.vue中引入的时候 <el-footer height="100%"> <myFooter></myFooter> </el-footer> 这个height一定要设置为100%, 否则就出现 只有一部分是灰色 的情况 因为element-ui默认设置为60px,所以我们要设置为100%,就整个背景都是灰色的了
- footer注意的地方
定位:geo组件s
- 位置、引入
- 位置:components/public/header/geo.vue
- 在components/public/header/topBar.vue中被引入
用户登录:user组件
- 位置、引入
- 位置:components/public/header/user.vue
- 在components/public/header/topBar.vue中被引入
- 登录部分:登录或者未登录两种状态
<!-- 登录 --> <template v-if="user"> 欢迎你 <span class="username">{{user}}</span> <nuxt-link to="/exit" class="exit">退出</nuxt-link> </template> <!-- 未登录 --> <template v-else> <nuxt-link to="/login" class="login">立即登录</nuxt-link> <nuxt-link to="/register" class="register">注册</nuxt-link> </template>
右上角->我的美团 手机app 商家中心 网址导航:nav组件
- nav.vue位置、引入:
- 位置:components/public/header/nav.vue
- 在components/public/header/topBar.vue中被引入
- 我的美团部分
用最简单的dom结构实现比较复杂交互 因为"我的美团" 这部分的内容既要兼顾着同级平行结构 又要有照顾到下面"我的订单"等那部分的内容 所以在这里并不将它和"我的订单"等部分内容放在一个结构里,如下: <li class="list"> <nuxt-link to="/my">我的美团</nuxt-link> <dl> <dd><nuxt-link to="/order">我的订单</nuxt-link></dd> <dd><nuxt-link to="/order">我的收藏</nuxt-link></dd> <dd><nuxt-link to="/order">抵用券</nuxt-link></dd> <dd><nuxt-link to="/order">账户设置</nuxt-link></dd> </dl> </li> <li> <nuxt-link to="/order">手机APP</nuxt-link> </li> ... ...
- 网址导航部分
官网上这部分的列表结构是有标题有内容 所以我们采取利用dl不是ul,,因为dl中dt和dd正好符合标题和内容这样的结构,如下: <li class="list site"> <nuxt-link to="/site">网址导航</nuxt-link> <div class="subContainer"> <dl class="hotel"> <dt>酒店旅游</dt> <dd>国际机票</dd> </dl> <dl class="food"> <dt>吃美食</dt> <dd>烤鱼</dd> </dl> <dl class="movie"> <dt>看电影</dt> <dd>热影电影</dd> </dl> <dl class="app"> <dt>手机应用</dt> <dd> <a href="#"> <img src="//s0.meituan.net/bs/fe-web-meituan/e5eeaef/img/appicons/meituan.png" alt="美团app" title="美团app"> </a> </dd> </dl> </div> </li>
搜索框部分:searchBar.vue
- 位置、引入
- 位置:components/public/header/searchbar.vue
- 在components/public/header/index.vue中被引入
- 搜索相关逻辑:
- 热门搜索:聚焦 没有内容的时候显示热门搜索
- 相关搜索:聚焦 有内容时显示相关搜索
- 这两个彼此独立,放在平行结构中,具体实现如下:
1. 利用两个变量 (1)是否聚焦 isFocus:false, (2)搜索框内容是否为空 search: '' 2. 利用计算属性监听: (1)isHotPlace:function(){ //已经聚焦并且搜索内容为空的时候显示热门搜索 return this.isFocus&&!this.search }, (2)isSearchList:function(){ //已经聚焦并且搜索内容不为空的时候显示热门 搜索 return this.isFocus&&this.search } 3. 利用v-if决定是否热门搜索要显示 (1)热门搜索栏<dl class="hotPlace" v-if="isHotPlace"> (2)相关推荐栏<dl class="searchList" v-if="isSearchList"> 4. 绑定事件,实现聚焦显示 <el-input placeholder="搜索商家或地点" v-model="search" @focus="focus" @blur="blur"/> focus: function(){ this.isFocus = true; }, blur: function(){ this.isFocus = false },
- 热门搜索:聚焦 没有内容的时候显示热门搜索
- 问题1:当我聚焦后想点击推荐中的链接的时候,会先触发input事件的blur事件,才能点击,所以在点它(链接)之前,已经触发了blur事件,导致你点击这个链接,没有生效
- 解决:就是我在失去焦点的时候,把isFocus的变化做个延时的处理
blur: function(){ //setInterval和setTimeout中传入函数时,函数中的this会指向window对象,所以用self现将this存起来 let self = this; setTimeout(function(){ self.focus = false },200) }
- 解决:就是我在失去焦点的时候,把isFocus的变化做个延时的处理
- 问题2:我怎么让推荐的内容随着我的输入内容改变,怎么更改数据发出去
- 方法1:监听v-model内容,也就是search
- 方法2: 直接观察input事件,在input标签中增加
<el-input placeholder="搜索商家或地点"@input="input"/>
全部分类部分menu.vue
- 位置、引入:
- 位置:components/index/menu.vue
- 在pages/index.vue中被引入
- 逻辑:
- 结构拆分:一级标题 —>全部分类
数据结构: menu: [ { type:'food', name:'美食', id:11, child:[ { title:'美食', child:['火锅', '汉堡', '小龙虾', '烤冷面', '小可爱'] } ] }, ] dom结构: <dt>全部分类</dt> <dd v-for="(item, index) in menu" :key="index"> <i :class="item.type"/>{{item.name}} <span class="arrow"/> </dd>
- 结构拆分:二级标题 —>子分类(美食,外卖,酒店等)
逻辑:每个标题下面对应的内容都不一样,我怎么确定当鼠标划过,我应该显示哪个内容呢DOM结构: <div class="detail" v-if="kind"> //在每个分类子项这样遍历 <template v-for="(item,index) in curdetail.child"> <h4 :key="index">{{item.title}}</h4> <span v-for="v in item.child" :key="v"> {{v}} </span> </template> </div> 当鼠标划过全部分类部分,触发事件@mouseenter="enter"->enter事件 enter事件 改变kind值为 鼠标划过当前i元素(比如说叫x) 的className值 enter: function(e){ this.kind = e.target.querySelector('i').className }, 计算属性curdetail,当kind改变,重新计算curdetail的值 computed:{ curdetail: function(){ // 设置过滤器 -> 取到所有type和kind相等数据中的第一个 let res = this.menu.filter(item => item.type === this.kind)[0] return res } }, 此时的curdetail中存储的值 就是x对应menu中的数据,然后在dom中进行渲染 然后鼠标离开全部分类大框后绑定事件,@mouseleave="mouseleave" mouseleave事件:让kind值为空,实现鼠标离开后,分类项下的组件不显示 mouseleave(){ let self = this; let self_time = setTimeout(function(){ //延时的原因:我们鼠标移动到分类项下的组件时 //必然:先触发mouseleave事件,然后kind就为'' //因为之前设置组件显示:v-if="kind" //所以此时分类项下的组件又不显示了,就很矛盾,所以这里设置了延迟 self.kind = ''; },150) },
- 关于鼠标滑动事件的处理:
因为全部分类下的分类项和分类项下的组件是并行结构 也就是我要是鼠标移入到分类项下的组件部分,就算做成移出了全部分类 这样的话,依照之前的原理,mouseleave触发事件令kind值为空,组件就会不显示 也就是说,我没法实现:移动到分类项下的组件 所以要解决这个问题 <div class="detail" v-if="kind" @mouseenter="temEnter" @mouseleave="temLeave"> 给 分类项下的组件 绑定事件 @mouseenter="temEnter" //-->如果从全部分类出来,移入到是子分类,就将定时器清除,kind不为'' temEnter: function(){ clearTimeout(this._timer), }, @mouseleave="temLeave" //-->如果从全部分类移出来,不是移入子分类,那就将kind改变为空,不显示子分类 temLeave: function(){ this.kind = '' }
- 结构拆分:一级标题 —>全部分类
休闲生活、住酒店、我是商家,登录,二维码部分:life.vue
- 位置、引入:
- 位置:components/index/life.vue
- 在pages/index.vue中引入
- 中间轮播图部分:
- 位置:components/index/silder.vue
- 在components/index/life.vue中被引入
- 写法:参照Element-UI
https://element.eleme.cn/#/zh-CN/component/carousel
注册组件:register
- 位置、引入
- 位置:pages/register.vue
- 访问 localhost:3000/register
- 编写组件
- 创建组件pages/register.vue
1. 表单样式:参见 Element-UI:https://element.eleme.cn/#/zh-CN/component/form 2. 表单数据见代码里的data 3. 中间有个表单验证规则 一个就是:name,emial什么的都不为空 还有一个验证两次密码相不相等的逻辑 // 二次验证,对比两次密码的内容,需要内置一个函数,支持验证函数的自定义 // validator是一个函数,函数的第一个是rule规则,第二个是value值,第三个是回调 validator:(rule, value, callback) => { if(value === ''){ callback(new Error('请再次输入密码')) }else if(value != this.ruleForm.pwd){ callback(new Error('两次输入密码不一致')) }else{ callback() } }, trigger:'blur'
- 创建模板:layouts/blank.vue
- 使用模板
export default { layout:'blank', }
- 创建模板原因:
因为这个注册组件样式上并不需要header和footer,所以不能使用我们配置好的默认模板:default.vue,要新建一个blank.vue的空模板
- 创建组件pages/register.vue
数据结构设计
用户:数据库设计,接口设计,用户注册、登录逻辑
- 数据库设计:
server dbs models -->放置数据库数据 user.js -->users表,包括usename,password,email config.js -->数据库配置文件(smtp服务, redis连接, mongodb连接)
- axios和passport.js配置关键代码:
- server/interface/utils/passport.js:
配置简单表单验证,具体可以上网找关于passport相关语法// passport是所有的node程序都可以应用的,koa-passport是对它进行了一个封装,适配koa的 import passport from 'koa-passport' // passport-local是passport本地的一个策略 import LocalStrategy from 'passport-local' import UserModel from '../../dbs/models/users' // 第一个参数是一个函数,函数又有三个参数username, password,和回调函数done passport.use(new LocalStrategy(async function(username, password, done){ // console.log(username, password);// 这个username和password就是注册后进行登录操作,传给signin的参数,也就是我刚刚注册的帐户名和密码 // 设置查询条件 let where = { username, }; // 利用模型 let result = await UserModel.findOne(where) if(result != null){ // 根据用户名查出来库里存储的该用户对应的密码,判断是否和当前用户输入的密码一样 if(result.password === password){ return done(null, result) }else{ return done(null, false, '密码错误') } }else{ return done(null, false, '用户不存在') } })) // 如果每次用户进来的时候,都自动通过session去验证 // passport提供的这两个api是固定用法,是库里封装好的api // 序列化:序列化指的是把用户对象存到session里 passport.serializeUser(function(user, done){ // 我查到用户登录验证成功之后,会把用户的数据存储到session中 done(null, user); }) // 反序列化:从session里取用户数据成对象,session 可能是存数据库的或者写文件里的 passport.deserializeUser(function(user, done){ // 在每次请求的时候,会从session中读取用户对象 return done(null, user); }) // 登录验证成功了,我把数据打到cookies中,因为http通信是没有状态的,session是存储在cookies中,存在浏览器端,下次再进来的时候,我会从cookies中把你的session的信息提出来,和服务端的session做验证对比,如果能找到的话,就说明这个人是登录状态,从而达到一个无状态到有状态的转变 export default passport
- server/interface/utils/axios.js:
请求路径,网页等,具体可以上网找关于axios相关知识点import axios from 'axios' const instance = axios.create({ //{process.penv.HOST||'localhost'}:判断当前环境变量的主机,如果host没有设置的话,默认取本机 //{process.env.POST||3000}:判断端口,如果没有的话,设置为3000 baseURL: `http://${process.env.HOST||'localhost'}:${process.env.PORT||3000}`, // 设置超时 timeout:2000, headers:{ } }) export default instance
- server/interface/utils/passport.js:
- 简要接口介绍,具体见代码:server/interface/users.js
- 接口
/users/signup 注册接口 /users/signin 登陆接口 /users/verify 发送验证码接口 /users/exit 退出 /users/getUser 登陆状态获取用户名
- 在server/index.js中引入路由:
import users from './interface/users' app.use(users.routes()).use(users.allowedMethods())
- 将axios和passport和users接口在server/index.js中引入
1. 引入: import mongoose from 'mongoose' // 处理和post相关的请求的包 import bodyParser from 'koa-bodyparser' // 操作session的包 import session from 'koa-generic-session' import Redis from 'koa-redis' ... ... 2. 注册: app.use(session({ key : 'mt', prefix: 'mt:uid', store: new Redis() })) // 扩展类型的配置 app.use(bodyParser({ extendTypes: ['json', 'form' , 'text'] })) // passport相关配置 app.use(passport.initialize()) app.use(passport.session()) ... ...
- 在上述后台配置结束后,在pages/register组件中定义方法,实现注册逻辑
- 发送验证码:sendMsg
1. 先验证用户名,密码是否符合规则 2. 如果符合规则,将用户输入的用户名(username)和密码(email)作为参数,请求/users/verify接口
- 注册:register
1. 判断所有校验逻辑是否正确 2. 将用户输入的:username, password, email, code作为参数,请求接口/users/signup 3. 注意:将password利用crypto-js插件进行加密后再传入, password: CryptoJS.MD5(self.ruleForm.pwd).toString(), 4. 注册成功,跳转到登录页面 location.href = '/login' 5. 注意:定时将错误信息清空,否则会给用户带来误导 setTimeout(function(){ self.error = ''; }, 1500)
- 发送验证码:sendMsg
- 实现登录逻辑pages/login.vue
- 登录login方法:
1. 将登录页面用户输入的username和password作为参数,请求接口/users/signin 2. 同样,密码需要加密 self.$axios.post('/users/signin', { username : window.encodeURIComponent(self.username), password : CryptoJS.MD5(self.password).toString() }) 3. 请求成功跳转到主页面 location.href="/"
- 跳转到主页面后,实现 左上角"立即登录" -> “用户名”
users/components/public/header/user.vue1. 我们已经定义了接口/users/getUser,通过请求这个接口就能获取到用户的用户名 2. 但是我们用什么时候请求接口呢,有两种方式: (1) 在vuex中同步这种状态, (2) 不增加SSR负担,在组件中页面渲染完毕之后 我们再去获取接口,我们这里用异步获取 在mounted生命周期:组件挂载到页面,渲染完毕再去请求,达到异步获取的效果 请求接口,我们可以用promise.then,也可以用async和await,我们这里用async,await
- 登录login方法:
- 退出逻辑pages/exit.vue
- 利用中间件
问:退出(exit.vue)组件中,为什么用中间件来实现退出操作呢, 答: 因为,我们点击users/components/public/header/user.vue文件中的退出后 跳转到 退出页面(page/exit.vue)之后,自动的去执行退出操作 所以利用middleware机制,触发这个获取退出的接口,让这个接口响应完之后, 我们再做自动化的执行动作
- 利用中间件
- 补充:开启SMTP服务
关于数据
- 获取数据获取有两种方式:
- 数据库:
- 数据库数据的导入
1. 进入到mongodb数据库安装位置 2. 执行:mongoimport d student -c areas areas.dat
- 举个栗子:使用数据库中的数据
server/interface/geo.js: import City from '../dbs/models/city' router.get('/province', async(ctx) =>{ let province = await Province.find() ctx.body = { province: province.map(item =>{ return { id: item.id, name: item.value[0] } }) } }) city.js import mongoose from 'mongoose' const Schema = mongoose.Schema const City = new Schema({ id: { type: String, require: true }, value: { type: Array, require: true } }) export default mongoose.model('City', City)
- 数据库数据的导入
- 通过别人的接口获取所有城市数据
- 接口:
http://cp-tools.cn/sign sign = 7296092/4224626
- 举个栗子
import axios from './utils/axios' const sign = '3e59babc3d4d2e7bc9a5b4fe302d574e' router.get('/province', async(ctx) =>{ let {status, data: {province}} = await axios.get(`http://cp-tools.cn/geo/province?sign=${sign}`) ctx.body = { province: status === 200 ? province : [] } })
- 接口:
- 我们这里所有数据获取都主要用接口的方式,可以自己练习一下数据库的方式
- 数据库:
城市服务等:接口设计,显示当前城市逻辑等
- 简要接口介绍,具体见代码:server/interface/geo.js
- 简要接口介绍:
/geo/getPosition 在接口发出请求到服务端,服务端根据当前的ip来查库,给出你当前城市的名称 /geo/province 获取省份的接口 /geo/province/:id 给出你指定的id的省份,每一个省份都有一个对应的id,根据id可以查询到这个省份下面所有管辖的城市 /geo/city 获取所有城市(不是按省份分类的城市) /geo/hotCity 获取热门城市 /geo/menu 获取全部分类下的菜单数据 接口测试工具:postman
- 在server/index.js中引入路由
import geo from './interface/geo' app.use(geo.routes()).use(geo.allowedMethods())
- 如何将接口反映到城市上去
两种办法: (1)直接在组件中请求接口,通过异步的方式,然后更改dom (2)用SSR方式,在服务端渲染的时候,拿到接口的值,返回页面,用户体验更高,因为过来的时候已经带来了结果
- 简要接口介绍:
- 获取当前城市,通过SSR方式渲染在初始页面的左上角:
- 创建文件:
store modules -->vuex子模块 geo.js -->当前城市 index.js -->vuex模块(汇总子模块并且定义一些操作)
- 逻辑
1. 在store/modules/geo.js中定义 改变位置的actions和mutations ->setPosition 2. 在store/index.js中引入geo.js 3. store/index.js中请求接口/geo/getPosition ---> 得到当前位置 4. 将得到的位置提交到vuex 5. components/public/header/geo.vue下使用数据 {{$store.state.geo.position.city}}
- 创建文件:
- 获取全部分类下的子类,通过SSR方式渲染到components/index/menu.vue
- 创建文件:
store modules -->vuex子模块 geo.js -->当前城市 home.js -->全部分类下的子类,和热门城市 index.js -->vuex模块(汇总子模块并且定义一些操作)
- 逻辑
1. 在store/modules/home.js中定义 actions和mutations setMenu 主页左边全部分类的子类 2. 在store/index.js中引入home.js 3. store/index.js中 请求接口/geo/menu ---> 得到所有子类 4. 将得到的子类数据 提交到vuex 5. components/index/menu.vue下使用数据 上面dom数据渲染改为:(item, index) in $store.state.home.menu 下面计算属性curdetail改为 let res = this.$store.state.home.menu.filter(item => item.type === this.kind)[0]
- 创建文件:
- 其他需要了解知识点
- vuex
- Nuxt工作流部分的nuxtServerInit
- 项目总结:
https://www.cnblogs.com/jielin/p/10258316.html
- 实战问答
https://coding.m.imooc.com/questiondetail.html?qid=101986
(通过更改qid后面的内容查看问答) - 关于axios.get,axios.post,router.get/post
axios.get: 请求页面获取数据 axios.post: 通过传递参数,请求页面获取数据的 router.get/post: 对于请求这个路由的浏览器,服务端返回给浏览器的数据
- 如何判断SSR效果是不是正确:通过查看源码,因为这个是服务端打回给自己的模板
搜索相关:接口,搜索逻辑等
- 简要接口介绍,具体见代码:server/interface/search.js
- 接口
/search/top /search/resultsByKeysWords 根据任何一个关键词可以查出来所有相关的列表 /search/hotPlace 热门景点/热门搜索 /search/products 查询列表,我们点击某一个关键词并进入后,它会在产品列表页推荐所有的产品 /search/products/:id 根据每个产品的id查询这个产品的详情
- 在server/index.js中引入路由
import geo from './interface/geo' app.use(geo.routes()).use(geo.allowedMethods())
- 接口
- 搜索:通过调用接口直接返回数据
- 注意:每输入一个字母都进行一次请求,显然浪费性能,所以引入lodash插件
import _ from 'lodash' // 只有在最后一次点击的300ms后,真正的函数func才会触发。 input: _.debounce(async function(){ let self = this; // 将后面的那个市字去掉, 因为第三方服务的限制,带着这个字就查不到 let city = self.$store.state.geo.position.city.replace('市', ''); self.searchList = []; let {status, data:{top}} = await self.$axios.get('/search/top', { params: { input : self.search, city } }) // 数据截取十条 self.searchList = top.slice(0, 10) }, 300)
- lodash详解:
https://segmentfault.com/a/1190000015312430
- 问题:Error: timeout of 1000ms exceeded
在axios.js配置文件中timeout改为2000
- 注意:每输入一个字母都进行一次请求,显然浪费性能,所以引入lodash插件
- 热门城市推荐,通过SSR方式渲染到components/public/header/searchbar.vue
- 定义 获取数据接口:server/interface/search.js
router.get('/hotPlace', async (ctx)=>{ let city = ctx.store?ctx.store.geo.position.city: ctx.query.city; let {status, data:{result}} = await axios.get(`http://cp-tools.cn/search/hotPlace`, { params: { sign, // 服务端没有做编码的要求,所以这里我们不用编码 city: city, } }) ctx.body = { result: status === 200? result : [] } })
- 将热门城市数据存到vuex中
- 创建文件:
store modules -->vuex子模块 geo.js -->当前城市 home.js -->全部分类下的子类,和热门城市 index.js -->vuex模块(汇总子模块并且定义一些操作)
- 存储步骤:
1. 在store/modules/home.js中定义 actions和mutations setHotPlace 热门推荐 2. 在store/index.js中引入home.js 3. store/index.js中 请求接口/search/hotPlace ---> 得到所有热门城市 4. 将得到的子类数据 提交到vuex
- 创建文件:
- 用vuex中的数据重新渲染searchbar.vue中的热门推荐
1. 第一个改动: <dt>热门搜索</dt> <dd v-for="(item, index) in $store.state.home.hotPlace.slice(0, 5)" :key="index"> <a :href="'/products?keyword='+encodeURIComponent(item.name)">{{item.name}}</a> </dd> 2. 第二个改动: <p class="suggest"> <a :href="'/products?keyword='+encodeURIComponent(item.name)" v-for="(item, index) in $store.state.home.hotPlace.slice(0, 5)" :key="index">{{item.name}}</a> </p>
- 定义 获取数据接口:server/interface/search.js
- 有格调部分components/index/artistic.vue,直接通过接口获取数据并渲染
- 接口:server/interface/search.js:/search/resultsByKeysWords
- 渲染:
1. 鼠标划过触发over事件 over事件: 1) 得到鼠标划过当前元素的kind值和keyword值 2) 把keyword和city(从vuex中取)作为参数传到/search/resultsByKeywords中获取数据 3) 将得到的数据做一个过滤,必须有图片的才能显示 4) 将得到的数据再做一个格式化,得到我们渲染dom需要的格式 2. 设置一个默认显示: 因为这个over事件是鼠标滑动才执行的 也就是如果我初始化页面,鼠标没有滑动,那么此时什么都不显示 这不是我们所期望的 解决:在mounted中就发送一次请求,让页面显示数据 和over事件执行的逻辑一样,只不过这个keyword是我们自己设定的默认显示数据
城市选择页面:changeCity
- 位置、引入
- 位置:pages/changeCity.vue
- 访问:localhost:3000/changeCity
- 模板:使用默认default.vue模板
- changeCity中组件
components:{ iSelect, Hot, Categroy }
- 这个页面的难点
- 拼音首字母怎么写,如果写26个英文字母标签再插入,是很失败的
- 如何通过后端给定接口,返回城市后,根据字母来分类
- 一个字母对应城市的显示
- 点击字母,快速定位到该字母对应的所有城市
按省份选择iselect.vue 那栏
- 位置、引入:
- 位置:components/changeCity/iselect.vue
- 在pages/changeCity.vue中被引入
- 逻辑:
- 搜索框参见Elmement-UI:
https://element.eleme.cn/#/zh-CN/component/input
- 确定需要哪些数据province,city…
- 将省份和城市做关联(利用watch监听属性),根据省份获取城市(利用axios)
省份: <el-select v-model="pvalue" placeholder="请选择"> 城市: <el-select v-model="cvalue" placeholder="请选择" :disabled="!city.length"> 联系: 根据pvalue找到该省的所有城市,城市结构的显示 依赖于该省所有城市的长度 这样就实现了城市和省份相关联 watch:{ pvalue: async function(newPvalue){ let self = this; let {status, data:{city}} = await self.$axios.get(`geo/province/${newPvalue}`); if(status == 200){ self.city = city.map(item =>{ return { value:item.id, label:item.name, } }) // 切换省份之后,将上一次选择的城市的值清空 self.cvalue=''; } } }
- 在页面被加载之前将所有省份获取过来,(mounted时候,axios请求数据)
mounted: async function(){ let self = this; let {status, data:{province}} = await self.$axios.get(`geo/province`); self.province = province.map(item =>{ return { value: item.id, label: item.name } }) },
- 直接搜索部分,数据的处理,利用延时处理lodash的debounce函数
DOM结构: <el-autocomplete v-model="input" :fetch-suggestions="querySearchAsync" placeholder="请输入城市中文名或拼音" @select="handleSelect" ></el-autocomplete> 引入lodash:import _ from 'lodash' 两个事件: fetch-suggestions="querySearchAsync" -> 用户输入内容的时候触发的事件 @select="handleSelect" -> 当列表被点击选中的时候,触发这个方法 querySearchAsync:_.debounce(async function(query, cb){ 1. 如果cities有值的话,直接在cities里面搜索 2. 如果citie没有值的话,从geo/city接口获取数据 3. 将获取到的数据格式化,我们只需要value值 4. 将数据进行过滤,就是城市中包含 我搜索关键字的才留下 }, 200), handleSelect:function(item){ 1. 将当前城市设置为item 2. 跳转页面,回到初始页 }
- 注意:直接搜索 范围是全国
- 搜索框参见Elmement-UI:
热门城市hot.vue 那栏
- 位置、引入:
- 位置:components/changeCity/hot.vue
- 在pages/changeCity.vue中被引入
- 逻辑:
- 结构采用dl dt dd,因为是一个标题,很多内容
- 在mounted声明周期函数中获取数据渲染
async mounted(){ let {status, data:{hots}} = await this.$axios.get(`/geo/hotCity`) if(status == 200){ this.list = hots; } }
按拼音首字母选择categroy.vue 那栏
- 位置、引入:
- 位置:components/changeCity/categroy.vue
- 在pages/changeCity.vue中被引入
- 逻辑:
- 确定显示字母用的节点,利用dl dt dd,举个栗子:
<dl class="m-categroy"> <dt>按拼音首字母选择</dt> <dd v-for="item in list" :key="item"> <!-- 因为点击字母要实现跳转,所以要用链接 --> <a :href="'#city-'+item">{{item}}</a> </dd> </dl>
- 点击字母,快速定位到该字母对应的所有城市->利用a标签的#,如下
遍历字母: <dl class="m-categroy"> <dt>按拼音首字母选择</dt> <dd v-for="item in list" :key="item"> <!-- 因为点击字母要实现跳转,所以要用链接 --> <a :href="'#city-'+item">{{item}}</a> </dd> </dl> 遍历每个字母对应的城市: <dl v-for="item in block" :key="item.title" class="m-categroy-section"> <dt :id="'city-'+item.title">{{item.title}}</dt> <dd> <span v-for="c in item.city" :key="c">{{ c }}</span> </dd> </dl> 上面的href和下面的id实现定位
- 左侧字母,右侧城市部分,选择合适的数据格式,有利于dom结点的减少
data(){ return{ list:'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''), // block用来存储 后面用字母 分类城市部分数据,title代表字母,city代表该字母对应城市 // block:[title, city:[]] block:[], } },
- 所有城市获取利用接口/geo/city
let {status, data:{city}} = await self.$axios.get('/geo/city');
- 将每个字母对应的城市选择出来, 将数据改为需要的格式,将字母连带着城市进行排序显示
- 汉语和拼音的转化:利用库
1. 引入: import pyjs from 'js-pinyin'
- 将字母对应城市选择出来
city.forEach(item => { // pyjs.getFullChars->拼音这个库自己本身的api,拿到这个参数的拼音全拼 // toLocaleLowerCase().slice(0, 1) ->转小写,然后拿到首字母 p = pyjs.getFullChars(item.name).toLocaleLowerCase().slice(0, 1); // 拿到p的unicode值 c = p.charCodeAt(0); // 如果没有这个字母的话,就创建一个新的 if(!d[p]){ d[p] = []; } d[p].push(item.name); })
- 将得到的数据由对象格式变为数组
for(let [k, v] of Object.entries(d)){ // 这个k和v就是[key, value] // for of 上网查 blocks.push({ title: k.toUpperCase(), city: v, }) }
- 将字母排序显示
blocks.sort((a, b)=>a.title.charCodeAt(0) - b.title.charCodeAt(0)) self.block = blocks;
- 汉语和拼音的转化:利用库
- 确定显示字母用的节点,利用dl dt dd,举个栗子:
产品列表页(products),就是点击搜索出来的页面:products
- 位置、引入
- 位置:pages/products.vue中被引入
- 访问:localhost:3000/products
- 模板:使用默认default.vue模板
- products中组件
components:{ Crumbs, ->哈尔滨美团哈尔滨哈尔滨融创乐园 Categroy, ->分类,区域的部分 List, -> 智能排序,景点详情部分 Amap ->地图 }
- 逻辑:pages/products.vue
- 通过SSR方式拿数据,举个栗子:
async asyncData(ctx){ let keyword = ctx.query.keyword; let city = ctx.store.state.geo.position.city.replace('市','') || "哈尔滨"; // count:一共多少条数据 // pois:数据 let {status,data:{count,pois}} = await ctx.$axios.get('/search/resultsByKeywords',{ params:{ keyword, city, } }) }
- 将获取到的数据进行
- 过滤:有图片的数据
- 格式化:只取我们需要的数据,并格式化成我们需要的数据格式
- 没有通往这个页面的入口,就是能触发 访问localhost:3000/products 操作的地方
在components/public/header/searchbar.vue中更改两个热门搜索,一个搜索列表,添加 类似如下语句 <a :href="'/products?keyword='+encodeURIComponent(item.name)">{{item.name}}</a>
- 待实现功能:
- 点击排序:点击按价格排序或者按人气排序可以实现下面列表的排序
- 鼠标划过景点简介时候,地图可以定位到相应位置
- 通过SSR方式拿数据,举个栗子:
- 注意decode和encode的问题:query的很多插件在源码中进行了decode,所以用的时候,有的已decode了,自己就没必要再写一遍了,会报错
哈尔滨美团哈尔滨哈尔滨融创乐园:Crumbs
-
位置、引入
- 位置:components/products/crumbs.vue
- 在pages/products.vue中被引入
-
逻辑:
- DOM结构,参见Element-UI:
https://element.eleme.cn/#/zh-CN/component/breadcrumb
- 数据:vuex中取数据
{{ $store.state.geo.position.city.replace('市','') }}美团 {{ $store.state.geo.position.city.replace('市','') }}{{ decodeURIComponent(keyword) }}
- DOM结构,参见Element-UI:
分类,区域的部分:categroy
- 位置、引入
- 位置:components/products/categroy.vue
- 在pages/products.vue中被引入
- categroy中组件
components:{ iselect ->下拉框(划过酒店住宿,周边游出现的下拉框) }
- 逻辑:
- DOM结构:
- 利用dl里面两个dt(分类和全部)和一个dd,dd里面循环引入组件iselect.vue,展示分类右边的数据,像什么酒店住宿,周边游之类的
- 将每一项都用一个公共的组件iselect.vue来实现,通过组件中数据的改变来实现页面的布局
<dl class="classic"> <dt>分类</dt> <dt>全部</dt> <dd v-for="(item,idx) in types" :key="idx"> <-- 下拉框(划过酒店住宿,周边游出现的下拉框) --> <iselect :name="item.type" :list="item.module"/> </dd> </dl>
- 下拉框 components/products/iselect.vue
- DOM结构:举个栗子:酒店住宿
<dl class="tab"> <!-- dt:酒店住宿 --> <dt>{{ name }}<i class="el-icon-arrow-down el-icon--right"/></dt> <dd> <!-- h3:酒店住宿 --> <h3>{{ name }}</h3> <!-- span:全部 公寓民宿 多人出行 --> <span v-for="(item,idx) in list" :key="idx">{{ item }}</span> </dd> </dl>
- DOM结构:举个栗子:酒店住宿
- DOM结构:
智能排序,景点详情部分:list
- 位置、引入
- 位置:components/products/list.vue
- 在pages/products.vue中被引入
- list中组件
import Item from './product.vue' components:{ Item ->每个景点的简要介绍:像几颗星,门票价格,位置等 }
- 逻辑:
- DOM结构
- 采用dl和dd,将智能排序,价格排序,人气等放在一个数组中,利用v-for循环输出数据
- 每个景点的信息利用组件(item)循环输出,每个item包括图片,描述等信息
1. 智能排序 价格排序 人气最高 评价最高 <dd v-for="item in nav" :key="item.name" :class="[item.name,item.acitve?'s-nav-active':'']" @click="navSelect" >{{ item.txt }}</dd> 2. 景点的简要介绍:Item(import Item from './product.vue') <Item v-for="(item,idx) in list" :key="idx" :meta="item"/>
- 景点的简要介绍:components/products/product.vue
- DOM结构:参见Element-UI:
https://element.eleme.cn/#/zh-CN/component/rate
- 数据:父组件传递
- DOM结构:参见Element-UI:
- DOM结构
地图控件Amap
- 位置、引入
- 位置:components/public/map.vue
- 在pages/products.vue中被引入
https://lbs.amap.com/api/javascript-api/guide/overlays/toolbar
详情页开发 detail.vue
-
需求分析
-
位置、引入
- 位置:pages/detail.vue
- 访问 localhost:3000/detail.vue
- 模板:使用默认default.vue模板
- detail.vue中组件
components:{ Crumbs, ->哈尔滨美团 > 哈尔滨美食 > 哈尔滨火锅 Summa, ->商品详情 List ->商家团购及优惠下的列表 }
- 跳转到该路由的链接:components/products/product.vue
<h3><nuxt-link :to="{path:'detail',query:{keyword:meta.name,type:meta.module}}">{{ meta.name }}</nuxt-link></h3>
-
逻辑:
- 判断是否显示:商家团购及优惠,显示的条件是登录或者有数据,利用v-if实现
<el-row v-if="canOrder || !login"> <el-col :span="24"> <!-- 下面这两个list和div是平行结构,只能有一个显示 --> <!-- 如果登录显示list组件 --> <list v-if="login" :list="list"/> <!-- 如果没登录,显示未登录 --> <div v-else></div> </el-col> </el-row>
- 思考:访问(详情页)localhost:3000/detail.vue时的请求参数:keyword,type,
为什么不在data中获取,而是asyncData中在访问localhost:3000/detail.vue时的请求参数keyword,type 只能通过:let {keyword,type}=ctx.query,在服务器端获取到 而asyncData中正好是在服务器端执行的, 所以写在asyncData中 代码见:pages/detail.vue中
- 在detail.vue中请求/search/products后
(请求回来的数据传递路线: detail.vue->list.vue->item.vue)
返回数据格式如下原因:和data关联,所以,返回数据后,data就不用同样再写一次了return { keyword, product, type, list, login }
- 判断是否显示:商家团购及优惠,显示的条件是登录或者有数据,利用v-if实现
商家团购及优惠下的列表 List
- 位置、引入
- 位置:components/details/list.vue
- 在pages/detail.vue中被引入
- list.vue中的组件
components:{ item ->每条数据 }
- 逻辑:
- DOM结构:
<ul> <li>{{ list.filter(item=>item.photos.length).length }}款套餐</li> <item v-for="(item,idx) in list" :key="idx" :meta="item" /> </ul>
- 数据的获取:两种方式
- SSR:我在页面下发的时候就把数据塞进去了
- SSR方式,用户体验好,用户直接拿到信息,
- 连 接口都保护起来了,比如说我创建购物车接口,我根本就暴露不出来,因为这个动作是在服务端执行的,客户端看不到创建购物车
- 拿到空页面之后额外请求数据
- SSR:我在页面下发的时候就把数据塞进去了
- item组件(components/details/item.vue)
- 用于渲染DOM结构的数据获取:
pages/detail.vue请求接口/search/products 将数据传递给components/details/list.vue list.vue将数据传递给item组件
- 点击抢购商品,创建购物车
1. 请求接口/cart/create:创建购物车,将刚创建的购物车id返回 2. 创建成功后,根据购物车id跳转到购物车页面->pages/cart.vue 3. 补充: 实际应用中,浏览器传给服务端一个产品的id 然后这个id对应产品库中的某个商品 然后再将该商品的名称,价钱等信息传给服务端, 但是我们这里没有真正的产品库,所以 只能通过 直接传给服务端商品的名称,价钱等信息 来获取服务器端对应的数据 这样的方式
- 创建购物车接口::server/interface/cart.js->/cart/create
接口实现功能: 1. 登录验证 2. 将购物车信息存入数据库中 3. 将创建好的购物车id返回给客户端 注册路由,让路由生效 server/index.js中: import cart from './interface/cart' app.use(cart.routes()).use(cart.allowedMethods())
- 用于渲染DOM结构的数据获取:
- DOM结构:
购物车:cart
- 位置、引入
- 位置:pages/cart.vue
- 访问 localhost:3000/cart.vue
- 模板:使用默认default.vue模板
- cart.vue中组件
components:{ list ->订单列表 }
- 跳转到该路由的链接:components/details/item.vue
window.location.href=`/cart/?id=${id}`
- 逻辑
- DOM结构:设计一个平行结构,考虑购物车为空和不为空的两种情况
<el-row class="page-cart"> <!-- 购物车不为空的时候 --> <el-col v-if="cart.length" :span="24" class="m-cart"> ... ... <list :cart-data="cart"/> ... ... </el-col> <!-- 购物车为空的时候 --> <el-col v-else class="empty">购物车为空</el-col> </el-row>
- 订单列表list.vue(components/cart/list.vue)
- DOM结构:参见Element-UI:
https://element.eleme.cn/#/zh-CN/component/table
- 数据:
父组件pages/cart.vue通过SSR获取数据(通过这个接口:/cart/getCart) 传给子组件list.vue 所有订单数据,由子组件全部渲染出来
- 逻辑:
父组件通过接口获取数据,传入子组件数组,存储在cartData中, 子组件通过Element-UI结构渲染数据, 如果我在子组件中更改了购买商品的数量,也就是cartData中的值被更改了, 那么,我们在父组件监听的total(所有订单总价),也就会重新计算 然后重新渲染父组件中 下面这个结构中的数据 <p> 应付金额:<em class="money">¥{{ total }}</em> </p>
- 注意:仔细看一下list.vue的数据计算和DOM结构!有一部分需要好好理解
- DOM结构:参见Element-UI:
- 提交订单:点击"提交订单",请求/order/createOrder接口,如果请求成功,跳转页面至全部订单页
- DOM结构:设计一个平行结构,考虑购物车为空和不为空的两种情况
全部订单页:order
- 需求分析
- 位置、引入
- 位置:pages/order.vue
- 访问 localhost:3000/order.vue
- 模板:使用默认default.vue模板
- detail.vue中组件
components:{ List ->订单列表 }
- 跳转到该路由的链接:pages/cart.vue
this.$alert(`恭喜您,已成功下单,订单号:${id}`, '下单成功', { confirmButtonText: '确定', callback: action => { location.href = '/order' } }) }
- 创建订单和返回全部订单接口:server/interface/order.js
/order/createOrder接口实现功能: 1. 根据请求接口的参数的:id(购物车id), price, count加上一些其他参数创建订单 2. 将订单存储到数据库中 /order/getOrders返回数据库中全部订单 最后:注册路由,让路由生效 server/index.js中: import order from './interface/order' app.use(order.routes()).use(order.allowedMethods())
- 逻辑
- DOM结构:参见Element-UI:
https://element.eleme.cn/#/zh-CN/component/tabs
- 获取全部订单,通过SSR方式渲染到pages/order中的list组件(components/order/list.vue)
- 从接口/order/getOrders获取全部订单数据
- 将数据格式化为 我们渲染页面想要的格式
async asyncData(ctx) { const { status, data: { code, list }} = await ctx.$axios.post('/order/getOrders') if (status === 200 && code === 0 && list.length) { return { // 将后端返回数据和前端数据进行映射 list: list.map(item => { return { img: item.imgs.length ? item.imgs[0].url : 'https://i.loli.net/2019/01/10/5c3767c4a52de.png', name: item.name, count: 1, total: item.total, status: item.status, statusText: item.status === 0 ? '待付款' : '已付款' } }), } } }
- 点击"全部订单"或者"待付款"或者"待使用"等,样式和数据对应改变
点击元素,触发handleClick事件 handleClick(tab) { this.activeName = tab.name } 监听activeName,如果改变,则改变数据 activeName(val) { //cur就是传递给当前应该显示的数据,默认是全部 this.cur = this.list.filter(item => { if (val === 'unpay') { return item.status === 0 } else if (val === 'all') { return true } else { return false } }) },
- DOM结构:参见Element-UI:
问题
- 搜索失去焦点,热门推荐还在
- 还有莫名其妙会报错,会出现什么靓丽什么的搜索结果
- 注册时候同一个验证码也可以注册
更多推荐
已为社区贡献2条内容
所有评论(0)