一、项目开发准备
1、项目描述
是个外卖项目,是一个前后端分离的spa应用(单页富应用程序),包括商家、商品、购物车、用户等多个子模块,使用Vue全家桶+ES6+Webpack等前端技术,采用组件化、模块化、工程化的模式开发。
2、技术选型
前台数据处理/交互/组件化:vue2.5、vue-router、vuex、mint-ui、vue-lazyload、vue-scroller 、better-scorll、swiper、moment、data-fns 其中vue全家桶包含vue2.5、vue-router、vuex、mint-ui、vue-lazyload、vue-scroller,滑动库包含vue-scroller 、better-scorll、swiper,日期处理包含moment、data-fns
前后台交互:mock数据:mockjs ,接口测试:postman,ajax请求:axios
模块化:ES6、babel
项目构建/工程化:webpack、vue-cli、eslint
css预编译器:stylus
3、API接口
前后台交互的接口,一个接口包含四个方面信息:url、请求方式、请求参数格式、响应数据的格式。测试接口俩方面:通不通,通了后是不是和文档一致。
4、从项目中学习到哪些
开发的方式和模式,一些插件和库的使用,在样式布局上的东西
二、第一天做了些什么
1、使用vue-cli脚手架创建项目
2、安装所有依赖和指定的依赖
3、分辨运行方式:开发环境运行(内存中打包)和生产环境打包和发布
4、stylus的理解和使用,和less一样都是css预编译器,结构化(有嵌套的层次结构与html层次结构一致)、变量、函数和minxin(混合)
5、vue-router的理解和使用:$router:路由器对象,包含一些操作路由的功能函数,来实现编程式导航(跳转路由) $route:当前路由对象,一些当前路由信息数据的容器,path/meta/params/query等 组件标签:router-view、router-link、keep-alive
项目路由拆分:app => Msite+Search+Order+Profile
底部导航组件:FootGuide ,通过路由的meta来显示/隐藏,头部组件:HeaderTop,通过slot来实现组件间通信标签结构 , 商家列表组件:ShopList
6、后台项目
启动:npm start
测试后台接口:postman
修正接口文档
7、前后台交互
ajax请求库:axios
ajax请求函数封装:axios+promise 通过promise直接获取response.data数据
Object.keys
返回一个所有元素为字符串的数组,其元素来自于从给定的object
上面可直接枚举的属性。这些属性的顺序与手动遍历该对象属性时的一致
注意:在ES5里,如果此方法的参数不是对象(而是一个原始值),那么它会抛出 TypeError。在ES2015中,非对象的参数将被强制转换为一个对象
// simple array
var arr = ['a', 'b', 'c'];
console.log(Object.keys(arr)); // console: ['0', '1', '2']
// array like object
var obj = { 0: 'a', 1: 'b', 2: 'c' };
console.log(Object.keys(obj)); // console: ['0', '1', '2']
// array like object with random key ordering
var anObj = { 100: 'a', 2: 'b', 7: 'c' };
console.log(Object.keys(anObj)); // console: ['2', '7', '100']
// getFoo is a property which isn't enumerable
var myObj = Object.create({}, {
getFoo: {
value: function () { return this.foo; }
}
});
myObj.foo = 1;
console.log(Object.keys(myObj)); // console: ['foo']
接口请求函数封装:每个后台接口
使用git将vue项目同步到远程仓库(vue项目如何同步到远程GitHub仓库中_godferyZhu的博客-CSDN博客)
8、异步显示数据
配置代理实现跨域(在config文件夹下的index.js中配置)
proxyTable: {
'/api': { //匹配所有以'/api'开头的请求路径
target: 'http://localhost:4000', //代理目标的基础路径
changeOrigin: true, //支持跨域
pathRewrite: { //重写路径:去掉路径开头中的'/api'
'^/api': ''
}
}
},
(代理:是一段程序,运行在前台服务器上。代理拦截前台应用向前台服务器发送的请求,转发给后台服务器,后台应用返回响应数据,后台服务器返回结果给代理,代理交给前台应用)
使用vuex管理状态:
1.在store文件夹下创建index配置和state、action、mutation、getter对象模块
2.各个对象模块暴露各自的属性和方法,在index中引入
3.在main.js中注册store,使用上vuex
4.在app.vue中更新状态,两种方式:
import {mapActions} from 'vuex'
export default {
mounted () {
// this.$store.dispatch('getAddress')
this.getAddress()
},
methods: {
...mapActions(['getAddress'])
},
5.在MSite中读address.name数据,异步显示当地地址
<HeaderTop :title="address.name">
import {mapState} from 'vuex'
export default {
computed: {
...mapState(['address'])
},
}
6.异步显示食品分类轮播列表
根据一维数组生成二维数组;在异步更新界面之前执行(使用watch和$nextTick解决)
watch: {
categorys (value) { // categorys数组中有数据了, 在异步更新界面之前执行
// 使用setTimeout可以实现效果, 但不是太好
/*setTimeout(() => {
// 创建一个Swiper实例对象, 来实现轮播
new Swiper('.swiper-container', {
loop: true, // 可以循环轮播
// 如果需要分页器
pagination: {
el: '.swiper-pagination',
},
})
}, 100)*/
// 界面更新就立即创建Swiper对象
this.$nextTick(() => {// 一旦完成界面更新, 立即调用(此条语句要写在数据更新之后)
// 创建一个Swiper实例对象, 来实现轮播
new Swiper('.swiper-container', {
loop: true, // 可以循环轮播
// 如果需要分页器
pagination: {
el: '.swiper-pagination',
},
})
// new BScroll('.miste-content-wrapper', {
// click: true
// })
})
}
},
7.异步显示商家数据
8.使用svg显示加载中界面
9.Star组件
9、注册登录功能
1.界面相关效果
切换登录方式:在对应a标签上添加click点击监听,改变class的loginWay状态
手机号合法检查:双向绑定手机号输入框,使用计算属性监视输入框的值,符合正则表达式则使'获取验证码'按钮样式改变成高亮显示,要给按钮加上点击后的class样式
倒计时效果 :将按钮样式设置为正确手机号时显示可点击,设置点击监听(button在form表单里,要取消点击按钮的默认表单提交行为)绑定倒计时数据,给点击获取倒计时函数加上循环计时器来达到倒计时效果(在时间小于0时记得清空定时器),最后在模板中更改发送前和发送后的显示效果
切换显示或隐藏密码:给密码输入框添加一个type为text的输入框,添加绑定showPwd数据,使用v-if切换不同密码输入框样式,用v-module给密码输入框添加双向绑定,给点击按钮添加点击监听切换输入框样式,绑定设置了移动动画样式div的class,根据showPwd数据改变状态
(只有一个样式时绑定class使用对象方式:{样式名:状态},两个样式时使用判断方式:状态?样式1:样式2)
前台验证提示:给验证码、账号、密码、图形验证码输入框双向绑定,创建AlertTip组件,接收alertText数据和分发自定义closeTip函数,给登录按钮绑定监听函数login,不符合条件输出对应弹出框(可创建函数封装重复代码,要记得绑定分发的自定义函数)
2.前后台交互相关问题
如何查看应用是否发送了某个ajax请求(浏览器的network)
发ajax请求404(请求的路径,代理是否生效或配置完是否重启,服务器应用是否运行)
后台返回了数据,但页面没有显示(vuex中是否有数据,组件中是否正确读取)
3.登录注册功能完善(前后台交互)
动态获取一次性图形验证码:在相应位置添加接口路径svg图到src属性值,绑定点击监听函数,点击刷新一次性图形验证码
(使用this.$refs得到组件,this.$refs.captcha.src设置图形验证码src属性值。注意点击时src得变化,可以在路径后面使用'?time='+Date.now()来增加一个时间戳使其发生变化)
(ref
被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs
对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例引用信息将会注册在父组件的 $refs
对象上,$refs 是所有注册过的ref的一个集合)
动态一次性短信验证码
使用云通讯注册账号,完善接口请求封装函数,在login路由组件中引入,在login函数中使用await和reqSendCode函数异步发送ajax请求,发送短信失败时显示弹出框
(intervalId需要设置成this.intervalId,否则无法在函数内部作为局部变量使用)
(此项目后台接口都已经写好了,只需要拿来使用,在发送短信时出现
statusCode: '111139', statusMsg: '【账号】主账户已暂停'
错误,要将ACCOUNT SID(主账户ID)和AUTH TOKEN(账户授权令牌)和AppID(默认)在接口代码中改一下,然后重新编译启动就可以了)
4.在密码和手机验证码登录的时候发现错误,服务器出现问题,密码登录出现500错误,手机验证码登录出现504错误,此处的服务器问题解决不了,登录功能有问题。
5.将用户信息存入state中,并创建一套获取和更改用户信息的方法,改变登录界面和个人页面界面的相应显示信息,并设置登录前和登录后不同的显示效果,添加点击跳转路由功能,在主页面相应部分也更新。
6.实现自动登录,因为用户登录信息存在了state中,页面刷新就登录信息,而后台程序中用户登录实现于cookie和session的使用,cookie在客户端保存账户id和决定保存时间,session在服务器端
(cookie和session区别cookie和session的详解与区别 - 测试开发喵 - 博客园)使用reqUserInfo函数来获取用户登录信息,从而实现自动登录
7.实现异步登出,使用mint-ui组件库,标签组件Button和非标签组件MessageBox、Toast,在使用mint-ui组件库时需要一些准备,先npm下载运行依赖,还有按需打包的开发依赖,再配置babel
"plugins": ["transform-vue-jsx", "transform-runtime",["component",
{
"libraryName": "mint-ui",
"style": true
}
]],
(此时注意不同版本配置的格式有不同,此时为最新版)
然后根据官方文档引入对应组件并使用logout函数实现用户登出
10、搭建商家整体界面
1.拆分路由组件,基本样式确定好(资源样式有问题,现在还没搞)
2.设计json数据
3.使用mockjs模拟数据,设计mockServer接口,此接口不需要暴露任何数据,只要保证运行即可
4.ajax请求mockjs模拟的接口,设置请求接口函数,在组件中mouted使用$store.dispath分发
5.设置商家头部显示
(在异步获取数据时,vue模板在初始显示时为空对象,不能显示数据,从后台获取数据后才能显示得到的值,此时会报错:TypeError:Cannot read property '0' of undefined或null。 可用v-if判断一下,在没数据时不显示,有数据时才显示 。 总结规律:vue模板解析表达式只有两级a.b时不会报错,但从第三级a.b.c开始就会报错,解析到第二级a.b时已经是undefined了)
(transition动画的设置复习,name="fade",css样式设置)
6.开发ShopGoods组件
在组件mounted中使用dispatch获取异步action数据到vuex,再在computed中使用...mapState获取vuex中数据到组件中,最后在模板上调用显示。
在分类列表和商品列表中使用better-scroll插件实现移动端的回弹滑动,和使用swiper时一样,要在列表数据更新后执行,使用$nextTick语句实现,这次使用回调函数技巧实现。
mounted() {
this.$store.dispatch('getGoods',() => { //数据执行后更新
this.$nextTick(() => { //列表数据更新显示后执行
new BScroll('menu-wrapper') //列表显示之后创建
new BScroll('foods-wrapper')
})
})
},
// 异步获取商品信息
async getGoods({commit},callback) {
const result = await reqGoods()
if (result.code === 0) {
const goods = result.data
commit(RECEIVE_GOODS,{goods})
//数据更新了,通知一下组件
callback && callback()
}
},
初始化右侧滑动的Y轴坐标scrollY和所有右侧分类li的top组成的数组tops
methods: {
// 初始化方法前面加_
// 初始化滚动
_initScroll () {
//列表显示之后创建
new BScroll('.menu-wrapper',{
})
const foodsScroll = new BScroll('.foods-wrapper',{
probeType: 2 //惯性滑动不会触发
})
// 给右侧列表绑定scroll监听
foodsScroll.on('scroll',({x,y}) => {
this.scrollY = Math.abs(y)
})
},
// 初始化tops
_initTops () {
// 初始化tops
const tops = []
let top = 0
// 收集
tops.push(top)
// 找到所有分类的li
const lis = this.$refs.foodsWarpperUl.getElementByClassName('food-list-hook')
// 将伪数组变为真数组
Array.prototype.slice.call(lis).forEach(li => {
top += li.clientHeight
tops.push(top)
});
// 更新数据
this.tops = tops
}
},
滑动右侧列表更新左侧选项值
currentIndex () { // 初始和相关数据发生了变化
// 得到数据条件
const {scrollY,tops} = this
// 根据条件计算出一个结果
const index = tops.findIndex((top,index) => {
return scrollY >= top && scrollY < tops[index + 1]
})
// 返回结果
return index
},
(解决惯性滑动不更新当前分类的bug
第一种方式将probeType配置值改为3,第二种方式给右侧列表绑定scrollEnd监听)
点击分类滑动右侧列表
// 事件回调方法
clickMenuItem (index) {
// 使右边列表滑动到相应位置
// 得到目标位置的scrollY值
const scrollY = this.tops[index]
// 立即更新scrollY值(点击立即更新显示左侧分类列表)
this.scrollY = scrollY
// 平滑滚动右侧列表
this.foodsScroll.scrollTo(0, -scrollY,300)
}
7.添加CartControl组件,接受food数据,和增加减少两个函数,可以使用布尔值传值来实现一个函数两个使用(判断是否添加)
碰到如何给已绑定的vue数据新增的属性添加数据绑定问题(第一种情况在vuex中:使用Vue语法的set方法,Vue.set(obj,'xxx',value)传三个值:对象,属性名,属性值即可实现。第二种情况在组件中:使用this.$set(obj,'xxx',value))
(为什么右侧食品列表滑动左侧分类列表不会一起滑动到最底部没显示的部分:
需要模拟点击获取,在浏览器鼠标滑动无效)
8.添加Food组件,在ShopGoods父组件中引入子组件的方法
// 显示点击的food
showFood (food) {
// 设置food
this.food = food
// 显示food组件 (在父组件中调用子组件对象的方法)
this.$refs.food.toggleShow()
}
9.添加ShopCart组件,使用state管理购物车中食物列表,getters管理计算购物车中食品全部的数量和价格,组件中计算属性监视购物车中食品数量来决定是否高亮显示和配送信息显示
(发现bug,可能是shop组件的样式问题,在点击shoplist后进入重定向的路由组件shopGoods中,但是router-link的三个按钮被挤掉了,如果没有重定向则会进入shop路由组件,点击router-link按钮后进入shopGoods子路由组件按钮又被挤掉了,由于技术问题没解决)
点击弹出的购物车是一个BScroll单例,多次点击不会产生多个BScroll实例对象,导致点击添加后出现多次购物车数量添加bug问题
由于ShopCart中的列表不是初始化的列表,是会动态刷新的,添加了better-scroll插件后要让滚动条刷新一下
绑定点击清空购物车监听,使用dispatch分发clearcart方法,更新state中cartFood数组里food.count数据为0并清空数组,达到清空购物车效果
10.添加ShopRatings路由组件,显示商家的用户评价,过滤数组ratings使得两个条件条件全部、好评、差评和是否只看有内容的评价 点击切换显示
filterRatings () {
const {ratings,onlyShowText,selectType} = this
// 产生一个过滤新数组
return ratings.filter(rating => {
const {rateType,text} = rating
/*
条件1:
selectType: 0/1/2
rateType: 0/1
selectType===2 || selectType===rateType
条件2
onlyShowText: true/false
text: 有值/没值
!onlyShowText || text.length>0
*/
return (selectType === 2 || selectType === rateType) && (!onlyShowText || text.length > 0)
})
}
11.添加ShopInfo路由组件,显示商家的店铺详细信息,设置垂直滑动和水平滑动,水平滑动时ul的值要包括所有li的值,在刷新路由页面后,初始值为空数组,如果在mouted中创建BScroll实例会出现[Vue warn]: Error in mounted hook: "TypeError: Cannot read property 'length' of undefined"的 bug,原因:当前路由对象在刷新后立即创建,异步请求获取数据(mouted挂载时要在数据已有的情况下使用)此时应考虑两种情况:1是其他路由组件切换过来时,2是在当前路由组件刷新时
解决1方式:
mounted() {
// 如果数据还没有, 直接结束
if (!this.info.pics) {
return
}
// 数据有了, 可以创建BScroll对象形成滑动
this._initScroll()
},
methods: {
_initScroll () {
new BScroll('.shop-info')
//动态计算ul的宽度
const ul = this.$refs.picsUl
const liWidth = 120
const space = 6
const count = this.info.pics.length
ul.style.width = (liWidth + space) * count - space + 'px'
new BScroll('.pic-wrapper',{
scrollX: true //水平滑动
})
}
},
解决2方式:
watch: {
info () { // 刷新流程--> 更新数据
this.$nextTick(() => { //异步创建
this._initScroll()
})
}
}
12.添加Search路由组件
主要功能为输入内容点击搜索显示有相关内容的商家列表,此时要注意需要监视searchShops中有值的时候才显示数据,搜索后没找到时显示无搜索结果,v-if中是否显示取决于监视方法getSearchShops是否有对应的值
computed: {
...mapState(['searchShops'])
},
methods: {
search () {
// 获取搜索关键字
const keyword = this.keyword.trim()
// 进行搜索
if (keyword) {
this.$store.dispatch('getSearchShops',keyword)
}
}
},
watch: {
getSearchShops (value) {
if (!value.length) { // 没有数据
this.noSearchShops = true
} else { // 有数据
this.noSearchShops = false
}
}
},
13.缓存路由组件对象
好处:复用路由组件对象,复用路由组件获取的后台数据,在浏览器端的内存中,把组件对象储存起来
<keep-alive>
<router-view></router-view>
</keep-alive>
14.路由组件懒加载
需要的时候去后台请求路由组件的代码
// import MSite from '../pages/MSite/MSite'
// import Order from '../pages/Order/Order'
// import Profile from '../pages/Profile/Profile'
// import Search from '../pages/Search/Search'
// 路由组件懒加载
const MSite = () => import('../pages/MSite/MSite')
const Order = () => import('../pages/Order/Order')
const Profile = () => import('../pages/Profile/Profile')
const Search = () => import('../pages/Search/Search')
15.图片懒加载
使用vue-lazyload插件
import VueLazyload from 'vue-lazyload'
import loading from './common/imgs/loading.gif'
Vue.use(VueLazyload, { // 内部自定义一个指令lazy
loading
})
<img v-lazy="food.image">
16.使用moment实现日期过滤器
在filters文件夹中配置index文件(可配置多个过滤器),然后在组件中使用
import Vue from 'vue'
// import moment from 'moment'
import format from 'date-fns/format'
// 自定义过滤器
Vue.filter('date-format', function (value,formatStr='YYYY-MM-DD HH:mm:ss') {
// return moment(value).format(formatStr)
return format(value, formatStr)
})
17.打包文件分析与优化
vue 脚手架提供了一个用于可视化分析打包文件的包 webpack-bundle-analyzer 和配置
启用打包可视化: npm run build --report 打包的文件形成一个可视化图形界面包含文件各部分
可以看到moment太大,使用date-fns代替
import Vue from 'vue'
// import moment from 'moment'
import format from 'date-fns/format'
// 自定义过滤器
// moment使用格式YYYY-MM-DD HH:mm:ss
Vue.filter('date-format', function (value,formatStr='yyyy-MM-dd HH:mm:ss') {
// return moment(value).format(formatStr)
return format(value, formatStr)
})
到6月23日完成了,虽然中间有些接口失效导致有些图片出不来,登录的两个方式由于服务器问题登录不了,还有些样式方面的bug(css样式方面学的不好,没能完善),中间摸鱼了一段时间,有些迷茫,但是现在已经确定以后要在前端道路上越走越远了,希望自己不忘初心,勇于探索。