Vue全家桶+SSR+Koa2实现美团网

因为是第一次在segmentfault上面写文章,所以有很多格式调不对,导致这篇文章看着很乱,很多序号也是不对的
如果喜欢这篇文章的话大家可以看我在CDSN上的发布
Vue全家桶+MongoDB+Koa2全栈开发美团网[https://blog.csdn.net/LFY8361...]
谢谢~

github网址[https://github.com/LFY836126/...]
慕课实战视频:[https://coding.imooc.com/clas...]

实战准备

  1. 项目安装:

    • 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
  2. 支持sass语法,安装插件:npm i sass-loader node-sass eslint@^3.18
  3. 支持axios npm install @nuxtjs/axios

     nuxt.config.js:
        modules: [
            '@nuxtjs/axios',
        ],
        axios: {
        
      },
  4. 版本介绍:

    • Node v10.15.0
    • Vue 2.9.6
    • NPM 6.4.1
    • Webpack 4.1.0
    • Nuxt 2.0.0
  5. 新知识点的网址:

    • koa-passport: https://segmentfault.com/a/1190000011557953/https://www.jianshu.com/p/622561ec7be2
    • axios: https://segmentfault.com/q/1010000016473273?sort=created
    • 邮件发送:https://www.jianshu.com/p/04e596da7d33
    • koa: https://www.liaoxuefeng.com/wiki/1022910821149312/1099752344192192
    • koa-route: https://www.jianshu.com/p/0d59a4270997
    • 实战笔记:https://www.cnblogs.com/xiaozhumaopao
  6. 项目目录

            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文件,还可以配置很多其他文件
  7. 逻辑目录:

        layouts/default.vue
            header组件
                topBar 
                    Geo
                    User
                    navBar
                searchBar
            content:按需要加载
            footer组件

    首页开发

    需求分析(模板设计)

  8. 思考

    • 如何节省网络请求

      • 有时候可以考虑将 部分的内容直接写死
    • 如何语义化
    • DOM最简化

    城市服务组件

  9. 业务逻辑:
    首先浏览器发出request请求,建立http连接,服务器端可以拿到request.ip,也就是浏览器端向我发起请求的时候,根据http协议,我就可以知道ip地址,然后我拿到ip地址去数据中心做映射,这个ip对应哪个城市,然后就可拿到城市名称,服务器拿到city之后下发给浏览器
  10. 思考:如何节省网络请求?

    • 传统方法:发送两次请求

      • 当页面渲染完了,我向服务器发一个请求(可以是空的内容,因为空的内容也会建立链接),建立链接,拿到ip,然后.....,最后拿到city,也就是组件是在mounted事件之后发送一个请求,然后服务器给你这个城市的名称,再渲染到组件上去
      • 弊端:拿到页面,获取城市,一共发了两次请求,除了浪费请求,还有体验问题,就是闪了一下
    • 节省网络请求:发送一次请求

    在请求文档的时候,那个时候服务器已经知道你的ip了,在那个时候,完全可以拿到ip对应的城市,这个数据是可以当时返回给你的,不需要额外再建立一次连接,利用vuex同步状态,再利用ssr,就可以做到一次请求就可以拿到数据

    用户数据&状态

  11. 业务逻辑:首先浏览器发一个request请求,然后服务器根据passport来验证当前是否是登录用户,passport会查当前redis,因为你发这个请求的时候,它会带着cookie过来,服务器的passport会用你的cookie再和redis去做认证,如果是登录状态的话,它会返回你的用户名
  12. 网络请求和上面城市组件一样

    组件设计:

    默认模板配置layouts/default.vue

  13. layouts/default.vue




```
  1. header部分:mt-app/components/public/header/index.vue
  2. footer部分:mt-app/components/public/footer/index.vue

    • footer注意的地方

      在default.vue中引入的时候
      
        
      
      这个height一定要设置为100%, 否则就出现 只有一部分是灰色 的情况
      因为element-ui默认设置为60px,所以我们要设置为100%,就整个背景都是灰色的了

定位:geo组件s

  1. 位置、引入

    • 位置:components/public/header/geo.vue
    • 在components/public/header/topBar.vue中被引入

用户登录:user组件

  1. 位置、引入

    • 位置:components/public/header/user.vue
    • 在components/public/header/topBar.vue中被引入
  2. 登录部分:登录或者未登录两种状态

    
    
    
    

右上角->我的美团 手机app 商家中心 网址导航:nav组件

  1. nav.vue位置、引入:

    • 位置:components/public/header/nav.vue
    • 在components/public/header/topBar.vue中被引入
  2. 我的美团部分

    用最简单的dom结构实现比较复杂交互
        因为"我的美团" 这部分的内容既要兼顾着同级平行结构
        又要有照顾到下面"我的订单"等那部分的内容
        所以在这里并不将它和"我的订单"等部分内容放在一个结构里,如下:
        
  3. 我的美团
    我的订单
    我的收藏
    抵用券
    账户设置
  4. 手机APP
  5. ... ...
  6. 网址导航部分

    官网上这部分的列表结构是有标题有内容
        所以我们采取利用dl不是ul,,因为dl中dt和dd正好符合标题和内容这样的结构,如下:
    
  7. 网址导航
    酒店旅游
    国际机票
    吃美食
    烤鱼
    看电影
    热影电影
    手机应用
    美团app

搜索框部分:searchBar.vue

  1. 位置、引入

    • 位置:components/public/header/searchbar.vue
    • 在components/public/header/index.vue中被引入
  2. 搜索相关逻辑:

    • 热门搜索:聚焦 没有内容的时候显示热门搜索
![在这里插入图片描述](https://img-blog.csdnimg.cn/20190710103442227.png)
+ 相关搜索:聚焦 有内容时显示相关搜索
    ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190710103637489.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0xGWTgzNjEyNg==,size_16,color_FFFFFF,t_70)
+ 这两个彼此独立,放在平行结构中,具体实现如下:
    ```
    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)热门搜索栏
(2)相关推荐栏
4. 绑定事件,实现聚焦显示 focus: function(){ this.isFocus = true; }, blur: function(){ this.isFocus = false }, ```
  1. 问题1:当我聚焦后想点击推荐中的链接的时候,会先触发input事件的blur事件,才能点击,所以在点它(链接)之前,已经触发了blur事件,导致你点击这个链接,没有生效

    • 解决:就是我在失去焦点的时候,把isFocus的变化做个延时的处理

      blur: function(){
              //setInterval和setTimeout中传入函数时,函数中的this会指向window对象,所以用self现将this存起来
              let self = this;
              setTimeout(function(){
                  self.focus = false
              },200)
          }
  2. 问题2:我怎么让推荐的内容随着我的输入内容改变,怎么更改数据发出去

    • 方法1:监听v-model内容,也就是search
    • 方法2: 直接观察input事件,在input标签中增加
``

全部分类部分menu.vue

  1. 位置、引入:

    • 位置:components/index/menu.vue
    • 在pages/index.vue中被引入
  2. 逻辑:

    • 结构拆分:一级标题 --->全部分类

      数据结构:
          menu: [
                  {
                      type:'food', 
                      name:'美食',
                      id:11,
                      child:[
                          {
                              title:'美食',
                              child:['火锅', '汉堡', '小龙虾', '烤冷面', '小可爱']
                          }
                      ]
                  },
              ]
      dom结构:
          
      全部分类
      {{item.name}}
    • 结构拆分:二级标题 --->子分类(美食,外卖,酒店等)

      逻辑:每个标题下面对应的内容都不一样,我怎么确定当鼠标划过,我应该显示哪个内容呢

      DOM结构:
          
      //在每个分类子项这样遍历
      当鼠标划过全部分类部分,触发事件@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值为空,组件就会不显示
       也就是说,我没法实现:移动到分类项下的组件
       所以要解决这个问题
       
      给 分类项下的组件 绑定事件 @mouseenter="temEnter" //-->如果从全部分类出来,移入到是子分类,就将定时器清除,kind不为'' temEnter: function(){ clearTimeout(this._timer), }, @mouseleave="temLeave" //-->如果从全部分类移出来,不是移入子分类,那就将kind改变为空,不显示子分类 temLeave: function(){ this.kind = '' }

休闲生活、住酒店、我是商家,登录,二维码部分:life.vue

  1. 位置、引入:

    • 位置:components/index/life.vue
    • 在pages/index.vue中引入
  2. 中间轮播图部分:

    • 位置:components/index/silder.vue
    • 在components/index/life.vue中被引入
    • 写法:参照Element-UIhttps://element.eleme.cn/#/zh-CN/component/carousel

注册组件:register

  1. 位置、引入

    • 位置:pages/register.vue
    • 访问 localhost:3000/register
  2. 编写组件

    • 创建组件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的空模板


数据结构设计

用户:数据库设计,接口设计,用户注册、登录逻辑

  1. 数据库设计:

    server
        dbs
            models              -->放置数据库数据
                user.js         -->users表,包括usename,password,email
            config.js           -->数据库配置文件(smtp服务, redis连接, mongodb连接)      
  2. 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
  3. 简要接口介绍,具体见代码: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())
  4. 将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())
        ...
        ...
  5. 在上述后台配置结束后,在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 = '';
  6. 实现登录逻辑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.vue

      1. 我们已经定义了接口/users/getUser,通过请求这个接口就能获取到用户的用户名
      2. 但是我们用什么时候请求接口呢,有两种方式:
        (1) 在vuex中同步这种状态,
        (2) 不增加SSR负担,在组件中页面渲染完毕之后
            我们再去获取接口,我们这里用异步获取
            在mounted生命周期:组件挂载到页面,渲染完毕再去请求,达到异步获取的效果
  7. 退出逻辑pages/exit.vue

    • 利用中间件

    问:退出(exit.vue)组件中,为什么用中间件来实现退出操作呢,
    答: 因为,我们点击users/components/public/header/user.vue文件中的退出后
        跳转到 退出页面(page/exit.vue)之后,自动的去执行退出操作
        所以利用middleware机制,触发这个获取退出的接口,让这个接口响应完之后,
        我们再做自动化的执行动作        
    ```
  1. 补充:开启SMTP服务

关于数据

  1. 获取数据获取有两种方式:

    • 数据库:

      • 数据库数据的导入

        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 : []
            }
        })
        ```
+ 我们这里所有数据获取都主要用接口的方式,可以自己练习一下数据库的方式

城市服务等:接口设计,显示当前城市逻辑等

  1. 简要接口介绍,具体见代码: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方式,在服务端渲染的时候,拿到接口的值,返回页面,用户体验更高,因为过来的时候已经带来了结果
  2. 获取当前城市,通过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下使用数据
  3. 获取全部分类下的子类,通过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]
    ```
  1. 其他需要了解知识点

    • 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效果是不是正确:通过查看源码,因为这个是服务端打回给自己的模板

