当前位置:   article > 正文

前端尚硅谷尚品汇Vue项目笔记_尚品汇笔记

尚品汇笔记

目录

1,脚手架初始化项目

1.1,要求

1.2,脚手架使用

1.3,文件目录分析

2,项目的其他配置

2.1,项目运行,浏览器自动打开

2.2,关闭eslint校验工具(不关闭会有各种规范,不按照规范就会报错)

2.3,src文件夹配置别名,创建jsconfig.json,用@/代替src/,exclude表示不可以使用该别名的文件

3,组件页面样式

3.1,清除vue页面默认的样式

4,项目路由的分析

5,完成非路路由组件Header与Footer业务

 6,路由组件的搭建  vue-router (pages文件夹)

6.1,配置路由

6.2,总结

6.3,路由跳转方式

7,Footer组件的显示与隐藏

8,路由传参

8.1,路由的跳转

8.2,路由传参,参数有几种写法?

8.3,面试题

1:路由传递参数(对象写法),path是否可以结合params参数一起使用?

2:如何制定params参数可传可不传?

3:params参数可以传递也可以不传递,但是如果传递的是空串,如何解决?

4:路由组件能不能传递props数据?

9,编程式路由弹出NavigationDuplicated的警告错误

9.1,为什么编程式导航进行路由跳转的时候,就有这种警告错误?

9.2, 原因&方法

9.3,这种写法治标不治本,将来在别的组件中push|replace,编程式导航还是会有类似错误

9.4

10,将Home组件的静态组件拆分

11,定义全局组件

12,项目实时更新

13,AIPOST测试接口

14,二次封装axios

14.1,二次封装好处:

14.2,API 文件

14.2.1,导入axios  

14.2.2,用creat方法创造axios实例,

14.2.3配置请求和响应拦截器,用创造的实例instance去配置

14.2.4,对外暴露

14.3,nprogress进度条

 15,前端解决跨域问题-webpack

16,接口同一管理

17,Vuex状态管理库

17.1,vuex是什么?

17.2,vuex的基本使用

17.2.1 ,安装

17.2.2 ,引入

根目录创建store文件夹,文件夹下创建index.js

18,动态展示三级联动列表数据

18.1,发送请求

18.2,async和await

18.3,接收数据

18.4,展示数据

19,优化商品分类三级列表

20,loadsh插件防抖和节流

20.1,正常

20.2,防抖

20.3,节流

21,三级联动路由跳转与传参(编程式导航+事件委派)

22,mock数据

23,轮播图swiper

24,开发floor组件

25,把首页当中的轮播图拆分为一个公用全局组件

26,Search模块--getters(vuex store中的计算属性)

27,params参数

28,面包屑

29,全局事件总线

30,自定义事件

31,排序结构

31.1,考虑的问题:

31.2,总结search模块开发

32,分页器

32.1,为什么用分页器?

32.2,分页器的展示,需要哪些数据(条件)?

33,滚动条

34,undefined细节

35,商品详情放大镜

36,浏览器存储功能

37,购物车组件(临时游客身份)

38,加入购物车

39,修改购物车产品的数量

39.1,判断底部复选框是否勾选【全部产品都选中才勾选】

 39.2, 修改某一个产品购物车的个数,操作减号过快时,数量会变成负数,所以这里要用节流

40,购物车状态修改和商品删除

40.1,删除购物车某一个产品

40.2,修改购物车里某一个商品的选中状态

41,全选(actions扩展)

41.1,删除全部选中的产品

41.2, 修改购物车里全部产品的勾选状态

42,注册(表单验证)

42.1,ES6  const语法

42.2,获取验证码

42.3,注册

42.4,表单验证

43,登录(token)

43.1,登录

43.2,登录后展示用户信息

43.3,退出登录

44,全局守卫

45,交易界面(不使用vuex)

45.1,统一接收api

45.2,提交订单

46,qrcode和弹窗

46.1,弹窗

46.2,qrcode

47,个人中心(我的订单二级路由)

48,组件独享守卫

49,图片懒加载

50,路由懒加载

51,打包项目

51.1,打包  文件夹右键git bash ----npm run build

51.2,购买服务器

51.3,nginx

51.4,nginx服务器跑起来

52,组件通信方式

52.1,porps

52.2,自定义事件

52.3,全局事件总线$bus

52.4,Pubsub-js在React框架中使用比较多(发布于订阅)

52.5,Vuex

52.6,插槽

53,自定义事件

54,v-model(组件通信方式的一种)

55,sync属性修饰符

56,$attrs与$listeners

 57,$children与$parent

58,混入mixin

59,插槽


1,脚手架初始化项目

1.1,要求

1:node + webpack + VScode + 谷歌浏览器 + git

2:数组的方法 + promise + await + async + 模块化…

1.2,脚手架使用

vue init webpack 项目的名字

新建文件夹后,cmd ----- vue create 项目名称

1.3,文件目录分析

node_mudules文件夹:项目依赖文件夹

public文件夹:一般放置一些静态资源(图片),需要注意,放在public文件夹中的静态资源,webpack进行打包的时候,会原封不动打包到dist文件夹中,而不会当做一个模块打包到 js文件里。

src文件夹(程序员代码文件夹):

  • assets文件夹:一般放置静态资源(一般放置多个组件共用的静态资源),需要注意,放在在assets文件夹里面静态资源,在webpack打包的时候,webpack会把静态资源当做一个模块打包到js文件里。
  • component文件夹,一般放置的是非路由组件(全局组件)
  • App.vue:唯一的根组件,Vue当中的组件(.vue)
  •  main.js:程序入口文件,也是整个程序当中最先执行的文件
  • babel.config.js:配置文件(babel相关)
  • package.json:认为项目‘身份证’,记录项目叫做什么,项目当中有哪些依赖,项目怎么运行
  • package-lock.json:缓存性文件
  • README.md:说明性文件

2,项目的其他配置

2.1,项目运行,浏览器自动打开

Package.json文件

  1. "scripts": {
  2. "serve": "vue-cli-service serve --open",
  3. "build": "vue-cli-service build",
  4. "lint": "vue-cli-service lint"
  5. },

2.2,关闭eslint校验工具(不关闭会有各种规范,不按照规范就会报错)

根目录下创建vue.config.js,进行配置

  1. module.exports = {
  2. lintOnSave: false,
  3. }

2.3,src文件夹配置别名,创建jsconfig.json,用@/代替src/,exclude表示不可以使用该别名的文件

  1. {
  2. "compilerOptions": {
  3. "target": "es5",
  4. "module": "esnext",
  5. "baseUrl": "./",
  6. "moduleResolution": "node",
  7. "paths": {
  8. "@/*": [
  9. "src/*"
  10. ]
  11. },
  12. "lib": [
  13. "esnext",
  14. "dom",
  15. "dom.iterable",
  16. "scripthost"
  17. ]
  18. }
  19. }

3,组件页面样式

组件页面的样式使用的是less样式,浏览器不识别该样式,需要下载相关依赖

npm install --save less less-loader@5

如果想让组件识别less样式,则在组件中设置

<script scoped lang="less">

3.1,清除vue页面默认的样式

vue是单页面开发,我们只需要修改public下的index.html文件

<link rel="stylesheet" href="reset.css">

4,项目路由的分析

vue-router

前端所谓路由:KV键值对

key:URL(地址栏中的路径)

value:响应的路由组件

注意:项目上中下结构

路由组件:

Home首页路由组件、Search路由组件、login登录路由、Refister注册路由

非路由组件:

Header【首页、搜索页】

Footer【在首页、搜索页】,但是在登录|注册页面是没有

5,完成非路路由组件Header与Footer业务

在开发项目时:

1:书写静态页面(HTML + CSS)

2:拆分组件

3:获取服务器的数据动态展示

4:完成响应的动态业务逻辑

创建组件的时候,组件结构 + 组件样式 + 图片资源

使用组件的步骤(非路由组件)

-创建或者定义

-引入

-注册

-使用

6,路由组件的搭建  vue-router (pages文件夹)

路由组件应该有四个:Home,Search,Login,Register

-components文件夹:经常放置的非路由组件 (共用全局组件)

pages|views文件夹:经常放置路由组件

6.1,配置路由

npm install--savevue-router

创建router文件夹,创建index.js进行路由配置,最终在main.js中引入注册

6.2,总结

路由组件和非路由组件区别:

1,-components文件夹:经常放置的非路由组件 (共用全局组件), pages|views文件夹:经常放置路由组件

2,非路由组件一般以标签的形式使用,路由组件一般需要在router文件夹中进行注册(使用的即为组件的名字)

3,在main.js注册玩路由,所有的路由和非路由组件身上都会拥有$router $route属性

$router:一般进行编程式导航进行路由跳转【push|replace】

$route: 一般获取路由信息(query 路径 params等)

6.3,路由跳转方式

路由的跳转就两种形式:声明式导航(router-link:务必要有to属性)

                                     编程式导航$route.push||replace

编程式导航更好用:因为可以书写自己的业务逻辑

7,Footer组件的显示与隐藏

footer在登录注册页面是不存在的,所以要隐藏,v-if 或者 v-show

这里使用v-show,因为v-if会频繁的操作dom元素消耗性能,v-show只是通过样式将元素显示或隐藏,

9.1我们可以根据组件身上的$route获取当前路由的信息,通过路由路径判断Footer显示与隐藏

9.2配置路由的时候,可以给路由配置元信息meta,路由需要配置对象,它的key不能乱写

在路由的原信息中定义show属性,用来给v-show赋值,判断是否显示footer组件

8,路由传参

8.1,路由的跳转

路由的跳转就两种形式:

声明式导航(router-link:务必要有to属性)

编程式导航$route.push||replace(编程式导航更好用:因为可以书写自己的业务逻逻辑)

8.2,路由传参,参数有几种写法?

query、params两个属性可以传递参数

query参数:不属于路径当中的一部分,类似于ajax中的queryString /home?k=v&kv=

                    get请求,地址栏表现为 /search?k1=v1&k2=v2   不需要占位

query参数对应的路由信息 path: "/search"

params参数:属于路径当中的一部分,需要注意,在配置路由的时候,需要占位 ,地址栏表现为 /search/v1/v2

params参数对应的路由信息,在routes.js文件里要修改为path: "/search/:keyword" 这里的/:keyword就是一个params参数的占位符

Hearder文件里给搜索键绑定click事件

  1. methods: {
  2. // 搜索按钮的回调函数,需要向search路由进行跳转
  3. goSearch(){
  4. // 路由传递参数:
  5. // 第一种:字符串形式,params参数:this.keyword(传参数前,要在原来的路径最后加/),需要占位,在路由的index.js文件里path: "/Search/:keyword", query参数:this.keyword.toUpperCase()
  6. // this.$router.push("/Search/" + this.keyword+"?k="+this.keyword.toUpperCase());
  7. // 第二种:模板字符串
  8. // this.$router.push(`/search/${this.keyword}?k=${this.keyword.toUpperCase}`)
  9. // 第三种:对象,需要给路由命名
  10. this.$router.push({name:"search",params:{keyword:''||undefined},query:{k:this.keyword.toUpperCase}})) //如果在搜索华为,则路径是127.0.0.1:8000/#/search/华为
  11. },
  12. }

routes.js文件里

  1. {
  2. path: "/search/:keyword?", //要先占位
  3. component: ()=>import("@/pages/Search"),
  4. meta: { show: true },
  5. name: "search",
  6. // 路由组件能不能传递props数据?
  7. // 布尔值写法:params
  8. // props:true,
  9. // 对象写法:额外给路由组件传递一些props
  10. // props:{a:1,b:2}
  11. // 函数写法:可以params参数、query参数,通过props传递给路由组件
  12. props: ($route) => ({ keyword: $route.params.keyword, k: $route.query.k })
  13. },

8.3,面试题

1:路由传递参数(对象写法),path是否可以结合params参数一起使用?

答:路由跳转传参的时候,对象的写法可以是name、path形式,但是path这种写法不能与params参数一起使用

2:如何制定params参数可传可不传?

1,比如:配置路由的时候,占位了(params参数),但是路由跳转的时候就不传递,路径会出现问题

路径从home跳转至search本该是这样 http://localhost:8080/#/search?k=QWE

但是现在没传参数看不到search     http://localhost:8080/#/?k=QEW

2,如何指定params参数可以传递或者不传递,在配置路由的时候,在占位的后面加上问号【params可以传递或者不传递】

这样得到的路径就是   http://localhost:8080/#/search?k=QWE

3:params参数可以传递也可以不传递,但是如果传递的是空串,如何解决?

this.$route.push({name:"search",params:{keyword:''},query:{k:this.keyword.toUpperCase}}),会导致路径看不到search

使用undefined解决   this.$route.push({name:"search",params:{keyword:''||undefined},query:{k:this.keyword.toUpperCase}})

4:路由组件能不能传递props数据?

可以的:三种写法

// 路由组件能不能传递props数据?

// 布尔值写法:params

// props:true,

// 对象写法:额外给路由组件传递一些props

// props:{a:1,b:2}

// 函数写法:可以params参数、query参数,通过props传递给路由组件

props:($route)=>({keyword:$route.params.keyword,k:$route.query.k})

9,编程式路由弹出NavigationDuplicated的警告错误

编程式路由跳转到当前路由(参数不变),多次执行会弹出NavigationDuplicated的警告错误

--路由跳转的两种形式:编程式导航,声明式导航

--声明式导航没有这类问题,因为vue-router底层已经处理好了

9.1,为什么编程式导航进行路由跳转的时候,就有这种警告错误?

"vue-router":3.5.3:最新的vue-router引入promise

9.2, 原因&方法

原因:push是一个promise,promise需要传递成功和失败两个参数,我们的push中没有传递。

方法:this.$router.push({name:‘Search’,params:{keyword:“…”||undefined}},()=>{},()=>{})后面两项分别代表执行成功和失败的回调函数。

9.3,这种写法治标不治本,将来在别的组件中push|replace,编程式导航还是会有类似错误

 push是VueRouter.prototype的一个方法,在router中的index重写该方法即可(看不懂也没关系,这是前端面试题)

9.4

this:当前组件实例VueComponent(search)

this.$router属性:是一个对象,当前这个属性,属性值VueRouter类的一个实例,当在入口文件注册路由的时候,给组件实例添加$router|$route属性

push:VueRouter类的一个实例,VueRouter实例上没有push方法,在VueRouter的原型对象上才有

let result = this.$router.push({name:"Search",query:{keyword:this.keyword}})

console.log(result)

执行一次上面代码:

多次执行出现警告:

  1. // 先把VueRouter原型对象的push,先保存一份
  2. let originPush = VueRouter.prototype.push;
  3. let originReplace = VueRouter.prototype.replace;
  4. // 重写push|replace
  5. // 第一个参数:告诉原来push方法,你往哪里跳(传递哪些参数)
  6. // 第二个参数:成功回调
  7. // 第三个参数:失败回调
  8. VueRouter.prototype.push = function(location,resolve,reject){ //resolve,reject成功与失败
  9. if(resolve && reject){
  10. // originPush(),不可以这么调用,因为这样调用,this是window
  11. // call与apply的区别
  12. // 相同点:都可以调用函数一次,都可以篡改函数的上下文this一次
  13. // 不同点:call与apply传递参数:call传递参数用逗号隔开;apply方法执行,传递数组
  14. originPush.call(this,location,resolve,reject); //这样调用,this是VueRouter类的实例
  15. }else{
  16. originPush.call(this,location,()=>{},()=>{});
  17. }
  18. }
  19. VueRouter.prototype.replace = function(location,resolve,reject){
  20. if(resolve && reject){
  21. originReplace.call(this,location,resolve,reject);
  22. }else{
  23. originReplace.call(this,location,()=>{},()=>{});
  24. }
  25. }

