上一篇:【vue:把一个JS项目改成vue框架】forkify项目(二)
在上一篇⚡需求九当中,有个小bug需要完善:我们应该确保用户点击“减少”人数的时候,不出现负值:
所以在decreaseServings()
函数中增加一个条件判断:
15. ⚡需求十:搜索另一个食谱时,应从第一页开始展示
在此之前,如果你是按顺序学习课程的话,当你看懂课程【Udemy排名第一的JavaScript课程】2023最新完整JavaScript课程 从入门到精通 – 通过项目、挑战和理论掌握JS(中英文字幕)下的P301时,就会感叹vue实在是太方便了!
-
Vue使用一个响应式系统来追踪数据的变化。当Vue实例的data对象中的属性发生变化时,Vue会自动检测到这些变化,并相应地更新DOM。
-
Vue负责根据数据的变化来更新视图,开发者通常不需要手动操作DOM。
P301, JS代码还在对比dom元素来实现动态更新
下面开始完成⚡需求十,记得切换分支,我的是withwatchquery
这个bug的起因是,当搜索pizza时,假如选到第四页,再搜索avocado时,我们应该为用户自动跳回到第一页,而不是留在第四页。
造成这个bug的原因是我们直接对curPage
进行+1/-1,而没有一个赋初值的操作。
解决:watch:{}
只要query
变了,那么curPage
就应该重新为1
这里就体现了我们之前预先把query
存放在状态管理中的优点了,现在我们可以直接通过状态管理获取到这个query
现在是食谱确实是从第一页开始显示,但是按钮还没改变,所以把这两个修改代码也要放到pagination组件中(你看,如果你不用状态管理query,那么你就要在两个地方想办法把query获取到)
16. ⚡需求十一:添加recipe书签
记得切换分支withbookmarks
在JS中,
在vue中,我们应该考虑上图的JS代码如何搬运到Vue框架下,
书签是在食谱详情页,所以找到相应组件,theRecipe
食谱已经加入书签列表,但在页面上按钮还未显示差别,接下去:
接下来,有一个问题要解决:当点击书签之后,我们选择别的食谱,再选择回点过书签的食谱,这个书签就返回原样了。正常来说,书签应该一直保持被点过的状态直到我们再一次修改它。
这个问题产生的原因就是我们每一次点击食谱的时候,它是从状态管理中的action中每次都重新申请的食谱,
所以我们应该在每一次获取到食谱之后,在bookmarksRecipe
序列中查找是否已经存在当前食谱,如果存在则将当前食谱新的属性bookmarked
设为true
一种简单一些的解决方案是:在theRecipe中,每一次都把当前recipe
和已经存储了数据的bookmarksRecipe
对比,如果这个序列中有当前食谱,那么就给当前食谱添加一个新的属性bookmarked
并设为true
另一种解决方案是:在每一次点击申请到Recipe
之后,就检查是否存在在序列中,如果存在,就把当前的Recipe
的bookmarked
属性设为true
第二种方式需要将bookmarksRecipe
和bookmarked
属性都更新到状态管理中,虽然听起来比第一种麻烦许多,但是我们应该把数据放在状态中,便于后续的代码优化
(比如我们之后又想新建一个组件,这个组件或者页面就是把用户的书签食谱展示出来,通过state我们就懂了直接getters
就能拿到,如果是别人拿到你的代码,并且你用的是第一种方式,那他还必须能猜到,哦,这个书签序列bookmarksRecipe
在theRecipe的data
中,我要通过props
或者别的方式才能获取到)。
而不是直接在theRecipe中判断是否为书签食谱,然后渲染。
那么,既然我们设置了Action动作,我们也必须把state中的其他代码补全:
然后我们在action中增加函数,使得外部调用这个函数,从而改变状态中的数据:
现在bookmarksRecipe
已经变成状态管理的一部分了,所以相应地在theRecipe组件中,我们不再使用data(){}
,应该使用getters
和dispatch
来访问这个变量
调试之后,发现其实在action中,代码的顺序应该要改一下:
下面我们理一下逻辑:
<button class="btn--round" @click="addBookmark">
<svg class="">
<use :href="bookmarkedIcon"></use>
</svg>
</button>
首先在theRecipe板块中点击书签按钮,我们会触发addBookmark()
动作,这个动作会把当前的recipe
对象传给action中的addBookmark()
,然后通过mutation中的addBookmark(state,payload)
改变状态中的bookmarksRecipe
序列变量。
有了这个序列变量,我们在每一次通过id
请求数据的时候(也就是在action中的showRecipe(context)
)就可以对比当前请求的数据(Recipe
)是否在这个序列中,如果在,我们就把bookmarked
属性设为true
最后,theRecipe组件中的bookmarkedIcon
变量会通过观察这个bookmarked
属性来决定当前的食谱的书签是否应该高亮
由于我们是对每一个请求的数据都添加了一个bookmarked
属性,所以无论何时返回到标过书签的食谱,我们都能保持书签高亮。
当然,除了addBookmark我们还应该有removeBookmark的功能,也就是用户再次点击,就取消书签高亮:
不管你是把 if
条件判断放在theRecipe中还是在action中还是mutation中,到目前为止都是可以的,但我十分建议,add
和remove
功能最好是分开,所以放在mutation中显然就不合适了,因为后续如果想要使用add
或remove
还要重写。
我打算把这个if
条件判断放在action中:
ps:抱歉,上图有个小错误,改为:const index = state.bookmarksRecipe.findIndex(el => el.id == payload)
现在要做的是:把已经标过书签的食谱渲染在这:
它位于布局中的theHeader部分,所以我们找到相应的html:
我不想在theHeader中写bookmarks有关的代码,我们也应该把组件分离、功能分离😉
所以另建bookmarks.vue组件,把代码复制过去,把组件注册好:
接下来在bookmarks.vue组件中,我们就可以直接通过getters获取bookmarksRecipe
序列数据了:
渲染部分很像theSearchResults部分,所以我们对照着填一下就可以了
最后完善一下,当有书签食谱的时候,不显示错误提示,没有任何书签标记的时候要显示错误提示:
并且当我们点击书签列表中的食谱时,也能自动跳到所点击的食谱详情页,这是因为,
点击食谱链接,我们会改变url我们食谱详情页是通过window.location.hash.slice(1)
获取到的url中改变的 id 号来加载食谱,
多亏了前面的工作,现在我们不需要再做任何工作就把书签功能完成了
接下来,我们不希望每一次用户刷新页面,所标过的书签都消失,我们要把书签食谱数据存在localStorage中:
什么时候存?
在我们点击书签后,触发事件后,改变原数据的时候存
存下之后,用户刷新,我们还要从localStorage中取出数据,渲染在页面上,
什么时候取?
在加载页面的一开始
在更新状态时,绝对不能直接this.$store.state.bookmarksRecipe = this.storageBookmarks
这点原因前面强调过,你应该还有印象吧?😉
然后我们写好状态管理里面的函数:
你看这里,我们当初在mutation中把add和remove功能分开是有用的,这里就只需要add,直接commit
就好了
这下,当我们重新加载页面之后,就仍然能看到我们打过书签的食谱了
你可以看到上图有一个问题,那就是书签食谱列表中有重复显示,下面我们解决这个bug:
ps:👀这里我建议你休息一下,自己找找bug原因,这个bug可不容易一眼看出,如果你能独立解决它,相信你会很有成就感👍
原因:
我错把 更新书签食谱 和 新增书签食谱 弄混了,
我以为循环地新增食谱就等于更新了食谱序列,但其实,新增食谱中除了push
食谱还有setItem
功能,如果使用循环,那么将反复setItem
,就会导致本地存储的食谱序列中产生重复值
解决方案就只能是把更新食谱功能单独拎出来
(因为在addBookmark
中setItem
是不可分离出来的,它应该要紧跟新增食谱state.bookmarksRecipe.push(payload)
之后,把这个食谱也新增到本地中)
解决:
我们在取了数据的之后,任务有两个:
①把原来存的多个本地食谱加载到本地;
②把原来存的多个本地食谱更新到状态中的bookmarsRecipe序列。
①已经通过原来的add和remove功能中的localStorage.setItem('bookmarks',JSON.stringify(state.bookmarksRecipe))
实现了
②就需要loadBookmarks()来执行
完成后,我们看看效果:
17. ⚡需求十二:用户自己上传食谱
记得切换分支withuploadrecipe
首先新建组件,粘贴html,注册好组件
这里再调整一下,App.vue中就应该只放布局的组件,把upload-recipe组件移到theHeader组件中(这个上传食谱的按钮本身也是放在header中)
下面,我简要记录一下这里遇到的一个问题:
也就是说我们要把showOverlay变量传到子组件
而我们如果也要改变父组件的showOverlay变量,又要从子组件把新的showOverlay传回去,
这个问题的解决方案光是听起来就一头雾水 吧👀(可能有更好、更清晰的解决办法,反正我已经晕了🙃)
所以,为了避免我们在子组件和父组件之间传来传去,让人一头雾水,我想到方案是把”打开上传食谱的按钮“和”关闭按钮“放在一个文件中处理
现在我们打开和关闭操作都在theHeader中操作了。
打开:
关闭:
既然两个函数一样,我们就写一个,调用同一个就可以了(@click=”closeForm“
)
接下来我们要处理的是upload按钮,点击它,提交表单,把用户的食谱数据整理出来,并且由于这又是一个数据,我们还是应该采用状态管理,所以收集数据应该放在action中…mutation改变…
此外,我们的思路是应该把这个用户数据变成一个类似从API获取的数据,再由我们去获取到这个用户数据,这样渲染部分就不需要重写了。
我们希望抛出的错误不止是在控制台,还应该在页面显示,因为用户点击按钮,错误显示在控制台的话,用户不会打开控制台,只会看到页面无反应。
然后用户点击upload按钮,如果没有错误的时候,这个表单应该自动关闭,
这里可以使用$emit
释放事件来由子组件告诉父组件“我有变化了”
现在,把这个用户数据变成一个类似从API获取的数据,再由我们去获取到这个用户数据,
接着,在action中的uploadRecipe
函数中写:
最后,请求成功!
渲染在theRecipe中
也要将用户自己的食谱作为书签食谱存在书签列表中,很简单,我们前面已经实现过
然后url的id
也应该变为用户上传食谱中的食谱id,
如果在请求的url中也加入KEY
,那么用户的食谱中如果包含关键词,搜索关键词就也会出现用户的食谱
现在要实现如果是用户上传的食谱,那么就显示这个图标,如果不是,就没有这个图标
首先要为每一个食谱都加上key属性,
在recipe/action中:
在theRecipe中:
同理对于左边的食谱列表:
在recipeList/action中:我们要添加key属性
在theSearchresults中:
好的,基本完成了,比较粗糙,但是完成比完美更重要,你说对吗?😉
我的代码可以在GitHubhttps://github.com/yudengisemily/forkify-from-js-to-vue.git找到,感谢你陪我到这,你肯定发现了一些代码不精简的地方,希望你能多多包涵,如果有合理建议,可以直接联系我,谢谢!💕💕💕