赞
踩
node_modules文件夹:项目依赖文件夹
public文件夹:一般放置一些静态资源(图片),需要注意的是:放在public文件夹中的静态资源,webpack进行打包的时候会原封不动的打包到dist文件夹中
src文件夹(程序员源代码文件夹):
assets文件夹:一般也是放置静态资源(一般放置多个组件共用的静态资源),需要注意:放置在assets文件夹里面的静态资源,在webpack打包的时候,webpack会把静态资源当做一个模块,打包到JS文件中
components文件夹:一般放置的是非路由组件(全局组件)
App.vue:唯一的根组件,Vue当中的组件(.vue)
babel.config.js:配置文件(babel相关)
package.json文件:认为是项目的“身份证”,记录项目叫做什么、项目中有哪些依赖、项目怎么运行
package-lock.json:缓存性文件
README.md:说明性文件
项目运行起来的时候,让浏览器自动打开:
找到package.json文件
修改scripts配置项的serve命令(添加--open)
- "scripts": {
- "serve": "vue-cli-service serve --open",
- "build": "vue-cli-service build",
- "lint": "vue-cli-service lint"
- }
关闭eslint校验:
添加下列语句即可
lintOnSave:false
src文件夹简写方法:配置别名 @(在jsconfig.json文件夹中)
webstorm里自带下列代码,有需要的朋友可以复制一下放到jsonconfig.json中
@代表的是src文件夹,这样将来即使文件过多,找的时候也很方便
- "compilerOptions": {
- "target": "es5",
- "module": "esnext",
- "baseUrl": "./",
- "moduleResolution": "node",
- "paths": {
- "@/*": [
- "src/*"
- ]
- }
项目路由的分析:vue-router
前端所谓路由:KV键值对
key:URL(地址栏中的路径)
value:相应的路由组件
项目为上中下结构
路由组件:Home首页路由组件、Search路由组件、Login登录组件、Register注册组件
非路由组件:Header头部组件(首页、搜索页)、Footer尾部组件(在首页、搜索页有,但是在登录页面是没有的)
开发项目的顺序:
1.书写静态页面(HTML+CSS)
2.拆分组件
3.获取服务器的数据动态展示
4.完成相应的动态业务逻辑
路由组件的搭建
下载vue-router:注意vue2对应vue-router@3,vue3对应vue-router@4
由上述分析,路由组件应该有四个:Home、Search、Login、Register
components文件夹:经常放置的是非路由组件(共用全局组件)
pages|views文件夹:经常放置路由组件
配置路由:项目中配置的路由一般放置在router文件夹中
总结:
路由组件与非路由组件的区别:
1.路由组件一般放置在pages|views文件夹下,非路由组件一般放置在components文件夹中
2.路由组件一般需要在router文件夹中进行注册(使用的即为组件的名字),非路由组件在使用的时候,一般都是以标签的形式使用
3.注册完路由,不管路由组件、还是非路由组件,身上都有$route、$router属性
$route:一般用于获取路由信息(路径、query、params等等)
$router:一般进行编程式导航,进行路由跳转(push、replace)
路由的跳转:
路由的跳转有两种形式:
声明式导航router-link,可以进行路由的跳转
编程式导航push|replace,可以进行路由跳转
编程式导航:
声明式导航能做的,编程式导航都能做
但是编程式导航除了可以进行路由跳转,还可以做一些其他的业务处理
- //搜索按钮的回调函数,需要向search路由进行跳转
- goSearch(){
- this.$router.push('/search')
- }
声明式导航:
<router-link to="/login">登录</router-link>
显示或者隐藏组件:v-if|v-show
Footer组件:在Home、Search显示Footer组件
Footer组件:在登录、注册的时候隐藏
<Footer v-show="$route.path=='/home'||$route.path=='/search'"/>
我们可以根据组件身上的$route获取当前路由信息,通过路由路径判断Footer的显示与隐藏
在配置路由的时候,可以给路由添加路由元信息[meta]
- routes:[
- {
- path:'/home',
- component:Home,
- meta:{show:true}
- },
- {
- path:"/search",
- component:Search,
- meta:{show:true}
- },
- {
- path:"/login",
- component:Login,
- meta:{show:false}
- },
- {
- path:"/register",
- component:Register,
- meta:{show:false}
- },
- //重定向:在项目跑起来的时候,访问"/",立马让它定向到首页
- {
- path:'*',
- redirect:"/home"
- }
- ]
<Footer v-show="$route.meta.show"/>
路由跳转有几种方式?
比如:A->B
声明式导航:router-link(务必要有to属性),可以实现路由的跳转
编程式导航:利用的是组件实例的$router.push|$router.replace方法,可以实现路由的跳转(可以书写一些自己的业务)
路由传参,参数有几种写法?
params参数:属于路径当中的一部分。需要注意,在配置路由的时候,需要占位
- {
- path:"/search/:keyword",
- component:Search,
- meta:{show:true}
- }
query参数:不属于路径当中的一部分,类似于ajax中的queryString /home?k=v&k=v,不需要占位
路由传递参数:
字符串形式:
this.$router.push('/search/'+this.keyword+"?k="+this.keyword.toUpperCase())
模板字符串形式:
this.$router.push(`/search/${this.keyword}?k=${this.keyword.toUpperCase()}`)
对象的形式:
注意:使用对象的形式时,需要给要跳转的路由命名,而不能使用路径来跳转
- this.$router.push({
- name:'search',
- params:{
- keyword:this.keyword
- },
- query:{
- k:this.keyword.toUpperCase()
- }
- })
面试题1:路由传递参数(对象写法)path是否可以结合params参数一起使用?
不能,路由跳转传参的时候,对象的写法可以是name、path形式,但需要注意的是,path这种写法不能和params参数一起写
面试题2:如何指定params参数可传可不传?
如果路由要求传递params参数,但你不传,那么url会出现问题
如果想要指定params参数可传可不传,需要在配置路由的时候在占位的后面加上一个问号
- {
- name:'search',
- path:"/search/:keyword?",
- component:Search,
- meta:{show:true}
- }
面试题3:params参数可以传递也可以不传递,但如果传递的是空串,如何解决?
使用undefined解决:params参数可以传递、不传递或传递空的字符串
- this.$router.push({
- name:'search',
- params:{keyword:""||undefined},
- query:{k:this.keyword.toUpperCase()}
- })
面试题4:路由组件能不能传递props数据?
布尔值写法:只能传递params参数
props:true
对象写法:额外的给路由组件传递一些props
props:{a:1,b:2}
函数写法:可以params参数、query参数,通过props传递给路由组件
props:($route)=>{
return {keyword:$route.params.keyword,k:$route.query.k}
}
编程式路由跳转到当前路由(参数不变),多次执行会抛出NavigationDuplicated的警告错误
首先,底层push方法会返回一个Promise对象
- function push(){
- return new Promise((resolve,reject)=>{
-
- })
- }
解决方法:
1.通过给push方法传递相应的成功、失败的回调函数,可以捕获到当前的错误并解决
这种写法治标不治本,将来在别的组件当中push|replace,编程式导航还有类似错误
- this.$router.push({
- name:'search',
- params:{keyword:this.keyword},
- query:{k:this.keyword.toUpperCase()},
- },()=>{},()=>{})
2.
this:当前组件实例(search)
this.$router属性:当前的这个属性,属性值VueRouter类的一个实例,当在入口文件注册路由的时候,给组件实例添加$router|$route属性
- function VueRouter(){
-
- }
- //原型对象的方法
- VueRouter.prototype.push=function(){
- //函数的上下文为VueRouter类的一个实例
- }
- let $router=new VueRouter()
- $router.push(xxx)
重写方法:
- //先把VueRouter原型对象的push保存一份
- let originPush=VueRouter.prototype.push
-
- //重写push|replace
- //第一个参数:告诉原来的push方法,你往哪里跳转(传递哪些参数
- VueRouter.prototype.push=function(location,resolve,reject){
- if(resolve&&reject){
- //call|apply区别
- //相同点:都可以调用函数依次,都可以篡改函数的上下文一次
- //不同点:call与apply传递参数:call传递参数用逗号隔开,apply方法执行传递数组
- originPush.call(this,location,resolve,reject)
- }else{
- originPush.call(this,location,()=>{},()=>{})
- }
- }
由于三级联动,在Home、Search、Detail中都有使用,所以把三级联动注册为全局组件
好处:只需要注册一次,就可以在项目的任何地方使用
全局组件的注册:
第一个参数:全局组件的名字
第二个参数:指定是哪一个组件
Vue.component(TypeNav.name,TypeNav)
为什么要对axios进行二次封装?
请求拦截器、响应拦截器:请求拦截器,可以在发请求之前处理一些业务
响应拦截器,当服务器数据返回以后,可以处理一些事情
在项目中API文件夹常用来放置axios相应文件
接口当中,路径都带有/api
==>baseURL:"/api"
- import axios from "axios";
-
- const requests = axios.create({
- baseURL:"/api",
- timeout:5000
- })
-
- //请求拦截器
- requests.interceptors.request.use((config)=>{
- //config是headers的请求头
-
- return config
- })
-
- //响应拦截器
- requests.interceptors.response.use((res)=>{
- //成功的回调函数:服务器响应数据回来以后,响应拦截器可以检测到,并做一些处理
- return res.data
- },
- (error)=>{
- //响应失败的回调函数
- return Promise.reject(new Error(error))
- })
-
-
- export default requests
什么是跨域?
协议、域名、端口号不同的请求,称之为跨域
http://localhost:8080/#/home ---前端项目本地服务器
http://39.98.123.211 ---后台服务器
- //配置代理跨域
- devServer:{
- proxy:{
- "/api":{
- target:"http://gmall-h5-api.atguigu.cn",
- }
- }
- }
- //引入进度条
- import nprogress from 'nprogress'
- //引入进度条的样式
- import 'nprogress/nprogress.css'
进度条的开始:
nprogress.start()
进度条的结束:
nprogress.end()
vuex是什么?
vuex是官方提供的一个插件,状态管理库,集中式管理项目中共用的数据
mapState传入参数是一个对象,对象的键值对中,值是一个函数。当时用这个计算属性的时候,右侧的函数会立即执行一次
函数注入一个参数为state,即为大仓库中的数据
- ...mapState({
- categoryList:(state)=>{
- return state.typeNav.categoryList
- }
- })
动态添加类:
- <div class="item"
- v-for="(c1,index) in categoryList"
- :key="c1.categoryId"
- :class="{cur:currentIndex===index}"
- >
currentIndex的改变,初始值为-1:
- changeIndex(index){
- this.currentIndex=index
- },
- leaveIndex(){
- this.currentIndex=-1
- }
类:
- .cur{
- background-color: skyblue;
- }
- <div class="item-list clearfix"
- :style="{display:currentIndex===index?'block':'none'}">
卡顿现象:事情触发非常频繁,而且每一次的触发,回调函数都要去执行(如果时间很短,而回调函数内部有计算,那么很有可能出现浏览器卡顿)
函数的节流与防抖:
节流:在规定的间隔时间范围内不会重复处罚回调,只有大于这个时间才会触发回调,把频繁触发变为少量触发
防抖:前面所有的触发都被取消,最后一次执行在规定的时间之后才会触发,也就是说如果连续快速的触发,则只会执行一次
插件lodash:封装了函数的防抖与节流业务
三级联动节流:
引入throttle函数:
import throttle from 'lodash/throttle'
使用节流函数throttle:
- changeIndex:throttle(function(index){
- this.currentIndex=index
- },50),
三级联动用户可以点击:一级分类、二级分类、三级分类
当你点击的时候,Home模块跳转到Search模块,一级会把用户选中的产品(产品的名字、产品的ID)在路由跳转的时候,进行传递
router-link是一个组件,当服务器的数据返回值后,会循环出很多的router-link组件实例
创建组件实例的时候,一瞬间创建1000+组件实例是很耗用内存的,因此可能出现卡顿现象
最好的解决方案:编程式导航+事件委派 实现路由的跳转和参数的传递
利用事件委派存在的一些问题:
1.点击的不一定是a标签
事件委派是把所有的子节点(h3/dt/dl/em)的时间委派给父亲结点。而只有点击a标签的时候,才会进行路由跳转,但如何确定点击的一定是a标签呢?
即使你能确定点击的是a标签,如何区分是一级、二级还是三级分类的a标签呢?
2.如何获取参数【1/2/3级分类的产品名字、id】
解决方案:
1.把子节点当中的a标签加上自定义属性data-categoryName,而其他的子节点是没有的
<a :data-categoryName="c1.categoryName">{{c1.categoryName}}</a>
2.给每一个a标签添加上对应的data-category1Id/data-category2Id/data-category3Id,用来区分不同级别的分类
总的解决方案:
- goSearch(event){
- let {categoryname,category1id,category2id,category3id} = event.target.dataset
- if(categoryname){
- //整理路由跳转的参数
- let location={name:'search'}
- let query={categoryName:categoryname}
- if(category1id){
- query.categoryName=category1id
- }else if(category2id){
- query.categoryName=category2id
- }else{
- query.categoryName=category3id
- }
- //整理参数
- location.query=query
- //路由跳转
- this.$router.push(location)
- }
- }
需求:
在home组件下,会默认显示二级菜单和三级菜单
但search组件下,只默认显示一级菜单
解决方案:
通过v-show动态控制二级菜单和三级菜单的显示与隐藏(v-show="show")
通过路由控制,决定show属性的true/false
原理:每跳转到一个组件,如果有TypeNav,都会重新创建一个TypeNav的实例,也就是说mounted函数会再次执行。可以将路由判断的逻辑写在mounted钩子里
- mounted(){
- //通知Vuex发送请求,获取数据,存储于仓库之中
- this.$store.dispatch('categoryList')
- if(this.$route.path!='/home'){
- this.show=false
- }
- }
在鼠标滑过“全部商品分类”时,应该有对应的二级分类、三级分类的展示
对于home组件,鼠标离开时二级分类不会消失
对于search组件,鼠标离开时只展示一级分类
逻辑判断:
TypeNav模块:
<div @mouseleave="leaveShow" @mouseenter="enterShow">
- enterShow(){
- this.show=true
- },
- leaveShow(){
- this.currentIndex=-1
- if(this.$route.path!='/home'){
- this.show=false
- }
- }
过渡动画:前提是组件/元素务必要有v-if/v-show指令,才可以进行过渡动画
- <!--过渡动画-->
- <transition name="sort">
- <div class="sort" v-show="show">
- ……中间内容省略
- </div>
- </transition>
- //过渡动画的样式
- .sort-enter{
- height:0;
- }
- .sort-enter-to{
- height:461px;
- }
- .sort-enter-active{
- transition:all .7s linear
- }
由于每次home/search组件的销毁和挂载都会导致TypeNav的销毁和挂载,即都会执行mounted函数,从而频繁地向服务器发送请求。但是三级联动组件获取到的内容是不变的,所以可以做优化
可以将三级联动的数据请求放在App.vue的钩子函数中:
- mounted(){
- //通知Vuex发送请求,获取数据,存储于仓库之中
- this.$store.dispatch('categoryList')
- }
捎带params参数:
- //判断:如果路由跳转的时候带有params参数,也要捎带传递过去
- if(this.$route.params){
- location.params=this.$route.params
- }
合并参数:
- //搜索按钮的回调函数,需要向search路由进行跳转
- goSearch(){
- let location={
- name:'search',
- params:{keyword:this.keyword||undefined}
- }
- if(this.$route.query){
- location.query=this.$route.query
- }
- this.$router.push(location)
- }
开发Home首页当中的ListContainer组件与Floor组件:
因为服务器返回的数据只有商品分类和菜单分类的数据,对于ListContainer组件与Floor组件数据服务器是没有提供的。可以通过mockjs模拟
使用步骤:
1)在项目当中src文件夹中创建mock文件夹
2)第二步准备JSON数据(mock文件夹中创建相应的JSON文件)
3)把mock数据需要的图片放置到public文件夹中(public文件夹在打包的时候,会把相应资源原封不动地放在dist文件夹下)
4)创建mockServer.js,开始实现mock虚拟数据
5)把mockServer.js文件在入口文件中引入(至少需要执行一次,才能模拟数据)
问:JSON数据格式文件根本没有对外暴露,为什么可以直接引入?
答:webpack默认对外暴露图片和JSON数据格式文件
Mock.mock()函数有两个参数
第一个参数是请求的地址
第二个参数是请求的数据
- //先引入mockjs模板
- import Mock from 'mockjs'
- //把JSON数据格式引入进来
- import banner from './banner.json'
- import floor from './floor.json'
-
- Mock.mock("/mock/banner",{
- code:200,
- data:banner
- })
- Mock.mock("/mock/floor",{
- code:200,
- data:floor
- })
封装mockServer.js
- import axios from 'axios'
- import nprogress from 'nprogress'
- import 'nprogress/nprogress.css'
-
- let requests=axios.create({
- baseURL:"/mock",
- timeout:5000
- })
-
- requests.interceptors.request.use((config)=>{
- nprogress.start()
- return config
- })
-
- requests.interceptors.response.use(
- (res)=>{
- nprogress.done()
- return res.data
- },
- (err)=>{
- return Promise.reject(new Error(err))
- }
- )
-
- export default requests
发送请求的函数:
- //获取banner(Home首页轮播图接口)
- export const reqGetBannerList=()=>mockRequest.get('/banner')
- //获取floor
- export const reqGetFloor=()=>mockRequest.get('/floor')
vuex处理:
- import { reqGetBannerList} from "@/api";
-
- const state={
- bannerList:[]
- }
- const mutations={
- GETBANNERLIST(state,bannerList){
- state.bannerList=bannerList
- }
- }
- const actions={
- async getBannerList({commit}){
- let result=await reqGetBannerList()
- if(result.code===200){
- commit('GETBANNERLIST')
- }
- }
- }
- const getters={}
-
- export default{
- state,mutations,actions,getters
- }
获取数据:
- computed:{
- ...mapState({
- bannerList:state=>state.home.bannerList
- })
- }
第一步:引包(引入相应的JS/CSS)
在main.js引入一次即可:
- import 'swiper/css/swiper.css'
- import 'swiper/js/swiper'
第二步:页面中的结构
- <div class="swiper-slide"
- v-for="(carousel,index) in bannerList" :key="carousel.id"
- >
- <img :src="carousel.imgUrl"/>
- </div>
第三步:new Swiper实例,给轮播图添加动态效果
- new Swiper(document.querySelector('.swiper-container'),{
- loop:true,
- pagination:{
- el:'.swiper-pagination',
- //点击小球的时候也切换图片
- clickable:true
- },
- //前进后退的按钮
- navigation:{
- nextEl:".swiper-button-next",
- prevEl:".swiper-button-prev"
- }
- })
安装swiper5版本比较稳定
首先需要明确,如果直接将swiper实例写在mounted函数中,尽管mounted是在页面结构加载完成后执行,但此时从服务器拿到的数据还没有放到仓库中,故swiper实例中拿不到bannerList的数据
解决方式一:
可以通过setTimeout等待一段时间,这段时间里从服务器拿到的数据就已经放到仓库中了
- setTimeout({
- //swiper实例的实现
- },2000)
解决方式二:
watch+nextTick
watch:数据监听,监听已有数据的变化
监听bannerList数据的变化:从空数组变为数组里有四个元素
如果执行handler方法,代表组件实例身上的这个属性的属性值已经有了
但是仅有watch是不够的,因为数据更新了,但是页面渲染可能还没结束
nextTick:
Vue.nextTick([callback,context])
用法:在下次DOM更新循环结束之后执行延迟回调。在修改数据之后立刻使用这个方法,获取更新后的DOM
$nextTick可以保证页面中的结构一定是有的,经常和很多需要DOM已经存在才能实现功能的插件一起使用
watch+nextTick实现功能:
- watch:{
- bannerList:{
- handler(newValue,oldValue){
- this.$nextTick(()=>{
- var mySwiper=new Swiper(document.querySelector('.swiper-container'),{
- loop:true,
- pagination:{
- el:'.swiper-pagination',
- //点击小球的时候也切换图片
- clickable:true
- },
- //前进后退的按钮
- navigation:{
- nextEl:".swiper-button-next",
- prevEl:".swiper-button-prev"
- }
- })
- })
- }
- }
- }
<div class="swiper-container" ref="mySwiper">
new Swiper(this.$refs.mySwiper,{
由于Floor组件在Home中被调用两次,如果是在Floor组件中触发getFloorList,很难实现两次调用数据不同
故应该在Home组件中触发
v-for可以在自定义组件上使用
组件间通信的方式有哪些?
props:用于父子组件通信
自定义事件:@on 与 @emit
全局事件总线:$bus 全能
pubsub-js:vue中几乎不用(react使用的较多)
插槽
vuex
List组件中的轮播图,是当前组件内部发送请求、动态渲染解构服务器返回的数据,因此必须使用watch+nextTick
Floor组件中的轮播图,请求由父组件Home发送,并且数据是父组件发送过来的,此时结构已经完全解析好了,所以可以在mounted里写轮播图实例
- mounted(){
- new Swiper(this.$refs.cur,{
- loop:true,
- pagination:{
- el:'.swiper-pagination',
- //点击小球的时候也切换图片
- clickable:true
- },
- //前进后退的按钮
- navigation:{
- nextEl:".swiper-button-next",
- prevEl:".swiper-button-prev"
- }
- })
- }
切记:以后在开发项目的时候,如果看到某一个组件在很多地方都使用,可以把它变成全局组件
- <template>
- <div class="swiper-container" ref="cur">
- <div class="swiper-wrapper">
- <div class="swiper-slide"
- v-for="(carousel,index) in list" :key="carousel.id"
- >
- <img :src="carousel.imgUrl">
- </div>
- </div>
- <!-- 如果需要分页器 -->
- <div class="swiper-pagination"></div>
-
- <!-- 如果需要导航按钮 -->
- <div class="swiper-button-prev"></div>
- <div class="swiper-button-next"></div>
- </div>
- </template>
-
- <script>
- import Swiper from "swiper";
-
- export default {
- name: "Carousel",
- props:['list'],
- watch:{
- list:{
- immediate:true,
- handler(){
- this.$nextTick(()=>{
- new Swiper(this.$refs.cur,{
- loop:true,
- autoplay:true,
- pagination:{
- el:'.swiper-pagination',
- //点击小球的时候也切换图片
- clickable:true
- },
- //前进后退的按钮
- navigation:{
- nextEl:".swiper-button-next",
- prevEl:".swiper-button-prev"
- }
- })
- })
- }
- }
- }
- }
- </script>
-
- <style scoped>
-
- </style>
getters,相当于计算属性
项目中getters的主要作用:简化仓库中的数据(简化数据而生)
可以把我们将来在组件当中需要用的数据简化一下【将来组件获取数据就简单多了】
数据的返回:
以goodsList为例,如果服务器数据回来了,返回一个数组
如果网络不给力/没有网,state.searchList.goodsList返回的就是undefined
- const getters={
- goodsList(state){
- return state.searchList.goodsList||[]
- },
- trademarkList(state){
- return state.searchList.trademarkList||[]
- },
- attrsList(state){
- return state.searchList.attrsList||[]
- }
- }
与getters对应的是mapGetters,此函数接收一个数组,不像state那样划分模块
- computed:{
- ...mapGetters(['goodsList'])
- }
使用Object.assign方法,快速合并数据
在beforeMount里整理数据,在mounted发送请求
- export default {
- name: 'Search',
-
- components: {
- SearchSelector
- },
-
- data(){
- return {
- //带给服务器的参数
- searchParams:{
- category1Id:"",
- category2Id:"",
- category3Id:"",
- categoryName:"",
- keyword:"",
- order:"",
- pageNo:1,
- pageSize:3,
- props:[],
- trademark:""
- }
- }
- },
-
- beforeMount(){
- Object.assign(this.searchParams,this.$route.query,this.$route.params)
- },
-
- mounted(){
- this.getData()
- },
-
- methods:{
- getData(){
- this.$store.dispatch('getSearchList',this.searchParams)
-
- }
- },
-
- computed:{
- ...mapGetters(['goodsList'])
- }
-
- }
当点击三级联动组件或者搜索框时,路径中的query或params会发生改变。那么路由就会改变,监听路由即可。
每一次请求完毕,应该把相应的1/2/3级分类的id只看,让它接收下一次相应的1/2/3级分类
分类名字和关键字不用清理:因为每一次路由发生变化,都会赋予它新的数据
- <!-- 分类的面包屑-->
- <li class="with-x" v-if="searchParams.categoryName">{{searchParams.categoryName}} <i @click="removeCategoryName">x</i></li>
- watch:{
- $route(newValue,oldValue){
- Object.assign(this.searchParams,this.$route.query,this.$route.params)
- this.getData()
- this.searchParams.category1Id=''
- this.searchParams.category2Id=''
- this.searchParams.category3Id=''
- }
- }
即使带给服务器的参数是为空的字符串,仍然会把相应的字段带给服务器
但如果把相应的字段变为undefined,当前这个字段就不会带给服务器了(性能优化)
骚操作:
对自己所在组件进行路由跳转,可以清除地址栏的指定参数(比如query参数)
- //删除分类的名字
- removeCategoryName(){
- //把带给服务器的参数置空,并给服务器发请求
- this.searchParams.categoryName=undefined
- this.searchParams.category1Id=undefined
- this.searchParams.category2Id=undefined
- this.searchParams.category3Id=undefined
- this.getData()
- this.$router.push({name:'search',params:this.$route.params})
- }
- <!-- 关键字的面包屑-->
- <li class="with-x" v-if="searchParams.keyword">{{searchParams.keyword}} <i @click="removeQueryName">x</i></li>
- removeQueryName(){
- this.searchParams.keyword=undefined
- this.getData()
-
- }
当面包屑中的关键字清除以后,需要让兄弟组件Header组件中的关键字清除
设计组件间通信:
props:父子
自定义事件:子父
vuex:仓库数据
插槽:父子
$bus:全局事件总线
注册全局事件总线:
- new Vue({
- render: h => h(App),
- beforeCreate(){
- Vue.prototype.$bus=this
- },
- router,
- store,
- }).$mount('#app')
在Search组件中触发事件:
this.$bus.$emit('clear')
在Header组件中绑定事件:
- mounted(){
- this.$bus.$on("clear",()=>{
- this.keyword=""
- })
- }
更新params参数:(Search组件)
this.$router.push({name:'search',query:this.$route.query})
在Search组件中展示品牌的面包屑:
注意trademark.tmName的展示方式
- <!-- 品牌的面包屑-->
- <li class="with-x" v-if="searchParams.trademark">{{searchParams.trademark.split(":")[1]}} <i @click="removeTrademark">x</i></li>
子父组件间通信:
父传子自定义事件:
- <!--selector-->
- <SearchSelector @trademarkInfo="trademarkInfo"/>
由于传递过来的trademark是个对象,而传递给服务器的trademark是个字符串,所以要进行字符串拼接
- trademarkInfo(trademark){
- console.log(trademark)
- this.searchParams.trademark=`${trademark.tmId}:${trademark.tmName}`
- this.getData()
- }
-
子触发父中的函数:
- trademarkHandler(trademark){
- this.$emit('trademarkInfo',trademark)
- }
父组件:
- <!-- 平台售卖的属性值展示-->
- <li class="with-x" v-for="(attrValue,index) in searchParams.props" :key="index">{{attrValue.split(":")[1]}} <i @click="removeAttr(index)">x</i> </li>
- </ul>
- attrInfo(attr,attrValue){
- let props=`${attr.attrId}:${attrValue}:${attr.attrName}`
- //数组去重
- if(this.searchParams.props.indexOf(props)===-1)
- this.searchParams.props.push(props)
- },
- removeAttr(index){
- //再次整理参数
- this.searchParams.props.splice(index,1)
- //再次发送请求
- this.getData()
- }
父子组件间通信:
- <!--selector-->
- <SearchSelector @trademarkInfo="trademarkInfo" @attrInfo="attrInfo"/>
- attrInfo(attr,attrValue){
- this.$emit("attrInfo",attr,attrValue)
- }
排序方式:
1:综合
2:价格
asc:升序
desc:降序
①order属性的属性值最多有4种写法:
1:asc
1:desc
2:asc
2:desc
- //排序的初始状态:综合:降序
- order:"1:desc",
②综合和价格谁应该具有类名active?
通过order属性值当中是包含1还是包含2来判断
- <ul class="sui-nav">
- <li :class="{active:searchParams.order.indexOf('1')!==-1}">
- <a href="#">综合</a>
- </li>
- <li :class="{active:searchParams.order.indexOf('2')!==-1}">
- <a href="#">价格</a>
- </li>
- </ul>
由于模板中不建议使用太长的表达式语句,可以用计算属性书写:
- <ul class="sui-nav">
- <li :class="{active:isOne}">
- <a href="#">综合</a>
- </li>
- <li :class="{active:isTwo}">
- <a href="#">价格</a>
- </li>
- </ul>
- computed:{
- ...mapGetters(['goodsList']),
- isOne(){
- return this.searchParams.order.indexOf('1')!==-1
- },
- isTwo(){
- return this.searchParams.or.indexOf('2')!==-1
- }
- }
③综合和价格,谁应该有箭头?
谁有类名active,谁就有箭头
④箭头用什么制作?
阿里图标库iconfont
在复制的地址前加 https:
放入index.html静态页面即可使用
<link rel="stylesheet" href="https://at.alicdn.com/t/c/font_3941403_5qqhi0nusus.css">
图标的基本使用:
- <ul class="sui-nav">
- <li :class="{active:isOne}">
- <a href="#">综合 <span v-show="isOne" class="iconfont icon-up-arrow"></span></a>
- </li>
- <li :class="{active:isTwo}">
- <a href="#">价格 <span v-show="isTwo" class="iconfont icon-arrow_down"></span></a>
- </li>
- </ul>
通过计算属性动态决定箭头上下:
- isAsc(){
- return this.searchParams.order.indexOf("asc")!==-1
- },
- isDesc(){
- return this.searchParams.order.indexOf("desc")!==-1
- }
- <ul class="sui-nav">
- <li :class="{active:isOne}">
- <a href="#">综合 <span v-show="isOne" class="iconfont" :class="{'icon-up-arrow':isAsc,'icon-arrow_down':isDesc}"></span></a>
- </li>
- <li :class="{active:isTwo}">
- <a href="#">价格 <span v-show="isTwo" class="iconfont" :class="{'icon-up-arrow':isAsc,'icon-arrow_down':isDesc}"></span></a>
- </li>
- </ul>
设定改变上下箭头的规则:
- changeOrder(flag){
- let originFlag=this.searchParams.order.split(":")[0]
- let originSort=this.searchParams.order.split(":")[1]
- let newOrder=''
- if(flag===originFlag){
- originSort=originSort==="desc"?"asc":"desc"
- newOrder=`${originFlag}:${originSort}`
- }else{
- newOrder=`${flag}:desc`
- }
- this.searchParams.order=newOrder
- this.getData()
- }
大家注意newOrder=`${flag}:desc`中的冒号一定要用英文字符,否则切换失败!
为什么很多项目采用分页功能?
因为比如电商平台同时展示的数据有很多(1w+),需要采用分页功能一次加载少量数据,防止卡顿
分页器展示,需要哪些数据(条件)?
需要知道当前是第几页:pageNo字段代表当前页数
需要知道每一页需要展示多少条数据:pageSize字段进行代表
需要知道整个分页器一共有多少条数据:total字段进行代表---【获取另外一条信息:总共有多少页】
需要知道分页器连续页面个数:5|7(一般是奇数,对称比较好看)
①总页数小于连续页数
仅展示总页数即可
②总页数大于/等于连续页数
根据当前页算起始、结束页码:
i.起始页码小于1,此时置起始页码为1,结束页码为连续页码数
ii.结束页码大于总页码,此时置结束页码为总页码,起始页码为:总页码-连续页码+1
iii.正常情况下,直接计算
- computed:{
- totalPage(){
- return Math.ceil(this.total/this.pageSize)
- },
- startNumAndEndNum(){
- let start=1,end=1;
- //不正常现象:总页数没有连续页码数多
- if(this.continues>this.totalPage){
- end=this.totalPage
- }else{
- start=this.pageNo-parseInt(this.continues)/2
- end=this.pageNo+parseInt(this.continues)/2
- if(start<1){
- start=1
- end=this.continues
- }
- if(end>this.totalPage){
- start=end-this.continues+1
- end=this.totalPage
- }
- }
- return {start,end}
- }
v-for也可以用于循环数字。指定要循环的数字,比如说10
则v-for会遍历0~10
如果希望从指定的数字开始遍历,可以用v-if加以限制
- <template>
- <div class="fr page">
- <div class="sui-pagination clearfix">
- <ul>
- <li class="prev disabled">
- <a href="#">«上一页</a>
- </li>
- <li class="active" v-if="startNumAndEndNum.start>1">
- <a href="#">1</a>
- </li>
- <li>
- <a href="#" v-if="startNumAndEndNum.start>2">...</a>
- </li>
- <li v-for="(page,index) in startNumAndEndNum.end" :key="index" v-if="page>=startNumAndEndNum.start">
- <a href="#">{{page}}</a>
- </li>
- <li class="dotted" v-if="startNumAndEndNum.end<(totalPage-1)"><span>...</span></li>
- <li>
- <a href="#" v-if="startNumAndEndNum.end<totalPage">{{totalPage}}</a>
- </li>
- <li>
- <a href="#">{{totalPage}}</a>
- </li>
- <li class="next">
- <a href="#">下一页»</a>
- </li>
- </ul>
- <div><span>共{{total}}条 </span></div>
- </div>
- </div>
- </template>
- <template>
- <div class="fr page">
- <div class="sui-pagination clearfix">
- <ul>
- <li class="prev" :disabled="pageNo===1" @click="$emit('getPageNo',pageNo-1)">
- <a>«上一页</a>
- </li>
- <li class="active" v-if="startNumAndEndNum.start>1" @click="$emit('getPageNo',1)">
- <a>1</a>
- </li>
- <li>
- <a v-if="startNumAndEndNum.start>2">...</a>
- </li>
- <li
- v-for="(page,index) in startNumAndEndNum" :key="index"
- v-if="page>=startNumAndEndNum.start"
- @click="$emit('getPageNo',page)"
- >
- <a>{{page}}</a>
- </li>
- <li class="dotted" v-if="startNumAndEndNum.end<(totalPage-1)"><span>...</span></li>
- <li>
- <a
- v-if="startNumAndEndNum.end<totalPage"
- @click="$emit('getPageNo',totalPage)"
- >{{totalPage}}</a>
- </li>
- <li>
- <a>{{totalPage}}</a>
- </li>
- <li class="next" :disabled="pageNo===totalPage" @click="$emit('getPageNo',pageNo+1)">
- <a>下一页»</a>
- </li>
- </ul>
- <div><span>共{{total}}条 </span></div>
- </div>
- </div>
- </template>
:class="{active:pageNo===page}"
开发某一个产品的详情页面?
1.静态组件
2.发请求
3.vuex
4.动态展示组件
当点击商品的图片的时候,跳转到详情页面,在路由跳转的时候需要带上产品的ID给详情页面
detail路由:
- {
- path:'/detail/:id',
- component:Detail,
- meta:{isShow:true}
- },
路由跳转:
- <router-link :to="`/detail/${good.id}`">
- <img :src="good.defaultImg" />
- </router-link>
滚动行为:
使用前端路由,当切换到新的路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。vue-router能做到,且做得更好,他可以让你自定义路由切换时页面如何滚动
注意:这个功能只在支持history.pushState的浏览器中可用
- export default new VueRouter({
- routes,
- scrollBehavior(to,from,savePosition){
- //返回的这个y=0,代表页面在最上面
- return {y:0}
- }
- })
获取数据的接口:
- //获取产品详细信息的接口 URL:/api/item/{skuId} 请求方式:get
- export const reqGoodsInfo=(skuId)=>requests({
- url:`/item/${skuId}`,
- method:'get'
- })
vuex仓库捞数据:
- import {reqGoodsInfo} from "@/api";
-
- const state={
- goodsInfo:{}
- }
- const mutations={
- GETGOODSINFO(state,goodsInfo){
- state.goodsInfo=goodsInfo
- }
- }
- const actions={
- //获取产品信息的action
- async getGoodsInfo({commit},skuId){
- let result=await reqGoodsInfo(skuId)
- if(result.code===200){
- commit(GETGOODSINFO(result.data))
- }
- }
- }
- const getters={}
-
- export default {
- state,mutations,actions,getters
- }
报错,但不影响程序运行
这是因为仓库中的getters方法:
state.goodsInfo初始状态为空对象,空对象的categoryView属性值为undefined。undefined.category1Id肯定会报错。
- const getters={
- categoryView(state){
- return state.goodsInfo.categoryView
- }
- }
可以改为:
那么此时计算出来的categoryView属性值至少是一个空对象,假的报错就不会有了
- const getters={
- categoryView(state){
- return state.goodsInfo.categoryView||{}
- }
- }
Detail组件和Zoom组件通信的时候,通用会报错,可以修改为:
- skuImageList(){
- return this.skuInfo.skuImageList||[{}]
- }
先通过遍历父数组将所有子元素的isChecked取消
再将点中的子元素isChecked点亮
- changeActive(spuSaleAttrValue,spuSaleAttrValueList){
- spuSaleAttrValueList.forEach(item=>{
- item.isChecked='0'
- })
- spuSaleAttrValue.isChecked='1'
- }
通过ImgList传递给Zoom的index,使得轮播图中的图片改变导致Zoom中展示的大图一起改变(联动效应)
ImgList:
- changeCurrentIndex(index){
- this.currentIndex=index
- this.$bus.$emit('getIndex',this.currentIndex)
- }
Zoom:
-
- data(){
- return {
- currentIndex:0
- }
- },
- computed:{
- imgObj(){
- return this.skuImageList[this.currentIndex]||{}
- }
- },
- mounted(){
- this.$bus.$on('getIndex',(index)=>{
- this.currentIndex=index
- })
- }
注意没见过的新属性slidesPereView和slidesPerGroup
- watch:{
- //监听数据:虽然可以保证skuImageList数据已经传递过来,但v-for未必已经遍历完成
- skuImageList(newValue,oldValue){
- this.$nextTick(()=>{
- new Swiper(".swiper-container",{
- navigation:{
- nextEl:".swiper-button-next",
- prevEl:".swiper-button-prev"
- },
- //显示一次显示3个图片
- slidesPerView:3,
- //设置一次切换1张图片
- slidesPerGroup:1
- })
- })
- }
- }
遮罩层方法剖析:
big是用来显示mask滑过的区域,mask向右的时候,big对应的图片应该向左,大小为两倍差
- handler(event){
- let mask=this.$refs.mask
- let big=this.$refs.big
- let left=event.offsetX-mask.offsetWidth/2
- let top=event.offsetY-mask.offsetHeight/2
- //约束范围
- if(left<=0) left=0
- else if(left>=mask.offsetWidth) left=mask.offsetWidth
- if(top<=0) top=0
- else if(top>=mask.offsetHeight) top=mask.offsetHeight
- //修改元素的left|top
- mask.style.left=left+'px'
- mask.style.top=top+'px'
- big.style.left=-2*left+'px'
- big.style.top=-2*top+'px'
- }
- }
总代码:
- <template>
- <div class="spec-preview">
- <img :src="imgObj.imgUrl" />
- <div class="event" @mousemove="handler"></div>
- <div class="big">
- <img :src="imgObj.imgUrl" ref="big"/>
- </div>
- <!-- 遮罩层-->
- <div class="mask" ref="mask"></div>
- </div>
- </template>
-
- <script>
- export default {
- name: "Zoom",
- props:['skuImageList'],
- data(){
- return {
- currentIndex:0
- }
- },
- computed:{
- imgObj(){
- return this.skuImageList[this.currentIndex]||{}
- }
- },
- mounted(){
- this.$bus.$on('getIndex',(index)=>{
- this.currentIndex=index
- })
- },
- methods:{
- handler(event){
- let mask=this.$refs.mask
- let big=this.$refs.big
- let left=event.offsetX-mask.offsetWidth/2
- let top=event.offsetY-mask.offsetHeight/2
- //约束范围
- if(left<=0) left=0
- else if(left>=mask.offsetWidth) left=mask.offsetWidth
- if(top<=0) top=0
- else if(top>=mask.offsetHeight) top=mask.offsetHeight
- //修改元素的left|top
- mask.style.left=left+'px'
- mask.style.top=top+'px'
- big.style.left=-2*left+'px'
- big.style.top=-2*top+'px'
- }
- }
- }
- </script>
-
- <style lang="less">
- .spec-preview {
- position: relative;
- width: 400px;
- height: 400px;
- border: 1px solid #ccc;
-
- img {
- width: 100%;
- height: 100%;
- }
-
- .event {
- width: 100%;
- height: 100%;
- position: absolute;
- top: 0;
- left: 0;
- z-index: 998;
- }
-
- .mask {
- width: 50%;
- height: 50%;
- background-color: rgba(0, 255, 0, 0.3);
- position: absolute;
- left: 0;
- top: 0;
- display: none;
- }
-
- .big {
- width: 100%;
- height: 100%;
- position: absolute;
- top: -1px;
- left: 100%;
- border: 1px solid #aaa;
- overflow: hidden;
- z-index: 998;
- display: none;
- background: white;
-
- img {
- width: 200%;
- max-width: 200%;
- height: 200%;
- position: absolute;
- left: 0;
- top: 0;
- }
- }
-
- .event:hover~.mask,
- .event:hover~.big {
- display: block;
- }
- }
- </style>
- //表单元素修改产品个数
- changeSkuNum(event){
- //用户输入进来的文本*1
- let value=event.target.value*1
- //如果用户输入进来的非法
- if(isNaN(value)||value<1){
- this.skuNum=1
- }else{
- this.skuNum=parseInt(value)
- }
- }
接口api:
血的教训,千万不要把method写成methods!!!!
- export const reqAddOrUpdateShopCart=(skuId,skuNum)=>requests({
- url:`/cart/addToCart/${skuId}/${skuNum}`,
- method:"post"
- })
加入购物车成功的路由:
- {
- name:'addcartsuccess',
- path:'/addcartsuccess',
- component:AddCartSuccess,
- meta:{isShow:true}
- }
仓库中的action:
- //将产品添加到购物车中
- async addOrUpdateShopCart({commit},{skuId,skuNum}){
- //服务器写入数据成功以后,并没有返回其他的数据,只是返回了code=200,代表此处操作成功
- let result= await reqAddOrUpdateShopCart(skuId,skuNum)
- if(result.code==200){
- return 'ok'
- }else{
- return Promise.reject(new Error('fail'))
- }
- }
浏览器存储功能:
本地存储:持久化的——5M
会话存储:并非持久——会话结束就消失
路由传递参数结合会话存储:
进行路由跳转的时候需要将产品信息带给下一级的路由组件
一些简单的数据skuNum,通过query形式给路由组件传递过去
复杂的产品信息数据(比如skuInfo),通过会话存储即可(不持久化,会话结束数据消失)
- async addShopCart(){
- //1.发请求——将产品加入到数据库(通知服务器)
- try {
- await this.$store.dispatch('addOrUpdateShopCart', {
- skuId: this.$route.params.skuId,
- skuNum: this.skuNum})
- //2.服务器存储成功——进行路由跳转传递参数
- sessionStorage.setItem('SKUINFO',JSON.stringify(this.skuInfo))
- this.$router.push({
- name: 'addcartsuccess',
- query:{
- skuNum:this.skuNum
- }
- })
- }catch(err) {
- //3.失败——给用户进行提示
- alert(err.message)
- }
- }
AddCartSuccess组件中使用到sessionStorage存储的item:
- computed:{
- skuInfo(){
- return JSON.parse(sessionStorage.getItem('SKUINFO'))
- }
- }
购物车静态组件——需要修改样式结构
1.调整css让各个项目对齐
2.向服务器发送ajax,获取购物车数据
3.UUID临时游客身份
4.动态展示购物车
获取购物车数据的接口:
- //获取购物车列表数据接口
- export const reqCartList=()=>requests({
- url:"/cart/cartList",
- method:"get"
- })
向服务器发送ajax请求获取购物车数据:
发请求的时候无法获取你购物车里面的数据,因为服务器不知道你是谁
可以使用UUID临时游客身份
uuid函数逻辑:
随机生成一个字符串,且每次执行不能发生变化,游客身份持久存储
①从本地存储中获取uuid
②如果没有
i.生成游客临时身份
ii.本地存储
一定要有返回值!!!
- import {v4 as uuidv4} from 'uuid'
-
- export const getUUID=()=>{
- let uuid_token=localStorage.getItem("UUIDTOKEN")
- if(!uuid_token){
- uuid_token=uuidv4()
- localStorage.setItem('UUIDTOKEN',uuid_token)
- }
- return uuid_token
- }
在请求拦截器中将存储好的uuid发往后台:
- //请求拦截器
- requests.interceptors.request.use((config)=>{
- if(store.state.detail.uuid_token){
- config.headers.userTempId=store.state.detail.uuid_token
- }
-
- //config是headers的请求头
- nprogress.start()
- return config
- })
- <a class="mins" @click="handler('minus',-1,cart)">-</a>
- <input
- autocomplete="off"
- type="text"
- :value="cart.skuNum"
- minnum="1"
- class="itxt"
- @change="handler('change',$event.target.value*1,cart)"
- >
- <a class="plus" @click="handler('add',1,cart)">+</a>
- //修改某一个产品的个数
- async handler(type,disNum,cart) {
- switch (type) {
- //加号
- case 'add':
- disNum = 1
- break
- case 'minus':
- disNum = cart.skuNum > 1 ? -1 : 0
- break
- case 'change':
- if(isNaN(disNum)||disNum<1){
- disNum=0
- }else{
- disNum=parseInt(disNum)-cart.skuNum
- }
- }
- try {
- await this.$store.dispatch('addOrUpdateShopCart', {
- skuId: cart.skuId,
- skuNum: disNum
- })
- this.getData()
- }catch(error){
- alert('修改失败')
- }
- }
特注:因为改变数量的时候如果点击过快可能会发生意想不到的结果,所以可以进行节流处理
- //修改某一个产品的个数
- handler:throttle(async function(type,disNum,cart){
- switch (type) {
- //加号
- case 'add':
- disNum = 1
- break
- case 'minus':
- disNum = cart.skuNum > 1 ? -1 : 0
- break
- case 'change':
- if(isNaN(disNum)||disNum<1){
- disNum=0
- }else{
- disNum=parseInt(disNum)-cart.skuNum
- }
- }
- try {
- await this.$store.dispatch('addOrUpdateShopCart', {
- skuId: cart.skuId,
- skuNum: disNum
- })
- this.getData()
- }catch(error){
- alert('修改失败')
- }
- })
- },
接口:
- //删除购物产品的接口
- export const reqDeleteCartById=(skuId)=>requests({
- url:`/cart/deleteCart/${skuId}`,
- method:"delete"
- })
具体的删除操作:
- //删除某一个产品的操作
- async deleteCartById(cart){
- try{
- await this.$store.dispatch('deleteCartListBySkuId',cart.skuId)
- //如果删除成功,再次发请求获取新的数据展示
- this.getData()
- }catch(error){
- alert(error.message)
- }
- }
接口:
- //修改商品的选中状态
- export const reqUpdateCheckedById=(skuId,isChecked)=>requests({
- url:`/cart/checkCart/${skuId}/${isChecked}`,
- method:"get"
- })
仓库:
- //修改购物车某一个产品的选中状态
- async updateCheckedById({commit},{skuId,isChecked}){
- let result = await reqUpdateCheckedById(skuId,isChecked)
- if(result.code===200){
- return 'ok'
- }else{
- return Promise.reject(new Error('fail'))
- }
- }
修改template模板内容:
- <input
- type="checkbox"
- name="chk_list"
- :checked="cart.isChecked==1"
- @change="updateChecked(cart,$event)"
- >
修改script脚本内容:
- async updateChecked(cart,event){
- try{
- let checked=event.target.checked?"1":"0"
- await this.$store.dispatch('updateCheckedById',{skuId:cart.skuId,isChecked:checked})
- this.getData()
- }catch(error){
-
- }
- }
1.加入购物车
UUID:点击加入购物车的时候,通过请求头给服务器带临时身份给服务器,存储某一个用户购物车数据
会话存储:去存储产品的信息以及展示功能
2.购物车功能
修改产品的数量
删除某一个产品的接口
某一个产品的勾选状态切换
context:小仓库,包含commit(提交mutations修改state)、getters(计算属性)、dispatch(派发action)、state(当前仓库数据)
- //删除全部勾选的产品
- deleteAllCheckedCart(context) {
-
- }
Promise.all([p1,p2,p3])
p1|p2|p3:每一个都是Promise对象,如果有一个Promise失败,都失败;如果都成功,返回成功
删除全部选中的商品
action逻辑:
- //删除全部勾选的产品
- deleteAllCheckedCart({dispatch,getters}) {
- let promiseAll=[]
- //获取购物车中的全部产品
- getters.cartList.cartInfoList.forEach(item=>{
- let promise=item.isChecked===1?dispatch("deleteCartListBySkuId",item.skuId):''
- promiseAll.push(promise)
- })
- return Promise.all(promiseAll)
- }
组件里的逻辑:
-
- async deleteAllCheckedCart(){
- try{
- await this.$store.dispatch("deleteAllCheckedCart")
- this.getData()
- }catch(error){
- alert(error.message)
- }
- }
actions里的逻辑:
- updateAllCartIsChecked({dispatch,state},isChecked){
- let promiseAll=[]
- state.cartList[0].cartInfoList.forEach(item=>{
- let promise=dispatch("updateCheckedById",{
- skuId:item.skuId,
- isChecked:isChecked
- })
- promiseAll.push(promise)
- return Promise.all(promiseAll)
- })
- }
组件里的逻辑:
- //修改全部产品选中的状态
- async updateAllCartChecked(event){
- try{
- let isChecked=event.target.checked?"1":"0"
- await this.$store.dispatch("updateAllCartIsChecked",isChecked)
- this.getData()
- }catch(error){
- alert(error.message)
- }
- }
组件的input全选框判断:
- <div class="select-all">
- <input class="chooseAll"
- type="checkbox"
- :checked="isAllChecked&&cartInfoList.length>0"
- @change="updateAllCartChecked($event)"
- >
- <span>全选</span>
- </div>
assets文件夹打包以后,整个项目在dist目录下,assets文件夹会消失
assets文件夹——放置全部组件共用的静态资源
在样式中也可以使用@符号:
url中使用@代表src路径:
background-image: url(~@/assets/images/icons.png);
接口api:
- //获取验证码
- export const reqGetCode=(phone)=>requests({
- url:`/user/passport/sendCode/${phone}`,
- method:"get"
- })
仓库:
- import {reqGetCode} from "@/api";
-
- const state={
- code:''
- }
- const mutations={
- GETCODE(state,code){
- state.code=code
- }
- }
- const actions={
- //获取验证码
- async getCode({commit},phone){
- let result=await reqGetCode(phone)
- if(result.code===200){
- commit("GETCODE",result.data)
- }else{
- return Promise.reject(new Error('fail'))
- }
- }
- }
- const getters={}
-
- export default {
- state,mutations,actions,getters
- }
发送请求到仓库:
<button style="width:100px;height:38px;" @click="$store.dispatch('getCode',phone)">获取验证码</button>
或:
- async getCode(){
- try{
- this.phone&&(await this.$store.dispatch("getCode",this.phone))
- console.log(this.$store.state.user.code)
- }catch(error){
- alert(error.message)
- }
- }
注册成功后跳转到登录页面:
接口api:
- //注册
- export const reqUserRegister=(data)=>requests({
- url:`/user/passport/register`,
- data,
- method:"post"
- })
仓库里的actions逻辑:
- //用户注册
- async userRegister({commit},user){
- let result=await reqUserRegister(user)
- if(result.code===200){
- return 'ok'
- }else{
- return Promise.reject(new Error('fail'))
- }
- }
组件里的逻辑:
- async userRegister(){
- try{
- const {phone,code,password,rePassword}=this
- phone&&code&&password===rePassword&&(await this.$store.dispatch("userRegister",{phone,code,password}))
- this.$router.push("/login")
- }catch(error) {
- alert(error.message)
- }
- }
登录业务:
注册:通过数据库存储用户信息(名字、密码)
登录:登录成功的时候,后台为了区分你这个用户是谁,会让服务器下发token(令牌:唯一标识符)
登陆接口:一般登陆成功后服务器会下发token,前台持久化存储token,然后前台带着token 去找服务器登录
接口api:
- //登录
- export const reqUserLogin=(data)=>requests({
- url:"/user/passport/login",
- data,
- method:"post"
- })
组件逻辑:
- async userLogin(){
- try{
- const {phone,password}=this
- (phone&&password)&&(await this.$store.dispatch("userLogin",{phone,password}))
- this.$router.push("/home")
- }catch(error){
- alert(error.message)
- }
- }
仓库逻辑:
actions:
- //登录业务
- async userLogin({commit},data){
- let result=await reqUserLogin(data)
- if(result.code===200){
- commit("USERLOGIN",result.data.token)
- return 'ok'
- }else{
- return Promise.reject(new Error('fail'))
- }
- }
mutations:
- USERLOGIN(state,token){
- state.token=token
- }
前台携带token获取用户信息:
export const reqUserInfo=()=>requests.get("/user/passport/auth/getUserInfo")
处理请求拦截器:
- //请求拦截器
- requests.interceptors.request.use((config)=>{
- if(store.state.detail.uuid_token){
- config.headers.userTempId=store.state.detail.uuid_token
- }
- if(store.state.user.token){
- config.headers.token=store.state.user.token
- }
-
- //config是headers的请求头
- nprogress.start()
- return config
- })
获取服务器返回的用户信息:
- async getUserInfo({commit}){
- let result=await reqUserInfo()
- if(result.code===200){
- commit("GETUSERINFO",result.data)
- return 'ok'
- }else{
- return Promise.reject(new Error('fail'))
- }
- }
- GETUSERINFO(state,userInfo){
- state.userInfo=userInfo
- }
处理Header组件:
- <div class="loginList">
- <p>尚品汇欢迎您!</p>
- <p v-if="!userName">
- <span>请</span>
- <router-link to="/login">登录</router-link>
- <router-link to="/register" class="register">免费注册</router-link>
- </p>
- <p v-else>
- <a>{{userName}}</a>
- <a class="register">退出登录</a>
- </p>
- </div>
vuex仓库存储的数据并不持久化,一刷新数据就没了。可以通过本地存储持久化token:
- //登录业务
- async userLogin({commit},data){
- let result=await reqUserLogin(data)
- if(result.code===200){
- commit("USERLOGIN",result.data.token)
- localStorage.setItem("TOKEN",result.data.token)
- return 'ok'
- }else{
- return Promise.reject(new Error('fail'))
- }
- }
- const state={
- code:'',
- token:localStorage.getItem("TOKEN"),
- userInfo:{}
- }
目前存在的bug:
①多个组件展示用户信息需要在每一个组件的mounted中触发this.$store.dispatch("getUserInfo")
②用户已经登陆,就不能再跳转到登录页
退出登录需要做的事:
①需要发送请求,通知服务器退出登录(清除一些数据,比如token)
②清除项目当中的数据
接口api:
export const reqLogout=()=>requests.get("/user/passport/logout")
清除记录的数据:
- async userLogout({commit}){
- let result=await reqLogout()
- if(result.code===200){
- commit("CLEAR")
- return "ok"
- }else{
- return Promise.reject(new Error("fail"))
- }
- }
- CLEAR(state){
- state.token=""
- state.userInfo={}
- localStorage.clear()
- }
派发action:
- async logout(){
- try{
- await this.$store.dispatch("userLogout")
- this.$router.push("/home")
- }catch(error){
- alert(error.message)
- }
- }
导航:表示路由正在发生变化,进行路由跳转
导航守卫可以简单地分为全局守卫、路由独享守卫、组件内守卫
全局前置守卫:
当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫resolve完之前一直处于等待中
参数剖析:
to:可以获取到你要跳转到的那个路由的信息
from:可以获取到你从哪个路由而来的信息
next:放行函数
next()放行
next("/login")放行到指定的路由中
next(false)中断当前的导航。如果浏览器的URL改变了(可能是用户手动或者浏览器后退按钮),那么URL地址会重置到from路由对应的地址
- router.beforeEach((to, from, next)=>{
-
- })
全局路由守卫逻辑:
1.如果已经登录
i.想去的是login路由,跳转到主页
ii.想去的不是login路由
①如果有用户名,直接放行(不能用userInfo判断,因为空对象为真值)
②如果没有用户名,可能是刷新导致仓库的数据消失了,再去捞一下数据,然后放行
如果捞不到数据,就直接退出登录,回到登录界面
2.如果没有登陆
直接放行(此时还没有做处理,后续可能会有追加)
- router.beforeEach(async (to, from, next)=>{
- let token=store.state.user.token
- let name=store.state.user.userInfo.name
- if(token) {
- //登陆了订单还想去login或register(不能去,停留在首页)
- if (to.path === '/login'||to.path === '/register') {
- next("/home")
- } else {
- //登陆了,去的不是login
- //如果用户信息
- if (name) {
- next()
- } else {
- try {
- //没有用户信息,派发action让仓库存储用户信息再跳转
- await store.dispatch("getUserInfo")
- next()
- } catch (error) {
- //token失效,获取不到用户信息,需要重新登陆
- //清除token
- await store.dispatch("userLogout")
- next("/login")
- }
- }
- }
- }else{
- next()
- }
- })
api接口:
- //获取用户地址信息
- export const reqAddressInfo=()=>requests({
- url:"/user/userAddress/auth/findUserAddressList",
- method:"GET"
- })
- //获取商品清单
- export const reqOrderInfo=()=>requests({
- url:"/order/auth/trade",
- method:"GET"
- })
仓库:
- import {reqAddressInfo, reqOrderInfo} from "@/api";
-
- const state={
- address:[],
- orderInfo:{}
- }
- const mutations={
- GETUSERADDRESS(state,address){
- state.address=address
- },
- GETORDERINFO(state,orderInfo){
- state.orderInfo=orderInfo
- }
- }
- const actions={
- async getUserAddress({commit}){
- let result = await reqAddressInfo()
- if(result.code===200){
- commit("GETUSERADDRESS",result.data)
- }
- },
- async getOrderInfo({commit}){
- let result = await reqOrderInfo()
- if(result.code===200){
- commit("GETORDERINFO",result.data)
- }
- }
- }
- const getters={}
-
- export default {
- state,mutations,actions,getters
- }
组件逻辑:
注意mapState和mapGetters的区别
- <script>
- import {mapState} from "vuex";
-
- export default {
- name: 'Trade',
- data(){
- return {
- msg:""
- }
- },
- mounted(){
- this.$store.dispatch("getUserAddress"),
- this.$store.dispatch("getOrderInfo")
- },
- computed:{
- ...mapState({
- addressInfo:state=>state.trade.address,
- orderInfo:state=>state.trade.orderInfo
- }),
- //将来提交订单最终选中地址
- userDefaultAddress(){
- return this.addressInfo.find(item=>item.isDefault==1)||{}
- },
- detailArrayList(){
- return this.orderInfo.detailArrayList||[]
- }
- },
- methods:{
- changeDefault(address,addressInfo){
- //find:查找数组当中符合条件的元素作为返回值返回
- addressInfo.forEach(item=>item.isDefault=0)
- address.isDefault=1
- }
- }
- }
- </script>
①静态组件
②点击提交订单的按钮时,还需要向服务器发送一次请求,把一些支付的相关信息传递给服务器
接口:
- export const reqSubmitOrder=(tradeNo,data)=>requests({
- url:`/order/auth/submitOrder?tradeNo=${tradeNo}`,
- data,
- method:"post"
- })
统一接口api文件夹里面全部的请求函数:
- import * as API from "@/api"
-
-
- new Vue({
- render: h => h(App),
- beforeCreate(){
- Vue.prototype.$bus=this
- Vue.prototype.$API=API
- },
- router,
- store,
- }).$mount('#app')
发送请求到支付页面:
- async submitOrder(){
- let {tradeNo}=this;
- let data={
- consignee:this.userDefaultAddress.consignee,
- consigneeTel:this.userDefaultAddress.phoneNum,
- deliveryAddress:this.userDefaultAddress.fullAddress,
- paymentWay:"ONLINE",
- orderComment:this.msg,
- orderDetailList:this.detailArrayList
- }
- let result=await this.$API.reqSubmitOrder(tradeNo,data)
- console.log(result)
- if(result.code===200){
- this.orderId=result.data
- this.$router.push(`/pay?orderId=${this.orderId}`)
- }else{
- alert(result.data)
- }
- }
接口:
- //获取支付的信息
- export const reqPayInfo=(orderId)=>requests({
- url:`/payment/weixin/createNative/${orderId}`,
- method:"get"
- })
生命周期函数中不能使用async
- <script>
- export default {
- name: 'Pay',
- data(){
- return {
- payInfo:{}
- }
- },
- computed:{
- orderId(){
- return this.$route.query.orderId
- }
- },
- mounted(){
- this.getPayInfo()
- },
- methods:{
- async getPayInfo(){
- let result=await this.$API.reqPayInfo(this.orderId)
- if(result.code===200){
- this.payInfo=result.data
- }
- }
- }
- }
- </script>
- //注册全局组件
- import {Button,MessageBox} from 'element-ui'
- //ElementUI注册组件的时候还有一种写法:挂在原型上
- Vue.use(Button)
- Vue.prototype.$msgbox = MessageBox;
- Vue.prototype.$alert = MessageBox.alert;
ElementUI按需引入:
借助 babel-plugin-component,我们可以只引入需要的组件,以达到减小项目体积的目的
①安装 babel-plugin-component
yarn add babel-plugin-component -D
②将babel.config.js修改为:
- module.exports = {
- presets: [
- '@vue/cli-plugin-babel/preset',
- ],
- "plugins":[
- [
- "component",
- {
- libraryName:'element-ui',
- "StyleLibraryName":'theme-chalk'
- }
- ]
- ]
-
- }
直接使用:
<el-button class="btn" @click="open">立即支付</el-button>
- //弹出框
- open(){
- this.$alert(`<strong>这是 <i>HTML</i> 片段</strong>`,'HTML片段',{
- dangerouslyUseHTMLString:true,
- //中间布局
- center:true,
- //是否显示取消按钮
- showCancelButton:true,
- //取消按钮的文本内容
- cancelButtonText:"支付遇见问题",
- //确定按钮的文本内容
- confirmButtonText:"已经支付成功",
- //右上角的叉叉是否显示
- showClose:false
- })
- }
使用qrcode,一个用于生成二维码的JavaScript库
获取支付订单状态的接口:
- //获取支付订单状态
- export const reqPayStatus=(orderId)=>requests({
- url:`/payment/weixin/queryPayStatus/${orderId}`,
- method:"get"
- })
弹出框业务:
- //弹出框
- async open(){
- //生成二维码地址
- let url=await QRCode.toDataURL(this.payInfo.codeUrl)
- this.$alert(`<img src="${url}"/>`,'HTML片段',{
- dangerouslyUseHTMLString:true,
- //中间布局
- center:true,
- //是否显示取消按钮
- showCancelButton:true,
- //取消按钮的文本内容
- cancelButtonText:"支付遇见问题",
- //确定按钮的文本内容
- confirmButtonText:"已经支付成功",
- //右上角的叉叉是否显示
- showClose:false,
- //关闭弹出框的配置值
- beforeClose:(type,instance,done)=>{
- //type:区分取消|确定按钮
- //instance:当前组件实例
- //done:关闭弹出框的方法
- if(type==='cancel'){
- alert('请联系管理员')
- clearInterval(this.timer)
- this.timer=null
- //关闭弹出框
- done()
- }else{
- //判断是否真的支付了
- if(this.code===200){
- clearInterval(this.timer)
- this.timer=null
- done()
- this.$router.push('/paysuccess')
- }
- }
- }
- })
- if(!this.timer){
- this.timer=setInterval(async ()=>{
- //发请求获取用户支付状态
- let result=await this.$API.reqPayStatus(this.orderId)
- //如果code===200
- if(result.code===200){
- //第一步:清除定时器
- clearInterval(this.timer)
- this.timer=null
- //保存支付成功返回的code
- this.code=result.code
- //关闭弹出窗
- this.$msgbox.close()
- //跳转到下一路由
- this.$router.push('/paysuccess')
- }
- },1000)
- }
- }
如果只进入/center,可能会出现右侧没有内容的情况,所以需要重定向到/center/myorder
- {
- path:'/center',
- component:Center,
- meta:{isShow:true},
- children:[
- {
- path:'myorder',
- component:MyOrder
- },
- {
- path:'grouporder',
- component:GroupOrder
- },
- {
- path:'/center',
- redirect:"/center/myorder"
- }
- ]
- }
接口api:
- //获取个人中心的数据
- export const reqMyOrderList=(page,limit)=>requests({
- url:`/order/auth/${page}/${limit}`,
- method:"get"
- })
全局前置守卫逻辑处理:
- //未登录:不能去交易相关、支付相关的页面(pay|paysuccess)、不能去个人中心
- //未登录若去上述页面---跳转至登录页面
- let toPath=to.path
- if(toPath.indexOf('/trade')!==-1||toPath.indexOf('/pay')!==-1||toPath.indexOf('/center')!==-1){
- next('/login?redirect='+toPath)
- }else {
- //去的不是上面这些路由(home|search|shopCart)---放行
- next()
- }
对login的处理:增加了对query参数的判断
- async userLogin(){
- try{
- const {phone,password}=this;
- (phone&&password)&&(await this.$store.dispatch("userLogin",{phone,password}))
- let toPath=this.$route.query.redirect||'/home'
- this.$router.push(toPath)
- }catch(error){
- alert(error.message)
- }
- }
路由独享守卫:
只有从购物车界面才能跳转到交易页面(创建订单)
- {
- path:"/trade",
- component: Trade,
- meta:{isShow:true},
- //路由独享守卫
- beforeEnter:(to,from,next)=>{
- if(from.path==='/shopcart'){
- next()
- }else{
- next(false)
- }
- }
- }
只有从交易页面(创建订单)页面才能跳转到支付页面
- {
- path:"/pay",
- component:Pay,
- meta:{isShow:true},
- beforeEnter:(to,from,next)=>{
- if(from.path==='/trade'){
- next()
- }else{
- next(false)
- }
- }
- }
只有从支付页面才能跳转到支付成功页面
组件内守卫:
beforeRouterEnter(to,from,next){ }:
在渲染该组件的对应路由被confirm前调用
不能获取组件的实例“this”
因为当守卫执行前,组件实例还没有被创建
- export default {
- name: 'PaySuccess',
- beforeRouteEnter(to,next,from){
- if(from.path==='/pay'){
- next()
- }else{
- next(false)
- }
- }
- }
beforeRouteUpdate(to,from,next){ }:
在当前路由改变、但是该组件被复用时调用
举例来说,对于一个带有动态参数的路径/foo/:id,在/foo/1和/foo/2之间跳转的时候
由于会渲染同样的Foo组件,因此组件实例会被复用,而这个钩子就是这种情况下被调用
可以访问组件实例“this”
beforeRouteLeave(to,from,next){ }:
导航离开该组件的对应路由时调用
可以访问组件实例的“this”
①下载插件vue-lazyload
注意vue2对应1.3.3版本的vue-lazyload,否则会报路径错误
yarn add vue-lazyload@1.3.3
②图片、json不需要对外暴露就可引入
- import VueLazyLoad from 'vue-lazyload'
- import winter from '@/assets/winter.jpg'
- Vue.use(VueLazyLoad,{
- //懒加载默认图片
- loading:winter
- })
在需要默认图片的模板中插入:
<img v-lazy="good.defaultImg" />
①安装:注意vue2对应vee-validate2版本
yarn add vee-validate@2
②在main.js中引入表单校验插件
- //引入表单校验插件
- import '@/plugins/validate'
③编写表单校验插件内容
- import Vue from 'vue'
- import VeeValidate from 'vee-validate'
- import zh_CN from 'vee-validate/dist/locale/zh_CN'
- Vue.use(VeeValidate)
-
- //表单验证
- VeeValidate.Validator.localize("zh_CN",{
- messages:{
- ...zh_CN.messages,
- is:(field)=>`${field}必须与密码相同`,
- },
- attributes:{
- phone:"手机号",
- code:"验证码",
- password:"密码",
- rePassword:"确认密码",
- agree:"协议"
- }
- })
④改写模板里的代码(以手机号校验为例)
- <div class="content">
- <label>手机号:</label>
- <input type="text"
- placeholder="请输入你的手机号"
- v-model="phone"
- name="phone" v-validate="{required:true,regex:/^1\d{10}$/}" :class="{invalid:errors.has('phone')}"
- >
- <span class="error-msg">{{errors.first("phone")}}</span>
- </div>
⑤效果图
不输入时:
输入错误时:
格式正确时警告会消失:
验证码校验:
- <div class="content">
- <label>验证码:</label>
- <input type="text"
- placeholder="请输入验证码"
- v-model="code"
- name="code" v-validate="{required:true,regex:/^\d{6}$/}" :class="{invalid:errors.has('code')}"
- >
- <button style="width:100px;height:38px;" @click="getCode">获取验证码</button>
- <span class="error-msg">{{errors.first("code")}}</span>
- </div>
密码校验:
- <div class="content">
- <label>登录密码:</label>
- <input type="password"
- placeholder="请输入你的登录密码"
- v-model="password"
- name="password" v-validate="{required:true,regex:/^[0-9a-zA-Z]{8,20}$/}" :class="{invalid:errors.has('password')}"
-
- >
- <span class="error-msg">{{errors.first("password")}}</span>
- </div>
确认密码校验:
- <div class="content">
- <label>确认密码:</label>
- <input type="password"
- placeholder="请输入确认密码"
- v-model="rePassword"
- name="rePassword" v-validate="{required:true,is:password}" :class="{invalid:errors.has('rePassword')}"
-
- >
- <span class="error-msg">{{errors.first("rePassword")}}</span>
- </div>
效果图:
对于勾选同意协议的复选框,必须自定义规则
- //自定义校验规则
- VeeValidate.Validator.extend("agree",{
- validate:value=>{
- return value
- },
- getMessage:field=>field+"必须同意"
- })
修改模板里的内容:
- <div class="controls">
- <input
- type="checkbox"
- :checked="agree"
- name="agree" v-validate="{required:true,'agree':true}" :class="{invalid:errors.has('agree')}"
- >
- <span>同意协议并注册《尚品汇用户协议》</span>
- <span class="error-msg">{{errors.first("agree")}}</span>
- </div>
如果所有的表单验证都通过,再向服务器发送请求进行注册
- async userRegister(){
- const success=await this.$validator.validateAll()
- if(success){
- try{
- const {phone,code,password,rePassword}=this
- await this.$store.dispatch("userRegister",{phone,code,password})
- this.$router.push("/login")
- }catch(error) {
- alert(error.message)
- }
- }
- }
当打包构建应用时,JavaScript包会变的非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,就会更加高效
以Search组件为例:
- {
- name:'search',
- path:"/search/:keyword?",
- component:()=>import("@/pages/Search.vue"),
- meta:{show:true}
- }
打包:yarn build
项目打包后,代码都是经过压缩加密的,如果运行时报错,输出的错误信息无法准确得知是哪里的代码报错。有了map就可以像未加密的代码一样,准确的输出是哪一行哪一列又错了
文件如果项目不需要可以去掉
Vue.config.js配置中productionSourceMap:false可以不生成体积很大的map文件
productionSourceMap:false
可以去阿里云或者腾讯云购买服务器,腾讯云更便宜
完结撒花❀❀~
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。