Jacleklm's Blog

MeiTuan-app项目总结

2019/11/16

缘起:前段时间面试SXF二面的时候发现自己项目经验有点少,一些组件实现思路也不清晰,决定恶补一波。该项目来源是慕课网上的《Vue全家桶+SSR+Koa2全栈开发美团网》,于近期学习完毕。

基础知识

Koa2

Koa2是基于Nodejs的web框架
对Node.js的http进行了封装(封装了一个服务器),用 Promise + async 实现异步 (Koa1是用Generator,Express是用Promise)
ctx是一个全局的信息对象,包含所有信息

使用方法

全局安装koa、 koa-generator。用koa2 -e ${项目名} 来初始化项目
主配置文件是 /app.js , 接口都是写在 /routers 文件夹中

中间件的原理

每个Request和Response都要经过中间件,并且进和出中间件的执行顺序的是相反的,所以中间件就类似于洋葱的层

Mongodb

基本概念

  • mongodb是非关系型数据库,mySQL是关系型数据库
  • 在mongodb中:database数据库,collection 数据表,一行的数据叫document,列叫filed
  • mongoose是用来操作mongodb的,能使操作更简单;可视化的数据管理工具RoBo 3T

使用方法

能在Koa2的项目中连接mongodb,然后写一些能操作数据库的接口

Redis

是一个快速读写的数据库,一般用来存session的数据库

Nuxt.js

简介

Nuxt.js是一个基于Vue的通用应用框架,基于Vue,包含Vue-router,支持Vuex、Vue SSR和vue-meta。SSR能解决Vue首屏时间长和SEO差的缺点;用Nuxt.js做Vue SSR很方便

生命周期


重要的是写在Vuex的action中的nuxtServerInit和写在组件中的asyncData()&fetch()

项目目录

/layout 是放布局,或者说是页面的模板。把页面共同的组件放到这里,eg. footer,登录框
/page 是页面的入口文件
/components 只放组件
/server 放服务器
/store 放Vuex

路由

<nuxt-link>标签相当于<router-link>,用法是完全一样的;<nuxt />相当于是<router-view>;page文件下的入口文件,其文件名就是页面路由地址

asyncData

一般情况

如果我们的组件需要获取异步数据,可以把mounted写成async mounted,然后在其中写axios.get()去获取数据

1
2
3
4
5
6
7
8
9
10
11
async mounted() {
    let self = this;
    //  status是响应的状态码, data属性是响应的数据
    let {
      status,
      data: { list }
    } = await axios.get("/city/list");
    if (status === 200) {
      self.list = list;
    }
  }

这种情况下,我们在页面的源码中是找不到异步数据的,是浏览器拿到服务器的响应的页面,加载,在加载的mounted阶段去获取。

SSR

SSR是在服务端就完成页面所有操作(包括请求异步数据),然后渲染好,服务器再响应。
所以在组件中我们可以用asyncData的写法(一般用来组件获取数据
)。fetch() 一般用来拿到数据修改Vuex状态

1
2
3
4
5
6
7
8
9
10
11
12
async asyncData() {
    let {
      status,
      data: { list }
    } = await axios.get("http://localhost:3000/city/list");
    if (status === 200) {
      // 这种return的写法就相当于把asyncData得到的list复制给data中的list
      return {
        list
      };
    }
  }

注意 由于asyncData方法是在组件初始化前被调用的,所以在方法内是没有办法通过this来引用组件的实例对象

Vuex

由于Nuxt有更新,现在的写法见链接

nuxtServerInit

一般是写在 /store/index.js 入口文件的actions中,用来获取异步数据并把数据添加到state中

项目小结

布局

  • 页面的header+body+footer的布局可以用Element UI提供的container布局。在Nuxt项目中,header和footer如果多个页面是一样的,可以写在 /layout/default.vue 中或者新建布局文件
  • 组件的布局用Element UI的layout布局-分栏间隔

组件实现