10,将Home组件的静态组件拆分

10.1 静态页面(样式)

10.2 拆分静态组件

10.3 发请求获取服务器数据进行展示

10.4 开发动态业务

拆分组件:结构+样式+图片资源

一共要拆分为七个组件

  1. <template>
  2. <div>
  3. <!-- 三级联动全局组件:三级联动已经注册为全局组件,因此不需要再引入 -->
  4. <TypeNav />
  5. <ListContainer />
  6. <Recommend />
  7. <Rank />
  8. <Like />
  9. <!-- Floor这个组件,不是自己在组件内部发请求的,数据是父组件给的 -->
  10. <Floor v-for="(floor,index) in floorList" :key="floor.id" :list="floor" />
  11. <Brand />
  12. <!-- <button @click="add">+1</button>
  13. <span>仓库的数据{{count}}</span>
  14. <button>-1</button> -->
  15. </div>
  16. </template>
  17. <script>
  18. // 引入其余的组件
  19. import ListContainer from "@/pages/Home/ListContainer";
  20. import Recommend from "@/pages/Home/Recommend";
  21. import Rank from "@/pages/Home/Rank";
  22. import Like from "@/pages/Home/Like";
  23. import Floor from "@/pages/Home/Floor";
  24. import Brand from "@/pages/Home/Brand";
  25. import { mapState } from "vuex";
  26. export default {
  27. name: "",
  28. components: {
  29. ListContainer,
  30. Recommend,
  31. Rank,
  32. Like,
  33. Floor,
  34. Brand,
  35. },

11,定义全局组件

11.1,全局组件的配置都需要在main.js中配置

11.2,全局组件可以在任一页面中直接使用,不需要导入声明

由于三级联动,在Home,Search,Detail,把三级联动注册为全局组件

好处:只需要在main.js注册一次,就可以在项目任意地方使用

  1. // 三级联动组件--全局组件
  2. import TypeNav from '@/components/TypeNav'
  3. // 第一个参数:全局组件的名字 第二个参数:哪一个组件
  4. Vue.component(TypeNav.name,TypeNav);

home组件如下

  1. <template>
  2. <div>
  3. <!-- 三级联动全局组件:三级联动已经注册为全局组件,因此不需要再引入 -->
  4. <TypeNav />
  5. <ListContainer />
  6. </div>
  7. </template>
  8. <script>
  9. // 引入其余的组件
  10. import ListContainer from "@/pages/Home/ListContainer";
  11. </script>

12,项目实时更新

根目录下vue.config.js文件设置

  1. module.exports = {
  2. //关闭eslint
  3. lintOnSave: false,
  4. devServer: {
  5. // true 则热更新,false 则手动刷新,默认值为 true
  6. inline: true,
  7. // development server port 8000
  8. port: 8001,
  9. }
  10. }

13,AIPOST测试接口

--刚刚经过工具测试,接口没有问题

--如果服务器返回的数据code字段200,代表服务器返回数据成功

--整个项目,接口前缀都有/api字样

Config.js文件下

  1. module.exports = {
  2. // transpileDependencies: true,
  3. productionSourceMap:false,
  4. lintOnSave: false,
  5. // 代理跨域
  6. devServer: {
  7. //代理服务器解决跨域
  8. proxy: {
  9. //会把请求路径中的/api换为后面的代理服务器
  10. // 前端在发请求的时候,路径中有/api的请求,代理服务器会工作,找 http://gmall-h5-api.atguigu.cn 这台服务器要数据,服务器与服务器之间没有跨域问题,浏览器之间才有
  11. '/api': {
  12. //提供数据的服务器地址
  13. target: 'http://gmall-h5-api.atguigu.cn',
  14. }
  15. },
  16. },
  17. }

14,二次封装axios

axios是一个基于promise的网络请求库,可以方便我们进行网络请求。

14.1,二次封装好处:

便于我们更好的管理我们的接口,不至于请求接口很多的情况下,出现混乱。

请求拦截器,响应拦截器:

请求拦截器:在发请求之前可以处理业务

响应拦截器:服务器数据返回以后,处理数据

14.2,API 文件

npm install -- save axios

在根目录src文件夹里创建 API 文件夹,一般都是用来放axios请求,创建request.js文件。

14.2.1,导入axios  

import axios from "axios";

14.2.2,用creat方法创造axios实例,

这里是const requests,那最后要暴露的也是 requests。

  1. const requests = axios.create({
  2. // 配置对象
  3. // 基础路径,发请求的时候,路径当中会出现api
  4. baseURL:"/api",
  5. timeout:5000, // 代表请求超时的时间5s
  6. });

14.2.3,配置请求和响应拦截器,用创造的实例instance去配置

  1. requests.interceptors.request.use((config)=>{
  2. //在请求发送前做的操作xxxxxxx;
  3. //config内主要是对请求头Header配置,比如添加token
  4. return config;
  5. });
  1. requests.interceptors.response.use((res)=>{
  2. //res:实质就是项目中发请求,服务器返回的数据
  3. //成功的回调函数:服务器响应数据回来以后,响应拦截器可以检测到,可以做一些事情
  4. return res.data;
  5. },(error)=>{
  6. alert(error.message); //响应失败的回调函数
  7. return Promise.reject(new Error('faile'));
  8. }
  1. if (error?.response.status === 401) {
  2. /** * 1. 获取当前出现401的页面路径(目的:成功登陆之后,回到上次访问的页面)
  3. * 2. 跳回登录页带上401页面的地址 */
  4. const redirectUrl = router.currentRoute.value.fullPath
  5. router.replace(`/login?redirectUrl=${redirectUrl}`) }
  6. });

14.2.4,对外暴露

export default requests;

14.3,nprogress进度条

// 进度条开始 nprogress.start();

// 进度条结束 nprogress.done();

如果要修改进度条的颜色,直接在nprogress.css修改bar

request.js文件全部代码

  1. // 对于axios进行二次封装
  2. import axios from "axios";
  3. // 引入进度条
  4. import nprogress from "nprogress";
  5. // 引入进度条样式  如果要修改进度条的颜色,直接在nprogress.css修改bar
  6. import "nprogress/nprogress.css"
  7. // start:进度条开始  done:进度条结束
  8. // 1:利用axios对象的方法create,去创建一个axios实例
  9. // 2:request就是axios,只不过稍微配置一下
  10. const requests = axios.create({
  11. // 配置对象
  12. // 基础路径,发请求的时候,路径当中会出现api
  13. baseURL:"/api",
  14. timeout:5000, // 代表请求超时的时间5s
  15. });
  16. // 请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求发出去之前做一些事情
  17. requests.interceptors.request.use((config)=>{
  18. // config:配置对象,对象里面有一个属性很重要,headers请求头
  19. // 进度条开始
  20. nprogress.start();
  21. return config;
  22. });
  23. // 响应拦截器
  24. requests.interceptors.response.use((res)=>{
  25. // 成功的回调函数:服务器响应数据回来以后,响应拦截器可以检测到,可以做一些事情
  26. // 进度条结束
  27. nprogress.done();
  28. return res.data;
  29. },(error)=>{
  30. // 响应失败的回调函数
  31. return Promise.reject(new Error('faile'));
  32. });
  33. // 对外暴露 这里暴露的不是axios本身,是进行二次封装的axios实例
  34. export default requests;

 15,前端解决跨域问题-webpack

在根目录下的vue.config.js中配置,proxy为通过代理解决跨域问题。

  1. module.exports = {
  2. // transpileDependencies: true,
  3. productionSourceMap:false,
  4. lintOnSave: false,
  5. // 代理跨域
  6. devServer: {
  7. //代理服务器解决跨域
  8. proxy: {
  9. //会把请求路径中的/api换为后面的代理服务器
  10. // 前端在发请求的时候,路径中有/api的请求,代理服务器会工作,找 http://gmall-h5-api.atguigu.cn 这台服务器要数据,服务器与服务器之间没有跨域问题,浏览器之间才有
  11. '/api': {
  12. //提供数据的服务器地址
  13. target: 'http://gmall-h5-api.atguigu.cn',
  14. }
  15. },
  16. },
  17. }

16,接口同一管理

项目小,可以在组件的声明周期函数中发请

项目大:axios.get('xxx');

16.1,封装所有请求:在 api 文件中创建index.js文件,引入封装的axios实例

  1. // 当前这个模块:API进行统一管理
  2. import requests from "./request";
  3. // 三级联动接口
  4. // /api/product/getBaseCategoryList get 无参数
  5. // 将来在别的模块中发请求,比如APP.vue或者其他组件发请求,只需要在这对外暴露一个函数,别的模块可以看到,调用就可以发送请求,把服务器返回数据给别的模块用
  6. // 写一个函数通过封装的axios发送请求,只要是需要请求相同api接口地方的直接调用函数就可以
  7. // 发请求:axios发请求返回的结果是Promise对象
  8. export const reqCategoryList = ()=>requests({url:'/product/getBaseCategoryList',method:'get'});
  9. // 函数体一执行就可以发请求,用axios请求函数的形式,传入请求的地址
  10. // 需要把服务器返回的结果return返回给模块使用,如果没写return,则返回的结果是undefined
  11. // return requests({url:'/product/getBaseCategoryList',method:'get'}) //baseURL已经加了api,所以这里的地址不需要加api了

16.2, 将每个请求分装为一个函数并暴露出去,通过封装的axios发送请求,将来在别的模块中比,如APP.vue或者其他组件发请求,只需要在这对外暴露一个函数,别的模块可以看到,调用就可以发送请求,把服务器返回数据给别的模块用。

函数体一执行就可以发请求,用axios请求函数的形式,传入请求的地址

export const reqCategoryList = () => {

        return requests ({ url:'/product/getBaseCategoryList',method:'get'});

}

当有组件需要使用相关请求,即前面二次封装axios请求时定义的nprogress.start和ngprogress.done,则在store文件夹的模块中,引入函数并调用。

import {reqCategoryList} from '@/api';

17,Vuex状态管理库

17.1,vuex是什么?

是一个插件,状态管理库,可以集中式管理项目中组件公用的数据

并不是全部的项目都需要vuex,项目小不需要,如果项目大,组件和数据多,数据维护费劲才需要

vuex有几大核心的概念:state,mutations,actions,modules

17.2,vuex的基本使用

17.2.1 ,安装

npm install vuex@next --save

17.2.2 ,引入

根目录创建store文件夹,文件夹下创建index.js

  1. import Vue from 'vue';
  2. import Vuex from 'vuex';
  3. // 需要使用插件一次
  4. Vue.use(Vuex);
  5. // state:仓库存储数据的地方
  6. /* const state = {
  7. count:1
  8. };
  9. // mutations:修改state的唯一手段
  10. const mutations = {
  11. ADD(state){
  12. state.count++;
  13. }
  14. };
  15. // actions:处理action,可以书写自己的业务逻辑,也可以处理异步
  16. const actions = {
  17. // 这里可以书写业务逻辑,但是不能修改state
  18. add({commit}){
  19. commit("ADD");
  20. }
  21. };
  22. // getters:理解为计算属性,用于简化仓库数据,让组件获取仓库的数据更加方便
  23. const getters = {}; */
  24. // 引入小仓库
  25. import home from './home';
  26. import search from './search';
  27. import detail from './detail';
  28. import shopCart from './shopCart';
  29. import user from './user'
  30. import trade from './trade';
  31. // 对外暴露store类的一个实例
  32. export default new Vuex.Store({
  33. /* state,
  34. mutations,
  35. actions,
  36. getters */
  37. modules:{
  38. home,
  39. search,
  40. detail,
  41. shopCart,
  42. user,
  43. trade
  44. }
  45. })

main.js中引入并注册

  1. // 引入仓库
  2. import store from '@/store';
  3. new Vue({
  4. render: h => h(App),
  5. // 注册仓库:组件实例的身上会多个属性$store属性
  6. store,
  7. }).$mount('#app')

18,动态展示三级联动列表数据

18.1,发送请求

如果前端应用和后端 API 服务器没有运行在同一主机上,那就需要通过vue.config.js 中的devServer.proxy 选项来配置,解决跨域问题,将 API 请求代理到 API 服务器,代理是前端和后端的中间商。

请求到 /api/xxx,那就会被代理请求到 http://gmall-h5-api.atguigu.cn/api/xxx ,现在我们要请求到的地址是 /api/product/getBaseCategoryList

  1. // const { defineConfig } = require('@vue/cli-service')
  2. module.exports = {
  3. // transpileDependencies: true,
  4. lintOnSave: false,
  5. // 代理跨域
  6. devServer: {
  7. //代理服务器解决跨域
  8. proxy: {
  9. //会把请求路径中的/api换为后面的代理服务器
  10. // 前端在发请求的时候,路径中有/api的请求,代理服务器会工作,找 http://gmall-h5-api.atguigu.cn 这台服务器要数据,服务器与服务器之间没有跨域问题,浏览器之间才有
  11. '/api': {
  12. //提供数据的服务器地址
  13. target: 'http://gmall-h5-api.atguigu.cn',
  14. }
  15. },
  16. },
  17. }

vuex核心的概念:state,mutations,actions,modules

state只能由mutations修改,actions获取到的数据用commit提交给mutations,{commit}是解构赋值的结果,dispatch是异步操作,用action接收,actions出来的是promise,需要async,因为三级联动是公共组件,home要使用的话,就要挂载,使用他的数据.

 要求当TypeNav组件挂在完毕就是mounted一执行,就得发请求,获取服务器数据和展示数据。

  1. <script>
  2. import { mapState } from "vuex";
  3. export default {
  4. name: "TypeNav",
  5. // 组件挂在完毕:可以向服务器发送请求
  6. mounted() {
  7. // 通知Vuex发请求,获取数据,存储于仓库中
  8. /* 因为用了模块化开发,dispatch方法需要传入两个参数,第一个参数指定哪个模块中的哪个方法,这里是'home/categoryList',
  9. 这里只需要发送ajax请求,不需要传值,所以不需要写第二个参数 */
  10. this.$store.dispatch("categoryList"); //派发一个action,起名categoryList,要去找到响应的仓库home,去书写action
  11. },
  12. };

 Store--home.js代码如下

  1. import {reqCategoryList} from '@/api';
  2. const actions = {
  3. // 通过API里面的接口函数调用,向服务器发请求,获取服务器数据,所以要引入之前封装的reqCategoryList并调用
  4. async categoryList({commit}){ //这里的{commit}=context.commit
  5. let result = await reqCategoryList(); //这里要拿到Promise的成功的结果,所以要价async和await
  6. if(result.code===200){ //如果code=200成功,就要修改state里面的数据
  7. commit("CATEGORYLIST",result.data); //提交mutation,起名CATEGORYLIST,提交的数据是result.data
  8. // commit 操作会传递两个参数 type,payload,type表示mutation中的名字,payload(载荷)表示参数,这时候要去书写mutations
  9. }
  10. state.categoryList.length = 16;
  11. }
  12. };
由于上面代码输出result得到的一个Promise对象,为了拿到Promise成功的结果,需要在函数前面加 await,同时在前面加上async,这两个是同时使用的。

18.2,async和await

async await使用
如果我们没有封装请求api,而是直接调用axios,就不需要使用async await。
案例:我们将一个axios请求封装为了函数,我们在下面代码中调用了该函数:

import {reqCateGoryList} from '@/api'
export default {
    actions:{
        categoryList(){
            let result =  reqCateGoryList()
            console.log(result)
        }
    }
}
浏览器结果

返回了一个promise,证明这是一个promise请求,但是我们想要的是图片中的data数据。
没有将函数封装前我们都会通过then()回调函数拿到服务器返回的数据,现在我们将其封装了,依然可以使用then获取数据,代码如下

actions:{
        categoryList(){
            let result =  reqCateGoryList().then(
                res=>{
                console.log("res")
                console.log(res)
                return res
                }
            )
            console.log("result")
            console.log(result)
        }
    }
结果

由于我们的promis是异步请求,我们发现请求需要花费时间,但是它是异步的,所有后面的console.log(“result”);console.log(result)会先执行,等我们的请求得到响应后,才执行console.log(“res”);console.log(res),这也符合异步的原则,但是我们如果在请求下面啊执行的是将那个请求的结果赋值给某个变量,这样就会导致被赋值的变量先执行,并且赋值为undefine,因为此时promise还没有完成。

所以我们引入了async await,async写在函数名前,await卸载api函数前面。await含义是async标识的函数体内的并且在await标识代码后面的代码先等待await标识的异步请求执行完,再执行。这也使得只有reqCateGoryList执行完,result 得到返回值后,才会执行后面的输出操作。

   async categoryList(){
            let result = await reqCateGoryList()
            console.log("result")
            console.log(result)
        }
结果

————————————————
版权声明:本文为CSDN博主「毛毛虫呜呜」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_43424325/article/details/121684101

这时候就拿到了服务器返回的结果,就是三级联动的数据。code = 200代表成功,那就要修改仓库里state中的数据,所以我们要解构出commit,提交mutations。
commit 操作会传递两个参数 type,payload,type表示mutation中的名字,payload(载荷)表示参数,这时候要去书写mutations。
  1. mutations:{
  2. // 每一项都是一个函数,可以声明两个形参
  3. //第一个参数是必须的,表示当前的state。在使用时不需要传入
  4. //第二个参数是可选的,表示载荷,是可选的。在使用时要传入的数据
  5. mutation名1function(state [, 载荷]) {
  6. },
  7. mutation名2function(state [, 载荷]) {
  8. }
  9. }
书写mutations时也要在state书写起始状态下state的起始值,是个空数组或者空对象,取决于服务器返回的形式。

18.3,接收数据

开发者工具仓库home 就得到了数据,所以要在组件当中TypeNav拿到仓库数据进行展示。首先引入辅助函数mapState,把它影射为组件实例身上的属性computed。

state中获取数据的方式:

1,直接使用: this.$store.state.xxx;

2,map辅助函数:computed:{

        ...mapState({属性名:属性值});       //对象写法

        ...mapState(['xxx']) ;      //数组写法

}

  1. computed: mapState({
  2. count: 'count', // string 映射 this.count 为 store.state.count的值
  3. // 箭头函数可使代码更简练
  4. name: (state) => state.name, // function 映射 this.name 为 store.state.name的值
  5. })
  6. }

TypeNav---index.vue代码如下

  1. <script>
  2. import { mapState } from "vuex";
  3. export default {
  4. name: "TypeNav",
  5. // 组件挂在完毕:可以向服务器发送请求
  6. mounted() {
  7. // 通知Vuex发请求,获取数据,存储于仓库中
  8. /* 因为用了模块化开发,dispatch方法需要传入两个参数,第一个参数指定哪个模块中的哪个方法,这里是'home/categoryList',这里只需要发送ajax请求,不需要传值,所以不需要写第二个参数 */
  9. this.$store.dispatch("categoryList"); //派发一个action,起名categoryList,要去找到响应的仓库home,去书写action
  10. },
  11. },
  12. computed: {
  13. ...mapState({
  14. // 对象写法,属性名:属性值
  15. // 右侧需要的是一个函数,当使用这个计算属性categoryList的时候比如插值语法{{categoryList}},右侧函数会立刻执行一次
  16. // 会注入一个参数state,这个state,其实即为大仓库中的数据,大仓库是一个数组,这里包含home和search,而home下面的categoryList是可以拿到的
  17. categoryList: (state) => {
  18. // console.log(state);
  19. return state.home.categoryList; //这个函数返回的结果就是计算属性的属性值
  20. // 这时候相应的组件TypeNav就可以拿到数据
  21. },
  22. }),
  23. },
  24. </script>

18.4,展示数据

这时候相应的组件TypeNav就拿到数据,要去template将数据展示出来。

由于拿到的数据是三层结构,所以要遍历三层,遍历的是计算出来的属性categoryList

  1. <div class="sort">
  2. <div class="all-sort-list2" >
  3. <!-- 遍历的是计算出来的属性categoryList -->
  4. <div class="item" v-for="(c1,index) in categoryList" :key="c1.categoryId">
  5. <h3>
  6. <a href="">{{c1.categoryName}}</a>
  7. </h3>
  8. <div class="item-list clearfix">
  9. <div class="subitem" v-for="(c2,index) in c1.categoryChild" :key="c2.categoryId">
  10. <dl class="fore">
  11. <dt>
  12. <a href="">{{c2.categoryName}}</a>
  13. </dt>
  14. <dd>
  15. <em v-for="(c3,index) in c2.categoryChild" :key="c3.categoryId">
  16. <a href="">{{c3.categoryName}}</a>
  17. </em>
  18. </dd>
  19. </dl>
  20. </div>
  21. </div>
  22. </div>
  23. </div>
  24. </div>

19,优化商品分类三级列表

由于home和search切换过程中需要执行无数次,所以放到APP组件,只会执行一次,this.$store.dispatch("categoryList");

代码不能放在main.js文件当中,因为main.js不是组件,没有$store,也没有this.

  1. mounted() {
  2. //派发一个action,起名categoryList,获取商品分诶的三级列表数据。由于home和search切换过程中需要执行无数次,所以放到APP组件,只会执行一次
  3. // 以下这个代码不能放在main.js文件当中,因为main.js不是组件,没有$store,也没有this
  4. this.$store.dispatch("categoryList");
  5. },

TypeNav组件代码如下

  1. mounted() {
  2. // 通知Vuex发请求,获取数据,存储于仓库中
  3. /* 因为用了模块化开发,dispatch方法需要传入两个参数,第一个参数指定哪个模块中的哪个方法,这里是'home/categoryList',
  4. 这里只需要发送ajax请求,不需要传值,所以不需要写第二个参数 */
  5. // this.$store.dispatch("categoryList"); //派发一个action,起名categoryList,要去找到响应的仓库home,去书写action
  6. // (由于home和search切换过程中需要执行无数次,所以放到APP组件,只会执行一次)
  7. // 当组件挂载完毕,让全部商品分类的show变为false
  8. // 如果不是Home路由组件,就将TypeNav隐藏
  9. if (this.$route.path != "/Home") {
  10. this.show = false;
  11. }
  12. },

由于路径跳转的时候没有search字样,所以要在路由index.js文件里给search路由起名,

name:“search”

整理路由参数时, let location = { name: "search" }; query = {...}

路由跳转时 this.$router.push({name:“search",query:{categoryName:'xxx'}})

20,loadsh插件防抖和节流

20.1,正常

事件触发非常频繁,而且每一次的触发,回调函数都要去执行(如果时间很短,而回调函数内部有计算,那么很可能出现浏览器卡顿)

//正常情况(用户慢慢的操作):鼠标进入,每一个一级分类h3,都会触发鼠标进入事件

//非正常情况(用户操作很快):本身全部的一级分类都应该触发鼠标进入事件,但是经过测试,只有部分h3触发了

//就是由于用户行为过快,导致浏览器反应不过来。如果当前回调函数中有一些大量业务,

20.2,防抖

前面的所有的触发都被取消,最后一次执行在规定的时间之后才会触发,也就是说如果连续快速的触发只会执行一次

例子:输入框搜索 输入完内容之后 一秒后才发送一次请求

使用插件lodash

lodash里面有自己封装的函数,可以用于防抖于节流:

解决: lodash插件或者引用js文件,封装函数的防抖与节流业务(闭包+延迟器)   npm i lodash  npm i --save lodash

import

 _.debounce(func, [wait=0], [options=])

20.3,节流

在规定的间隔时间范围内不会重复触发回调,只有大于这个时间间隔才会触发回调,把频繁触发变为少量触发

例子:计数器限定一秒内不管用户点击按钮多少次,数值只能加一

解决: _throttle()

_.throttle(func, [wait=0], [options=])   这一整个是个函数

throttle回调函数别用箭头函数,可能出现上下文this

TypeNav代码如下

  1. import { mapState } from "vuex";
  2. methods: {
  3. // 鼠标进入,修改响应式数据currentIndex属性
  4. /* changeIndex(index){
  5. // index:鼠标移上某一个一级分类的元素的索引值
  6. this.currentIndex = index;
  7. }, */
  8. // throttle回调函数别用箭头函数,可能出现上下文this
  9. changeIndex: throttle(function (index) {
  10. this.currentIndex = index;
  11. }, 20),
  12. }

21,三级联动路由跳转与传参(编程式导航+事件委派)

三级联动用户可以点击:一级分类,二级分类,当你点击的时候,Home模块跳转到Search模块,一级会把用户选中的产品(产品的名字,产品的ID)在路由跳转的时候带着,工作量大
路由跳转:
1,声明式导航:router-link, 如果使用声明式导航router-link,可以实现路由的跳转与传递参数 但是会出现卡顿。
当服务器的数据返回之后,循环出很多的router-link组件【创建组件实例】1000+, 当服务器的数据返回之后,循环出很多的router-link组件【创建组件实例】1000+
2,编程式导航:push|replace,
所以用编程式导航和事件委派的方法来解决。
事件委派: 将事件统一绑定给元素共同的祖先元素(后代元素事件触发时,通过冒泡,通过祖先元素的响应函数来处理事件),这样可以只绑定一次,即可应用到多个元素上。事件的委派利用了冒泡,通过委派可以减少事件绑定的次数,提高程序的性能event中的target表示的触发事件的对象 ,使用它对触发事件的元素进行判断。
利用事件委派存在一些问题:
1,是把全部子节点【h3,dt,dl,em】的事件委派给父亲节点,点击a标签时,才会进行路由跳转【怎么确定点击的一定是a标签】
2,即使你能确定点击的是a标签,如何获取参数【1,2,3级分来的产品的名字、id】
解决方法:
1,把子节点当中a标签,我自己加上自定义属性data-categoryName,其余的子节点是没有的
<a :data-categoryName="c1.categoryName" :data-category1Id="c1.categoryId">{{c1.categoryName}}
</a>
2,在点击事件goSearch函数中 传入event参数,通过event.target属性获取当前点击节点,节点有一个属性dataset属性,可以获取节点的自定义属性与属性值, 再通过dataset属性获取节点的属性信息。
例如,console.log(element.dataset);    这里点击a标签才会得到一个对象例如{xxx:'xxx',categoryname:‘手机’}(浏览器自动匹配成小写的categoryname),所以我们可以把它解构出来
同时,给各级a标签加上自定义属性 data-category1Id,这样通过dataset可以获取1,2,3级a标签的商品名字和id。
let { categoryname, category1id, category2id, category3id } =element.dataset;

合并params和query参数,如果路由跳转的时候带有params参数,也要捎带传递过去,这里的goSearch是点击三级联动。

我们可以通过在函数中传入event参数,获取当前的点击事件,通过event.target属性获取当前点击节点,再通过dataset属性获取节点的属性信息

event是系统属性,所以我们只需要在函数定义的时候作为参数传入,在函数使用的时候不需要传入该参数。

  1. goSearch(event) {
  2. // 最好的解决方法:编程式导航 + 事件委派
  3. // 利用事件委派存在一些问题:1,是把全部子节点【h3,dt,dl,em】的事件委派给父亲节点
  4. // 点击a标签时,才会进行路由跳转【怎么确定点击的一定是a标签】
  5. // 即使你能确定点击的是a标签,如何获取参数【1,2,3级分来的产品的名字、id】
  6. // 第一个问题:把子节点当中a标签,我自己加上自定义属性data-categoryName,其余的子节点是没有的
  7. let element = event.target;
  8. // 获取到当前触发这个事件的节点【h3,a,dt,dl】,需要带有data-categoryname这样节点【一定是a标签】
  9. // 节点有一个属性dataset属性,可以获取节点的自定义属性与属性值
  10. // console.log(element.dataset); 这里点击a标签才会得到一个对象例如{xxx:'xxx',categoryname:‘手机’}(浏览器自动匹配成小写的categoryname),所以我们可以把它解构出来
  11. let { categoryname, category1id, category2id, category3id } =
  12. element.dataset;
  13. // 如果标签身上拥有categoryname一定是a标签
  14. if (categoryname) {
  15. // 整理路由跳转的参数
  16. let location = { name: "search" };
  17. let query = { categoryName: categoryname }; //categoryname是解构出来的值
  18. // 一级分类,二级分类,三级分类的a标签
  19. if (category1id) {
  20. query.category1Id = category1id;
  21. } else if (category2id) {
  22. query.category2Id = category2id;
  23. } else {
  24. query.category3Id = category3id;
  25. }
  26. // 判断:如果路由跳转的时候带有params参数,也要捎带传递过去
  27. if (this.$route.params) {
  28. location.params = this.$route.params;
  29. // 整理完参数
  30. // console.log(location,query); 这里得到的是两个对象,需要将这两个对象合并
  31. location.query = query;
  32. // 路由跳转
  33. this.$router.push(location);
  34. }
  35. }
  36. },

Header组件代码如下,这里的goSearch是点击Header的搜索按钮。

  1. methods: {
  2. // 搜索按钮的回调函数,需要向search路由进行跳转
  3. goSearch(){
  4. // 路由传递参数:
  5. // 第一种:字符串形式,params参数:this.keyword(传参数前,要在原来的路径最后加/),需要占位,在路由的index.js文件里path: "/Search/:keyword", query参数:this.keyword.toUpperCase()
  6. // this.$router.push("/Search/" + this.keyword+"?k="+this.keyword.toUpperCase());
  7. // 第二种:模板字符串
  8. // this.$router.push(`/search/${this.keyword}?k=${this.keyword.toUpperCase}`)
  9. // 第三种:对象,需要给路由命名
  10. // this.$router.push({name:"search",params:{keyword:this.keyword || undefined}}) //如果在搜索华为,则路径是127.0.0.1:8000/#/search/华为
  11. if(this.$route.query){
  12. // 代表的是如果有query参数也带过去
  13. let location = {name:"search",params:{keyword:this.keyword || undefined}};
  14. location.query = this.$route.query;
  15. this.$router.push(location);
  16. }
  17. },
  18. },

22,mock数据

服务器返回的数据(接口)只有商品分类菜单数据,对于ListContainer组件与Floor组件数据服务器没有提供

mock数据(模拟):如果你想mock数据,需要用到一个插件mockjs,拦截ajax请求,前端mock数据不会和你的服务器进行任何通信

npm i --save mockjs

使用步骤:
1,在项目当中src创建mock文件夹
2,准备json数据(在mock文件夹中创建json文件)---格式化,别留有空格,会跑不起来
3,把mock数据需要的图片放到public文件夹中,public文件夹在打包的时候,会把相应的资源原封不动打包到 dist文件夹中
4,开始mock(虚拟的数据)创建mockServe . js通过mockjs插件实现模拟数据
5,把mockServe.js文件在入口文件中main.js引入(至少引入一次,才能模拟数据)
  1. // 引入MockServe.js---mock数据
  2. import '@/mock/mockServe';

 MockServe.js代码如下

  1. // 把mockServe当成一个本地服务器,这个mockServe就是服务器的入口文件,只需要运行一下就是启动了,不需要暴露
  2. // 先引入mockjs模块,这里一定要写Mock
  3. import Mock from 'mockjs';
  4. // 把JSON数据格式引入进来(json数据格式没有对外暴露但是可以引入)
  5. // webpack默认对外暴露的:图片,JSON数据格式
  6. import banner from './banner.json';
  7. import floor from './floor.json';
  8. // mock数据
  9. // Mock是对象,对象身上有个mock方法,这个方法需要两个参数:第一个参数请求地址 第二个参数请求数据
  10. Mock.mock("/mock/banner",{code:200,data:banner}); //模拟首页大的轮播图
  11. Mock.mock("/mock/floor",{code:200,data:floor})
6,在api文件夹下创建mockAjax.js文件,复制request.js文件,将baseurl改为/mock,在api文件夹下的index.js文件中引入mockAjax.js
  1. // 获取banner (Home首页轮播图接口)
  2. export const reqGetBannerList = () =>mockRequest.get('/banner');
7,去ListContainer文件下要求一挂载就派发action--getBannerList,系统会输出不认识getBannerList
8,由于是home组件下的,所以要去找store文件夹下的home,引入函数reqGetBannerList,并派发action,书写mutations和state
这时候home仓库已经拿到了数据就是四个轮播图,但是ListContainer组件没有数据,所以要去组件引入
  1. computed: {
  2. ...mapState({
  3. bannerList: (state) => state.home.bannerList, //大仓库下的小仓库
  4. }),
  5. },
总结:只要是公共数据都会放在store中,之后的实现步骤就是上面的固定步骤。

23,轮播图swiper

在new Swpier实例之前,页面中结构必须的有,先做body结构再做script

ListContainer组件开发的重点:安装swiper插件,最新版本是6,要用5 npm install --save swiper@5

main.js文件代码

  1. // 引入swiper样式,没有对外暴露,不需要from b
  2. import "swiper/css/swiper.css";
  1. var mySwiper = new Swiper ('.swiper', {
  2. direction: 'vertical', // 垂直切换选项
  3. loop: true, // 循环模式选项
  4. // 如果需要分页器
  5. pagination: {
  6. el: '.swiper-pagination',
  7. },
  8. // 如果需要前进后退按钮
  9. navigation: {
  10. nextEl: '.swiper-button-next',
  11. prevEl: '.swiper-button-prev',
  12. },
  13. // 如果需要滚动条
  14. scrollbar: {
  15. el: '.swiper-scrollbar',
  16. },
  17. })

注意:在创建swiper对象时,我们会传递一个参数用于获取展示轮播图的DOM元素,官网直接通过class(而且这个class不能修改,是swiper的css文件自带的)获取。但是这样有缺点:当页面中有多个轮播图时,因为它们使用了相同的class修饰的DOM,就会出现所有的swiper使用同样的数据,这肯定不是我们希望看到的。

解决方法:在轮播图最外层DOM中添加ref属性

<div class="swiper-container" id="mySwiper" ref="cur">

通过ref属性值获取DOM

let mySwiper = new Swiper(this.$refs.cur,{...})

经常制作轮播图(移动端,pc端都可以使用)

使用步骤:

1)引入相应依赖包swiper.js,swiper.css

2)页面的结构必须有