搜索相关:接口,搜索逻辑等

  1. 简要接口介绍,具体见代码: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())
  2. 搜索:通过调用接口直接返回数据

    • 注意:每输入一个字母都进行一次请求,显然浪费性能,所以引入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
  3. 热门城市推荐,通过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  --->  得到所有热门城市
    • 用vuex中的数据重新渲染searchbar.vue中的热门推荐

      1. 第一个改动:
          
      热门搜索
      {{item.name}}
      2. 第二个改动:

      {{item.name}}

  4. 有格调部分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中就发送一次请求,让页面显示数据
    
    #### 城市选择页面:changeCity
  5. 位置、引入

    • 位置:pages/changeCity.vue
    • 访问:localhost:3000/changeCity
    • 模板:使用默认default.vue模板
    • changeCity中组件

    components:{
        iSelect,    
        Hot,
        Categroy
    }
    ```
+ 这个页面的难点
    - 拼音首字母怎么写,如果写26个英文字母标签再插入,是很失败的
    - 如何通过后端给定接口,返回城市后,根据字母来分类
        + 一个字母对应城市的显示
        + 点击字母,快速定位到该字母对应的所有城市

按省份选择iselect.vue 那栏

  1. 位置、引入:

    • 位置:components/changeCity/iselect.vue
    • 在pages/changeCity.vue中被引入
  2. 逻辑:

    • 搜索框参见Elmement-UI:https://element.eleme.cn/#/zh-CN/component/input
    • 确定需要哪些数据province,city.....
    • 将省份和城市做关联(利用watch监听属性),根据省份获取城市(利用axios)

      省份:
          
      城市:
          
      联系:
          根据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结构:
          
      引入lodash:import _ from 'lodash'
      两个事件:
          fetch-suggestions="querySearchAsync"  -> 用户输入内容的时候触发的事件
          @select="handleSelect"                -> 当列表被点击选中的时候,触发这个方法
      
          querySearchAsync:_.debounce(async function(query, cb){  
              1. 如果cities有值的话,直接在cities里面搜索  
              2. 如果citie没有值的话,从geo/city接口获取数据
              3. 将获取到的数据格式化,我们只需要value值
        }, 200),
        handleSelect:function(item){
            1. 将当前城市设置为item
            2. 跳转页面,回到初始页
        }
    ```