首页

登录状态组件 user.vue

  • 需求:显示用户登录状态/用户名
  • 思路:在接收请求时,服务器就通过请求带的cookie在Redis中找是否有这个用户,有就返回 user: 用户名 为响应;否则就是 user: ‘’
  • 实现:首先组件的DOM分为两部分,一部分v-if=’username’,里面写this.username,这样有username这个数据的时候显示用户信息的状态;另一部分v-else,里面写立刻登录,就是没有用户信息的时候的状态。在组件的async mounted 去axios.get()一个写好的接口,这个接口能按上述逻辑去获取user(如果没有则 user: ‘’)

这里的async mounted写法缺点是页面刷新的时候页面的用户名那里会闪一下才书来数据,如果认为不影响的话可以这么写。也可以用SSR,写成async Data()。写成async fetch的话,把state.user写成得到的user;然后DOM 里写$store.state.user

问:如果在Vue中,没有后台的话怎么实现
先在Vuex中,设置state.user=’’。在组件中导入mapState后就能直接用state.user。组件一部分v-if=这个值,里面的用户也是这个值;组件另一部分写v-else,里面写立即登录

城市服务组件 geo.vue

  • 需求:第一次访问网站的时候显示定位是用户目前在的城市。若用户手动修改则x显示新城市
  • 思路:在浏览器向服务器请求首页资源的时候,服务器就解读IP地址找到city,加进首页中,把整个页面响应回去。用Vuex + SSR 就能实现
  • 实现:async fetch中去axios.get()一个接口,能接口能根据IP地址定位city。然后写进store.state中。DOM里写$store.state.city
  • 需求:用简单的DOM实现下图

  • DOM的实现:用了一种障眼法。分为两部分:美团规则,网站导航这些是一部分;里面的展开内容自己是一部分。第一部分用 ul>li 实现;第二部分用一个或多个 dl>dt 实现

搜索框 search.vue组件

  • 需求:不focus是普通状态;focus时出现下拉框,显示 热门搜索;输入内容的时候下拉框显示 搜索推荐
  • 实现:
    • DOM:input框,热门搜索,搜索推荐是兄弟节点。后两者是通过v-if显示
    • 事件:
      • @focus:this.isFocus = true
      • @blur(失去焦点): let self = this; setTimeout(function() {self.isFocus = false;}, 200); setTimeout是防止点击推荐内容的同时推荐内容会消失。
      • @input:访问一个接口,这个接口能根据input的输入内容去返回一些数据。这些数据能v-for生成 搜索推荐
    • input的v-model=’search’。v-if后面是跟计算属性,计算属性是返回 isFocus 和 search 这两个变量的判断
    • 热门搜索 的数据是直接在Vuex拿,Vuex在 nuxtServerInit 中 axios.get() 接口,并把数据commit到state中;推荐搜索的,是绑定搜索框的input事件,向后端接口获取数据/ search变量的watch

菜单 menu.vue组件

  • 需求:见下图。鼠标移动到左边的item会出现右边。移开别处则右边会消失;移到右边则不会消失
  • 实现:
    • DOM: 左边的部分是 dl>dt(全部分类)>dd(每个栏);右边的部分是 div>template>h4+span 实现,右边的v-if=’kind’
    • 事件
      • 左边item的@mouseenterthis.kind =e.target.querySelector('i').classname。e是函数参数(鼠标移动到的节点)
      • 左边的@mouseleave:
        1
        2
        let self = this; // 这里用设置timer的方法,让鼠标移动到右框的时候就clearTimeout让右框不消失
        self._timer = setTimeout(function(){self.kind = ''},150)
      • 右边的@mouseenter:clearTimeout(this._timer)
      • 右边的@mouseleave:this.kind=''
  • 数据结构:见下图。name是左边item的内容,eg. 美食;type用来绑定左边item的class属性(其实是为了便于显示前面的icon);child是item对应的子栏(右边)
  • 左边部分直接v-for in $store.state.home.menu生成;右边部分由两层v-for生成,第一层v-for一个计算属性,该属性 return this.$store.state.home.menu.filter(item => item.type===this.kind)[0]。所以这个组件的数据也是Vuex在 nuxtServerInit 中 axios.get() 接口,并把数据commit到state中

