抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

缘起:前段时间面试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)) { } 用来遍历对象,能获取对象的键和键值

评论