github代码地址
我的git的仓库地址
项目主要步骤1.脚手架目录分析(1)静态页面,拆分组件
(2)向服务器发请求(API),捞取数据
(3)Vuex三连环(state,actions,mutations)
(4)组件获取仓库数据,动态展示数据
public文件夹:静态资源,webpack进行打包的时候会原封不动打包到dist文件夹中。
src文件夹:(程序员代码文件夹)
assets文件夹:经常放置一些静态资源(图片)assets文件夹里面资源webpack会进行打包为一个模块(js文件夹里面)
components文件夹:一般放置非路由组件(或者项目共用的组件)
App.vue 唯一的根组件
main.js 入口文件【程序最先执行的文件】
babel.config.js:babel配置文件
package.json:看到项目描述、项目依赖、项目运行指令
README.md:项目说明文件
2.项目配置
2.1 项目运行,浏览器自动打开
package.json
"scripts": {
"serve": "vue-cli-service serve --open",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
2.2 关闭eslint校验工具(在根目录下创建vue.config.js进行配置)
module.exports = {
//关闭eslint
lintOnSave: false
}
2.3 src文件夹的别名的设置
因为项目大的时候src(源代码文件夹):里面目录会很多,找文件不方便,设置src文件夹的别名的好处,找文件会方便一些
创建jsconfig.json文件
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": [
"src/*"
]
}
},
"exclude": [
"node_modules",
"dist"
]
}
3.项目主要界面搭建
3.1创建Header、Footer 组件
选择辉洪老师的静态页面
3.2引入静态页面,引入样式,引入图片静态资源,在APP.vue上注册使用项目采用的less样式,浏览器不识别less语法,需要一些loader进行处理,把less语法转换为CSS语法
3.3安装less less-loader@54.配置路由切记less-loader安装5版本的,不要安装在最新版本,安装最新版本less-loader会报错,报的错误setOption函数未定义
需要在style标签的身上加上lang=“less”
router文件夹下创建index.js
import Vue from 'vue';
import VueRouter from 'vue-router';
//使用插件
Vue.use(VueRouter);
//对外暴露VueRouter类的实例
export default new VueRouter({
routes:[
{
path:'/home',
component:Home
}
]
})
5.路由组件总结
$router:进行编程式导航的路由跳转
this.$router.push|this.$router.replace
$route:可以获取路由的信息|参数
this.$route.path
this.$route.params|query
this.$route.meta
6.定义全局组件编程式导航路由跳转到当前路由(参数不变), 多次执行会抛出NavigationDuplicated的警告错误?
注意:编程式导航(push|replace)才会有这种情况的异常,声明式导航是没有这种问题,因为声明式导航内部已经解决这种问题。
这种异常,对于程序没有任何影响的。
为什么会出现这种现象:
由于vue-router最新版本3.5.2,引入了promise,当传递参数多次且重复,会抛出异常,因此出现上面现象,
第一种解决方案:是给push函数,传入相应的成功的回调与失败的回调
第一种解决方案可以暂时解决当前问题,但是以后再用push|replace还是会出现类似现象,因此我们需要从‘根’治病;
我们的三级联动组件是全局组件,全局的配置都需要在main.js中配置
//将三级联动组件注册为全局组件
import TypeNav from '@/pages/Home/TypeNav';
//第一个参数:全局组件名字,第二个参数:全局组件
Vue.component(TypeNav.name,TypeNav);
在Home组件中使用该全局组件
7.Home首页其他组件
8.封装axios
没安装的小伙伴先安装axios
Axios官方地址
在根目录下创建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配置
//比如添加token
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;
9、前端通过代理解决跨域问题
我们在封装axios的时候已经设置了baseURL为api,所以所有的请求都会携带/api,这里我们就将/api进行了转换。如果你的项目没有封装axios,或者没有配置baseURL,建议进行配置。要保证baseURL和这里的代理映射相同,此处都为’/api’。
module.exports = {
//关闭eslint
lintOnSave: false,
devServer: {
// true 则热更新,false 则手动刷新,默认值为 true
inline: false,
// development server port 8000
port: 8001,
//代理服务器解决跨域
proxy: {
//会把请求路径中的/api换为后面的代理服务器
'/api': {
//提供数据的服务器地址
target: 'http://39.98.123.211',
}
},
}
}
10.封装请求接口
在文件夹api中创建index.js文件,用于封装所有请求
export const reqCategoryList = () => requests({ url: '/product/getBaseCategoryList', method: 'get' });
11.nprogress进度条插件
打开一个页面时,往往会伴随一些请求,并且会在页面上方出现进度条。它的原理时,在我们发起请求的时候开启进度条,在请求成功后关闭进度条,所以只需要在request.js中进行配置。
在api下创建一个名为request.js的文件
//对于axios进行二次封装
import axios from "axios"
//在当前模块中引入store
import store from "@/store"
//引入进度条
import nprogress from 'nprogress'
//引入进度条样式
import "nprogress/nprogress.css"
//1.利用axios对象的方法create,去创建一个axios实例
//2.request就是axios,只不过稍微配置一下
const request = axios.create({
//配置对象
baseURL: "/api",
//代表请求超时的时间5s
timeout: 5000,
})
//请求拦截器:在发送请求之前,请求拦截器可以检测到,可以在请求发出之前做一些事情
request.interceptors.request.use((config) => {
//config:配置对象,对象里面有一个属性很重要,headers请求头
//进度条开始动
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
}
nprogress.start()
return config
})
//响应拦截器
request.interceptors.response.use((res) => {
//成功的回调函数:服务器响应数据回来以后,响应拦截器可以检测到,可以做一些事
//进度条结束
nprogress.done()
return res.data;
}, (error) => {
//响应失败的回调函数
return Promise.reject(new Error('faile'))
})
//对外暴露
export default request
可以通过样式来调整进度条的颜色(我这儿设置的天蓝色,大家看情况自己更改就好)
12、Vuex没有下载的小伙伴先下载Vuex
cnpm i --save vuex
首先在根目录创建一个名为store的文件夹,文件下创建index.js作为我们的大仓库,未来会有很多的小仓库
大仓库的代码如下
import Vue from 'vue'
import Vuex from 'vuex'
//需要使用插件一次
Vue.use(Vuex)
import home from './home'
import search from './search'
import detail from './detail'
import shopcart from './shopcart'
import user from './user'
import trade from './trade'
//对外暴露Store类的一个实例
export default new Vuex.Store({
//实现Vuex仓库模块式开发存储数据
modules: {
home,
search,
detail,
shopcart,
user,
trade
}
})
小仓库的代码如下(这儿以home为例)
import { reqCategoryList,reqFloorList,reqGetBannerList } from "@/api"
//home模块的小仓库
const state = {
//state中数据默认初始值别瞎写,服务器返回的是对象,起始的就是对象,返回的是数组,起始值就是数组
categoryList: [],
//轮播图的数据
bannerList: [],
//floor组件的数据
floorList:[]
}
const actions = {
//通过API里面的接口函数调用,向服务器发请求,获取服务器的数据
async categoryList({ commit }) {
let result = await reqCategoryList()
if (result.code == 200) {
commit("CATEGORYLIST",result.data.slice(0,16))
}
},
//获取首页轮播图的数据
async getBannerList({ commit }) {
let result = await reqGetBannerList()
if (result.code == 200) {
commit('GETBANNERLIST',result.data)
}
},
async getFloorList({ commit }) {
let result = await reqFloorList()
if (result.code == 200) {
commit('GETFLOORLIST',result.data)
}
}
}
const mutations = {
CATEGORYLIST(state, categoryList) {
state.categoryList=categoryList
},
GETBANNERLIST(state, bannerList) {
state.bannerList=bannerList
},
GETFLOORLIST(state, floorList) {
state.floorList=floorList
}
}
//计算属性
const getters = {}
export default {
state,
actions,
mutations,
getters
}
注意:这儿使用action时,函数的第一个参数,我用的是{commit},这样下面可以直接commit(),当然,第一个参数也可以commit,不过下面就得context.commit(),这儿使用{commit}更简便一些,后面会用到在action里面继续dispatch调用actions,我们可以打印context,就会发现它里面包含的东西,如图:
13.loadsh插件防抖和节流防抖
指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
节流
指连续触发事件但是在 n 秒中只执行一次函数。节流会稀释函数的执行频率
//引入lodash:是把lodash全部封装好的函数全都引入进来了
//按需引入:只是引入节流函数,其他的函数没有引入(模块),这样做的好处是,当你打包项目的时候体积会小一些
import throttle from "lodash/throttle";
methods: {
// 鼠标进入修改currentIndex属性
changeIndex: throttle(function (index) {
//index:鼠标一段移上某一个一级分类的元素的索引值
//正常情况(用户慢慢的 *** 作):鼠标进入,每一个一级分类h3都会进入事件
//非正常情况(用户 *** 作很快):本身全部的一级分类都应该触发鼠标进入事件,但是经过测试,只有部分h3触发了
//就是由于用户行为过快,导致浏览器反应不过来,如果当前回调函数中有一些大量业务,也可能出现卡顿现象
this.currentIndex = index;
}, 50),
leaveIndex() {
this.currentIndex = -1;
},
},
14、编程式导航+事件委托实现路由跳转
用户可以点击三级联动,Home模块跳转到Search模块,同时会把你选中的产品(产品名字、ID)传递过去
我们可以通过在函数中传入event参数,获取当前的点击事件,通过event.target属性获取当前点击节点,再通过dataset属性获取节点的属性信息。
{{c1.categoryName}}
{{c2.categoryName}}
{{c3.categoryName}}
注意:event是系统属性,所以我们只需要在函数定义的时候作为参数传入,在函数使用的时候不需要传入该参数。
15.swiper插件实现轮播图Swiper官网
官网有具体实例,小伙伴们有不懂的可以直接去官网查看
(1)安装swiper
(2)在需要使用轮播图的组件内导入swpier和它的css样式
(3)在组件中创建swiper需要的dom标签(html代码,参考官网代码)
(4)创建swiper实例
完美解决方案:使用watch+this.$nextTick()
官方介绍:this. $nextTick它会将回调延迟到下次 DOM 更新循环之后执行(循环就是这里的v-for)。
16.props父子通信
父组件:home文件下的index.js
//...省略
子组件:Floor下的index.vue
//...省略
17、将轮播图模块提取为公共组件
注意:
(1)组件在main.js中引入,并定义为全局组件。我这里只是在使用到该组件的地方引入并声明
(2)引用组件时要在components中声明引入的组件。
(3)我们将轮播图组件已经提取为公共组件Carouse,所以我们只需要在Carouse中引入swiper和相应css样式。
18.开发 Search 模块
静态界面直接使用资料中准备好的文件,不再一一拆分了
然后使用Vuex发请求获取数据
import { reqGetSearchInfo } from "@/api";
// search模块小仓库
const state = {
//仓库初始状态
searchList: {}
};
const mutations = {
GETSEARCHLIST(state, searchList) {
state.searchList = searchList;
},
};
const actions = {
//获取search模块数据
async getSearchList({ commit }, params = {}) {
//当前这个reqGetSearchInfo这个函数在调用获取服务器数据的时候,至少传递一个参数(空对象)
//params形参:是当用户派发action的时候,第二个参数传递过来的,至少是一个空对象
let result = await reqGetSearchInfo(params);
if (result.code == 200) {
commit("GETSEARCHLIST", result.data);
}
},
};
mounted() {
this.$store.dispatch('getSearchList', {})
},
19.getters使用
如果不使用getters属性,我们在组件获取state中的数据表达式为:this.$store.state.子模块.属性
,
个人理解:getters将获取store中的数据封装为函数,代码维护变得更简单(和我们将请求封装为api一样)。而且getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
注意:仓库中的getters是全局属性,是不分模块的。即store中所有模块的getter内的函数都可以通过$store.getters.函数名获取
下图为store内容
store中search模块代码
import {reqGetSearchInfo} from '@/api';
const state = {
searchList:{},
}
const mutations = {
SEARCHLIST(state,searchList){
state.searchList = searchList
}
}
const actions = {
//第二个参数data默认是一个空对象
async getSearchListr({commit},data={}){
let result = await reqGetSearchInfo(data)
if(result.code === 200){
commit("SEARCHLIST",result.data)
}
}
}
const getters = {
goodsList(state){
//网络出现故障时应该将返回值设置为空
return state.searchList.goodsList||[]
}
}
export default {
state,
mutations,
actions,
getters,
}
在Search组件中使用getters获取仓库数据
//只展示了使用getters的代码
20.利用watch监听路由信息变化实现动态搜索
如果在每个三级分类列表和搜索按钮加一个点击按钮事件,只要点击了就执行搜索函数
但是这样子做会生成很多回调函数,很消耗性能。
最好解决方法:用watch监听路由信息变化
我们每次进行新的搜索时,我们的query和params参数中的部分内容会发生变化,而且这两个参数都是路由的属性,所以可以通过监听路由信息变化来动态发起搜索请求。
search组件watch部分代码
// 数据监听:监听组件实例身上的属性的属性值是否变化
watch: {
$route(newValue, oldValue) {
// 再次处理请求参数
Object.assign(this.searchParams, this.$route.query, this.$route.params);
console.log(this.searchParams);
this.getData();
//分类名字与关键字不用清理:因为每一次路由发生变化的时候,都会给他赋予新的数据
this.searchParams.category1Id = undefined;
this.searchParams.category2Id = undefined;
this.searchParams.category3Id = undefined;
},
},
21.面包屑处理分类(处理query参数,地址栏的处理)
SearchSelector组件有两个属性也会生成面包屑,分别为品牌名、手机属性。如下图所示
在点击删除分类时,我们需要categoryName和 category3Id(或者是category1Id、category2Id)删除掉,但是params中的keyword参数(华为)不需要删除。
所以我们需要把 categoryName 、category3Id、category1Id 、category2Id 赋值为 undefined ;接着我们再次发请求更新页面上的数据
这个时候我们点击分类,页面上的数据的确发生变化了,但是地址栏上的内容并没有变化,事实上页面上地址栏同样也需要改变,而且路径中的params不应该删除,路由跳转的时候应该带着
我们应该重新跳转当前页面,并携带params参数
至此面包屑部分内容结束。
总结:面包屑由四个属性影响:parads、query、品牌、手机属性
面包屑生成逻辑
判断searchParams相关属性是否存在,存在即显示。
面包屑删除逻辑
Search.vue js代码()
22.商品排序(计算属性)
排序的逻辑比较简单,只是改变一下请求参数中的order字段,后端会根据order值返回不同的数据来实现升降序。
order属性值为字符串,例如‘1:asc’、‘2:desc’。1代表综合,2代表价格,asc代表升序,desc代表降序。
升降序是通过箭头图标来辨别的,如图所示:
图标是iconfont网站的图标,通过引入在线css的方式引入图标
iconfont-阿里巴巴矢量图标库
挑选一个自己喜欢的箭头
在public文件index引入该css
在search模块使用该图标
综合
价格
isOne、isTwo、isAsc、isDesc计算属性代码
computed:{
...mapGetters(['goodsList']),
isOne(){
return this.searchParams.order.indexOf('1')!==-1
},
isTwo(){
return this.searchParams.order.indexOf('2')!==-1
},
isDesc(){
return this.searchParams.order.indexOf('desc')!==-1
},
isAsc(){
return this.searchParams.order.indexOf('asc')!==-1
},
},
点击‘综合’或‘价格’的触发函数changeOrder
//flag用于区分综合、价格,1:综合,2:价格
changeOrder(flag){
let newSearchOrder = this.searchParams.order
//将order拆为两个字段orderFlag(1:2)、order(asc:desc)
let orderFlag = this.searchParams.order.split(':')[0]
let order = this.searchParams.order.split(':')[1]
//由综合到价格、由价格到综合
if(orderFlag!==flag){
//点击的不是同一个按钮
newSearchOrder = `${flag}:desc`
this.searchInfo()
}else{
//多次点击的是不是同一个按钮
newSearchOrder = `${flag}:${order==='desc'?'asc':'desc'}`
}
//需要给order重新赋值
this.searchParams.order = newSearchOrder;
//再次发请求
this.searchInfo();
}
23.手写分页器
实际开发中是不会手写的,一般都会用一些开源库封装好的分页,比如element ui。但是我们还是要知道手写该怎么写(日历也是一样的)
核心属性:
pageNo 当前页pageSize 每一页展示多少数据total 一共多少数据continues 分页连续页码数由pageSize和total可以得到另一信息:共页数
totalPage 总页数 == (total / pageSize) //连续页码的起始页码、末尾页码
startNumAndEnd(){
let start = 0 , end = 0;
//规定连续页码数字5(totalPage至少5页)
//不正常现象
if(this.continues > this.totalPage){
start = 1
end = this.totalPage
}else{
//正常现象 Math.floor:想下取整
start = this.pageNo - Math.floor(this.continues/2)
end = this.pageNo + Math.floor(this.continues/2)
//start出现不正常现象纠正
if(start < 1){
start = 1
end = this.continues
}
//end出现不正常现象纠正
if(end > this.totalPage){
end = this.totalPage
start = this.totalPage - this.continues + 1
}
}
return {start,end}
}
24.undefined细节
访问undefined的属性值会引起红色警告,可以不处理(我们称之为假报错)
以获取商品categoryView信息为例,categoryView是一个对象。
对应的getters代码
const getters = {
categoryView(state){
return state.goodInfo.categoryView
}
}
对应的computed代码
computed:{
...mapGetters(['categoryView'])
}
下细节在于getters的返回值。如果getters按上面代码写为return state.goodInfo.categoryView,
页面可以正常运行,但是会出现红色警告。
原因:假设我们网络故障,导致goodInfo的数据没有请求到,即goodInfo是一个空的对象,当我们去调用getters中的return state.goodInfo.categoryView时,因为goodInfo为空,所以也不存在categoryView,即我们getters得到的categoryView为undefined。所以我们在html使用该变量时就会出现没有该属性的报错。
即:网络正常时不会出错,一旦无网络或者网络问题就会报错。
总结:所以我们在写getters的时候要养成一个习惯在返回值后面加一个||条件。即当属性值undefined时,会返回||后面的数据,这样就不会报错。
如果返回值为对象加||{},数组:||[ ]。
此处categoryView为对象,所以将getters代码改为return state.goodInfo.categoryView||{}
商品详情唯一难点就是放大镜
在轮播图组件中设置一个currendIndex,用来记录所点击图片的下标,并用currendIndex实现点击图片高亮设置。当符合图片的下标满足currentIndex===index
时,该图片就会被标记为选中。
轮播图组件和放大镜组件是兄弟组件,所以要通过全局总线通信。
在轮播图组件中,点击图片触发全局事件changeImg,参数为图片所在数组的下标
changeImg(index){
//将点击的图片标识位高亮
this.currentIndex = index
//通知兄弟组件修改大图图片
this.$bus.$emit("changeImg",index)
}
对应的放大镜组件,首先在mounted监听该全局事件
mounted() {
this.$bus.$on("changeImg",(index)=>{
//修改当前响应式图片
this.currentIndex = index;
})
},
放大镜组件中也会有一个currentIndex,他用表示大图中显示的图片的下标(因为放大镜组件只能显示一张图片),全局事件传递的index赋值给currentIndex ,通过computed计算属性改变放大镜组件展示的图片下标。
computed:{
imgObj(){
return this.skuImageList[this.currentIndex] || {}
}
},
26.购买产品个数的 *** 作
这里可以点击 “+” 或者 “-” ,也可以在输入框输入
+
1 ? skuNum-- : (skuNum = 1)"
>-
//表单元素修改产品个数
changeSkuNum(event) {
//用户输入进来的文本 * 1
let value = event.target.value * 1;
//如果用户输入进来的非法,出现NaN或者小于1
if (isNaN(value) || value < 1) {
this.skuNum = 1;
} else {
//正常大于1【大于1整数不能出现小数】
this.skuNum = parseInt(value);
}
},
26.加入购物车(sessionStorage存储数据)
点击加入购物车时,会向后端发送API请求,但是该请求的返回值中data为null,所以我们只需要根据状态码code判断是否跳转到‘加入购物车成功页面’。
detail组件‘加入购物车’请求函数:
async addShopCar() {
try{
await this.$store.dispatch("addOrUpdateShopCart", {
skuId: this.$route.params.skuId,
skuNum: this.skuNum
});
//一些简单的数据,比如skuNum通过query传过去
//复杂的数据通过session存储,
//sessionStorage、localStorage只能存储字符串 sessionStorage.setItem("SKUINFO",JSON.stringify(this.skuInfo))
this.$router.push({name:'AddCartSuccess',query:{'skuNum':this.skuNum}})
}catch (error){
alert(error.message)
}
}
detail store对应代码
//将产品添加到购物车中
async addOrUpdateShopCart({commit},{skuId,skuNum}){
let result = await reqAddOrUpdateShopCart(skuId,skuNum)
if(result.code === 200){
return 'ok'
}else{
return Promise.reject(new Error('faile'))
}
}
其实这里当不满足result.code === 200条件时,也可以返回字符串‘faile’,自己在addShopCar中判断一下返回值,如果为‘ok’则跳转,如果为‘faile’(或者不为‘ok’)直接提示错误。当然这里出错时返回一个Promise.reject更加符合程序的逻辑。
跳转‘加入购物车成功页面’的同时要携带商品的信息。本项目只是传递的商品的一些标签属性,并没有传递商品的型号类别的信息,比如颜色、内存等信息,自己可以手动实现,比较简单。
当我们想要实现两个毫无关系的组件传递数据时,首相想到的就是路由的query传递参数,但是query适合传递单个数值的简单参数,所以如果想要传递对象之类的复杂信息,就可以通过Web Storage实现。
sessionStorage、localStorage概念:
sessionStorage:为每一个给定的源维持一个独立的存储区域,该区域在页面会话期间可用(即只要浏览器处于打开状态,包括页面重新加载和恢复)。
localStorage:同样的功能,但是在浏览器关闭,然后重新打开后数据仍然存在。
注意:无论是session还是local存储的值都是字符串形式。如果我们想要存储对象,需要在存储前JSON.stringify()将对象转为字符串,在取数据后通过JSON.parse()将字符串转为对象。
一个网站是有很多用户的,每个用户自己的购物车都不一样,所以每一个人的购物车页面展示的东西都不一样
当你以游客身份访问网站时:
每个用户需要一个uuidToken,用来验证用户身份,让服务器知道你是谁,但是这个请求函数没有参数,所以我们把uuidToken加在请求头中
根据api接口文档封装请求函数
export const reqGetCartList = () => {
return requests({
url:'/cart/cartList',
method:'GET'
})}
创建utils工具包文件夹,创建生成uuid的js文件,对外暴露为函数(记得导入uuid => npm install uuid
)
生成临时游客的uuid(随机字符串),每个用户的uuid不能发生变化,还要持久存储
import {v4 as uuidv4} from 'uuid'
//生成临时游客的uuid(随机字符串),每个用户的uuid不能发生变化,还要持久存储
export const getUUID = () => {
//1、判断本地存储是否由uuid
let uuid_token = localStorage.getItem('UUIDTOKEN')
//2、本地存储没有uuid
if(!uuid_token){
//2.1生成uuid
uuid_token = uuidv4()
//2.2存储本地
localStorage.setItem("UUIDTOKEN",uuid_token)
}
//当用户有uuid时就不会再生成
return uuid_token
}
用户的uuid_token定义在store中的detail模块
const state = {
goodInfo:{},
//游客身份
uuid_token: getUUID()
}
在request.js中设置请求头
import store from '@/store';
requests.interceptors.request.use(config => {
//config内主要是对请求头Header配置
//1、先判断uuid_token是否为空
if(store.state.detail.uuid_token){
//2、userTempId字段和后端统一
config.headers['userTempId'] = store.state.detail.uuid_token
}
//比如添加token
//开启进度条
nprogress.start();
return config;
})
28.购物车商品数量修改(函数节流)
这里有三个 *** 作,减一、加一、中间是修改输入框的数字,统一使用一个回调函数
传三个参数,第一个表示 *** 作类型、第二个是disNum(变化量)、第三个表示哪一个产品(身上有id)
-
+
handler函数,修改商品数量时,加入节流 *** 作。(防止用户快速点击,请求还没回来,导致输入框变为负数)
节流 *** 作:在规定时间范围内不会重复触发回调函数,只有大于这个时间间隔才会触发下一次
//修改某一个产品的个数[节流]
handler: throttle(async function (type, disNum, cart) {
//type:为了区分这三个元素
//disNum形参:+ 变化量(1) -变化量(-1) input最终的个数(并不是变化量)
//cart:哪一个产品【身上有id】
//向服务器发请求,修改数量
switch (type) {
//加号
case "add":
disNum = 1;
break;
case "minus":
//判断产品的个数大于1,才可以传递给服务器-1
//如果出现产品的个数小于等于1,传递给服务器个数0(原封不动)
disNum = cart.skuNum > 1 ? -1 : 0;
break;
case "change":
// //用户输入进来的最终量,如果非法的(带有汉字|出现负数),带给服务器数字零
if (isNaN(disNum) || disNum < 1) {
disNum = 0;
} else {
//属于正常情况(小数:取证),带给服务器变化的量 用户输入进来的 - 产品的起始个数
disNum = parseInt(disNum) - cart.skuNum;
}
// disNum = (isNaN(disNum)||disNum<1)?0:parseInt(disNum) - cart.skuNum;
break;
}
//派发action
try {
//代表的是修改成功
await this.$store.dispatch("addOrUpdateShopCart", {
skuId: cart.skuId,
skuNum: disNum,
});
//再一次获取服务器最新的数据进行展示
this.getData();
} catch (error) {}
}, 1000),
29.购物车状态修改和商品删除
这里唯一需要注意的是当store的action中的函数返回值data为null时,应该采用下面的写法(重点是if,else部分)
action部分:以删除购物车某个商品数据为例
//修改购物车某一个产品的选中状态
async reqUpdateCheckedById({commit},{skuId,isChecked}){
let result = await reqUpdateCheckedById(skuId,isChecked)
if(result.code === 200){
return 'ok'
}else{
return Promise.reject(new Error('fail'))
}
}
method部分:(重点是try、catch)
async reqUpdateCheckedById(cart,event){
let isChecked = event.target.checked ? 1 :0
try{
await this.$store.dispatch("reqUpdateCheckedById",{skuId:cart.skuId,isChecked:isChecked})
//修改成功,刷新数据
this.$store.dispatch()
}catch (error){
this.$store.dispatch("getCartList")
}
}
30.删除多个商品(actions扩展)
由于后台只提供了删除单个商品的接口,所以要删除多个商品时,只能多次调用actions中的函数。
我们可能最简单的方法是在method的方法中多次执行dispatch删除函数,当然这种做法也可行,但是为了深入了解actions,我们还是要将批量删除封装为actions函数。
actions扩展,这儿我们打印一下content(有印象的小伙伴应该记得12有提到)
context中是包含dispatch、getters、state的,即我们可以在actions函数中通过dispatch调用其他的actions函数,可以通过getters获取仓库的数据。
actions函数代码如下:
/删除选中的所有商品
deleteAllCheckedById({dispatch,getters}) {
getters.getCartList.cartInfoList.forEach(item => {
let result = [];
//将每一次返回值添加到数组中
result.push(item.isChecked === 1?dispatch('deleteCartById',item.skuId):'')
})
return Promise.all(result)
},
上面代码使用到了Promise.all
Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。
购物车组件method批量删除函数
//删除选中的所有商品
async deleteAllCheckedById(){
try{
await this.$store.dispatch('deleteAllCheckedById')
//删除成功,刷新数据
this.$store.dispatch("getCartList")
}catch (error){
alert(error)
}
},
修改商品的全部状态和批量删除的原理相同,直接贴代码。
actions
//修改购物车全部产品的选中状态
async updateAllChecked({dispatch,getters},flag){
let result = []
getters.getCartList.cartInfoList.forEach(item => {
result.push(dispatch('reqUpdateCheckedById',{skuId:item.skuId,isChecked:flag
}))
})
return Promise.all(result)
}
ShopCart购物车组件method批量删除函数
//修改全部商品的状态
async allChecked(event){
let flag = event.target.checked ? 1 : 0
console.log(flag)
try{
await this.$store.dispatch('updateAllChecked',flag)
//修改成功,刷新数据
this.$store.dispatch("getCartList")
}catch (error){
alert(error)
}
}
31.注册登录业务
app\src\store\user.js
//获取验证码
async getCode({ commit }, phone) {
//获取验证码的这个接口:把验证码返回,但是正常情况,后台把验证码发到用户手机上【省钱】
let result = await reqGetCode(phone);
if (result.code == 200) {
commit("GETCODE", result.data);
return "ok";
} else {
return Promise.reject(new Error("faile"));
}
},
//用户注册
async userRegister({ commit }, user) {
let result = await reqUserRegister(user);
if (result.code == 200) {
return "ok";
} else {
return Promise.reject(new Error("faile"));
}
},
app\src\pages\Register\index.vue
//获取验证码
async getCode() {
//简单判断一下---至少用数据
try {
//如果获取到验证码
const { phone } = this;
phone && (await this.$store.dispatch("getCode", phone));
//将组件的code属性值变为仓库中验证码[验证码直接自己填写了]
this.code = this.$store.state.user.code;
} catch (error) {}
},
//用户注册
async userRegister() {
const { phone, password, code } = this;
try {
phone &&
password &&
code &&
(await this.$store.dispatch("userRegister", {
phone,
password,
code,
}));
//注册成功跳转到登陆页面,并且携带用户账号
await this.$router.push({
path: "/login",
query: { name: this.phone },
});
} catch (error) {
alert(error);
}
},
32.登录(持久化储存token)
用户登录时,会向服务器发请求(组件派发action:userLogin),登录成功的话服务器就会返回一个token,将token储存在vuex里面
app\src\pages\Login\index.vue
//登录的回调函数
async userLogin() {
try {
//登录成功
const { phone, password } = this;
phone &&
password &&
(await this.$store.dispatch("userLogin", { phone, password }));
//登录的路由组件:看路由当中是否包含query参数,有:调到query参数指定路由,没有:调到home
// let toPath = this.$route.query.redirect||"/home";
this.$router.push("/home");
} catch (error) {
alert(error.message);
}
},
服务器返回token字段 ,将token保存在vuex里面
app\src\store\user.js
USERLOGIN(state, token) {
state.token = token;
},
//登录业务
async userLogin({ commit }, data) {
let result = await reqUserLogin(data);
console.log(result, 'result');
//服务器下发token,用户唯一标识符(uuid)
//将来经常通过带token找服务器要用户信息进行展示
if (result.code == 200) {
//用户已经登录成功且获取到token
commit("USERLOGIN", result.data.token);
//持久化存储token
// setToken(result.data.token);
return "ok";
} else {
return Promise.reject(new Error("faile"));
}
},
(token代表一个用户的身份,不同token获取不同的用户信息)
这时,我们只是将token保存在仓库,还需要将token添加到请求头,这样就可以获取用户信息
app\src\api\request.js
// 请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求之前做一些事情
request.interceptors.request.use((config) => {
if (store.state.detail.uuid_token) {
//请求头添加一个字段(userTempId):和后台老师商量好了
config.headers.userTempId = store.state.detail.uuid_token;
}
if (store.state.user.token) {
//请求头添加一个字段(token)
config.headers.token = store.state.user.token;
}
// 进度条开始
nprogress.start();
// config:配置对象,对象里面有一个属性很重要,headers请求头
return config;
});
当跳转到首页时,请求头已经添加token字段,所以发请求可以获取到用户信息,将用户信息展示在首页
app\src\pages\Login\index.vue
mounted() {
this.$store.dispatch("getFloorList")
// 获取用户信息在首页展示
this.$store.dispatch("getUserInfo")
},
app\src\store\user.js
GETUSERINFO(state, userInfo) {
state.userInfo = userInfo;
},
//获取用户信息
async getUserInfo({ commit }) {
let result = await reqUserInfo();
if (result.code == 200) {
//提交用户信息
commit("GETUSERINFO", result.data);
return 'ok';
} else {
return Promise.reject(new Error('faile'));
}
},
但是vuex储存数据不是持久化的 ,一旦刷新页面,vuex里面的数据就没了,即token也会清空,这样就没有token去发请求获取用户信息
因此我们需要持久化储存token
获取到token后,将token保存在本地(localStorage),点击刷新也可以在本地获取token
这儿我在utils文件夹下创建了token.js的文件中转一下
33.全局前置守卫(登录与未登录两种情况)
导航守卫
导航:表示路由正在发生改变,进行路由跳转守卫:可以把它当做“紫禁城护卫”全局守卫: 只要发生路由变化,守卫就可以监听到
举例子:紫禁城【皇帝、太后、妃子】,紫禁城大门守卫,全要排查
全局前置路由守卫(比较常用)
有三个参数
( path 前面肯定有/ 例子:/login /home)
已登录时的路由守卫
问题:
之前提到过,在首页之外的页面点击刷新,无法获取用户信息,因为其他页面没有派发action去获取用户信息,所以我们通过使用前置路由守卫来解决这个问题
解决方法:
在用户已经登录的情况下(访问的是非登录与注册),在每次路由跳转之前,判断一下是否拥有用户信息,如果没有用户信息,就先去派发action获取用户信息再放行
获取用户信息需要token,如果token失效,就需要重新登录获取并保存token
app\src\router\index.js
// 全局守卫: 前置守卫(路由跳转之前进行判断)
router.beforeEach(async (to, from, next) => {
// to:获取到要跳转到的路由信息
// from:获取到从哪个路由跳转过来来的信息
// next: next() 放行 next(path) 放行
// console.log(to);
// console.log(from);
let token = store.state.user.token;
let name = store.state.user.userInfo.name;
// 用户已经登录
if (token) {
// path 前面肯定有/ 例子:/login /home
// 用户已经登录还想去login【不能去,停留在首页】
if (to.path == '/login') {
next('/home')
} else {
//已经登陆了,访问的是非登录与注册
//登录了且拥有用户信息放行
if (name) {
next();
} else {
//登陆了且没有用户信息
//在路由跳转之前获取用户信息且放行
try {
await store.dispatch('getUserInfo');
next();
} catch (error) {
//token失效从新登录
await store.dispatch('userLogout');
next('/login')
}
}
}
} else {
// 未登录
next()
}
})
2.未登录时的路由守卫
用户未登录时,不能去交易、支付相关【pay|paysuccess】、个人中心
如果点击前往这些页面(例子:pay页面),首先会跳转到登录页面,并把未去成的信息存储在地址栏中,登陆之后就跳转到该页面(例子:pay页面)
app\src\router\index.js
else {
//未登录:不能去交易相关、不能去支付相关【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();
}
}
登录的回调函数里面,需要判断一下路由当中是否包含query参数
有query参数跳转到指定的路由,没有就跳转到home
app\src\pages\Login\index.vue
//登录的回调函数
async userLogin() {
try {
//登录成功
const { phone, password } = this;
phone &&
password &&
(await this.$store.dispatch("userLogin", { phone, password }));
//登录的路由组件:看路由当中是否包含query参数,有:调到query参数指定路由,没有:调到home
let toPath = this.$route.query.redirect||"/home";
this.$router.push(toPath);
} catch (error) {
alert(error.message);
}
},
34.路由独享守卫(定义在对应的路由身上)
如果想跳转支付页面,必须是从交易页面跳转过来的
app\src\router\routes.js
{
path: '/pay',
component: Pay,
meta: {
show: true
},
// 路由独享守卫
beforeEnter: (to, from, next) => {
// 去支付页面,必须是从交易页面而来
if (from.path == '/trade') {
next()
} else {
next(false)
// console.log('不111可以跳转');
}
}
},
如果要跳转交易页面,必须是从购物车跳转过来的
app\src\router\routes.js
{
path: '/trade',
component: Trade,
meta: {
show: true
},
// 路由独享守卫
beforeEnter: (to, from, next) => {
// 去交易页面,必须是从购物车页面而来
if (from.path == '/shopcart') {
next()
} else {
next(false)
// console.log('不111可以跳转');
}
}
},
35.组件内守卫(定义在对应的组件身上)
有三种情况:
1.beforeRouteEnter(在进入这个组件之前调用)
app\src\pages\PaySuccess\index.vue(进入Paysuccess前调用)
name: "PaySuccess",
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
if (from.path == "/pay") {
next();
} else {
next(false);
}
},
2.beforeRouteUpdate(该组件被复用时调用)
app\src\pages\PaySuccess\index.vue
beforeRouteUpdate(to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
console.log("12313131311313");
},
3.beforeRouteLeave(导航离开该组件时调用)
app\src\pages\PaySuccess\index.vue
beforeRouteLeave(to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
next();
},
36.路由懒加载
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
原本路由是这样的
import AddCartSuccess from '@/pages/AddCartSuccess'
{
path: '/addcartsuccess',
component: AddCartSuccess,
},
路由懒加载:
{
path: "/register",
component: () => import('@/pages/Register'),
},
37.项目上线
37.1项目打包
npm run build
map文件可以准确输出哪一行哪一列报错,但是对于项目上线无意义
在vue.config.js设置项目打包时去掉map文件
module.exports = {
// 打包时去掉map文件
productionSourceMap:false,
// 关闭ESLINT校验工具
lintOnSave: false,
//配置代理跨域
devServer: {
proxy: {
"/api": {
target: "http://39.98.123.211",
},
},
},
};
37.2服务器的购买与使用
我目前白嫖一个月的阿里云(天翼云也可以一个月,不过服务器没弄成功),需要的小伙伴记得每天早上10点去抢名额
37.3Xftp 和 Xshell项目上线需要下载这两个软件,小伙伴们自行下载
Xftp的使用
创建会话,把本地的dist复制到Xftp创建的会话中去
Xshell的使用
创建会话,切换到nginx目录
/ => etc => nginx
vim nginx.conf
INSERT进入编辑模式,添加以下内容
esc退出编辑,:wq保存编辑的内容(根据自己的API地址进行修改)
XSHELL7启动nginx服务器
systemctl start nginx.service
具体指令介绍:
#启动nginx服务
systemctl start nginx.service
#停止nginx服务
systemctl stop nginx.service
#重启nginx服务
systemctl restart nginx.service
#重新读取nginx配置(这个最常用, 不用停止nginx服务就能使修改的配置生效)
systemctl reload nginx.service
这样小伙伴们就可以通过自己的服务器的ip地址访问项目了
希望大家都能成功完成自己的项目
最后附带我的项目的ip地址
47.96.15.112
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)