尚硅谷商品笔记
(天数是为了和视频分区,不是真实学习天数)

Day1

1.vue-cli脚手架初始化项目

node + webpack + 淘宝镜像cnpm

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

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

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

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

2.项目的其他配置:

2.1 项目运行起来的时候,浏览器自动打开

--package.json"scripts": {"serve": "vue-cli-service serve --open",...}

2.2 eslint校验功能关闭

在根目录下,找到(创建一个)vue.config.js

2.3 src文件夹简写方法,配置别名

jsconfig.json配置别名@提示 【@代表src文件,文件过多时寻找方便】

{"compilerOptions": {"baseUrl": "./","paths": {"@/*": ["src/*"]}},"exclude": ["node_modules", "dist"]
}

3.项目路由的分析

vue-router
前端所谓路由:KV键值对
key:URL (地址栏中的路径)
value:相对应的路由组件

注意:项目上中下结构,只有中间在发生变化

路由组件:
Home首页路由组件、search路由组件、login登录路由、register注册路由

非路由组件:
Header【首页、搜索页】
Footer【首页,搜索页】,但是在登录|注册页面没有

4.完成非路由组件Header与Footer业务

在项目当中,不以HTML + CSS为主,主要搞业务、逻辑
在开发项目的时候:
1.书写静态页面(HTML + CSS)
2.拆分组件
3.获得服务器的数据动态展示
4.完成相应的动态业务逻辑

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

注意2:项目采用的时less样式,浏览器不识别less样式,需要通过less、less-loader【安装版本@5】进行处理less,把less样式变为css样式,浏览器才可以识别.

注意3:如果想让组件识别less样式,需要在style标签上加上lang=“less”

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

非路由组件使用分为几步:
第一步:定义
第二步:引入
第三步:注册
第四步:使用

5.路由组件的搭建

路由组件:Home、Search、Login(没有底部的Footer组件,带有二维码的)、Register(没有底部的Footer组件,带二维码的)
-components文件夹:经常放置的非路由组件(公用全局组件)
-pages|views文件夹:经常放置路由组件
安装路由:npm install --save vue-router@3.5.3

5.1配置路由

项目中配置的路由一般放置在router文件夹中

5.2总结

路由组件与非路由组件的区别:
1.路由组件一般放置在pages|views文件夹,非路由组件一般放置在components文件夹
2.路由组件一般需要在router文件夹中进行注册(使用的即为组件名字),非路由组件在使用的时候,一般是以标签的形式使用。
3.注册完路由,不管路由组件还是非路由组件,其身上都有$route、$router属性

$route:一般获取路由信息【路径、query、params等等】
$router:一般进行编程式导航进行路由跳转【push|replace】

5.3路由的跳转

路由的跳转有两种形式:
1.声明式导航**router-link 务必要有to属性**
2.编程式导航push|replace编程式导航:声明式导航能做的,编程式导航都能做,但编程式导航除了可以进行路由跳转,还可以做一些其它的业务逻辑。

6.Footer组件显示与隐藏

显示或者隐藏组件:v-if | v-show

面试题:v-show与v-if区别?
v-show:通过样式display控制
v-if:通过元素上树与下树进行操作
面试题:开发项目的时候,优化手段有哪些?
1:v-show|v-if
2:按需加载

Footer组件:在Home、search显示 在登录、注册时候隐藏

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

<Footer v-show="$route.path=='/home'||$route.path=='/search'"></Footer>

6.2 配置路由的时候,可以给路由添加路由元信息【meta】,路由需要配置对象,它的key是固定的,不能瞎写

<Footer v-show="$route.meta.show"></Footer>

7.路由传参

7.1 路由跳转有几种方式?

参考5.3

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

params参数:路由需要占位,属于URL当中一部分。(属于路径当中的一部分,在配置路由时,需要占位。)
query参数:路由不需要占位,写法类似于ajax当中query参数 (不属于路径中的一部分,类似于ajax中的queryString /home?k=v&kv=, 不需要占位)

如:
搜索按钮的事件处理函数,用于跳转到search路由组件当中
router文件夹中的routes.js的search对应path也要更改成
path: ‘/search/:keyword’ :keyword 相当于占位符

goSearch() {
//路由传递参数1.字符串形式传递this.$router.push("/search/"+this.keyword+"?k="+this.keyword.toUpperCase());}2.模板字符串传递 (注意push的路径用"`"包裹)this.$router.push(`/search/${this.keyword}?k=${this.keyword.toUpperCase()}`)3.对象写法,最常用this.$router.push({name:"search",params:{keyword:this.keyword},query:{k:this.keyword.toUpperCase()}})

+this.keyword_是params参数,+"?k="+this.keyword.toUpperCase()是query参数
获取参数值
params参数{{$route.params.keyword}} query参数{{$route.query.k}}

v-model就是vue的双向绑定的指令,能将页面上控件输入的值同步更新到相关绑定的data属性,也会在更新data绑定属性时候,更新页面上输入控件的值。

路由传递参数相关面试题

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

如题目要求的写法:
$router.push({path:'/search',params:{keyword:this.keyword},query:{k:this.keyword.toUpperCase()}});
这种写法是不可以的,运行的时候只能跳转不能成功传参,官网声明解释为路径参数缺失是无法匹配path里面的占位符
路由跳转传参的时候对象的写法可以是name或者path形式,但是path这种写法不能与params参数一起使用

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

比如:配置路由的时候,params参数占位了,但是路由跳转的时候就不传递
如题目要求的写法: this.$router.push({name:'search',query:{k:this.keyword.toUpperCase()}})
此时跳转之后路径中的search也会消失
所以就是,如果路由要求传递params参数,但是你就不传递,此时URL会有问题
如何指定params参数可传递或不传递,在配置路由的时候,可以在占位的后面加上一个问号 即path: '/search/:keyword?'

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

如题目要求写法:
this.$router.push({name:"search",params:{keyword:''},query:{k:this.keyword.toUpperCase()}})
此时跳转后路径中的search也消失了
**解决方法:使用undefined** params参数可以传递、不传递(空的字符串)
即可以改为params:{keyword:''||undefined} 此时路径无问题,只是无params参数

4:如果指定name与params配置, 但params中数据是一个""(空字符串), 无法跳转,此时路径会出问题

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

可以 有三种写法
1.布尔值写法  可以把params参数作为路由自己的属性在search路由加上 props: true 在search.vue上接收参数 props:['keyword']此时keyword成为search自己的属性,可以直接借此获取params参数{{keyword}}
2.对象写法   可以额外给路由组件传递propsprops: {a:1,b:2} props:['keyword', 'a', 'b']此时a和b也作为属性可以直接获取
3.函数写法(最常用写法)   可以将params参数,query参数传递给路由组件props:($route)=>{return {keyword:$route.params.keyword,k:$route.query.k}} 可缩写成:props:($route)=>({keyword:$route.params.keyword,k:$route.query.k})}props:['keyword','k']

Day2

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

–声明式导航没有这类问题,因为vue-router底层已经处理好了 最新的vue-router引入了promis

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

push是一个promise,promise需要传递成功和失败两个参数,我们的push中没有传递。
push( location: RawLocation, onComplete?: Function, onAbort?: ErrorHandler ): void

1.2通过给push方法传递相应的成功、失败的回调函数,可以捕获到当前的错误来解决

1.3可以通过底部的代码实现错误的解决

即:this.$router.push({name:‘Search’,params:{keyword:"…"||undefined}},()=>{},()=>{})后面两项分别代表执行成功和失败的回调函数。
这种写法治标不治本,将来在别的组件中push|replace,编程式导航还是会有类似错误,每个编程式导航都需要手动添加回调函数,代码复杂

1.4解决的标准方法

this:当前组件实例(search)
this.$router属性:是一个对象,当前的这个属性是属性值VueRouter类的一个实例,当入口文件注册路由的时候,给组件添加$router|$route属性
push:VueRouter类的一个实例
即push是VueRouter.prototype原型对象的一个方法,在router中的index重写该方法即可

//1、先把VueRouter原型对象的push,保存一份
let originPush = VueRouter.prototype.push;
//2、重写push|replace
//第一个参数:告诉原来的push,跳转的目标位置和传递了哪些参数
VueRouter.prototype.push = function (location,resolve,reject){if(resolve && reject){originPush.call(this,location,resolve,reject)}else{originPush.call(this,location,() => {},() => {})}
}

call || apply区别(属于js)
相同点:都可以调用函数一次,都可以篡改函数的上下文一次
不同点:call与apply传递参数:call传递参数用逗号隔开,apply方法执行传递数组

2.Home模块组件拆分

–静态页面(样式)
–拆分静态组件
–发请求获取服务器数据进行展示
–开发动态业务
拆分组件:结构+样式+图片资源

一共要拆分为七个组件

3.三级联动组件完成

–由于三级联动在Home、Search、Detail都有,所以把三级联动注册为全局组件
只用注册一次就可以在项目任意地方使用

4.完成其余静态组件

HTML + CSS + 图片资源

5.POSTMAN接口测试

(视频陈述,未自测)
—如果服务器返回的数据code字段200,代表服务器返回数据成功
—整个项目,接口前缀都有/api字符

