缘起:前段时间面试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 | async mounted() { |
这种情况下,我们在页面的源码中是找不到异步数据的,是浏览器拿到服务器的响应的页面,加载,在加载的mounted阶段去获取。
SSR
SSR是在服务端就完成页面所有操作(包括请求异步数据),然后渲染好,服务器再响应。
所以在组件中我们可以用asyncData的写法(一般用来组件获取数据
)。fetch() 一般用来拿到数据修改Vuex状态
1 | async asyncData() { |
注意 由于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
nav.vue组件
- 需求:用简单的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生成 搜索推荐
- @focus:
- 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的@mouseenter:
this.kind =e.target.querySelector('i').classname
。e是函数参数(鼠标移动到的节点) - 左边的@mouseleave:
1
2let self = this; // 这里用设置timer的方法,让鼠标移动到右框的时候就clearTimeout让右框不消失
self._timer = setTimeout(function(){self.kind = ''},150) - 右边的@mouseenter:
clearTimeout(this._timer)
- 右边的@mouseleave:
this.kind=''
- 左边item的@mouseenter:
- 数据结构:见下图。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
40async 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(0, 1);
// 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
5import _ from "lodash";
...
method: {
test: _.debounce(async function() { ... }, 200)
} for (let [k, v] of Object.entries(d)) { }
用来遍历对象,能获取对象的键和键值