+ 注意:直接搜索 范围是全国

热门城市hot.vue 那栏

  1. 位置、引入:

    • 位置:components/changeCity/hot.vue
    • 在pages/changeCity.vue中被引入
  2. 逻辑:

    • 结构采用dl dt dd,因为是一个标题,很多内容
    • 在mounted声明周期函数中获取数据渲染

      async mounted(){
          let {status, data:{hots}} = await this.$axios.get(`/geo/hotCity`)
          if(status == 200){
              this.list = hots;
          }
      }

按拼音首字母选择categroy.vue 那栏

  1. 位置、引入:

    • 位置:components/changeCity/categroy.vue
    • 在pages/changeCity.vue中被引入
  2. 逻辑:

    • 确定显示字母用的节点,利用dl dt dd,举个栗子:

      按拼音首字母选择
      {{item}}
    • 点击字母,快速定位到该字母对应的所有城市->利用a标签的#,如下

      遍历字母:
          
      按拼音首字母选择
      {{item}}
      遍历每个字母对应的城市:
      {{item.title}}
      {{ c }}
      上面的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;
        ```

产品列表页(products),就是点击搜索出来的页面:products

  1. 位置、引入

    • 位置:pages/products.vue中被引入
    • 访问:localhost:3000/products
    • 模板:使用默认default.vue模板
    • products中组件

      components:{
          Crumbs,         ->哈尔滨美团哈尔滨哈尔滨融创乐园
          Categroy,       ->分类,区域的部分
          List,           -> 智能排序,景点详情部分
          Amap            ->地图
      }
  2. 逻辑: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中更改

      两个热门搜索,一个搜索列表,添加 类似如下语句
      {{item.name}}
    • 待实现功能:

      • 点击排序:点击按价格排序或者按人气排序可以实现下面列表的排序
      • 鼠标划过景点简介时候,地图可以定位到相应位置
  3. 注意decode和encode的问题:query的很多插件在源码中进行了decode,所以用的时候,有的已decode了,自己就没必要再写一遍了,会报错

哈尔滨美团哈尔滨哈尔滨融创乐园:Crumbs

  1. 位置、引入

    • 位置:components/products/crumbs.vue
    • 在pages/products.vue中被引入
  2. 逻辑:

    • 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) }}

分类,区域的部分:categroy

  1. 位置、引入

    • 位置:components/products/categroy.vue
    • 在pages/products.vue中被引入
    • categroy中组件

      components:{
          iselect        ->下拉框(划过酒店住宿,周边游出现的下拉框)
      }
  2. 逻辑:

    • DOM结构:

      • 利用dl里面两个dt(分类和全部)和一个dd,dd里面循环引入组件iselect.vue,展示分类右边的数据,像什么酒店住宿,周边游之类的
      • 将每一项都用一个公共的组件iselect.vue来实现,通过组件中数据的改变来实现页面的布局
      分类
      全部
      <-- 下拉框(划过酒店住宿,周边游出现的下拉框) -->
    • 下拉框 components/products/iselect.vue

      • DOM结构:举个栗子:酒店住宿

        {{ name }}

        {{ name }}

        {{ item }}

智能排序,景点详情部分:list

  1. 位置、引入

    • 位置:components/products/list.vue
    • 在pages/products.vue中被引入
    • list中组件

      import Item from './product.vue'
      components:{
          Item            ->每个景点的简要介绍:像几颗星,门票价格,位置等
      }
  2. 逻辑:

    • DOM结构

      • 采用dl和dd,将智能排序,价格排序,人气等放在一个数组中,利用v-for循环输出数据
      • 每个景点的信息利用组件(item)循环输出,每个item包括图片,描述等信息

        1. 智能排序 价格排序 人气最高 评价最高
            
        {{ item.txt }}
        2. 景点的简要介绍:Item(import Item from './product.vue')
    + 景点的简要介绍:components/products/product.vue
        ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190709203217893.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0xGWTgzNjEyNg==,size_16,color_FFFFFF,t_70)
        - DOM结构:参见Element-UI:
    `https://element.eleme.cn/#/zh-CN/component/rate`
        - 数据:父组件传递
    
    #### 地图控件Amap
  3. 位置、引入

    • 位置:components/public/map.vue
    • 在pages/products.vue中被引入
  4. https://lbs.amap.com/api/javascript-api/guide/overlays/toolbar

    详情页开发 detail.vue

  5. 需求分析
  6. 位置、引入

    • 位置:pages/detail.vue
    • 访问 localhost:3000/detail.vue
    • 模板:使用默认default.vue模板
    • detail.vue中组件

    components:{
        Crumbs,         ->哈尔滨美团 > 哈尔滨美食 > 哈尔滨火锅
        Summa,          ->商品详情
        List            ->商家团购及优惠下的列表
    }
    ```
+ 跳转到该路由的链接:components/products/product.vue
    ```
    