6.axios二次封装 *重点

6.1为什么需要进行二次封装axios?

请求拦截器:可以在发送请求之前处理一些业务
响应拦截器:当服务器数据返回以后,可以处理一些事情
—安装axios: npm install --save axios

6.2在项目当中【axios】通常用API文件夹配置

request.js是创造axios实例和axios拦截器以及发送真正的网络请求的地方
1.利用axios对象的方法create,创造一个axios
const requests = axios.create({ baseURL: "/api", timeout: 5000, });

请求拦截器
requests.interceptors.request.use((config) => { return config; });
config:配置对象,对象里面有一个属性很重要,headers请求头
—请求头是 HTTP 头的一种,它可在 HTTP 请求中使用,并且和请求主体无关 。某些请求头如 Accept、Accept-*、 If-* 允许执行条件请求。某些请求头如:Cookie, User-Agent 和 Referer 描述了请求本身以确保服务端能返回正确的响应。

响应拦截器----服务器手动请求成功的回调函数
requests.interceptors.response.use((res) => { return res.data; }, (err) => { return Promise.reject(new Error('fail')); } );
对外暴露一个函数,只要外部调用这个函数,就想服务器发起ajax请求、获取数据。当前这个函数只需要把服务器返回结果返回即可。
export const reqgetCategoryList = () =>requests.get(`/product/getBaseCategoryList`);

测试请求成不成功:(在main.js测试,先引入再调用)
import {reqgetCategoryList} from '@/api' reqgetCategoryList();

6.3可以在git|NPM查看axios相关文档

7.接口统一管理

项目很小:可以在组件的生命周期函数中发请求
项目大:将接口进行统一管理

7.1跨域问题

什么是跨域:
浏览器从一个域名的网页去请求另一个域名的资源时,域名、端口、协议任一不同,都是跨域
注意:
1、端口和协议的不同,只能通过后台来解决
2、localhost和127.0.0.1虽然都指向本机,但也属于跨域
3.服务器和服务器之间没有跨域问题,浏览器之间才有跨域问题
跨域限制:
1、无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB
2、无法接触非同源网页的 DOM
3、无法向非同源地址发送 AJAX 请求(可以发送,但浏览器会拒绝接受响应)

http://localhost:8080/#/home ----前端项目的本地服务器
http://39.98.123.211 ----后台服务器
(只有协议相同,域名和端口号都不同,所以会报错404)

7.2如何解决跨域问题

JSONP、CROS、代理

webpack官网->配置->devserver->proxy 放在vue.config.js里面代理跨域
devServer:{ proxy: { "/api": { target: "http://localhost:3000", pathRewrite: {"^/api" : ""} } } }
此时只要浏览器路径中出现"/api",代理服务器会找真实服务器要数据
配置文件需要重新运行才会正常展示

8.nprogress进度条的使用

安装:npm ionstall --save nprogress

在axios请求数据时使用进度条(进度条是一个对象,里面有很多方法)
在请求拦截器使用nprogress.start();表示进度条开始
在响应拦截器使用nprogress.done();表示进度条结束

如果出现进度条没有显示:此时还未引入进度条样式
import "nprogress/nprogress.css";
可以在node_modules文件夹中搜索nprogress,找到css文件手动修改进度条的默认样式

9.vuex状态管理库

9.1 vuex是什么

vuex是官方提供的一个插件,状态管理库,集中式管理项目中组件共用的数据
项目很小的时候完全不用需要vuex,项目大、组件多、数据多、维修费劲的时候才需要vuex

state:仓库存储数据的地方
mutations:修改state的唯一手段
actions:处理action,可以书写自己的业务逻辑,不可以修改state,但可以处理异步,
getters:理解为计算属性,用于简化仓库数据,让组件获取仓库的数据更加方便
modules:模块式开发

安装vuex npm install --save vuex@3.6.2 (和项目的vue版本兼容)

9.2 vuex基本使用

mapState 辅助函数:—(有数组形式和对象形式,常用对象形式)
  当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性,让你少按几次键.
import {mapState} from 'vuex';
...mapState({ categoryList:state=>state.home.categoryList, ... })
将数据保存到当前组件中

9.3 vuex实现模块式开发

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象,当应用变得复杂时,store对象会变得非常臃肿。
vuex的store为一个大仓库,但可以把大仓库通过modules变成一个个小仓库,每个小仓库都有自己的state、mutation、action、getter,实现模块式开发,更利于维修。

10.完成TypeNav三级联动展示数据业务

异步发送请求获得数据

async getCategoryList({ commit }) {let result = await reqgetCategoryList();if (result.code == 200) {commit("GETCATEGORYLIST", result.data);}}

需要用await接受成功返回的结果,await必须要结合async一起使用,其优势在于处理 then 链
—async 是“异步”的简写,而 await 可以认为是 async wait 的简写。所以应该很好理解 async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成,等待的是一个返回值。而async 函数返回的是一个 Promise 对象

请求数据:
this.$store.dispatch('模块/方法');此时模块的命名空间namespaced: true
this.$store.dispatch('方法'); 此时模块的命名空间namespaced: false,默认为false

commit和dispatch的使用对象
----this.$store.commit()触发->mutations ----this.$store.dispatch()触发->actions

Day3

1.通过JS完成一级分类动态添加背景颜色

2.通过JS控制二三级商品分类的显示与隐藏

3.卡顿现象

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

lodash插件:里面封装函数的防抖与节流的业务【闭包+延时器】
lodas函数库对外暴露_函数

防抖: 前面的所有触发都被取消,最后一次执行在规定时间之后才会触发,也就是说连续快速的触发只会执行一次
防抖函数:
_.deboundce(function(){ ... },1000)

节流: 在规定时间间隔范围内不会重复触发回调,只有大于这个时间间隔才触发,把频繁触发变为少量触发
类似于验证码,只有时间间隔之后才能再次点击
节流函数:
_.throttle(function(){ ... },1000)

4.完成三级联动的节流操作

import {throttle} from 'lodash';直接导入具体要使用的函数

5.三级联动组件的路由跳转与传递参数

点击三级联动,Home模块跳转到Search模块: 一级会把用户选中的产品(名字和ID)在路由跳转的时候进行传递

如果使用声明式导航router-link,可以实现路由的跳转和参数的传递,但是会出现卡顿现象
原因:router-link可以作为一个组件,当服务器的数据返回后,循环出很多的router-link组件,而创建组件实例的时候,瞬间创建会出现卡顿

解决方法:【编程式导航+事件委派】
存在的问题:事件委派是把全部子节点【h3、dt、dl、em】的事件委派给父亲节点【div】。此时点击a标签的时候才会路由跳转,那么怎么确定点击的一定是a标签?即使确定的是a标签,怎么确定是哪一级的a标签

怎么确定是a标签:把子节点当中的a标签加上自定义属性,其余节点是没有的
<a :data-categoryName="c1.categoryName" :data-category1Id="c1.categoryId" >{{ c1.categoryName }}</a>
怎么确定是哪一级的a标签:

