上一篇:【vue:把一个JS项目改成vue框架】forkify项目(一)
12. ⚡需求七:请求一系列数据并渲染在TheSearchresults组件中
在JS中:
对于vue来说,这个query
肯定是在theHeader中的<input>
得到,并传递给请求数据的函数,该函数中,通过添加query
的API得到的数据将存储一个state中。所以上面的JS代码应该移到action中。同样是状态管理,与前面不同的是,现在action中的函数也要获取外部的参数了(query
),可以通过this.$store.dispatch('actionWithParams', param1, param2)
首先,切换分支,
先获取到query
再把query
交给action,由action请求数据,
搭建好recipeList
这个state,
接下来就剩在action中,请求数据,并根据请求结果格式把数据存放在recipeList
中
请求数据,然后看看数据是什么样的:
success表示请求成功了,点开看看数据:
状态管理是正常的,
- 但是出现一个这样的问题:
第一次查询pizza的时候:没有数据
当我再次点击提交的时候:出现pizza的查询结果
但当我查询avocado的时候:仍然出现pizza的查询结果
我再次提交avocado的时候:才出现avocado的查询结果
不知道你看到这里有没有明白错误的地方在哪里?
实际上,这个错误在前面的⚡需求中同样出现过,就是:这个状态值还没来得及更新,就打印出来了,而这一次打印的结果是上一次更新的结果。
回看代码:问题出在异步的调用:
在getters中console.log(state)
, console.log(state.recipeList)
在函数中尝试调用getters
,并查看控制台结果:
控制台打印出getters中的打印结果和submit
函数中的打印结果:
可以发现,是异步函数在执行的时候,我们并没有等它更新完state
就着急打印了。
- 解决:
await
跟前面一样,我们有了一个重大经验,在调用action中的async
函数时,我们应该要采取await
,因为虽然状态管理会实时更新状态,但是更新状态本身这些代码也需要时间,我们应该等待这个dispatch
行为,从而才能保证我们的状态是完完全全地实时更新。
在JS中,也有类似的经验:
ps:虽然存储query到state里看似多余,但实际上也有作用,比如其他组件万一需要这个query值来渲染界面就可以从state里调用。虽然在这好像多写了代码,但请相信我,未来肯定有用。
现在我们终于可以开始渲染部分了(把得到的数据在TheSearchresults组件页面中显示)
在JS中,渲染,不过是通过template string
把变量插入到html元素中,再通过querySelector
和insertHtml
,把数据插入到页面中,这其中就涉及到html的清空,整理,插入,数据变量的传递等。通常这些操作还会涉及到一些重复代码,会用到父类子类继承等等。
(可以查看JS课程P296,就能明白我的意思:【Udemy排名第一的JavaScript课程】2023最新完整JavaScript课程 从入门到精通 – 通过项目、挑战和理论掌握JS(中英文字幕)下
而在vue中我们只需要关注当前的vue文件中的<template>
需要什么变量,从而在<script>
中从状态state
中拿取所需变量就可以了。
在搜索框headers中,我们只需要存储好query和根据query调用数据,不需要获取任何state的数据,所以整理如下:
在theSearchresults中,我们从state获取recipeList,再用v-for渲染:
这样我们就会产生多个(对pizza来说,应该有59个)一样的(因为我们还没有修改这段html中的变量)这段html。
修改html,随着具体数据渲染:
此时,我们随便选择一个,右方都会根据id去渲染(点击,url的id改变,recipe状态中的action会重新请求数据,recipe状态更新,TheRecipe组件获取到的状态是实时的,TheRecipe组件重新渲染)
主要功能已完成,现在修修边幅:
左边列表不应该全部高亮:
如果用户输入一个乱七八糟的值,我们应该反映一个错误,而不是不渲染任何东西,比如这样:
和TheRecipe中一样,把error组件注册好:
(你可以看到组件化有多方便!)
然后把错误信息的显示条件hasError
写好:
接着我们在error中:
下面,我们把上图中的errorFromRecipeList
写好:
几乎和errorFromRecipe
一样,复制粘贴就完事儿
ps:忘记切换分支了,所以有关这个错误警告的修改在withpagenation
的分支里,而不是在withrecipelist
里
13. ⚡需求八:Pagination 翻页
记得切换分支:
获得了一系列食谱之后,我们不希望结果全部列出来,应该让用户翻页查找。
在我们的searchResults组件中,recipeList
代表的是全部的查询结果,然而,我们应该只获取前十个,然后再获取之后十个,再之后十个,每一次这个recipeList
都应该是十个但又随着用户的操作(比如点击下一页)而更新。
(ps: 抱歉,这里config.js的路径错了,应为../config.js
)
recipePerPage
可以用config.js管理,之后如果要修改一次性列出的结果数,就可以在一处地方修改,这里的代码也更易理解。
然后可以试着将<template>
中的recipeList
修改为只返回十个结果的getRecipePerPage
变量
没有报错,只返回了十个结果,接下去要做的工作就是动态修改page
这个变量,以及渲染“上一页”,“下一页”的按钮。
在JS中:
针对不同的情况,产生不同的html
在theSearchResults中,有pagination有关的html,我们应该另建一个组件,使得这个功能能够重复使用,也利于单独管理和调整代码。
先把组件建立好,注册好:
查看是否建立好,可以把pagination组件中的注释代码还原:
接下来,我们要做的是让theSearchresults组件获取到pagination组件中动态变化的page
,使得渲染的十个食谱是根据页码进行筛选的。
在JS中,是通过分类讨论情况,然后插入不同的html来实现按钮的显示:
那么在vue中,可以通过变量来空值v-if
显示不同的html,
在pagination中:pagination的输入是页数numPages
(因为要根据这个判断最后一页显示的按钮情况);输出是当前的页码curPage
(因为父组件searchresults需要借助这个当前页码来显示相应的食谱)
numPages
是从theSearchresults组件中传递过来的值,
curPage
是我们需要传递给父组件theSearchresults的值,
此时,我们可以修改pagination中的curPage
变量,看看结果是否符合:
curPage = 1
curPage = 6 //最后一页
现在我们就要思考,如何将这个curPage值传出去,使得渲染的结果也跟着页码改变:这里的技术是从子组件向父组件传递值,可以采用
// 子
this.$emit('事件名',要传出去的变量)
// 父
<pagination @事件名="xxx"></pagination>
methods:{
xxx(传过来的变量名){
}
}
在pagination中:释放一个事件passing-curpage
,并打包第二个参数this.curPage
同时,也要触发事件(调用函数的操作):
当点击按钮的时候,curPage
改变,然后通过passingCurPage()
释放passing-curpage
事件并传递curPage
,事件被触发后再与updateCurpage()
操作绑定,从而实现改变theSearchreults中的curPage
变量。
在theSearchresults中,注册好事件,并绑定操作,
用alert
检查是否成功传递,或者打印newCurPage
更好
接下来在pagination中,要根据按钮的变化,获取到最新的curPage变量。
在JS中,是通过在html中设置dataset,从而获取到curPage+1/-1的值
把gotoPage传出去
获取到gotoPage,改变食谱
在Vue中,我们已经把curPage
存为data了,所以可以这样解决:
设两个方法(分别对应curPage-1
和curPage+1
操作,当然你也可以只用一个方法,然后用if
设置这俩操作),这两个方法都要释放pagination的事件,从而把新的curPage
值传出去。
那么,我们可以将原来的passingCurpage
方法分出两个方法,分别对应“上一页”,“下一页”。
记得调用这两个方法,
然后可以检查一下是否正确传递:
那么在theSearchresults组件中,就会根据这个curPage渲染不同的食谱。翻页功能完成!
或许我在前面未强调清楚,在pagination组件和theSearchresults组件中,都有一个名为curPage的变量,他们不是同一个变量,只是在不同的组件中有不同的功能的同名变量,他们的内在联系仅仅是代码的理解:
- 在theSearchresults组件中,curPage用于
//根据页码筛选出每一页展示的食谱
getRecipePerPage(){
const start = (this.curPage - 1) * this.recipePerPage // 0
const end = this.curPage * this.recipePerPage // 9
return this.recipeList.slice(start,end);
}
// 这个页码来自newCurPage
methods:{
updateCurpage(newCurPage){
this.curPage = newCurPage;
console.log(this.curPage)
}
}
它的目的是把newCurPage
接过来,从而反映在这个组件的data中,提供一个数据的功能,使得其他变量能调用这个变量,而不是直接地使用newCurPage
,
如果直接使用,弊端就在于别人拿到代码,一时半会儿会搞不清楚这个newCurPage-1
是什么东西,为什么要从pagination获取,又为什么要获取;如果还有其他函数想要调用当前页码,功能就会不明晰,updateCurpage
的功能就不单一了,我们应该保留谁干什么就干什么的编程思想,我想这也是vue想体现的精华所在。
- 在pagination组件中,
curPage
主要用于和button交互,不仅button的click行为会改变curPage
,并且也需要一个data中的变量能够动态地更改<template>
模板中的显示
14. ⚡需求九:按钮更改servings(食谱适用人数),能相应更新食材数目
记得切换分支,我的名称是withservings
这个操作在theRecipe布局中,所以在theRecipe组件中找到按钮的html。
要做的工作:设置点击事件,并绑定操作,该操作中要改变recipe
的ingredients
中的quantity
就行,有点类似paginantion,我们对增加和减少servings要区别对待。
基本ok,存在一个小问题,一般食材的数量我们不好用小数去表达,通常会适用比例或分数,并且,如果食材是根据口味自行决定,我们在食谱中也不会表达数值或0,我的意思是,请看下图:
首先解决0的事,把未改变之前的食谱打印出来,
我们应该做的是:如果ing.quantity
是null,则不做任何修改:
其次解决小数的事,可以用一个第三方库,这里我用fracty,
当我们需要对template中的变量进行处理再返回处理后的值,在以往JS中是直接调用库,就可以,但是在vue中,template中不会识别出库,所以只能在script模块中使用库。
在JS中
在vue中,我们需要通过调用方法,由于方法是放在script模块中,所以方法中可以使用库fracty。