主要为自己项目过程做记录
内容较口语化,知识不够专业化
有错误之处,可批评指正!感谢
node + webpack + 淘宝镜像
node_mudules:项目依赖文件夹
public:放置静态资源
src(程序员源代码文件夹):
assets:放置静态资源
components:放置非路由组件(全局组件)
App.vue:唯一的根组件
main.js:程序入口文件,整个程序中最先执行的文件
babel.config.js:配置文件
package.json:类似项目身份证
package-lock.json:缓存性文件
1.cmd中npm run serve时可以自动打开页面
在package.json文件中
"scripts": {
"serve": "vue-cli-service serve --open --host=localhost",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
2.关闭语法校验
在vue.config.js里
module.exports = defineConfig({
transpileDependencies: true,
lintOnSave:false//关闭语法校验
})
3.src文件夹简写方法,给src配置别名
因为项目大的时候src(源代码文件夹):里面目录会很多,找文件不方便,设置src文件夹的别名的好处,找文件会方便一些
创建jsconfig.json文件
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": [
"src/*"
]
}
},
"exclude": [//表示@这个别名在以下两个文件夹中不可使用
"node_modules",
"dist"
]
}
即一组映射关系(key-value)
key:url(地址栏中的路径)
value:相应的路由组件
在该项目中:
路由组件:
Home首页路由组件、Search路由组件、Login路由组件、Register注册路由组件
非路由组件:
Header(首页、搜索页)、Footer(首页、搜素页)
创建组件时,准备好组件结构+样式+资源
非路由组件放在components文件夹里
1.创建或定义组件
2.引入
3.注册
4.使用
1.html:在资料里将home.html文件中将Header的html结构复制到我们自己的项目文件的Header.vue的template里
2.CSS:再将css中的home.less的Header部分复制到Header.vue的style
此时会出现问题,浏览器不识别less样式,需通过less、less-loader进行处理,将less样式变为css样式
安装
cnpm install --save less less-loader@5
安装成功后
再在style标签里加上lang=“less”,即可识别less
3.images:相应的图片也要复制过来
记得script里name:‘Header’
App里引入组件
import Header from './components/Header'
components:{'Header'}
写组件标签
<Header></Header>
Footer部分同上
但此时由于一些默认样式的存在,使得样式出现问题
复制reset.css文件到public文件夹下,记得在index.html文件中引入,清除整体的默认样式
1.样式没显示
原因:import引入的是Header文件夹
并且components 是复数!后面是{}
路由组件放在pages或views文件夹里
搭建路由组件必须使用vue-router插件
安装:
cnpm install --save vue-router@3
//此处安装的是可以支持vue2的vue-router的3版本
配置路由放在router文件夹内
步骤1-4都在router/index.js内完成
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
import Home from '@/pages/Home'
import Login from '@/pages/Login'
import Register from '@/pages/Register'
import Search from '@/pages/Search'
export default new VueRouter({
routes:[
{
path:"/home",
component:Home
},
{
path:"/search",
component:Search
},
{
path:"/login",
component:Login
},
{
path:"/register",
component:Register
},
//重定向,使访问时直接定向到首页
{
path:'*',
redirect:"/home"
}
]
})
在main.js
//引入路由
import router from '@/router'
//注册路由
new Vue({
render: h => h(App),
router,//完整写法为router:router,但因kv一致,可以简写
}).$mount('#app')
此处写router,使得每一个组件都拥有了 r o u t e 和 route和 route和router属性
$route:一般获取路由信息,路径、query、params等
$router:一般进行编程式路由导航、进行路由跳转,push、replace等
在根组件App.vue内
<template>
<div>
<Header></Header>
<router-view></router-view>//路由出口
<Footer></Footer>
</div>
</template>
我们都知道,路由指的是组件和路径的一种映射关系。Router-view也被称为路由的出口,
也就是:
路径--------------------------------------------------------------->页面
可以把router-view理解成一类代码存放的位置。
在App.vue加个路由组件存放的位置即可。
因为App.vue是根组件,最开始的页面就显示在这里。
总结:
1.router-view是路由的出口,没有它页面则没法进行显示。
2.二级路由的出口对应在一级路由里面进行配置。
有两种形式:
我认为它适合作用在链接身上
一定要有to属性
适合一些点击按钮后跳转,可以包含一定业务逻辑
输出一下push,会发现返回值为promise
因为vue-router引入了promise,而promise有成功和失败的回调,
解决方法,在push里写两个()=>{},传入相应成功和失败的回调,但是治标不治本
this.$router.push({
name:'search',
params:{
keyWord:this.keyWord
},
query:{
k:this.keyWord.toUpperCase()
}
},()=>{},()=>{})
从this.$router.push来深究一下
this:当前组件vc实例
this. r o u t e r :是这个 v c 身上的一个属性,是 V u e R o u t e r 类的一个实例,来自 m a i n . j s 里注册路由时使每一个组件都有了 router:是这个vc身上的一个属性,是VueRouter类的一个实例,来自main.js里注册路由时使每一个组件都有了 router:是这个vc身上的一个属性,是VueRouter类的一个实例,来自main.js里注册路由时使每一个组件都有了router$route属性
push:VueRouter原型对象里的一个方法
最终要治本,我得改vuerouter有关push的代码
重写replace同上,就复制一下改push为replace
1.相同点:都可以调用函数一次,都可以篡改函数上下文一次(上下文就是指this的作用域)
不同点:call传参逗号隔开,apply传数组
1.文件夹不同:路由组件->pages/views
非路由组件->components
2.使用方法不同:路由组件->要在router文件夹中注册
以形式呈现
不写组件标签,无需在根组件中引入注册
非路由组件->以组件标签形式使用
要引入要注册
写组件标签以使用
3.相同点:注册路由后,组件都有 r o u t e 和 route和 route和router属性
footer在登录和注册时隐藏
根据$router.path获取当前路由路径信息
<Footer v-show="$route.path=='/home' || $route.path=='/search'"></Footer>
v-if:通过操纵dom元素来切换显示状态,会频繁操作DOM,表达式的值为true,元素存在于dom树中,为false,从dom树中移除
v-show:只是样式的显示与隐藏,是display:block/none
有时,你可能希望将任意信息附加到路由上,如过渡名称、谁可以访问路由等。这些事情可以通过接收属性对象的meta
属性来实现,并且它可以在路由地址和导航守卫上都被访问到。
我们无法随意向配置对象上加属性,配置对象身上的属性是固定的,但专门留了个空的meta来让程序员可进行一些自定义操作,所以信息可以向meta里加
配合v-show进行判断,即可实现根据路由路径信息来判断是否显示Footer
<Footer v-show="$route.meta.showFooter"></Footer>
在路由配置时一定要在path里写占位符,且一定要给路由命名!
且若为对象形式,路径只能写name不能写path
答:不可以。
路由跳转传递参数的时候, 对象的写法可以是 name、path形式。
但是需要注意的是, path这种写法不能与 params参数一起使用,会导致对应页面获取不到params参数。
前提:
若我已经在路由配置的时候给路径写了占位符,相当于路由要求传递params参数,但我用了name且只用了query,这就会导致URL出现问题,即从http://localhost:8080/#/search/?k=DOWN,直接变成http://localhost:8080/#/?k=DOWN
解决方法:在路由配置的时候给路径写了占位符,再加个问号,如此便可使params参数可有可没有,路径不会错
path:"/search/:keyWord?",
即使有name,即使加了问号,即使query和params都有,但我一旦params传的是空字符串,则路径会再次出现上一题中的问题
解决方法:
params: {keyWord: '' || undefined},
虽然对应页面获取不到params参数了,但至少路径没出错
反正就是,在编程式导航里面,用了name,那就不能让它空着,用query不行,即使有params但空字符串也不行
因为 params参数属于路径地址当中的一部分, 在配置路由的时候已经占位准备接收了。但是没有接收到所以路径就出现问题了。
答:可以,有三种方式。
配置时额外传一些数据,接收方要props接收
针对params参数,发送方就正常用params参数传数据,接收方要props接收,插值语法里直接写id,可以省得$route.params.id
query或者params都行,发送方就正常用参数传数据,路由这边中转一下,接收方要props接收,插值语法里直接写id
1.传params参数时忘记带斜杠
三级联动多次使用,所以注册为全局组件
先把样式结构图片都复制过来
1.在main.js中注册全局组件
import TypeNav from '@/components/TypeNav'
vue.component(TypeNav.name,TypeNav)
2.使用
因为是全局组件,所以无需再引入
耐心
图片里 style里有 template里也有
且不要将images变成多级目录,不论原本资料里的怎么写图片路径,我们都只创建一个images,与改组件相关的图片都放里面
先把样式结构图片都复制过来
再引用使用
1.图片路径出错
注意路径!
why二次封装?
为了使用请求拦截器和响应拦截器
安装axios
cnpm install --save axios
在src下新建目录api 新建文件request.js
//对axios进行二次封装
import axios from 'axios'
//request即为axios,只不过稍微配置一下
//利用axios对象的方法create,去创建一个axios实例
const requests = axios.create({
//配置对象
baseURL:'/api',//基础路径,发请求的时候让路径自带api
timeout:5000,//设置超时时间
});
//请求拦截器:监测发送请求
requests.interceptors.request.use((config)=>{//config:配置对象,其中header请求头属性很重要
return config;
})
//响应拦截器
requests.interceptors.response.use((res)=>{
return res.data;
},(error)=>{
return Promise.reject(new Error('fail'));
});
//对外暴露
export default requests;
在api文件下新建文件index.js
(以三级联动接口为例)
本地浏览器向服务器发送请求,出现问题,因为跨域了
常用解决方法:JSONP、CORS、代理
选择代理方法
从webpack官网文档中截取以下代码,加入vue.config.js
注意,服务器之间没有跨域问题,浏览器之间才有
//代理跨域
devServer: {
proxy: {
'/api': {
target: 'http://gmall-h5-api.atguigu.cn',
//一旦发请求时路径中带有/api,则代理服务器会转发请求,向真实服务器要数据
},
},
},
安装
cnpm install --save nprogress
在request.js中使用
引入进度条
import nprogress from 'nprogress'
引入进度条样式
import "nprogress/nprogress.css" ;//想改样式 进入该css文件即可
在请求拦截器return之前加入进度条开始
nprogress.start();
在响应拦截器return之前加入进度条结束
nprogress.done();
vuex是官方提供的一个插件,是一个状态管理库,集中式管理项目中组件公用的数据
cnpm install --save vuex@3 //下载vue2对应的vuex版本
在src新建store文件夹 index.js(总仓库)
此处模块化开发
//该文件用于创建Vuex中最为核心的store
import Vue from 'vue'
//引入Vuex
import Vuex from 'vuex'
//应用Vuex插件
Vue.use(Vuex)
import home from './home'
import search from './search'
//创建并暴露store
export default new Vuex.Store({
modules:{
home,
search
}
})
举个例子 为home模块的小仓库
export default {
namespaced:true,
actions:{},
mutations:{},
state:{},
getters:{}
}
在main.js中
import store from './store'
new Vue({
render: h => h(App),
//注册路由
router,//完整写法为router:router,但因kv一致,可以简写
//注册仓库
store,//组件实例会增加一个$store属性
}).$mount('#app')
1.vuex版本下载太新了,不适配vue2
2.modules 是复数
挂载完毕就请求数据,放在根组件里,这样防止重复请求数据
mounted() {
//通知Vuex发送请求,获取数据,存储于仓库之中
this.$store.dispatch('home/categoryList');
},
在对应组件仓库中,即store/home/index.js
引入三级联动接口
import {reqCategoryList} from '@/api'
通过actions向接口请求数据
actions:{
async categoryList({commit}){
let result =await reqCategoryList();
if(result.code===200){
commit('CATEGORYLIST',result.data)
}
}
},
如果不加async和await,获取到的就是一个promise对象,写了则可直接获得data
改变数据
mutations:{
CATEGORYLIST(state,categoryList){
state.categoryList=categoryList
}
},
存储数据
state:{
categoryList:[]
},
传递数据给三级联动部分 回到TypeNav/index.js
引入mapstate
import {mapState} from 'vuex'
映射为组件实例的一个属性
computed:{
// ...mapState('home',['categoryList'])//数组写法
...mapState({//对象写法
categoryList:(state)=>{
return state.home.categoryList//home是仓库名,categoryList是state里的数据
}
})
}
分结构,利用v-for层层循环嵌套
<div class="sort">
<div class="all-sort-list2">
<div class="item" v-for="c1 in categoryList" :key="c1.categoryid">//一级
<h3>
<a href="">{{c1.categoryName}}</a>
</h3>
<div class="item-list clearfix">
<div class="subitem" v-for="c2 in c1.categoryChild" :key="c2.categoryid">//二级
<dl class="fore">
<dt>
<a href="">{{c2.categoryName}}</a>
</dt>
<dd>
<em v-for="c3 in c2.categoryChild" :key="c3.categoryid">//三级
<a href="">{{c3.categoryName}}</a>
</em>
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
1.开了命名空间使用了模块化一定注意路径问题!!!!!
手写方式:仓库命名/函数名
this.$store.dispatch('home/categoryList');
css改也行
此处使用js来完成样式改变
在data中存储一个currentindex属性,值为-1,
给一级标题绑定鼠标进事件changeindex(index)
在methods的changeindex方法中使得this.currentindex=index
动态绑定类名:class={cur:currentindex===index}
注意 添加鼠标离开事件
改变三级分类位置 使得其和全部商品分类成为兄弟
另一个则是关于二三级分类显示与隐藏
原本用的是hover
改为js
:style="{display:currentIndex===index?'block':'none'}"
1.动态绑定样式 注意block和none都要加引号!!!!
在规定时间内,不论你触发多少次事件,都不会触发回调,规定时间结束后,再给你挨个触发回调
在规定时间内,触发一次事件,会等待一段时间再执行回调,如果在这个等待时间段内再次触发事件,则会被打断,你又得重新计时
类似:回城,老被人打断,就得再重读时间条
多次快速触发三级联动,会出现问题,用户移动鼠标太快,浏览器反应不过来,利用节流,给你延时一会,让浏览器反应一下来解决
引入lodash(按需引入)
import throttle from 'lodash/throttle'
使用
methods: {
// changeIndex(index){
// this.currentIndex = index;
// },
changeIndex:throttle(function(index){//节流,此处不要用箭头函数
setTimeout(() => {
this.currentIndex = index;
}, 50);
}),
clearIndex(){
setTimeout(() => {//解决bug
this.currentIndex = -1;
}, 55);
}
},
加了节流后,快速移入移出会导致某个一级分类一直是蓝色,原因是移出的代码执行后又执行了一次移入的代码。移出时,currentindex确实为-1了,但是因为移入身上的定时器,致使虽然移出但我的定时器还在,延时时间结束,执行了this.currentIndex = index;的回调,导致某个一级分类一直是蓝色。
解决:给移出的代码加个延时器,时间大于或等于移入的时间
如果直接把a改为router-link,会导致卡顿,因router-link是一个组件,一旦开始循环层层渲染,反复创建router-link组件,而组件又转化为组件实例,此过程很耗内存
如果绑定点击事件,也会导致回调太多
故而利用编程式导航+委派
给父节点委派绑定点击事件,则点击范围太广,无法定位到a标签身上,即我给父节点委派了,但只要子节点里的a标签才能触发,并且,还要区分一级二级三级分类
首先为父节点委派绑定点击任务
为a添加自定义属性,:data-cateName=“c1/2/3.categoryName”(只有a标签有),:data-cate1id=“c1.categoryId”(不一一举例了)(只有相应分类有),配合event.target,实现定位a并分类
在获取目标事件源的情况,一般情况下我们获取目标事件源都是谁是调用者谁就是事件源,但是当有批量的DOM需要操作的时候,要判断哪一个dom是事件源就不是很明确了,使用e.target可以准确地获取事件源,并且在使用的过程中可以比较判断,从而实现我们的代码。
event.target可以获取当前点击元素,返回结果如下
如此便可以锁定a标签并分类
而event.target.dataset,可以获取自定义属性
注意!自定义属性里我们可能data-cateName写成驼峰命名法,而浏览器会自动将自定义属性全部转化为小写!!!
这意味着标签里管他咋写,但在方法里我们得小写!!!!
let {catename,cate1id,cate2id,cate3id} = event.target.dataset
//这里用了解构赋值,花括号里的和dataset里面的属性名称对应上了,就能把属性值赋给它
console.log(catename,cate1id,cate2id,cate3id);
锁定a标签并分类,目的是为点击跳转时,不同级别标题我们能跳转往不同页面并且携带参数,结合编程式导航
goSearch(event){//千万注意!标签里绑定点击事件要传参,且因为传的是event,所以绑定的时候是@click="goSearch($event)"
let {catename,cate1id,cate2id,cate3id} = event.target.dataset
if(catename){//是a标签吗?
let location = {name:'search'}//跳转路径
let query = {catename:catename}//传参,我占位符后面加了问号,可以name和query配合
if(cate1id){//是一级标题吗?
query.cate1id=cate1id
}else if(cate2id){//是二级吗?
query.cate2id=cate2id
}else if(cate3id){//是三级吗?
query.cate3id=cate3id
}
location.query =query;//为location添加一个query属性
this.$router.push(location);
}
}
1.注意!自定义属性里我们可能写成驼峰命名法,而浏览器会自动将自定义属性全部转化为小写!!!
这意味着标签里管他咋写,但在方法里我们得小写!!!!
2.千万注意!标签里绑定点击事件要传参,且因为传的是event,所以绑定的时候是**@click=“goSearch($event)”**
每次push时看看¥route里面有没有query或者params参数,有就塞在location里一起带过去
1.search中 注意是挂载完成后三级分类板块就不再显示,
2.对于鼠标移入移出,注意绑定事件对象,是父亲而不是儿子,否则鼠标放在”全部商品分类“上刚展示一级菜单,鼠标一移开就不显示了,在显示的同时是需要一级菜单保留的,而绑定在父亲身上就不会产生此类问题
安装
cnpm install mockjs
在src创建mock文件夹
准备json数据
将mock需要的图片放在public的images下
创建mockServer.js文件,通过mockjs插件实现模拟数据
import Mock from 'mock.js'
//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})
在入口文件中引入
import '@/mock/mockServer'
配置接口文件
在api文件夹下新建MockAjax.js
文件和二次封装axios的文件一模一样,唯独就是baseURL那里改成 ‘/mock’
在api/index.js
引入配置文件
import mockRequests from './MockAjax'
创建并暴露接口
export const reqgetBannerList =()=> mockRequests({url:'/banner',method:'get'})
在store/home/index.js内
引入reqgetBannerList接口
把连环状态啥的都再写一遍
在app.js中用dispatch发送请求,然后listcontainer里引入mapstate来接收
再循环
跟三级组件差不多
安装
cnpm install --save swiper@5
引入swiper样式(在main里引入)
import swiper from 'swiper'
引入swiper 在相应组件里引入
import Swiper from 'swiper'
创建swiper实例
不能在mounted里写,因为v-for在动态遍历服务器返回的数据,此时页面结构不完整,却先实例化了swiper,而在实例化swiper时之前,页面结构必须完整
update也不行,因为数据更新一次就创建一次
总而言之就是dispatch设计到了异步问题
如上图所示,数据还没修改完就实例化swiper,页面结构不完整,所以无法实现轮播图
根据我在弹幕上看到的,由async和await来解决
async mounted() {
await this.$store.dispatch("home/getBannerList");
var mySwiper = new Swiper(document.querySelector(".swiper-container"), {
//此处可以用ref,获取DOM,写法为:new Swiper(this.$refs.xxxxx(这个xxx看你上面ref:"xxxx"里面咋起的名了),{})
// autoplay: 5000, //可选选项,自动滑动
loop: true, //可选选项,开启循环
pagination: {
el: ".swiper-pagination",
},
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
});
},
或利用watch监听属性+nextTick,监听bannerList数据的变化
watch:
bannerList:{
handler(newValue,oldValue){
var mySwiper = new Swipe......
//这样也会出问题,因为我监听到了数据变化,但没不知道有没有渲染完成,只有v-for执行完毕才有了完整结构
}
}
可以保证页面有结构
mounted() {
this.$store.dispatch("home/getBannerList");
this.$nextTick(()=>{
var mySwiper = new Swiper(document.querySelector(".swiper-container"), {
//autoplay: 5000, //可选选项,自动滑动
loop: true, //可选选项,开启循环
pagination: {
el: ".swiper-pagination",
},
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
});
})
},
引入Mock时时mockjs 没有点
dispatch是又忘了模块化时写法改变了
ref做标记,
组件通信
1.props
2.自定义事件
3.全局事件总线
4.消息订阅与发布
5.插槽
6.vuex
1.在home里floor组件有两个,这意味着这两个floor是要利用v-for来使用的,所以dispatch不是在floor的vue内,而是在home的vue内
2.因为获取数据是在home这个父组件内,需要父子间进行组件通信,此处利用props,:list=“floor”,这个list来传递,故而在子组件里也是props:[‘list’],是list,不是floor,是名,不是内容
3.在home内已经完成了floor的的遍历,当我们使用:list=“floor”,向子组件传递过来了一个floor,所以接收后,对于list已经无需再遍历,懂不!我给你了一个floor,名子叫list,那在子组件里你直接用插值语法{{list.name}}来获取数据就完事了!
4.别瞎用遍历,普通图片直接写就行,就:src="list.recommendList[0]"就行,没必要再遍历
5.此处可以直接在mounted里面new swiper了,因为没有在floor里dispatch,其数据由父组件已经接收了,结构已经搭建完毕,父组件将数据发给子组件,子组件的v-for只会在等到了依赖的数据后才会开始解析渲染