goSearch(event) {let element = event.target; //获得点击的节点信息// 节点有一个属性dataset,可以获得节点的自定义属性与属性值let { categoryname, category1id, category2id, category3id } = element.dataset;if (categoryname) {let location = {name:'search'};let query = {categoryName:categoryname};if (category1id) {query.category1Id = category1id;} else if (category2id) {query.category2Id = category2id;} else {query.category3Id = category3id;}location.query = query;this.$router.push(location);} }

Day4

1.开发Search模块中TypeNav商品分菜单

重点使用if(this.$route.path!='/home'){...}v-show实现search模块的分菜单下拉效果
注意要在TypeNav组件挂载之后(mounted)就判断分菜单是否隐藏

过渡动画:前提是组件|元素要有v-if|v-show指令才可以进行过渡动画v-enter:过渡的初始状态(隐藏);在过渡开始前被添加,在过渡开始时会被移除
v-enter-to:过渡的结束状态(显示);在过渡开始时被添加,在过渡完成时会被移除
v-enter-active:这里包含了上面的v-enter、v-enter-to两个时间段,在这里可以对上半场过渡定义过渡时间、曲线等
v-leave:过渡的初始状态(显示);在过渡开始前被添加,在过渡开始时会被移除
v-leave-to:过渡的结束状态(隐藏);在过渡开始时被添加,在过渡完成时会被移除
v-leave-active:这里包含了上面的v-leave、v-leave-to两个时间段,在这里可以对下半场过渡定义过渡时间、曲线等`<transition name="sort">`
通过name属性设置过渡名称,之后就会根据该名称创建六个类
例如name = ‘demo’
表示显示的过程(由隐藏的状态变成显示的状态)
.demo-enter .demo-enter-to .demo-enter-active
表示隐藏的过程(由显示状态变成隐藏的状态)
.demo-leave .demo-leave-to .demo-leave-active
transition组件只是加了这六各类,具体的效果还需要通过css过渡或者动画(借助css3实现的)
入场过渡

2.商品分类的三级列表的优化

每次跳转页面之后再使用TypeNav组件都会重新发送请求
优化将TypeNav中的代码this.$store.dispatch("home/getCategoryList");移到App.vue
即程序一运行就发送请求,存入了本地仓库,后期使用不用再重复请求

3.合并参数

此时query参数和params参数只能二存一
1.当你点击a标签的时候,会进行路由的跳转,将产品的名字与id传递给search模块----(query)
2.点击搜索按钮的时候,用户输入进来的关键字,点击按钮的时候会通过params参数传递给search模块-----(params)
所以当用户在商品分类跳转的时候,如果有params参数,应该捎带传递
即在两边跳转的时候分别判断是否有params和query参数,一并加入路径,如:
if(this.$route.query){ location.query = this.$route.query; }

此时如果search点击后params获得了keyword但时路径跳转没有keyword,可以查看route配置路由的时候是否选择了参数可选择性出现,即指定params参数可传递或不传递,在配置路由的时候,可以在占位的后面加上一个问号 即path: '/search/:keyword?'

4.开发Home首页当中的ListContainer组件与Floor组件

http://docschina.org/ 各种前端框架
服务器返回数据只有商品分类菜单。
mock数据(模拟):生成随机数据,拦截Ajax请求。
【插件mock.js】安装:npm install mockjs 【并非mock.js mock-js】
mock的数据会被拦截,只会在前端使用,不会和服务器进行任何通信。

使用步骤
第一步:安装依赖包mockjs

第二步:在src文件夹下创建一个文件夹,文件夹mock文件夹。

第三步:准备模拟的数据
把mock数据需要的图片放置于public文件夹中!
比如:listContainer中的轮播图的数据
[ {id:1,imgUrl:'xxxxxxxxx'}, {id:2,imgUrl:'xxxxxxxxx'}, {id:3,imgUrl:'xxxxxxxxx'}, ]

第四步:在mock文件夹中创建一个server.js文件模拟数据
注意:在server.js文件当中对于banner.json||floor.json的数据没有暴露,但是可以在server模块中使用。-----对于webpack当中一些模块:图片、json,不需要对外暴露,因为默认就是对外暴露。

第五步:通过mock模块模拟出数据
mock数据:第一个参数请求地址 第二个参数:请求数据
Mock.mock("/mock/banner", {code:200,data:banner});

通过Mock.mock方法进行模拟数据

第六步:回到入口文件,引入serve.js(此时的js不用暴露可以理解为Mock.mock已经进行了一次封装,默认暴露)
mock需要的数据|相关mock代码页书写完毕,mock当中serve.js需要执行一次,如果不执行,和没有书写一样。

第七步:在API文件夹中创建mockRequest【axios实例:baseURL:’/mock’】
专门获取模拟数据用的axios实例。

关于mock:
—对于将来实际工作的时候,后台没有准备好接口(服务器没有开发出来),前端工程师可以利用mock技术,
实现模拟数据,将来项目上线(后台真实接口)写好了,替换为真实接口即可。

5.ListContainer组件开发重点

swiper轮播图效果:安装指定版本的插件npm install --save swiper@5
【carousel:轮播图】

使用swiper的三步:
1.引包(相应的JS|CSS)
2.页面结构,静态页面中结构必须完整【container、wrap、slider】,类名不能瞎写
3.new swiper实例【轮播图添加动态效果】

swiper样式已经对外暴露,引入不用from import "swiper/css/swiper.css"

关于swiper实例应该放在哪里:
1)mounted:组件挂载完毕,正常说组件结构(DOM)已经全有了。然而为什么swiper实例在mounted当中直接书写不可以:因为结构还没有完整(dispatch异步导致),此时的v-for只有等请求回数据才会循环遍历获得图片,组件暂未获取到数据。 个人理解:请求数据是异步操作,请求完数据才会对仓库数据进行修改,而此时swiper在mounted中请求数据的后面,直接执行,还不能获取到仓库的数据。
2)不能放在updated中,因为如果要获取的数据太多,会导致每次刷新(加载到数据)都重新加载swiper轮播图
3)不能放在created里面:created执行与mounted执行,时间差可能2ms,更加不可能
可以用settimeout定时器,但是会受网络影响

Day5

1.轮播图问题的最完美解决方案

第一种解决方案问题出现在哪里:v-for,在遍历来自于Vuex(数据:通过ajax向服务器发请求,存在异步)

最完美解决方案:【watch+$nexttick】
watch:监听属性,watch可以检测到属性值的变化,当属性值发生变化的时候,可以触发一次。
handle:watch中需要具体执行的方法,执行即代表watch监听到了属性的属性值变化

Vuex当中的仓库数据bannerList(组件在使用):
bannerList仓库数据有没有发生过变化?
一定是有的:bannerList初始值空数组,当服务器的数据返回以后,它的bannerList存储的属性值会发生变化【变为服务器返回的数据】
组件实例在使用仓库中的bannerList,组件的这个属性bannerList一定是发生过变化,watch可以监听到。
虽然watch监听到数据请求到了,但是v-for遍历需要时间,渲染需要时间,这时候只能保证数据有了不能保证v-for已经执行结束(页面结构还不完整),此时swiper还是不能正常实现动态效果。

组件实例的一个方法:$nextTick
this.$nextTick(()=>{...})
nextTick官网解释:
在下次DOM更新, 循环结束之后,执行延迟回调。在 修改数据之后 立即使用这个方法,获取更新后的DOM。
注意:组件实例的$nextTick方法,在工作当中经常使用,经常结合第三方插件使用,获取更新后的DOM节点

swiper实例中获取节点不通过dom,在vue里面使用ref, this.$refs.mySwiper

ref 有三种用法:1、ref 加在普通的元素上,用this.$refs.(ref值) 获取到的是dom元素2、ref 加在子组件上,用this.$refs.(ref值) 获取到的是组件实例,可以使用组件的所有方法。在使用方法的时候直接this.$refs.(ref值).方法() 就可以使用了。3、如何利用 v-for 和 ref 获取一组数组或者dom 节点:1)如果通过v-for 遍历想加不同的ref时记得加 :号,即 :ref =某变量 ;2)通过 :ref =某变量 添加ref(即加了:号) ,如果想获取该ref时需要加 [0],如this.$refs[refsArrayItem] [0];如果不是:ref =某变量的方式而是 ref =某字符串时则不需要加,如this.$refs[refsArrayItem]。注意:
1、ref 需要在dom渲染完成后才会有,在使用的时候确保dom已经渲染完成。比如在生命周期 mounted(){} 钩子中调用,或者在 this.$nextTick(()=>{}) 中调用。
2、如果ref 是循环出来的,有多个重名,那么ref的值会是一个数组 ,此时要拿到单个的ref 只需要循环就可以了。

2.开发Floor组件

开发Floor组件:Floor组件它被复用的(重复使用两次)

2.1 Floor组件获取mock数据,发请求的action书写在哪里?

派发action应该是在父组件(Home路由组件)的组件挂载完毕生命周期函数mounted中书写,因为父组件需要通知Vuex发请求,父组件获取到mock数据,通过v-for遍历 生成多个floor组件,因此达到复用作用。
(数据此时都在父组件中,需要传到floor子组件)
<floor v-for="(floor,index) in floorList" :key="floor.id" :list="floor"/>

2.2 组件间通信(面试必问)

props父传子通信:
父组件通过props向下传递数据给子组件,父组件中用 v-on 的方式实现,子组件通过props接收。如:父组件发送数据<floor :list="floor"/> ,前者自定义名称方便子组件调用,后者为要传递的数据名 子组件接收数据props:['list']
注:组件中的数据共有三种形式:data、props、computed

插槽slot: 父子
自定义事件: 子父
全局事件总线$bus: 万能
pubsub: 万能
Vuex: 万能,存储数据
$ref: 父子通信

2.3 为什么在Floor组件的mounted中初始化SWiper实例轮播图可以使用.

因为父组件的mounted发请求获取Floor组件,当父组件的mounted执行的时候,Floor组件结构可能没有完整,但是服务器的数据传给子组件Floor之后(父子通信之后),Floor组件获得数据,其结构就一定是完成的了,因此v-for在遍历来自于服务器的数据,如果服务器的数据有了,Floor结构一定的完整的。否则,都看不见Floor组件

3.拆分首页轮播图组件为carsouel共用组件

组件放置位置一个是watch一个是mounted,不好拆分,需要把floor改成listcontainer风格一样
floor组件的watch不能监听到list的变化,可以加上个immediate:true表示立即监听,但是此时只能监听到数据已经有了,v-for动态渲染结构还是无法确定,所以还是要使用$nextTick,和listcontainer使用相同,此时再给listcontainer组件也加上immediate属性就可以拆分组件了

使用全局组件展示轮播图<carousel :list="list.carouselList"/>

4.search模块开发

开发search搜索模块:
1.书写静态页面【布局、样式】
2.拆分组件
3.发请求(API)
4.vuex(三连环)
5.组件获取仓库数据,动态展示数据

4.1发请求

1)获取搜索模块数据 地址:/api/list 请求方式:post 参数:需要带参数

