携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第一天,点击查看活动详情
前言
夏日炎炎,现在的每天气温都高达30度+,想必大家都不愿意出门暴晒,小编给大家准备美团外卖 0.0.1版,欢迎大伙下单,骑手小哥给大伙送货上门哟,我给大家打骨折八折。
咳咳,言归正传,笔者最近学习了最新的react hooks + redux,由于最近疫情问题,每天都点外卖,点多了,有了感情,于是想着要不自己也整个。所以就开发了一个破解版美团外卖,下单不用钱,欢迎大伙来捧个场子(手动狗头)。
技术栈总览
react全家桶:react、react-router、react-router-domredux:Redux 是 JavaScript 状态容器,提供可预测化的状态管理。redux-thunk:用最简单的方式搭建异步 action 构造器,属于redux生态系统的中间件axios:Axios,是一个基于promise的网络请求库,作用于node.js和浏览器中,常用来获取数据classnames:一个简单的 JavaScript 实用程序,用于有条件地将类名连接在一起。font-awesome:字体图标库react-lazyload:顾名思义,react中的懒加载库react-weui与antd-mobile:好用的UI组件库styled-components:react官方都觉得好用,称之为css in js,用来编写css样式swiper:移动端最常用的轮播图组件库
项目初体验
先给大伙安利一下,认个眼熟哈哈
- 白天的首页
- 商品详情页
- 购物车
- 商品评价和店家信息
项目起步
1. 项目结构
├─ public
├─ src
├─api // 网路请求及相关配置
config.js // 接口配置文件
request.js // 接口请求文件
adapter.js // 移动端自适应配置
utils.js // 封装的工具函数
├─assets // 字体图标、图片等静态资源
├─components // 可复用的 UI 组件
├─common // 通用组件
├─pages // 页面级别组件
Home // 首页
Cities // 位置详情
HomeDetail // 商品详情
Order // 订单
Mine // 我的
Search // 搜索
├─routes // 路由配置文件
├─store // redux总仓库
├─styles // css初始化样式
└─utils // 全局的工具函数
App.jsx // 根组件
main.jsx // 入口文件
2. 路由配置
在最开始配置路由时,是直接在根组件引入Route,可随着路由数量的增多,就有必要对路由进行单独管理。于是我在routes目录封装了RoutesConfig函数,在根组件引入,并在RoutesConfig组件外层嵌套Suspense组件。
PS:在路由配置时可以使用react自带的lazy和Suspense对页面进行懒加载,这样页面相当于按需加载,只有你使用它的时候才会加载它,可以减少首页白屏时间。
- 根组件具体代码如下
- RoutesConfig组件
// 独立配置文件
import { lazy} from 'react'
import { Routes, Route, Link } from 'react-router-dom'
import Home from '@/pages/Home'
const Order = lazy(() => import('@/pages/Order'))
const Mine = lazy(() => import('@/pages/Mine'))
const Cities = lazy(() => import('@/pages/Cities'))
const Search = lazy(() =>import('@/pages/Search'))
const HomeDetail = lazy(() => import('@/pages/HomeDetail'))
const HomeOrder = lazy(() => import('@/pages/HomeDetail/HomeOrder'))
const HomeComment = lazy(() => import('@/pages/HomeDetail/HomeComment'))
const HomeBusiness = lazy(() => import('@/pages/HomeDetail/HomeBusiness'))
const RoutesConfig = () => {
return (
<Routes>
<Route path="/" element={<Home /> }></Route>
<Route path="/home" element={<Home />}></Route>
<Route path="/order" element={<Order />}></Route>
<Route path="/mine" element={<Mine />}></Route>
<Route path="/cities" element={<Cities />}></Route>
<Route path="/search" element={<Search/>}></Route>
<Route path="/homedetail/:id" element={<HomeDetail />}>
<Route path='/homedetail/:id/order' element={<HomeOrder/>}></Route>
<Route path='/homedetail/:id/comment' element={<HomeComment/>}></Route>
<Route path='/homedetail/:id/business' element={<HomeBusiness/>}></Route>
</Route>
</Routes>
)
}
export default RoutesConfig
3. 数据接口
众所周知,在项目开发的初期,有一个完善的后端接口真是美事啊。在这里给大家安利一个别人给我安利的好插件。
JSON-Handle:对于杂乱的JSON文件进行解析编写,得到你想要的JSON数据文件;大伙可以在F12开发者工具里的Netword栏里,再筛选fetch/xhr请求找到可以用的数据文件。
可是大伙们,俺是在项目快搞定的时候才记得有这儿一回事o(╥﹏╥)o(手扒了整个项目的数据)。有没有专业的大哥告诉俺咋怎么快速获取有用的接口数据,可以在评论区告诉小编
本项目的数据是在FastMock上存储的,这是一个很好用的免费模拟接口网站
然后在api目录封装了request.js,用以保存对所有接口的引入
4. 使用redux对数据流进行管理
到了这里准备工作已经就绪了,开始项目开发了。
在redux官方文档中,我们可以得知什么时候该用redux来管理你的项目state。有一位古希腊的哲学家说过:当你在犹豫要不要使用它的时候,那你还是先别用它。不过我犹豫了我也用就是
-
首先在src目录下新建一个store文件夹,里面存储着本项目唯一的store仓库和reducer函数。
Redux 应用只有一个单一的 store。当需要拆分数据处理逻辑时,你应该使用 reducer 组合 而不是创建多个 store。 就跟我们学习的二叉树、多叉树一样,每一个单页面就是一个分叉,在reducer中combineReducers一下,就绘制成了一颗完整的树
-
redux状态管理
-
store代码如下
import { createStore, compose, applyMiddleware } from 'redux' import thunk from 'redux-thunk' import reducer from './reducer' const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; const store = createStore(reducer, composeEnhancers( applyMiddleware(thunk) )) export default store -
reducer代码如下
import {combineReducers} from 'redux' import {reducer as CitiesReducer} from '@/pages/Cities/store/index' import {reducer as HomeReducer} from '@/pages/Home/store/index' import {reducer as SearchReducer} from '@/pages/Search/store/index' import {reducer as HomeDetailGoodsReducer} from '@/pages/HomeDetail/HomeOrder/store/index' import {reducer as HomeDetailCommentsReducer} from '@/pages/HomeDetail/HomeComment/store/index' import {reducer as HomeDetailBusinessReducer} from '@/pages/HomeDetail/HomeBusiness/store/index' export default combineReducers({ cities:CitiesReducer, home:HomeReducer, search:SearchReducer, comment:HomeDetailCommentsReducer, business:HomeDetailBusinessReducer, goods:HomeDetailGoodsReducer })
-
-
store.js中使用的第三方包或函数
redux-thunk可以实现redux处理异步action。applyMiddleware: 顾名思义,这是对于中间件的引用,通过compose函数可以将多个中间件合并为一个中间件对象const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose:可以让浏览器插件 Redux DevTools 生效,以便开发人员模式运行应用。
-
在各个页面创建子仓库,每一个子仓库就是一个分支。
拿首页举例
├─Home // 首页 store // 仓库 actionCreators.js // action纯函数 constants.js // 保存着action的type index.js // 子仓库本身 reducer.js // reducer指定了应用状态的变化如何响应actions并发送到store的这样方便对每一个路由页面进行数据流管理,而不用在总仓库中堆积
-
最后,在入口文件
main.js中引入总store仓库我们要先在根组件
<APP/>最外层包裹一层react-redux提供的Provide声明。这样每次store数据改变时才能通知到每个组件。import React from 'react' import ReactDOM from 'react-dom/client' import App from './App' import { BrowserRouter } from 'react-router-dom' // 引入初始化css样式文件 import '@/styles/reset.css' // 引入字体图标 import 'font-awesome/css/font-awesome.min.css' import 'swiper/dist/css/swiper.min.css' // 使用redux import { Provider } from 'react-redux' import store from './store' ReactDOM.createRoot(document.getElementById('root')).render( <Provider store={store}> <BrowserRouter> <App /> </BrowserRouter> </Provider**>** ) -
对了,在组件中使用
redux管理好的数据需要做好这两点- 还是那首页举例,首先来创建容器组件把这些展示组件和
Redux关联起来。技术上讲,容器组件就是使用store.subscribe()从Redux state树中读取部分数据,并通过props来把这些数据提供给要渲染的组件。 - 所以我们可以使用
react-redux库的connect()方法来生成容器组件,这个方法做了性能优化来避免很多不必要的重复渲染
- 模板代码如下
import { getRestaurantsList, getBannersList } from './store/actionCreators' function Home(props) { const { restaurants, banners, loading } = props const { getRestaurantsListDispatch, getBannersListDispatch } = props useEffect(() => { getBannersListDispatch() getRestaurantsListDispatch() }, []) return( ... ) } const mapStateToProps = (state) => { return { banners: state.home.BannersList, restaurants: state.home.RestaurantsList, loading: state.home.Loading } } const mapDispatchToProps = (dispatch) => { return { getBannersListDispatch() { dispatch(getBannersList()) }, getRestaurantsListDispatch() { dispatch(getRestaurantsList()) }, } } export default connect(mapStateToProps, mapDispatchToProps)(Home)其中的
connect方法的两个参数:mapStateToProps:状态树,允许组件对store进行读操作的函数。mapDispatchToProps:对store进行写操作的派发函数.
- 还是那首页举例,首先来创建容器组件把这些展示组件和
5. 首页的实现
首页是长这个样子的
5.1. 我们来拆分一下,看看一个完整的单页面由几个组件构成。
├─pages // 页面级别组件
Home // 首页
Banners // 广告组件
CitySelect // 选择城市组件
Search // 搜索组件
Modal // 模态框组件
SetMeal // 轮播图组件
StoreList // 商家排序列表组件
StoreInfo // 全部商家信息组件
哇塞!一个小小的单页面就用了7个组件,集齐七颗龙珠了属于是。
不过这里也体现了React的强大。React 认为渲染逻辑本质上与其他UI逻辑内在耦合,比如,在 UI 中需要绑定处理事件、在某些时刻状态发生变化时需要通知到 UI,以及需要在 UI 中展示准备好的数据。这样的组件可以很好的复用。
5.2.然后我们对城市选择组件分析一下
// 子组件一般不做数据请求 由父组件统一并传参过来
export default function CitySelect(props) {
let { cityName } = props
cityName =='' && window.sessionStorage.getItem("cityName") ? cityName = window.sessionStorage.getItem("cityName") : (window.sessionStorage.cityName = cityName)
return (
<Wrapper>
<Link
className="citygps"
to="/cities">
<i className="icon_city"></i>
<span>{cityName ? cityName : '获取城市坐标'}</span>
<i className=" next"></i>
</Link>
</Wrapper>
)
}
很简单的逻辑,点击获取城市坐标,跳转到Citis页面,选择你所在的位置,然后返回首页。
亮点:在这个对于城市名cityName进行了持久化,保存在了浏览器的sessionStorage中,跳转到其他页面,再返回首页时城市位置不变。
5.3. 再看看搜索框组件
这里是使用了微信的weui里面的搜索组件,添加了一个点击事件,点击跳转到搜索页面
<SearchBar
placeholder="请输入商家或者商品名称"
lang={{ cancel: '取消' }}
className='search'
/>
实现效果如下:
具体的功能还未完善,比如搜索关键词,页面出现相应的热词,点击跳转到商品详情页
5.4.首页的一些小功能
- 通过绑定滑动事件,页面滑动时,搜索框和商家排序组件会固定在顶部,还有滑动到底部出现backTop图标,可以点击它,返回页面顶部
实现效果如下
这是封装了一个工具函数,代码如下
export const isFixed = (str, height) => {
let box = document.querySelector(str)
let fn = function () {
var scrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop;
// console.log(scrollTop);
if (scrollTop > height) {
box.classList.add('fixed')
} else {
box.classList.remove('fixed')
}
}
window.addEventListener('scroll', fn)
}
- 然后就是点击综合排序,出现modal,并且页面会立刻吸顶,然后再次点击遮罩层或者综合排序都可以结束状态
实现效果如下:
6. 商家详情页
商家详情页是这个样子的
6.1. 老规矩还是先给大家看看页面结构
├─HomeDetail // 页面级别组件
HomeBussiness // 商家信息
HomeComment // 评论
HomeOrder // 点单
在这里是使用二级路由,由上文可知,点餐、评价、商家三个先是共用了HomeDetail组件的头部, 然后三个页面通过tap栏切换,跳转到不同的路由。
6.2. 最重要的就是HomeOrder组件,里面有较多的state状态管理
在这里的功能我是参照正版的美团外卖
- 点击
+号,实现商品数量、总价的更新,还有购物车的样式变化,并且在购物车商品数量大于0的时候,才可以点击小人(外卖小哥)弹出购物车详情页,展示你选购的商品信息 部分代码如下:
function HomeOrder(props) {
// 商品数量加减
const changeGoodNum = (e, status, id) => {
e.preventDefault();
e.stopPropagation();
let data = {
status: status,
id: id
}
changeGoodsNumDispatch(data)
}
//小红点数量的变化
const cartNumber = () => {
let num = 0
details.map((item) => {
if (item.name != '热销') {
item.spus.map((ele) => {
num += ele.praise_num
})
}
})
return num
}
//总价的计算
const mapStateToProps = (state) => {
let arr = []
state.goods.GoodsList.forEach((item) => {
if (item.name != '热销') {
item.spus.forEach((item) => {
let price = 0
price += (item.praise_num > 0 ? item.min_price * item.praise_num : 0)
arr.push(price)
})
}
})
return {
price: arr.reduce((pre, curr) => pre += curr, 0)
}
}
-
点击
-号 ,也是实现商品数量、总价的更新,并且在购物车商品数量==0的时候,-号会隐藏,同时还有购物车的样式也会变化部分重要代码如下:
const reducer = (state = defaulState, action) => {
let val = state.GoodsList
let changeList = []
switch (action.type) {
// +-
case actionTypes.CHNAGE_GOODS_NUM:
for (let i = 0; i < val.length; i++) {
changeList.push(val[i].spus)
}
changeList.map((item) => {
item.map((item) => {
if (item.id == action.data.id) {
if (action.data.status == 'add') {
item.praise_num++
state.SingleCart.push(item)
} else {
item.praise_num--
state.SingleCart.pop()
}
}
})
})
return Object.assign({}, state, { GoodsList: [...val] })
default:
return state
}
}
export default reducer
- 购物车详情:在购物车商品数量大于0的时候才会存在,本质是个modal,可以通过点击遮罩层或者那个小人实现隐藏显示;还有清空购物车的功能,点击会清除购物车的商品信息、总价,还有清空商品列表的状态。
//清空购物车
const clearCart = () => {
changeGoodsAllNumDispatch()
}
const reducer = (state = defaulState, action) => {
let val = state.GoodsList
let changeList = []
switch (action.type) {
//清除每个商品的数量
case actionTypes.CHNAGE_GOODSAll_NUM:
for (let i = 0; i < val.length; i++) {
changeList.push(val[i].spus)
}
changeList.map((item) => {
item.map((item) => {
item.praise_num = 0
})
})
state.SingleCart = []
return Object.assign({}, state, { GoodsList: [...val] })
default:
return state
}
}
export default reducer
- 还有就是左侧的sideBar,点击会实现双联动效果,并且对应的样式会改变,可以说是点哪里亮哪里
// 点击 获取右侧商品的id 然后scrollIntoView事件方法滚动到对应位置
const scrollToAnchorLeft = (anchorName) => {
if (anchorName) {
let anchorElement = document.getElementById(anchorName)
anchorElement && anchorElement.scrollIntoView({
block: 'start',
behavior: 'smooth'
})
}
return true
}
补充:
-
在购物车这里,我在
reducer函数中定义了SingleCart作为选购商品的的状态容器,无论+-都是GoodsList(整个商品列表)的状态变化使得SingleCart的状态变化。所以在HomeOrder组件中的商品列表的添加商品、减少商品都会使得购物车组件的数据变化 -
在
+-按钮处定义了点击事件函数,点击会changeGoodNum函数,这就会引起GoodsList状态的变化,通过新旧状态变化将新数据重新渲染到页面上 -
在这里还采用了自顶向下的数据流,即父组件(HomeOrder组件)传递给子组件(ShoppingCart组件)的;
实现效果如下:
6.3.评价和商品详情组件
-
HomeComment组件的亮点在于使用了tap栏切换,并且不同的tap栏展示的内容会经过筛查渲染到页面
-
HomeBussiness组件没有亮点
实现效果如下:
项目优化
1.图片懒加载
引入react-lazyload 实现图片懒加载,
2.组件性能优化memo
如果你的组件在相同
props的情况下渲染相同的结果,那么你可以通过将其包装在React.memo中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React将跳过渲染组件的操作并直接复用最近一次渲染的结果。
给常使用的组件添加此组件,可以减少消耗
const Home = () => {}
export default React.memo(Home)
3. 路由懒加载
React.lazy函数能让你像渲染常规组件一样处理动态引入(的组件)。此代码将会在组件首次渲染时,自动导入包含OtherComponent组件的包。
React.lazy接受一个函数,这个函数需要动态调用import()。它必须返回一个Promise,该 Promise 需要 resolve 一个defaultexport 的 React 组件。然后应在
Suspense组件中渲染 lazy 组件,如此使得我们可以使用在等待加载 lazy 组件时做优雅降级(如 loading 指示器等)。
具体代码上文已经实现,就不再重复了
4. 移动端页面自适应 rem
因为本项目是移动端的美团外卖,为了更好的适配移动端屏幕大小,本项目中采用了rem布局。
- 首先就是要设置
HTML的font-size大小var init = function () { var clientWidth = document.documentElement.clientWidth || document.body.clientWidth; if (clientWidth >= 640) { clientWidth = 640; } var fontSize = 16 / 375 * clientWidth; document.documentElement.style.fontSize = fontSize + "px"; } init(); window.addEventListener("resize", init); ```
如上所示,封装了一个工具函数,在页面大小变化的时候可以自动的计算出HTML的font-size大小,从而达到适配不同屏幕尺寸的手机
5.路径别名
相信大家都用过这样的别名设置
'@/components/common/Scroll'。 这是为了避免一长串的../../../来引入不同目录下的文件,可能会导致文件之间的相对路径的引用错误,我们可以在vite.config.js中添加一下代码。
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve:{
alias:{
"@":path.join(__dirname,'src')
}
},
base:'./'
最后再给大伙推荐一个好用的开发插件
Path Autocomplete:在VSCode扩展中即可下载。
-
- 作用: 在vscode中使用
@提示路劲
- 作用: 在vscode中使用
-
- 配置: 打开vscode设置 搜索
settings.json添加以下代码
// 导入文件时是否携带文件的拓展名 "path-autocomplete.extensionOnImport": true, // 配置@的路径提示 "path-autocomplete.pathMappings": { "@": "${folder}/src" }, - 配置: 打开vscode设置 搜索
- 3.效果如下:
总结
- 整个项目还有许多功能没有实现的,有点遗憾
- 搜索页面的模糊搜索
- 订单页面的数据管理
- 购物车点击结算后,依照正版的美团外卖还有个
Payment页面组件 - 我的页面的注册登录功能
- 项目中还存在一些bug,在未来解决
HomeOrder商品列表页问题:每个商品会有初始数量,所以需要先清空购物车再进行购买商品,这不符合事实逻辑- 首页的页面滚动问题:在点击
综合排序遮罩层出现后,还是可以滑动底下的页面 - 还有就是项目的页面加载时间太长了:在切换路由时,白屏等待时间过长
以上的几点,对于笔者来说是鞭策亦是动力。对于没有完成的功能,在不久的将来,我将逐一实现,并将这个项目发展为全栈项目(笔者在努力学习
node+koa) , 为的就是让大伙在家里也能吃上我做的外卖,哦不,用上我的美团外卖0.0.1(手动狗头)
最后
大伙都看到这里啦,如果对于你来说也有帮助的话,不妨点赞评论加关注一波,这对于我来说真的很重要!!!
还有就是如果有小伙伴在这上面下单成功,记得给我打电话,我会去二道口捞你的(手动狗头)