3)初始化swiper实例,给轮播图添加动态效果

4)mock数据,通过mockjs模块实现

将轮播图作为LsitContainer的子组件,由于在new Swpier实例之前,页面中结构必须的有,所以不能在mounted里new Swiper实例,因为这时候结构中的bannerList不完整,还没接收到数据。
解决方法1:在mounted加定时器,延迟一秒new Swiper实例
解决方法2: 监听属性watch,监听bannerList数据的变化:因为这条数据发生过变化--由空数组变为数组里面有四个元素,只要发生变化就newSwiper。如果执行handler方法,代表组件实例身上这个属性的属性值已经有了【数组:四个元素】, 当前这个方法执行,只能保证数组的数据有了,但是没法保证v-for已经执行结束,结构已经完整,v-for执行完毕,才会有结构【watch无法保证】

解决方法3:watch监听属性+nextTick

watch:监听已有数据的变化

$nextTick:在下次DOM更新循环结束会后执行延迟回调,在修改数据之后,立即使用这个方法,获取更新后的DOM

可以保证页面中的结构已经有了,经常和很多插件一起使用【都需要DOM存在了】

nextTick:在下次DOM更新循环结束会后执行延迟回调,在修改数据之后,立即使用这个方法,获取更新后的DOM,当你执行这个回调的时候,保证服务器数据回来了,v-for执行完毕了【一定轮播图的结构已经有了】