{//接口所需要带的参数"category1Id": "61",       //一级分类的id"category2Id": "61",       //二级分类的id"category3Id": "61",       //三级分类的id"categoryName": "手机",    //产品的名字"keyword": "小米",         //关键字"order": "1:desc",         //排序"pageNo": 1,               //当前第几页"pageSize": 10,            //每一页需要展示多少条数据"props": ["1:1700-2799:价格", "2:6.65-6.74英寸:屏幕尺寸"],//平台属性的选择参数"trademark": "4:小米"      //品牌参数
}

当前这个函数需不需要接受外部传递参数
当前这个接口要获取搜索模块数据给服务器传递params参数时,至少要是一个空对象
export const reqGetSearchInfo = (params)=>requests({url:"/list",method:"post",data:params});
(params在axios中使用时要放在data里)

4.2vuex(三连环)

此时需要用到计算属性getters,项目当中getters主要的作用是:简化仓库中的数据(简化数据而生)
在getters里可以把我们将来在组件当中需要用的数据简化一下【将来组件在获取数据的时候就方便了】
如: goodsList(state){ return state.searchList.goodsList||[];}
当前形参state,当前仓库中的state,并非大仓库中的那个state
state.searchList.goodsList如果服务器数据回来了,没问题是一个数组
假如网络不给力|没有网state.searchList.goodsList应该返回的是undefined
计算新的属性的属性值至少给人家来一个数组

在search组件里获取
computed:{...mapGetters(['goodsList'])}
mapGetters里面的写法:传递的数组,因为getters计算时没有划分模块的(但如果开了命名空间就会分模块)

每次点击不同分类都应该请求一次对应分类的服务器数据,所以dispatch不能只在mounted里使用一次,可以写成一个函数重复使用
getData() {this.$store.dispatch("getSearchList",this.searchParams);},

8个生命周期函数

beforeCreate() 在实例创建之间执行,数据未加载状态
created() 在实例创建、数据加载后,能初始化数据,dom渲染之前执行
beforeMount() 虚拟dom已创建完成,在数据渲染前最后一次更改数据
mounted() 页面、数据渲染完成,真实dom挂载完成
beforeUpadate() 重新渲染之前触发
updated() 数据已经更改完成,dom 也重新 render 完成,更改数据会陷入死循环
beforeDestory() 和 destoryed() 前者是销毁前执行(实例仍然完全可用),后者则是销毁后执行

在挂载之前调用一次|可以在发请求之前将带有参数进行修改
beforeMount() { Object.assign(this.searchParams, this.$route.query, this.$route.params); }
在发请求之前,把接口需要传递参数,进行整理(在给服务器发请求之前,把参数整理好,服务器就会返回查询的数据)