{{ meta.name }}

```
  1. 逻辑:

    • 判断是否显示:商家团购及优惠,显示的条件是登录或者有数据,利用v-if实现

      
        
          
              
          
              
          
    • 思考:访问(详情页)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
        }

商家团购及优惠下的列表 List

  1. 位置、引入

    • 位置:components/details/list.vue
    • 在pages/detail.vue中被引入
    • list.vue中的组件

      components:{
          item        ->每条数据
      }
  2. 逻辑:

    • DOM结构:

      • {{ list.filter(item=>item.photos.length).length }}款套餐
    • 数据的获取:两种方式

      • 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())
        ```

购物车:cart

  1. 位置、引入

    • 位置:pages/cart.vue
    • 访问 localhost:3000/cart.vue
    • 模板:使用默认default.vue模板
    • cart.vue中组件

      components:{
          list            ->订单列表
      }
    • 跳转到该路由的链接:components/details/item.vue

      window.location.href=`/cart/?id=${id}`
  2. 逻辑

    • DOM结构:设计一个平行结构,考虑购物车为空和不为空的两种情况

      
         
         
             ...
             ...
             
             ...
             ...
         
         
         购物车为空
       
    • 订单列表list.vue(components/cart/list.vue)
![在这里插入图片描述](https://img-blog.csdnimg.cn/20190709205032422.png)
    - DOM结构:参见Element-UI:
        `https://element.eleme.cn/#/zh-CN/component/table`
    - 数据:
        ```
        父组件pages/cart.vue通过SSR获取数据(通过这个接口:/cart/getCart)
        传给子组件list.vue 所有订单数据,由子组件全部渲染出来
        ```
    - 逻辑:
        ```
        父组件通过接口获取数据,传入子组件数组,存储在cartData中,
        子组件通过Element-UI结构渲染数据,
        如果我在子组件中更改了购买商品的数量,也就是cartData中的值被更改了,
        那么,我们在父组件监听的total(所有订单总价),也就会重新计算
        然后重新渲染父组件中 下面这个结构中的数据
           

应付金额:¥{{ total }}

``` - 注意:仔细看一下list.vue的数据计算和DOM结构!有一部分需要好好理解 + 提交订单:点击"提交订单",请求/order/createOrder接口,如果请求成功,跳转页面至全部订单页

全部订单页:order

  1. 需求分析

  1. 位置、引入

    • 位置: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加上一些其他参数创建订单
    /order/getOrders返回数据库中全部订单
    最后:注册路由,让路由生效
        server/index.js中:
            import order from './interface/order'
            app.use(order.routes()).use(order.allowedMethods())
    ```
  1. 逻辑

    • 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
              }
            })
          },
      

问题

  1. 搜索失去焦点,热门推荐还在
  2. 还有莫名其妙会报错,会出现什么靓丽什么的搜索结果
  3. 注册时候同一个验证码也可以注册

你可能感兴趣的:(node.js,koa,express,mongodb)