这个项目是用react全家桶重构之前用vue2写的购物网站
项目地址:https://gitee.com/feng-chengxiang/shopping-mall-vue.git
我在这里不会记录一些重复的笔记,因为之前的项目已经写的十分详尽了。
这个项目有一个redux分支,该分支把主分支里的兄弟组件通信,以及重复发送的异步请求
都用redux进行管理。
组件挂载时会发送请求,怎么做到只发送一次请求,切换路由组件又回来后,不再发送请求?是只能用状态管理工具了吧。有没有keepalive呀?
const [CategoryList,SetCategoryList] = useState([])
useEffect(()=>{
const doAsync = async()=>{
let result = await reqCategoryList()
if(result.code==200){
// console.log("hahahhaha")
SetCategoryList(result.data)
}
}
doAsync().catch((error)=>console.log(error.msg))
},[])
// console.log(CategoryList,'List')
之前学的都好好的,到实战还真不知道具体的使用场景,还需要多练练啊。我打算把所有业务逻辑写完后,再来系统地看看那些地方可以优化。
ReactRouter6,相比较5,多了很多钩子
我这个项目需要共享的状态不多也不少,可以使用状态管理工具。我选择的是React-Redux,采用connect方案。
也就是容器组件连接UI组件和redux的方案。最新的话redux提供了两个新钩子useSelector和useDispatch,不用connect也
可以实现,有机会会实验一下。
最后的成品没有用到状态管理工具。
PS:分支上用到了
父子用props,兄弟我用的是PubSub消息订阅。
然后用这个PubSub,我感觉使用体验上和Vue的自定义事件差不多,写的时候很爽,但是比较混乱。
(26条消息) React 代理 setupProxy 无法启动项目_YoungJayDo257248的博客-CSDN博客
https://blog.csdn.net/weixin_42681295/article/details/108670040
为防止css作用域污染,React要麻烦一些。我使用的方案是index.module.css的方式引入。使用classNames和一个插件快速替换
传送门:
如何在React中优雅的写CSS - 掘金 (juejin.cn)
(26条消息) className多种样式的三种写法_重阳微噪的博客-CSDN博客_classname 写多个
在react中使用css module - 秦伟杰 - 博客园 (cnblogs.com)
reset.css里面就只有清除浮动这一个是自定义的属性,直接写在style里面就行。但我觉得一定有更好的解决方案,不然一旦需要清除浮动就需要手动添加clearFix到对应的xxx.module.css里面,那这样在public里面全局引入的reset.css还有什么意义呢?
不用1号方案那样写。
直接这样写:
className={[style.tab,'clearFix'].join(' ')}
img必须闭合引入src有要求
这个属性写成defaultChecked,代表非受控组件
写成defaultValue代表非受控,和上边的defaultChecked一个道理
react里面的value和defaultValue_唐霜的博客 (tangshuang.net)
必须写成驼峰
用JSX map的时候,必须保证返回一个节点,v-for没这限制
这个地方有两种方案:
方案1.npm run eject
单向操作不可逆,npm run eject命令暴露项目的配置,这样用户就可以完全取得 webpack 文件的控制权我不是很推荐这种做法
方案2.react-app-rewired或者carco
通过创建一个 config-overrides.js
文件来对 webpack 配置进行扩展。推荐。
我用的高版本的中间件要换种写法
https://blog.csdn.net/weixin_42681295/article/details/108670040
(26条消息) React 代理 setupProxy 无法启动项目_YoungJayDo257248的博客-CSDN博客
这一部分我在Vue2区别和React二者区别那里,已经解决了。不过没找到更好的方法。
原项目中依赖路由元信息,选择是否展示Footer组件。我看了下,只有两个组件:登录组件和注册组件不用展示,
那么直接判断pathname就行了。这显然不是最优解,很想知道react怎么自定义路由配置项。
Swiper在React中的使用,要变下使用方式
官网:Swiper React Components (swiperjs.com)
这个怎么说官方也提供了相关钩子,很方便就可以实现轮播图。以后使用的时候去查查参数就行
import { Navigation, Pagination, Scrollbar, A11y } from 'swiper';
import { Swiper, SwiperSlide } from 'swiper/react';
import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';
import 'swiper/css/scrollbar';
const Carousel = () => {
return (
<Swiper
modules={[Navigation, Pagination, Scrollbar, A11y]}
// spaceBetween={50}
slidesPerView={1}
navigation
pagination={{ clickable: true }}
scrollbar={{ draggable: true }}
>
<SwiperSlide><img src={require('./images/banner1.jpg')} alt="" /></SwiperSlide>
<SwiperSlide><img src={require('./images/banner2.jpg')} alt="" /></SwiperSlide>
<SwiperSlide><img src={require('./images/banner3.jpg')} alt="" /></SwiperSlide>
<SwiperSlide><img src={require('./images/banner4.jpg')} alt="" /></SwiperSlide>
...
</Swiper>
);
};
export default Carousel
组件挂载时会发送请求,怎么做到只发送一次请求,切换路由组件又回来后,不再发送请求?
const [CategoryList,SetCategoryList] = useState([])
useEffect(()=>{
const doAsync = async()=>{
let result = await reqCategoryList()
if(result.code==200){
// console.log("hahahhaha")
SetCategoryList(result.data)
}
}
doAsync().catch((error)=>console.log(error.msg))
},[])
// console.log(CategoryList,'List')
这个是H5让我们获取自定义属性的,我们在三级联动那里需要用到,原笔记说的很清楚。
而且只有我们没有点击空白区域,才能正确获得自定义属性。
function goSearch(event) {
event.preventDefault()
let element =event.target
let { categoryname, categoryid_1, categoryid_2, categoryid_3 } =element.dataset;
console.log(categoryname,categoryid_1,categoryid_2,categoryid_3)
}
react里面使用animate.css的方案不太一样,有点麻烦。这种锦上添花,影响用户体验的部分,我放到后面来做。
这个在reactRouter里面该怎么设置呀,以前是在占位后面加一个‘?’。但我试了下,不行呀。
TypeNav的search参数和搜索框里面的params参数需要合并,传递给search路由组件。当然可能出现用户不搜索直接选择
TypeNav的可能,这个时候我们的params参数为空。但路由注册时已经占位,不能为空。
那么,我想到了3种方案:
判断一下用户是否为直接点击TypeNav过来的,这种情况我们手动添加一个params参数占位就行。可以用分类名占位,语义化一些
放弃params参数,改用state参数。因为最终我们的目的还是是发送请求,只要能获取到参数就行,useNavigate对
state参数支持还更友好。
再加一个没有params参数占位的路由,不知道这样标不标准,但确实可以正常运行哈
我最后用的是方案三。
我发现在react-router里面获取search参数真的很麻烦额,以前要解析字符串。虽然现在多了个useSearchParams钩子,
但是取得时候必须晓得键才可以,因为有3种不同商品ID作为search参数,我需要不断判断一下,很麻烦。
尚未解决。
我采用useRef存储最新状态。
传送门:
react useState 异步 数据获取不到 - 掘金 (juejin.cn)
useState改变值之后立刻获取最新的状态 - 掘金 (juejin.cn)
成功解决,下面就是发请求,动态展示数据了。
而且由于是额外添加了一个容器存放参数对象,我感觉useState都可以省了。
我同时用vue2项目,apiPost和现在写的这个项目测试接口。因为后台不是我自己写的,频繁发送请求,直接504了……
B站的小伙伴大多也遇见了这种情况呢。可能因为我测试的时候还有很多人也在一起用吧。
类似v-for的操作,我们都需要map喔。JSX里面map return的时候必须只能返回一个节点
说实话,原来的vue2项目这部分逻辑就写的不太好,老师也没写好。但主主要的原因还是,后台接口不完善,可用数据不是很多,而且数据比较混乱,很多商品和其销售属性牛头不对马嘴。白嫖的还是可以理解。
然后就是我感觉面包屑也可以拆出去变成一个单独的组件喔,但是这个项目只有search组件这一个地方用了面包屑,不拆也行,就是业务逻辑变多了。
删除面包屑,发请求,接着更新地址栏。更新地址栏的方法只有通过路由自己跳自己吗?
场景是删除关键字面包屑(search组件),搜索栏清空(header组件),这两是兄弟关系。
涉及到兄弟组件通信喔,context,pubsub,redux,props多传几次都行。先暂时放在这里,我看下后面还有没需要兄弟组件通信的地方。
我都是在public下在线引入css
这里写法又不一样了
react写法
<li className={isOne() ? style.active : null} onClick={changeOrder('1')}>
<a>综合
{
isOne() &&
<span
className={['iconfont', isDesc() ? 'icon-xiangxiajiantou' : null, isAsc() ? 'icon-xiangshangjiantou' : null].join(' ')}
></span>
}
</a>
</li>
vue写法
<li :class="{ active: isOne }" @click="changeOrder('1')">
<a>综合
<span
v-if="isOne"
class="iconfont"
:class="{
'icon-xiangshangjiantou': isAsc,
'icon-xiangxiajiantou': isDesc,
}"
></span
></a>
</li>
差别还是很大的
分页器这里,我又添了个改变每页展示数据的功能。原笔记把自定义分页器说得十分详尽,这里不再赘述。
前一天已经把Search组件基本开发完成。今天开始开发detail组件。
声明的时候要用{}包一下,老是要搞忘。
<Link to={`/detail/${goods.id}`}>
<img src={goods.defaultImg} />
</Link>
只要涉及到后台,就会有一些奇怪的数据喔。有时候你点图片可以发请求,发请求也成功了,数据也得到了,但是数据键都有,就是没值,全为null或者0.就很坑。还有些情况是有商品数据,没有图片数据,也要注意这个坑。所以在你看见购物车详情组件显示有误时,就是我没考虑到后台数据的不完整性。后台不要添加些奇怪的数据啊!
这个是因为虽然改变了isChecked,但是没有重新渲染。这个很容易理解,我们只需要重新set一下,改变状态就行。
而且必须是传入一个新的对象,也就是深拷贝。直接spread展开就行,但是我觉得创建这个新对象代价是不是太大了?
既然必须重新渲染,我想了个邪门的方法,维护一个没用的且简单的state,改变isChecked的时候,更新这个无用的state就行了。不知道真实开发中应该怎么做,有没啥更好的方法呢?
有坑,因为获得的鼠标移动事件不是原生的事件,是React的合成事件。SyntheticBaseEvent。然后他上面没有offsetX。
我们用event.nativeEvent获取原生的事件。
传送门:React的mouseEvent没有offsetX/offsetY - VoidCC
然后就是放大器Zoom组件和下方缩略图ImageList组件是兄弟组件,需要把ImageList的图片索引传给Zoom。涉及到兄弟组件通信,我用的是PubSub来通信。
在useEffect添一句window.scrollTo(0,0)就行。在vue2项目里面我们使用的是滚动事件解决。
今天开始开发购物车组件了。
配合后台在header里面添加一个自定义请求头userTempld,往里面放一个uuid就行。然后我们的uuid是放在localStorage里面做持久化存储。
我们使用input框改变商品数目时,发现无法输入。
react里面的value和defaultValue_唐霜的博客 (tangshuang.net)
我发现这个输入框有重大bug。
我想使用onBlur失去焦点后再发请求,但是直接报错。因为value必须要和onChange搭配,不然无法输入。
暂时没找到啥好的解决方案。而且由于添加了500ms的节流,用户输入会有延迟。
改变商品数目真的需要发请求吗?
我感觉可以最后点结算再发请求,但这样就需要使用非受控组件。但这样与数量有关的数据显示,就不能实时更新了。
只能说更加细化地限制用户输入input动作了。
我发现如果有多个商品,可以正常删除。但是如果只有一个商品,删除后,需要用户手动更新一下才能正确显示购物车为空的提示信息,这是为啥?我觉得删除完后,再次发送获取购物车的请求,会改变状态,应该重新渲染才对,应该可以正确显示,但事实并非如此。目前还没找到错。
这个涉及到在循环内使用async await。我们和第三点一样,还是有类似的问题,如果把所有的都删除,需要用户手动更新一下才能正确显示购物车为空的提示信息。
五种在循环中使用 async/await 的方法 - 知乎 (zhihu.com)
我这个是真的不知道为啥,已经checked,但是页面还是没有渲染已勾选状态。有点懵
今天主要开发登录和注册组件
vue2项目用的是vee-validate插件进行验证。我在这里用的是formik进行表单验证。把官方例子抄过来,改改就行了。我感觉这个formik做表单验证很方便,以后react项目里面就用它了。
这个验证码,根本不用用户输入,直接点击按钮就行。而且是个假的验证码,不能通过手机号发送,因为通过手机接收要钱,可以理解。那我感觉把验证码框直接disabled得了。
这里又是受控组件input的value的坑,不再赘述。
这个后台接口不是很完善,输入错误手机号,输入正确手机号但密码错误都没有相应接口。
经我测试以上错误情况统一返回是这个:
这个地方需要是否已经登录,登录后才能从结算按钮跳转过来。原vue2项目写的是路由守卫,我在这里只是根据token简单判断了下。然后后台有一个获取收件人信息的接口,只有用老师的账号和密码才能获取收件人信息数据
账号:13700000000 密码:111111
今天主要是Pay组件和Center组件开发
发请求注意参数可能不存在的情况:
用可选链的形式
let data={
consignee:address?.consignee,//收件人
consigneeTel: address?.phoneNum,//电话
deliveryAddress: address?.fullAddress,//地址
paymentWay: "ONLINE", //支付方式
orderComment: msgRef.current.value, //买家的留言信息
orderDetailList: detailArrayList, //商品清单
}
这里面有个点击按钮弹出消息对话框的UI,vue2项目用的是Element-UI,这里当然是用antD啦。顺便也可以按需引入。
感觉会配置很久……
我使用的是react-app-rewired,不是carco配置。捣鼓了半天按需引入成功,自定义主题失败……
微信支付这里如何判断用户是否真的支付成功?我用的是一个定时器不断地向后台发请求,直至成功或者关闭对话框。
真实开发中真的是这样做的吗?
然后点击对话框x或者取消按钮触发的是同一个回调函数,怎么区分开来?我直接把x按钮取消掉了
怎么说,antD设计之初就是为了开发一整套应用,没找到啥好的解决办法。我看网上很多人,都直接eject来配置的,我就算了。按官网文档操作不起作用。
自定义主题也失败了……
安装npm install --save react-lazyload失败。
根据提示加参数npm install --save react-lazyload --legacy-peer-deps
传送门:(26条消息) npm的“--force“和“--legacy-peer-deps“参数_奔跑吧邓邓子的博客-CSDN博客_--force npm
使用:(26条消息) 《图片懒加载之react-lazyload》_杨晓风-linda的博客-CSDN博客_react懒加载图片
这个怎么说,我的路由使用的是useRoutes引入路由表的方式,还真不知道这种方式怎么用lazy+Suspense的方式实现路由懒加载。容我先研究研究。
可以这样写
// 懒加载
const lazyLoad = (path) => {
const Comp = lazy(() => import(`@pag/${path}`))
return (
<Suspense fallback={
<>
<img src={"images/加载.gif"}
style={{ display: 'block', width: '500px', height: '500px', margin: '0 auto' }}
alt="懒加载的图都找不到吗?" />
</>}>
<Comp />
</Suspense>
)
}
const routes = [
//一级路由
{
path: '/home',
element: lazyLoad('Home')
},
…………………………………………………………………………………………
//嵌套路由
{
path: '/center',
element: lazyLoad('Center'),
children: [
{
path: 'myOrder',
element: lazyLoad('Center/MyOrder')
},
{
path: 'groupOrder',
element: lazyLoad('Center/GroupOrder')
},
{
path: '',
element: <Navigate to="/center/myOrder" />
}
]
},
{
path: '/',
element: <Navigate to="/home" />
},
]
这个东西,react好像还真没有类似vue中beforeEach的函数。不过我们可以在跳转里面自己写跳转逻辑,就是这样写比较分散,而且会写很多遍,肯定不能这么写。
我看网上大多数都是封装下Route,因为我用的是useRoutes引入路由表的方式,所以不太可能用这种方法。所以到底该咋办呀?
可以调节闪光颜色和角度。我觉得很好看!
这个可以做到,只是控制颗粒度我感觉没有以前的vue项目细。
三个简单方案:
我可以在路由表里面实现:
就像这样:
{
path: '/login',
// 已经登录还想登录(通过地址栏输入可以做到),跳到首页
element: getToken()?<Navigate to="/home"/>:lazyLoad('Login')
},
我如果想在里面再加一些逻辑呢?比如有个提示框啥的,这个到底该咋写呀?
还有一些权限控制是特定页面向特定页面跳转才会有限制,我通过什么方法可以拿到from和to呢?
只能说用的版本太新了,网上也没找到啥好办法。
我看到有用useHistory的,但是这个方法6版本里面没了……
如果想实现更多逻辑,应该是只能在对应组件里面写逻辑了,没办法在配置路由表里写了。这样写极其分散,很难一眼看出来那些地方做了路由拦截。
就像这样:
//首次挂载发请求
useEffect(() => {
if(!getToken()){
message.warning('请登录!')
navigate('/login')
}
}, [])
而且因为写在useEffect,且依赖项为空数组的情况下。不就需要先render一次,再跳转吗?这还算路由拦截吗?虽然也能实现功能
我真傻,真的。直接封装组件就行了嘛。
注意不能封装成函数,因为这样就只有第一次会生效了。
就像这样:
路由表使用
{
path: '/login',
element: <RouterGuard path="login">{lazyLoad('Login')}</RouterGuard>
},
封装成组件
function RouterGuard({path,children}){
// 登录的情况下又登录
if(path==='login'){
if(getToken()){
alert('你已经登录!')
// 用antD组件会报错!
// message.warning('你已经登录!')
return <Navigate to="/home" />
}
return children
}
}
用antD组件message会报错!纯函数的原因,alert就没问题
这个很简单,是因为之前的input是非受控组件,改成受控组件,再把onClick改为onChange即可
这个bug解决也很简单,当初也不知道怎么的,这个都没解决。
我们删除对应商品后,会再去调用getCartList。
如果已经是最后一个商品,删除以后再调用getCartList,虽然code为200.但是result.data.length已经为0了,根本
不能SetCartList
//获取购物车列表
const getCartList = async () => {
let result = await reqGetCartList()
if (result.code === 200 && result.data.length > 0) {
SetCartList(result.data[0].cartInfoList)
}
}
改成下面这样就行了
//获取购物车列表
const getCartList = async () => {
let result = await reqGetCartList()
if (result.code === 200 ) {
// 删除完购物车后,把CartList重新置空
if(result.data.length===0){
SetCartList([])
}else{
SetCartList(result.data[0].cartInfoList)
}
}
}
我想再用redux重构一下整个项目的状态管理,使用redux-thunk处理异步action。
已经建好了一个git分支redux,会把一些需要共享的状态,和一些重复发送的请求用redux管理
有两个地方兄弟通信,用的pubsub。改一下用redux管理,这部分就不用redux-thunk了。比较简单
分别是:
- Seacrh组件keyword面包屑点击取消后,搜索栏清空
- detail组件里面的小图列表组件和大图组件通信,点击小图传递对应小图索引给大图。
三级联动请求:reqCategoryList
有重复发送的现象,实际上只发一次存储在state里就行了。
我们可以用react-thunk中间件解决发送axios请求获取数据的问题
对应的action,我们可以返回一个函数喔,会自动执行的
import { get_Category_List } from '@redux/constant'
import { reqCategoryList } from '@/api'
export function getCategoryListAction() {
return async (dispatch) => {
try {
let result = await reqCategoryList()
if (result.code === 200) {
//console.log('三级联动1')
dispatch({ type: get_Category_List, payLoad: result.data })
} else {
alert('请求失败!')
}
} catch (error) {
alert(error)
}
}
}
对应的reducer。
import {get_Category_List} from '@redux/constant'
export function getCategoryListReducer(preState=[],action){
let {type,payLoad}=action
switch(type){
// 直接用请求回来的值替换掉state
case get_Category_List:
return payLoad
default:
return preState
}
}
实际使用的时候,我们只需要先判断一下仓库数据是否为空,只有为空的时候才发送action。这样就做到只用发一次请求
了。
我感觉还可以再优化一下,比如用户已经获取到三级联动数据了。刷新一下,对于这种基本上不会改变的数据,可以
持久化一下,让它再用户刷新后依然保留仓库数据。
然后一定要注意自定义持久化!不然所有仓库数据都会保存下来。
按照官方的例子配就行。
传送门:rt2zz/redux-persist: persist and rehydrate a redux store (github.com)
我有一个地方配错了,就是白名单那里
//持久化配置
const persistConfig = {
key: 'CategoryList',
storage,
//白名单,只持久化三级联动,注意这里是combineReducers里面的键喔
whitelist:['CategoryListState']
}
尝试使用了下useSelector,useDispath这两个新钩子,就不用connect了,减少了模板代码。
用了下ahooks的useRequest,确实好用,封装的很好,所有的异步请求其实都可以用这个。
类似的库还有react-use,基本上能想到的需求,都有现成的钩子可以使用。
reactjs - Does React have something like Vue's keep-alive element? - Stack Overflow
react好像还真没路由缓存,我觉得有些地方的路由缓存还是很有必要的吧。组件里面的状态可以用redux保存起来,也可以用react-persist做持久化。具体场景具体分析吧。