1、vue-cli 脚手架初始化项目
2、node +webpack+淘宝镜像
1、node_modules — 放置项目依赖文件夹
2、public — 一般放置一些共用的静态资源(图片),打包上线的时候,public文件夹里面资源原封不动打包到dist文件夹里面
3、src — 程序员源代码文件夹
1、项目运行,浏览器自动打开
<!-- package.json文件夹 -->
"scripts": {
"serve": "vue-cli-service serve --open",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
2、关闭eslint校验工具
module.exports = {
lintOnSave:false,
}
3、src文件夹配置别名,创建jsconfig.json,用@/代替src/,exclude表示不可以使用该别名的文件
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": [
"src/*"
]
}
},
"exclude": [
"node_modules",
"dist"
]
}
vue-router
前端路由:kv键值对
Key — 即为URL(地址栏中的路径)
Value — 即为相应的路由组件
放置在 router 文件夹中
声明式导航 — < router-link > ,(务必要有to属性)
编程式导航 —< push | replace >
注:声明式导航能做的编程式都能做,而且还可以处理一些业务
参数对应的路由信息要修改为
path: "/search/:id"
这里的/:id
就是一个params参数的占位符
书写形式:
path: "/search?k=v"
路由传递参数(对象写法)path是否可以结合params参数一起使用
---- 不可以,程序会崩掉
如何指定params参数可传可不传?
— 解决方法:在占位后面加 ? =>
path: "/search/:keyword?"
params参数可以传递也可以不传递,但是如果传递是空串,如何解决?
— 解决方法:用 undefined =>
params:{keyword:''||undefined}})
路由组件能不能传递props数据?
— 可以,但是只能传递params参数,具体知识为props属性
this.$router.push(“/search/”+this.params传参+“?k=”+this.query传参)
this.$router.push(`/search/${this.params传参}?k=${this.query传参}`)
this.$router.push({name:“路由名字”,params:{传参},query:{传参})
程式导航路由跳转到当前路由(参数不变), 多次执行会抛出NavigationDuplicated的警告错误?
原因:vue-router3.1.0之后, 引入了push()的promise的语法, 如果没有通过参数指定回调函数就返回一个promise来指定成功/失败的回调, 且内部会判断如果要跳转的路径和参数都没有变化, 会抛出一个失败的promise
解决方法:
1、是给push 方法,传入相应的成功的回调与失败的回调,可以捕捉当前错误,代码如下
// 这种写法治标不治本,将来在别的组件中push|replace,编程式导航还是会有类似错误
this.$router.push({name:‘Search’,params:{keyword}},()=>{},()=>{})
2、重写push 方法(二次封装),代码如下
//1、先把VueRouter原型对象的push,保存一份
let originPush = VueRouter.prototype.push;
//2、重写push|replace
//第一个参数:告诉原来的push,跳转的目标位置和传递了哪些参数
VueRouter.prototype.push = function (location,resolve,reject){
if(resolve && reject){
originPush.call(this,location,resolve,reject)
}else{
originPush.call(this,location,() => {},() => {})
}
}
// mian.js 中书写
//将三级联动组件注册为全局组件
import TypeNav from '@/pages/Home/TypeNav';
//第一个参数:全局组件名字,第二个参数:全局组件
Vue.component(TypeNav.name,TypeNav);
//在页面中使用时: 直接通过 使用,不需要引入
//在根目录下创建api文件夹,创建request.js文件
import axios from "axios";
//1、对axios二次封装
const requests = axios.create({
//基础路径,requests发出的请求在端口号后面会跟改baseURl
baseURL:'/api',
timeout: 5000,
})
//2、配置请求拦截器
requests.interceptors.request.use(config => {
//config内主要是对请求头Header配置
return config;
})
//3、配置相应拦截器
requests.interceptors.response.use((res) => {
//成功的回调函数
return res.data;
},(error) => {
//失败的回调函数
console.log("响应失败"+error)
return Promise.reject(new Error('fail'))
})
//4、对外暴露
export default requests;
module.exports = {
//代理服务器解决跨域
proxy: {
//会把请求路径中的/api换为后面的代理服务器
'/api': {
//提供数据的服务器地址
// 时间2022/10/23,服务器地址为http://gmall-h5-api.atguigu.cn
target: 'http://gmall-h5-api.atguigu.cn',
}
},
}
//在文件夹api中创建index.js文件,用于封装所有请求
import requests from "@/api/request";
//首页三级分类接口
export const reqCateGoryList = () => {
return requests({
url: '/product/getBaseCategoryList',
method: 'GET'
})
}
//安装nprogress进度条插件
cnpm install --save nprogress
相关配置
//引入进度条
import nprogress from 'nprogress';
//引入进度条样式
import "nprogress/nprogress.css";
requests.interceptors.request.use(config => {
//开启进度条
nprogress.start();
return config;
})
requests.interceptors.response.use((res) => {
//响应成功,关闭进度条
nprogress.done()
},(error) => {
})
并不是所有项目都需要vuex,如果项目小,完全不需要使用vuex,当项目很大、组件很多、数据维护费劲时使用
集中式管理项目中的组件公用的数据
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
//对外暴露store的一个实例
export default new Vuex.Store({
//state:仓库存储数据的地方,公共状态
state:{},
//mutations:修改state的唯一手段,只支持同步
mutations:{},
//actions:处理action,书写自己的业务逻辑,提交mutation,但是不能修改state支持同步异步
actions:{},
//getters:理解为计算属性,用于简化仓库数据,让组件获取仓库的数据更加方便(一般不使用)
getters:{},
})
采用vuex模块式管理数据的原因:
1、建立小仓库 =>在store文件下 home.js,
// home 模块的小仓库
//home 相关逻辑在此页面进行书写
const state = {}
const getters = {}
const mutations = {}
const actions = {}
const modules = {}
export default {
state,
getters,
mutations,
actions,
modules
}
2、在大仓库中引入小仓库 =>在store文件下 index.js,
import Vue from 'vue'
import Vuex from 'vuex'
// 引入小仓库
import home from './home'
Vue.use(Vuex)
export default new Vuex.Store({
// 实现vuex 仓库模块式开发存储数据
modules: {
home
}
})
出现卡顿现象原因:事件触发非常频繁,而且每一次的触发,回调函数都要去执行(如果时间很短,而回调函数内部有计算,那么很可能出现浏览器卡顿
此时我们可以采用debounce(防抖)和throttle(节流)的方式来减少调用频率,同时又不影响实际效果
安装lodash插件,该插件提供了防抖和节流的函数【闭包 + 延迟器】,我们可以引入js文件,直接调用。向外暴露 _ 函数
ladash官网
前面的所有的触发都被取消,最后一次执行在规定的时间之后才会触发,也就是说如果连续快速的触发,只会执行最后一次
用户操作很频繁,但是只是执行一次
ladash官网–防抖用法
在规定的间隔时间范围内不会重复触发回调,只有大于这个时间间隔才会触发回调,把频繁触发变为少量触发
用户操作很频繁,但是把频繁的操作变为少量操作【给浏览器充裕的时间解析代码】
ladash官网–节流用法
// 全部引入 =>import _ from 'lodash'=>使用时 _.throttle
// 最好按需加载
import throttle from 'lodash/throttle'
methods: {
//采用键值对形式创建函数,将changeIndex定义为节流函数,该函数触发很频繁时,设置50ms才会执行一次
// throttle回调函数别用箭头函数,防止出现this问题
changeIndex: throttle(function (index){
this.currentIndex = index
},50),
}
缺点:由于生成的标签过多,出现卡顿现象
出现卡顿的原因:
— router-link是一个组件:相当于VueComponent类的实例对象,一瞬间new VueComponent很多实例(1000+),很消耗内存,因此导致卡顿。
我们是通过触发点击事件实现路由跳转。同理有多少个a标签就会有多少个触发函数。虽然不会出现卡顿,但是也会影响性能。
事件委派即把子节点的触发事件都委托给父节点
1、如何确定我们点击的一定是a标签呢?如何保证我们只能通过点击a标签才跳转呢?
a
标签,加上自定义属性 :data-categoryName
,其余的子节点没有2、如何区分一、二、三级的标签
为三个等级的a标签再添加自定义属性data-category1Id
、:data-category2Id
、:data-category3Id
来获取三 个等级a标签的商品id,用于路由跳转。
我们可以通过在函数中传入event参数,通过event.target
属性获取当前点击节点,再通过dataset属性获取节点的属性信息
mock官网
生成随机数据,拦截 Ajax 请求,返回我们自定义的数据用于测试前端接口
前端mock数据不会和你的服务器进行任何通信
cnpm install mockjs
在项目当中src文件夹中创建mock文件夹
设计JSON 数据结构(在mock文件中创建相应JSON文件)
注意:JSON文件需要格式化一下,不能留有空格,否则跑不起来
将所需图片放置在public文件夹中
创建mockServe.js,通过mockjs插件实现模拟数据
import Mock from 'mockjs'
//webpack默认对外暴露:json、图片
import banner from './banner.json'
import floor from './floor.json'
//mock数据:第一个参数请求地址、第二个参:请求数据
Mock.mock("/mock/banner",{code:200,data:banner})
Mock.mock("/mock/floor",{code:200,data:floor})
mockServe.js文件在入口文件中引入(至少需要执行一次,才能模拟数据)
//在main.js中引入
import ''@/mock/mockServe
在API文件夹中创建mockRequest.js
把公共数据、获取数据方法都统一放在store中,取其数据的固定步骤如下:(轮播)
1、派发action:通过vuex 发起Ajax请求,将数据储存在仓库中
//ListContainer.vue 文件
mounted() {
this.$store.dispatch("getBannerList")
},
2、在store文件中的home.js中,书写相关逻辑,并提交mutations
actions:{
//获取首页轮播图数据
async getBannerList({commit}){
let result = await reqGetBannerList()
if(result.code === 200){
commit("BANNERLIST",result.data)
}
}
}
3、获取到数据后,在mutations
中修改state
mutations:{
BANNERLIST(state,bannerList){
state.bannerList = bannerList
}
},
4、在state中,添加 bannerList
状态
state:{
bannerList:[]
}
5、在ListContainer.vue组件在store中获取轮播图数据。
import {mapState} from "vuex";
export default {
computed: {
...mapState({
bannerList: state => state.home.bannerList
})
},
}
swiper官网
swiper使用方法
原因:在mounted中,先异步请求轮播图数据,然后创建swiper实例。由于请求数据是异步的,所以浏览器不会等待该请求执行完再去创建swiper,而是先创建了swiper实例,但是此时我们的轮播图数据还没有获得,就导致了轮播图展示失败。
解决方法:
1、添加延迟器(不是完美的解决方案)
mounted() {
setTimeout(()=>{
let mySwiper = new Swiper(...)
},2000)
},
缺点:无法确定用户请求到底需要多长时间,因此没办法确定延迟器时间
2、watch + nextTick
this. $nextTick
:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM
<template>
<!--banner轮播-->
<div class="swiper-container" ref="mySwiper">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="(carouse,index) in bannerList" :key="carouse.id">
<img :src="carouse.imgUrl" />
</div>
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>
<!-- 如果需要导航按钮 -->
<div class="swiper-button-prev" ></div>
<div class="swiper-button-next"></div>
</div>
</div>
</template>
<script>
// 引入Swiper
import Swiper from 'swiper'
// 引入Swiper样式
import 'swiper/css/swiper.css'
import {mapState} from "vuex";
export default {
mounted() {
// ajax请求轮播图图片
this.$store.dispatch("getBannerList")
},
computed:{
...mapState({
// 从仓库中获取轮播图数据
bannerList: (state) => {return state.home.bannerList}
})
},
watch:{
bannerList(newValue,oldValue){
// this.$nextTick()使用
this.$nextTick(()=>{
let mySwiper = new Swiper(this.$refs.mySwiper,{
pagination:{
el: '.swiper-pagination',
clickable: true,
},
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
}
})
})
}
}
}
</script>
由于 floor.vue 中,数据是由父组件传递过来的,且从来没有发生改变,因此监听不到list。所以我们需要借助immediate
。
immediate
:立即监听,不管数据是否变化,都监听一次
<template>
<div>
<div class="swiper-container"
ref="cur">
<div class="swiper-wrapper">
<div class="swiper-slide"
v-for="carousel 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>
</div>
</template>
<script>
import Swiper from 'swiper'
export default {
name: 'Carousel',
props: ['list'],
watch: {
list: {
// 立即监听:不管数据是否变化,都监听一次
immediate: true,
handler () {
this.$nextTick(() => {
// eslint-disable-next-line no-unused-vars
const mySwiper = new Swiper(this.$refs.cur, {
loop: true,
// 如果需要分页器
pagination: {
el: '.swiper-pagination',
clickable: true
},
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev'
}
})
})
}
}
}
}
</script>
<style lang="less" scoped>
</style>
mapGetters
里面的写法:数组,因为getters
计算是没有划分模块的(state
:划分home,search)Object.assign()
方法将所有可枚举的自有属性从一个或多个源对象复制到目标对象,返回修改后的对象。
实现对象拷贝,合并对象
// 举例
const obj1 = {
a:1,
b:2
};
const obj2 = Object.assign({b:3, c:4}, obj1);
console.log(object1) // { a: 1, b: 2}
console.log(object2) // { b: 2 c: 4, a: 1}
// 如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖。后面的源对象的属性将类似地覆盖前面的源对象的属性
方法:删除时,将属性值赋值成undefined
,并且进行路由跳转(自己跳自己)
removeCategoryName () {
// 赋值成undefined(不消耗宽带)
this.searchParams.categoryName = undefined
this.searchParams.category1Id = undefined
this.searchParams.category2Id = undefined
this.searchParams.category3Id = undefined
this.getData()
// 地址栏需要改:进行路由跳转
if (this.$route.params) {
this.$router.push({ name: 'search', params: this.$route.params })
}
},
undefined
,然后修改路由信息,同时删除兄弟组件Header中输入框内的关键字在main.js中配置$bus
new Vue({
//全局事件总线$bus配置
beforeCreate() {
//此处的this就是这个new Vue()对象
Vue.prototype.$bus = this
},
}).$mount('#app')
在Search.vue中触发$bus
$emit ('事件名',data)
removeKeyword () {
// 参数赋空
this.searchParams.keyword = undefined
this.getData()
// 通知兄弟组件清除关键字
// data用于传递数据,此页面只是用于通知header组件进行相应操作,故不传data
this.$bus.$emit('clear')
// 进行路由跳转
this.$router.push({ name: 'search', query: this.$route.query })
}
在Header.vue 中监听$bus
$on('事件名',( )=>{ })
mounted () {
// 通过全局总线清除关键字
this.$bus.$on('clear', () => {
this.keyword = ''
})
}
// 连续页数的起始页数和结束页数
startNumAndEndNum () {
const { continues, pageNo, totalPage } = this
let start = 0
let end = 0
// 判断总页数是否等于连续页(5),
// 不等于
if (continues > totalPage) {
start = 1
end = totalPage
} else {
// 等于
start = pageNo - parseInt(continues / 2)
end = pageNo + parseInt(continues / 2)
// start= 0/负数
if (start < 1) {
start = 1
end = continues
}
// end>总页码
if (end > totalPage) {
start = totalPage - continues + 1
end = totalPage
}
}
return { start, end }
}
当切换到新路由时,滚动条的位置,vue-router就可以做到。
vue-route 提供了scrollBehavior
方法(只在支持history.pushState
的浏览器中可用)
scrollBehavior (to, from, savedPosition) {
// return 期望滚动到哪个的位置
return { y: 0 }
}
HTML5中新增,本地存储和会话存储。
本地存储–localStorage:持久化的(5M)
会话存储–sessionStorage:并非持久–会话结束(关闭浏览器)就消失、
发现:发请求时,获取不得购物车里面的数据
=>原因:因为服务器不知道你是谁
=>解决方案:生成uuid(临时游客身份)
由于每个用户的uuid不能发生变化,还要持久存储,所以封装游客身份模块uuid—生成一个随机字符串
创建utils文件夹=>创建uuid_token.js文件
// 我用 uuid_token 会有红杠,提示没有用驼峰写法,因此加上了下面注释
/* eslint-disable camelcase */
import { v4 as uuidv4 } from 'uuid'
// 生成一个随机字符串,且每次执行都不能发生变化,还要持久存储
export const getUUID = () => {
// 判断本地存储是否由uuid
let uuid_token = localStorage.getItem('UUIDTOKEN')
// 本地存储没有uuid
if (!uuid_token) {
// 生成uuid
uuid_token = uuidv4()
// 存储本地
localStorage.setItem('UUIDTOKEN', uuid_token)
}
// 当用户有uuid时就不会再生成
return uuid_token
}
购物车的请求函数中没有参数,无法传 uuid_token
=>解决方法:把uuid_token加在请求头中
在store中的detail模块中定义uuid_token
const state = {
//游客身份
uuid_token: getUUID()
}
在request.js中设置请求头
// 引入store
import store from '@/store';
requests.interceptors.request.use(config => {
// 判断uuid_token是否为空
if (store.state.detail.uuid_token) {
// 请求头添加一个字段:userTempId字段和后端统一
config.headers.userTempId = store.state.detail.uuid_token
}
return config;
})
=>解决方法:every
//判断底部勾选框是否全部勾选
isAllCheck() {
//every遍历某个数组,判断数组中的元素是否满足表达式,全部为满足返回true,否则返回false
return this.cartInfoList.every(item => item.isChecked === 1)
}
@click @change 使用同一个方法进行修改
=>问题:如何辨别点击哪一个
=>解决方法:通过传参来区分谁是谁
<a @click="handler('minus',-1,cartInfo)">-a>
<input @change="handler('change',$event.target.value,cartInfo)">
<a @click="handler('add',1,cartInfo)">+a>
handler 函数=> 需要节流
import throttle from 'lodash/throttle'
handler: throttle(async function (type, disNum, cart) {
// type:区分三个元素
// disNum:+ 变化量(1) -变化量(-1) input最终个数(不是变化量)
// cart:分辨那个产品
// 向服务器发请求
switch (type) {
case 'add':
disNum = 1
break
case 'minus':
// 判断产品个数: skuNum>1,传-1,skuNum<=1,传0
disNum = cart.skuNum > 1 ? -1 : 0
break
case 'change':
// 用户输入非数字||负数=>0
disNum = (isNaN(disNum) || disNum < 1) ? 0 : parseInt(disNum) - cart.skuNum
break
}
try {
await this.$store.dispatch('addOrUpdateShopCart', { skuId: cart.skuId, skuNum: disNum })
this.getDate()
} catch (error) {
}
}, 1000)
=> 通过数据库存储用户信息(名字、密码)
=>登录成功时,后台为了区分用户是谁,服务器会下发token【token:令牌=>唯一的标识符===uuid】
=>注意:一般流程=>登录成功服务器下发token,前台持久化存储token【带着token找服务器要用户信息进行展示】
=>导航:表示路由正在发生改变。进行路由跳转
=>守卫:相当于“紫禁城护卫”
=>在项目中,只要发生路由变化,守卫就能监听到
前置守卫:在路由跳转之前进行判断
router.beforeEach((to, from, next) => {
// to:可以获取你要跳转到的那个路由信息
// from:可以获取你从哪里来的那个路由信息
// next:放行函数 next()=>直接放行 next(path)=>放行到指定路由 next(false)
})
后置守卫:路由跳转已经完成在执行。
=>只在进入路由时触发
beforeEnter: (to, from,next) => {
}
beforeRouteEnter
//进入 beforeRouteUpdate
//更新,指的是参数发生更新时触发beforeRouteLeave
//离开Vue官方提供的一个表单验证的插件:vee-validate
=>不好用,看懂老师操作即可
【elementUI】推荐用它!
路由懒加载:当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效。
写法:
// 将 import Home from '@/views/Home'
// 替换成
const Home = () => import('@/views/Home')
const router = createRouter({
routes: [{ path: '/home', component: Home }],
})
//上面是官网上的介绍,下面是优化写法
const router = createRouter({
routes: [{ path: '/home', component:() => import('@/views/Home') }],
})
执行npm run build
map文件:因为代码是经过加密的,如果运行时报错,输出的错误信息无法准确得知时那里的代码报错。有了map就可以向未加密的代码一样,准确的输出是哪一行那一列有错,所以该文件如果项目不需要是可以去掉的。【map文件较大】
//在vue.config.js配置
module.exports ={
productionSourceMap: false
}
// 修改需要重新build