监听路由变化而再次向服务器发请求获取数据

 watch: {//监听路由的信息是否发生变化,如果发生变化,再次发起请求$route(newValue, oldValue) {//每一次请求完毕,应该把相应的1、2、3级分类的id置空的,让他接受下一次的相应1、2、3//再次发请求之前整理带给服务器参数Object.assign(this.searchParams, this.$route.query, this.$route.params);//再次发起ajax请求this.getData();//分类名字与关键字不用清理:因为每一次路由发生变化的时候,都会给他赋予新的数据this.searchParams.category1Id = undefined;this.searchParams.category2Id = undefined;this.searchParams.category3Id = undefined;},},

Day6

1.动态开发面包屑

1.1动态开发面包屑中的分类名

v-if动态生成面包屑,判断是否点击分类栏来生成分类名
<li class="with-x" v-if="searchParams.categoryName">{{ searchParams.categoryName}}<i @click="removeCategoryName">x</i></li>
同时删除面包屑实现路由回到无query参数的页面,此时分类名的id也要清空。
地址栏也需要需改:进行路由跳转(现在的路由跳转只是跳转到自己这里),即再次请求一次新地址的数据this.getData()
要注意:本意是删除query,如果路径当中出现params不应该删除,路由跳转的时候应该带着
if (this.$route.params) {this.$router.push({ name: "search", params: this.$route.params });}

1.2动态开发面包屑中的关键字

此时置空的是params参数,要实现兄弟组件header中的搜索栏也清空,此时数据通信要通过全局事件总线$bus
在main.js里面配置全局实现总线beforeCreate(){Vue.prototype.$bus = this;},(这里的this指大写的VM)
在search组件的点击事件中实现通信:(通知兄弟组件Header清除关键字)this.$bus.$emit("clear");
在兄弟组件header挂载的时候就接收事件:mounted() {this.$bus.$on("clear", () => {this.keyword = "";});},

2.品牌与平台属性的数据进行动态展示

tradeMark—品牌
举例子:理解平台属性 【用户购买一个手机】
颜色【平台属性】:红色、白色、紫色【平台属性值】
价格【平台属性】:1299,6999,899【平台属性值】
操作系统【平台属性】:window、linux【平台属性值】
尺寸: 中、短
材料:塑料、涤纶
[ {attrId:1,attrName:尺寸,attrValueList:['中','短']}, {attrId:2,attrName:材料,attrValueList:['塑料','涤纶']},]

2.1动态开发面包屑中的品牌信息

实现功能:点击品牌(苹果),需要整理参数,向服务器发请求获取相应的数据进行展示,此时形成品牌面包屑,点击删除后又会重新请求数据

父组件中searchParams参数是带给服务器参数,子组件(品牌展示SearchSeletor)组件把你点击的品牌的信息传给父组件
可以在子组件中自定义事件传信息给父组件tradeMarkHandler(trademark) {this.$emit("trademarkInfo", trademark);},
在父组件中接收信息<search-selector @trademarkInfo="trademarkInfo"/>,并自定义事件回调trademarkInfo(trademark) {this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}`;this.getData();},
整理品牌字段的参数 “ID:品牌名称”------${}模板字符串

2.2动态查找平台售卖属性

类似于品牌信息的实现,重点要注意平台属性携带参数格式:
props【Array】 商品属性的数组: [“属性ID:属性值:属性名”] 示例: [“2:6.0~6.24英寸:屏幕尺寸”]
props:[‘属性的ID:属性值:属性名’]

3.排序操作

order:服务器需要字段,代表的是排序方式
order这个字段需要的是字符串(可以传递也可以不传递)
1:代表综合
2:代表价格
3:asc代表升序
4:desc代表降序

3.1告诉服务器排序方式有几种情况?

“1:asc” “1:desc” “2:asc” “2:desc”

3.2考虑的问题

3.2.1谁有类名active

取决于order中包含的是"1"(综合)||“2”(价格)

3.2.2谁应该有箭头

谁有类名谁有箭头
箭头来源:阿里图标库 选中后生成项目,获取font class的在线链接
在public文件夹的index页面中引用,注意要带上网络协议https:
哪里需要使用的时候直接加类名"iconfont 图标名"

3.2.3箭头的动态切换

自定义点击事件的时候应该注意怎么获取点击到的是哪个标签(综合||价格)
创建形参flag:用户每一次点击li标签的时候,用于区分是综合(1)还是价格(2)

然后需要获取order的初始状态
let originOrder = this.searchParams.order;
let orginsFlag = originOrder.split(":")[0];
let originSort = originOrder.split(":")[1];
判断的是多次点击的是不是同一个按钮
if (flag == orginsFlag) { newOrder = `${orginsFlag}:${originSort == "desc" ? "asc" : "desc"}`; } else { newOrder = `${flag}:${"desc"}`; }

Day7

1.电商平台为什么需要分页

比如:搜索一个奶粉,奶粉的产品有10000+,一次渲染10000+条数据,可能慢。
数据多的时候,可以选择分页,比如每一次只是展示10

1.1拆分分页组件(静态组件)

注册为全局组件,因为其他模块也在使用分页功能。(search和我的订单都有分页的需求)

1.2分页器封装业务分析:

封装分页器组件的时候:需要知道哪些条件?

1:分页器组件需要知道我一共展示多少条数据 ----total【100条数据】

2:每一个需要展示几条数据------pageSize【每一页3条数据】

3:需要知道当前在第几页-------pageNo[当前在第几页]

4:需要知道连续页码数【起始数字、结束数字:连续页码数市场当中一般5、7、9】奇数,对称好看 continues

已经条件: total=【99】 pageSize =【3】 pageNo=6 continues 5

4 5 6 7 8

已经条件: total=【99】 pageSize =【3】 pageNo= 1 continues 5

错误:-1 0 1 2 3
正确: 1 2 3 4 5

已经条件: total=【99】 pageSize =【3】 pageNo= 2 continues 5

错误: 0 1 2 3 4
正确:1 2 3 4 5

已经条件: total=【99】 pageSize =【3】 pageNo= 33 continues 5

错误: 31 32 33 34 35
正确:29 30 31 32 33

已经条件: total=【99】 pageSize =【3】 pageNo= 32 continues 5

错误:30 31 32 33 34
正确: 29 30 31 32 33

连续页码5: 8 [6,7,8,9,10]
连续页码7: 8 [5,6,7,8,9,10,11]

连续页码5: 8 [6,7,8,9,10]
连续页码7: 8 [5,6,7,8,9,10,11]

2.分页器封装

从父组件search传数据给pagination组件

2.1总共页数

总页数需要向上取整,可以多,不能少 totalPage() {return Math.ceil(this.total / this.pageSize);},

2.2连续页码显示

计算出连续的页码的起始数字与结束数字[连续页码的数字:至少是5]

    startNumAndEndNum() {const { continues, pageNo, totalPage } = this;//先定义两个变量存储起始数字与结束数字let start = 0, end = 0;//连续页码数字5【就是至少五页】,如果出现不正常的现象【就是不够五页】//不正常现象【总页数没有连续页码多】if (continues > totalPage) {start = 1;end = totalPage;} else {//正常现象【连续页码5,但是你的总页数一定是大于5的】//起始数字start = pageNo - parseInt(continues / 2);//结束数字end = pageNo + parseInt(continues / 2);//把出现不正常的现象【start数字出现0|负数】纠正if (start < 1) {start = 1;end = continues;}//把出现不正常的现象[end数字大于总页码]纠正if (end > totalPage) {end = totalPage;start = totalPage - continues + 1;}}return { start, end };},

2.3页数button的显示

2.3.1 上一页的button实现

控制当前页为第一页的时候不能点击上一页,否则每次点击都向getPageNo传递当前页码的上一页
注意当连续页面的起始页为1时,第1页的button不再重复显示
当连续页面的起始页大于2时,起始页和第一页之间的页数用省略号显示

<button :disabled="pageNo == 1" @click="$emit('getPageNo', pageNo - 1)">上一页</button><buttonv-if="startNumAndEndNum.start > 1"@click="$emit('getPageNo', 1)":class="{ active: pageNo == 1 }"> 1 </button><button v-if="startNumAndEndNum.start > 2">···</button>
2.3.2 下一页的button实现
    <!-- 下 --><button v-if="startNumAndEndNum.end < totalPage - 1">···</button><buttonv-if="startNumAndEndNum.end < totalPage"@click="$emit('getPageNo', totalPage)":class="{active:pageNo==totalPage}"> {{ totalPage }} </button><button:disabled="pageNo == totalPage"@click="$emit('getPageNo', pageNo + 1)"> 下一页 </button>
2.3.3 中间页(连续页数)的实现

只有连续页码的当前page>=连续页码起始页才显示

 <!-- 中间部分 --><buttonv-for="(page, index) in startNumAndEndNum.end":key="index"v-show="page >= startNumAndEndNum.start"@click="$emit('getPageNo', page)":class="{ active: pageNo == page }"> {{ page }} </button>

3.总结:

3.1面试问题:

1)v-for与v-if优先级? ------ v-for优先级更高
2)工作当中是否自己封装过一些通用的组件?

3.2对于一个分页器:

1)需要知道数据总条数
2)每一个需要展示数据条数
3)知道当前是第几页
4)连续页码数字
5)自定义事件【子给父通信的】

4.开发详情页

4.1开发详情业务

与search搜索模块开发步骤相同:
1.书写静态页面【布局、样式】
2.拆分组件
3.发请求(API)
4.vuex(三连环)
5.组件获取仓库数据,动态展示数据

4.2静态组件

组件一定要注意有没有在router文件夹注册为路由组件
path: '/detail/:skuid'
此时,点击商品图片的时候,会跳转到详情页面,但在路由跳转的时候需要带上产品对应ID给详情页面

动态to实现不同参数的跳转,而参数写入路径用模板字符串

4.3滚动行为的设置

vue里vue-router的滚动行为
scrollBehavior(to, from, savedPosition) { return { y: 0 }; }
返回的这个y=0,代表的滚动条在最上方,且滚动行为应该与router里的routes平级

4.4发请求(API)

获取产品详情信息的接口 URL: /api/item/{ skuId } 请求方式:get
export const reqGoodsInfo = (skuId)=>requests({url:/item/${skuId},method:'get'});

4.5vuex获取产品详情信息

vuex中需要新增一个模块detail(即在store文件夹新建detail.js)

在detail组件中挂在完毕后派发action获取产品详情的信息
this.$store.dispatch("getGoodInfo", this.$route.params.skuid);

4.6产品详情显示动态数据

项目当中控制台
vue-warn:警告(不影响的你程序),对于你的代码提出一个警告。
对于程序没有任何影响,俗称假报错。

关于分类导航的的假报错:(详情页左上角v-show判断分类存在时创建span展示导航)
应该在路径导航简化数据的时候返回至少一个空对象
categoryView(state) {return state.goodInfo.categoryView || {};}
同理,简化产品信息数据和产品售卖属性简化时也应该至少返回一个空对象防止假报错

Day8

1.zoom放大镜展示数据

1.1数据传输

传输方式:父传子,从detail传给zoom放大镜组件
注意在申请数组skuImageList的时候,要考虑数据返回为空的情况:(至少给一个空对象)
如果服务器数据没有回来,在父亲skuInfo这个对象是空对象
skuImageList() {return this.skuInfo.skuImageList || []; }
传给儿子之后,取数组的第一个值作为放大镜的主图,此时也要考虑数组为空时的情况,应该也返回一个空对象
imgObj(){return this.skuImageList[this.currentIndex]||{}}

1.2轮播图实现[watch+$nextTick]

参考Day5的1.轮播图问题的最完美解决方案

使用swiper的三步:
1.引包
2.页面结构完整
3.new swiper实例

zoom放大镜里的swiper和共用组件的轮播图不同,像自动播放loop,分页器pagination和滚动条都不需要
在swiper官网查找文档,设置slidesPerView: 3 显示几个图片
slidesPerGroup:1 每一次切换图片个数

此时要通过swiper点击图片,通知兄弟组件选中图片的index来使得上下一致
通知兄弟组件:当前的索引值为几 this.$bus.$emit('getIndex',this.currentIndex);
全局事件总线:获取兄弟组件传递过来的索引值,并直接修改当前index来实现响应式 this.$bus.$on('getIndex',(index)=>{this.currentIndex = index;})

1.3放大镜的mask跟随效果

1)要注意获取的offset是主图到光标的距离,需要减去半个mask的宽度let left = event.offsetX - mask.offsetWidth/2;
2)修改元素的left|top属性值实现mask的鼠标跟随效果 mask.style.left = left+'px';
3)需要给mask加上约束范围
if(left <=0) left = 0;
if(left >=mask.offsetWidth) left = mask.offsetWidth;

1.4放大镜放大框的效果

放大镜放大两倍,所以移动的距离也应该为两倍 big.style.left = - 2 * left+'px';

2.产品售卖属性值排他操作

产品的售卖属性值切换高亮的实现:

    changeActive(saleAttrValue, arr) {//遍历全部售卖属性值isChecked为零没有高亮了arr.forEach((item) => {item.isChecked = 0;});//点击的那个售卖属性值变为1saleAttrValue.isChecked = 1;}

3.购买产品个数的操作

自定义事件@change="changeSkuNum" 当修改数值时判断是否符合规范

4.加入购物车的业务

购物车:每一个人都有属于自己的购物车,那为什么不同用户登录自己账号,可以看见属于自己产品
一定是用户点击加入购物车,把你的产品信息提交给服务器进行保存,当你下次在进入购物车的时候,
需要向服务器发请求,获取你购物车里面的信息展示

4.1发请求api

将产品添加到购物车中(获取更新某一个产品的个数)
/api/cart/addToCart/{ skuId }/{ skuNum } POST
接口:export const reqAddOrUpdateShopCart = (skuId,skuNum)=>requests({url:`/cart/addToCart/${skuId}/${skuNum}`,method:"post"})

派发acion,异步向接口发送请求
注意:很多请求是没有数据返回值的,这个时候应该有失败提示

加入购物车||修改某一个产品的个数async addOrUpdateShopCart({ commit }, { skuId, skuNum }) {//发请求:前端带一些参数给服务器【需要存储这些数据】,存储成功了,没有给返回数据,不需要再三连环(仓库存储数据了)//注意:async函数执行返回的结果一定是一个promise【要么成功,要么失败】let result = await reqAddOrUpdateShopCart(skuId, skuNum);if (result.code == 200) {return "ok";} else {return Promise.reject(new Error("faile"));}},

给加入购物车自定义点击事件
1)在点击加入购物车这个按钮的时候,做的第一件事情,将参数带给服务器(发请求),通知服务器加入购车的产品是谁
this.$store.dispatch('addOrUpdateShopCart'),说白了,它是在调用vuex仓库中的这个addOrUpdateShopCart函数。
2)你需要知道这次请求成功还是失败,如果成功进行路由跳转,如果失败,需要给用户提示
3)进行路由跳转
4)在路由跳转的时候还需要将产品的信息带给下一级的路由组件

async addShopcar() {try {//成功//一些简单的数据skuNum,通过query形式给路由组件传递过去await this.$store.dispatch("addOrUpdateShopCart", {skuId: this.$route.params.skuid,skuNum: this.skuNum,});//产品信息的数据【比较复杂:skuInfo】,通过会话存储(不持久化,会话结束数据就消失)//本地存储|会话存储,一般存储的是字符串sessionStorage.setItem("SKUINFO",JSON.stringify(this.skuInfo));this.$router.push({name:'addcartsuccess',query:{skuNum:this.skuNum}});} catch (error) {//失败alert(error.message);}},

路由跳转到加入购物成功,要将组件挂载

{ path: '/addcartsuccess',component: AddCartSuccess },

在跳转后的AddCartSuccess组件中把会话存储中的数据读取出来,动态展示,此时应该以对象的形式取出
skuInfo() {return JSON.parse(sessionStorage.getItem("SKUINFO"));}

面试题:
1)GET与POST
相同点:
—都是HTTP协议。
不同点:
—1:GET请求携带参数是有上限的 post请求携带的参数是没有’上限的’
—2:GET请求相对而言不安全,POST安全

2)H5新增那些特性?
CSS4、本地存储、多媒体、canvas

3)本地存储与会话存储区别?
localStorage 和 sessionStorage 属性允许在浏览器中存储 key/value 对的数据。
Cookie:是指保存在客户端的纯文本文件。比如txt文件。
相同点:都存储在客户端
不同点:
    1.大小
      cookie的存储大小不超过4KB
      sessionStorage和localStorage可以达到5MB或更大;
    2.数据存在时间:
       LocalStorage:关闭浏览器数据依旧会存在,除非主动删除其中数据;持久化的
       sessionStorage:当前页关闭则数据删除;不持久化
       cookie:根据用户设置的cookie保存时间来定,在设定时间到来之前都会一直存在;
    3.数据与服务器之间的交互方式
       cookie的数据会自动传递到服务器,服务的也可以写到客户端
       LocalStroage和sessionStroage的数据仅保存在本地

4)token相关的面试

  1. 为什么会有token的出现?
    答:首先,session的存储是需要空间的,其次,session的传递一般都是通过cookie来传递的,或者url重写的方式;而token在服务器是可以不需要存储用户的信息的,而token的传递方式也不限于cookie传递,当然,token也是可以保存起来的;

  2. token的生成方式?
    答:浏览器第一次访问服务器,根据传过来的唯一标识userId,服务端会通过一些算法,如常用的HMAC-SHA256算法,然后加一个密钥,生成一个token,然后通过BASE64编码一下之后将这个token发送给客户端;客户端将token保存起来,下次请求时,带着token,服务器收到请求后,然后会用相同的算法和密钥去验证token,如果通过,执行业务操作,不通过,返回不通过信息;

  3. token和session的区别?
    token和session其实都是为了身份验证,session一般翻译为会话,而token更多的时候是翻译为令牌;
    session服务器会保存一份,可能保存到缓存,文件,数据库;同样,session和token都是有过期时间一说,都需要去管理过期时间;
    其实token与session的问题是一种时间与空间的博弈问题,session是空间换时间,而token是时间换空间。两者的选择要看具体情况而定。

虽然确实都是“客户端记录,每次访问携带”,但 token 很容易设计为自包含的,也就是说,后端不需要记录什么东西,每次一个无状态请求,每次解密验证,每次当场得出合法 /非法的结论。这一切判断依据,除了固化在 CS 两端的一些逻辑之外,整个信息是自包含的。这才是真正的无状态。
而 sessionid ,一般都是一段随机字符串,需要到后端去检索 id 的有效性。万一服务器重启导致内存里的 session 没了呢?万一 redis 服务器挂了呢?

  1. token面试题:项目当中token过期、失效如何处理?
    答:清除本地token(本地存储),让用户回到登录页,获取最新的token

5)promise问题。【基础问题】

4.2添加购物车成功的AddCartSuccess组件

点击"查看商品详情"需要跳转回商品详情页,此时应该用router-link导航跳转(不能直接后退因为商品数量会受影响)
点击"查看购物车"需要跳转到shopcart组件

Day9

1.购物车组件

1.1静态组件

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

获取购物车列表数据接口 URL:/api/cart/cartList     method:get
export const reqCartList = ()=>requests({url:'/cart/cartList ',method:'get'});

2. UUID临时游客身份

发请求的时候,服务器获取不到你的身份导致也获取不到你购物车里的数据
生成的uuid临时游客身份应该保存到本地存储,后续使用还是同一个游客身份

创建utils文件夹,将uuid创建封装成一个函数

import { v4 as uuidv4 } from 'uuid';
export const getUUID = ()=>{//先从本地存储获取uuid(看一下本地存储里面是否有)let uuid_token = localStorage.getItem('UUIDTOKEN');//如果没有if(!uuid_token){//生成游客临时身份uuid_token = uuidv4();//本地存储存储一次localStorage.setItem('UUIDTOKEN',uuid_token);}//切记有返回值,没有返回值undefinedreturn uuid_token;
}

在detail组件传数据给购物车的时候要指明身份,除此之外,uuid还需要写在请求拦截器里,在请求发送之前配置好当前的游客身份
if(store.state.detail.uuid_token){ config.headers.userTempId = store.state.detail.uuid_token; }// 请求头添加一个字段(userTempId)

3.动态展示购物车

注意数据格式!

4.购物车数量的操作

修改购物车产品数量的时候,需要发请求的,通知服务器产品最新的个数【服务器需要保存】,
(有bug,快速点击"-"会出现负数的情况,并且刷新很慢)

产品个数变化接口参数:
skuID string Y 商品ID
skuNum:在修改产品个数的时候,需要给服务器传递的是【变化的数值】

比如:佩奇 起始数量 4 用户在表单元素中输入 6 ----->给服务器参数是2
佩奇 起始数量4 用户在表单元素中输入1 ------>给服务器的参数-3
佩奇 起始的数量4 用户在表单元素中输入4 ------>给服务器的参数0

blur:失去焦点—>点击空白的地方
change:文本需要有变化,而且还需要点击空白的地方
input:只要文本发生变化立马执行【不许点击空白的地方】

修改产品个数bug的解决:
【函数节流】因为设置一秒只能点击一次,此时可以数据刷新完后再发送第二次的请求,做到实时判断数量是否大于1

5.删除购物车产品

删除购物产品的接口
URL:/api/cart/deleteCart/{skuId} method:DELETE
export const reqDeleteCartById = (skuId)=>requests({url:`/cart/deleteCart/${skuId}`,method:'delete'});

6.修改产品状态

修改商品的选中状态的接口
URL:/api/cart/checkCart/{skuId}/{isChecked} method:get
export const reqUpdateCheckedByid = (skuId,isChecked)=>requests({url:`/cart/checkCart/${skuId}/${isChecked}`,method:'get'});

7.删除全部选中商品

context:小仓库,包含commit【提交mutations修改state】 getters【计算属性】 dispatch【派发action】 state【当前仓库数据】

let PromiseAll = [];getters.cartList.cartInfoList.forEach((item) => {let promise =item.isChecked == 1? dispatch("deleteCartListBySkuId", item.skuId): "";//将每一次返回的Promise添加到数组当中PromiseAll.push(promise);});return Promise.all(PromiseAll);

Promise.all([p1,p2,p3,…])
只要全部的p1|p2…都成功,返回结果即为成功,如果有一个失败,返回即为失败结果

8.全部产品勾选状态修改

和"删除全部选中商品"功能相类似,但是要注意:当不存在商品时不能改变全选框的状态了:checked="isAllChecked&&cartInfoList.length>0"
(还是存在bug,可以尝试加个防抖)

Day10

1.登录注册静态组件

assets【放置静态资源文件的地方】
一般放置所有组件共用的静态资源
在样式当中也可以使用@,在样式当中使用@,前面加上~

2.注册页面

【正则】
手机号:11
验证码:4-6
登录密码|确认密码:首字母大写、包含英文、数字、特殊字符等等。

2.1验证码

接口:获取验证码
URL:/api/user/passport/sendCode/{phone} method:get
export const reqGetCode = (phone)=>requests({url:`/user/passport/sendCode/${phone}`,method:'get'});
(考虑给验证码获取过1s再赋值)

2.2注册

注册
url:/api/user/passport/register method:post phone code password
export const reqUserRegister = (data)=>requests({url:'/user/passport/register',data,method:'post'});

全部表单验证成功,再向服务器发请求,进行注册

3.登录页面

export const reqUserLogin = (data)=>requests({url:'/user/passport/login',data,method:'post'});
登录成功的时候,后台为了区分用户是谁—服务器下发token【令牌:唯一标识符】存储于vuex当中,如果想获取用户信息,还需要再发请求【用户信息】,携带token给服务器。
项目的登录接口做的不完美,一般登录成功服务器会下发token,前台持久化存储token
在路由组件获取到token后持久化存储setToken(result.data.token);

当你点击登录按钮的时候,需要把手机号、密码需要携带给服务器,服务器需要判断,你是不是我的用户【注册过的】
如果是用户登录成功,进行登录,如果用户登录失败给一个提示即可。

  1. 为什么去别的模块【非home模块】获取用户信息失败?
    因为你去别的模块根本没有发请求获取用户信息,没办法展示用户信了
    怎么解决:
    每一个组件都在mounted里面发起获取用户信息,进行展示(可以太麻烦)
    残留的问题:用户在home模块刷新的时候,用户信息一直在展示(mounted执行的时候在向服务器发请求、获取用户信息展示)
    home->search[用户信息刷新数据就没了,因为在search模块当中根本没有发请求获取用户信息]
    search-detail[根本没有获取用户信息进行展示]

  2. 用户已经登陆就不应该再回登陆界面
    退出登录功能的实现(导航守卫)

4.获取用户信息【需要带着用户的token向服务器要用户信息】

URL:/api/user/passport/auth/getUserInfo method:get
export const reqUserInfo = ()=>requests({url:'/user/passport/auth/getUserInfo',method:'get'});

typenav的登录和退出登录的切换显示可以用v-if判断是否有用户名来判断是否登录了v-if="!userName"

5.退出登录

  1. 发请求,需要通知服务器,把现在用户身份token【销毁】
    URL:/api/user/passport/logout get
    export const reqLogout = ()=> requests({url:'/user/passport/logout',method:'get'});
  2. 清除仓库数据+本地存储数据【都需要清理】

Day11

1.导航守卫

(vueRouter的进阶)
守卫条件判断

导航:表示路由正在发生改变。
守卫:古代的守门的士兵’守卫’,守卫可以通过条件判断路由能不能进行跳转。

三大守卫:
1)全局守卫:项目当中任何路由变化都可以检测到,通过条件判断可不可以进行路由跳转。
2)前置守卫:路由跳转之前可以做一些事情。
3)后置守卫:路由跳转已经完成再执行。
//全局守卫:[后置守卫:在路由跳转完毕之后才会执行一次]
router.afterEach(()=>{
console.log(‘守卫:路由跳转完毕才会执行一次’)
})

2.用户登录判断

用户已经登录了,不应该再访问login【通过什么条件能判断用户登录、未登录】
1)路由独享守卫:
针对某一个路由的守卫

2)组件内守卫:
也是负责某一个路由守卫

身份凭证:
以后登录:TOKEN身份为大
1)UUID生成的临时身份
2)用户(注册与登录)token【正式身份】

3.路由守卫判断

to:获取到要跳转到的路由信息
from:获取到从哪个路由跳转过来的信息
next: next() 放行 next(path) 放行

router.beforeEach(async (to, from, next) => {//获取仓库中的token-----可以确定用户是登录了let token  = store.state.user.token;let name = store.state.user.userInfo.name;//用户登录了if(token){//已经登录而且还想去登录------不行if(to.path=="/login"||to.path=='/register'){next('/');}else{//已经登陆了,访问的是非登录与注册//登录了且拥有用户信息放行if(name){next();}else{//登陆了且没有用户信息//在路由跳转之前获取用户信息且放行try {await store.dispatch('getUserInfo');next();} catch (error) {//token失效从新登录await store.dispatch('userLogout');next('/login')}}}}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();}}
});

4.Trade交易页面

4.1发送请求api

获取用户地址信息
URL:/api/user/userAddress/auth/findUserAddressList method:get
export const reqAddressInfo = ()=>requests({url:'/user/userAddress/auth/findUserAddressList',method:'get'});
发送请求:

async getUserAddress({ commit }) {let result = await reqAddressInfo();if (result.code == 200) {commit("GETUSERADDRESS", result.data);}},

请求数据:this.$store.dispatch("getUserAddress");
注意:只有用户登录了才能获取用户地址信息

获取商品清单信息
URL:/api/order/auth/trade method:get
export const reqOrderInfo = ()=>requests({url:'/order/auth/trade',method:'get'});

4.2vuex获取信息

简化获取到的信息

...mapState({addressInfo: (state) => state.trade.address,orderInfo: (state) => state.trade.orderInfo,}),

4.3动态展示数据

将来提交订单最终选中地址:(computed计算属性)
userDefaultAddress() {return this.addressInfo.find((item) => item.isDefault == 1) || {};},
find:查找数组当中符合条件的元素返回,最为最终结果
寄送至:<span>{{ userDefaultAddress.fullAddress }}</span>
收货人:<span>{{ userDefaultAddress.consignee }}</span>
<span>{{ userDefaultAddress.phoneNum }}</span>

此时的商品价格总计是直接后台返回的

5.提交订单

接口暴露需要多次引入问题的解决:import * as API from '@/api' Vue.prototype.$API = API;

提交订单业务
当用户点击提交订单按钮的时候,需要发请求的
提交订单的请求地址:/api/order/auth/submitOrder?tradeNo={tradeNo}
export const reqSubmitOrder = (tradeNo,data)=>requests({url:`/order/auth/submitOrder?tradeNo=${tradeNo}`,data,method:'post'});
此时向服务器请求数据await this.$API.reqSubmitOrder(tradeNo, data);

前台:需要告诉服务器:谁买的、买的啥、买几个、 支付多少钱、留言信息…

6.支付页面

微信支付、支付宝支付等等
交易编码(服务器:字符串hash)
收件人名字
收件人手机号
收件的地址
买家留言信息
支付产品

6.1获取支付信息

URL:/api/payment/weixin/createNative/{orderId} GET
export const reqPayInfo = (orderId)=>requests({url:`/payment/weixin/createNative/${orderId}`,method:'get'});

6.2支付页面ElementUI的使用

element-ui官方UI组件库(插件)

react框架:
UI组件库antd【蚂蚁金服旗下PC端UI组件库】
antd-mobile【蚂蚁金服旗下的移动端UI组件库】

Vue框架:
element-UI【饿了吗旗下的UI组件库,官方承认的PC组件库插件】
vant【Vue官方提供移动端UI组件库】

官网地址:https://element.eleme.cn/#/zh-CN
官网地址:https://youzan.github.io/vant/#/zh-CN/

  1. 第一步:项目中安装element-ui组件库 [2.15.6版本:Vue2]
    npm install --save element-ui
  2. 第二步:在入口文件引入elementUI组件库
    1:全部引入【不采用:因为项目中只是用到一个组件,没必要全都引入进来】
    2:按需引入【按照开发需求引入相应的组件,并非全部组件引入】
  3. 第三步:按需引入,安装相应的插件
    cnpm install babel-plugin-component -D
    文档中说的.babelrc文件,即为babel.config.js文件,在文件中进行相应配置,配置文件发生变化以后,项目需要重启
plugins: [["component",{libraryName: "element-ui",styleLibraryName: "theme-chalk",},],],
  1. 第四步:按照需求引入相应的组件即可
    1:Vue.component();
    2:Vue.prototype.$xxx = xxx;

如:
引入:import {Button} from 'element-ui'
Vue.component(Button.name,Button)

使用:<el-button type="primary" icon="el-icon-phone">测试</el-button>

6.3支付业务【微信支付】

this.$alert('<strong>这是 <i>HTML</i> 片段</strong>', 'HTML 片段', {dangerouslyUseHTMLString: true});

展示二维码----qrcode插件
npm install --save qrcode
通过qrCode.toDataUrl方法,将字符串转换为加密的在线二维码链接,通过图片进行展示。
moment.js
swiper.js
nprogress.js
qrcode.js

async open() {//生成二维(地址)let url = await QRCode.toDataURL(this.payInfo.codeUrl);this.$alert(`<img src=${url} />`, "请扫码微信支付", {dangerouslyUseHTMLString: true,center: true,//中间布局showCancelButton: true,//是否显示取消按钮cancelButtonText: "支付遇见问题",//取消按钮的文本内容confirmButtonText: "已支付成功",//确定按钮的文本showClose: false,//右上角的叉子没了beforeClose: (type, instance, done) => {//关闭弹出框的配置值//type:区分取消|确定按钮//instance:当前组件实例//done:关闭弹出框的方法if (type == "cancel") {alert("请联系管理员豪哥");clearInterval(this.timer);this.timer = null;//清除定时器done();//关闭弹出框} else {//判断是否真的支付了if (this.code == 200) {clearInterval(this.timer);this.timer = null;done();this.$router.push("/paysuccess");}}},});

6.4获取支付订单状态

URL:/api/payment/weixin/queryPayStatus/{orderId} get
export const reqPayStatus = (orderId)=>requests({url:`/payment/weixin/queryPayStatus/${orderId}`,method:'get'});

6.5订单支付成功路由跳转

Day12

1.我的订单页面

1.1组件封装

将我的订单页面的右侧封装成两个部分:grouporder(团购订单)和myorder(我的订单)
在引入路由的时候,将这两个组件看作是我的订单页面的子组件

1.2组件显示

我的订单

路由组件出口的位置 --> <router-view></router-view>

1.3获取个人中心的数据

URL:api/order/auth/{page}/{limit} get
export const reqMyOrderList = (page,limit)=>requests({url:`/order/auth/${page}/${limit}`,method:'get'});

1.4未登录的导航守卫判断

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();}

2.图片懒加载

路由懒加载
面试【高频的面试】:项目的性能优化手段有哪些?
v-if|v-show:尽可能采用v-show
按需引入【lodash、elementUI】
防抖与节流
路由懒加载:当用户访问的时候,加载对应组件进行展示。

图片懒加载:
vue-lazyload:图片懒加载
npm install vue-lazyload
引入图片:

import atm from "@/assets/12.png"
Vue.use(VueLazyload, {loading: atm
});

使用懒加载:
<img v-lazy="good.defaultImg"/>
图片:比用用户网络不好,服务器的数据没有回来,
总不可能让用户看白色,至少有一个默认图片在展示。

3.自定义插件

(用来理解lazyload懒加载为什么那样写)
myplugins自定义组件

let myPlugins = {};
myPlugins.install = function(Vue,options){Vue.directive(options.name,(element,params)=>{element.innerHTML = params.value.toUpperCase();});}

在程序入口main.js引用组件:

import myPlugins from './plugins/myPlugins'
Vue.use(myPlugins,{name:'upper'
})

使用组件
<h1 v-upper="xxx"></h>

Vue.directive()
指令跟组件一样需要注册才能使用,同样有两种方式,一种是全局注册:Vue.directive('name',function(){})一种是局部注册:new Vue({directive:{name:{定义指令}}})
指令的定义

指令定义,官方提供了五个钩子函数来供我们使用,分别代表了一个组件的各个生命周期
bind: 只调用一次,指令第一次绑定到元素时调用,用这个钩子函数可以定义一个在绑定时执行一次的初始化动作。
inserted: 被绑定元素插入父节点时调用(父节点存在即可调用,不必存在于 document 中)。
update: 被绑定元素所在的模板更新时调用,而不论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新
componentUpdated: 被绑定元素所在模板完成一次更新周期时调用。
unbind: 只调用一次, 指令与元素解绑时调用。

钩子函数的定义

el: 指令所绑定的元素,可以用来直接操作 DOM 。
binding: 一个对象,包含以下属性:
name: 指令名,不包括 v- 前缀。
value: 指令的绑定值, 例如: v-my-directive="1 + 1", value 的值是 2。
oldValue: 指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。
expression: 绑定值的字符串形式。 例如 v-my-directive="1 + 1" , expression 的值是 "1 + 1"。
arg: 传给指令的参数。例如 v-my-directive:foo, arg 的值是 "foo"。
modifiers: 一个包含修饰符的对象。 例如: v-my-directive.foo.bar, 修饰符对象 modifiers 的值是{ foo: true, bar: true }。
vnode: Vue 编译生成的虚拟节点,查阅 VNode API 了解更多详情。
oldVnode: 上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。

4.表单验证

【后台管理系统:大量使用elementUI】
以后工作的时候经常会进行表单验证【element-ui】进行表单验证,so 简单。
项目当中表单验证功能比较常见的。

4.1vee-validate插件

Vue官方提供的一个表单验证的插件【大概看懂即可】
使用步骤:
1:安装vee-validate,别安装最新版本@2
npm install vee-validate@2 --save
2:在plugins文件夹中创建一个validate.js[专门注册vee-valadite]
3:注册插件
4:注册插件的时候,用中文,以及需要验证的字段【用中文显示提示形式】
5:在入口文件需要引入执行一次
6:使用vee-valadiate插件
如给手机号input加上属性name="phone" v-validate="{ required: true, regex: /^1\d{10}$/ }"

vee-validate插件:表单验证区域
import Vue from "vue";
import VeeValidate from "vee-validate";
//中文提示信息
import zh_CN from "vee-validate/dist/locale/zh_CN";
Vue.use(VeeValidate);//表单验证
VeeValidate.Validator.localize("zh_CN", {messages: {...zh_CN.messages,is: (field) => `${field}必须与密码相同`, // 修改内置规则的 message,让确认密码和密码相同},attributes: {phone: "手机号",code: "验证码",password: "密码",password1: "确认密码",agree:'协议'},
});//自定义校验规则
VeeValidate.Validator.extend("tongyi", {validate: (value) => {return value;},getMessage: (field) => field + "必须同意",
});

5.路由懒加载

整个网页默认是刚打开就去加载所有页面,路由懒加载就是只加载你当前点击的那个模块。

按需去加载路由对应的资源,提高首屏加载速度(tip:首页不用设置懒加载,而且一个页面加载过后再次访问不会重复加载)。

实现原理:将路由相关的组件,不再直接导入了,而是改写成异步组件的写法,只有当函数被调用的时候,才去加载对应的组件内容。

如:component: ()=>import('@/pages/home/Home'),

完结撒花!!还有一些自己测试发现的bug尽量改出来然后再发一个博客水水

尚品汇前端每日跟练笔记相关推荐

  1. Vue前端项目【尚品汇】

    Vue前端项目[尚品汇] 1. 说明 2. 对项目创建 3.结构 4. 项目运行起来时,浏览器自动打开 5.关闭ESLint校验功能 5.路由分析 6.路由元信息 7. 路由传参 7.1 路由的跳转方 ...

  2. 尚品汇项目笔记(持续更新中)

    项目网络教学视频链接:尚硅谷VUE项目实战,前端项目-尚品汇(大型\重磅)_哔哩哔哩_bilibili 目录 一. 使用vue-cli脚手架去初始化项目 二.项目的其他配置 三.项目路由分析 四.创建 ...

  3. 前端 | ( 九)尚品汇实操练习 | 尚硅谷前端html+css零基础教程2023最新

    学习来源:尚硅谷前端html+css零基础教程,2023最新前端开发html5+css3视频 系列笔记: [HTML4](一)前端简介 [HTML4](二)各种各样的常用标签 [HTML4](三)表单 ...

  4. 电商项目尚品汇学习笔记

    本文参考其他文章自己整理补充的,要阅读原文请查看:尚品汇项目笔记_爱哭的毛毛虫的博客-CSDN博客_尚品汇项目 1.vue文件目录 public文件夹:静态资源,webpack进行打包的时候会原封不动 ...

  5. 尚品汇小结(-前端完成)

    滚动行为 在页面滚动到底部点击detail的商品图片之后,用router-link跳转,同时请求的数据也要通过三连环来实现 <router-link :to="`/detail/${g ...

  6. 尚硅谷VUE项目实战,前端项目-尚品汇

    001-尚硅谷-尚品汇-教程简介_哔哩哔哩_bilibili 需要的知识点   搭建vue-router pages文件夹放置路由页面 router文件夹配置路由 回到入口文件注册(main.js) ...

  7. vue实战项目-电商商城前台-(学习尚硅谷的)尚品汇

    文章目录 最好使用视频上的账号密码,13700000000 密:111111 最新服务端接口地址:http://gmall-h5-api.atguigu.cn 脚手架使用 1.创建项目 2.脚手架默认 ...

  8. vue尚品汇商城项目-day00【项目介绍:此项目是基于vue2的前台电商项目和后台管理系统】

    文章目录 本人其他相关文章链接 项目介绍:此项目是基于vue2的前台电商项目和后台管理系统 此项目为在线电商Web App (SPA) 包括首页, 搜索列表, 商品详情, 购物车, 订单, 支付, 用 ...

  9. 尚硅谷 VUE 尚品汇项目实战问题解决方式整理(Vue3 版)

    教学视频:https://www.bilibili.com/video/BV1Vf4y1T7bw 不回要资料的评论,资料在视频底下评论有些仓库里面有. 试图自行解决问题也是程序员必备技能之一,多做尝试 ...

最新文章

  1. postgres 入门
  2. Springboot 使用thymeleaf模板layout布局
  3. MySQL的字符编码体系(一)——数据存储编码
  4. OPENCV标定外参
  5. x265-bitstream.cpp
  6. sync是同步还是非同步_音视频是怎么保持同步的?(四)
  7. 分布式事务,EventBus 解决方案:CAP【中文文档】
  8. python乘法函数_Python中列表与元组的乘法操作示例
  9. poj1753_flipgame_枚举
  10. [转载] Docker网络原则入门:EXPOSE,-p,-P,-link
  11. 张高兴的 Windows 10 IoT 开发笔记:三轴数字罗盘 HMC5883L
  12. 个人博客,个人博客模版,用HTML+CSS做一个漂亮简单的个人网页,个人博客网站html源码
  13. 简单说说Java SE、Java EE、Java ME三者之间的区别
  14. CO-PA: 获利能力分析类型(基于成本核算、基于会计核算)解析
  15. AltiumDesigner16安装教程
  16. matlab怎么导入程序出错,Matlab导入数据时出错!十分困扰!
  17. AHU计科(伪)新生指南
  18. kafka内外网连接问题
  19. WSL1安装rust报错thread ‘main‘ panicked的解决方法
  20. 【DFS 水洼数目】

热门文章

  1. 代理服务器 (proxy) 的使用
  2. 周小桥老师--技术与产品敏捷创新项目管理资深专家--沪师经纪刘建
  3. 【开源】小桥流水秒赞 3.8版本全解密无后门
  4. 计算机电源的正确使用,终于懂得电脑电源保养方法
  5. 用代码来过端午节---基于HTML的端午节划龙舟小游戏
  6. C++中的friend class 用法总结
  7. 什么是ROM,RAM,SRAM,SDRAM,DDR,DDR2,DDR3
  8. 【Linux】必备故障及原因50个
  9. 第一章 简介(待续)
  10. php不能使用class,php – 致命错误:不能使用stdClass类型的对象作为数组