this.$nextTick(() => { }

24,开发floor组件

仓库当中的state数据格式,取决于服务器返回的数据

getFloorList这个action在哪里触发,需要在home路由组件当中的mounted发,因为floor有两个,我们需要在home组件v-for遍历floor组件

Home组件下的代码如下:

  1. <template>
  2. <div>
  3. <!-- Floor这个组件,不是自己在组件内部发请求的,数据是父组件给的 -->
  4. <Floor v-for="(floor,index) in floorList" :key="floor.id" :list="floor" />
  5. </div>
  6. </template>
  1. mounted() {
  2. // 派发action,获取floor组件的数据
  3. this.$store.dispatch("getFloorList");
  4. // 获取用户信息在登录后,在首页展示
  5. // this.$store.dispatch("getUserInfo"); 放在路由里
  6. },

floor组件代码如下:Floor是子组件,home是父组件,我们在home组件中实现了由home组件向Floor组件传递信息的操作,即父组件向子组件传递信息。

  1. <!-- 轮播图的地方 这里是props,不是遍历-->
  2. <Carousel :list="list.carouselList" />
  3. export default {
  4. name: "",
  5. props: ["list"],
  6. }

v-for也可以在自定义标签上使用

组件通信的方式有哪些:

props:用于父子组件通信

自定义事件:@on,@emit 可以实现子给父通信

pubsub-js:vue当中几乎不用 全能

插槽

vuex

原理:父组件设置一个属性绑定要传递的数据

子组件props接受该属性值

本项目的

父组件:home文件下的index.js

<template>

<div>

//...省略

<!--  父组件通过自定义属性list给子组件传递数据-->

  <Floor v-for="floor in floorList"  :key="floor.id" :list="floor"/>

<!--  商标-->

</div>

</template>

子组件:Floor下的index.vue

<template>

  <!--楼层-->

  <div class="floor">

    //...省略

  </div>

</template>

<script>

export default {

  name: "floor",

//子组件通过props属性接受父组件传递的数据

  props:['list']

}

</script>

25,把首页当中的轮播图拆分为一个公用全局组件

以后在开发项目时,如果看到一个组件在很多地方都使用,就把它变成全局组件
注册一次,可以在任意地方使用,公用组件放在component文件夹中

将轮播图放在Carousel组件中,这样ListContainer和Floor都能引用

floor组件代码如下:

  1. <!-- 轮播图的地方 这里是props,不是遍历-->
  2. <Carousel :list="list.carouselList" />  
  3. export default {
  4. name: "",
  5. props: ["list"],
  6. }

Carousel组件代码如下:

  1. <template>
  2. <div class="swiper-container" ref="floor2Swiper">
  3. <div class="swiper-wrapper">
  4. <div class="swiper-slide" v-for="(carousel,index) in list" :key="carousel.id">
  5. <img :src="carousel.imgUrl" />
  6. </div>
  7. </div>
  8. <!-- 如果需要分页器 -->
  9. <div class="swiper-pagination"></div>
  10. <!-- 如果需要导航按钮 -->
  11. <div class="swiper-button-prev"></div>
  12. <div class="swiper-button-next"></div>
  13. </div>
  14. </template>
  15. <script>
  16. import Swiper from "swiper";
  17. export default {
  18. name: "Carousel",
  19. props: ['list'],
  20. watch: {
  21. list: {
  22. // 为什么watch监听不到list,因为数据没有发生过变化,数据是父亲给的,是一个对象
  23. immediate: true,
  24. handler() {
  25. this.$nextTick(() => {
  26. // 当你执行这个回调的时候,保证服务器数据回来了,v-for执行完毕了【一定轮播图的结构已经有了】
  27. var mySwiper = new Swiper(
  28. // document.querySelector(".swiper-container"),
  29. this.$refs.floor2Swiper,
  30. {
  31. loop: true, // 循环模式选项
  32. // 如果需要分页器
  33. pagination: {
  34. el: ".swiper-pagination",
  35. clickable: true, //点击小球的时候也切换图片
  36. },
  37. // 如果需要前进后退按钮
  38. navigation: {
  39. nextEl: ".swiper-button-next",
  40. prevEl: ".swiper-button-prev",
  41. },
  42. }
  43. );
  44. });
  45. },
  46. },
  47. },
  48. };
  49. </script>

26,Search模块--getters(vuex store中的计算属性)

发送请求,由于本次请求的方式POST,并且需要带参数
  1. // 获取搜索模块数据,地址/api/list,请求的方式是POST,需要带参数
  2. // 当前这个函数需要接收外部传递的参数
  3. // 两种写法:axios.get(`url`)--对象写法 axios({})--函数调用
  4. // 当前这个接口,给服务器传递一个默认参数(调用的时候),至少是一个空对象,例如console.log(reqGetSearchInfo({}))
  5. export const reqGetSearchInfo = (params)=>requests({url:"/list",method:"post",data:params});

获取search模块需要的服务器数据,并且存储于仓库中,并且有一些数组数据我们已经通过getters进行简化

  1. import {reqGetSearchInfo} from '@/api';
  2. // search模块的小仓库
  3. const state = {
  4. searchList:{}, //初识状态是个对象
  5. };
  6. const mutations = {
  7. GETSEARCHLIST(state,searchList){
  8. state.searchList = searchList;
  9. }
  10. };
  11. const actions = {
  12. // 获取search模块数据
  13. // async 函数第一个参数是上下文,第二个参数是dispatch派发一个action时传递过来的
  14. async getSearchList({commit},params={}){
  15. //当前这个reqGetSearchInfo函数在调用获取服务器数据的时候,至少传递一个参数(空对象)
  16. // params形参:是当用户派发action的时候,第二个参数传递过来的,至少是一个空对象
  17. let result = await reqGetSearchInfo(params);
  18. if(result.code=200){
  19. commit("GETSEARCHLIST",result.data);
  20. }
  21. },
  22. };
  23. // 计算属性,在项目当中,为了简化仓库中的数据而生
  24. // 可以把我们将来在组件当中需要用的数据简化一下【将来组件在获取数据的时候就方便了】
  25. const getters = {
  26. // 当前形参state,是当前仓库中的state,不是大仓库中的state
  27. goodsList(state){
  28. // 这样书写是有问题的,state是从空对象变成有内容的,如果没网,请求发不出去,那state就还是个空对象,那就找不到state.searchList.goodsList,是undefined,
  29. // 所以要返回一个空数组
  30. return state.searchList.goodsList||[];
  31. },
  32. trademarkList(state){
  33. return state.searchList.trademarkList;
  34. },
  35. attrsList(state){
  36. return state.searchList.attrsList;
  37. },
  38. };
  39. export default {
  40. state,
  41. mutations,
  42. actions,
  43. getters
  44. }

如果不使用getters属性,我们在组件获取state中的数据表达式为:this.$store.state.子模块.属性,

如果有多个组件需要用到此属性,我们要么复制这个表达式,或者抽取到一个共享函数然后在多处导入它——无论哪种方式都不是很理想。

Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

个人理解:getters将获取store中的数据封装为函数,代码维护变得更简单(和我们将请求封装为api一样)。而且getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

注意:仓库中的getters是全局属性,是不分模块的。即store中所有模块的getter内的函数都可以通过$store.getters.函数名获取

Search模块中获取仓库里的数据,用...mapGeters,mapGetters里面的写法:传递的数组,因为getters是不分模块的,state会分state下的home或者search模块,直接写函数名即可。

  1. computed: {
  2. // 这里不再用mapState,用mapGetters,数据都在getters里
  3. /* ...mapState({
  4. goodsList:state=>state.search.searchList.goodsList
  5. }) */
  6. // mapGetters里面的写法:传递的数组,因为getters是不分模块的,state会分state下的home或者search模块
  7. ...mapGetters(["goodsList"]),
  8. }

27,params参数

向服务器发送请求获取Search模块数据

  1. data() {
  2. return {
  3. // 带给服务器的参数
  4. searchParams: {
  5. // 一级分类的id
  6. category1Id:"",
  7. // 二级分类的id
  8. category2Id:"",
  9. // 三级分类的id
  10. category3Id: "",
  11. // 分类名字
  12. categoryName: "",
  13. // 搜索框输入关键字
  14. keyword: "",
  15. // 排序:初识的窗台应该是综合降序
  16. order: "1:desc",
  17. // 分页器代表现在第几页
  18. pageNo: 1,
  19. // 每一页展示数据个数
  20. pageSize: 10,
  21. // 平台售卖属性操作低的参数
  22. props: [],
  23. // 品牌
  24. trademark: "",
  25. },
  26. };
  27. },
  28. mounted() {
  29. // 在发请求之前带给服务器参数【searchParams参数发生变化有数值带给服务器】
  30. this.getData();
  31. },
  32. methods: {
  33. // 向服务器发送请求获取Search模块数据(根据参数不同返回不同的数据进行展示)
  34. // 把这次请求封装成一个函数,当需要在调用时候调用即可
  35. getData() {
  36. // 先测试接口返回的数据是数组还是对象,第二个参数是需要传递的params参数
  37. this.$store.dispatch("getSearchList",this.searchParams);
  38. },
  39. }

28,面包屑

Search组件代码如下:

  1. beforeMount() {
  2. // 复杂的写法
  3. /* this.searchParams.category1Id = this.$route.query.category1Id;
  4. this.searchParams.category2Id = this.$route.query.category2Id;
  5. this.searchParams.category3Id = this.$route.query.category3Id;
  6. this.searchParams.categoryName = this.$route.query.categoryName;
  7. this.searchParams.keyword = this.$route.params.keyword; */
  8. // Object.assign:ES6新增的语法,合并对象
  9. // 在发请求之前,把接口需要传递参数,进行整理(再给服务器发请求之前,把参数整理好,服务器会返回查询的数据)
  10. Object.assign(this.searchParams,this.$route.query,this.$route.params)
  11. },
  1. <ul class="fl sui-tag">
  2. <!-- 分类的面包屑 -->
  3. <li class="with-x" v-if="searchParams.categoryName">{{searchParams.categoryName}}<i @click="removeCategoryName">x</i></li>
  4. <!-- 关键字搜索框的面包屑 -->
  5. <li class="with-x" v-if="searchParams.keyword">{{searchParams.keyword}}<i @click="removeKeyword">x</i></li>
  6. <!-- 品牌的面包屑,由于这里展示的品牌是id加名字,`${trademark.tmId}:${trademark.tmName}`,所以用数组的方法只展示名字,不需要id -->
  7. <li class="with-x" v-if="searchParams.trademark">{{searchParams.trademark.split(":")[1]}}<i @click="removeTradeMark">x</i></li>
  8. <!-- 平台的售卖属性面包屑,因为点击多个售卖属性,props数组会依次叠加数据,所以要用v-for,删除数据的点击事件要传索引值,才能知道要删除哪个-->
  9. <li class="with-x" v-for="(attrValue,index) in searchParams.props" :key="index">{{attrValue.split(":")[1]}}<i @click="removeAttr(index)">x</i></li>
  10. </ul>

29,全局事件总线

要求删除搜索框的面包屑时,兄弟组件Header中的搜索框的keyword也会消失。
先在入口文件main.js中配置全局事件总线$bus
  1. new Vue({
  2. render: h => h(App),
  3. // 全局事件总线$bus配置
  4. beforeCreate(){
  5. Vue.prototype.$bus = this; //这里的this是VM
  6. },
  7. // 注册路由,底下的写法KV一致省略V【router小写的】
  8. // 当这里书写router的时候,组件身上都拥有$route,$router属性
  9. router,
  10. // 注册仓库:组件实例的身上会多个属性$store属性
  11. store,
  12. }).$mount('#app')

search模块,删除面包屑的点击事件代码如下

  1. removeKeyword(){
  2. // 给服务器带的参数searchParams的keyword置空
  3. this.searchParams.keyword = undefined;
  4. // 再次发请求 让search重回手机列表
  5. this.getData();
  6. // 通知兄弟组件Header清除关键字
  7. this.$bus.$emit("clear");
  8. // 需要进行路由的跳转
  9. if(this.$route.query){ //和上面一样,这里要加query判断,这里只是为了删除params参数,如果本来就有query参数那就要带上
  10. this.$router.push({name:"search",query:this.$route.query});
  11. }
  12. },

Header模块的代码如下

  1. mounted() {
  2. // 通过全局事件总线清除关键字
  3. this.$bus.$on("clear",()=>{
  4. this.keyword = "";
  5. })
  6. },

30,自定义事件

子组件SearchSelector下获取仓库getters的数据

  1. methods: {
  2. tradeMarkHandler(trademark){
  3. // 点击了品牌(苹果),还是需要整理参数,向服务器获取相应的数据进行展示
  4. // 在哪个组件中发请求:父组件,因为父组件中searchParams参数是带给服务器参数,子组件把你点击的品牌的信息,需要给父组件传递过去-----用自定义事件
  5. this.$emit('trademarkInfo',trademark); //把品牌信息trademark带过去
  6. },
  7. // 平台售卖属性的点击事件
  8. attrInfo(attr,attrValue){
  9. // [“属性ID:属性值:属性名"]
  10. this.$emit("attrInfo",attr,attrValue);
  11. }
  12. },

Search模块下的代码

  1. <!-- selector--自定义事件 -->
  2. <SearchSelector @trademarkInfo="trademarkInfo" @attrInfo="attrInfo"/>
  1. methods: {
  2. trademarkInfo(trademark){ //自定义事件
  3. // console.log(trademark); 这里的trademark是个对象
  4. // 1,整理品牌字段的参数 “ID:品牌名称"
  5. this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}`;
  6. this.getData();
  7. },
  8. attrInfo(attr,attrValue){
  9. // [“属性ID:属性值:属性名"]
  10. // 参数格式整理好
  11. let props = `${attr.attrId}:${attrValue}:${attr.attrName}`;
  12. // 数组去重
  13. // if语句里面只有一行代码,可以省略大花括号
  14. if(this.searchParams.props.indexOf(props)==-1)this.searchParams.props.push(props);
  15. this.getData();
  16. },
  17. }

31,排序结构

排序方式:

1: 综合,2: 价格 asc: 升序,desc: 降序  

示例: "1:desc"

31.1,考虑的问题:

1,谁应该有类名(红色底色),默认是综合,点击价格会变成价格有红色:通过order的属性值当中包含1(综合)还是包含2(价格)

2,谁应该有箭头,谁有箭头取决于谁有类名是红色,用v-if  v-show

3,箭头用什么制作:阿里图标库,到public index文件里引入,箭头上升还是下降取决于是asc还是desc

  1. <!-- 引入图标样式在线地址,地址前要价https -->
  2. <link rel="stylesheet" href="https://at.alicdn.com/t/c/font_3627924_s1qul2sqzg.css">
  1. <!-- 排序结构-->
  2. <ul class="sui-nav">
  3. <!-- 当order里包含1或者2时,才有active,才会变红色,将判断写在计算属性-->
  4. <!-- 点击事件要传参,因为要区分点击的是综合还是价格 -->
  5. <li :class="{active:isOne}" @click="changeOrder('1')">
  6. <a href="#">综合<span v-show="isOne" class="iconfont" :class="{'icon-long-arrow-up':isAsc,'icon-long-arrow-down':isDesc}"></span></a>
  7. </li>
  8. <li :class="{active:isTwo}" @click="changeOrder('2')">
  9. <a href="#">价格<span v-show="isTwo" class="iconfont" :class="{'icon-long-arrow-up':isAsc,'icon-long-arrow-down':isDesc}"></span></a>
  10. </li>
  11. </ul>
  1. computed: {
  2. // 判断order是否包含1和2
  3. isOne() {
  4. //这里得到的是布偶值
  5. return this.searchParams.order.indexOf("1") != -1;
  6. },
  7. isTwo() {
  8. return this.searchParams.order.indexOf("2") != -1;
  9. },
  10. // 判断是升序还是降序,才可以确定图标
  11. isAsc() {
  12. return this.searchParams.order.indexOf("asc") != -1;
  13. },
  14. isDesc() {
  15. return this.searchParams.order.indexOf("desc") != -1;
  16. },
  17. // 获取Search模块展示产品一共多少数据
  18. ...mapState({
  19. total: (state) => state.search.searchList.total,
  20. }),
  21. },
  1. methods:{
  2. changeOrder(flag){
  3. // flag形参:它是一个标记,代表用户点击的是综合(1)还是价格(2) 【用户点击的时候传递进来的】
  4. let originOrder = this.searchParams.order;
  5. // 这里获取到的是最开始的状态,例如 1:desc
  6. let originFlag = this.searchParams.order.split(":")[0]; //获取是综合1还是价格2
  7. let originSort = this.searchParams.order.split(":")[1]; //获取是desc还是asc
  8. //准备一个新的order属性值
  9. let newOrder = '';
  10. // 点击的是综合,因为初始值是综合1
  11. if(flag==originFlag){ //如果点击的和初始值一样,那就将降序改为升序
  12. newOrder = `${originFlag}:${originSort == "desc" ? "asc":"desc"}`;
  13. }else{
  14. // 点击的是价格
  15. newOrder = `${flag}:${'desc'}`; //默认价格降序
  16. }
  17. // 将新的order赋予searchParams.order
  18. this.searchParams.order = newOrder;
  19. this.getData();
  20. }
  21. }

31.2,总结search模块开发

1,先静态页面+静态组件拆分
2,发请求(API)
3,vuex三连环
4,组件获取仓库数据,动态展示数据

32,分页器

32.1,为什么用分页器?

很多电商平台为什么要用分页器,比如电商平台同时展示的数据有很多,渲染会卡顿

采用分页功能

ElementUI是有相应的分页组件,使用起来超级简单,但是我们前台项目目前不用,自主掌握

面试的时候:是否封装过组件:分页器,日历

32.2,分页器的展示,需要哪些数据(条件)?

1, 需要知道当前是第几页:pageNo字段代表当前页数

需要知道每页展示多少条数据:pageSize字段进行代表

需要知道整个分页器一共有多少条数据:total字段进行代表---【获取另外一条信息:一共有多少页】,向上取整

...连续的页码数...   分页器连续的页码个数;5|7【奇数对称好看】

总结:对于分页器而言,自定义的前提需要知道四个前提条件

pageNo

pageSize

total

Continues

2,自定义分页器。在开发的时候先自己传递假的数据进行调试,调试成功后再用服务器的数据,

3,对于分页器而言,重要的地方【算出:连续页面起始数字和结束数字】

当前第八页                 6 7 8 9 10

当前第十五页                13  14 15 16 17  

前提:分页器数字没有0和负数

假如当前是第一页  -1 0 1 2 3(不行)----- 1 2 3 4 5

假如当前是第二页  0 1 2 3 4(不行)---- 1 2 3 4 5

假如当前是第三页  1 2 3 4 5(可以 )

假如当前页是31,总页数是31

29 30 31 32 33(不行)------- 27 28 29 30 31

假如当前页是30 , 28 29 30 31 32 (不行)-------27 28 29 30 31

假如当前页是29, 27 28 29 30 31(可以)

4,分页器的动态展示【上,中,下】

v-for:数组|对象|字符串|数字,都能遍历,

  1. <template>
  2. <div class="pagination">
  3. <!-- 上 -->
  4. <!-- 自定义事件,第一个参数为要触发的事件,第二个事件为要传递的数据 -->
  5. <button :disabled="pageNo==1" @click="$emit('getPageNo',pageNo-1)">上一页</button>
  6. <button v-if="startNumAndEndNum.start>1" @click="$emit('getPageNo',1)" :class="{active:pageNo==1}">1</button>
  7. <button v-if="startNumAndEndNum.start>2">···</button>
  8. <!-- 中间部分 -->
  9. <button v-for="(page,index) in startNumAndEndNum.end"
  10. :key="index"
  11. v-if="page>=startNumAndEndNum.start"
  12. @click="$emit('getPageNo',page)"
  13. :class="{active:pageNo==page}">
  14. {{page}}
  15. </button>
  16. <!-- 下 -->
  17. <button v-if="startNumAndEndNum.end<totalPage-1">···</button>
  18. <button v-if="startNumAndEndNum.end<totalPage" @click="$emit('getPageNo',totalPage)" :class="{active:pageNo==totalPage}">{{totalPage}}</button>
  19. <button :disabled="pageNo==totalPage" @click="$emit('getPageNo',pageNo-1)">下一页</button>
  20. <button style="margin-left: 30px">共{{total}}条</button>
  21. </div>
  22. </template>
  1. computed:{
  2. // 总共多少页
  3. totalPage(){
  4. // 向上取整
  5. return Math.ceil(this.total/this.pageSize);
  6. },
  7. //计算出连续页码的起始数字与结束数字[连续页码的数字:至少是4]
  8. startNumAndEndNum(){
  9. const {continues,pageNo,totalPage} = this; //这里是解构,如果不解构下面就要写this
  10. // 先定义两个变量存储起始数字和结束数字
  11. let start = 0, end = 0;
  12. // 连续页码数字5【就是至少是5页】,如果出现不正常现象【就是不够5页】
  13. if(continues > totalPage){
  14. start = 1;
  15. end = totalPage;
  16. }else{
  17. // 正常现象【连续页码数是5,但是你的总页数一定是大于5】
  18. // 起始数字
  19. start = pageNo - parseInt(continues/2); //这里不能写死pageNo-2,因为这样就把连续页码数写死为5
  20. // 结束数字
  21. end = pageNo + parseInt(continues/2);
  22. // 把出现不正常的现象【start数字出现0和负数】纠正
  23. if(start<1){
  24. start = 1;
  25. end = continues;
  26. };
  27. // 把出现不正常的现象【end>总页数total】纠正
  28. if(end > totalPage){
  29. start = totalPage - continues + 1;
  30. end = totalPage;
  31. };
  32. };
  33. return { start, end };
  34. },
  35. }

33,滚动条

使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。 使用声明式导航vue-router。

router--index.js代码如下:

  1. // 配置路由
  2. let router= new VueRouter({
  3. // 配置路由
  4. // routes:routes
  5. routes,
  6. // 滚动行为,路由跳转的时候滚动条位置
  7. scrollBehavior (to, from, savedPosition) {
  8. // 返回的这个y=0,代表的滚动条在最上方
  9. return {y:0};
  10. }
  11. });

34,undefined细节

访问undefined的属性值会引起红色警告,可以不处理,但是要明白警告的原因。

以获取商品categoryView信息为例,categoryView是一个对象。

  1. //对应的getters代码
  2. const getters = {
  3. categoryView(state){
  4. return state.goodInfo.categoryView
  5. }
  6. //对应的computed代码
  7. computed:{
  8. ...mapGetters(['categoryView'])
  9. }
  10. //html代码
  11. <div class="conPoin">
  12. <span v-show="categoryView.category1Name" >{{categoryView.category1Name}}</span>
  13. <span v-show="categoryView.category2Name" >{{categoryView.category2Name}}</span>
  14. <span v-show="categoryView.category3Name" >{{categoryView.category3Name}}</span>
  15. </div>

下细节在于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||{}

35,商品详情放大镜

商品详情唯一难点就是点击轮播图图片时,改变放大镜组件展示的图片。

在轮播图组件中设置一个currendIndex,用来记录所点击图片的下标,并用currendIndex实现点击图片高亮设置。当符合图片的下标满足currentIndex===index时,该图片就会被标记为选中。

轮播图组件和放大镜组件是兄弟组件,所以要通过全局总线通信。

轮播图组件和放大镜组件是兄弟组件,所以要通过全局总线通信。

在轮播图组件中,点击图片触发全局事件changeCurrentIndex,参数为图片所在数组的下标。

轮播图组件ImageList.vue代码如下:

  1. methods: {
  2. // 修改响应式数据
  3. changeCurrentIndex(index){
  4. this.currentIndex = index;
  5. // 通知兄弟组件:当前的索引值为几,才能在点击小图的时候,传到放大镜,把数据送出去所以用$emit,不用$on
  6. this.$bus.$emit('getIndex',this.currentIndex);
  7. }
  8. },

放大镜组件zoom.vue代码如下:

放大镜组件中也会有一个currentIndex,他用表示大图中显示的图片的下标(因为放大镜组件只能显示一张图片),全局事件传递的index赋值给currentIn

  1. <div class="big">
  2. <img :src="imgObj.imgUrl" ref="big"/>
  3. </div>
  4. mounted() {
  5. // 全局事件总线:获取兄弟组件传递过来的索引值
  6. this.$bus.$on('getIndex',(index)=>{ //第一个参数是自定义事件,第二个参数是回调
  7. // 修改当前响应式数据
  8. this.currentIndex = index;
  9. })
  10. },
  11. computed:{
  12. // 要求数组的[0]至少是个对象,不能是undefined
  13. imgObj(){
  14. return this.skuImageList[this.currentIndex]||{}
  15. }
  16. },

36,浏览器存储功能

浏览器存储功能:HTML5中新增的,本地存储和会话存储

本地存储:持久化的-----5M (就算浏览器关闭和关机都有)

会话存储:并非持久-----会话结束就消失

sessionStorage、localStorage概念:

sessionStorage:为每一个给定的源维持一个独立的存储区域,该区域在页面会话期间可用(即只要浏览器处于打开状态,包括页面重新加载和恢复)。

localStorage:同样的功能,但是在浏览器关闭,然后重新打开后数据仍然存在。

注意:无论是session还是local存储的值都是字符串形式。如果我们想要存储对象,需要在存储前JSON.stringify()将对象转为字符串,在取数据后通过JSON.parse()将字符串转为对象。

37,购物车组件(临时游客身份)

服务器发起ajax,获取购物车数据 ,操作vuex三连环,组件获取数据展示数据

发现:发请求的时候,获取不到你购物车里面的数据,因为服务器不知道你是谁

如果想要获取详细信息,还需要一个用户的uuidToken,用来验证用户身份。但是该请求函数没有参数,所以我们只能把uuidToken加在请求头中。

创建utils工具包文件夹,创建生成uuid的js文件,对外暴露为函数(记得导入uuid => npm install uuid)。

生成临时游客的uuid(随机字符串),每个用户的uuid不能发生变化,还要持久存储

新建utils文件夹,新建uuid_token.js文件

  1. import {v4 as uuidv4} from 'uuid';
  2. // 生成一个随机字符串,且每次执行不能发生变化,游客身份持久存储
  3. export const getUUID = () =>{
  4. // 先从本地存储获取uuid,看一下本地存储里面是否有
  5. let uuid_token = localStorage.getItem('UUIDTOKEN');
  6. // 如果没有
  7. if(!uuid_token){
  8. // 我生成游客临时身份
  9. uuid_token = uuidv4();
  10. // 本地存储存储一次
  11. localStorage.setItem('UUIDTOKEN',uuid_token);
  12. }
  13. // 切记有返回值,没有的话是undefined
  14. return uuid_token
  15. }

store---detail.js

  1. // 封装游客身份模块uuid--生成一个随机字符串(不能再变)
  2. import {getUUID} from "@/utils/uuid_token";
  3. const state = {
  4. goodInfo:{},
  5. // 游客的临时身份
  6. uuid_token:getUUID() //这个函数不论执行一次还是一百次,返回的临时身份只有一个,且函数要有返回值
  7. };
  1. // 在当前模块中引入store
  2. import store from "@/store";
  3. // 请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求发出去之前做一些事情
  4. requests.interceptors.request.use((config)=>{
  5. // config:配置对象,对象里面有一个属性很重要,headers请求头
  6. // 进度条开始
  7. if(store.state.detail.uuid_token){
  8. // 请求头添加一个字段:和后台老师商量好了
  9. config.headers.userTempId=store.state.detail.uuid_token;
  10. }
  11. nprogress.start();
  12. return config;
  13. });

注意this.$store只能在组件中使用,不能再js文件中使用。如果要在js中使用,需要引入import store from '@/store';


38,加入购物车

store--detail.js代码如下

  1. // 将产品添加到购物车中
  2. async addOrUpdateShopCart({commit},{skuId,skuNum}){ //{skuId,skuNum}用的解构,传参的时候可以不按顺序
  3. // 加入购物车返回的结果
  4. // 加入购物车以后(发请求),前台将参数带给服务器
  5. // 服务器写入数据成功,并没有返回其他的数据,只是返回code=200,代表这次操作成功
  6. // 因为服务器没有返回其余数据,因此咱们不需要三连环存储数据
  7. let result = await reqAddOrUpdateShopCart(skuId,skuNum);
  8. if(result.code==200){ //代表服务器加入购物车成功
  9. return "ok"
  10. }else{ //代表加入购物车失败
  11. return Promise.reject(new Error('faile'));
  12. }
  13. }

Detail--index.js代码如下

  1. // 加入购物车的回调
  2. async addShopcar(){
  3. // 派发actions
  4. // 1,发情求--将产品加入到数据库(通知服务器),点击购物车按钮的时候,将参数带给服务器(发请求),通知服务器加入购物车的产品是谁
  5. // 当前这里是派发一个action,也向服务器发请求,判断加入购物车是成功还是失败,进行相应的操作
  6. /* 下面这行代码,是调用了仓库中addOrUpdateShopCart这个函数,仓库中只是声明,这个方法之前在仓库里加上async,返回的一定是个Promise,要么成功要么失败,
  7. 所以这里加上await等待这个Promise的成功与失败,方法前面加上async */
  8. // let result = await this.$store.dispatch('addOrUpdateShopCart',{skuId:this.$route.params.skuid,skuNum:this.skuNum}) //{skuId,skuNum}用的解构,传参的时候可以不按顺序
  9. // 2,需要知道这次请求是成功还是失败,如果成功进行路由跳转,如果失败需要给用户提示
  10. try {
  11. // 成功
  12. await this.$store.dispatch('addOrUpdateShopCart',{
  13. skuId:this.$route.params.skuid,
  14. skuNum:this.skuNum
  15. });
  16. // 3,进行路由跳转
  17. // 4,在路由跳转的时候还需要将产品的信息带给下一级的路由组件
  18. // 一些简单的数据skuNum,通过query形式给路由组件传递过去,产品信息的数据【比较复杂:skuInfo】,通过会话存储(不会持久,会话结束数据再消失)
  19. // 本地存储或者是会话存储,一般存储的是字符串
  20. sessionStorage.setItem("SKUINFO",JSON.stringify(this.skuInfo));
  21. this.$router.push({name:'addCartSuccess',query:{skuNum:this.skuNum}});
  22. // 5,
  23. } catch (error) {
  24. // 失败--给用户进行提示
  25. alert(error.message);
  26. }
  27. },

39,修改购物车产品的数量

  1. <div class="select-all">
  2. <input class="chooseAll" type="checkbox" :checked="isAllChecked&&cartInfoList.length>0" @change="updateAllCartChecked"/>
  3. <span>全选</span>
  4. </div>

39.1,判断底部复选框是否勾选【全部产品都选中才勾选】

  1. isAllChecked() {
  2. // every遍历数组里面的元素,只要所有元素isChecked属性都为1才会返回true,只要有一个不等于1,就返回false
  3. return this.cartInfoList.every((item) => item.isChecked == 1);
  4. },

  1. <!-- $event.target.value*1 非法字符串*1都是1 -->
  2. <input autocomplete="off" type="text" minnum="1" class="itxt" :value="cart.skuNum" @change="handler('change', $event.target

39.2, 修改某一个产品购物车的个数,操作减号过快时,数量会变成负数,所以这里要用节流

  1. handler: throttle(async function (type, disNum, cart) {
  2. // type:为了区分这三个元素是加还是减还是input框
  3. // disNum:+变化量(1),-变化量(-1),input最终的个数(并不是变化量)
  4. // cart:哪一个产品(身上有id)
  5. // 向服务器发请求,修改数量
  6. switch (type) {
  7. // 加号
  8. case "add":
  9. // 带给服务器变化量
  10. disNum = 1;
  11. break;
  12. case "minus":
  13. // 判断产品的个数大于1,才可以传递给服务器-1
  14. // 如果出现产品的个数小于等于1,传递给服务器个数为0(原封不动)
  15. /* if(cart.skuNum > 1){
  16. disNum = -1;
  17. }else{
  18. // 产品的个数小于等于1
  19. disNum = 0;
  20. } */
  21. disNum = cart.skuNum > 1 ? -1 : 0;
  22. break;
  23. case "change":
  24. // 假如用户输入进来的最终量,是非法的(带有汉字|出现负数),带个服务器数字0
  25. /* if(isNaN(disNum)||disNum<1){
  26. disNum = 0 ; //这里的disNum不再是最终个数而是变化量
  27. }else{
  28. // 假如用户输入了小数点数字,例如用户输入12.5,原来的数量为10,则变化量为2,parseInt是取整,不是向上取整,这里的disNum不再是最终个数而是变化量
  29. disNum = parseInt(disNum) - cart.skuNum;
  30. } */
  31. disNum =
  32. isNaN(disNum) || disNum < 1 ? 0 : parseInt(disNum) - cart.skuNum;
  33. break;
  34. }
  35. // 派发action
  36. // this.$store.dispatch('addOrUpdateShopCart',{skuId:cart.skuId,skuNum:disNum});
  37. // 因为这里只是发送数据,修改后台数据库的数据,并没有修改当前组件数据,所以需要重新获取一次数据
  38. try {
  39. //由于detail仓库里的action,addOrUpdateShopCart有写成功与失败,所以这里要写trycatch
  40. // 代表的是修改成功
  41. await this.$store.dispatch("addOrUpdateShopCart", {
  42. skuId: cart.skuId,
  43. skuNum: disNum,
  44. });
  45. // 再一次获取服务器最新的数据进行展示,因为修改的是假数据,要是请求失败了或者用户没有网,不断点就不断加,这是不合理的,所以要重新获取数据,不能直接在前端修改展示
  46. // 后端服务假如用户操作过快点十次加,但是解析慢只执行了八次代码,前端的数据加了十,但后端服务器只加了把,这样前后端数据就乱了,所以重新获取数据是较好的方法
  47. this.getData();
  48. } catch (error) {}
  49. }, 500),

40,购物车状态修改和商品删除

store--shopcart.js代码如下

40.1,删除购物车某一个产品

  1. async deleteCartListBySkuId({commit},skuId){
  2. let result = await reqDeleteCartById(skuId); // 因为服务器没有返回其余数据,因此咱们不需要三连环存储数据
  3. // 组件需要知道是成功与失败,成功再发请求,失败弹出错误信息
  4. if(result.code==200){
  5. return "ok"
  6. }else{
  7. return Promise.reject(new Error('faile'));
  8. }
  9. },

shopcart---index.js代码如下

  1. // 删除某一个产品的操作
  2. async deleteCartById(cart) {
  3. try {
  4. // 如果删除成功再次发请求获取新的数据进行展示
  5. await this.$store.dispatch("deleteCartListBySkuId", cart.skuId);
  6. this.getData();
  7. } catch (error) {
  8. alert(error.message);
  9. }
  10. },

store--shopcart.js代码如下

40.2,修改购物车里某一个商品的选中状态

  1. async updateCheckedById({commit},{skuId,isChecked}){
  2. let result = await reqUpdateCheckedById(skuId,isChecked); // 因为服务器没有返回其余数据,因此咱们不需要三连环存储数据
  3. // 组件需要知道是成功与失败,成功再发请求,失败弹出错误信息
  4. if(result.code==200){
  5. return "ok"
  6. }else{
  7. return Promise.reject(new Error('faile'));
  8. }
  9. },

shopcart---index.js代码如下

  1. // 修改购物车里某一个产品的状态
  2. async updateChecked(cart,event){
  3. // console.log(event.target.checked); 这里输出的是布偶值,不是0和1,我们带给服务器的参数isChecked应该是0|1
  4. try {
  5. // 如果修改数据成功,再次获取服务器数据(购物车)
  6. let isChecked = event.target.checked ? "1":"0";
  7. await this.$store.dispatch('updateCheckedById',{
  8. skuId:cart.skuId,
  9. isChecked,
  10. });
  11. this.getData();
  12. } catch (error) {
  13. // 如果失败提示
  14. alert(error.message);
  15. }
  16. },

41,全选(actions扩展)

由于后台只提供了删除单个商品的接口,所以要删除多个商品时,只能多次调用actions中的函数。

我们可能最简单的方法是在method的方法中多次执行dispatch删除函数,当然这种做法也可行,但是为了深入了解actions,我们还是要将批量删除封装为actions函数。

actions扩展

官网的教程,一个标准的actions函数如下所示:

 deleteAllCheckedById(context) {

        console.log(context)

    }

我们可以看一下context到底是什么

 context中是包含dispatch、getters、state的,即我们可以在actions函数中通过dispatch调用其他的actions函数,可以通过getters获取仓库的数据。

这样我们的批量删除就简单了,对应的actions函数代码让如下

Store---shopcart.js代码如下

41.1,删除全部选中的产品

注意:没有一次删除很多产品的接口,但是有通过ID可以删除产品的接口(一次删除一个)

 promise.all([p1,p2,p3])

 p1|p2|p3 每一个都是promise对象,如果有一个promise失败,都失败,如果都成功,返回的结果就是成功

  1. deleteAllCheckedCart({dispatch,getters}){ //这里传dispatch是为了派发action,传getters是为了捞取数据
  2. // context:小仓库,commit【提交mutations修改state】 getters【计算属性】 dispatch【派发action】 state【当前仓库数据】
  3. // 获取购物车中全部的产品(是一个数组)
  4. let PromiseAll = []; // promise.all([p1,p2,p3])
  5. getters.cartList.cartInfoList.forEach(item => {
  6. // 派发action要用dispatch
  7. // 当勾选状态位1时,执行dispatch('deleteCartListBySkuId',item.skuId),返回值是 async deleteCartListBySkuId({commit},skuId) 的返回值,是个Promise
  8. let promise = item.isChecked==1?dispatch('deleteCartListBySkuId',item.skuId):'';
  9. // 将每一次返回的Promise添加到数组当中
  10. PromiseAll.push(promise);
  11. });
  12. // 只要全部的p1|p2|p3....都成功,返回的结果即成功,如果有一个失败,则返回失败结果,组件在等待是成功还是失败,所以这里要返回结果
  13. return Promise.all(PromiseAll);

Shopcart--index.js代码如下

  1. // 删除全部选中的产品
  2. // 这个回调函数没办法收集到一些有用的数据,拿不到cart.id
  3. async deleteAllCheckedCart(){
  4. // 派发一个action
  5. try {
  6. await this.$store.dispatch("deleteAllCheckedCart");
  7. // 再发请求获取购物车列表
  8. this.getData();
  9. } catch (error) {
  10. alert(error.message) ;
  11. }
  12. },

Store---shopcart.js代码如下

41.2, 修改购物车里全部产品的勾选状态

  1. updateAllCartChecked({dispatch,state},isChecked){
  2. // 数组
  3. let PromiseAll = [];
  4. state.cartList[0].cartInfoList.forEach(item => {
  5. let promise = dispatch('updateCheckedById',{skuId:item.skuId,isChecked}) ; //这里要传多少个参数,得看原来的action里是多少个参数async updateCheckedById({commit},{skuId,isChecked})
  6. PromiseAll.push(promise);
  7. });
  8. // 最终返回结果
  9. return Promise.all(PromiseAll);
  10. },

Shopcart--index.js代码如下

  1. // 修改全部产品选中的状态
  2. async updateAllCartChecked(event){
  3. let isChecked = event.target.checked?"1":"0";
  4. // 派发action
  5. try {
  6. await this.$store.dispatch('updateAllCartChecked',isChecked);
  7. this.getData();
  8. } catch (error) {
  9. alert(error.message);
  10. }
  11. }

42,注册(表单验证)

42.1,ES6  const语法

const {comment,index,context} = this

上面的这句话是一个简写,最终的含义相当于:

const  comment = this.comment

const  index = this.index

const  context= this.context

42.2,获取验证码

42.3,注册

Store-user.js代码如下:

  1. // 用户注册
  2. async userRegister({commit},user){ //函数里面的参数只是形参,具体传什么参数,要到组件里派发action再传,这里的形参是user,是个对象,真正的参数是{phone,code,password}
  3. let result = await reqUserRegister(user);
  4. if(result.code==200){
  5. commit("GETCODE",result.data);
  6. return 'ok' //这里要知道是否注册成功,没有返回data,所以不需要数据三连环
  7. }else{
  8. return Promise.reject(new Error('faile'));
  9. }
  10. },

Register---index.js代码如下

  1. // 注册
  2. async userRegister(){
  3. const success = await this.$validator.validateAll();
  4. //全部表单验证成功再向服务器发请求,进行注册,只要有一个表单不成功,不会发请求
  5. if(success){
  6. try {
  7. const{phone,code,password,password1} = this;
  8. // (phone&&code&&password==password1) && await this.$store.dispatch('userRegister',{phone,code,password}); 表单全部验证成功就不需要前面的东西了
  9. await this.$store.dispatch('userRegister',{phone,code,password});
  10. // 如果成功,路由跳转到登录页面
  11. this.$router.push('/login') //这里填的是路由index.js里面的路径
  12. } catch (error) {
  13. alert(error.message);
  14. }
  15. }
  16. },

42.4,表单验证

表单验证个人推荐使用element ui的from表单验证,看一下官网的示例就会用。

element ui from表单验证链接

npm i vee-validate@2

main.js文件代码:

  1. // 引入表单校验插件
  2. import "@/plugins/validate";

根目录下新建plugins文件夹,新建validate.js文件

第一步:插件安装与引入

  1. // vee-validate插件:表单验证区域
  2. import Vue from 'vue';
  3. import VeeValidate from 'vee-validate';
  4. // 中文提示信息
  5. import zh_CN from 'vee-validate/dist/locale/zh_CN'
  6. Vue.use(VeeValidate);

第二步:提示信息

  1. // 表单验证
  2. VeeValidate.Validator.localize('zh_CN', {
  3. messages: {
  4. ...zh_CN.messages,
  5. is: (field) => `${field}必须与密码相同` // 修改内置规则的 message,让确认密码和密码相同
  6. },
  7. attributes: { // 给校验的 field 属性名映射中文名称
  8. phone: '手机号', //如果这里不写手机号,会显示phone格式无效
  9. code: '验证码',
  10. password:'密码',
  11. password1:'确认密码',
  12. agree:'协议'
  13. }
  14. });

第三步:基本使用

  1. <div class="content">
  2. <label>手机号:</label>
  3. <input placeholder="请输入你的手机号" v-model="phone" name="phone" v-validate="{ required: true, regex: /^1\d{10}$/ }" :class="{ invalid: errors.has('phone') }" />
  4. <span class="error-msg">{{ errors.first("phone") }}</span>
  5. </div>
  6. <div class="controls">
  7. <input type="checkbox" v-model="agree" name="agree" v-validate="{ required: true, 'agree':true }" :class="{ invalid: errors.has('agree') }" />
  8. <span>同意协议并注册《尚品汇用户协议》</span>
  9. <span class="error-msg">{{ errors.first("agree") }}</span>
  10. </div>
  11. // 自定义校验规则
  12. VeeValidate.Validator.extend('agree', {
  13. validate: value => {
  14. return value
  15. },
  16. getMessage: field => field + '必须同意'
  17. });

43,登录(token)

43.1,登录

  1. const state = {
  2. code:"",
  3. // token: localStorage.getItem('TOKEN'), 这样写本来就是null,与空字符无差别,点击注册按钮,就会派发action,获取到服务器的token,vuex三连环修改,就会将这个数据由null改为真实token
  4. // token永久化存储了,下一次就从本地存储里捞取token
  5. token:getToken(),
  6. userInfo:{},
  7. };
  8. // 登录
  9. async userLogin({commit},data){ //这里带参data其实是个对象,带的是{phone,password}
  10. let result = await reqUserLogin(data);
  11. // 服务器下发token,用户唯一标识(类似于uuid)
  12. // 将来经常通过带token找服务器要用户信息进行展示,比如用户登录之后,需要在顶部显示用户名
  13. if(result.code==200){
  14. // 用户已经登录成功且获取到token
  15. commit("USERLOGIN",result.data.token);
  16. // localStorage.setItem("TOKEN",result.data.token);
  17. // 持久化存储token
  18. setToken(result.data.token);
  19. return 'ok'
  20. }else{
  21. return Promise.reject(new Error('faile'));

43.2,登录后展示用户信息

登陆成功时,后台为了区分你这个用户是谁,服务器下发token(令牌,唯一标识),获取到token,存储于仓库当中(非持久化),路由跳转到home首页

登录接口:做的不完美,一般登陆成功服务器会下发token,前台持久化存储token,(带着token找服务器要用户信息进行展示)

一刷新home 首页,获取不到用户信息(token:vuex非持久化存储)

持久化存储token,在utils文件夹下新建token.js文件

  1. // 对外暴露一个函数,存储token
  2. export const setToken = (token)=>{
  3. localStorage.setItem('TOKEN',token);
  4. }
  5. // 获取token
  6. export const getToken = ()=>{
  7. // 这里要有返回值,在仓库才能调用
  8. return localStorage.getItem('TOKEN');
  9. }
  10. // 清除本地存储的token
  11. export const removeToken =()=>{
  12. localStorage.removeItem('TOKEN');
  13. }

Api-request.js

  1. // 请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求发出去之前做一些事情
  2. requests.interceptors.request.use((config)=>{
  3. // config:配置对象,对象里面有一个属性很重要,headers请求头
  4. // 进度条开始
  5. if(store.state.detail.uuid_token){
  6. // 请求头添加一个字段:和后台老师商量好了
  7. config.headers.userTempId=store.state.detail.uuid_token;
  8. }
  9. // 需要携带token带给服务器
  10. if(store.state.user.token){ //一旦仓库里面有token,就带给服务器
  11. config.headers.token = store.state.user.token;
  12. }
  13. nprogress.start();
  14. return config;
  15. });

43.3,退出登录

  1. const mutations = {
  2. // 清除本地数据
  3. CLEAR(state){
  4. // 帮仓库中相关用户信息情况
  5. state.token='';
  6. state.userInfo={};
  7. // 本地存储数据清空
  8. removeToken();
  9. }
  10. // 退出登录
  11. async userLogout({commit}){
  12. // 只是向服务器发起一次请求,通知服务器清除token
  13. let result = await reqLogout();
  14. // action里面不能操作state,提交mutation去修改state
  15. if(result.code==200){
  16. commit("CLEAR");
  17. return'ok';
  18. }else{
  19. return Promise.reject(new Error('faile'));
  20. }
  21. }

44,全局守卫

导航:表示路由正在发生改变,进行路由跳转

守卫:你把它当做紫禁城守卫

 

全局守卫:你的项目当中只要发生路由变化,守卫就能监听到

举例子:紫禁城【皇帝,太后,妃子】,紫禁城大门守卫全要排查

路由独享守卫

举例子:紫禁城【皇帝,太厚,妃子】,是相应【皇帝,太后,妃子】路上守卫

组件内守卫

举例子:我要去皇帝的屋子,只负责皇帝屋子的门口

比如:用户已经登录,用户不应该还会在login页面

 

router index.js全局前置守卫代码

  1. // 全局守卫:前置守卫(在路由跳转之前进行判断)
  2. router.beforeEach(async(to,from,next)=>{ //next放行
  3. // to:获取到你要跳转到哪个路由信息
  4. // from:获取你从哪个路由跳转而来的信息
  5. // next:放行函数 写法:next(); next(path);放行到指定的路由 next(false);
  6. next();
  7. // 用户登陆了,才会有token,未登录不会有token
  8. let token = store.state.user.token;
  9. // 用户信息
  10. let name = store.state.user.userInfo.name; //这里不能直接通过userInfo来判断,因为即使是空对象,它的布偶值也为真,所以要通过对象里面的属性来判断
  11. // 用户已经登陆了
  12. if(token){
  13. // 用户已经登陆了不能再去login(不能去,停留在home首页)
  14. if(to.path=='/login'){
  15. next('/home');
  16. }else{
  17. // 登陆了,但是去的不是login【可能是home,shopcart,search】
  18. // 如果用户名已有
  19. if(name){
  20. next();
  21. }else{
  22. // 没有用户信息,要派发action让仓库存储用户信息再跳转
  23. try {
  24. // 获取用户信息成功
  25. await store.dispatch('getUserInfo');
  26. // 放行
  27. next();
  28. } catch (error) {
  29. // token失效了,获取不到 用户信息,重新登录
  30. // 清除token
  31. await store.dispatch('userLogout');
  32. next('/login');
  33. }
  34. }
  35. }
  36. }else{
  37. // 未登录,不能去交易相关,支付相关【pay,paySuccess】界面,不能去个人中心
  38. // 未登录去上面的路由,-----登录
  39. let toPath = to.path;
  40. // if(toPath=='/trade')
  41. if(toPath.indexOf('/trade')!=-1 || toPath.indexOf('/pay')!=-1 || toPath.indexOf('/center')!=-1){
  42. // 把未登录的时候想去但是没去成的信息,存储于地址栏中【路由】
  43. next('/login?redirect='+toPath);
  44. }else{
  45. // 去的不是上面这些路由【home,search,shopCart】----放行
  46. next();
  47. }
  48. }
  49. })

45,交易界面(不使用vuex)

45.1,统一接收api

Main.js文件代码如下

  1. // 统一接收api文件夹里面全部请求函数
  2. // 统一引入
  3. import * as API from '@/api';
  4. new Vue({
  5. render: h => h(App),
  6. // 全局事件总线$bus配置
  7. beforeCreate(){
  8. Vue.prototype.$bus = this; //这里的this是VM
  9. Vue.prototype.$API = API; //所有的请求接口统一接收到了,且挂载在Vue.prototype原型对象上,这样所有的组件都不需要一个一个引入了,直接找这个对象去用
  10. },
  11. // 注册路由,底下的写法KV一致省略V【router小写的】
  12. // 当这里书写router的时候,组件身上都拥有$route,$router属性
  13. router,
  14. // 注册仓库:组件实例的身上会多个属性$store属性
  15. store,
  16. }).$mount('#app')

45.2,提交订单

  1. data() {
  2. return {
  3. // 收集买家留言的信息
  4. msg:'',
  5. // 订单号
  6. orderId:''
  7. }
  8. },
  9. // 提交订单
  10. async submitOrder(){
  11. // console.log(this.$API);
  12. // 交易编码
  13. let {tradeNo} = this.orderInfo; //解构 orderInfo:{tradeNo:xxxxxx}
  14. // 其余的六个参数
  15. let data = {
  16. consignee: this.userDefaultAddress.consignee, //最终的收件人名字
  17. consigneeTel: this.userDefaultAddress.phoneNum, //最终的收件人手机号
  18. deliveryAddress:this.userDefaultAddress.fullAddress, //收件人地址
  19. paymentWay: "ONLINE", //支付方式
  20. orderComment: this.msg, //买家的留言信息
  21. orderDetailList: this.orderInfo.detailArrayList, //商品清单
  22. };
  23. // 需要带参数:tradeNo,data
  24. let result = await this.$API.reqSubmitOrder(tradeNo,data);
  25. console.log(result);
  26. // 提交订单成功
  27. if(result.code==200){
  28. this.orderId = result.data;
  29. // 路由跳转+路由传参
  30. this.$router.push('/pay?orderId='+this.orderId)
  31. // 提交订单失败
  32. }else{
  33. alert(result.message)
  34. }
  35. }

46,qrcode和弹窗

46.1,弹窗

Element UI基本使

npm i element-ui -S

 

按需引入

借助 babel-plugin-component,我们可以只引入需要的组件,以达到减小项目体积的目的。

首先,安装 babel-plugin-component:

npm install babel-plugin-component -D

然后,将 .babelrc 修改为:

然后,将 .babelrc(.babel.config.js) 修改为:

  1. module.exports = {
  2. presets: [
  3. '@vue/cli-plugin-babel/preset'
  4. ],
  5. "plugins": [
  6. [
  7. "component",
  8. {
  9. "libraryName": "element-ui",
  10. "styleLibraryName": "theme-chalk"
  11. }
  12. ]
  13. ]
  14. }
  • 文档中说的.babelrc文件,即为babel.config.js文件
  • 修改完babel.config.js配置文件以后,项目重启

接下来,如果你只希望引入部分组件,那么需要在 main.js 中写入以下内容:

按照需求引入相应的组件即可

Vue.component();

Vue.prototype.$xxx = xxx;

  1. import { Button, MessageBox } from 'element-ui';
  2. // 注册全局组件,用elementUI里面的button还有MessageBox弹窗
  3. Vue.component(Button.name,Button);
  4. // ElementUI注册组件的时候,还有一种写法,挂在原型上
  5. Vue.prototype.$msgbox = MessageBox;
  6. Vue.prototype.$alert = MessageBox.alert;

46.2,qrcode

npm i qrcode --save

  1. import QRCode from 'qrcode'
  2. // 点击支付弹窗
  3. async open() {
  4. // 生成二维码(地址)
  5. let url = await QRCode.toDataURL(this.payInfo.codeUrl);
  6. this.$alert(<img src={url} />,"请你微信支付", {
  7. dangerouslyUserHTMLString:true,
  8. // 中间布局
  9. center:true,
  10. // 是否显示取消按钮
  11. showCancelButton:true,
  12. // 取消按钮的文本
  13. cancelButtonText:"支付遇见问题",
  14. // 确定按钮的文本
  15. confirmButtonText:"已支付成功",
  16. // 右上角的叉子没了
  17. showClose:false,
  18. //关闭弹出框的配置值
  19. beforeClose:(type,instance,done)=>{
  20. // type:区分取消还是确定按钮
  21. // instance:当前组件实例
  22. // done:关闭弹出框方法
  23. if(type=='cancel'){ // 点击支付遇见问题
  24. alert('请联系管理员');
  25. // 清除定时器
  26. clearInterval(this.timer);
  27. this.timer = null;
  28. // 关闭弹出框
  29. done();
  30. }else{ // 点击已支付成功
  31. // 判断是否真的支付
  32. // 开发人员为了自己方便,这里判断先不要了
  33. // if(this.code==200){
  34. clearInterval(this.timer);
  35. this.timer = null;
  36. done();
  37. this.$router.push('/paysuccess');
  38. // }
  39. }
  40. }
  41. });
  42. // 你需要知道是支付成功还是失败
  43. // 支付成功,路由跳转,如果支付失败,提示信息
  44. // 定时器没有,开启一个新的定时器
  45. if(!this.timer){
  46. this.timer = setInterval(async()=>{
  47. // 发请求获取用户支付状态
  48. let result = await this.$API.reqPayStatus(this.orderId);
  49. console.log(result);
  50. if(result.code==200){
  51. // 支付成功
  52. // 第一部:清除定时器
  53. clearInterval(this.timer);
  54. // 保存支付成功返回的code,防止没有扫码直接点支付成功
  55. this.code = result.code;
  56. // 关闭弹出框
  57. this.$msgbox.close();
  58. // 跳转到下一页路由
  59. this.$router.push('/paySuccess');
  60. }
  61. },1000)
  62. }
  63. }

判断支付状态

GET|POST:短轮询,请求发一次,服务器响应一次,完事。

第一种做法:前端开启定时器,一直找服务器要用户支付信息【定时器】

第二种做法:项目务必要上线 + 和后台紧密配合

当用户支付成功以后,需要后台重定向到项目某一个路由中,将支付情况通过URL参数形式传给前端,

前端获取到服务器返回的参数,就可以判断了。

47,个人中心(我的订单二级路由)

routers.js

注意: 二级路由要么不写/,要么写全:‘/center/myorder’。

{ path: '', redirect: 'myorder' }表示当我们访问center路由时,center中的router-view部分默认显示myorder二级路由内容。

  1. {
  2. path: "/center",
  3. component: Center,
  4. meta: { show: true },
  5. // 二级路由组件
  6. children:[
  7. {
  8. path:"myorder",
  9. component:MyOrder,
  10. },
  11. {
  12. path:"grouporder",
  13. component:GroupOrder,
  14. },
  15. {
  16. // 重定向
  17. path:'/center',
  18. redirect:'/center/myorder'
  19. }
  20. ]
  21. },

48,组件独享守卫

 只要从购物车界面才能跳转到交易界面(创建订单)

只有从交易界面(创建订单)页面才能跳转到支付页面

只有从支付页面才能跳转到支付成功页面

routers.js

  1. {
  2. path: "/trade",
  3. component: Trade,
  4. meta: { show: true },
  5. // 路由独享守卫
  6. beforeEnter: (to, from, next) => {
  7. // 去交易界面,必须是从购物车而来
  8. if(from.path=="/shopcart"){
  9. next();
  10. }else{
  11. // 其他的路由组件而来,停留在当前
  12. next(false);
  13. }
  14. }
  15. },

49,图片懒加载

npm i vue-lazyload@1.3.3

  1. // 引入插件
  2. import VueLazyload from "vue-lazyload";
  3. // 引入图片
  4. import atm from '@/assets/1.gif';
  5. // 注册插件
  6. Vue.use(VueLazyload,{
  7. // 懒加载默认的图片
  8. loading:atm
  9. });
  10. 组件内图片位置要改成
  11. <!-- <img :src="good.defaultImg"/> -->
  12. <img v-lazy="good.defaultImg"/>

50,路由懒加载

路由懒加载链接

Routes.js

  1. // 不再用这种写法
  2. // 引入一级路由组件
  3. // import Home from '@/pages/Home';
  4. // import Search from '@/pages/search';
  1. {
  2. path: "/home",
  3. component: ()=>import("@/pages/Home"), //当访问到home的时候才会执行
  4. meta: { show: true },
  5. },
  6. {
  7. path: "/search/:keyword?", //要先占位
  8. component: ()=>import("@/pages/Search"),
  9. meta: { show: true },
  10. name: "search",
  11. // 路由组件能不能传递props数据?
  12. // 布尔值写法:params
  13. // props:true,
  14. // 对象写法:额外给路由组件传递一些props
  15. // props:{a:1,b:2}
  16. // 函数写法:可以params参数、query参数,通过props传递给路由组件
  17. props: ($route) => ({ keyword: $route.params.keyword, k: $route.query.k })
  18. },

51,打包项目

项目到此基本就完成了,接下来就是打包上线。在项目文件夹下执行npm run build。会生成dist打包文件。

dist就是我们打包好的项目文件

dist文件下的js文件存放我们所有的js文件,并且经过了加密,并且还会生成对应的map文件。

map文件作用:因为代码是经过加密的,如果运行时报错,输出的错误信息无法准确得知时那里的代码报错。有了map就可以向未加密的代码一样,准确的输出是哪一行那一列有错。

当然map文件也可以去除(map文件大小还是比较大的)

在vue.config.js配置productionSourceMap: false即可。

注意:vue.config.js配置改变,需要重启项目

51.1,打包  文件夹右键git bash ----npm run build

项目打包后,代码都是经过压缩加密的,如果运行时报错,输出的错误信息无法准确得知是哪里的代码报错

有了map就可以像未加密的代码一样,准确的输出是哪一行哪一列有错

所以该文件如果项目不需要是可以去除调

vue.config.js配置

productionSourceMap:false,

51.2,购买服务器

1,阿里云 2,腾讯云  

2,设置安全组,让服务器一些端口号打开

3,利用xshell工具登录服务器

4,linus:/更目录

3,linux常用指令:cd跳转目录  ls查看 mkdir创建目录  pwd:查看绝对路径

51.3,nginx

1:为什么访问服务器io地址就可以访问到我们的项目--配置一些东西

刚刚在服务器上=>root/jch/www/shangpinhui/dist

http://182.92.128.115;

2.项目的数据源来自于http://gmall-h5-api.atguigu.cn,

3,nginx配置

1,xshell进入根目录/ect

2,进入etc目录,这个目录下有一个nginx目录,进入到这个目录【已经安装过nginx:如果没安装过,四五个文件】

3,如果想安装nginx :yum install nginx

4,安装完nginx服务器之后,你会发现在nginx目录下,多了一个nginx.conf文件,在这个文件中进行配置

5,vim nginx.conf进行编辑,主要添加如下两项

location / {

  root /root/jch/www/shangpinhui/dist

  index index.html;

  try_files $uri/ /index.html;

}

//解决第二个问题

locatin /api{

  proxy_pass ttp://gmall-h5-api.atguigu.cn;

},

51.4,nginx服务器跑起来

service nginx start

52,组件通信方式

52.1,porps

适用于场景:父子组件通信

注意事项:

如果父组件给子组件传递数据(函数):本质是子组件给父组件传递数据

如果父组件给子组件传递的数据(非函数):本质就是父组件给子组件传递数据

书写方式:3种

['todos'] , {type:Array,default[ ]}

小提示:路由的props

书写形式:布偶值,对象,函数形式

52.2,自定义事件

适用于场景:子组件给父组件传递数据

$on  $emit

52.3,全局事件总线$bus

适用于场景:万能

Vue.protype.$bus = this ;

52.4,Pubsub-js在React框架中使用比较多(发布于订阅)

适用于场景:万能

52.5,Vuex

适用于场景:万能

52.6,插槽

适用于场景:父子组件通信(一般结构

默认插槽

具名插槽

作用域插槽

53,自定义事件

事件:系统事件(原生DOM事件):click,双击,鼠标系列等等

 自定义事件:事件源:给谁绑定

                      事件类型

                      事件回调

1,原生DOM---button可以绑定系统事件--click单机事件等等

2,组件标签--event1可以绑定系统事件(不起作用:以内属于自定义事件)---native(可以把自定义事件变为原生DOM事件)

  1. <!-- event1组件不是原生DOM节点,而绑定的click事件并非原生DOM事件,而是自定义事件
  2. @click.native,可以把自定义事件变为原生DOM事件
  3. 当前原生DOM click事件,其实是给子组件的根节点绑定了点击事件---利用了事件的委派(不管点击event1组件里的h2还是span都会触发点击事件 -->
  4. <Event1 @click.native="handler1"></Event1>
  5. <!-- 下面的写法是给原生DOM绑定自定义事件,是没有意义的,因为没有办法触发$emit函数 -->
  6. <button @xxx="handler4">原生DOM绑定自定义事件</button>

组件标签身上写的@click是自定义事件

<Event2 @click="handler3" @xxx="handler4"></Event2>

在原生DOM写的@click是原生DOM事件

<button @click="$emit('xxx','我是自定义事件xxx')">分发自定义xxx事件</button><br>

handler3(params){

      console.log(params);

   },

  1. import Event1 from './Event1.vue'
  2. import Event2 from './Event2.vue'
  3. export default {
  4. name: 'EventTest',
  5. components: {
  6. Event1,
  7. Event2,
  8. },

54,v-model(组件通信方式的一种)

v-model是Vue框架指令,它主要结合表单元素一起使用(文本框,复选,单选框等)

主要的作用是收集表单数据

v-model实现原理:value与input事件实现的,而且还需要注意可以通过v-model实现父子组件数据同步

父组件代码

  1. <!-- 原生DOM当中是有oninput事件,它经常结合表单元素一起使用,当表单元素文本内容发生变化的时候就会触发一次回调
  2. Vue2可以通过valule与input事件实现v-model功能 -->
  3. <input type="text" placeholder="与v-model功能一样" :value="msg1" @input="msg1=$event.target.value"/>
  4. <!-- 这里的:value是props,父子组件通信
  5. @input到底是什么?非原生DOM的input事件,属于自定义事件,当子组件触发自定义事件时,修改父组件中的msg2为自定义事件传递过来的参数-->
  6. <CustomInput :value="msg2" @input="inputHandler"></CustomInput>
  7. <!-- 底下的代码与顶上的代码原理一样的 -->
  8. <CustomInput v-model="msg2"></CustomInput>
  9. //自定义事件input回调
  10. inputHandler(params){
  11. this.msg2 = params;
  12. }

子组件代码

  1. <h2>input包装组件</h2>
  2. <!-- 这里的:value是给原型DOM标签绑定原型响应式,动态属性 -->
  3. <!-- 这里的@input,是给原生DOM绑定单击事件 -->
  4. <input type="text" :value="value" @input="$emit('input',$event.target.value)">

55,sync属性修饰符

属性修饰符.sync,组件通信方式的一种,可以实现父子数据同步。

父组件

  1. <!-- 给子组件绑定一个自定义事件:update:money -->
  2. <!-- $event ,在父组件监听这个事件的时候,可以通过$event访问到被抛出的这个值 -->
  3. <!-- :money:父组件给子组件传递props
  4. @update:money 给子组件绑定的自定义事件只不过名字叫做update:money
  5. 目前现在这种操作,其实和v-model相似,可以实现父子组件数据同步-->
  6. <Child :money="money" @update:money="money=$event"></Child>
  7. <h2>使用sync修改符</h2>
  8. <!-- :money.sync:第一,父组件给子组件传递props money
  9. 第二,给当前子组件绑定了一个自定义事件,而且事件名臣即为update:money -->
  10. <Child :money.sync="money"></Child>

子组件

  1. <span>小明每次花100元</span>
  2. <!-- 第二个参数,把组附件传递过来props的money拿来计算,然后包装成回调函数给父组件传递过去 -->
  3. <button @click="$emit('update:money',money-100)">花钱</button>
  4. 爸爸还剩{{money}}元

56,$attrs与$listeners

他们两者是组件实例的属性,可以获取到父组件给子组件传递props与自定义事件

父组件

  1. <template>
  2. <div>
  3. <hintButton type="warning" icon="el-icon-s-help" size="mini" tip="豪哥提示信息" @click="handler"></hintButton>
  4. </div>
  5. </template>
  6. <script type="text/ecmascript-6">
  7. import HintButton from './HintButton.vue'
  8. export default {
  9. name: 'AttrsListenersTest',
  10. components:{
  11. HintButton
  12. },
  13. methods: {
  14. handler(){
  15. alert(123);
  16. }
  17. },
  18. }
  19. </script>

子组件

  1. <template>
  2. <div>
  3. <!-- 可以巧妙利用a标签实现按钮带有提示功能 -->
  4. <a title="title"></a>
  5. <!-- 由于el-button身上可能有很多属性要写很多个,可以直接写$attrs,$attrs是父组件给子组件传的属性,但是这种写法不能用 : ,要直接写v-bind ,v-on也不能用@符号简化-->
  6. <el-button v-bind="$attrs" v-on="$listeners"></el-button>
  7. <el-button v-bind="$attrs.type"></el-button>
  8. </a>
  9. </div>
  10. </template>
  11. <script>
  12. export default {
  13. name:"",
  14. props:['title'],
  15. mounted() {
  16. // $attr属于组件的一个属性,可以获取到父组件传递过来props数据
  17. // 对于子组件而言,父组件给的数据可以利用props接收,但是需要注意,如果子组件通过props接收的属性,在$attr属性当中获取不到
  18. console.log(this.$attrs);
  19. //$listeners是组件实例自身的一个属性,它可以获取到父组件给子组件传递自定义事件
  20. console.log(this.$listeners);
  21. },
  22. }
  23. </script>

 57,$children与$parent

ref:可以在父组件内部获取子组件---实现父子通信

$children:可以在父组件内部获取全部的子组件【返回数组】

$parent:可以在子组件内部获取唯一的父组件【返回组件实例】

父组件

  1. <template>
  2. <div>
  3. <h2>BABA有存款: {{ money }}</h2>
  4. <button @click="borrowFromXM">找小明借钱100</button><br />
  5. <button @click="borrowFromXH">找小红借钱150</button><br />
  6. <button @click="borrowFromAll">找所有孩子借钱200</button><br />
  7. <br />
  8. <!-- ref:可以获取到真实DOM节点,可以获取相应组件的实例VC(操作子组件的数据与方法) -->
  9. <!-- ref也算在一种通信手段:在父组件中可以获取子组件(属性|方法) -->
  10. <Son ref="son" />
  11. <br />
  12. <Daughter ref="dau" />
  13. </div>
  14. </template>
  15. methods: {
  16. //小明借用100元
  17. borrowFromXM() {
  18. //父亲的钱加上100元
  19. this.money += 100;
  20. // <!-- ref :可以获取到真实DOM节点,可以获取相应组件的实例VC(操作子组件的数据与方法) -->
  21. this.$refs.son.money -= 100;
  22. },
  23. borrowFromXH() {
  24. this.money += 150;
  25. this.$refs.dau.money -= 150;
  26. },
  27. borrowFromAll() {
  28. //VC:组件实例自身有一个$children属性,可以获取当前组件的全部子组件[这个属性在用的时候很少用索引值获取子组件,因为没有办法确定数组里面的元素到底是哪一个子组件
  29. this.money += 400;
  30. this.$children.forEach((item) => {
  31. item.money -= 200;
  32. });
  33. // 切记别这样书写,如果子组件过多,第0项不确定是哪个
  34. // this.$childeren[0].money-=200;
  35. },
  36. },

子组件

  1. <template>
  2. <div style="background: #ccc; height: 50px;">
  3. <h3>儿子小明: 有存款: {{money}}</h3>
  4. <button @click="giveMoney">给BABA钱: 50</button>
  5. </div>
  6. </template>
  7. <script>
  8. export default {
  9. name: 'Son',
  10. data () {
  11. return {
  12. money: 30000
  13. }
  14. },
  15. methods: {
  16. giveMoney(){
  17. this.money-=50;
  18. //$parent,VC一个属性,可以获取当前组件(属性|方法)
  19. this.$parent.money+=50;
  20. }
  21. }
  22. }
  23. </script>

58,混入mixin

如果项目当中出现很多结构类似功能,想到组件复用。

如果项目当中很多的组件JS业务逻辑相似。想到mixin。【可以把多个组件JS部分重复、相似地方】

将不同的组件里共同的methods封装成一个js文件,组件使用的时候引入

  1. import myMixin from '.....'
  2. import Son from "./Son";
  3. import Daughter from "./Daughter";
  4. export default {
  5. name: "ChildrenParentTest",
  6. mixins:[myMixin]
  7. }

59,插槽

插槽:父子组件通信(HTML结构)

插槽slot:

默认插槽|具名插槽|作用域插槽

作用域插槽:子组件的数据来源于父组件,但是子组件决定不了自身结构与外观。

父组件

  1. <template>
  2. <div>
  3. <h1>作用域插槽</h1>
  4. <!-- 子组件:数据来源于父组件 -->
  5. <List :todos="todos">
  6. <!-- 子组件决定不了外观与结构,子组件的slot相当于留了一个坑 ,父组件往坑里填东西,填的就是结构-->
  7. <!-- <template v-slot="todo"> todo是对象,{"todo":{id: 1, text: 'AAA', isComplete: false},"$index:0",可解构出下面-->
  8. <template v-slot="{todo,$index}">
  9. <h1 :style="{color:todo.isComplete?'red':'cyan'}">{{todo.text}}----{{$index}}</h1>
  10. </template>
  11. </List>
  12. </div>
  13. </template>
  14. <script type="text/ecmascript-6">
  15. import List from './List'
  16. export default {
  17. name: 'ScopeSlotTest',
  18. data () {
  19. return {
  20. todos: [
  21. {id: 1, text: 'AAA', isComplete: false},
  22. {id: 2, text: 'BBB', isComplete: true},
  23. {id: 3, text: 'CCC', isComplete: false},
  24. {id: 4, text: 'DDD', isComplete: false},
  25. ]
  26. }
  27. },
  28. components: {
  29. List,
  30. }
  31. }
  32. </script>

子组件

  1. <template>
  2. <ul>
  3. <li v-for="(item,index) in todos" :key="index">
  4. <!-- 作用于插槽的使用,把数据回传给父组件 -->
  5. <!-- 下面写法起始不是props,就是作用域插槽的语法,将数据回传给父组件,这里的item指的是todos里的每个对象-->
  6. <!-- todo是数组里的每个元素,index是每个元素的索引值 -->
  7. <slot :todo="item" :$index="index"></slot>
  8. </li>
  9. </ul>
  10. </template>
  11. <script>
  12. export default {
  13. name: 'List',
  14. props: {
  15. todos: Array
  16. }
  17. }
  18. </script>

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/69846?site
推荐阅读
相关标签
  

闽ICP备14008679号