目录
1. 从零开始-项目的搭建
1.1 创建项目
安装vue-cli脚手架
npm install -g @vue/cli
yarn global add @vue/cli
通过VueCLI创建项目
vue create app
注:vue-cli@4.5及之后版本的脚手架可以选择vue3项目
1.2 项目结构分析
├── node_modules
├── public: 静态资源,webpack进行打包的时候会原封不动打包到dist文件夹中。
│ ├── favicon.ico: 页签图标
│ └── index.html: 主页面
├── src
│ ├── api: 存放API请求的模块
│ ├── assets: 存放静态资源
│ │ └── logo.png
│ │── component: 存放公共组件
│ │ └── HelloWorld.vue
│ │── mock: 存放mock假数据以及配置
│ │── views/pages: 存放路由组件
│ │── router: 存放路由配置
│ │── store: 存放拆分的vuex模块
│ │── utils: 存放所有工具类
│ │── App.vue: 汇总所有组件
│ │── main.js: 入口文件
├── .editorconfig: 统一代码风格的配置文件
├── .env.development: 开发环境的配置
├── .env.production: 生产环境的配置
├── .env.staging: 预发布环境的配置
├── .eslintignore: eslint校验忽略的配置
├── .eslintrc.js: eslint代码校验配置
├── .gitignore: git版本管制忽略的配置
├── babel.config.js: babel的配置文件
├── package.json: 应用包配置文件
├── README.md: 应用描述文件
├── package-lock.json:包版本控制文件
├── vue.config.js:vue项目的可选配置文件
1.3 配置项目中的一些细节
1.3.1 项目运行浏览器自动打开
package.json:在scripts配置项下找到serve
或dev
,在后面空一格加上--open
"scripts": {
"dev": "vue-cli-service serve --open",
....
},
1.3.1 关闭eslint校验工具(工作中建议开启,代码规范有利于同组人员协助开发)
在项目文件夹根目录下找到vue.config.js
(与src同级),没有的话创建一个。
module.exports = {
//关闭eslint提示。
lintOnSave: false
}
1.3.2 配置代理跨域
在项目文件夹根目录下找到vue.config.js
(与src同级),没有的话创建一个。
devServer: {
proxy: {
// 代理地址的前缀
'/dev-api' : {
// 代理地址
target: 'http://gmall-h5-api.atguigu.cn',
// 是否更改请求头中的host地址,true:更改,为target地址; false:不更改,默认为本机地址。
changeOrigin: true,
// 重写请求地址
pathRewrite: { '^/dev-api': '' },
}
}
}
1.3.3 配置@为路径别名
- paths:配置路径别名
- exclude:排除的文件夹
{
"compilerOptions": {
"paths": {
"@/*": [
"src/*"
]
}
},
"exclude": [
"node_modules",
"dist"
]
}
2. 路由问题
2.1 路由传参
路由传参可看这篇文章Vue2高级-配置路由、路由传参与路由路径的两种模式
2.2 编程式路由导航多次点击报错问题
-
原因:Vue-router@3.0 之后添加了同一路径跳转错误异常功能导致。
-
具体原因
let result = this.$router.push({name:"Search",query:{keyword:this.keyword}}) console.log(result)
多次执行警告
执行结果发现:push的返回结果为一个Promise函数,因为Promise函数需要成功和失败的回调作为两个参数而此处没有传递,所以导致了报错。
此时可以想到给push传递两个成功和失败的回调即可,但是该方法过于麻烦,并且治标不治本。例:
this.$router.push({name:"Search",query:{keyword:this.keyword}},()=>{},()=>{})
-
完美的解决方法:重写
$router.push()
和$router.replace()
方法// 重写push|replace // function(跳转路径,请求成功回调函数,请求失败回调函数) // call|apply区别 // 相同点:都可以调用函数一次,都可以篡改函数上下文一次。 // 不同点:call传参用逗号隔开,apply传参用数组传递。 VueRouter.prototype.push = function (location, resolve, reject) { if (resolve && reject) { //代表着两个形参接受参数【箭头函数】 originPush.call(this, location, resolve, reject); } else { originPush.call(this, location, () => {}, () => {}); } }; VueRouter.prototype.replace = function (location, resolve, reject) { if (resolve && reject) { //代表着两个形参接受参数【箭头函数】 originReplace.call(this, location, resolve, reject); } else { originReplace.call(this, location, () => {}, () => {}); } }
3. 封装Axios&引入进度条
-
axios的使用可以参考Axios文档
-
提示:在js文件中可以引入其他js文件,所以在axios.js配置文件中使用store也是可以的,可以将这些配置仅当成js文件来看待。
-
nprogress进度条:
import nprogress from 'nprogress'
nprogress.start()
进度条开始,nprogress.done()
进度条结束
// 对于axios进行二次封装 import axios from 'axios' // 引入进度条 import nprogress from 'nprogress' // 引入进度条样式 import "nprogress/nprogress.css" // 引入store import store from "@/store"; // start: 进度条开始; done:进度条结束 // 1.利用axios对象的方法create,创建一个axios实例 // 2.request就是axios,只是稍微配置一下 const requests = axios.create({ // 配置对象 // 基础路径 baseURL: '/api', // 请求超时的时间 timeout: 5000, }) // 请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求发出去之前做一些事情 requests.interceptors.request.use((config) => { nprogress.start() // config:配置对象,对象里面的Header请求头很重要。 if (store.state.detail.uuid_token) { // 给请求头添加字段,后台为 userTempId config.headers.userTempId = store.state.detail.uuid_token } // 判断:携带token给服务器 if (store.state.user.token) { config.headers.token = store.state.user.token } return config }) // 响应拦截器 requests.interceptors.response.use((res) => { // 成功的回调函数:服务器数据回来后,响应拦截器可以拦截到,处理一些事情。 nprogress.done() return res.data }, (error) => { console.log(error); // 失败的回调函数 // 终止Promise链 return new Promise() } ) // 对外暴露 export default requests
3.1 请求接口统一封装
在文件夹api中创建index.js文件,用于封装所有请求
将每个请求封装为一个函数,并暴露出去,组件只需要调用相应函数即可,这样当我们的接口比较多时,如果需要修改只需要修改该文件即可。
//当前模块,API进行统一管理,即对请求接口统一管理
import requests from "@/api/request";
//首页三级分类接口
export const reqCateGoryList = () => {
return requests({
url: '/product/getBaseCategoryList',
method: 'GET'
})
}
3.2 请求接口获取
-
通过引入js获取
import {reqCateGoryList} from './api' //发起请求 reqCateGoryList();
-
将js模块统一暴露挂载到Vue实例上
// * AS API :统一引入,别名为API import * as API from '@/api' // 统一接收api文件夹里面全部请求函数 new Vue({ render: (h) => h(App), ... beforeCreate() { // 将API模块挂载到Vue原型上 Vue.prototype.$API = API } }).$mount("#app");
4. 模块化引入Vuex
-
store/index.js
下配置Vuex模块import Vue from 'vue' import Vuex from 'vuex' // 引入模块仓库 import home from './home' import search from './search' import detail from "./detail"; import cart from "./cart"; import user from "./user"; import trade from "./trade"; // 使用插件 Vue.use(Vuex) // 对外暴露Store类的一个实例 export default new Vuex.Store({ modules: { home, search, detail, cart, user, trade } })
-
main.js
下将Vuex挂载到Vue实例身上import store from '@/store' // 引入Vuex仓库 new Vue({ render: (h) => h(App), // 注册仓库:组件实例身上会多一个$store属性 store, }).$mount("#app");
5. 防抖与节流
防抖与节流可以看我的这篇文章,总结的比较详细 Vue细节篇【防抖与节流】
6. ES6知识-await和async
-
使用原因:封装了axios,导致返回结果为Promise函数。
import {reqCateGoryList} from '@/api' export default { actions:{ categoryList({commit}){ let result = reqCateGoryList() console.log(result) } } }
-
代码改写
actions:{ categoryList({commit}){ let result = reqCateGoryList().then( res=>{ console.log(res) return res } ) console.log(result) commit('CATEGORYLIST', result.data) } }
由于Promise是异步请求,执行此段代码后发现,
console.log(result)
比console.log(res)
先执行,此时就导致commit('CATEGORYLIST', result.data)
方法并不能拿到result.data数据,所以需要await和async。 -
含义:await含义是async标识的函数体内的并且在await标识代码后面的代码,先等待await标识的异步请求执行完,再执行。这也使得只有reqCateGoryList执行完,result 得到返回值后,才会执行后面的输出操作。
async categoryList(){ let result = await reqCateGoryList() console.log("result") console.log(result) }
7. 编程式路由导航+事件委派实现三级分类路由跳转
-
案例:三级标签列表有很多,每一个标签都是一个页面链接,我们要实现通过点击表现进行路由跳转。
路由跳转的两种方法:导航式路由<router-link/>
、编程式路由。1. 导航式路由
<router-link/>
:有多少个a标签就会生成多少个<router-link/>
标签,这样当我们频繁操作时会出现卡顿现象。
2. 编程式路由$router.push()
:我们是通过触发点击事件实现路由跳转。同理有多少个a标签就会有多少个触发函数。虽然不会出现卡顿,但是也会影响性能。 -
事件委派问题:
(1)如何确定我们点击的一定是a标签呢?如何保证我们只能通过点击a标签才跳转呢?
(2)如何获取子节点标签的商品名称和商品id(我们是通过商品名称和商品id进行页面跳转的)
-
解决方法:
-
问题1:为三个等级的a标签添加自定义属性date-categoryName绑定商品标签名称来标识a标签(其余的标签是没有该属性的)。
-
问题2:为三个等级的a标签再添加自定义属性data-category1Id、data-category2Id、data-category3Id来获取三个等级a标签的商品id,用于路由跳转。
我们可以通过在函数中传入event参数,获取当前的点击事件,通过event.target属性获取当前点击节点,再通过dataset属性获取节点的属性信息。
-
<div class="all-sort-list2" @click="goSearch">
<div class="item" v-for="(c1, index) in categoryList" :key="c1.categoryId"
:class="{ cur: currentIndex === index }">
<div v-show="index !== 9 && index !== 16">
<!-- 一级分类 -->
<h3 @mouseenter="changeIndex(index)">
<a :data-categoryName="c1.categoryName" :data-category1Id="c1.categoryId">{{ c1.categoryName }}</a>
</h3>
<!-- 二级、三级分类 -->
<div class="item-list clearfix"
:style="{ display: currentIndex === index ? 'block' : 'none' }">
<div class="subitem" v-for="(c2) in c1.categoryChild" :key="c2.categoryId">
<dl class="fore">
<dt>
<a :data-categoryName="c2.categoryName"
:data-category2Id="c2.categoryId">{{ c2.categoryName }}</a>
</dt>
<dd>
<em v-for="(c3) in c2.categoryChild" :key="c3.categoryId">
<a :data-categoryName="c3.categoryName"
:data-category3Id="c3.categoryId">{{ c3.categoryName }}</a>
</em>
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
- 对应的goSearch方法
// 进行路由跳转的方法
goSearch(event) {
let element = event.target
console.log(element);
// <a data-v-18b3c0cc data-categoryname="手机" data-category3id="61">手机</a>
let { categoryname, category1id, category2id, category3id } = element.dataset
// 标签身上拥有categoryname一定是a标签
if (categoryname) {
// 定义路由跳转的参数
let location = { name: 'search' }
let query = { categoryName: categoryname }
// 区分一级、二级、三级分类标签
if (category1id) {
query.category1Id = category1id
} else if (category2id) {
query.category2Id = category2id
} else if (category3id) {
query.category3Id = category3id
}
// 判断:如果路由中有params参数,则需要一起收集
if (this.$route.params) {
location.params = this.$route.params
// 整理完参数
location.query = query
this.$router.push(location)
}
}
},
8. 数据获取问题
当遇到需要多次切换的路由时,将该路由的数据获取放到
App.vue
的mounted
中能够减少性能的消耗
export default {
name: 'App',
mounted() {
// 将获取数据的方法放在App中会更好,每次组件切换不会重新获取,仅会获取一次。
// 触发Vuex发送请求获取数据
this.$store.dispatch('home/categoryList')
},
}
</script>
9. 提取轮播图为公共组件
需要注意的是我们要把定义swiper对象放在watch中执行,并且还要设置immediate:true属性,这样可以实现,无论数据有没有变化,上来立即监听一次。
公共组件Carousel代码
<template>
<div class="swiper-container" ref="cur" id="floor1Swiper">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="(carouse,index) in carouselList" :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>
</template>
<script>
import Swiper from "swiper";
import 'swiper/css/swiper.css'
export default {
name: "Carousel",
props:["carouselList"],
watch: {
carouselList: {
//这里监听,无论数据有没有变化,上来立即监听一次
immediate: true,
//监听后执行的函数
handler(){
//第一次ListContainer中的轮播图Swiper定义是采用watch+ this.$nextTick()实现
this.$nextTick(() => {
let mySwiper = new Swiper(this.$refs.cur,{
loop: true, // 循环模式选项
// 如果需要分页器
pagination: {
el: '.swiper-pagination',
// clickable: true
},
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
// 如果需要滚动条
scrollbar: {
el: '.swiper-scrollbar',
},
})
})
}
}
}
}
</script>
10. ES6知识-合并对象
Object.assign()
方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
// 组件挂载之前执行
beforeMount() {
// 商品分类搜索条件--复杂的写法
// this.searchParams.category1Id = this.$route.query.category1Id;
// this.searchParams.category2Id = this.$route.query.category2Id;
// this.searchParams.category3Id = this.$route.query.category3Id;
// this.searchParams.categoryName = this.$route.query.categoryName;
// 合并对象,将第一个参数之后的参数摊入第一个参数中。
Object.assign(this.searchParams, this.$route.query, this.$route.params)
},
11. 组件间通信方式(六种)
详细介绍请点击此处查看Vue2进阶篇-组件间通信的6钟方式
12. Vuex获取数据undefined警告
-
对应的getters代码
const getters = { categoryView(state){ return state.goodInfo.categoryView } }
-
getters获取数据
return state.goodInfo.categoryView
,页面可以正常运行,但是会出现红色警告。
-
原因:假设我们网络故障,导致goodInfo的数据没有请求到,即goodInfo是一个空的对象,当我们去调用getters中的
return state.goodInfo.categoryView
时,因为goodInfo为空,所以也不存在categoryView,即我们getters得到的categoryView为undefined。所以我们在html使用该变量时就会出现没有该属性的报错。
即:网络正常时不会出错,一旦无网络或者网络问题就会报错。 -
总结:所以我们在写
getters
的时候要养成一个习惯在返回值后面加一个||
条件。即当属性值undefined
时,会返回||后面的数据,这样就不会报错。
如果返回值为对象加|| {}
,数组:|| []
。
13. 路由导航守卫
-
作用:拦截路由进入和离开,可进行权限判断等操作。
-
全局前置守卫实现页面跳转验证
//设置全局导航前置守卫
router.beforeEach(async(to, from, next) => {
let token = store.state.user.token
let name = store.state.user.userInfo.name
//1、有token代表登录,全部页面放行
if(token){
//1.1登陆了,不允许前往登录页
if(to.path==='/login'){
next('/home')
} else{
//1.2、因为store中的token是通过localStorage获取的,token有存放在本地
// 当页面刷新时,token不会消失,但是store中的其他数据会清空,
// 所以不仅要判断token,还要判断用户信息
//1.2.1、判断仓库中是否有用户信息,有放行,没有派发actions获取信息
if(name)
next()
else{
//1.2.2、如果没有用户信息,则派发actions获取用户信息
try{
await store.dispatch('getUserInfo')
next()
}catch (error){
//1.2.3、获取用户信息失败,原因:token过期
//清除前后端token,跳转到登陆页面
await store.dispatch('logout')
next('/login')
}
}
}
}else{
//2、未登录,首页或者登录页可以正常访问
if(to.path === '/login' || to.path === '/home' || to.path==='/register')
next()
else{
alert('请先登录')
next('/login')
}
}
})
14. Vue图片引入问题
非js内引入图片(html):一般都是通过路径引入,例如:<img src="../assets/pay.jpg">
。
js内引入图片: 可分为通过路径引入和不通过路径引入。
1、如果想要通过路径方式在vue中的js引入图片,必须require引入。
例如:js中引入个人支付二维码可以通过下面方式实现
this.$alert(`<img height="200px" width="200px" src="${require('@/assets/pay.jpg')}" / >`, '请使用微信扫码', {
dangerouslyUseHTMLString: true,
showCancelButton: true,
center: true
});
15. 表单验证
表单验证个人推荐使用element ui的from表单验证,看一下官网的示例就会用。
element ui from表单验证链接
16. 图片懒加载
懒加载vue-lazyload插件官网
插件的使用直接参考官方教程,很简单。
17. 路由懒加载
component: () => import('../pages/Detail')
,只有在调用时才会执行该回调,然后挂在组件
//详情页面组件
{
//需要params传参(产品id)
path: "/detail/:skuId",
name: 'Detail',
component: ()=> import('../pages/Detail'),
meta:{show: true},
},
//添加购物车成功
{
path: "/addcartsuccess",
name: 'AddCartSuccess',
component: ()=> import('../pages/AddCartSuccess'),
meta:{show: true},
},
18. 项目打包发布
在项目文件夹下执行npm run build
。会生成dist打包文件。
将打包好的项目文件直接扔在服务器端,通过Nginx配置即可访问。