城市组件 geo.vue

功能:一开始是显示用户所在的城市;用户也可以在切换城市页面进行城市切换
实现:组件中读取Vuex的State的city
Vuex中在action中nuxtServerInit阶段就请求一个接口,这个接口根据IP地址找到城市,返回。Vuex得到城市后就保存在state.city中;
而切换城市页面调用的Vuex的mutations的changCity这个方法,会让state.city = city 并localstorage.city = city;
同时Vuex的state读取city是 if(localstorage.city) { city = localstorage.city} 如果localstorage没有就读取ip地址的

注册页

需求

用户输入昵称和邮箱后,给注册者的邮箱法验证码;输入正确的验证码书和遍密码后,就能进行注册

实现

  • 样式:通过Element UI的 表单-表单验证 实现 样式 和 校验规则。eg. 验证前后两次输入的密码是否一致(这个是Element UI的表单验证-自定义校验规则)
  • 发送验证
    点击发送验证码的按钮之后,绑定的函数会POST注册者的昵称和邮箱给一个接口。这个接口会用腾讯邮件提供的SMTP功能,给注册者的邮箱发送随机生成的验证码,并把验证码、过期时间、验证码和昵称的对应关系放进Redis中
  • 注册
    点击注册的按钮之后,绑定的函数会POST昵称、用户名、密码(加密传输,用
    crypto-js)和验证码给一个接口。这个接口先校验注册者的验证码和Redis中保存的是否一致,然后从数据库中看改昵称是否被注册等,都符合的话就对新用户信息进写库

登录页

用 passport 验证,据说有固定写法

切换城市页

由于数据结构上的麻烦,我们这里点击城市后跳转到首页后,首页的geo.vue那里还是显示ip地址对应的城市。按理应该把Vuex中的city给改一下。

iselect.vue 组件

  • 需求:按 省份+城市 进行下拉框选择;直接搜索
  • DOM:Element UI的 选择器 和 输入框-远程搜索
  • 实现
    • 省份 + 城市 下拉框选择:在mounted的时候请求得到省份数据,写进 province 变量中,v-for就能得到省份的下拉框;对省份下拉框v-model=pvalue,对pvalue变量进行watch,有变化就请求城市数据写进 city 变量中
    • 直接搜索。input事件直接访问接口,最好用loadshd的debounce做延时处理

按拼音选择的category.vue组件

  • 需求:按拼音字母选择城市;点某个字母页面会move到该字母城市所在位置
  • 实现
    • DOM: dl>dt>dd
    • 点击字母实现页面滚动。把字母写成超链接<a :href="# item.id">,然后后面的栏都写id属性即可
    • 通过async mounted得到后端给我们的中国所有城市的数据后,我们怎么进行拼音分类
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      async mounted() {
        let self = this;
        let blocks = [];
        let {
          status,
          data: { city }
        } = await self.$axios.get("/geo/city");
        // 对全国的城市做拼音处理并分类
        if (status === 200) {
          let p;
          let c;
          let d = {};
          // 拼音处理
          city.forEach(item => {
            // p 变量保存城市名拼音的首位
            p = pyjs
              .getFullChars(item.name)
              .toLocaleLowerCase()
              .slice(01);
            // c 变量保存城市名拼音的首位的 ASCII 码。大写的A-Z是65-90,小写的a-z是97-122。我们通过ASCII码进行排序
            c = p.charCodeAt(0);
            if (c > 96 && c < 123) {
              if (!d[p]) {
                d[p] = [];
              }
              d[p].push(item.name);
            }
          });
          // 整理成我们想要的数据结构
          for (let [k, v] of Object.entries(d)) {
            blocks.push({
              title: k.toUpperCase(),
              city: v
            });
          }
          // 排序
          blocks.sort((a, b) => a.title.charCodeAt(0) - b.title.charCodeAt(0));
          self.block = blocks;
        }
      }
      for (let [k, v] of Object.entries(d)) { } 用来遍历对象,能获取对象的键和键值

产品列表页

category.vue组件

  • 需求
  • 实现
    • DOM:dl(分类)>dt(全部)>dd(item),item是另一个组件,能有下拉框

map.vue 组件

  • 需求:显示产品的地图位置
  • 实现:用高德开放平台的JS API。使用方法见代码、笔记和官方文档

详情页

页面的路由可以是/detail/:id
由其他页面跳转到详情页可以有登录拦截,实现对登录和没登录的使用者呈现不同页面。实现方法还是获取数据的时候(eg. async Data)的时候请求接口,由接口根据passport判断登录状态

购物车页面

用Element UI的 表格-多选,表格-自定义 和 计时器 实现。
纯手写实现的购物车见 /mycart 页面 。两个注意点:

  • checkbox的v-model=item.choose(源数据的item可以没有choose这个属性)
  • @click=item.count++ 可以直接写在html中,方便获取具体的item.count

后端与数据库

说实话,还是不太熟,只能大概看懂;数据库操作相关的接口也只能大概看懂

接口设计

见笔记和代码吧,这部分只能大概看懂而已
其中一些接口(eg. 由ip地址定位,中国所有城市的数据)是访问作者写的一个接口实现的

一些样式

一些技巧

  • 用lodash的debounce做函数延时执行的处理。第一个参数是要执行的函数,第二个参数是延迟的毫秒数。使用方法如下:
    1
    2
    3
    4
    5
    import _ from "lodash";
    ...
    method: {
    test: _.debounce(async function() { ... }, 200)
    }
  • for (let [k, v] of Object.entries(d)) { } 用来遍历对象,能获取对象的键和键值
CATALOG
  1. 1. 基础知识
    1. 1.1. Koa2
      1. 1.1.1. 使用方法
      2. 1.1.2. 中间件的原理
    2. 1.2. Mongodb
      1. 1.2.1. 基本概念
      2. 1.2.2. 使用方法
    3. 1.3. Redis
    4. 1.4. Nuxt.js
      1. 1.4.1. 简介
      2. 1.4.2. 生命周期
      3. 1.4.3. 项目目录
      4. 1.4.4. 路由
      5. 1.4.5. asyncData
        1. 1.4.5.1. 一般情况
        2. 1.4.5.2. SSR
      6. 1.4.6. Vuex
        1. 1.4.6.1. nuxtServerInit
  2. 2. 项目小结
    1. 2.1. 布局
    2. 2.2. 组件实现
      1. 2.2.1. 首页
        1. 2.2.1.1. 登录状态组件 user.vue
        2. 2.2.1.2. 城市服务组件 geo.vue
        3. 2.2.1.3. nav.vue组件
        4. 2.2.1.4. 搜索框 search.vue组件
        5. 2.2.1.5. 菜单 menu.vue组件
        6. 2.2.1.6. 城市组件 geo.vue
      2. 2.2.2. 注册页
        1. 2.2.2.1. 需求
        2. 2.2.2.2. 实现
      3. 2.2.3. 登录页
      4. 2.2.4. 切换城市页
        1. 2.2.4.1. iselect.vue 组件
        2. 2.2.4.2. 按拼音选择的category.vue组件
      5. 2.2.5. 产品列表页
        1. 2.2.5.1. category.vue组件
        2. 2.2.5.2. map.vue 组件
      6. 2.2.6. 详情页
      7. 2.2.7. 购物车页面
    3. 2.3. 后端与数据库
      1. 2.3.1. 接口设计
    4. 2.4. 一些样式
    5. 2.5. 一些技巧